Skip to content

Commit d3aa695

Browse files
bloveclaude
andauthored
fix(chat): never leave a client tool call unanswered on the server (#807)
* docs(specs): client-tool continuation correctness fixes Design for five defects found reviewing the shipped client-tool continuation stack (#782-#805). Four violate one unstated invariant: the server thread must never hold a client tool call without a result. Adds flush() to ClientToolsCapability so a settled result can be made durable without continuing the run, and maps all five fixes onto it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(plans): client-tool continuation fixes implementation plan Ten tasks covering all five defects, TDD per task, with a live browser verification gate whose decisive step is reload-then-continue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(chat): add flush() to ClientToolsCapability * feat(langgraph): batched flush() with submit-drain fallback * fix(langgraph): take flush() batch ownership at snapshot and clear staging on thread switch * fix(chat): flush settled client-tool groups and stop discarding limited results * fix(chat): settle aborted client tools and wrap agent.stop once * fix(chat): flush blocked client-tool groups once on completion Flushing per blocked call stranded every batch after the first: adapter flush() implementations coalesce concurrent calls by returning the in-flight promise, so only the first batch was ever snapshotted. Gate the blocked-group flush on group completion, mirroring the terminal path. Drop the blockedIds guard: both shipped adapters mark a call resolved inside settle() so pending() drops it immediately, meaning a settled call can never be re-presented to the executor effect. The hazard it guarded was an artifact of a test double whose settle() left calls pending forever; the fakes now mirror adapter behavior instead. Warn rather than silently discard when a blocked call cannot be recorded because the capability implements no settle(). * fix(middleware): enforce tenant isolation in the client tool execution store * fix(chat): never continue the run when settling a cancelled client tool * feat(examples): add terminal client tool to the chat demo * docs: regenerate API docs for client-tool flush surface Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(langgraph): chain concurrent flushes and drop stale-thread client tool results --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 0c66d1c commit d3aa695

25 files changed

Lines changed: 3079 additions & 83 deletions

apps/website/content/docs/chat/api/api-docs.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6952,6 +6952,12 @@
69526952
"description": "",
69536953
"optional": true
69546954
},
6955+
{
6956+
"name": "settleWithoutContinuing",
6957+
"type": "(toolCall: ToolCall, result: ClientToolResult) => void",
6958+
"description": "Settlement for calls that must NOT continue the run (user abort, teardown).",
6959+
"optional": true
6960+
},
69556961
{
69566962
"name": "shouldExecuteToolCall",
69576963
"type": "(toolCall: ToolCall) => boolean",
@@ -7024,6 +7030,12 @@
70247030
}
70257031
],
70267032
"methods": [
7033+
{
7034+
"name": "flush",
7035+
"signature": "flush(): void | Promise<void>",
7036+
"description": "Make every result recorded via settle durable on the server\nWITHOUT continuing the run. No-op for adapters whose settle() is already\ndurable. Adapters that buffer locally MUST clear their buffer only on a\nsuccessful write, so a failure degrades to a later flush or submit.",
7037+
"params": []
7038+
},
70277039
{
70287040
"name": "resolve",
70297041
"signature": "resolve(toolCallId: string, result: ClientToolResult): void",
@@ -8698,6 +8710,25 @@
86988710
},
86998711
"examples": []
87008712
},
8713+
{
8714+
"name": "cancelledClientToolResult",
8715+
"kind": "function",
8716+
"description": "Result recorded when the user stops a run while a client tool is running.",
8717+
"signature": "cancelledClientToolResult(toolCallId: string): ClientToolResult",
8718+
"params": [
8719+
{
8720+
"name": "toolCallId",
8721+
"type": "string",
8722+
"description": "",
8723+
"optional": false
8724+
}
8725+
],
8726+
"returns": {
8727+
"type": "ClientToolResult",
8728+
"description": ""
8729+
},
8730+
"examples": []
8731+
},
87018732
{
87028733
"name": "citationSourceVisual",
87038734
"kind": "function",

apps/website/content/docs/middleware/api/api-docs.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@
381381
{
382382
"name": "tenantId",
383383
"type": "string | null",
384-
"description": "",
384+
"description": "Tenant scope for every read and write. Defaults to `''` (the single-tenant scope).",
385385
"optional": true
386386
}
387387
],
@@ -472,7 +472,7 @@
472472
"name": "THREADPLANE_CLIENT_TOOL_EXECUTIONS_SCHEMA",
473473
"kind": "const",
474474
"description": "",
475-
"signature": "\"\\nCREATE TABLE IF NOT EXISTS threadplane_client_tool_executions (\\n tenant_id text,\\n thread_id text NOT NULL,\\n tool_call_id text NOT NULL,\\n status text NOT NULL,\\n result jsonb,\\n created_at timestamptz NOT NULL DEFAULT now(),\\n updated_at timestamptz NOT NULL DEFAULT now(),\\n PRIMARY KEY (thread_id, tool_call_id)\\n);\\n\"",
475+
"signature": "\"\\nCREATE TABLE IF NOT EXISTS threadplane_client_tool_executions (\\n tenant_id text NOT NULL DEFAULT '',\\n thread_id text NOT NULL,\\n tool_call_id text NOT NULL,\\n status text NOT NULL,\\n result jsonb,\\n created_at timestamptz NOT NULL DEFAULT now(),\\n updated_at timestamptz NOT NULL DEFAULT now(),\\n PRIMARY KEY (tenant_id, thread_id, tool_call_id)\\n);\\n\"",
476476
"examples": []
477477
},
478478
{

0 commit comments

Comments
 (0)