feat(ingestion): implement session_log MCP tool
Appends a JSON line to brainDir/sessions/<session_id>.jsonl using the session package copied in Task 2. Required for upcoming pass-rate logging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
|
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
|
||||||
"github.com/mathiasbq/hyperguild/ingestion/internal/extract"
|
"github.com/mathiasbq/hyperguild/ingestion/internal/extract"
|
||||||
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
|
||||||
"github.com/mathiasbq/hyperguild/ingestion/internal/search"
|
"github.com/mathiasbq/hyperguild/ingestion/internal/search"
|
||||||
|
"github.com/mathiasbq/hyperguild/ingestion/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tools returns the tool descriptors. Handler bodies for each tool are filled
|
// tools returns the tool descriptors. Handler bodies for each tool are filled
|
||||||
@@ -198,6 +200,45 @@ func (s *Server) brainIngest(ctx context.Context, args json.RawMessage) (json.Ra
|
|||||||
return s.runIngest(ctx, a.Content, a.Source, a.DryRun)
|
return s.runIngest(ctx, a.Content, a.Source, a.DryRun)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sessionLogArgs struct {
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
Skill string `json:"skill,omitempty"`
|
||||||
|
Phase string `json:"phase,omitempty"`
|
||||||
|
ProjectRoot string `json:"project_root,omitempty"`
|
||||||
|
FinalStatus string `json:"final_status,omitempty"`
|
||||||
|
FilePath string `json:"file_path,omitempty"`
|
||||||
|
ModelUsed string `json:"model_used,omitempty"`
|
||||||
|
DurationMs int64 `json:"duration_ms,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sessionLog(ctx context.Context, args json.RawMessage) (json.RawMessage, error) {
|
||||||
|
var a sessionLogArgs
|
||||||
|
if err := json.Unmarshal(args, &a); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse args: %w", err)
|
||||||
|
}
|
||||||
|
if a.SessionID == "" {
|
||||||
|
return nil, fmt.Errorf("session_id is required")
|
||||||
|
}
|
||||||
|
entry := session.Entry{
|
||||||
|
SessionID: a.SessionID,
|
||||||
|
Timestamp: time.Now().UTC(),
|
||||||
|
Skill: a.Skill,
|
||||||
|
Phase: a.Phase,
|
||||||
|
ProjectRoot: a.ProjectRoot,
|
||||||
|
FinalStatus: a.FinalStatus,
|
||||||
|
FilePath: a.FilePath,
|
||||||
|
ModelUsed: a.ModelUsed,
|
||||||
|
DurationMs: a.DurationMs,
|
||||||
|
Message: a.Message,
|
||||||
|
}
|
||||||
|
dir := filepath.Join(s.brainDir, "sessions")
|
||||||
|
if err := session.Append(dir, a.SessionID, entry); err != nil {
|
||||||
|
return nil, fmt.Errorf("append: %w", err)
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]string{"status": "ok", "session_id": a.SessionID})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) runIngest(ctx context.Context, content, source string, dryRun bool) (json.RawMessage, error) {
|
func (s *Server) runIngest(ctx context.Context, content, source string, dryRun bool) (json.RawMessage, error) {
|
||||||
result, err := pipeline.Run(ctx, s.pipeline, s.brainDir, content, source, dryRun)
|
result, err := pipeline.Run(ctx, s.pipeline, s.brainDir, content, source, dryRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -170,3 +170,27 @@ func TestBrainIngestRequiresLLMConfigured(t *testing.T) {
|
|||||||
errObj := resp["error"].(map[string]any)
|
errObj := resp["error"].(map[string]any)
|
||||||
assert.Contains(t, errObj["message"].(string), "LLM not configured")
|
assert.Contains(t, errObj["message"].(string), "LLM not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionLogAppends(t *testing.T) {
|
||||||
|
brainDir := t.TempDir()
|
||||||
|
srv := mcp.NewServer(brainDir, nil, nil)
|
||||||
|
|
||||||
|
resp := toolCall(t, srv, "session_log", map[string]any{
|
||||||
|
"session_id": "session-x",
|
||||||
|
"skill": "tdd",
|
||||||
|
"phase": "red",
|
||||||
|
"final_status": "ok",
|
||||||
|
})
|
||||||
|
require.Nil(t, resp["error"])
|
||||||
|
|
||||||
|
got, err := os.ReadFile(filepath.Join(brainDir, "sessions", "session-x.jsonl"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, string(got), `"skill":"tdd"`)
|
||||||
|
assert.Contains(t, string(got), `"phase":"red"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionLogRequiresSessionID(t *testing.T) {
|
||||||
|
srv := mcp.NewServer(t.TempDir(), nil, nil)
|
||||||
|
resp := toolCall(t, srv, "session_log", map[string]any{"skill": "tdd"})
|
||||||
|
require.NotNil(t, resp["error"])
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ func (s *Server) handleCall(ctx context.Context, name string, args json.RawMessa
|
|||||||
return s.brainIngestRaw(ctx, args)
|
return s.brainIngestRaw(ctx, args)
|
||||||
case "brain_ingest":
|
case "brain_ingest":
|
||||||
return s.brainIngest(ctx, args)
|
return s.brainIngest(ctx, args)
|
||||||
|
case "session_log":
|
||||||
|
return s.sessionLog(ctx, args)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown tool: %s", name)
|
return nil, fmt.Errorf("unknown tool: %s", name)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user