mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): codemode and MCP
This adds codemode and MCP support to pi. Codemode (packages/codemode) runs model-written JavaScript in a QuickJS wasm VM inside a worker. From there the script calls pi's tools as async functions, uses a per-session store, and can read the model catalog and run classifiers. The codemode tool in coding-agent is compatible with Codex's exec tool, and codemode.mode works like Codex's tool modes (on or only). Large results are written to a JSON temp file. MCP (packages/mcp) is a standalone client for stdio and streamable HTTP servers, with OAuth. The core only gets general mechanisms; codemode, tool_search and MCP are built-in extensions that use them. Other extensions can replace these built-ins. Tools declare an exposure: direct, model-only, codemode, deferred or hidden. prepareLoadout() lets an orchestrating tool change which tool declarations the model sees. ctx.executeTool() runs nested calls through validation and the tool_call/tool_result hooks. Those calls emit events with parentToolCallId and are recorded as bounded nestedCalls on the parent result, which compaction and HTML export use. Tools can also return structuredContent with an outputSchema, and can report errors with isError instead of throwing. MCP servers come from mcp.json (global, or per project after the project is trusted) or from pi.registerMcpServer(). They are managed with /mcp and pi mcp list|login|logout, and their runtimes load on first use. Closes #10040
This commit is contained in:
parent
b485fa3128
commit
8562bcf66a
143 changed files with 17620 additions and 225 deletions
|
|
@ -80,6 +80,7 @@ Automatic retries, recovery, compaction, or queued work can continue afterward.
|
|||
| Persist non-context session data | `pi.appendEntry()` |
|
||||
| Change active tools, model, or thinking level | Session control methods on `pi` |
|
||||
| Add a model provider | `pi.registerProvider()` |
|
||||
| Add an MCP server | `pi.registerMcpServer()` |
|
||||
| Route each request to a model | [`pi.registerVirtualModel()`](virtual-models.md) |
|
||||
| Add terminal rendering | Renderer registration and `ctx.ui` |
|
||||
| Communicate with another extension | `pi.events` |
|
||||
|
|
@ -142,14 +143,61 @@ Use sequential execution when tools share mutable in-memory state.
|
|||
File-mutating tools should wrap the complete read-modify-write operation with `withFileMutationQueue()`.
|
||||
Truncate large model-facing results and tell the model where to read the complete output.
|
||||
|
||||
Declare `outputSchema` and return a matching `structuredContent` when the result is data. The model still receives `content`; programmatic callers such as codemode scripts receive `structuredContent` instead of the text. Tools without `outputSchema` are passed to scripts as their text content. To report a failure that still carries data, return the result with `isError: true` instead of throwing: the model sees an error, and scripts still receive `structuredContent`.
|
||||
|
||||
A tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate })`. Nested calls go through argument validation and the `tool_call` and `tool_result` handlers like model-issued calls, and emit `tool_execution_start`, `tool_execution_update`, and `tool_execution_end`; all of these events carry `parentToolCallId`, and their `toolCallId` is assigned by pi as `<parent id>/<n>`. These ids do not appear as tool calls or tool results in the transcript. Nested calls do not add transcript entries: their results only reach the calling tool, which reports them itself, for example through `onUpdate` and `details`. The session keeps a bounded record of them (name, arguments, status, duration, error; never results) as `nestedCalls` on the calling tool's result message. It is used for compaction file lists and shown in HTML exports. Arguments over 8 KiB per call or 32 KiB per tool result are omitted, at most 256 calls are kept, and `complete: false` marks a record that lost anything. `ctx.tools` lists the tools `ctx.executeTool()` can call. `tool_result` handlers that redact `content` should also replace `structuredContent`; replacing only `content` drops it.
|
||||
|
||||
See [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).
|
||||
|
||||
### Tool exposure
|
||||
|
||||
`exposure` controls how the model reaches a tool. "Callable" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:
|
||||
|
||||
- `direct` (default): declared to the model while active, and callable while active.
|
||||
- `model-only`: declared to the model while active, never callable. Use it for tools that orchestrate other tools or ask the user.
|
||||
- `codemode`: callable whenever registered, and listed by the `codemode` tool. Not declared to the model unless activated explicitly.
|
||||
- `deferred`: like `codemode`, but codemode tools do not list it; `tool_search` can find and activate it.
|
||||
- `hidden`: registered but unreachable. Re-register a tool with `exposure: "hidden"` to withdraw it, since tools cannot be unregistered.
|
||||
|
||||
`namespace: { name, description }` groups related tools, as MCP servers do. Codemode tools list a namespace under one heading.
|
||||
|
||||
Registering a `direct` or `model-only` tool activates it; the other exposures are not activated on registration. The active set (`pi.getActiveTools()`, `pi.setActiveTools()`) is the set of tools declared to the model. `pi.getAllTools()` reports each tool's `exposure`, `namespace`, and `annotations`.
|
||||
|
||||
`annotations` are hints about what a tool does, with the meaning of MCP tool annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. MCP tools carry the hints their server declares. Missing hints take the MCP defaults: a tool is not read-only, and may be destructive and reach an open world. The hints are not verified, but a permission extension can use them to decide which calls to confirm. This confirms the calls Codex asks approval for:
|
||||
|
||||
```typescript
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
const hints = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;
|
||||
const needsApproval =
|
||||
hints?.destructiveHint === true ||
|
||||
(!hints?.readOnlyHint && ((hints?.destructiveHint ?? true) || (hints?.openWorldHint ?? true)));
|
||||
if (needsApproval && !(await ctx.ui.confirm("Allow tool call?", event.toolName))) {
|
||||
return { block: true, reason: `${event.toolName} was not approved` };
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
A tool that orchestrates other tools can adjust what the model sees while it is active with `prepareLoadout(loadout)`. It runs whenever the active tools change and receives the declared tools, the callable tools, and every registered tool with its exposure and namespace. It returns replacement `descriptions` for declared tools (including its own) and `hiddenDeclarations`: active tools whose declarations requests leave out while they stay active and callable. `codemode` and `tool_search` use only this hook, `exposure`, and `ctx.executeTool()`, so another tool can implement the same behavior under a different name.
|
||||
|
||||
### Activate tools dynamically
|
||||
|
||||
Register every tool first, keep optional tools inactive, and use `pi.setActiveTools()` from a loader tool to select the desired active tools. Names must already be registered; unknown names are ignored.
|
||||
|
||||
Pi records the initial prompt and tool set in the transcript's first system message, then appends tool and prompt changes before the next model request. Providers that cannot represent the transition receive a complete transcript checkpoint, which can invalidate the cached prefix.
|
||||
|
||||
### MCP servers
|
||||
|
||||
`pi.registerMcpServer(name, config)` adds an MCP server for the current session. `config` has the shape of an `mcpServers` entry in [`mcp.json`](mcp.md): `command`, `args`, `env`, and `cwd` for stdio servers, `url`, `headers`, and `oauth` for HTTP servers, plus `exposure`, `toolExposure`, `enabled`, and `timeout`.
|
||||
|
||||
```typescript
|
||||
pi.registerMcpServer("jira", { url: "https://mcp.example.com/jira", exposure: "codemode" });
|
||||
pi.unregisterMcpServer("jira");
|
||||
```
|
||||
|
||||
Servers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `pi.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.
|
||||
|
||||
The built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `pi.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.
|
||||
|
||||
<a id="extensioncontext"></a>
|
||||
<a id="extensioncommandcontext"></a>
|
||||
<a id="use-extension-context"></a>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue