Skip to content

Commit 100107c

Browse files
committed
perf(agent): serialize each tool schema once per drift
1 parent 2b42830 commit 100107c

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

‎src/agent/dynamic-tool-runner.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,4 +441,51 @@ describe("agent tool set pin", () => {
441441
changed: ["web_fetch"],
442442
});
443443
});
444+
445+
test("drift serializes each tool schema once when pinned and never when unpinned", () => {
446+
const count = 4;
447+
const tools = Array.from({ length: count }, (_, index) =>
448+
stringTool(`tool_${index}`, "ok"),
449+
);
450+
const runner = createDynamicToolRunner(tools);
451+
const stringify = JSON.stringify;
452+
const serializationsDuring = (body: () => void): number => {
453+
let serializations = 0;
454+
JSON.stringify = ((
455+
value: unknown,
456+
replacer?: Parameters<typeof JSON.stringify>[1],
457+
space?: Parameters<typeof JSON.stringify>[2],
458+
) => {
459+
serializations += 1;
460+
return stringify(value, replacer, space);
461+
}) as typeof JSON.stringify;
462+
try {
463+
body();
464+
} finally {
465+
JSON.stringify = stringify;
466+
}
467+
return serializations;
468+
};
469+
470+
expect(
471+
serializationsDuring(() => {
472+
expect(runner.agentToolSetDrift()).toEqual({
473+
added: [],
474+
removed: [],
475+
changed: [],
476+
});
477+
}),
478+
).toBe(0);
479+
480+
runner.pinAgentToolSet();
481+
expect(
482+
serializationsDuring(() => {
483+
expect(runner.agentToolSetDrift()).toEqual({
484+
added: [],
485+
removed: [],
486+
changed: [],
487+
});
488+
}),
489+
).toBe(count);
490+
});
444491
});

‎src/agent/dynamic-tool-runner.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,21 @@ export function createDynamicToolRunner(
120120

121121
const agentToolSetDrift = (): AgentToolSetDrift => {
122122
if (pinned === undefined) return { added: [], removed: [], changed: [] };
123+
// The pin holds the same definition objects the live set walks on an idle
124+
// tool set. Remember the shape by identity for this call so each schema is
125+
// serialized once, not again inside the changed filter.
126+
const shapes = new WeakMap<ToolDefinition, string>();
127+
const shapeOf = (definition: ToolDefinition): string => {
128+
const cached = shapes.get(definition);
129+
if (cached !== undefined) return cached;
130+
const shape = definitionShape(definition);
131+
shapes.set(definition, shape);
132+
return shape;
133+
};
123134
const pinnedShapes = new Map(
124135
pinned.definitions.map((definition) => [
125136
definition.name,
126-
definitionShape(definition),
137+
shapeOf(definition),
127138
]),
128139
);
129140
const live = currentDefinitions();
@@ -136,7 +147,7 @@ export function createDynamicToolRunner(
136147
changed: live
137148
.filter((definition) => {
138149
const before = pinnedShapes.get(definition.name);
139-
return before !== undefined && before !== definitionShape(definition);
150+
return before !== undefined && before !== shapeOf(definition);
140151
})
141152
.map((definition) => definition.name),
142153
};

0 commit comments

Comments
 (0)