Skip to content

Commit cb5db51

Browse files
committed
Fold input schemas into the tool-set digest
The digest gates the tool-set-changed log line, and the tools array heads the provider's cached prompt prefix: a schema-only change reshapes the wire bytes while leaving a name:description hash untouched, so the cache bust goes unlogged. Hash the schema too; the wire behavior is unchanged.
1 parent 7ca090b commit cb5db51

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

src/agent/director.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
ReactorInboundEvent,
66
ReactorState,
77
} from "@intx/types/runtime";
8-
import { createChatDirector } from "./director.js";
8+
import { createChatDirector, toolSetDigest } from "./director.js";
99

1010
const mockState: ReactorState = { turns: [] } as unknown as ReactorState;
1111

@@ -100,6 +100,36 @@ async function runToolOnlyStreak(
100100
return last;
101101
}
102102

103+
describe("toolSetDigest", () => {
104+
const base = {
105+
name: "read_file",
106+
description: "read a file",
107+
inputSchema: { type: "object" },
108+
};
109+
110+
test("identical sets share a digest", () => {
111+
expect(toolSetDigest([{ ...base }])).toBe(toolSetDigest([{ ...base }]));
112+
});
113+
114+
// The digest gates the tool-set-changed log line, and the serialized tools
115+
// array is the head of the provider's cached prompt prefix — an
116+
// inputSchema-only change reshapes the wire bytes, so it must move the
117+
// digest or the cache bust goes unlogged.
118+
test("an inputSchema-only change alters the digest", () => {
119+
const before = [{ ...base }];
120+
const after = [
121+
{
122+
...base,
123+
inputSchema: {
124+
type: "object",
125+
properties: { path: { type: "string" } },
126+
},
127+
},
128+
];
129+
expect(toolSetDigest(after)).not.toBe(toolSetDigest(before));
130+
});
131+
});
132+
103133
describe("ChatDirector tool-only loop protection", () => {
104134
const providerlessPolicy = { providerName: "test-provider" };
105135

src/agent/director.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ const logger = getLogger([LOG_NAMESPACE_ROOT, "agent", "director"]);
6262
// otherwise invisible — it shows up only as a billing and latency spike a turn
6363
// later. Hashed rather than logged verbatim: MCP tool descriptions are
6464
// arbitrary-length, server-supplied text and do not belong in the log stream.
65-
function toolSetDigest(tools: readonly ToolDefinition[]): string {
66-
const shape = tools.map((t) => `${t.name}:${t.description ?? ""}`).join("|");
65+
export function toolSetDigest(tools: readonly ToolDefinition[]): string {
66+
const shape = tools
67+
.map(
68+
(t) =>
69+
`${t.name}:${t.description ?? ""}:${JSON.stringify(t.inputSchema ?? null)}`,
70+
)
71+
.join("|");
6772
return createHash("sha256").update(shape).digest("hex").slice(0, 12);
6873
}
6974

0 commit comments

Comments
 (0)