Simplify session lifecycle and MCP cleanup

This commit is contained in:
2026-05-14 20:51:37 +01:00
parent 27361f79c4
commit cc4bf9e904
16 changed files with 439 additions and 255 deletions

View File

@@ -14,6 +14,15 @@ import (
// names live in the -32000 range with a structured `data.kind` so the
// caller can branch on the error type rather than parsing strings.
const (
ErrorKindInvalidArgs = "invalid_args"
ErrorKindInvalidKind = "invalid_kind"
ErrorKindNeedsTrust = "needs_trust"
ErrorKindRoleForbidden = "role_forbidden"
ErrorKindNotRelated = "not_related"
ErrorKindNotFound = "not_found"
ErrorKindWrongKind = "wrong_kind"
ErrorKindUnknownAgent = "unknown_agent"
codeParseError = -32700
codeInvalidRequest = -32600
codeMethodNotFound = -32601
@@ -81,7 +90,10 @@ type ToolHost interface {
TimerWait(callerID string, seconds float64, label string) (string, error)
// Scratchpads.
Scratchpads() *scratchpad.Store
ScratchpadList() ([]scratchpad.Entry, error)
ScratchpadRead(name string) (content string, revision string, err error)
ScratchpadWrite(name, content, expectedRevision string) (revision string, err error)
ScratchpadAppend(name, content string) error
// Meta.
WhoAmI(callerID string) WhoAmI
@@ -105,14 +117,14 @@ type ProcessInfo struct {
// ProcessInfo: includes pane geometry, cursor, and active screen.
type ProcessStatus struct {
ProcessInfo
WorkingDir string `json:"working_dir,omitempty"`
WorkingDir string `json:"working_dir,omitempty"`
Argv []string `json:"argv,omitempty"`
StartedAt string `json:"started_at,omitempty"`
ActiveScreen string `json:"active_screen,omitempty"`
Rows int `json:"rows,omitempty"`
Cols int `json:"cols,omitempty"`
Cursor Cursor `json:"cursor"`
ScreenVersion int64 `json:"screen_version,omitempty"`
StartedAt string `json:"started_at,omitempty"`
ActiveScreen string `json:"active_screen,omitempty"`
Rows int `json:"rows,omitempty"`
Cols int `json:"cols,omitempty"`
Cursor Cursor `json:"cursor"`
ScreenVersion int64 `json:"screen_version,omitempty"`
}
// Cursor matches SPEC §7's `{x, y}` payload.
@@ -124,10 +136,10 @@ type Cursor struct {
// ProjectStatus is what get_project_status returns — everything an
// agent needs to orient itself in one call.
type ProjectStatus struct {
Project ProjectMeta `json:"project"`
Caller WhoAmI `json:"caller"`
Processes []ProcessInfo `json:"processes"`
Scratchpads []scratchpad.Entry `json:"scratchpads"`
Project ProjectMeta `json:"project"`
Caller WhoAmI `json:"caller"`
Processes []ProcessInfo `json:"processes"`
Scratchpads []scratchpad.Entry `json:"scratchpads"`
}
// ProjectMeta is the project root info echoed in many payloads.
@@ -178,20 +190,20 @@ type PortSighting struct {
// SpawnAgentArgs is the input shape for spawn_agent.
type SpawnAgentArgs struct {
Agent string `json:"agent"`
AgentInstructions string `json:"agent_instructions"`
Name string `json:"name"`
Agent string `json:"agent"`
AgentInstructions string `json:"agent_instructions"`
Name string `json:"name"`
}
// SpawnProcessArgs is the input shape for spawn_process.
type SpawnProcessArgs struct {
Kind string `json:"kind"` // "terminal" | "command"
Preset string `json:"preset"`
Argv []string `json:"argv"`
Name string `json:"name"`
WorkingDir string `json:"working_dir"`
Kind string `json:"kind"` // "terminal" | "command"
Preset string `json:"preset"`
Argv []string `json:"argv"`
Name string `json:"name"`
WorkingDir string `json:"working_dir"`
Env map[string]string `json:"env"`
Shell bool `json:"shell"`
Shell bool `json:"shell"`
}
// SendInputArgs is the input shape for send_input — covers text /
@@ -214,12 +226,12 @@ type SendInputResult struct {
// WhoAmI is the whoami return shape.
type WhoAmI struct {
ProcessID string `json:"process_id"`
Name string `json:"name"`
Role CallerRole `json:"role"`
ParentProcessID string `json:"parent_process_id,omitempty"`
ProcessID string `json:"process_id"`
Name string `json:"name"`
Role CallerRole `json:"role"`
ParentProcessID string `json:"parent_process_id,omitempty"`
Project ProjectMeta `json:"project"`
AvailableTools []string `json:"available_tools"`
AvailableTools []string `json:"available_tools"`
}
// HelpResponse is the help return shape.
@@ -332,7 +344,9 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return mapToolResult(info, err)
case "start_process":
var p struct{ ProcessID string `json:"process_id"` }
var p struct {
ProcessID string `json:"process_id"`
}
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
@@ -364,7 +378,9 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return mapToolResult(info, err)
case "close_process":
var p struct{ ProcessID string `json:"process_id"` }
var p struct {
ProcessID string `json:"process_id"`
}
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
@@ -387,7 +403,9 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return "ok", 0, "", nil
case "select_process":
var p struct{ ProcessID string `json:"process_id"` }
var p struct {
ProcessID string `json:"process_id"`
}
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
@@ -397,12 +415,16 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return "ok", 0, "", nil
case "list_processes":
var p struct{ Kind string `json:"kind"` }
var p struct {
Kind string `json:"kind"`
}
_ = unmarshalParamsOptional(params, &p)
return h.ListProcesses(callerID, p.Kind), 0, "", nil
case "get_process_status":
var p struct{ ProcessID string `json:"process_id"` }
var p struct {
ProcessID string `json:"process_id"`
}
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
@@ -490,7 +512,9 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return map[string]any{"matched": matched, "snippet": snippet}, 0, "", nil
case "get_process_ports":
var p struct{ ProcessID string `json:"process_id"` }
var p struct {
ProcessID string `json:"process_id"`
}
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
@@ -552,18 +576,20 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return map[string]string{"timer_id": id}, 0, "", nil
case "scratchpad_list":
entries, err := h.Scratchpads().List()
entries, err := h.ScratchpadList()
if err != nil {
return nil, codeInternal, err.Error(), nil
}
return entries, 0, "", nil
case "scratchpad_read":
var p struct{ Name string `json:"name"` }
var p struct {
Name string `json:"name"`
}
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
content, rev, err := h.Scratchpads().Read(p.Name)
content, rev, err := h.ScratchpadRead(p.Name)
if err != nil {
return nil, codeInternal, err.Error(), nil
}
@@ -578,7 +604,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
rev, err := h.Scratchpads().Write(p.Name, p.Content, p.ExpectedRevision)
rev, err := h.ScratchpadWrite(p.Name, p.Content, p.ExpectedRevision)
if err != nil {
// Optimistic-concurrency miss returns ok:false + current_revision
// rather than a JSON-RPC error so callers can re-read + merge.
@@ -598,7 +624,7 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
if err := unmarshalParams(params, &p); err != nil {
return nil, codeInvalidParams, err.Error(), nil
}
if err := h.Scratchpads().Append(p.Name, p.Content); err != nil {
if err := h.ScratchpadAppend(p.Name, p.Content); err != nil {
return nil, codeInternal, err.Error(), nil
}
return map[string]any{"ok": true}, 0, "", nil
@@ -607,7 +633,9 @@ func callTool(h ToolHost, callerID, method string, params json.RawMessage) (any,
return h.WhoAmI(callerID), 0, "", nil
case "help":
var p struct{ Topic string `json:"topic"` }
var p struct {
Topic string `json:"topic"`
}
_ = unmarshalParamsOptional(params, &p)
return h.Help(callerID, p.Topic), 0, "", nil
}
@@ -632,17 +660,19 @@ func mapToolError(err error) (any, int, string, any) {
if errors.As(err, &te) {
code := codeInternal
switch te.Kind {
case "needs_trust":
case ErrorKindInvalidArgs, ErrorKindInvalidKind:
code = codeInvalidParams
case ErrorKindNeedsTrust:
code = codeNeedsTrust
case "role_forbidden":
case ErrorKindRoleForbidden:
code = codeRoleForbidden
case "not_related":
case ErrorKindNotRelated:
code = codeNotRelated
case "not_found":
case ErrorKindNotFound:
code = codeNotFound
case "wrong_kind":
case ErrorKindWrongKind:
code = codeWrongKind
case "unknown_agent":
case ErrorKindUnknownAgent:
code = codeUnknownAgent
}
return nil, code, te.Message, structuredKind(te.Kind)