codexapi 0.6.7__tar.gz → 0.6.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codexapi-0.6.7/src/codexapi.egg-info → codexapi-0.6.8}/PKG-INFO +1 -1
- {codexapi-0.6.7 → codexapi-0.6.8}/pyproject.toml +1 -1
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/__init__.py +1 -1
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/task.py +81 -50
- {codexapi-0.6.7 → codexapi-0.6.8/src/codexapi.egg-info}/PKG-INFO +1 -1
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/SOURCES.txt +2 -1
- codexapi-0.6.8/tests/test_task_progress.py +74 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/LICENSE +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/README.md +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/setup.cfg +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/__main__.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/agent.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/cli.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/foreach.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/gh_integration.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/pushover.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/ralph.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/rate_limits.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/science.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/taskfile.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/watch.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/welfare.py +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/dependency_links.txt +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/entry_points.txt +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/requires.txt +0 -0
- {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/top_level.txt +0 -0
|
@@ -486,6 +486,27 @@ class Task:
|
|
|
486
486
|
"""Ask the agent to summarize remaining issues after retries."""
|
|
487
487
|
return _failure_prompt(error)
|
|
488
488
|
|
|
489
|
+
def _estimate_progress(self, agent_output, check_output):
|
|
490
|
+
"""Run a progress estimate and return parsed data or an error string."""
|
|
491
|
+
try:
|
|
492
|
+
return (
|
|
493
|
+
estimate(
|
|
494
|
+
self.prompt,
|
|
495
|
+
agent_output or "",
|
|
496
|
+
check_output or "",
|
|
497
|
+
self.cwd,
|
|
498
|
+
self._yolo,
|
|
499
|
+
self._flags,
|
|
500
|
+
self._progress_total,
|
|
501
|
+
),
|
|
502
|
+
None,
|
|
503
|
+
)
|
|
504
|
+
except Exception as exc:
|
|
505
|
+
error = _single_line(str(exc))
|
|
506
|
+
if not error:
|
|
507
|
+
error = exc.__class__.__name__
|
|
508
|
+
return None, error
|
|
509
|
+
|
|
489
510
|
def __call__(self, debug=False, progress=False):
|
|
490
511
|
"""Run the task with checker-driven retries.
|
|
491
512
|
If debug is True, log debug messages.
|
|
@@ -499,29 +520,24 @@ class Task:
|
|
|
499
520
|
|
|
500
521
|
progress_updates = progress or self._progress_updates
|
|
501
522
|
self._progress_enabled = progress
|
|
523
|
+
start_time = time.monotonic()
|
|
524
|
+
self._progress_start = start_time
|
|
502
525
|
if progress_updates:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
self.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
self._progress_total,
|
|
519
|
-
remaining,
|
|
520
|
-
None,
|
|
521
|
-
)
|
|
522
|
-
else:
|
|
523
|
-
start_time = time.monotonic()
|
|
524
|
-
self._progress_start = start_time
|
|
526
|
+
estimate_result, estimate_error = self._estimate_progress("", "")
|
|
527
|
+
if estimate_result is not None:
|
|
528
|
+
remaining, _summary = estimate_result
|
|
529
|
+
self._progress_total = remaining
|
|
530
|
+
self.on_progress(
|
|
531
|
+
0,
|
|
532
|
+
self.max_iterations,
|
|
533
|
+
self._progress_total,
|
|
534
|
+
remaining,
|
|
535
|
+
None,
|
|
536
|
+
)
|
|
537
|
+
elif debug:
|
|
538
|
+
_logger.debug(
|
|
539
|
+
"Skipping initial progress update: %s", estimate_error
|
|
540
|
+
)
|
|
525
541
|
|
|
526
542
|
# Start with the initial prompt
|
|
527
543
|
output = self.agent(self.prompt)
|
|
@@ -541,37 +557,52 @@ class Task:
|
|
|
541
557
|
check_output = self.last_check_output
|
|
542
558
|
if self.check_skipped:
|
|
543
559
|
check_output = "Verification skipped."
|
|
544
|
-
|
|
545
|
-
|
|
560
|
+
progress_data = None
|
|
561
|
+
estimate_result, estimate_error = self._estimate_progress(
|
|
546
562
|
self.last_output or "",
|
|
547
563
|
check_output or "",
|
|
548
|
-
self.cwd,
|
|
549
|
-
self._yolo,
|
|
550
|
-
self._flags,
|
|
551
|
-
self._progress_total,
|
|
552
|
-
)
|
|
553
|
-
total_estimate = self._progress_total
|
|
554
|
-
if total_estimate is None or remaining > total_estimate:
|
|
555
|
-
total_estimate = remaining
|
|
556
|
-
self._progress_total = total_estimate
|
|
557
|
-
elapsed = _format_elapsed(time.monotonic() - start_time)
|
|
558
|
-
status_prefix = (
|
|
559
|
-
f"[{_format_turns(iteration, self.max_iterations)} @ {elapsed}]"
|
|
560
|
-
)
|
|
561
|
-
is_final = not error or (
|
|
562
|
-
self.max_iterations and iteration >= self.max_iterations
|
|
563
|
-
)
|
|
564
|
-
if is_final:
|
|
565
|
-
marker = "✅" if not error else "❌"
|
|
566
|
-
summary = f"{marker} {summary}".strip()
|
|
567
|
-
status_line = f"{status_prefix}: {summary}".rstrip()
|
|
568
|
-
self.on_progress(
|
|
569
|
-
iteration,
|
|
570
|
-
self.max_iterations,
|
|
571
|
-
total_estimate,
|
|
572
|
-
remaining,
|
|
573
|
-
status_line,
|
|
574
564
|
)
|
|
565
|
+
if estimate_result is not None:
|
|
566
|
+
remaining, summary = estimate_result
|
|
567
|
+
total_estimate = self._progress_total
|
|
568
|
+
if total_estimate is None or remaining > total_estimate:
|
|
569
|
+
total_estimate = remaining
|
|
570
|
+
self._progress_total = total_estimate
|
|
571
|
+
progress_data = (total_estimate, remaining, summary)
|
|
572
|
+
else:
|
|
573
|
+
total_estimate = self._progress_total
|
|
574
|
+
if total_estimate is None:
|
|
575
|
+
if debug:
|
|
576
|
+
_logger.debug(
|
|
577
|
+
"Skipping progress update: %s", estimate_error
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
summary = f"Progress estimate unavailable: {estimate_error}"
|
|
581
|
+
progress_data = (
|
|
582
|
+
total_estimate,
|
|
583
|
+
total_estimate,
|
|
584
|
+
summary,
|
|
585
|
+
)
|
|
586
|
+
if progress_data is not None:
|
|
587
|
+
total_estimate, remaining, summary = progress_data
|
|
588
|
+
elapsed = _format_elapsed(time.monotonic() - start_time)
|
|
589
|
+
status_prefix = (
|
|
590
|
+
f"[{_format_turns(iteration, self.max_iterations)} @ {elapsed}]"
|
|
591
|
+
)
|
|
592
|
+
is_final = not error or (
|
|
593
|
+
self.max_iterations and iteration >= self.max_iterations
|
|
594
|
+
)
|
|
595
|
+
if is_final:
|
|
596
|
+
marker = "✅" if not error else "❌"
|
|
597
|
+
summary = f"{marker} {summary}".strip()
|
|
598
|
+
status_line = f"{status_prefix}: {summary}".rstrip()
|
|
599
|
+
self.on_progress(
|
|
600
|
+
iteration,
|
|
601
|
+
self.max_iterations,
|
|
602
|
+
total_estimate,
|
|
603
|
+
remaining,
|
|
604
|
+
status_line,
|
|
605
|
+
)
|
|
575
606
|
if not error:
|
|
576
607
|
summary = self.agent(self.success_prompt())
|
|
577
608
|
if debug:
|
|
@@ -20,4 +20,5 @@ src/codexapi.egg-info/SOURCES.txt
|
|
|
20
20
|
src/codexapi.egg-info/dependency_links.txt
|
|
21
21
|
src/codexapi.egg-info/entry_points.txt
|
|
22
22
|
src/codexapi.egg-info/requires.txt
|
|
23
|
-
src/codexapi.egg-info/top_level.txt
|
|
23
|
+
src/codexapi.egg-info/top_level.txt
|
|
24
|
+
tests/test_task_progress.py
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
7
|
+
|
|
8
|
+
from codexapi.task import Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _FakeAgent:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.thread_id = "thread-123"
|
|
14
|
+
|
|
15
|
+
def __call__(self, prompt):
|
|
16
|
+
return "ok"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _FakePushover:
|
|
20
|
+
def ensure_ready(self, announce=True):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
def send(self, title, message):
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ImmediateSuccessTask(Task):
|
|
28
|
+
def __init__(self):
|
|
29
|
+
super().__init__("do the thing", max_iterations=3)
|
|
30
|
+
self.agent = _FakeAgent()
|
|
31
|
+
self._pushover = _FakePushover()
|
|
32
|
+
self.set_up_called = False
|
|
33
|
+
self.tear_down_called = False
|
|
34
|
+
|
|
35
|
+
def set_up(self):
|
|
36
|
+
self.set_up_called = True
|
|
37
|
+
|
|
38
|
+
def tear_down(self):
|
|
39
|
+
self.tear_down_called = True
|
|
40
|
+
|
|
41
|
+
def check(self, output=None):
|
|
42
|
+
self.last_check_output = '{"success": true, "reason": "ok"}'
|
|
43
|
+
self.check_skipped = False
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def notify_pushover(self, result):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TaskProgressEstimateFailureTests(unittest.TestCase):
|
|
51
|
+
def test_progress_does_not_crash_when_initial_estimate_fails(self):
|
|
52
|
+
task = _ImmediateSuccessTask()
|
|
53
|
+
with patch("codexapi.task.estimate", side_effect=RuntimeError("bad json")):
|
|
54
|
+
result = task(progress=True)
|
|
55
|
+
self.assertTrue(result.success)
|
|
56
|
+
self.assertEqual(result.iterations, 1)
|
|
57
|
+
self.assertTrue(task.set_up_called)
|
|
58
|
+
self.assertTrue(task.tear_down_called)
|
|
59
|
+
|
|
60
|
+
def test_progress_does_not_crash_when_later_estimate_fails(self):
|
|
61
|
+
task = _ImmediateSuccessTask()
|
|
62
|
+
with patch(
|
|
63
|
+
"codexapi.task.estimate",
|
|
64
|
+
side_effect=[(5, "initial"), RuntimeError("bad json")],
|
|
65
|
+
):
|
|
66
|
+
result = task(progress=True)
|
|
67
|
+
self.assertTrue(result.success)
|
|
68
|
+
self.assertEqual(result.iterations, 1)
|
|
69
|
+
self.assertTrue(task.set_up_called)
|
|
70
|
+
self.assertTrue(task.tear_down_called)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|