Compare commits
8 Commits
587c0d3b1c
...
7d5289ac54
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d5289ac54 | ||
|
|
3d8fc9dacd | ||
|
|
f9f804cd49 | ||
|
|
85f142ade0 | ||
|
|
0dfad02513 | ||
|
|
c44eb680b2 | ||
|
|
38ada998a2 | ||
|
|
74547c2bdf |
@@ -101,7 +101,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
attempts := make([]iexec.AttemptRecord, 0, len(chain))
|
attempts := make([]iexec.AttemptRecord, 0, len(chain))
|
||||||
orch := iexec.NewOrchestrator(chain, litellmExec.Run, claudeExec.Run, verifier, models.LlamaSwapURL(), &attempts)
|
orch := iexec.NewOrchestrator(chain, litellmExec.Run, claudeExec.Run, verifier, models.LlamaSwapURL(), &attempts)
|
||||||
return orch.Run(ctx, req)
|
result, err := orch.Run(ctx, req)
|
||||||
|
result.Attempts = attempts // attach orchestration metadata before returning
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +166,7 @@ func main() {
|
|||||||
mux.Handle("/mcp", srv)
|
mux.Handle("/mcp", srv)
|
||||||
|
|
||||||
addr := ":" + cfg.Port
|
addr := ":" + cfg.Port
|
||||||
logger.Info("supervisor starting", "addr", addr, "version", "v0.3.1")
|
logger.Info("supervisor starting", "addr", addr, "version", "v0.4.0")
|
||||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||||
logger.Error("server stopped", "err", err)
|
logger.Error("server stopped", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ type Result struct {
|
|||||||
Skill string `json:"skill"` // tdd | review | ...
|
Skill string `json:"skill"` // tdd | review | ...
|
||||||
FilePath string `json:"file_path"` // absolute path to generated file
|
FilePath string `json:"file_path"` // absolute path to generated file
|
||||||
RunnerOutput string `json:"runner_output"` // raw stdout+stderr from test runner
|
RunnerOutput string `json:"runner_output"` // raw stdout+stderr from test runner
|
||||||
Verified bool `json:"verified"` // based on exit code, never self-report
|
Verified bool `json:"verified"` // based on exit code, never self-report
|
||||||
ModelUsed string `json:"model_used"` // model name or "self"
|
ModelUsed string `json:"model_used"` // model name or "self"
|
||||||
Message string `json:"message"` // one sentence summary
|
Message string `json:"message"` // one sentence summary
|
||||||
|
Attempts []AttemptRecord `json:"attempts,omitempty"` // populated by orchestrator, not Claude
|
||||||
}
|
}
|
||||||
|
|
||||||
var validStatuses = map[string]bool{"pass": true, "fail": true, "error": true}
|
var validStatuses = map[string]bool{"pass": true, "fail": true, "error": true}
|
||||||
|
|||||||
26
internal/session/attempts.go
Normal file
26
internal/session/attempts.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// internal/session/attempts.go
|
||||||
|
package session
|
||||||
|
|
||||||
|
import iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
|
|
||||||
|
// AttemptsFrom converts exec.AttemptRecord slice to session.Attempt slice
|
||||||
|
// for writing into a session JSONL entry.
|
||||||
|
func AttemptsFrom(records []iexec.AttemptRecord) []Attempt {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]Attempt, len(records))
|
||||||
|
for i, r := range records {
|
||||||
|
out[i] = Attempt{
|
||||||
|
Attempt: i + 1,
|
||||||
|
Model: r.Model,
|
||||||
|
Tier: r.Tier,
|
||||||
|
DurationMs: r.DurationMs,
|
||||||
|
WarmStart: r.WarmStart,
|
||||||
|
Verdict: r.Verdict,
|
||||||
|
Feedback: r.Feedback,
|
||||||
|
Verified: r.Verdict == "accept",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
37
internal/session/attempts_test.go
Normal file
37
internal/session/attempts_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package session_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mathiasbq/supervisor/internal/exec"
|
||||||
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAttemptsFromEmpty(t *testing.T) {
|
||||||
|
result := session.AttemptsFrom(nil)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAttemptsFromSetsIndex(t *testing.T) {
|
||||||
|
records := []exec.AttemptRecord{
|
||||||
|
{Model: "ollama/phi4", Tier: "local", DurationMs: 1200, WarmStart: true, Verdict: "escalate", Feedback: "too vague"},
|
||||||
|
{Model: "claude-sonnet-4-6", Tier: "subagent", DurationMs: 3400, WarmStart: false, Verdict: "accept"},
|
||||||
|
}
|
||||||
|
result := session.AttemptsFrom(records)
|
||||||
|
require.Len(t, result, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, result[0].Attempt)
|
||||||
|
assert.Equal(t, "ollama/phi4", result[0].Model)
|
||||||
|
assert.Equal(t, "local", result[0].Tier)
|
||||||
|
assert.Equal(t, int64(1200), result[0].DurationMs)
|
||||||
|
assert.True(t, result[0].WarmStart)
|
||||||
|
assert.Equal(t, "escalate", result[0].Verdict)
|
||||||
|
assert.Equal(t, "too vague", result[0].Feedback)
|
||||||
|
assert.False(t, result[0].Verified)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, result[1].Attempt)
|
||||||
|
assert.Equal(t, "claude-sonnet-4-6", result[1].Model)
|
||||||
|
assert.True(t, result[1].Verified)
|
||||||
|
}
|
||||||
@@ -36,3 +36,21 @@ func FormatHistory(entries []Entry, excludePhase string) string {
|
|||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrependHistory reads the session log for sessionID and prepends a formatted
|
||||||
|
// history block to task. Returns task unchanged if sessionID or sessionsDir is
|
||||||
|
// empty, or if no prior entries exist.
|
||||||
|
func PrependHistory(sessionsDir, sessionID, currentPhase, task string) string {
|
||||||
|
if sessionID == "" || sessionsDir == "" {
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
entries, err := Read(sessionsDir, sessionID)
|
||||||
|
if err != nil || len(entries) == 0 {
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
history := FormatHistory(entries, currentPhase)
|
||||||
|
if history == "" {
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
return history + "\n---\n\n" + task
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
package session_test
|
package session_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFormatHistoryEmpty(t *testing.T) {
|
func TestFormatHistoryEmpty(t *testing.T) {
|
||||||
@@ -39,3 +41,45 @@ func TestFormatHistoryExcludesCurrentPhase(t *testing.T) {
|
|||||||
assert.Contains(t, result, "red done")
|
assert.Contains(t, result, "red done")
|
||||||
assert.NotContains(t, result, "green done")
|
assert.NotContains(t, result, "green done")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrependHistoryNoSessionID(t *testing.T) {
|
||||||
|
result := session.PrependHistory("", "", "review", "do the task")
|
||||||
|
assert.Equal(t, "do the task", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrependHistoryNoLog(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
result := session.PrependHistory(dir, "sess-abc", "review", "do the task")
|
||||||
|
assert.Equal(t, "do the task", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrependHistoryPrependsHistory(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
entry := session.Entry{
|
||||||
|
SessionID: "sess-abc", Skill: "tdd", Phase: "red",
|
||||||
|
FinalStatus: "pass", Message: "wrote test",
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
require.NoError(t, session.Append(dir, "sess-abc", entry))
|
||||||
|
|
||||||
|
result := session.PrependHistory(dir, "sess-abc", "review", "do the task")
|
||||||
|
assert.Contains(t, result, "## Session history")
|
||||||
|
assert.Contains(t, result, "wrote test")
|
||||||
|
assert.True(t, strings.HasSuffix(result, "do the task"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrependHistoryExcludesCurrentPhase(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
require.NoError(t, session.Append(dir, "sess-abc", session.Entry{
|
||||||
|
SessionID: "sess-abc", Skill: "tdd", Phase: "red",
|
||||||
|
FinalStatus: "pass", Message: "red done", Timestamp: time.Now(),
|
||||||
|
}))
|
||||||
|
require.NoError(t, session.Append(dir, "sess-abc", session.Entry{
|
||||||
|
SessionID: "sess-abc", Skill: "tdd", Phase: "green",
|
||||||
|
FinalStatus: "pass", Message: "green done", Timestamp: time.Now(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
result := session.PrependHistory(dir, "sess-abc", "green", "do the task")
|
||||||
|
assert.Contains(t, result, "red done")
|
||||||
|
assert.NotContains(t, result, "green done")
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
@@ -43,11 +44,12 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
"phase: debug\nproject_root: %s\nerror: %s\ncontext: %s\nmodel: %s",
|
"phase: debug\nproject_root: %s\nerror: %s\ncontext: %s\nmodel: %s",
|
||||||
a.ProjectRoot, a.Error, a.Context, model,
|
a.ProjectRoot, a.Error, a.Context, model,
|
||||||
)
|
)
|
||||||
task = s.prependHistory(a.SessionID, "debug", task)
|
task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "debug", task)
|
||||||
|
|
||||||
if s.cfg.ExecutorFn == nil {
|
if s.cfg.ExecutorFn == nil {
|
||||||
return nil, fmt.Errorf("no executor configured")
|
return nil, fmt.Errorf("no executor configured")
|
||||||
}
|
}
|
||||||
|
t0 := time.Now()
|
||||||
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
||||||
SkillPrompt: s.cfg.SkillPrompt,
|
SkillPrompt: s.cfg.SkillPrompt,
|
||||||
TaskPrompt: task,
|
TaskPrompt: task,
|
||||||
@@ -57,24 +59,25 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.SessionID != "" && s.cfg.SessionsDir != "" {
|
||||||
|
_ = session.Append(s.cfg.SessionsDir, a.SessionID, session.Entry{
|
||||||
|
SessionID: a.SessionID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Skill: "debug",
|
||||||
|
Phase: "debug",
|
||||||
|
ProjectRoot: a.ProjectRoot,
|
||||||
|
Attempts: session.AttemptsFrom(result.Attempts),
|
||||||
|
FinalStatus: result.Status,
|
||||||
|
ModelUsed: result.ModelUsed,
|
||||||
|
DurationMs: time.Since(t0).Milliseconds(),
|
||||||
|
Message: result.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(result)
|
b, err := json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("marshal result: %w", err)
|
return nil, fmt.Errorf("marshal result: %w", err)
|
||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Skill) prependHistory(sessionID, currentPhase, task string) string {
|
|
||||||
if sessionID == "" || s.cfg.SessionsDir == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
entries, err := session.Read(s.cfg.SessionsDir, sessionID)
|
|
||||||
if err != nil || len(entries) == 0 {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
history := session.FormatHistory(entries, currentPhase)
|
|
||||||
if history == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
return history + "\n---\n\n" + task
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
@@ -52,6 +53,7 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
if s.cfg.ExecutorFn == nil {
|
if s.cfg.ExecutorFn == nil {
|
||||||
return nil, fmt.Errorf("no executor configured")
|
return nil, fmt.Errorf("no executor configured")
|
||||||
}
|
}
|
||||||
|
t0 := time.Now()
|
||||||
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
||||||
SkillPrompt: s.cfg.SkillPrompt,
|
SkillPrompt: s.cfg.SkillPrompt,
|
||||||
TaskPrompt: taskPrompt,
|
TaskPrompt: taskPrompt,
|
||||||
@@ -62,6 +64,18 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
return nil, fmt.Errorf("retrospective worker: %w", err)
|
return nil, fmt.Errorf("retrospective worker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = session.Append(s.cfg.SessionsDir, a.SessionID, session.Entry{
|
||||||
|
SessionID: a.SessionID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Skill: "retrospective",
|
||||||
|
Phase: "retrospective",
|
||||||
|
Attempts: session.AttemptsFrom(result.Attempts),
|
||||||
|
FinalStatus: result.Status,
|
||||||
|
ModelUsed: result.ModelUsed,
|
||||||
|
DurationMs: time.Since(t0).Milliseconds(),
|
||||||
|
Message: result.Message,
|
||||||
|
})
|
||||||
|
|
||||||
b, err := json.Marshal(result)
|
b, err := json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("marshal result: %w", err)
|
return nil, fmt.Errorf("marshal result: %w", err)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
@@ -44,11 +45,12 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
"phase: review\nproject_root: %s\nfiles: %s\ncontext: %s\nmodel: %s",
|
"phase: review\nproject_root: %s\nfiles: %s\ncontext: %s\nmodel: %s",
|
||||||
a.ProjectRoot, strings.Join(a.Files, ", "), a.Context, model,
|
a.ProjectRoot, strings.Join(a.Files, ", "), a.Context, model,
|
||||||
)
|
)
|
||||||
task = s.prependHistory(a.SessionID, "review", task)
|
task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "review", task)
|
||||||
|
|
||||||
if s.cfg.ExecutorFn == nil {
|
if s.cfg.ExecutorFn == nil {
|
||||||
return nil, fmt.Errorf("no executor configured")
|
return nil, fmt.Errorf("no executor configured")
|
||||||
}
|
}
|
||||||
|
t0 := time.Now()
|
||||||
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
||||||
SkillPrompt: s.cfg.SkillPrompt,
|
SkillPrompt: s.cfg.SkillPrompt,
|
||||||
TaskPrompt: task,
|
TaskPrompt: task,
|
||||||
@@ -58,24 +60,26 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.SessionID != "" && s.cfg.SessionsDir != "" {
|
||||||
|
_ = session.Append(s.cfg.SessionsDir, a.SessionID, session.Entry{
|
||||||
|
SessionID: a.SessionID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Skill: "review",
|
||||||
|
Phase: "review",
|
||||||
|
ProjectRoot: a.ProjectRoot,
|
||||||
|
Attempts: session.AttemptsFrom(result.Attempts),
|
||||||
|
FinalStatus: result.Status,
|
||||||
|
FilePath: result.FilePath,
|
||||||
|
ModelUsed: result.ModelUsed,
|
||||||
|
DurationMs: time.Since(t0).Milliseconds(),
|
||||||
|
Message: result.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(result)
|
b, err := json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("marshal result: %w", err)
|
return nil, fmt.Errorf("marshal result: %w", err)
|
||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Skill) prependHistory(sessionID, currentPhase, task string) string {
|
|
||||||
if sessionID == "" || s.cfg.SessionsDir == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
entries, err := session.Read(s.cfg.SessionsDir, sessionID)
|
|
||||||
if err != nil || len(entries) == 0 {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
history := session.FormatHistory(entries, currentPhase)
|
|
||||||
if history == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
return history + "\n---\n\n" + task
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
@@ -48,11 +49,12 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
"phase: spec\nproject_root: %s\nrequirements: %s\noutput_path: %s\ncontext: %s\nmodel: %s",
|
"phase: spec\nproject_root: %s\nrequirements: %s\noutput_path: %s\ncontext: %s\nmodel: %s",
|
||||||
a.ProjectRoot, a.Requirements, outputPath, a.Context, model,
|
a.ProjectRoot, a.Requirements, outputPath, a.Context, model,
|
||||||
)
|
)
|
||||||
task = s.prependHistory(a.SessionID, "spec", task)
|
task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "spec", task)
|
||||||
|
|
||||||
if s.cfg.ExecutorFn == nil {
|
if s.cfg.ExecutorFn == nil {
|
||||||
return nil, fmt.Errorf("no executor configured")
|
return nil, fmt.Errorf("no executor configured")
|
||||||
}
|
}
|
||||||
|
t0 := time.Now()
|
||||||
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
result, err := s.cfg.ExecutorFn(ctx, iexec.Request{
|
||||||
SkillPrompt: s.cfg.SkillPrompt,
|
SkillPrompt: s.cfg.SkillPrompt,
|
||||||
TaskPrompt: task,
|
TaskPrompt: task,
|
||||||
@@ -62,24 +64,26 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.SessionID != "" && s.cfg.SessionsDir != "" {
|
||||||
|
_ = session.Append(s.cfg.SessionsDir, a.SessionID, session.Entry{
|
||||||
|
SessionID: a.SessionID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Skill: "spec",
|
||||||
|
Phase: "spec",
|
||||||
|
ProjectRoot: a.ProjectRoot,
|
||||||
|
Attempts: session.AttemptsFrom(result.Attempts),
|
||||||
|
FinalStatus: result.Status,
|
||||||
|
FilePath: result.FilePath,
|
||||||
|
ModelUsed: result.ModelUsed,
|
||||||
|
DurationMs: time.Since(t0).Milliseconds(),
|
||||||
|
Message: result.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
b, err := json.Marshal(result)
|
b, err := json.Marshal(result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("marshal result: %w", err)
|
return nil, fmt.Errorf("marshal result: %w", err)
|
||||||
}
|
}
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Skill) prependHistory(sessionID, currentPhase, task string) string {
|
|
||||||
if sessionID == "" || s.cfg.SessionsDir == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
entries, err := session.Read(s.cfg.SessionsDir, sessionID)
|
|
||||||
if err != nil || len(entries) == 0 {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
history := session.FormatHistory(entries, currentPhase)
|
|
||||||
if history == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
return history + "\n---\n\n" + task
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
@@ -70,8 +71,15 @@ func (s *Skill) handleGreen(ctx context.Context, raw json.RawMessage) (json.RawM
|
|||||||
"phase: green\nproject_root: %s\ntest_path: %s\nmodel: %s\ntest_cmd: %s",
|
"phase: green\nproject_root: %s\ntest_path: %s\nmodel: %s\ntest_cmd: %s",
|
||||||
args.ProjectRoot, args.TestPath, s.resolveModel(args.Model), args.TestCmd,
|
args.ProjectRoot, args.TestPath, s.resolveModel(args.Model), args.TestCmd,
|
||||||
)
|
)
|
||||||
task = s.prependHistory(args.SessionID, "green", task)
|
task = session.PrependHistory(s.cfg.SessionsDir, args.SessionID, "green", task)
|
||||||
return s.execute(ctx, task)
|
|
||||||
|
t0 := time.Now()
|
||||||
|
result, err := s.execute(ctx, task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.logAttempt(args.SessionID, args.ProjectRoot, "tdd", "green", t0, result)
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type refactorArgs struct {
|
type refactorArgs struct {
|
||||||
@@ -101,23 +109,15 @@ func (s *Skill) handleRefactor(ctx context.Context, raw json.RawMessage) (json.R
|
|||||||
"phase: refactor\nproject_root: %s\ntest_path: %s\nimpl_path: %s\nmodel: %s\ntest_cmd: %s",
|
"phase: refactor\nproject_root: %s\ntest_path: %s\nimpl_path: %s\nmodel: %s\ntest_cmd: %s",
|
||||||
args.ProjectRoot, args.TestPath, args.ImplPath, s.resolveModel(args.Model), args.TestCmd,
|
args.ProjectRoot, args.TestPath, args.ImplPath, s.resolveModel(args.Model), args.TestCmd,
|
||||||
)
|
)
|
||||||
task = s.prependHistory(args.SessionID, "refactor", task)
|
task = session.PrependHistory(s.cfg.SessionsDir, args.SessionID, "refactor", task)
|
||||||
return s.execute(ctx, task)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Skill) prependHistory(sessionID, currentPhase, task string) string {
|
t0 := time.Now()
|
||||||
if sessionID == "" || s.cfg.SessionsDir == "" {
|
result, err := s.execute(ctx, task)
|
||||||
return task
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
entries, err := session.Read(s.cfg.SessionsDir, sessionID)
|
s.logAttempt(args.SessionID, args.ProjectRoot, "tdd", "refactor", t0, result)
|
||||||
if err != nil || len(entries) == 0 {
|
return result, nil
|
||||||
return task
|
|
||||||
}
|
|
||||||
history := session.FormatHistory(entries, currentPhase)
|
|
||||||
if history == "" {
|
|
||||||
return task
|
|
||||||
}
|
|
||||||
return history + "\n---\n\n" + task
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Skill) resolveModel(override string) string {
|
func (s *Skill) resolveModel(override string) string {
|
||||||
@@ -127,6 +127,7 @@ func (s *Skill) resolveModel(override string) string {
|
|||||||
return s.cfg.DefaultModel
|
return s.cfg.DefaultModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute calls ExecutorFn and returns the marshaled result.
|
||||||
func (s *Skill) execute(ctx context.Context, task string) (json.RawMessage, error) {
|
func (s *Skill) execute(ctx context.Context, task string) (json.RawMessage, error) {
|
||||||
if s.cfg.ExecutorFn == nil {
|
if s.cfg.ExecutorFn == nil {
|
||||||
return nil, fmt.Errorf("no executor configured")
|
return nil, fmt.Errorf("no executor configured")
|
||||||
@@ -141,3 +142,28 @@ func (s *Skill) execute(ctx context.Context, task string) (json.RawMessage, erro
|
|||||||
}
|
}
|
||||||
return json.Marshal(result)
|
return json.Marshal(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logAttempt writes a session.Entry for a completed phase if session_id is set.
|
||||||
|
// raw is the marshaled Result returned by execute; we unmarshal to extract fields.
|
||||||
|
func (s *Skill) logAttempt(sessionID, projectRoot, skill, phase string, t0 time.Time, raw json.RawMessage) {
|
||||||
|
if sessionID == "" || s.cfg.SessionsDir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result iexec.Result
|
||||||
|
if err := json.Unmarshal(raw, &result); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = session.Append(s.cfg.SessionsDir, sessionID, session.Entry{
|
||||||
|
SessionID: sessionID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Skill: skill,
|
||||||
|
Phase: phase,
|
||||||
|
ProjectRoot: projectRoot,
|
||||||
|
Attempts: session.AttemptsFrom(result.Attempts),
|
||||||
|
FinalStatus: result.Status,
|
||||||
|
FilePath: result.FilePath,
|
||||||
|
ModelUsed: result.ModelUsed,
|
||||||
|
DurationMs: time.Since(t0).Milliseconds(),
|
||||||
|
Message: result.Message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
iexec "github.com/mathiasbq/supervisor/internal/exec"
|
||||||
"github.com/mathiasbq/supervisor/internal/session"
|
"github.com/mathiasbq/supervisor/internal/session"
|
||||||
@@ -58,6 +59,7 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 2: Writer agent (receives reader candidates) ────────────────────
|
// ── Step 2: Writer agent (receives reader candidates) ────────────────────
|
||||||
|
t0 := time.Now()
|
||||||
writerTask := fmt.Sprintf(
|
writerTask := fmt.Sprintf(
|
||||||
"role: writer\nsession_id: %s\nbrain_dir: %s\n\nreader_summary: %s\nreader_candidates:\n%s",
|
"role: writer\nsession_id: %s\nbrain_dir: %s\n\nreader_summary: %s\nreader_candidates:\n%s",
|
||||||
a.SessionID, s.cfg.BrainDir, readerResult.Message, readerResult.RunnerOutput,
|
a.SessionID, s.cfg.BrainDir, readerResult.Message, readerResult.RunnerOutput,
|
||||||
@@ -72,6 +74,18 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
|
|||||||
return nil, fmt.Errorf("writer agent: %w", err)
|
return nil, fmt.Errorf("writer agent: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = session.Append(s.cfg.SessionsDir, a.SessionID, session.Entry{
|
||||||
|
SessionID: a.SessionID,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Skill: "trainer",
|
||||||
|
Phase: "trainer",
|
||||||
|
Attempts: session.AttemptsFrom(writerResult.Attempts),
|
||||||
|
FinalStatus: writerResult.Status,
|
||||||
|
ModelUsed: writerResult.ModelUsed,
|
||||||
|
DurationMs: time.Since(t0).Milliseconds(),
|
||||||
|
Message: writerResult.Message,
|
||||||
|
})
|
||||||
|
|
||||||
b, err := json.Marshal(writerResult)
|
b, err := json.Marshal(writerResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("marshal result: %w", err)
|
return nil, fmt.Errorf("marshal result: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user