From 116aebd948f51698d7efd73b610ccd67cb4c109e Mon Sep 17 00:00:00 2001 From: grishberg Date: Mon, 13 Jul 2026 20:06:40 +0300 Subject: [PATCH 1/2] feat(core): detect tool calls leaked into reasoning and text --- .../src/session/runner/publish-llm-event.ts | 34 +++++-- packages/core/test/publish-llm-event.test.ts | 98 +++++++++++++++++++ 2 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 packages/core/test/publish-llm-event.test.ts diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 5177be7457ba..bb2fd1288877 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -12,6 +12,17 @@ type Input = { readonly model: ModelV2.Ref } +/** Detect XML-style tool calls like `` or `\n` emitted as plain text. */ +export const findToolCallName = (text: string): string | null => { + const funcMatch = text.match(//i) + if (funcMatch) return funcMatch[1] + const bracketMatch = text.match(/\[Assistant tool call\]\s*:\s*(\w+)\s*\(/i) + if (bracketMatch) return bracketMatch[1] + const jsonMatch = text.match(/>[\s\S]*?"name"\s*:\s*"([^"]+)"/u) + if (jsonMatch) return jsonMatch[1] + return null +} + const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0) const tokens = (usage: Usage | undefined) => { @@ -115,19 +126,15 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) const text = fragments("text", (textID, value) => Effect.gen(function* () { - const bracketToolCallMatch = value.match( - /\[Assistant tool call\]\s*:\s*(\w+)\s*\(/i, - ) - const xmlToolCallMatch = value.match(/<\w+>[\s\S]*?"name"\s*:\s*"([^"]+)"/u) - const toolCallMatch = bracketToolCallMatch ?? xmlToolCallMatch - if (toolCallMatch) { + const toolCallName = findToolCallName(value) + if (toolCallName) { textBasedToolCall = true yield* events.publish(SessionEvent.Text.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), timestamp: yield* timestamp, textID, - text: `[ERROR] You outputted a tool call as text ("${toolCallMatch[1]}") instead of using the structured tool_calls field in the API response. The tool was NOT executed. You MUST retry using the proper tool_calls mechanism.`, + text: `[ERROR] You outputted a tool call as text ("${toolCallName}") instead of using the structured tool_calls field in the API response. The tool was NOT executed. You MUST retry using the proper tool_calls mechanism.`, }) return } @@ -142,6 +149,19 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) ) const reasoning = fragments("reasoning", (reasoningID, value, providerMetadata) => Effect.gen(function* () { + const toolCallName = findToolCallName(value) + if (toolCallName) { + textBasedToolCall = true + yield* events.publish(SessionEvent.Reasoning.Ended, { + sessionID: input.sessionID, + assistantMessageID: yield* currentAssistantMessageID(), + timestamp: yield* timestamp, + reasoningID, + text: `[ERROR] You outputted a tool call as text ("${toolCallName}") in your reasoning instead of using the structured tool_calls field in the API response. The tool was NOT executed. You MUST retry using the proper tool_calls mechanism.`, + providerMetadata, + }) + return + } yield* events.publish(SessionEvent.Reasoning.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), diff --git a/packages/core/test/publish-llm-event.test.ts b/packages/core/test/publish-llm-event.test.ts new file mode 100644 index 000000000000..8d515378b4b2 --- /dev/null +++ b/packages/core/test/publish-llm-event.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from "bun:test" +import { findToolCallName } from "../src/session/runner/publish-llm-event" + +describe("findToolCallName", () => { + test("detects with ", () => { + const text = ` + + +/home/grishberg/projects/java/opengl_kbd_render/cad3d/src/main/java/com/github/grishberg/javascad/StlValidator.java + + +snapTolerance|countNakedEdges|validateAndRepair + + +` + expect(findToolCallName(text)).toBe("grep") + }) + + test("detects bare without wrapper", () => { + expect(findToolCallName("")).toBe("read") + }) + + test("detects on one line", () => { + expect(findToolCallName("/tmp/x")).toBe("write") + }) + + test("detects tool call mixed with reasoning before", () => { + const text = `Мне нужно прочитать файл, чтобы понять структуру. + + + +/src/main.ts + + +` + expect(findToolCallName(text)).toBe("read") + }) + + test("detects tool call mixed with reasoning after", () => { + const text = ` + +foo + + +Отлично, теперь я вижу структуру.` + expect(findToolCallName(text)).toBe("grep") + }) + + test("detects tool call surrounded by reasoning on both sides", () => { + const text = `Начну с поиска. + + +main + + +Теперь прочитаю файл.` + expect(findToolCallName(text)).toBe("grep") + }) + + test("returns null for plain text without tool calls", () => { + expect(findToolCallName("просто обычный текст без вызова инструмента")).toBeNull() + }) + + test("returns null for empty string", () => { + expect(findToolCallName("")).toBeNull() + }) + + test("returns null for without ", () => { + expect(findToolCallName("")).toBeNull() + }) + + test("detects with multiline parameter value", () => { + const text = ` + +/src/utils.ts + +function foo() { + return bar +} + + +function foo() { + return baz +} + + +` + expect(findToolCallName(text)).toBe("edit") + }) + + test("detects bare (PascalCase)", () => { + expect(findToolCallName("...")).toBe("Task") + }) + + test("detects with snake_case params", () => { + expect(findToolCallName("abc")).toBe("task") + }) +}) From 686b1a21f4892cfdd251e6874a5eba6a1be6d9a7 Mon Sep 17 00:00:00 2001 From: grishberg Date: Mon, 13 Jul 2026 20:49:46 +0300 Subject: [PATCH 2/2] docs: add build and bot config verification rules --- AGENTS.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 02b1c4cb7725..3a6b10daeb7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,6 +145,78 @@ const table = sqliteTable("session", { - Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly. +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + +## Building opencode + +- Always use `build_opencode.sh` from the repo root to build the opencode binary. Do not use `bun run build` from individual packages. +- After building, verify the binary path in `bot/config.json` (`opencode_bin_path`) matches the generated binary at `packages/opencode/dist/opencode-linux-x64/bin/opencode`. +- Check file timestamps and sizes to confirm the binary was updated. + ## V2 Session Core - Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.