--- title: 'Tools and shared state' description: 'Approving a tool call, running a tool in your frontend, and the state an agent shares with the UI.' position: 4 --- # Tools and shared state > A turn can stop for a human to approve a tool call, hand a tool to your > frontend to run, or publish state your UI renders as it changes. ## Tool approvals An agent can pause a turn on a sensitive tool call until a human decides. When a session has a decision pending, the **next** chat call against it returns `409` before doing any work, so you never stream fresh input into a paused thread: ```jsonc { "error": { "message": "This conversation is waiting on a human decision. Resolve it before sending a new message.", "type": "invalid_request_error", "code": "approval_pending", }, } ``` A pause needs a durable session. Send `x-stackbone-session` (see [Session keys](/docs/cli/protocol/auth#session-keys)): a stateless turn has no thread an approval could wake, so the agent runs the tool instead of pausing. The pending tool call itself already streamed to you as a normal `tool_calls` (OpenAI) or `tool_use` (Anthropic) block before the turn stopped. A caller that already inspects tool calls sees nothing unusual until it tries to send the next message. You make the decision out of band, not over this wire: from **Stackbone Studio**, or from the CLI with `stackbone hitl list` and `stackbone hitl approve --yes` / `stackbone hitl reject --yes`. ### Tool approvals over AG-UI AG-UI models the same pause as a first-class **interrupt**, and lets the client resolve it instead of going through Stackbone Studio. A paused run closes with a `RUN_FINISHED` carrying an interrupt outcome instead of a plain success: ```jsonc { "type": "RUN_FINISHED", "threadId": "thread-1", "runId": "run-1", "outcome": { "type": "interrupt", "interrupts": [ { "id": "int_abc", "reason": "tool_call", "toolCallId": "call_1", "description": "Refund order ord_42 for 19.99", "allowedDecisions": ["approve", "reject"], "responseSchema": { "…": "…" }, }, ], }, } ``` `allowedDecisions` tells you which of approve, reject and edit this pause accepts, and `responseSchema` is the JSON Schema of the payload it expects back, so a generic client can render the pause without knowing the tool. A pause that carried no description omits the field. You resolve it by sending the decision back in the **next** request's top-level `resume` array, addressed to the interrupt's `id`: ```jsonc { "threadId": "thread-1", "runId": "run-2", "messages": [], "resume": [{ "interruptId": "int_abc", "status": "resolved", "payload": { "approved": true } }], } ``` Reject a call with `{ "approved": false }`, or edit its arguments before it runs with `{ "editedArgs": { "...": "..." } }`. A `status: "cancelled"` entry always rejects. Sending a fresh message to a thread with an unresolved interrupt still fails, but as a protocol event on the stream (a `RUN_ERROR`) rather than a bare `409`. AG-UI clients watch the event stream, never a response status code. ## Frontend-executed tools `RunAgentInput.tools` declares tools that only make sense in the browser: an API only reachable from the client, reading local state, or opening a modal. When the agent calls one of these, the run finishes **normally** (`outcome: { "type": "success" }`), not as an interrupt: there's nothing to approve, so your frontend runs the tool itself. You send the result back as a trailing `role: "tool"` message on the same `threadId`, and the agent picks up where it left off: ```jsonc { "threadId": "thread-1", "runId": "run-3", "messages": [{ "role": "tool", "toolCallId": "call_2", "content": "{\"opened\":true}" }], } ``` This requires a durable thread, the same as tool approvals: a stateless run (no `threadId`) has nothing to resume against. ## Shared state and generative UI Beyond messages, an agent can publish a **shared state** object for your UI to render live, for example a task list or a work-in-progress draft. A run opens with a `STATE_SNAPSHOT` (the full object) and closes with a `STATE_DELTA` when something changed, an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) JSON Patch you apply with any off-the-shelf patch library: ```jsonc { "type": "STATE_DELTA", "delta": [{ "op": "replace", "path": "/todos/0/done", "value": true }] } ``` You can seed that state from the UI too, by sending `state` on a request. The agent's own code decides which keys a client is allowed to write, with `editableState` on `defineDeepAgent(...)`: ```ts export default defineDeepAgent({ name: 'planner', model: 'openai/gpt-4o-mini', editableState: { draft: { type: 'string' }, }, }); ``` The agent drops any key not listed there from an incoming `state` payload. Like frontend tools, seeding requires a durable thread: there's no graph state to write to on a stateless run. If you reopen an existing thread, the run also opens with a `MESSAGES_SNAPSHOT`, the full prior conversation, so your UI doesn't have to replay history itself. Whenever the agent hands off to a subagent, the run brackets that stretch of work with `STEP_STARTED` / `STEP_FINISHED`, so you can show which phase is running.