Skip to content

Host hooks & opt-in builtins

Every tool now carries hints = { readOnly, destructive, openWorld } — set on every builtin, read from MCP tool annotations, nil/absent when unknown (treat as mutating). They arrive on the beforeTool event. There is no separate permit option; the gate is beforeTool:

  • allow — return nothing;
  • deny — return an error result (the model sees it, the run continues);
  • ask — return pending(request), which waitFor / waitForCtx resolves.
const agent = createClient({
baseUrl, style: "openai", model, apiKey,
hooks: {
beforeTool: (e) => {
if (e.hints?.readOnly) return // let reads run
return { result: pending({ kind: "input", prompt: `Run ${e.name}?` }) } // ask
},
},
waitFor: async (request, { signal }) => ui.ask(request, signal), // ends with the turn
})

A call through call_tool (see tool tiers) is unwrapped before the hook, so the gate sees the real tool. Hints are advisory and never sent to the model.

Off unless configured; the default ten builtins are unchanged.

builtin what it adds
background (a host-owned BackgroundShells) bash gains run_in_background; bash_output(id) returns new output plus [status: …]; kill_shell(id) stops the process group. Call closeAll on shutdown
webSearch (a provider you supply; none ships) websearch with numbered title / URL / snippet results
notebookEdit notebook_edit: replace, insert or delete an .ipynb cell
McpSource.resourceTools() list_mcp_resources / read_mcp_resource, read-only

Also: grep takes context / before / after, and read / grep report a cut in metadata (truncated, nextOffset / total) while the text stays the same bytes.

JavaScript Python Go Java C# Elixir Clojure
cancellable ask waitFor(req, { signal }) wait_for_ctx WaitForCtx waitForCtx WaitForCtx wait_for_ctx :wait-for-ctx
MCP readiness loadMcp(cfg, { onServer }) load_mcp(cfg, on_server=) LoadMcpWithOptions + OnServer LoadOptions().onServer OnServer Mcp.load(cfg, on_server:) :on-server
MCP resources resourceTools() resource_tools() ResourceTools() resourceTools() ResourceTools() Mcp.resource_tools/1 mcp/resource-tools
hints on the event e.hints hints Hints BeforeToolEvent.hints() Hints hints :hints
background shells background: new BackgroundShells() BackgroundShells BackgroundShells background BackgroundShells Builtin.BackgroundShells builtin/background-shells
compaction notice compactor({ onCompact }) on_compact OnCompact onCompact OnCompact on_compact :on-compact
subagent under gates asToolWith + runtime waitFor as_tool_with AsToolWith asToolWith AsToolWith Agents.as_tool_with/2 as-tool-with
tool-call id ctx.callId call_id ToolContext.CallID callId() CallId call_id :call-id
list models listModels(opts) list_models ListModels LlmClient.listModels LlmClient.ListModelsAsync Client.list_models/1 client/list-models
  • Register mid-turn: a tool added to a running toolkit is offered from the next model call (js, python, go, java, csharp, rust). Elixir and Clojure toolkits are immutable values, so a running turn keeps the toolkit it started with.
  • Cancellation is the turn context in Go, an abort signal in JS, a CancelToken / thread interrupt in Java, the turn’s pid (ctx.signal) in Elixir, and a cooperative :cancel token in Clojure (checked between model calls and tool batches).
  • Background shells are POSIX only in Clojure.
  • ACP host hooks are Go only for now — see ACP host hooks.

Full field tables and runnable examples are on each port’s API pages: hooks, waitFor, MCP load, MCP resources, background shells, builtins, list models (switch language from any page).