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)
| 147 | // are aggregated on the Runner across all files. The returned bool is true |
| 148 | // only when the model explicitly calls task_done. |
| 149 | func (r *Runner) RunPerFile(ctx context.Context, messages []llm.Message, newPath string) (bool, error) { |
| 150 | toolReqCount := r.deps.Template.MaxToolRequestTimes |
| 151 | const maxConsecutiveEmptyRounds = 3 |
| 152 | consecutiveEmptyRounds := 0 |
| 153 | sessionID := uuid.NewString() |
| 154 | |
| 155 | // Async compression is owned by this conversation alone; the deferred |
| 156 | // cancel aborts any job still in flight when the conversation ends. |
| 157 | st := &compressionState{} |
| 158 | defer r.cancelPendingCompression(st) |
| 159 | |
| 160 | for toolReqCount > 0 { |
| 161 | select { |
| 162 | case <-ctx.Done(): |
| 163 | return false, ctx.Err() |
| 164 | default: |
| 165 | } |
| 166 | |
| 167 | toolReqCount-- |
| 168 | |
| 169 | fs := r.deps.Session.GetOrCreateFileSession(newPath) |
| 170 | rec := fs.AppendTaskRecord(session.MainTask, append([]llm.Message(nil), messages...)) |
| 171 | startTime := time.Now() |
| 172 | |
| 173 | _, llmSpan := telemetry.StartLLMSpan(ctx, r.deps.Model) |
| 174 | resp, err := r.deps.LLMClient.CompletionsWithCtx(ctx, llm.ChatRequest{ |
| 175 | Model: r.deps.Model, |
| 176 | Messages: messages, |
| 177 | Tools: r.deps.MainToolDefs, |
| 178 | MaxTokens: r.deps.Template.MaxTokens, |
| 179 | SessionID: sessionID, |
| 180 | }) |
| 181 | duration := time.Since(startTime) |
| 182 | if err != nil { |
| 183 | rec.SetError(err, duration) |
| 184 | telemetry.RecordLLMResult(llmSpan, duration, 0, err) |
| 185 | llmSpan.End() |
| 186 | telemetry.RecordLLMRequest(ctx, r.deps.Model, duration, 0, "error") |
| 187 | return false, fmt.Errorf("LLM completion error: %w", err) |
| 188 | } |
| 189 | rec.SetResponse(resp, duration) |
| 190 | totalTokens := int64(0) |
| 191 | if resp.Usage != nil { |
| 192 | totalTokens = resp.Usage.TotalTokens |
| 193 | atomic.AddInt64(&r.totalInputTokens, resp.Usage.PromptTokens) |
| 194 | atomic.AddInt64(&r.totalOutputTokens, resp.Usage.CompletionTokens) |
| 195 | atomic.AddInt64(&r.totalCacheReadTokens, resp.Usage.CacheReadTokens) |
| 196 | atomic.AddInt64(&r.totalCacheWriteTokens, resp.Usage.CacheWriteTokens) |
| 197 | } |
| 198 | telemetry.RecordLLMResult(llmSpan, duration, totalTokens, nil) |
| 199 | llmSpan.End() |
| 200 | telemetry.RecordLLMRequest(ctx, r.deps.Model, duration, totalTokens, "ok") |
| 201 | |
| 202 | content := resp.Content() |
| 203 | calls := resp.ToolCalls() |
| 204 | |
| 205 | if len(calls) == 0 { |
| 206 | fmt.Fprintf(stdout.Writer(), "[ocr] No tool calls parsed for %s, retrying...\n", newPath) |