fix(ai): keep system updates after pending tool results (#52426)

This commit is contained in:
Aiden Cline
2026-10-01 02:46:37 -05:00
committed by GitHub
Unverified
parent 8433dd732f
commit aa6a4f93bd
4 changed files with 72 additions and 8 deletions
+15 -2
View File
@@ -6,10 +6,17 @@ const MISSING_TOOL_RESULT = "Tool result missing"
export function normalizeToolHistory(messages: ReadonlyArray<Message>) {
const normalized: Message[] = []
const pending = new Map<string, ToolCallPart>()
// System updates cannot sit between a tool call and its results, so they wait until every pending call is answered.
const held: Message[] = []
const releaseHeld = () => {
if (pending.size > 0) return
normalized.push(...held)
held.length = 0
}
const appendMissingResults = () => {
if (pending.size === 0) return
normalized.push(missingToolResults(pending.values()))
if (pending.size > 0) normalized.push(missingToolResults(pending.values()))
pending.clear()
releaseHeld()
}
for (const message of messages) {
@@ -18,6 +25,12 @@ export function normalizeToolHistory(messages: ReadonlyArray<Message>) {
if (message.role === "tool") {
const tool = normalizeToolMessage(message, pending)
if (tool) normalized.push(tool)
releaseHeld()
continue
}
if (message.role === "system" && pending.size > 0) {
held.push(message)
continue
}
+2 -2
View File
@@ -147,7 +147,7 @@ describe("Anthropic Messages effort updates", () => {
}),
)
it.effect("accepts a marker between a tool call and its result", () =>
it.effect("moves a marker between a tool call and its result after the result", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
@@ -166,8 +166,8 @@ describe("Anthropic Messages effort updates", () => {
expect(prepared.body.messages).toEqual([
{ role: "user", content: [{ type: "text", text: "Weather?" }] },
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{ role: "system", content: [], output_config: { effort: "low" } },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: '{"temp":72}' }] },
{ role: "system", content: [], output_config: { effort: "low" } },
])
}),
)
@@ -588,9 +588,9 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("rejects a system update between a local tool call and its result", () =>
it.effect("moves a system update between a local tool call and its result after the result", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
const prepared = yield* compileRequest(
LLM.request({
model: opus48,
messages: [
@@ -601,9 +601,13 @@ describe("Anthropic Messages route", () => {
],
cache: "none",
}),
).pipe(Effect.flip)
)
expect(error.message).toContain("system updates cannot split a local tool call from its tool result")
expect(prepared.body.messages.slice(1)).toEqual([
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: '"Done."' }] },
{ role: "system", content: [{ type: "text", text: "Too early.", cache_control: undefined }] },
])
}),
)
+47
View File
@@ -112,4 +112,51 @@ describe("tool history normalization", () => {
expect(normalized[1]?.content[0]).toMatchObject({ name: "lookup", namespace: undefined })
})
test("moves system updates after the results of pending calls", () => {
const calls = Message.assistant([toolCall("first"), toolCall("second")])
const first = toolResult("first", "one", "first", "text")
const second = toolResult("second", "two", "second", "text")
const update = Message.system("First update.")
const later = Message.system("Second update.")
const user = Message.user("Continue.")
const missing = (id: string) =>
Message.tool(ToolResultPart.make({ id, name: id, result: "Tool result missing", resultType: "error" }))
expect(normalizeToolHistory([calls, first, update, later, second, user])).toEqual([
calls,
first,
second,
update,
later,
user,
])
expect(normalizeToolHistory([Message.assistant(toolCall("first")), update, user])).toEqual([
Message.assistant(toolCall("first")),
missing("first"),
update,
user,
])
expect(normalizeToolHistory([Message.assistant(toolCall("first")), update])).toEqual([
Message.assistant(toolCall("first")),
missing("first"),
update,
])
})
test("moves effort updates after the results of pending calls", () => {
const call = Message.assistant(toolCall("first"))
const result = toolResult("first", "one", "first", "text")
const effort = Message.effort({ effort: "low", previous: "high" })
expect(normalizeToolHistory([call, effort, result])).toEqual([call, result, effort])
})
test("keeps system updates in place when no call is pending", () => {
const history = [Message.assistant(toolCall("first")), toolResult("first", "one", "first", "text")]
const update = Message.system("Update.")
const input = [Message.user("Start."), update, ...history, update, Message.user("Continue.")]
expect(normalizeToolHistory(input)).toBe(input)
})
})