Skip to content

Commit e75351b

Browse files
committed
fix(core): detect XML-style malformed tool calls in raw text stream
2 parents 04cfb85 + e4e0437 commit e75351b

2 files changed

Lines changed: 26 additions & 1 deletion

File tree

packages/core/src/session/runner/publish-llm-event.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
115115

116116
const text = fragments("text", (textID, value) =>
117117
Effect.gen(function* () {
118-
const toolCallMatch = value.match(
118+
const bracketToolCallMatch = value.match(
119119
/\[Assistant tool call\]\s*:\s*(\w+)\s*\(/i,
120120
)
121+
const xmlToolCallMatch = value.match(/<\w+>[\s\S]*?"name"\s*:\s*"([^"]+)"/u)
122+
const toolCallMatch = bracketToolCallMatch ?? xmlToolCallMatch
121123
if (toolCallMatch) {
122124
textBasedToolCall = true
123125
yield* events.publish(SessionEvent.Text.Ended, {

packages/core/test/session-runner-tool-events.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,29 @@ test("binary failure emits no success event", async () => {
112112
expect(published.some((event) => event.type === "session.next.tool.failed.1")).toBe(true)
113113
})
114114

115+
test("xml-style tool call in text emits error and sets hasTextBasedToolCall", async () => {
116+
const { published, publisher } = capture()
117+
const textID = "text-0"
118+
119+
await Effect.runPromise(publisher.publish(LLMEvent.textStart({ id: textID })))
120+
await Effect.runPromise(
121+
publisher.publish(
122+
LLMEvent.textDelta({
123+
id: textID,
124+
text: '<tool_call>\ntool_use\n{"name": "read", "path": "foo.txt"}\n</tool_call>',
125+
}),
126+
),
127+
)
128+
await Effect.runPromise(publisher.publish(LLMEvent.textEnd({ id: textID })))
129+
130+
const textEnded = published.find((event) => event.type === "session.next.text.ended.1")
131+
expect(textEnded).toBeDefined()
132+
expect((textEnded!.data as any).text).toContain("[ERROR]")
133+
expect((textEnded!.data as any).text).toContain("read")
134+
expect((textEnded!.data as any).text).not.toContain("tool_use")
135+
expect(publisher.hasTextBasedToolCall()).toBe(true)
136+
})
137+
115138
test("old success event data containing result still decodes", () => {
116139
const decoded = Schema.decodeUnknownSync(SessionEvent.Tool.Success.data)({
117140
sessionID,

0 commit comments

Comments
 (0)