| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "slices" |
| 8 | "time" |
| 9 | |
| 10 | "reasonix/internal/event" |
| 11 | "reasonix/internal/provider" |
| 12 | "reasonix/internal/tool" |
| 13 | ) |
| 14 | |
| 15 | func (s *Session) addWriteIntent(callID string, raw json.RawMessage) bool { |
| 16 | s.mu.Lock() |
| 17 | defer s.mu.Unlock() |
| 18 | for i := range slices.Backward(s.Messages) { |
| 19 | for j, c := range s.Messages[i].ToolCalls { |
| 20 | if c.ID != callID { |
| 21 | continue |
| 22 | } |
| 23 | calls := append([]provider.ToolCall(nil), s.Messages[i].ToolCalls...) |
| 24 | calls[j].WriteIntents = append(append([]json.RawMessage(nil), c.WriteIntents...), append(json.RawMessage(nil), raw...)) |
| 25 | s.Messages[i].ToolCalls = calls |
| 26 | s.version++ |
| 27 | s.rewriteVersion++ |
| 28 | return true |
| 29 | } |
| 30 | } |
| 31 | return false |
| 32 | } |
| 33 | |
| 34 | func (a *Agent) withWriteRecovery(ctx context.Context, call provider.ToolCall) context.Context { |
| 35 | return tool.WithWriteIntentHook(ctx, func(intent tool.FileWriteIntent) error { |
| 36 | raw, err := json.Marshal(intent) |
| 37 | if err != nil { |
| 38 | return err |
| 39 | } |
| 40 | if !a.sess.conversation.addWriteIntent(call.ID, raw) { |
| 41 | return fmt.Errorf("write intent has no durable tool call: %s", call.ID) |
| 42 | } |
| 43 | return event.EmitChecked(a.svc.sink, event.Event{Kind: event.Notice, WriteIntent: true}) |
| 44 | }) |
| 45 | } |
| 46 | |
| 47 | func (a *Agent) verifyInterruptedWrites(ctx context.Context, r *provider.InterruptedTurnRecovery) *provider.InterruptedTurnRecovery { |
| 48 | // Recovery preserves the original execution facts. Current file contents |
| 49 | // cannot prove that an earlier call succeeded, and uncertain calls never |
| 50 | // become execution barriers or synthetic successes. |
| 51 | return r |
| 52 | } |
| 53 | |
| 54 | // A terminal length limit can leave syntactically valid but incomplete args. |
| 55 | func (a *Agent) recordTruncatedToolResults(ctx context.Context, calls []provider.ToolCall) error { |
| 56 | for _, call := range calls { |
| 57 | outcome := toolOutcome{output: "error: tool was not executed because the model output reached its length limit; regenerate complete arguments", errMsg: "truncated tool arguments"} |
| 58 | committedMessage := a.buildBatchToolResult(ctx, call, outcome) |
| 59 | if err := a.emitBatchToolResult(ctx, call, outcome, committedMessage, 0, 0, false, time.Time{}); err != nil { |
| 60 | return err |
| 61 | } |
| 62 | a.sess.conversation.Add(committedMessage) |
| 63 | } |
| 64 | return nil |
| 65 | } |
| 66 |