fix(core): execute renamed tool definitions
This commit is contained in:
parent
dd014120cd
commit
4086aa8079
2 changed files with 58 additions and 9 deletions
|
|
@ -191,22 +191,31 @@ export const layer = Layer.effect(
|
||||||
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
|
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
|
||||||
const toolDefinitions = tools.definitions
|
const toolDefinitions = tools.definitions
|
||||||
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
||||||
|
// Object identity preserves registry provenance when a hook moves a definition to a new key.
|
||||||
|
const toolNamesByDefinition = new Map<object, string>()
|
||||||
// Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit.
|
// Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit.
|
||||||
|
const hookTools = Object.fromEntries(
|
||||||
|
toolDefinitions.map((tool) => {
|
||||||
|
const definition = { description: tool.description, input: { ...tool.inputSchema } }
|
||||||
|
toolNamesByDefinition.set(definition, tool.name)
|
||||||
|
return [tool.name, definition]
|
||||||
|
}),
|
||||||
|
)
|
||||||
const contextEvent = yield* hooks.trigger("session", "context", {
|
const contextEvent = yield* hooks.trigger("session", "context", {
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
model: resolved.ref,
|
model: resolved.ref,
|
||||||
system,
|
system,
|
||||||
messages,
|
messages,
|
||||||
tools: Object.fromEntries(
|
tools: hookTools,
|
||||||
toolDefinitions.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
|
const executableTools = new Map<string, string>()
|
||||||
const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
|
const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
|
||||||
const registered = toolsByName.get(name)
|
const registeredName = toolNamesByDefinition.get(tool)
|
||||||
return registered
|
const registered = registeredName ? toolsByName.get(registeredName) : undefined
|
||||||
? [{ ...registered, description: tool.description, inputSchema: tool.input }]
|
if (!registered || !registeredName) return []
|
||||||
: []
|
executableTools.set(name, registeredName)
|
||||||
|
return [{ ...registered, name, description: tool.description, inputSchema: tool.input }]
|
||||||
})
|
})
|
||||||
const request = LLM.request({
|
const request = LLM.request({
|
||||||
model,
|
model,
|
||||||
|
|
@ -259,10 +268,14 @@ export const layer = Layer.effect(
|
||||||
const executeTool: Prepared["executeTool"] = (executeInput) => {
|
const executeTool: Prepared["executeTool"] = (executeInput) => {
|
||||||
if (stepLimitReached)
|
if (stepLimitReached)
|
||||||
return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" })
|
return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" })
|
||||||
if (toolsByName.has(executeInput.call.name) && !Object.hasOwn(contextEvent.tools, executeInput.call.name))
|
const registeredName = executableTools.get(executeInput.call.name)
|
||||||
|
if (!registeredName && toolsByName.has(executeInput.call.name))
|
||||||
return new Tool.Error({ message: `Tool is not available for this request: ${executeInput.call.name}` })
|
return new Tool.Error({ message: `Tool is not available for this request: ${executeInput.call.name}` })
|
||||||
return tools
|
return tools
|
||||||
.execute(executeInput)
|
.execute({
|
||||||
|
...executeInput,
|
||||||
|
call: { ...executeInput.call, name: registeredName ?? executeInput.call.name },
|
||||||
|
})
|
||||||
.pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline)))
|
.pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline)))
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -887,6 +887,42 @@ describe("SessionRunnerLLM", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("advertises and executes a tool renamed by a session context hook", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const session = yield* setup
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
|
yield* hooks.register("session", "context", (event) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const tool = event.tools.echo
|
||||||
|
if (!tool) return
|
||||||
|
event.tools.renamed_echo = tool
|
||||||
|
delete event.tools.echo
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
yield* admit(session, "Use the renamed tool")
|
||||||
|
yield* TestLLM.push(TestLLM.tool("call-renamed", "renamed_echo", { text: "renamed" }), [])
|
||||||
|
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
expect(requests[0]?.tools.map((tool) => tool.name)).toContain("renamed_echo")
|
||||||
|
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("echo")
|
||||||
|
expect(executions).toEqual(["renamed"])
|
||||||
|
expect(yield* session.context(sessionID)).toMatchObject([
|
||||||
|
{ type: "user", text: "Use the renamed tool" },
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool",
|
||||||
|
id: "call-renamed",
|
||||||
|
state: { status: "completed", content: [{ type: "text", text: "renamed" }] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("advertises and executes a location registered tool", () =>
|
it.effect("advertises and executes a location registered tool", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue