fix(core): stabilize tool definition ordering (#38590)

This commit is contained in:
Kit Langton 2026-07-23 21:30:06 -04:00 committed by GitHub
parent bb3f4cc3c7
commit c228fc4886
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 35 additions and 5 deletions

View file

@ -323,7 +323,10 @@ const registryLayer = Layer.effect(
const codemodeTool = (yield* codeMode.materialize(permissions)).tool
return {
definitions: [
...Array.from(direct, ([name, registration]) => toLLMDefinition(name, registration.tool)),
// Definitions are prompt-cache prefix bytes, so order only after effective registrations settle.
...Array.from(direct)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([name, registration]) => toLLMDefinition(name, registration.tool)),
...(codemodeTool ? [toLLMDefinition("execute", codemodeTool)] : []),
],
execute: (input: ExecuteInput) => {

View file

@ -300,8 +300,8 @@ describe("PluginV2", () => {
yield* plugins.activate([versioned(plugin)])
expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toEqual([
"plain",
"context7_look_up",
"plain",
"execute",
])
}),

View file

@ -121,6 +121,33 @@ describe("ToolRegistry", () => {
}),
)
it.effect("canonicalizes effective definitions and keeps Code Mode last", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
const tool = make()
const capture = (registrations: Parameters<typeof service.registerBatch>[0]) =>
Effect.scoped(
Effect.gen(function* () {
yield* service.registerBatch(registrations)
return (yield* service.snapshot()).definitions
}),
)
const first = yield* capture([
{ tools: { zeta: tool, alpha: tool }, options: { codemode: false } },
{ tools: { beta: tool }, options: { namespace: "alpha", codemode: false } },
{ tools: { echo: tool } },
])
const second = yield* capture([
{ tools: { echo: tool } },
{ tools: { beta: tool }, options: { namespace: "alpha", codemode: false } },
{ tools: { alpha: tool, zeta: tool }, options: { codemode: false } },
])
expect(first).toEqual(second)
expect(first.map((definition) => definition.name)).toEqual(["alpha", "alpha_beta", "zeta", "execute"])
}),
)
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
@ -142,7 +169,7 @@ describe("ToolRegistry", () => {
{ action: "*", resource: "*", effect: "deny" },
]),
).toEqual([])
expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["question", "bash"])
expect(yield* names([{ action: "edit", resource: "*", effect: "deny" }])).toEqual(["bash", "question"])
}),
)

View file

@ -1099,7 +1099,7 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(1)
expect(requests[0]?.model).toBe(model)
expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["echo", "defect", "storefail"])
expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"])
expect(requests[0]?.messages.map((message) => ({ role: message.role, content: message.content }))).toEqual([
{ role: "user", content: [{ type: "text", text: "First" }] },
{ role: "user", content: [{ type: "text", text: "Second" }] },
@ -2392,7 +2392,7 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["echo", "defect", "storefail"])
expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"])
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Use tools" },
{