Simplify session lifecycle and MCP cleanup
This commit is contained in:
@@ -166,10 +166,10 @@ func toolCatalog() []toolDescriptor {
|
||||
},
|
||||
{
|
||||
Name: "get_process_output",
|
||||
Description: "Read rendered grid (\"grid\") or scrollback (\"scrollback\") output, with screen-version watermark.",
|
||||
Description: "Read rendered grid (\"grid\") or ANSI-stripped stream (\"stream\") output, with screen-version watermark.",
|
||||
InputSchema: objectSchema(map[string]any{
|
||||
"process_id": stringProp("Target process id."),
|
||||
"mode": stringProp("\"grid\" (default) or \"scrollback\"."),
|
||||
"mode": stringProp("\"grid\" (default) or \"stream\"."),
|
||||
"since_offset": integerProp("Watermark offset from a previous call."),
|
||||
}, []string{"process_id"}),
|
||||
},
|
||||
@@ -198,7 +198,7 @@ func toolCatalog() []toolDescriptor {
|
||||
"process_id": stringProp("Target process id."),
|
||||
"pattern": stringProp("Regex pattern."),
|
||||
"timeout_seconds": numberProp("Max time to wait (seconds)."),
|
||||
"scope": stringProp("\"new\" (default) or \"all\"."),
|
||||
"scope": stringProp("\"grid\" (default) or \"scrollback\"."),
|
||||
}, []string{"process_id", "pattern"}),
|
||||
},
|
||||
{
|
||||
@@ -215,7 +215,7 @@ func toolCatalog() []toolDescriptor {
|
||||
"process_id": stringProp("Target process id."),
|
||||
"kind": stringProp("\"text\", \"paste\", or \"key\"."),
|
||||
"text": stringProp("Text payload for kind=text/paste."),
|
||||
"key": stringProp("Named key for kind=key (e.g. \"enter\", \"esc\")."),
|
||||
"key": stringProp("Named key for kind=key (e.g. \"enter\", \"escape\")."),
|
||||
"submit": booleanProp("Whether to append a submit keystroke."),
|
||||
"wait_ms": integerProp("After sending, wait this many ms before tailing."),
|
||||
"tail_mode": stringProp("\"none\" (default), \"stream\", or \"grid\"."),
|
||||
|
||||
@@ -126,3 +126,19 @@ func TestPingReturnsEmptyObject(t *testing.T) {
|
||||
t.Fatal("ping result missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypedInvalidArgsMapToInvalidParams(t *testing.T) {
|
||||
for _, errKind := range []string{ErrorKindInvalidArgs, ErrorKindInvalidKind} {
|
||||
_, code, msg, data := mapToolError(Errorf(errKind, "bad args"))
|
||||
if code != codeInvalidParams {
|
||||
t.Fatalf("%s code = %d, want %d", errKind, code, codeInvalidParams)
|
||||
}
|
||||
if msg != "bad args" {
|
||||
t.Fatalf("%s message = %q", errKind, msg)
|
||||
}
|
||||
kind, ok := data.(map[string]string)
|
||||
if !ok || kind["kind"] != errKind {
|
||||
t.Fatalf("%s data = %#v", errKind, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user