package agent
import "codey/internal/llm"
// interruptedResult is what a tool call that never finished sends back. It is
// an error result rather than empty text because that is what happened, and
// because the model reads it: told the call was stopped, it asks what to do
// instead of assuming the tool returned nothing.
const interruptedResult = "[interrupted: the turn was stopped before this tool finished]"
// answerDanglingToolCalls closes out a conversation that ends on an assistant
// turn still asking for tools. Every tool_use must be followed by a
// tool_result — the API rejects the whole request over a single missing one,
// on every request that follows, which is why an interrupt that leaves one
// behind poisons the session rather than merely ending a turn.
//
// done supplies the results of calls that did finish before the stop, so the
// work already paid for is kept; the rest are marked interrupted. All of them
// go back in one user message, as they would have unharmed.
//
// Hosted calls (server_tool_use) are deliberately skipped: the API ran them
// and supplied the result itself, and a local tool_result for one of those ids
// is an error of its own.
func answerDanglingToolCalls(msgs []llm.Message, done []llm.ContentBlock) []llm.Message {
if len(msgs) == 0 {
return msgs
}
last := msgs[len(msgs)-1]
if last.Role != llm.RoleAssistant {
return msgs
}
byID := make(map[string]llm.ContentBlock, len(done))
for _, r := range done {
byID[r.ToolUseID] = r
}
var results []llm.ContentBlock
for _, b := range last.Content {
if b.Type != llm.BlockToolUse {
continue
}
if r, ok := byID[b.ID]; ok {
results = append(results, r)
continue
}
results = append(results, llm.ToolResultBlock(b.ID, interruptedResult, true))
}
if len(results) == 0 {
return msgs
}
return append(msgs, llm.Message{Role: llm.RoleUser, Content: results})
}
// closeInterruptedTurn applies the repair to the live conversation, with the
// results dispatch managed to produce before it was stopped.
func (a *Agent) closeInterruptedTurn(done []llm.ContentBlock) {
h := a.state.History()
if repaired := answerDanglingToolCalls(h, done); len(repaired) != len(h) {
a.state.Replace(repaired)
}
}