package agent import "github.com/johannSo/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) } }