Vectrum
Sign In
62 lines · 2.2 KB · Go
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)
	}
}