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