
A framework-neutral execution pattern for validating, approving, retrying, reconciling, and tracing AI assistant tool calls without duplicating real-world actions.
charge_card tool succeeds upstream, but the HTTP response times out. The assistant sees an exception, concludes that the charge failed, and calls the tool again.call_id, then the application executing it and sending back correlated output. That call ID is simply the identifier that pairs one request with its result. Anthropic documents client tools as tool_use requests that application code executes before returning tool_result. The MCP tools specification puts invocation on the client and recommends confirmation for sensitive operations, timeouts, audit logs, and result validation. 123const call = await model.next(messages);
const args = JSON.parse(call.arguments);
const result = await tools[call.name](args);
messages.push(toolResult(call.id, result));| Decision | Model contribution | Runner responsibility |
|---|---|---|
| Which tool? | Proposes a tool name | Checks that the tool is registered and allowed in this run |
| With what input? | Produces arguments | Parses, validates, normalizes, and freezes them |
| On whose authority? | May explain intent | Enforces user identity, permissions, scope, and approval policy |
| In what order? | May request several calls | Serializes conflicting writes and limits concurrency |
| After an error? | May suggest another action | Classifies the outcome and applies the retry policy |
| Did it succeed? | Reads the returned result | Records the authoritative result before summarizing it |
| When should the run end? | May return a final answer | Enforces turn, time, cost, and tool-call budgets |
| State | What the runner does | What must be durable before moving on |
|---|---|---|
proposed | Accepts a provider call and correlates it with the current run | Provider call_id, tool name, raw arguments |
validated | Parses the schema, normalizes values, checks permissions, assigns risk | Frozen arguments, argument hash, policy decision |
awaiting_approval | Shows the exact operation to an approver when policy requires it | Approval request and expiry |
executing | Calls the tool under a timeout, attempt budget, and idempotency policy | Logical operation ID, idempotency key (a stable token reused for the same retryable operation), attempt number |
recorded | Stores the raw success or typed failure | Authoritative result, timestamps, error class |
reported | Sends a compact, correlated result back to the model | Provider result payload and delivery status |
rejected, failed_permanent, and budget_exhausted are terminal states. outcome_unknown is not. It means the runner needs reconciliation before it can safely choose a terminal state or another attempt.executing before the network call. Persist the result before returning it to the model. If the process dies between those writes, recovery starts from a known operation ID instead of inventing a new call.type ToolRisk = "read" | "reversible_write" | "irreversible_write";
type ToolErrorCode =
| "invalid_arguments"
| "forbidden"
| "approval_rejected"
| "rate_limited"
| "upstream_unavailable"
| "timeout_unknown"
| "permanent_failure";
type ToolSpec<I, O> = {
name: string;
inputSchema: Schema<I>;
outputSchema: Schema<O>;
risk: ToolRisk;
timeoutMs: number;
maxAttempts: number;
supportsIdempotency: boolean;
execute(ctx: ExecutionContext, input: I): Promise<O>;
};
type Invocation<I> = {
runId: string;
callId: string;
logicalOperationId: string;
toolName: string;
frozenInput: I;
inputHash: string;
idempotencyKey?: string;
attempt: number;
};
type ToolOutcome<O> =
| { kind: "succeeded"; value: O }
| { kind: "rejected"; code: ToolErrorCode }
| { kind: "failed_retryable"; code: ToolErrorCode; retryAfterMs?: number }
| { kind: "failed_permanent"; code: ToolErrorCode }
| { kind: "outcome_unknown"; code: "timeout_unknown" };failed_retryable, and only after the registered tool policy confirms that another attempt is safe. An irreversible write without idempotency support should become outcome_unknown or a terminal failure, never an automatic retry.frozenInput must not change between attempts. If the model revises an address, amount, or recipient, that is a new operation that needs a new policy decision.outcome_unknown prevents a timeout from masquerading as a clean failure. The server may have committed the write before the connection broke.catch (err) { retry() }:| Outcome | Example | Runner action |
|---|---|---|
| Invalid request | Schema mismatch, impossible enum | Do not call the tool; return a correction-safe error |
| Forbidden | User lacks scope for the resource | Stop; do not let the model negotiate around policy |
| Approval rejected | Human declines a destructive action | Stop that operation and report the rejection |
| Transient failure | 429, selected 5xx, connection refused | Retry only when the tool policy allows it; use backoff and stop at the declared budget |
| Duplicate or replay | The logical operation already has a stored result | Return the stored outcome; do not call the tool again |
| Unknown outcome | Timeout after sending a write | Reconcile by idempotency key or status lookup; do not create a fresh operation |
| Retry budget exhausted | Attempts, time, or spend reaches its limit | Enter budget_exhausted; block further attempts for that operation |
| Cancellation requested | User stops an in-flight call | Send transport cancellation when supported; close the model turn and record any late result |
| Permanent failure | Closed account, deleted resource, invalid state transition | Stop and return a typed reason |
outcome_unknown for a person or higher-level workflow to resolve.{
"status": "outcome_unknown",
"code": "timeout_unknown",
"logical_operation_id": "op_01J...",
"message": "The request was sent, but completion is not confirmed.",
"allowed_next_actions": ["check_status", "request_human_review"]
}trace_id, run_id, model turn, and provider call_idexecute_tool span with a tool name, call ID, result, and error type; arguments and results are opt-in because they may be sensitive. 1112| Test | Expected trace |
|---|---|
| Arguments fail schema validation | No network span; terminal validation error |
| User lacks permission | No execution attempt; policy denial recorded |
| Approval is rejected | Run resumes with rejection; tool never starts |
Tool returns 503 twice, then succeeds | Three attempts under one logical operation ID and one retry budget |
| Write commits, response times out | outcome_unknown, then reconciliation; no new logical operation |
| Process crashes after the write | Recovery finds the recorded intent and reuses the idempotency key |
| Model keeps requesting tools | Runner stops at the turn or tool-call budget |
| Cancelled call returns late | Late result is recorded but not attached to the closed turn |
outcome_unknown distinct from success and failure; add a reconciliation path.platform.openai.com
platform.claude.com
modelcontextprotocol.io
developers.openai.com
docs.temporal.io
docs.stripe.com
developers.openai.com
platform.claude.com
modelcontextprotocol.io
opentelemetry.io
github.com
anthropic.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.