RunPerFile drives the main LLM conversation loop for a single file. It sends messages with the configured tool definitions, executes any tool calls returned by the model, and collects review comments until task_done is called or limits are reached. Token usage and warnings are aggregated on the Runn
(ctx context.Context, messages []llm.Message, newPath string)
| 175 | // MainLoopStop return classifies a non-completed, non-error stop at its trigger |
| 176 | // point so the caller never has to infer the cause from text or context state. |
| 177 | func (r *Runner) RunPerFile(ctx context.Context, messages []llm.Message, newPath string) (bool, MainLoopStop, error) { |
| 178 | toolReqCount := r.deps.Template.MaxToolRequestTimes |
| 179 | const maxConsecutiveEmptyRounds = 3 |
| 180 | consecutiveEmptyRounds := 0 |
| 181 | sessionID := uuid.NewString() |
| 182 | |
| 183 | // Async compression is owned by this conversation alone; the deferred |
| 184 | // cancel aborts any job still in flight when the conversation ends. |
| 185 | st := &compressionState{} |
| 186 | defer r.cancelPendingCompression(st) |
| 187 | |
| 188 | // stop defaults to StopMaxRounds: if the for-loop exits because toolReqCount |
| 189 | // reached zero, the run stopped on the round budget. The empty-round and |
| 190 | // compression breaks overwrite it at their trigger points. |
| 191 | stop := StopMaxRounds |
| 192 | for toolReqCount > 0 { |
| 193 | select { |
| 194 | case <-ctx.Done(): |
| 195 | return false, StopNone, ctx.Err() |
| 196 | default: |
| 197 | } |
| 198 | |
| 199 | toolReqCount-- |
| 200 | |
| 201 | fs := r.deps.Session.GetOrCreateFileSession(newPath) |
| 202 | rec := fs.AppendTaskRecord(session.MainTask, append([]llm.Message(nil), messages...)) |
| 203 | startTime := time.Now() |
| 204 | |
| 205 | _, llmSpan := telemetry.StartLLMSpan(ctx, r.deps.Model) |
| 206 | resp, err := r.deps.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{ |
| 207 | Model: r.deps.Model, |
| 208 | Messages: messages, |
| 209 | Tools: r.deps.MainToolDefs, |
| 210 | MaxTokens: r.deps.Template.CompletionTokenLimit(), |
| 211 | SessionID: sessionID, |
| 212 | }) |
| 213 | duration := time.Since(startTime) |
| 214 | if err != nil { |
| 215 | rec.SetError(err, duration) |
| 216 | telemetry.RecordLLMResult(llmSpan, duration, 0, err) |
| 217 | llmSpan.End() |
| 218 | telemetry.RecordLLMRequest(ctx, r.deps.Model, duration, 0, "error") |
| 219 | return false, StopNone, fmt.Errorf("LLM completion error: %w", err) |
| 220 | } |
| 221 | rec.SetResponse(resp, duration) |
| 222 | totalTokens := int64(0) |
| 223 | if resp.Usage != nil { |
| 224 | totalTokens = resp.Usage.TotalTokens |
| 225 | atomic.AddInt64(&r.totalInputTokens, resp.Usage.PromptTokens) |
| 226 | atomic.AddInt64(&r.totalOutputTokens, resp.Usage.CompletionTokens) |
| 227 | atomic.AddInt64(&r.totalCacheReadTokens, resp.Usage.CacheReadTokens) |
| 228 | atomic.AddInt64(&r.totalCacheWriteTokens, resp.Usage.CacheWriteTokens) |
| 229 | } |
| 230 | telemetry.RecordLLMResult(llmSpan, duration, totalTokens, nil) |
| 231 | llmSpan.End() |
| 232 | telemetry.RecordLLMRequest(ctx, r.deps.Model, duration, totalTokens, "ok") |
| 233 | |
| 234 | content := resp.Content() |