From 318cff805de5ddfe1968272792908f870733fca1 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 00:05:51 +0000 Subject: [PATCH] feat(agentic): expose session start and continue commands Co-Authored-By: Virgil --- pkg/agentic/commands_session.go | 79 ++++++++++++++++++ pkg/agentic/commands_session_test.go | 116 +++++++++++++++++++++++++++ pkg/agentic/commands_test.go | 4 + 3 files changed, 199 insertions(+) diff --git a/pkg/agentic/commands_session.go b/pkg/agentic/commands_session.go index eab0743..2ae736a 100644 --- a/pkg/agentic/commands_session.go +++ b/pkg/agentic/commands_session.go @@ -8,6 +8,10 @@ import ( func (s *PrepSubsystem) registerSessionCommands() { c := s.Core() + c.Command("session/start", core.Command{Description: "Start a stored session for a plan", Action: s.cmdSessionStart}) + c.Command("agentic:session/start", core.Command{Description: "Start a stored session for a plan", Action: s.cmdSessionStart}) + c.Command("session/continue", core.Command{Description: "Continue a stored session from saved context", Action: s.cmdSessionContinue}) + c.Command("agentic:session/continue", core.Command{Description: "Continue a stored session from saved context", Action: s.cmdSessionContinue}) c.Command("session/handoff", core.Command{Description: "Pause a stored session with handoff context", Action: s.cmdSessionHandoff}) c.Command("agentic:session/handoff", core.Command{Description: "Pause a stored session with handoff context", Action: s.cmdSessionHandoff}) c.Command("session/end", core.Command{Description: "End a stored session with status, summary, and handoff notes", Action: s.cmdSessionEnd}) @@ -24,6 +28,81 @@ func (s *PrepSubsystem) registerSessionCommands() { c.Command("agentic:session/replay", core.Command{Description: "Build replay context for a stored session", Action: s.cmdSessionReplay}) } +// core-agent session start ax-follow-up --agent-type=codex +func (s *PrepSubsystem) cmdSessionStart(options core.Options) core.Result { + planSlug := optionStringValue(options, "plan_slug", "plan", "_arg") + agentType := optionStringValue(options, "agent_type", "agent") + if planSlug == "" { + core.Print(nil, "usage: core-agent session start --agent-type=codex [--context='{\"repo\":\"go-io\"}']") + return core.Result{Value: core.E("agentic.cmdSessionStart", "plan_slug is required", nil), OK: false} + } + if agentType == "" { + core.Print(nil, "usage: core-agent session start --agent-type=codex [--context='{\"repo\":\"go-io\"}']") + return core.Result{Value: core.E("agentic.cmdSessionStart", "agent_type is required", nil), OK: false} + } + + result := s.handleSessionStart(s.commandContext(), core.NewOptions( + core.Option{Key: "plan_slug", Value: planSlug}, + core.Option{Key: "agent_type", Value: agentType}, + core.Option{Key: "context", Value: optionAnyMapValue(options, "context")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdSessionStart", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(SessionOutput) + if !ok { + err := core.E("agentic.cmdSessionStart", "invalid session start output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "session: %s", output.Session.SessionID) + core.Print(nil, "plan: %s", output.Session.PlanSlug) + core.Print(nil, "agent: %s", output.Session.AgentType) + core.Print(nil, "status: %s", output.Session.Status) + return core.Result{Value: output, OK: true} +} + +// core-agent session continue ses-abc123 --agent-type=codex +func (s *PrepSubsystem) cmdSessionContinue(options core.Options) core.Result { + sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg") + agentType := optionStringValue(options, "agent_type", "agent") + if sessionID == "" { + core.Print(nil, "usage: core-agent session continue [--agent-type=codex] [--work-log='[{\"type\":\"checkpoint\",\"message\":\"...\"}]'] [--context='{\"repo\":\"go-io\"}']") + return core.Result{Value: core.E("agentic.cmdSessionContinue", "session_id is required", nil), OK: false} + } + + result := s.handleSessionContinue(s.commandContext(), core.NewOptions( + core.Option{Key: "session_id", Value: sessionID}, + core.Option{Key: "agent_type", Value: agentType}, + core.Option{Key: "work_log", Value: optionAnyMapSliceValue(options, "work_log")}, + core.Option{Key: "context", Value: optionAnyMapValue(options, "context")}, + )) + if !result.OK { + err := commandResultError("agentic.cmdSessionContinue", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(SessionOutput) + if !ok { + err := core.E("agentic.cmdSessionContinue", "invalid session continue output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "session: %s", output.Session.SessionID) + core.Print(nil, "agent: %s", output.Session.AgentType) + core.Print(nil, "status: %s", output.Session.Status) + if len(output.Session.WorkLog) > 0 { + core.Print(nil, "work log: %d item(s)", len(output.Session.WorkLog)) + } + return core.Result{Value: output, OK: true} +} + // core-agent session handoff ses-abc123 --summary="Ready for review" --next-steps="Run the verifier" func (s *PrepSubsystem) cmdSessionHandoff(options core.Options) core.Result { sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg") diff --git a/pkg/agentic/commands_session_test.go b/pkg/agentic/commands_session_test.go index 218f4b7..c67ac71 100644 --- a/pkg/agentic/commands_session_test.go +++ b/pkg/agentic/commands_session_test.go @@ -21,6 +21,10 @@ func TestCommandsSession_RegisterSessionCommands_Good(t *testing.T) { assert.Contains(t, c.Commands(), "session/handoff") assert.Contains(t, c.Commands(), "agentic:session/handoff") + assert.Contains(t, c.Commands(), "session/start") + assert.Contains(t, c.Commands(), "agentic:session/start") + assert.Contains(t, c.Commands(), "session/continue") + assert.Contains(t, c.Commands(), "agentic:session/continue") assert.Contains(t, c.Commands(), "session/end") assert.Contains(t, c.Commands(), "agentic:session/end") assert.Contains(t, c.Commands(), "session/complete") @@ -35,6 +39,118 @@ func TestCommandsSession_RegisterSessionCommands_Good(t *testing.T) { assert.Contains(t, c.Commands(), "agentic:session/replay") } +func TestCommandsSession_CmdSessionStart_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/sessions", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + + bodyResult := core.ReadAll(r.Body) + require.True(t, bodyResult.OK) + + var payload map[string]any + parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload) + require.True(t, parseResult.OK) + assert.Equal(t, "codex", payload["agent_type"]) + assert.Equal(t, "ax-follow-up", payload["plan_slug"]) + + _, _ = w.Write([]byte(`{"data":{"session_id":"ses-start","plan_slug":"ax-follow-up","agent_type":"codex","status":"active"}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdSessionStart(core.NewOptions( + core.Option{Key: "_arg", Value: "ax-follow-up"}, + core.Option{Key: "agent_type", Value: "codex"}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(SessionOutput) + require.True(t, ok) + assert.Equal(t, "ses-start", output.Session.SessionID) + assert.Equal(t, "ax-follow-up", output.Session.PlanSlug) + assert.Equal(t, "codex", output.Session.AgentType) +} + +func TestCommandsSession_CmdSessionStart_Bad_MissingPlanSlug(t *testing.T) { + subsystem := testPrepWithPlatformServer(t, nil, "secret-token") + + result := subsystem.cmdSessionStart(core.NewOptions(core.Option{Key: "agent_type", Value: "codex"})) + + assert.False(t, result.OK) + require.Error(t, result.Value.(error)) + assert.Contains(t, result.Value.(error).Error(), "plan_slug is required") +} + +func TestCommandsSession_CmdSessionStart_Ugly_InvalidResponse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"data":`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdSessionStart(core.NewOptions( + core.Option{Key: "_arg", Value: "ax-follow-up"}, + core.Option{Key: "agent_type", Value: "codex"}, + )) + assert.False(t, result.OK) +} + +func TestCommandsSession_CmdSessionContinue_Good(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/v1/sessions/ses-continue/continue", r.URL.Path) + require.Equal(t, http.MethodPost, r.Method) + + bodyResult := core.ReadAll(r.Body) + require.True(t, bodyResult.OK) + + var payload map[string]any + parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload) + require.True(t, parseResult.OK) + assert.Equal(t, "codex", payload["agent_type"]) + + _, _ = w.Write([]byte(`{"data":{"session_id":"ses-continue","agent_type":"codex","status":"active","work_log":[{"type":"checkpoint","message":"continue"}]}}`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdSessionContinue(core.NewOptions( + core.Option{Key: "_arg", Value: "ses-continue"}, + core.Option{Key: "agent_type", Value: "codex"}, + core.Option{Key: "work_log", Value: []map[string]any{{"type": "checkpoint", "message": "continue"}}}, + )) + require.True(t, result.OK) + + output, ok := result.Value.(SessionOutput) + require.True(t, ok) + assert.Equal(t, "ses-continue", output.Session.SessionID) + assert.Equal(t, "codex", output.Session.AgentType) + require.Len(t, output.Session.WorkLog, 1) +} + +func TestCommandsSession_CmdSessionContinue_Bad_MissingSessionID(t *testing.T) { + subsystem := testPrepWithPlatformServer(t, nil, "secret-token") + + result := subsystem.cmdSessionContinue(core.NewOptions(core.Option{Key: "agent_type", Value: "codex"})) + + assert.False(t, result.OK) + require.Error(t, result.Value.(error)) + assert.Contains(t, result.Value.(error).Error(), "session_id is required") +} + +func TestCommandsSession_CmdSessionContinue_Ugly_InvalidResponse(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"data":`)) + })) + defer server.Close() + + subsystem := testPrepWithPlatformServer(t, server, "secret-token") + result := subsystem.cmdSessionContinue(core.NewOptions( + core.Option{Key: "_arg", Value: "ses-continue"}, + core.Option{Key: "agent_type", Value: "codex"}, + )) + assert.False(t, result.OK) +} + func TestCommandsSession_CmdSessionHandoff_Good(t *testing.T) { dir := t.TempDir() t.Setenv("CORE_WORKSPACE", dir) diff --git a/pkg/agentic/commands_test.go b/pkg/agentic/commands_test.go index 9026654..a6e2ea1 100644 --- a/pkg/agentic/commands_test.go +++ b/pkg/agentic/commands_test.go @@ -1421,6 +1421,10 @@ func TestCommands_RegisterCommands_Good_AllRegistered(t *testing.T) { assert.Contains(t, cmds, "plan/archive") assert.Contains(t, cmds, "plan/delete") assert.Contains(t, cmds, "agentic:plan-cleanup") + assert.Contains(t, cmds, "session/start") + assert.Contains(t, cmds, "agentic:session/start") + assert.Contains(t, cmds, "session/continue") + assert.Contains(t, cmds, "agentic:session/continue") assert.Contains(t, cmds, "session/end") assert.Contains(t, cmds, "agentic:session/end") assert.Contains(t, cmds, "pr-manage")