Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 27 additions & 7 deletions packages/core/src/session/runner/publish-llm-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ type Input = {
readonly model: ModelV2.Ref
}

/** Detect XML-style tool calls like `<function=grep>` or `<tool_call>\n<function=grep>` emitted as plain text. */
export const findToolCallName = (text: string): string | null => {
const funcMatch = text.match(/<function=(\w+)>/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) => {
Expand Down Expand Up @@ -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
}
Expand All @@ -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(),
Expand Down
98 changes: 98 additions & 0 deletions packages/core/test/publish-llm-event.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test"
import { findToolCallName } from "../src/session/runner/publish-llm-event"

describe("findToolCallName", () => {
test("detects <tool_call> with <function=grep>", () => {
const text = `<tool_call>
<function=grep>
<parameter=path>
/home/grishberg/projects/java/opengl_kbd_render/cad3d/src/main/java/com/github/grishberg/javascad/StlValidator.java
</parameter>
<parameter=pattern>
snapTolerance|countNakedEdges|validateAndRepair
</parameter>
</function>
</tool_call>`
expect(findToolCallName(text)).toBe("grep")
})

test("detects bare <function=read> without <tool_call> wrapper", () => {
expect(findToolCallName("<function=read>")).toBe("read")
})

test("detects <tool_call><function=write> on one line", () => {
expect(findToolCallName("<tool_call><function=write><parameter=path>/tmp/x</parameter></function></tool_call>")).toBe("write")
})

test("detects tool call mixed with reasoning before", () => {
const text = `Мне нужно прочитать файл, чтобы понять структуру.
<tool_call>
<function=read>
<parameter=filePath>
/src/main.ts
</parameter>
</function>
</tool_call>`
expect(findToolCallName(text)).toBe("read")
})

test("detects tool call mixed with reasoning after", () => {
const text = `<tool_call>
<function=grep>
<parameter=pattern>foo</parameter>
</function>
</tool_call>
Отлично, теперь я вижу структуру.`
expect(findToolCallName(text)).toBe("grep")
})

test("detects tool call surrounded by reasoning on both sides", () => {
const text = `Начну с поиска.
<tool_call>
<function=grep>
<parameter=pattern>main</parameter>
</function>
</tool_call>
Теперь прочитаю файл.`
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 <tool_call> without <function>", () => {
expect(findToolCallName("<tool_call></tool_call>")).toBeNull()
})

test("detects <function=edit> with multiline parameter value", () => {
const text = `<tool_call>
<function=edit>
<parameter=filePath>/src/utils.ts</parameter>
<parameter=oldString>
function foo() {
return bar
}
</parameter>
<parameter=newString>
function foo() {
return baz
}
</parameter>
</function>
</tool_call>`
expect(findToolCallName(text)).toBe("edit")
})

test("detects bare <function=Task> (PascalCase)", () => {
expect(findToolCallName("<function=Task><parameter=prompt>...</parameter></function>")).toBe("Task")
})

test("detects <function=task> with snake_case params", () => {
expect(findToolCallName("<function=task><parameter=session_id>abc</parameter></function>")).toBe("task")
})
})
Loading