Skip to content

Commit eb661e6

Browse files
committed
fix(core): settle unadvertised tool calls and repair session hook test contexts
1 parent 3379e44 commit eb661e6

5 files changed

Lines changed: 30 additions & 5 deletions

File tree

packages/core/src/session/runner/llm.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,25 @@ const layer = Layer.effect(
315315
}
316316
yield* publish(event)
317317
if (event.type !== "tool-call" || event.providerExecuted) return
318-
if (!toolMaterialization || !advertisedTools.has(event.name)) {
318+
if (!toolMaterialization) {
319319
yield* serialized(
320320
publisher.failUnsettledTools({
321321
type: "tool.execution",
322+
message: "Tools are disabled after the maximum agent steps",
323+
}),
324+
)
325+
return
326+
}
327+
// A request hook hid this registered tool from the current request. Fail only
328+
// this call durably and continue so the model can react, instead of executing
329+
// a tool that was not advertised. Unregistered tools flow through settle, which
330+
// durably fails them as unknown.
331+
if (!advertisedTools.has(event.name) && availableTools.has(event.name)) {
332+
needsContinuation = true
333+
yield* publish(
334+
LLMEvent.toolError({
335+
id: event.id,
336+
name: event.name,
322337
message: `Tool is not available for this request: ${event.name}`,
323338
}),
324339
)

packages/core/test/lib/tool.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Tool } from "@opencode-ai/core/tool/tool"
66
import { Tools } from "@opencode-ai/core/tool/tools"
77
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
88
import { Effect, type Scope } from "effect"
9+
import { host } from "../plugin/host"
910

1011
export const toolIdentity = {
1112
agent: AgentV2.ID.make("build"),
@@ -48,7 +49,7 @@ export const registerToolPlugin = <R>(plugin: {
4849
}): Effect.Effect<void, never, R | Tools.Service | Scope.Scope> =>
4950
Effect.gen(function* () {
5051
const tools = yield* Tools.Service
51-
const context: Pick<PluginContext, "tool"> = {
52+
const context = host({
5253
tool: {
5354
transform: (callback) =>
5455
Effect.gen(function* () {
@@ -71,8 +72,8 @@ export const registerToolPlugin = <R>(plugin: {
7172
}),
7273
hook: () => Effect.die("registerToolPlugin does not support tool hooks"),
7374
},
74-
}
75-
yield* plugin.effect(context as PluginContext)
75+
})
76+
yield* plugin.effect(context)
7677
})
7778

7879
export const settleTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput, model = testModel) =>

packages/core/test/location-layer.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ describe("LocationServiceMap", () => {
290290
"edit",
291291
"glob",
292292
"grep",
293+
"patch",
293294
"question",
294295
"read",
295296
"shell",
@@ -307,6 +308,7 @@ describe("LocationServiceMap", () => {
307308
"edit",
308309
"glob",
309310
"grep",
311+
"patch",
310312
"question",
311313
"read",
312314
"shell",

packages/core/test/plugin/host.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ export function host(overrides: Overrides = {}): PluginContext {
8383
prompt: () => Effect.die("unused session.prompt"),
8484
command: () => Effect.die("unused session.command"),
8585
interrupt: () => Effect.die("unused session.interrupt"),
86-
hook: () => Effect.die("unused session.hook"),
86+
// Plugins register session hooks during setup, so a bare host accepts the
87+
// registration; the callback only runs when a test triggers the request pipeline.
88+
hook: () => Effect.succeed({ dispose: Effect.void }),
8789
},
8890
}
8991
}

packages/core/test/tool-subagent.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ const withSubagent = (location: Location.Ref) =>
107107
const locations = yield* LocationServiceMap.Service
108108
yield* AgentV2.Service.use((agents) =>
109109
agents.transform((draft) => {
110+
// The caller identity used by executeTool; subagent permission asserts against it.
111+
draft.update(toolIdentity.agent, (agent) => {
112+
agent.mode = "primary"
113+
agent.permissions.push({ action: "*", resource: "*", effect: "allow" })
114+
})
110115
draft.update(AgentV2.ID.make("reviewer"), (agent) => {
111116
agent.mode = "subagent"
112117
agent.model = childModel

0 commit comments

Comments
 (0)