From 7af249b190f8a57c8867aeca00dc10e0b1e06d2c Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 16 Aug 2026 02:31:57 +0200 Subject: [PATCH] fix(coding-agent): preserved tool-search history on disk --- docs/provider-quirks.md | 2 +- .../src/session/session-persistence.ts | 8 ++--- .../signature-persistence.test.ts | 30 +++++++++++++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/provider-quirks.md b/docs/provider-quirks.md index 8b368f3d8..674342249 100644 --- a/docs/provider-quirks.md +++ b/docs/provider-quirks.md @@ -152,7 +152,7 @@ The Anthropic provider (`packages/ai/src/providers/anthropic.ts`) implements the - **Claude Code Fingerprint Headers & Betas**: Default headers include `anthropic-version: 2023-06-01`, `anthropic-dangerous-direct-browser-access: true`, `x-app: cli`, and `User-Agent: claude-cli/2.1.220 (external, claude-desktop)` (`coworkUserAgent`). Active beta flags (`buildCoworkBetas`) include `claude-code-20250219`, `interleaved-thinking-2025-05-14`, `thinking-token-count-2026-05-13`, `context-management-2025-06-27`, `prompt-caching-scope-2026-01-05`, `mid-conversation-system-2026-04-07`, `advanced-tool-use-2025-11-20`, `effort-2025-11-24`, and `fallback-credit-2026-06-01` (`context-1m-2025-08-07` is omitted to avoid 429 credit errors on subscription tokens, #7238). Fingerprint metadata (`generateClaudeCloakingUserId`, `deriveClaudeDeviceId`, `generateClaudeJsonUserId`) generates device/session IDs. Billing attestation headers (`createClaudeBillingHeader`, `wrapFetchForCch`, `patchCch`) embed `cch=00000` XXHash64 hashes into `system[0]`. - **System-Prompt Injection**: `buildAnthropicSystemBlocks` (`packages/ai/src/providers/anthropic.ts`) automatically prepends `claudeCodeSystemInstruction` ("You are a Claude agent, built on Anthropic's Claude Agent SDK.") as `system[0]` for OAuth credentials. Mid-conversation system messages in turn history are enabled for Opus 4.8+ / Sonnet 5+ via `mid-conversation-system-2026-04-07`. - **Thinking Signatures & Redacted Thinking**: Replaying modified or unsigned thinking blocks causes Anthropic API errors (`invalid signature in thinking block`). `convertAnthropicMessages` converts `ThinkingContent` and `RedactedThinkingContent` (`type: "redacted_thinking"`, `data`). `maybeAddReplayUnsignedThinkingHint` attaches recovery hints on signature errors, while `unwrapAnthropicThinkingEnvelope` strips legacy `` XML wrappers. -- **Tool Use Replay & Prefixes**: `encodeAnthropicToolName` / `decodeAnthropicToolName` (`packages/ai/src/providers/anthropic.ts`) prefixes custom tool names with `_` (`claudeToolPrefix`) when using OAuth to prevent collisions with built-in tools (`web_search`, `code_execution`, `text_editor`, `computer`). Server-executed web searches (`ServerToolUseBlockParam`, `WebSearchToolResultBlockParam` in `anthropic-wire.ts`) are detected via `isAnthropicWebSearchHistoryBlock` for turn replay. Empty tool errors are filled by `ensureErrorToolResultWireContent`. +- **Tool Use Replay & Prefixes**: `encodeAnthropicToolName` / `decodeAnthropicToolName` (`packages/ai/src/providers/anthropic.ts`) prefixes custom tool names with `_` (`claudeToolPrefix`) when using OAuth to prevent collisions with built-in tools (`web_search`, `code_execution`, `text_editor`, `computer`). Server-executed web searches and tool searches (`AnthropicServerToolHistoryBlockParam` in `anthropic-wire.ts`) are detected via `isAnthropicServerToolHistoryBlock` for turn replay. Empty tool errors are filled by `ensureErrorToolResultWireContent`. - **Strict-Tool Schema Normalization & Fallback**: `normalizeAnthropicToolSchema` and `normalizeAnthropicStrictSchema` strip unsupported JSON schema keywords (e.g. `minItems`/`maxItems` on objects) for the `structured-outputs-2025-12-15` beta. If a strict tool schema causes HTTP 400, `streamAnthropicOnce` calls `dropAnthropicStrictTools` and automatically retries without strict mode. - **Adaptive vs Budget Thinking**: `ThinkingConfigParam` (`anthropic-wire.ts`) supports budget thinking (`{ type: "enabled", budget_tokens: N }` enforced by `ensureMaxTokensForThinking`) and adaptive thinking (`{ type: "adaptive" }` paired with `output_config: { effort: level }` via `effort-2025-11-24` beta). Forced tool choices (`disableThinkingIfToolChoiceForced`) automatically disable thinking. - **Prompt Cache Breakpoints**: `applyPromptCaching` (`packages/ai/src/providers/anthropic.ts`) attaches `{ type: "ephemeral", scope: "global" }` breakpoints to system prompts (`cacheSystemPrefixBreakpoints`), tool definitions, and historical user turns. `enforceCacheControlLimit` caps total breakpoints to 4 per request. diff --git a/packages/coding-agent/src/session/session-persistence.ts b/packages/coding-agent/src/session/session-persistence.ts index 22e95a1cf..ee1a95546 100644 --- a/packages/coding-agent/src/session/session-persistence.ts +++ b/packages/coding-agent/src/session/session-persistence.ts @@ -1,4 +1,4 @@ -import { isAnthropicWebSearchHistoryBlock } from "@oh-my-pi/pi-ai/providers/anthropic-wire"; +import { isAnthropicServerToolHistoryBlock } from "@oh-my-pi/pi-ai/providers/anthropic-wire"; import { type BlobStore, externalizeImageDataSync, @@ -100,8 +100,8 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string // Persist signed blocks verbatim — never truncate, externalize, or descend. // Unsigned blocks (e.g. an interrupted stream) have no such binding and stay // truncatable for size control. - // Anthropic validates native web-search history byte-for-byte on replay. - // Keep the complete typed block atomic, including nested encrypted_content. + // Anthropic validates native web-search and tool-search history byte-for-byte + // on replay. Keep the complete typed block atomic, including opaque content. if (typeof obj === "object" && "type" in obj && obj.type === "anthropicServerTool" && "block" in obj) { const block = obj.block; if (typeof block === "object" && block !== null && "type" in block && typeof block.type === "string") { @@ -112,7 +112,7 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string ...("tool_use_id" in block ? { tool_use_id: block.tool_use_id } : {}), ...("content" in block ? { content: block.content } : {}), }; - if (isAnthropicWebSearchHistoryBlock(validationView)) return obj; + if (isAnthropicServerToolHistoryBlock(validationView)) return obj; } } if (typeof obj === "object" && "type" in obj) { diff --git a/packages/coding-agent/test/session-manager/signature-persistence.test.ts b/packages/coding-agent/test/session-manager/signature-persistence.test.ts index 849bc986a..bec4db6a6 100644 --- a/packages/coding-agent/test/session-manager/signature-persistence.test.ts +++ b/packages/coding-agent/test/session-manager/signature-persistence.test.ts @@ -278,16 +278,16 @@ describe("SessionManager signature persistence", () => { await reloaded.close(); }, 15_000); - it("preserves oversized Anthropic web-search results byte-for-byte across reload", async () => { + it("preserves oversized Anthropic server-tool results byte-for-byte across reload", async () => { using tempDir = TempDir.createSync("@pi-session-anthropic-server-tool-persistence-"); const session = SessionManager.create(tempDir.path(), tempDir.path()); - const encryptedContent = `ENCRYPTED_WEB_SEARCH_RESULT_${"W".repeat(600_000)}`; + const oversizedPayload = "W".repeat(600_000); const serverToolContent: AssistantMessage["content"] = [ { type: "anthropicServerTool", block: { type: "server_tool_use", - id: "srvtoolu_search", + id: "srvtoolu_web_search", name: "web_search", input: { query: "current UTC date" }, }, @@ -296,8 +296,28 @@ describe("SessionManager signature persistence", () => { type: "anthropicServerTool", block: { type: "web_search_tool_result", - tool_use_id: "srvtoolu_search", - content: [{ type: "web_search_result", encrypted_content: encryptedContent }], + tool_use_id: "srvtoolu_web_search", + content: [{ type: "web_search_result", encrypted_content: `ENCRYPTED_WEB_SEARCH_${oversizedPayload}` }], + }, + }, + { + type: "anthropicServerTool", + block: { + type: "server_tool_use", + id: "srvtoolu_tool_search", + name: "tool_search_tool_bm25", + input: { query: "read" }, + }, + }, + { + type: "anthropicServerTool", + block: { + type: "tool_search_tool_result", + tool_use_id: "srvtoolu_tool_search", + content: { + type: "tool_search_tool_search_result", + tool_references: [{ type: "tool_reference", tool_name: `READ_TOOL_${oversizedPayload}` }], + }, }, }, ];