From 6e5f4c5d37606c5372527447cd137148085936e9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 19:28:56 +0000 Subject: [PATCH] feat(agentic): add session command aliases Co-Authored-By: Virgil --- pkg/agentic/commands.go | 1 + pkg/agentic/commands_session.go | 78 +++++++++++++++++ pkg/agentic/commands_session_test.go | 123 +++++++++++++++++++++++++++ pkg/agentic/prep_test.go | 2 + 4 files changed, 204 insertions(+) create mode 100644 pkg/agentic/commands_session.go create mode 100644 pkg/agentic/commands_session_test.go diff --git a/pkg/agentic/commands.go b/pkg/agentic/commands.go index 709ac35..ca063e1 100644 --- a/pkg/agentic/commands.go +++ b/pkg/agentic/commands.go @@ -42,6 +42,7 @@ func (s *PrepSubsystem) registerCommands(ctx context.Context) { c.Command("prompt", core.Command{Description: "Build and display an agent prompt for a repo", Action: s.cmdPrompt}) c.Command("extract", core.Command{Description: "Extract a workspace template to a directory", Action: s.cmdExtract}) s.registerPlanCommands() + s.registerSessionCommands() s.registerTaskCommands() s.registerLanguageCommands() } diff --git a/pkg/agentic/commands_session.go b/pkg/agentic/commands_session.go new file mode 100644 index 0000000..4432d81 --- /dev/null +++ b/pkg/agentic/commands_session.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + core "dappco.re/go/core" +) + +func (s *PrepSubsystem) registerSessionCommands() { + c := s.Core() + 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}) +} + +func (s *PrepSubsystem) cmdSessionResume(options core.Options) core.Result { + sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg") + if sessionID == "" { + core.Print(nil, "usage: core-agent session resume ") + return core.Result{Value: core.E("agentic.cmdSessionResume", "session_id is required", nil), OK: false} + } + + result := s.handleSessionResume(s.commandContext(), core.NewOptions( + core.Option{Key: "session_id", Value: sessionID}, + )) + if !result.OK { + err := commandResultError("agentic.cmdSessionResume", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(SessionResumeOutput) + if !ok { + err := core.E("agentic.cmdSessionResume", "invalid session resume 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, "status: %s", output.Session.Status) + if len(output.HandoffContext) > 0 { + core.Print(nil, "handoff: %d item(s)", len(output.HandoffContext)) + } + if len(output.RecentActions) > 0 { + core.Print(nil, "recent: %d action(s)", len(output.RecentActions)) + } + if len(output.Artifacts) > 0 { + core.Print(nil, "artifacts: %d", len(output.Artifacts)) + } + return core.Result{Value: output, OK: true} +} + +func (s *PrepSubsystem) cmdSessionReplay(options core.Options) core.Result { + sessionID := optionStringValue(options, "session_id", "session-id", "id", "_arg") + if sessionID == "" { + core.Print(nil, "usage: core-agent session replay ") + return core.Result{Value: core.E("agentic.cmdSessionReplay", "session_id is required", nil), OK: false} + } + + result := s.handleSessionReplay(s.commandContext(), core.NewOptions( + core.Option{Key: "session_id", Value: sessionID}, + )) + if !result.OK { + err := commandResultError("agentic.cmdSessionReplay", result) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + output, ok := result.Value.(SessionReplayOutput) + if !ok { + err := core.E("agentic.cmdSessionReplay", "invalid session replay output", nil) + core.Print(nil, "error: %v", err) + return core.Result{Value: err, OK: false} + } + + core.Print(nil, "session: %s", sessionID) + core.Print(nil, "context items: %d", len(output.ReplayContext)) + return core.Result{Value: output, OK: true} +} diff --git a/pkg/agentic/commands_session_test.go b/pkg/agentic/commands_session_test.go new file mode 100644 index 0000000..99e3189 --- /dev/null +++ b/pkg/agentic/commands_session_test.go @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "testing" + "time" + + core "dappco.re/go/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCommandsSession_RegisterSessionCommands_Good(t *testing.T) { + c := core.New(core.WithOption("name", "test")) + s := &PrepSubsystem{ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{})} + + s.registerSessionCommands() + + assert.Contains(t, c.Commands(), "session/resume") + assert.Contains(t, c.Commands(), "session/replay") +} + +func TestCommandsSession_CmdSessionResume_Good(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + require.NoError(t, writeSessionCache(&Session{ + SessionID: "ses-abc123", + AgentType: "codex", + Status: "paused", + ContextSummary: map[string]any{"repo": "go-io"}, + WorkLog: []map[string]any{ + {"type": "checkpoint", "message": "build passed"}, + {"type": "decision", "message": "open PR"}, + }, + Artifacts: []map[string]any{ + {"path": "pkg/agentic/session.go", "action": "modified"}, + }, + Handoff: map[string]any{ + "summary": "Ready for review", + }, + })) + + result := s.cmdSessionResume(core.NewOptions(core.Option{Key: "session_id", Value: "ses-abc123"})) + require.True(t, result.OK) + + output, ok := result.Value.(SessionResumeOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, "ses-abc123", output.Session.SessionID) + assert.Equal(t, "active", output.Session.Status) + assert.NotEmpty(t, output.HandoffContext) + assert.Len(t, output.RecentActions, 2) + assert.Len(t, output.Artifacts, 1) +} + +func TestCommandsSession_CmdSessionResume_Bad_MissingSessionID(t *testing.T) { + s := newTestPrep(t) + + result := s.cmdSessionResume(core.NewOptions()) + + assert.False(t, result.OK) + require.Error(t, result.Value.(error)) + assert.Contains(t, result.Value.(error).Error(), "session_id is required") +} + +func TestCommandsSession_CmdSessionResume_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.cmdSessionResume(core.NewOptions(core.Option{Key: "session_id", Value: "ses-bad"})) + + 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_CmdSessionReplay_Good(t *testing.T) { + dir := t.TempDir() + t.Setenv("CORE_WORKSPACE", dir) + + s := newTestPrep(t) + require.NoError(t, writeSessionCache(&Session{ + SessionID: "ses-replay", + AgentType: "codex", + Status: "active", + WorkLog: []map[string]any{ + {"type": "checkpoint", "message": "started", "timestamp": time.Now().Format(time.RFC3339)}, + {"type": "decision", "message": "kept scope small", "timestamp": time.Now().Format(time.RFC3339)}, + {"type": "error", "message": "flaky test", "timestamp": time.Now().Format(time.RFC3339)}, + }, + Artifacts: []map[string]any{ + {"path": "pkg/agentic/commands_session.go", "action": "created"}, + }, + })) + + result := s.cmdSessionReplay(core.NewOptions(core.Option{Key: "session_id", Value: "ses-replay"})) + require.True(t, result.OK) + + output, ok := result.Value.(SessionReplayOutput) + require.True(t, ok) + assert.True(t, output.Success) + assert.Equal(t, "ses-replay", output.ReplayContext["session_id"]) + assert.Contains(t, output.ReplayContext, "checkpoints") + assert.Contains(t, output.ReplayContext, "decisions") + assert.Contains(t, output.ReplayContext, "errors") +} + +func TestCommandsSession_CmdSessionReplay_Bad_MissingSessionID(t *testing.T) { + s := newTestPrep(t) + + result := s.cmdSessionReplay(core.NewOptions()) + + assert.False(t, result.OK) + require.Error(t, result.Value.(error)) + assert.Contains(t, result.Value.(error).Error(), "session_id is required") +} diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 6141221..0025df2 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -629,6 +629,8 @@ func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) { assert.Contains(t, c.Commands(), "lang/list") assert.Contains(t, c.Commands(), "plan-cleanup") assert.Contains(t, c.Commands(), "plan/from-issue") + assert.Contains(t, c.Commands(), "session/resume") + assert.Contains(t, c.Commands(), "session/replay") assert.Contains(t, c.Commands(), "review-queue") assert.Contains(t, c.Commands(), "task") assert.Contains(t, c.Commands(), "task/create")