Zum Inhalt springen

DOCS

Tool confirmation

Sensitive tools pause the turn until the chat user taps Confirm or Deny. Opt-in only — nothing runs with confirmation unless the host or client tool declares it.

Tool confirmation

Opt-in gate before destructive or sensitive host and frontend tools run. The chat user approves or denies in the UI; the runtime executes only after approve, using arguments from the stored turn checkpoint.

What it does

Some tools should not run the moment the model asks for them — for example deleting a row, charging a card, or sending mail. With confirmation enabled, the agent loop stops after emitting tool.call and waits for an explicit chat decision. The host MCP handler or frontend handler runs only after approve. On deny the tool is not called; the model receives a structured failure instead.

  • Opt-in. Tools without the flag stay automatic. Existing create_post / update_post behavior is unchanged.
  • Not the same as mode. mode: delete on a host tool is a policy hint ( policies ). It does not turn on chat confirmation by itself.
  • Not renderHint. renderHint: confirm_dialog on tool.result is unused for this flow. The decision happens before execution, on tool.call.
  • Client cannot disable host confirmation. The flag comes from MCP _meta or from the tool catalog the server already listed — not from a field the browser adds to the message body.

Declaring confirmation

Two channels share one runtime rule: publish confirmation: "required" only when you want the chat gate. Omit the key or use auto for automatic execution.

Host MCP tools

Host SDKs map an optional confirmation() / confirmation field on the tool class or object into MCP tools/list metadata. Custom MCP servers (not using our SDK) should set the same shape on _meta.

{
  "name": "delete_post",
  "description": "…",
  "_meta": {
    "domain": "posts",
    "mode": "delete",
    "confirmation": "required"
  }
}
export const deletePostTool = {
  name: 'delete_post',
  description: 'Delete a post. The chat user must confirm before it runs.',
  mode: MODE_DELETE,
  domain: 'posts',
  confirmation: 'required',
  schema() {
    return { post_id: { type: 'integer', required: true } }
  },
  handle(args) { … },
}
class DeletePostTool implements HostTool
{
    public function confirmation(): string
    {
        return 'required';
    }

    public function mode(): string
    {
        return self::MODE_DELETE;
    }
    // name, schema, handle …
}

Other host packages follow the same contract: Java HostTool.confirmation(), .NET IHostTool.Confirmation, Python confirmation on the tool class, Ruby confirmation, Go ConfirmingTool. See each host SDK guide.

Frontend (client) tools

Register on SvedaToolRegistry with confirmation: 'required'. The registry sends it on every stream body as clientTools (only when required — auto tools omit the field). Details: MCP and tools.

tools.register({
  name: 'archive_selection',
  description: 'Archive the current UI selection',
  confirmation: 'required',
  parameters: { type: 'object', properties: {} },
  handler: async (input, { chatId, toolCallId }) => ({ … }),
})

Stream protocol

When the model selects a confirming tool, the runtime emits tool.call with optional confirmation: "required" alongside toolCallId, toolName, input, and target ( backend or frontend ). It does not call the host or the browser handler yet.

data: {"type":"tool.call","toolCallId":"call_del_1","toolName":"delete_post","target":"backend","input":{"post_id":1},"confirmation":"required"}

The turn ends with message.end and finishReason consistent with other tool pauses. Other tools in the same model step still run if they do not require confirmation; confirming calls wait until every pending decision has a tool.result.

Resuming with toolDecisions

The client sends a follow-up POST /sveda/stream with an empty prompt, the current messages (including the assistant message that owns the tool call), and toolDecisions. Each entry is { toolCallId, decision: "approve" | "deny" } — no arguments. The server loads the original input from the stored chat checkpoint so the client cannot swap ids or parameters at decision time.

POST /sveda/stream
Content-Type: application/json
x-sveda-embed-token: sveda_embed_…

{
  "chatId": "chat_1",
  "prompt": "",
  "messages": [ … ],
  "toolDecisions": [
    { "toolCallId": "call_del_1", "decision": "approve" }
  ]
}
  • approve + backend: runtime calls host MCP tools/call once with stored arguments; duplicate approve is a no-op.
  • approve + frontend: @sveda-ai/core runs the registered handler locally, then posts tool.result on the stream (same as other frontend tools).
  • deny: host is not called; result includes denied: true and a fixed error string (see below).
{
  "success": false,
  "denied": true,
  "error": "The user denied this action and it was not executed."
}

Full event list: stream protocol.

Server rules

  • Pending confirmation and a new user message without decisions → HTTP 409 Tool confirmation is required before the conversation can continue.
  • Decisions but no stored chat history → 409 Chat history is unavailable.
  • Decisions when nothing is pending → ignored; the turn continues normally.
  • Unresolved confirming calls after decisions → emit tool.result only; do not call the model until every confirming call has a result.
  • spawn_tasks refuses confirmation-required tools with an error (subagents cannot show the chat UI).
  • Display/history merges tool.result into the existing assistant message by toolCallId and keeps confirmation: "required" on parts so buttons survive refresh.

Chat UI

@sveda-ai/vue (embed and <sveda-chat> ) renders a compact card under the tool row: prompt text, JSON arguments, Confirm and Deny buttons. The composer is disabled while any tool call in the current chat still has confirmation: "required" and no result. iOS uses the same protocol fields and sends toolDecisions from the native chat surface.

import { useSvedaChatPage } from '@sveda-ai/vue'

const chat = useSvedaChatPage({ … })
// chat.confirmationPending — composer locked while a tool waits
// @confirm-tool / @deny-tool → session.resolveToolConfirmation(toolCallId, 'approve' | 'deny')

Custom UIs should call session.resolveToolConfirmation(toolCallId, 'approve' | 'deny') on SvedaChatSession ( JS client ). Do not auto-run frontend handlers for confirming tools while autoSubmitFrontendToolResults is on — the core client skips them until approve.

Policies and visibility

Confirmation only matters if the model can see the tool. Embed policies filter host MCP tools on every turn: mcp.allow must include the tool name (or a matching glob), and max_mode must allow the tool mode. Delete tools need max_mode: "delete" — a policy capped at write hides delete_post from the model even when the host registers it.

{
  "agent": {
    "web": true,
    "code": false,
    "mcp": {
      "allow": ["search_posts", "create_post", "update_post", "delete_post"],
      "domains": ["posts"],
      "max_mode": "delete"
    },
    "client": { "allow": [] }
  }
}

The host still chooses which tools to expose per user via resolveToolsUsing ; that list cannot widen past the token policy. Row-level rules (only delete your own rows) stay in the host handle method — confirmation is an extra UX gate, not authorization.

Try it in the playground

Laravel (and other) SDK playgrounds register delete_post with confirmation: required for agent users. Set the runtime agent policy to allow delete_post and max_mode: delete at sveda.yaml, open the JS chat, and ask to delete a post by id.

Gate destructive tools in the chat.

Declare confirmation on the host tool or client registry — the runtime and UI handle the pause.

MCP and tools