diff --git a/src/agents/session-tool-result-guard-wrapper.ts b/src/agents/session-tool-result-guard-wrapper.ts index 956247a24..fcc0ef3b3 100644 --- a/src/agents/session-tool-result-guard-wrapper.ts +++ b/src/agents/session-tool-result-guard-wrapper.ts @@ -18,6 +18,7 @@ export function guardSessionManager( agentId?: string; sessionKey?: string; allowSyntheticToolResults?: boolean; + dropOrphanToolResults?: boolean; }, ): GuardedSessionManager { if (typeof (sessionManager as GuardedSessionManager).flushPendingToolResults === "function") { @@ -48,6 +49,7 @@ export function guardSessionManager( const guard = installSessionToolResultGuard(sessionManager, { transformToolResultForPersistence: transform, allowSyntheticToolResults: opts?.allowSyntheticToolResults, + dropOrphanToolResults: opts?.dropOrphanToolResults, }); (sessionManager as GuardedSessionManager).flushPendingToolResults = guard.flushPendingToolResults; return sessionManager as GuardedSessionManager; diff --git a/src/agents/session-tool-result-guard.test.ts b/src/agents/session-tool-result-guard.test.ts index 1bfcb31ed..def38a7ac 100644 --- a/src/agents/session-tool-result-guard.test.ts +++ b/src/agents/session-tool-result-guard.test.ts @@ -72,6 +72,25 @@ describe("installSessionToolResultGuard", () => { expect(messages.map((m) => m.role)).toEqual(["assistant", "toolResult"]); }); + it("drops orphan toolResult messages by default", () => { + const sm = SessionManager.inMemory(); + installSessionToolResultGuard(sm); + + sm.appendMessage({ + role: "toolResult", + toolCallId: "orphan_call", + content: [{ type: "text", text: "no matching tool call" }], + isError: true, + } as AgentMessage); + + const messages = sm + .getEntries() + .filter((e) => e.type === "message") + .map((e) => (e as { message: AgentMessage }).message); + + expect(messages).toHaveLength(0); + }); + it("preserves ordering with multiple tool calls and partial results", () => { const sm = SessionManager.inMemory(); const guard = installSessionToolResultGuard(sm); diff --git a/src/agents/session-tool-result-guard.ts b/src/agents/session-tool-result-guard.ts index feb6b854c..6cfc8aeb0 100644 --- a/src/agents/session-tool-result-guard.ts +++ b/src/agents/session-tool-result-guard.ts @@ -49,6 +49,11 @@ export function installSessionToolResultGuard( * Defaults to true. */ allowSyntheticToolResults?: boolean; + /** + * Whether to drop toolResult messages that do not match a pending tool call. + * Defaults to true to avoid corrupting transcripts with orphan tool results. + */ + dropOrphanToolResults?: boolean; }, ): { flushPendingToolResults: () => void; @@ -66,6 +71,7 @@ export function installSessionToolResultGuard( }; const allowSyntheticToolResults = opts?.allowSyntheticToolResults ?? true; + const dropOrphanToolResults = opts?.dropOrphanToolResults ?? true; const flushPendingToolResults = () => { if (pending.size === 0) return; @@ -90,6 +96,9 @@ export function installSessionToolResultGuard( if (role === "toolResult") { const id = extractToolResultId(message as Extract); const toolName = id ? pending.get(id) : undefined; + if (id && !pending.has(id) && dropOrphanToolResults) { + return undefined as never; + } if (id) pending.delete(id); return originalAppend( persistToolResult(message, {