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.
Files changed (26) hide show
  1. {codexapi-0.6.7/src/codexapi.egg-info → codexapi-0.6.8}/PKG-INFO +1 -1
  2. {codexapi-0.6.7 → codexapi-0.6.8}/pyproject.toml +1 -1
  3. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/__init__.py +1 -1
  4. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/task.py +81 -50
  5. {codexapi-0.6.7 → codexapi-0.6.8/src/codexapi.egg-info}/PKG-INFO +1 -1
  6. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/SOURCES.txt +2 -1
  7. codexapi-0.6.8/tests/test_task_progress.py +74 -0
  8. {codexapi-0.6.7 → codexapi-0.6.8}/LICENSE +0 -0
  9. {codexapi-0.6.7 → codexapi-0.6.8}/README.md +0 -0
  10. {codexapi-0.6.7 → codexapi-0.6.8}/setup.cfg +0 -0
  11. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/__main__.py +0 -0
  12. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/agent.py +0 -0
  13. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/cli.py +0 -0
  14. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/foreach.py +0 -0
  15. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/gh_integration.py +0 -0
  16. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/pushover.py +0 -0
  17. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/ralph.py +0 -0
  18. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/rate_limits.py +0 -0
  19. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/science.py +0 -0
  20. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/taskfile.py +0 -0
  21. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/watch.py +0 -0
  22. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi/welfare.py +0 -0
  23. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/dependency_links.txt +0 -0
  24. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/entry_points.txt +0 -0
  25. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/requires.txt +0 -0
  26. {codexapi-0.6.7 → codexapi-0.6.8}/src/codexapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codexapi"
7
- version = "0.6.7"
7
+ version = "0.6.8"
8
8
  description = "Minimal Python API for running the Codex CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -27,4 +27,4 @@ __all__ = [
27
27
  "task_result",
28
28
  "watch",
29
29
  ]
30
- __version__ = "0.6.7"
30
+ __version__ = "0.6.8"
@@ -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
- remaining, _summary = estimate(
504
- self.prompt,
505
- "",
506
- "",
507
- self.cwd,
508
- self._yolo,
509
- self._flags,
510
- None,
511
- )
512
- self._progress_total = remaining
513
- start_time = time.monotonic()
514
- self._progress_start = start_time
515
- self.on_progress(
516
- 0,
517
- self.max_iterations,
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
- remaining, summary = estimate(
545
- self.prompt,
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -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