Skip to content

Commit 528ca2c

Browse files
Aurelioloclaude
andcommitted
fix: address post-push review feedback from Greptile, CodeRabbit, Copilot
- Remove non-existent FAILED from executable statuses docstring - Per-turn CostRecord recording instead of single aggregate - Pass tracker as explicit parameter to _submit_cost (eliminates type: ignore) - Preserve prepared context in _handle_fatal_error on post-setup failures - Clarify MemoryError/RecursionError propagation in _record_costs docstring - Log before re-raising non-recoverable errors in prompt builder - Remove body truncation from PR review skill fetch commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0fb8035 commit 528ca2c

3 files changed

Lines changed: 92 additions & 58 deletions

File tree

.claude/skills/aurelio-review-pr/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Fetch from three GitHub API sources **in parallel** using `gh api` — **always
262262
1. **Review submissions** (top-level review bodies):
263263

264264
```bash
265-
gh api repos/OWNER/REPO/pulls/NUMBER/reviews --paginate --jq '.[] | {author: .user.login, state: .state, body: (.body // "" | if length > 15000 then .[0:15000] else . end)}'
265+
gh api repos/OWNER/REPO/pulls/NUMBER/reviews --paginate --jq '.[] | {author: .user.login, state: .state, body: (.body // "")}'
266266
```
267267

268268
Extract: author, state, body. List ALL unique authors to identify every reviewer.
@@ -272,15 +272,15 @@ Fetch from three GitHub API sources **in parallel** using `gh api` — **always
272272
2. **Inline review comments** (comments on specific lines):
273273

274274
```bash
275-
gh api repos/OWNER/REPO/pulls/NUMBER/comments --paginate --jq '.[] | {author: .user.login, path: .path, line: .line, body: (.body // "" | if length > 5000 then .[0:5000] else . end)}'
275+
gh api repos/OWNER/REPO/pulls/NUMBER/comments --paginate --jq '.[] | {author: .user.login, path: .path, line: .line, body: (.body // "")}'
276276
```
277277

278278
Extract: author, file path, line number, body. **Include ALL authors.**
279279

280280
3. **Issue-level comments** (general PR comments, e.g. CodeRabbit walkthrough):
281281

282282
```bash
283-
gh api repos/OWNER/REPO/issues/NUMBER/comments --paginate --jq '.[] | {author: .user.login, body: (.body // "" | if length > 15000 then .[0:15000] else . end)}'
283+
gh api repos/OWNER/REPO/issues/NUMBER/comments --paginate --jq '.[] | {author: .user.login, body: (.body // "")}'
284284
```
285285

286286
Extract: author, body (look for actionable items, not just summaries). **Include ALL authors.**
@@ -289,7 +289,7 @@ After fetching, **enumerate all unique external reviewers** found across all thr
289289

290290
**Important:** Use `gh api` with `--jq` for filtering fields only (not filtering authors). Keep it simple and robust — no complex Python scripts to parse JSON.
291291

292-
**Important:** When review bodies are large (e.g. CodeRabbit's review with embedded outside-diff comments), fetch the **full body** without truncation. Use `head -c` with a generous limit (e.g. 15000 chars) rather than `--jq '.body[0:500]'` truncation. Outside-diff comments are typically at the top of the review body.
292+
**Important:** When review bodies are large (e.g. CodeRabbit's review with embedded outside-diff comments), fetch the **full body** without truncation. Outside-diff comments are typically at the top of the review body.
293293

294294
## Phase 5: Consolidate and triage
295295

src/ai_company/engine/agent_engine.py

Lines changed: 81 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ai_company.engine.loop_protocol import (
1616
ExecutionResult,
1717
TerminationReason,
18+
TurnRecord,
1819
)
1920
from ai_company.engine.prompt import SystemPrompt, build_system_prompt
2021
from ai_company.engine.react_loop import ReactLoop
@@ -33,7 +34,7 @@
3334
EXECUTION_ENGINE_TASK_TRANSITION,
3435
)
3536
from ai_company.providers.enums import MessageRole
36-
from ai_company.providers.models import ChatMessage, TokenUsage
37+
from ai_company.providers.models import ChatMessage
3738
from ai_company.tools.invoker import ToolInvoker
3839

3940
if TYPE_CHECKING:
@@ -50,8 +51,8 @@
5051
_EXECUTABLE_STATUSES = frozenset({TaskStatus.ASSIGNED, TaskStatus.IN_PROGRESS})
5152
"""Task statuses the engine will accept for execution.
5253
53-
CREATED tasks lack an assignee; terminal statuses (COMPLETED, CANCELLED,
54-
FAILED) and BLOCKED/IN_REVIEW are not executable.
54+
CREATED tasks lack an assignee; terminal statuses (COMPLETED, CANCELLED)
55+
and BLOCKED/IN_REVIEW are not executable.
5556
"""
5657

5758

@@ -146,15 +147,25 @@ async def run(
146147
)
147148

148149
start = time.monotonic()
150+
ctx: AgentContext | None = None
151+
system_prompt: SystemPrompt | None = None
149152
try:
150-
return await self._execute(
153+
ctx, system_prompt = self._prepare_context(
151154
identity=identity,
152155
task=task,
153156
agent_id=agent_id,
154157
task_id=task_id,
155-
completion_config=completion_config,
156158
max_turns=max_turns,
157159
memory_messages=memory_messages,
160+
)
161+
return await self._execute(
162+
identity=identity,
163+
task=task,
164+
agent_id=agent_id,
165+
task_id=task_id,
166+
completion_config=completion_config,
167+
ctx=ctx,
168+
system_prompt=system_prompt,
158169
start=start,
159170
)
160171
except MemoryError, RecursionError:
@@ -174,6 +185,8 @@ async def run(
174185
agent_id=agent_id,
175186
task_id=task_id,
176187
duration_seconds=time.monotonic() - start,
188+
ctx=ctx,
189+
system_prompt=system_prompt,
177190
)
178191

179192
async def _execute( # noqa: PLR0913
@@ -184,19 +197,11 @@ async def _execute( # noqa: PLR0913
184197
agent_id: str,
185198
task_id: str,
186199
completion_config: CompletionConfig | None,
187-
max_turns: int,
188-
memory_messages: tuple[ChatMessage, ...],
200+
ctx: AgentContext,
201+
system_prompt: SystemPrompt,
189202
start: float,
190203
) -> AgentRunResult:
191-
"""Prepare context, run the execution loop, record costs, and build result."""
192-
ctx, system_prompt = self._prepare_context(
193-
identity=identity,
194-
task=task,
195-
agent_id=agent_id,
196-
task_id=task_id,
197-
max_turns=max_turns,
198-
memory_messages=memory_messages,
199-
)
204+
"""Run the execution loop, record costs, and build result."""
200205
budget_checker = _make_budget_checker(task)
201206
tool_invoker = self._make_tool_invoker()
202207

@@ -369,11 +374,15 @@ async def _record_costs(
369374
agent_id: str,
370375
task_id: str,
371376
) -> None:
372-
"""Record accumulated costs to the CostTracker if available.
377+
"""Record per-turn costs to the CostTracker if available.
373378
374-
Cost recording failures are logged but do not affect the
375-
execution result — a successful run is never downgraded to
376-
an error because of a recording failure.
379+
Each turn produces its own ``CostRecord``, preserving per-call
380+
granularity. Turns with zero cost and zero tokens are skipped.
381+
382+
Recording failures for regular exceptions are logged but do not
383+
affect the execution result. ``MemoryError`` and
384+
``RecursionError`` propagate unconditionally as non-recoverable
385+
system errors.
377386
"""
378387
if self._cost_tracker is None:
379388
logger.debug(
@@ -384,45 +393,56 @@ async def _record_costs(
384393
)
385394
return
386395

387-
usage = result.context.accumulated_cost
388-
# Skip only when provably nothing happened (zero cost and zero
389-
# tokens); a run with tokens but zero cost (e.g., a free-tier
390-
# provider) is still recorded.
391-
if (
392-
usage.cost_usd <= 0.0
393-
and usage.input_tokens == 0
394-
and usage.output_tokens == 0
395-
):
396-
logger.debug(
397-
EXECUTION_ENGINE_COST_SKIPPED,
396+
tracker = self._cost_tracker
397+
398+
for turn in result.turns:
399+
# Skip only when provably nothing happened (zero cost and
400+
# zero tokens); a turn with tokens but zero cost (e.g., a
401+
# free-tier provider) is still recorded.
402+
if (
403+
turn.cost_usd <= 0.0
404+
and turn.input_tokens == 0
405+
and turn.output_tokens == 0
406+
):
407+
logger.debug(
408+
EXECUTION_ENGINE_COST_SKIPPED,
409+
agent_id=agent_id,
410+
task_id=task_id,
411+
turn_number=turn.turn_number,
412+
reason="zero cost and zero tokens",
413+
)
414+
continue
415+
416+
record = CostRecord(
398417
agent_id=agent_id,
399418
task_id=task_id,
400-
reason="zero cost and zero tokens",
419+
provider=identity.model.provider,
420+
model=identity.model.model_id,
421+
input_tokens=turn.input_tokens,
422+
output_tokens=turn.output_tokens,
423+
cost_usd=turn.cost_usd,
424+
timestamp=datetime.now(UTC),
425+
)
426+
await self._submit_cost(
427+
record,
428+
turn,
429+
agent_id,
430+
task_id,
431+
tracker=tracker,
401432
)
402-
return
403-
404-
record = CostRecord(
405-
agent_id=agent_id,
406-
task_id=task_id,
407-
provider=identity.model.provider,
408-
model=identity.model.model_id,
409-
input_tokens=usage.input_tokens,
410-
output_tokens=usage.output_tokens,
411-
cost_usd=usage.cost_usd,
412-
timestamp=datetime.now(UTC),
413-
)
414-
await self._submit_cost(record, usage, agent_id, task_id)
415433

416434
async def _submit_cost(
417435
self,
418436
record: CostRecord,
419-
usage: TokenUsage,
437+
turn: TurnRecord,
420438
agent_id: str,
421439
task_id: str,
440+
*,
441+
tracker: CostTracker,
422442
) -> None:
423443
"""Submit a cost record to the tracker, logging failures."""
424444
try:
425-
await self._cost_tracker.record(record) # type: ignore[union-attr]
445+
await tracker.record(record)
426446
except MemoryError, RecursionError:
427447
logger.error(
428448
EXECUTION_ENGINE_COST_FAILED,
@@ -438,17 +458,17 @@ async def _submit_cost(
438458
agent_id=agent_id,
439459
task_id=task_id,
440460
error=f"{type(exc).__name__}: {exc}",
441-
cost_usd=usage.cost_usd,
442-
input_tokens=usage.input_tokens,
443-
output_tokens=usage.output_tokens,
461+
cost_usd=turn.cost_usd,
462+
input_tokens=turn.input_tokens,
463+
output_tokens=turn.output_tokens,
444464
)
445465
return
446466

447467
logger.info(
448468
EXECUTION_ENGINE_COST_RECORDED,
449469
agent_id=agent_id,
450470
task_id=task_id,
451-
cost_usd=usage.cost_usd,
471+
cost_usd=turn.cost_usd,
452472
)
453473

454474
def _handle_fatal_error( # noqa: PLR0913
@@ -460,9 +480,16 @@ def _handle_fatal_error( # noqa: PLR0913
460480
agent_id: str,
461481
task_id: str,
462482
duration_seconds: float,
483+
ctx: AgentContext | None = None,
484+
system_prompt: SystemPrompt | None = None,
463485
) -> AgentRunResult:
464486
"""Build an error ``AgentRunResult`` when the execution pipeline fails.
465487
488+
When ``ctx`` and ``system_prompt`` are provided (i.e. context
489+
preparation succeeded before the failure), they are preserved in
490+
the error result so that accumulated state (conversation, task
491+
transition) is not lost.
492+
466493
If constructing the error result itself fails, the original
467494
exception is re-raised so it is never silently lost.
468495
"""
@@ -475,13 +502,13 @@ def _handle_fatal_error( # noqa: PLR0913
475502
)
476503

477504
try:
478-
ctx = AgentContext.from_identity(identity, task=task)
505+
error_ctx = ctx or AgentContext.from_identity(identity, task=task)
479506
error_execution = ExecutionResult(
480-
context=ctx,
507+
context=error_ctx,
481508
termination_reason=TerminationReason.ERROR,
482509
error_message=error_msg,
483510
)
484-
error_prompt = SystemPrompt(
511+
error_prompt = system_prompt or SystemPrompt(
485512
content="",
486513
template_version="error",
487514
estimated_tokens=0,

src/ai_company/engine/prompt.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ def build_system_prompt( # noqa: PLR0913
218218
except PromptBuildError:
219219
raise # Already logged by inner functions.
220220
except MemoryError, RecursionError:
221+
logger.error(
222+
PROMPT_BUILD_ERROR,
223+
agent_id=str(agent.id),
224+
agent_name=agent.name,
225+
error="non-recoverable error building prompt",
226+
exc_info=True,
227+
)
221228
raise
222229
except Exception as exc:
223230
logger.exception(

0 commit comments

Comments
 (0)