feat(session): accept handoff_notes in session end
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
bebf9f8df5
commit
cc552ed9dd
2 changed files with 105 additions and 12 deletions
|
|
@ -56,12 +56,13 @@ type SessionContinueInput struct {
|
|||
Context map[string]any `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
// input := agentic.SessionEndInput{SessionID: "ses_abc123", Status: "completed"}
|
||||
// input := agentic.SessionEndInput{SessionID: "ses_abc123", Status: "completed", HandoffNotes: map[string]any{"summary": "Ready for review"}}
|
||||
type SessionEndInput struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Handoff map[string]any `json:"handoff,omitempty"`
|
||||
SessionID string `json:"session_id"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Handoff map[string]any `json:"handoff,omitempty"`
|
||||
HandoffNotes map[string]any `json:"handoff_notes,omitempty"`
|
||||
}
|
||||
|
||||
// out := agentic.SessionOutput{Success: true, Session: agentic.Session{SessionID: "ses_abc123"}}
|
||||
|
|
@ -206,10 +207,11 @@ func (s *PrepSubsystem) handleSessionContinue(ctx context.Context, options core.
|
|||
// result := c.Action("session.end").Run(ctx, core.NewOptions(core.Option{Key: "session_id", Value: "ses_abc123"}))
|
||||
func (s *PrepSubsystem) handleSessionEnd(ctx context.Context, options core.Options) core.Result {
|
||||
_, output, err := s.sessionEnd(ctx, nil, SessionEndInput{
|
||||
SessionID: optionStringValue(options, "session_id", "id", "_arg"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
Summary: optionStringValue(options, "summary"),
|
||||
Handoff: optionAnyMapValue(options, "handoff"),
|
||||
SessionID: optionStringValue(options, "session_id", "id", "_arg"),
|
||||
Status: optionStringValue(options, "status"),
|
||||
Summary: optionStringValue(options, "summary"),
|
||||
Handoff: optionAnyMapValue(options, "handoff"),
|
||||
HandoffNotes: optionAnyMapValue(options, "handoff_notes", "handoff-notes"),
|
||||
})
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
|
|
@ -455,8 +457,10 @@ func (s *PrepSubsystem) sessionEnd(ctx context.Context, _ *mcp.CallToolRequest,
|
|||
if input.Summary != "" {
|
||||
body["summary"] = input.Summary
|
||||
}
|
||||
if len(input.Handoff) > 0 {
|
||||
body["handoff"] = input.Handoff
|
||||
handoff := mergeSessionHandoff(input.Handoff, input.HandoffNotes)
|
||||
if len(handoff) > 0 {
|
||||
body["handoff"] = handoff
|
||||
body["handoff_notes"] = handoff
|
||||
}
|
||||
|
||||
path := core.Concat("/v1/sessions/", input.SessionID, "/end")
|
||||
|
|
@ -715,7 +719,10 @@ func sessionEndFromInput(session Session, input SessionEndInput) Session {
|
|||
session.Summary = input.Summary
|
||||
}
|
||||
if len(session.Handoff) == 0 && len(input.Handoff) > 0 {
|
||||
session.Handoff = input.Handoff
|
||||
session.Handoff = mergeSessionHandoff(input.Handoff, input.HandoffNotes)
|
||||
}
|
||||
if len(session.Handoff) == 0 && len(input.HandoffNotes) > 0 {
|
||||
session.Handoff = mergeSessionHandoff(input.HandoffNotes, nil)
|
||||
}
|
||||
if session.Status == "completed" || session.Status == "failed" || session.Status == "handed_off" {
|
||||
if session.EndedAt == "" {
|
||||
|
|
@ -725,6 +732,25 @@ func sessionEndFromInput(session Session, input SessionEndInput) Session {
|
|||
return session
|
||||
}
|
||||
|
||||
func mergeSessionHandoff(primary, fallback map[string]any) map[string]any {
|
||||
if len(primary) == 0 && len(fallback) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
merged := map[string]any{}
|
||||
for key, value := range primary {
|
||||
if value != nil {
|
||||
merged[key] = value
|
||||
}
|
||||
}
|
||||
for key, value := range fallback {
|
||||
if value != nil {
|
||||
merged[key] = value
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func sessionCacheRoot() string {
|
||||
return core.JoinPath(CoreRoot(), "sessions")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,6 +212,73 @@ func TestSession_HandleSessionEnd_Good(t *testing.T) {
|
|||
assert.Equal(t, "All green", output.Session.Summary)
|
||||
}
|
||||
|
||||
func TestSession_HandleSessionEnd_Good_HandoffNotes(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/v1/sessions/ses_handoff/end", 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)
|
||||
require.Equal(t, "paused", payload["status"])
|
||||
require.Equal(t, "Ready for review", payload["summary"])
|
||||
|
||||
handoffNotes, ok := payload["handoff_notes"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "Ready for review", handoffNotes["summary"])
|
||||
assert.Equal(t, []any{"Run the verifier"}, handoffNotes["next_steps"])
|
||||
assert.Equal(t, []any{"Needs input"}, handoffNotes["blockers"])
|
||||
|
||||
_, _ = w.Write([]byte(`{"data":{"session_id":"ses_handoff","agent_type":"codex","status":"paused","summary":"Ready for review","handoff_notes":{"summary":"Ready for review","next_steps":["Run the verifier"],"blockers":["Needs input"]}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
|
||||
result := subsystem.handleSessionEnd(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "session_id", Value: "ses_handoff"},
|
||||
core.Option{Key: "status", Value: "paused"},
|
||||
core.Option{Key: "summary", Value: "Ready for review"},
|
||||
core.Option{Key: "handoff_notes", Value: `{"summary":"Ready for review","next_steps":["Run the verifier"],"blockers":["Needs input"]}`},
|
||||
))
|
||||
require.True(t, result.OK)
|
||||
|
||||
output, ok := result.Value.(SessionOutput)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "paused", output.Session.Status)
|
||||
assert.Equal(t, "Ready for review", output.Session.Summary)
|
||||
require.NotNil(t, output.Session.Handoff)
|
||||
assert.Equal(t, "Ready for review", output.Session.Handoff["summary"])
|
||||
assert.Equal(t, []any{"Run the verifier"}, output.Session.Handoff["next_steps"])
|
||||
assert.Equal(t, []any{"Needs input"}, output.Session.Handoff["blockers"])
|
||||
}
|
||||
|
||||
func TestSession_HandleSessionEnd_Bad_MissingSessionID(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
|
||||
result := subsystem.handleSessionEnd(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "status", Value: "completed"},
|
||||
core.Option{Key: "summary", Value: "All green"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
||||
func TestSession_HandleSessionEnd_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.handleSessionEnd(context.Background(), core.NewOptions(
|
||||
core.Option{Key: "session_id", Value: "ses_abc123"},
|
||||
core.Option{Key: "status", Value: "completed"},
|
||||
core.Option{Key: "summary", Value: "All green"},
|
||||
))
|
||||
assert.False(t, result.OK)
|
||||
}
|
||||
|
||||
func TestSession_HandleSessionLog_Good(t *testing.T) {
|
||||
subsystem := testPrepWithPlatformServer(t, nil, "")
|
||||
require.NoError(t, writeSessionCache(&Session{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue