Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/sep-phase4-commands-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@smooai/smooth-extension-sdk": minor
"@smooai/smooth-operator": patch
---

SEP Phase 4 (spec + SDK) — commands, flags, shortcuts, and session actions.

**Spec.** New `command-complete.schema.json` (argument autocomplete). `session.schema.json` now carries the dispatch `context` on every params object (the wire form of the command-tier + epoch guard the host enforces) and adds `send_user_message` (`deliver_as` steer/follow_up/next_turn). `initialize.schema.json` gains a `flags` delivery map on the params and a `shortcuts` list (+ `ShortcutRegistration`) on the registrations. New conformance fixtures for command/complete, session send_user_message/append_entry, shortcuts, and flag delivery; new `$invalid` cases proving `context` is required on a session action and `value` on a completion. The reference `echo.mjs` registers a command + shortcut and answers command/execute + command/complete.

**SDK.** `smooth.registerCommand` (with an optional `complete` completer), `registerFlag` (+ `smooth.getFlag`), and `registerShortcut`. Command handlers receive a `CommandContext` bound to their command-tier context, exposing `session.sendMessage` / `sendUserMessage` / `appendEntry`, `ui`, `hasUI`, and `args`. `createTestHost` gains `runCommand`, `completeCommand`, and a `session/*` service that enforces the same command-tier guard the engine does (event-tier → -32003), recording every session call for assertions. `runConformance` now replays command/execute + command/complete.

**Demo.** `plan-mode` — the flagship extension that exercises phases 2–4 together: a `--plan` flag and a `/plan` command toggle plan mode; a `tool_call` intercept blocks write/edit/apply_patch/bash while it is on; each toggle pushes a `set_widget` render block and persists an LLM-invisible `appendEntry`, so the state survives a hot reload (the flag re-seeds it, the transcript keeps the history).
10 changes: 10 additions & 0 deletions spec/extension/conformance/echo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ rl.on('line', (line) => {
parameters: { type: 'object', properties: { phrase: { type: 'string' } }, required: ['phrase'] },
},
],
commands: [{ name: 'echo-cmd', description: 'Echo a slash-command back.' }],
shortcuts: [{ key: 'ctrl+e', command: 'echo-cmd', description: 'Run echo-cmd' }],
subscriptions: ['turn_start', 'turn_end', 'message_end'],
},
});
Expand All @@ -55,6 +57,14 @@ rl.on('line', (line) => {
reply(id, { content: params?.arguments?.phrase ?? '' });
break;

case 'command/execute':
reply(id, { content: `ran ${params?.command ?? ''}` });
break;

case 'command/complete':
reply(id, { completions: [{ value: `${params?.partial ?? ''}-done`, description: 'echo completion' }] });
break;

case 'shutdown':
reply(id, {});
process.exit(0);
Expand Down
50 changes: 43 additions & 7 deletions spec/extension/conformance/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
"session": { "id": "sess-abc123" },
"mode": "tui",
"ui_capabilities": ["select", "confirm", "input", "notify", "set_status", "set_widget", "set_title"],
"flags": { "plan": true },
"capabilities_enabled": { "tools": true, "commands": true, "ui": true, "exec": true, "kv": true, "bus": true, "session": true }
}
},

"initialize_result": {
"$schema_ref": "methods/initialize.schema.json#/$defs/Result",
"description": "Echo extension's handshake reply: negotiated protocol version, identity, and one registered tool plus one event subscription.",
"description": "An extension's handshake reply: negotiated protocol version, identity, a tool, a slash-command, a declared flag, a keyboard shortcut, and event subscriptions.",
"instance": {
"protocol_version": 1,
"extension": { "name": "echo", "version": "0.1.0" },
"extension": { "name": "plan-mode", "version": "0.1.0" },
"registrations": {
"tools": [
{
Expand All @@ -29,9 +30,10 @@
"parameters": { "type": "object", "properties": { "phrase": { "type": "string" } }, "required": ["phrase"] }
}
],
"commands": [],
"flags": [],
"subscriptions": ["turn_start"]
"commands": [{ "name": "plan", "description": "Toggle plan mode." }],
"flags": ["plan"],
"shortcuts": [{ "key": "ctrl+p", "command": "plan", "description": "Toggle plan mode" }],
"subscriptions": ["turn_start", "session_start", "session_shutdown"]
}
}
},
Expand Down Expand Up @@ -236,8 +238,32 @@

"session_send_message_params": {
"$schema_ref": "methods/session.schema.json#/$defs/SendMessageParams",
"description": "Extension posting an assistant message into the session.",
"instance": { "text": "Done processing.", "role": "assistant" }
"description": "Extension posting an assistant message into the session. Carries the command-tier context so the host can validate the tier + epoch.",
"instance": { "context": { "token": "epoch-7", "tier": "command" }, "text": "Done processing.", "role": "assistant" }
},

"session_send_user_message_params": {
"$schema_ref": "methods/session.schema.json#/$defs/SendUserMessageParams",
"description": "Extension delivering a user message that steers the in-flight turn.",
"instance": { "context": { "token": "epoch-7", "tier": "command" }, "text": "Actually, focus on the tests first.", "deliver_as": "steer" }
},

"session_append_entry_params": {
"$schema_ref": "methods/session.schema.json#/$defs/AppendEntryParams",
"description": "Extension appending an LLM-invisible transcript entry (persisted, not sent to the model).",
"instance": { "context": { "token": "epoch-7", "tier": "command" }, "entry": { "kind": "plan_mode", "enabled": true } }
},

"command_complete_params": {
"$schema_ref": "methods/command-complete.schema.json#/$defs/Params",
"description": "Host asking the owning extension for argument completions on a partial slash-command.",
"instance": { "command": "plan", "context": { "token": "epoch-7", "tier": "command" }, "partial": "on" }
},

"command_complete_result": {
"$schema_ref": "methods/command-complete.schema.json#/$defs/Result",
"description": "Two argument completions offered by the extension.",
"instance": { "completions": [{ "value": "on", "description": "enable plan mode" }, { "value": "off", "description": "disable plan mode" }] }
},

"session_state_result": {
Expand Down Expand Up @@ -487,6 +513,16 @@
"name": "ui_request_confirm_extra_property",
"$schema_ref": "methods/ui-request.schema.json#/$defs/Params",
"instance": { "kind": "confirm", "prompt": "Sure?", "options": ["yes", "no"] }
},
{
"name": "session_send_message_missing_context",
"$schema_ref": "methods/session.schema.json#/$defs/SendMessageParams",
"instance": { "text": "no context — the tier/epoch guard field is required" }
},
{
"name": "command_complete_result_completion_missing_value",
"$schema_ref": "methods/command-complete.schema.json#/$defs/Result",
"instance": { "completions": [{ "description": "missing the required value" }] }
}
]
}
58 changes: 58 additions & 0 deletions spec/extension/methods/command-complete.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smooth-agent.dev/spec/extension/methods/command-complete.schema.json",
"title": "CommandComplete",
"description": "Method `command/complete` (host → ext, request). Asks the extension that owns a registered slash-command for argument completions given the partial text typed so far. Best-effort: an extension that does not implement completion replies with an empty `completions` array. The frontend surfaces the returned values in its autocomplete popup.",

"$defs": {
"Params": {
"title": "CommandCompleteParams",
"type": "object",
"required": ["command", "context"],
"additionalProperties": false,
"properties": {
"command": { "type": "string", "description": "Registered slash-command name, without the leading `/`." },
"context": {
"type": "object",
"required": ["token", "tier"],
"additionalProperties": false,
"properties": {
"token": { "type": "string" },
"tier": { "type": "string", "enum": ["event", "command"] }
}
},
"partial": { "type": "string", "description": "The partial argument text typed after the command name." }
}
},

"Completion": {
"title": "Completion",
"type": "object",
"required": ["value"],
"additionalProperties": false,
"properties": {
"value": { "type": "string", "description": "The completion candidate to insert." },
"description": { "type": "string", "description": "Optional label shown alongside the candidate." }
}
},

"Result": {
"title": "CommandCompleteResult",
"type": "object",
"additionalProperties": false,
"properties": {
"completions": {
"type": "array",
"items": { "$ref": "#/$defs/Completion" },
"description": "Argument completion candidates; empty when the extension offers none."
}
}
}
},

"oneOf": [
{ "$ref": "#/$defs/Params" },
{ "$ref": "#/$defs/Completion" },
{ "$ref": "#/$defs/Result" }
]
}
19 changes: 18 additions & 1 deletion spec/extension/methods/initialize.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
"items": { "type": "string" },
"description": "UI request kinds the frontend can render, e.g. `select`, `confirm`, `input`, `notify`, `set_status`, `set_widget`, `set_title`. Empty ⇒ headless."
},
"flags": {
"type": "object",
"description": "Parsed values for the flags the extension declares (name → value). A host with a CLI surface fills this; hosts without one send it empty or omit it. The extension reads its flag values here at startup."
},
"capabilities_enabled": {
"type": "object",
"description": "Which host capability groups the extension may call. A method in a disabled group is rejected with -32004 CapabilityDisabled.",
Expand Down Expand Up @@ -93,7 +97,8 @@
"properties": {
"tools": { "type": "array", "items": { "$ref": "#/$defs/ToolRegistration" } },
"commands": { "type": "array", "items": { "$ref": "#/$defs/CommandRegistration" } },
"flags": { "type": "array", "items": { "type": "string" }, "description": "CLI/slash flags the extension owns." },
"flags": { "type": "array", "items": { "type": "string" }, "description": "CLI/slash flag names the extension owns; the host delivers their parsed values in `initialize` params `flags`." },
"shortcuts": { "type": "array", "items": { "$ref": "#/$defs/ShortcutRegistration" }, "description": "Keyboard shortcuts binding a chord to a registered command. Only frontends with a key surface (the TUI) honor them." },
"subscriptions": {
"type": "array",
"items": { "type": "string" },
Expand Down Expand Up @@ -124,6 +129,18 @@
"name": { "type": "string", "description": "Slash-command name, without the leading `/`." },
"description": { "type": "string" }
}
},

"ShortcutRegistration": {
"title": "SepShortcutRegistration",
"type": "object",
"required": ["key", "command"],
"additionalProperties": false,
"properties": {
"key": { "type": "string", "description": "Human-typed chord, e.g. `ctrl+p` or `f2`; the frontend parses it." },
"command": { "type": "string", "description": "The registered command name this chord invokes (no leading `/`)." },
"description": { "type": "string" }
}
}
},

Expand Down
54 changes: 48 additions & 6 deletions spec/extension/methods/session.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,27 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://smooth-agent.dev/spec/extension/methods/session.schema.json",
"title": "Session",
"description": "The `session/*` methods (ext → host, requests): `session/send_message` posts a message into the session, `session/append_entry` appends a raw transcript entry, `session/set_model` switches the session's model, and `session/state` reads back session state. All four require the **command** context tier — an extension attempting them from an event-tier context is rejected with -32003 ContextViolation.",
"description": "The `session/*` methods (ext → host, requests): `session/send_message` posts a message into the session, `session/send_user_message` delivers a user message (steer/follow_up/next_turn), `session/append_entry` appends a raw transcript entry (persisted but NOT sent to the model), `session/set_model` switches the session's model, and `session/state` reads back session state. All require the **command** context tier — an extension attempting them from an event-tier context, or with a token minted before a reload bumped the epoch, is rejected with -32003 ContextViolation. Every params object therefore carries the `context` it was dispatched with, so the host can validate the tier and epoch.",

"$defs": {
"Context": {
"title": "Context",
"type": "object",
"required": ["token", "tier"],
"additionalProperties": false,
"properties": {
"token": { "type": "string" },
"tier": { "type": "string", "enum": ["event", "command"] }
}
},

"SendMessageParams": {
"title": "SessionSendMessageParams",
"type": "object",
"required": ["text"],
"required": ["context", "text"],
"additionalProperties": false,
"properties": {
"context": { "$ref": "#/$defs/Context" },
"text": { "type": "string", "description": "Message text to post into the session." },
"role": { "type": "string", "enum": ["user", "assistant"], "default": "assistant", "description": "Who the message is attributed to." }
}
Expand All @@ -22,13 +34,37 @@
"properties": {}
},

"SendUserMessageParams": {
"title": "SessionSendUserMessageParams",
"type": "object",
"required": ["context", "text"],
"additionalProperties": false,
"properties": {
"context": { "$ref": "#/$defs/Context" },
"text": { "type": "string", "description": "User message text to deliver into the session." },
"deliver_as": {
"type": "string",
"enum": ["steer", "follow_up", "next_turn"],
"default": "follow_up",
"description": "When to deliver: `steer` interrupts the in-flight turn, `follow_up` queues after it, `next_turn` starts the next turn."
}
}
},
"SendUserMessageResult": {
"title": "SessionSendUserMessageResult",
"type": "object",
"additionalProperties": false,
"properties": {}
},

"AppendEntryParams": {
"title": "SessionAppendEntryParams",
"type": "object",
"required": ["entry"],
"required": ["context", "entry"],
"additionalProperties": false,
"properties": {
"entry": { "type": "object", "description": "Raw transcript entry to append; shape mirrors the host's own transcript entry format." }
"context": { "$ref": "#/$defs/Context" },
"entry": { "type": "object", "description": "Raw transcript entry to append; persisted but NOT sent to the model. Shape mirrors the host's own transcript entry format." }
}
},
"AppendEntryResult": {
Expand All @@ -41,9 +77,10 @@
"SetModelParams": {
"title": "SessionSetModelParams",
"type": "object",
"required": ["model"],
"required": ["context", "model"],
"additionalProperties": false,
"properties": {
"context": { "$ref": "#/$defs/Context" },
"model": { "type": "string", "description": "Model identifier to switch the session to." }
}
},
Expand All @@ -57,8 +94,11 @@
"StateParams": {
"title": "SessionStateParams",
"type": "object",
"required": ["context"],
"additionalProperties": false,
"properties": {}
"properties": {
"context": { "$ref": "#/$defs/Context" }
}
},
"StateResult": {
"title": "SessionStateResult",
Expand All @@ -73,6 +113,8 @@
"oneOf": [
{ "$ref": "#/$defs/SendMessageParams" },
{ "$ref": "#/$defs/SendMessageResult" },
{ "$ref": "#/$defs/SendUserMessageParams" },
{ "$ref": "#/$defs/SendUserMessageResult" },
{ "$ref": "#/$defs/AppendEntryParams" },
{ "$ref": "#/$defs/AppendEntryResult" },
{ "$ref": "#/$defs/SetModelParams" },
Expand Down
Loading
Loading