mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 11:39:41 +03:00
fix(ai): keep system updates after pending tool results (#52426)
This commit is contained in:
committed by
GitHub
Unverified
parent
8433dd732f
commit
aa6a4f93bd
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user