Skip to content

Commit aa5b75f

Browse files
committed
fix(agents): preserve Symbol-keyed metadata through normalizeToolParameters so re-wrap path recovers source tool (fixes #92973)
1 parent d2688db commit aa5b75f

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

src/agents/agent-tools.before-tool-call.e2e.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
runBeforeToolCallHook,
2929
wrapToolWithBeforeToolCallHook,
3030
} from "./agent-tools.before-tool-call.js";
31+
import { normalizeToolParameters } from "./agent-tools.schema.js";
3132
import { CRITICAL_THRESHOLD } from "./tool-loop-detection.js";
3233
import type { AnyAgentTool } from "./tools/common.js";
3334
import { callGatewayTool } from "./tools/gateway.js";
@@ -418,6 +419,53 @@ describe("before_tool_call loop detection behavior", () => {
418419
expect(execute).toHaveBeenCalledTimes(1);
419420
});
420421

422+
it("re-wraps a normalized already-wrapped tool with the current context (createOpenClawCodingTools path)", async () => {
423+
const execute = vi.fn().mockResolvedValue({
424+
content: [{ type: "text", text: "ok" }],
425+
details: { ok: true },
426+
});
427+
const params = { path: "/tmp/file" };
428+
429+
// Step 1: wrap with initial context (shared tool registry)
430+
const firstContext = {
431+
...enabledLoopDetectionContext,
432+
runId: "initial-run",
433+
sessionKey: "initial-session",
434+
};
435+
const baseTool: AnyAgentTool = { name: "read", execute } as any;
436+
const wrappedOnce = wrapToolWithBeforeToolCallHook(
437+
{ name: "read", execute } as any,
438+
firstContext,
439+
);
440+
expect(isToolWrappedWithBeforeToolCallHook(wrappedOnce)).toBe(true);
441+
442+
// Step 2: normalize (simulating createOpenClawCodingTools pipeline)
443+
// normalizeToolParameters does { ...tool, parameters } which drops
444+
// non-enumerable Symbol metadata (BEFORE_TOOL_CALL_SOURCE_TOOL,
445+
// BEFORE_TOOL_CALL_HOOK_CONTEXT). After the fix, those Symbols
446+
// are preserved via Object.getOwnPropertySymbols.
447+
const normalized = normalizeToolParameters(wrappedOnce, {
448+
modelProvider: "anthropic",
449+
});
450+
451+
// Step 3: re-wrap with new context (the second half of the pipeline)
452+
const newContext = {
453+
...enabledLoopDetectionContext,
454+
runId: "new-run",
455+
sessionKey: "new-session",
456+
};
457+
const reWrapped = wrapToolWithBeforeToolCallHook(normalized, newContext);
458+
459+
// After normalization + re-wrap, the tool should be wrapped with new context
460+
expect(isToolWrappedWithBeforeToolCallHook(reWrapped)).toBe(true);
461+
// The re-wrapped tool is a fresh wrapper, not the normalized one
462+
expect(reWrapped).not.toBe(normalized);
463+
464+
// Execute: the hook should fire exactly once with the new context
465+
await expectUnblockedToolExecution(reWrapped, "call-1", params);
466+
expect(execute).toHaveBeenCalledTimes(1);
467+
});
468+
421469
it("escalates generic repeat diagnostics from warning to critical", async () => {
422470
await withToolLoopEvents(async (emitted) => {
423471
const { tool, params } = createGenericReadRepeatFixture();

src/agents/agent-tools.schema.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,21 @@ export function normalizeToolParameters(
8282
return tool;
8383
}
8484
const parameters = normalizeToolParameterSchema(schema, options);
85-
return preserveToolMeta({
85+
const result = preserveToolMeta({
8686
...tool,
8787
...addEmptyObjectArgumentPreparation(tool, parameters),
8888
parameters,
8989
});
90+
// Preserve Symbol-keyed metadata (e.g. before_tool_call wrapper markers)
91+
// that object spread drops when non-enumerable. This ensures already-wrapped
92+
// tools retain their source-tool reference and hook context through normalization.
93+
for (const sym of Object.getOwnPropertySymbols(tool)) {
94+
const desc = Object.getOwnPropertyDescriptor(tool, sym);
95+
if (desc) {
96+
Object.defineProperty(result, sym, desc);
97+
}
98+
}
99+
return result;
90100
}
91101

92102
/**

0 commit comments

Comments
 (0)