feat(agentic): add session handoff command

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 19:57:11 +00:00
parent 81e56c143c
commit b491f68b91
2 changed files with 113 additions and 0 deletions

View file

@ -8,10 +8,55 @@ import (
func (s *PrepSubsystem) registerSessionCommands() {
c := s.Core()
c.Command("session/handoff", core.Command{Description: "Pause a stored session with handoff context", Action: s.cmdSessionHandoff})
c.Command("session/resume", core.Command{Description: "Resume a paused or handed-off session from local cache", Action: s.cmdSessionResume})
c.Command("session/replay", core.Command{Description: "Build replay context for a stored session", Action: s.cmdSessionReplay})
}
// 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")
summary := optionStringValue(options, "summary")
if sessionID == "" {
core.Print(nil, "usage: core-agent session handoff <session-id> --summary=\"Ready for review\" [--next-steps=\"Run the verifier\"] [--blockers=\"Needs input\"]")
return core.Result{Value: core.E("agentic.cmdSessionHandoff", "session_id is required", nil), OK: false}
}
if summary == "" {
core.Print(nil, "usage: core-agent session handoff <session-id> --summary=\"Ready for review\" [--next-steps=\"Run the verifier\"] [--blockers=\"Needs input\"]")
return core.Result{Value: core.E("agentic.cmdSessionHandoff", "summary is required", nil), OK: false}
}
result := s.handleSessionHandoff(s.commandContext(), core.NewOptions(
core.Option{Key: "session_id", Value: sessionID},
core.Option{Key: "summary", Value: summary},
core.Option{Key: "next_steps", Value: optionStringSliceValue(options, "next_steps", "next-steps")},
core.Option{Key: "blockers", Value: optionStringSliceValue(options, "blockers")},
core.Option{Key: "context_for_next", Value: optionAnyMapValue(options, "context_for_next", "context-for-next")},
))
if !result.OK {
err := commandResultError("agentic.cmdSessionHandoff", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(SessionHandoffOutput)
if !ok {
err := core.E("agentic.cmdSessionHandoff", "invalid session handoff output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "session: %s", sessionID)
core.Print(nil, "summary: %s", summary)
if blockers, ok := output.HandoffContext["blockers"].([]string); ok && len(blockers) > 0 {
core.Print(nil, "blockers: %d", len(blockers))
}
if nextSteps, ok := output.HandoffContext["next_steps"].([]string); ok && len(nextSteps) > 0 {
core.Print(nil, "next steps: %d", len(nextSteps))
}
return core.Result{Value: output, OK: true}
}
func (s *PrepSubsystem) cmdSessionResume(options core.Options) core.Result {
sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg")
if sessionID == "" {

View file

@ -17,10 +17,78 @@ func TestCommandsSession_RegisterSessionCommands_Good(t *testing.T) {
s.registerSessionCommands()
assert.Contains(t, c.Commands(), "session/handoff")
assert.Contains(t, c.Commands(), "session/resume")
assert.Contains(t, c.Commands(), "session/replay")
}
func TestCommandsSession_CmdSessionHandoff_Good(t *testing.T) {
dir := t.TempDir()
t.Setenv("CORE_WORKSPACE", dir)
s := newTestPrep(t)
require.NoError(t, writeSessionCache(&Session{
SessionID: "ses-handoff",
AgentType: "codex",
Status: "active",
WorkLog: []map[string]any{
{"type": "checkpoint", "message": "build passed"},
{"type": "decision", "message": "hand off for review"},
},
}))
result := s.cmdSessionHandoff(core.NewOptions(
core.Option{Key: "session_id", Value: "ses-handoff"},
core.Option{Key: "summary", Value: "Ready for review"},
core.Option{Key: "next_steps", Value: []string{"Run the verifier", "Merge if clean"}},
core.Option{Key: "blockers", Value: []string{"Need final approval"}},
core.Option{Key: "context_for_next", Value: map[string]any{"repo": "go-io"}},
))
require.True(t, result.OK)
output, ok := result.Value.(SessionHandoffOutput)
require.True(t, ok)
assert.True(t, output.Success)
assert.Equal(t, "ses-handoff", output.HandoffContext["session_id"])
handoffNotes, ok := output.HandoffContext["handoff_notes"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "Ready for review", handoffNotes["summary"])
cached, err := readSessionCache("ses-handoff")
require.NoError(t, err)
require.NotNil(t, cached)
assert.Equal(t, "paused", cached.Status)
assert.NotEmpty(t, cached.Handoff)
}
func TestCommandsSession_CmdSessionHandoff_Bad_MissingSummary(t *testing.T) {
s := newTestPrep(t)
result := s.cmdSessionHandoff(core.NewOptions(core.Option{Key: "session_id", Value: "ses-handoff"}))
assert.False(t, result.OK)
require.Error(t, result.Value.(error))
assert.Contains(t, result.Value.(error).Error(), "summary is required")
}
func TestCommandsSession_CmdSessionHandoff_Ugly_CorruptedCacheFallsBackToRemoteError(t *testing.T) {
dir := t.TempDir()
t.Setenv("CORE_WORKSPACE", dir)
s := newTestPrep(t)
require.True(t, fs.EnsureDir(sessionCacheRoot()).OK)
require.True(t, fs.WriteAtomic(sessionCachePath("ses-bad"), "{not-json").OK)
result := s.cmdSessionHandoff(core.NewOptions(
core.Option{Key: "session_id", Value: "ses-bad"},
core.Option{Key: "summary", Value: "Ready for review"},
))
assert.False(t, result.OK)
require.Error(t, result.Value.(error))
assert.Contains(t, result.Value.(error).Error(), "no platform API key configured")
}
func TestCommandsSession_CmdSessionResume_Good(t *testing.T) {
dir := t.TempDir()
t.Setenv("CORE_WORKSPACE", dir)