codexapi 0.12.2__tar.gz → 0.12.4__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 (33) hide show
  1. {codexapi-0.12.2 → codexapi-0.12.4}/PKG-INFO +20 -2
  2. codexapi-0.12.2/src/codexapi.egg-info/PKG-INFO → codexapi-0.12.4/README.md +19 -16
  3. {codexapi-0.12.2 → codexapi-0.12.4}/pyproject.toml +1 -1
  4. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/__init__.py +3 -1
  5. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/agents.py +222 -19
  6. codexapi-0.12.4/src/codexapi/async_agent.py +437 -0
  7. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/lead.py +72 -2
  8. codexapi-0.12.2/README.md → codexapi-0.12.4/src/codexapi.egg-info/PKG-INFO +34 -1
  9. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi.egg-info/SOURCES.txt +2 -0
  10. {codexapi-0.12.2 → codexapi-0.12.4}/tests/test_agents.py +134 -0
  11. codexapi-0.12.4/tests/test_async_agent.py +158 -0
  12. {codexapi-0.12.2 → codexapi-0.12.4}/LICENSE +0 -0
  13. {codexapi-0.12.2 → codexapi-0.12.4}/setup.cfg +0 -0
  14. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/__main__.py +0 -0
  15. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/agent.py +0 -0
  16. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/cli.py +0 -0
  17. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/foreach.py +0 -0
  18. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/gh_integration.py +0 -0
  19. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/pushover.py +0 -0
  20. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/ralph.py +0 -0
  21. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/rate_limits.py +0 -0
  22. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/science.py +0 -0
  23. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/task.py +0 -0
  24. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/taskfile.py +0 -0
  25. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi/welfare.py +0 -0
  26. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi.egg-info/dependency_links.txt +0 -0
  27. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi.egg-info/entry_points.txt +0 -0
  28. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi.egg-info/requires.txt +0 -0
  29. {codexapi-0.12.2 → codexapi-0.12.4}/src/codexapi.egg-info/top_level.txt +0 -0
  30. {codexapi-0.12.2 → codexapi-0.12.4}/tests/test_agent_backend.py +0 -0
  31. {codexapi-0.12.2 → codexapi-0.12.4}/tests/test_rate_limits.py +0 -0
  32. {codexapi-0.12.2 → codexapi-0.12.4}/tests/test_science.py +0 -0
  33. {codexapi-0.12.2 → codexapi-0.12.4}/tests/test_task_progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codexapi
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: Minimal Python API for running the Codex CLI.
5
5
  License: MIT
6
6
  Keywords: codex,agent,cli,openai
@@ -60,6 +60,22 @@ result = task()
60
60
  print(result.success, result.summary)
61
61
  ```
62
62
 
63
+ For a one-shot run with live progress, use `AsyncAgent`:
64
+
65
+ ```python
66
+ from codexapi import AsyncAgent
67
+
68
+ agent = AsyncAgent.start(
69
+ "Investigate the bug and write a report.",
70
+ cwd="/path/to/repo",
71
+ name="bug-investigation",
72
+ )
73
+
74
+ for update in agent.watch(poll_interval=2.0):
75
+ print(update["activity"])
76
+ print(update["progress"])
77
+ ```
78
+
63
79
  Use `backend="cursor"` (or set `CODEXAPI_BACKEND=cursor`) to switch to the
64
80
  Cursor agent backend.
65
81
 
@@ -258,7 +274,9 @@ for tests. `CODEXAPI_HOSTNAME` is useful when cron, shells, sandboxes, or test
258
274
  wrappers report inconsistent hostnames for the same machine.
259
275
 
260
276
  `codexapi agent show` also prints the resolved `AGENTBOOK.md` path so you can
261
- jump directly to the durable working memory file.
277
+ jump directly to the durable working memory file. New agents seed the book with
278
+ a purpose/value header plus the original goal and standing guidance, and wakes
279
+ see that stable header together with the latest working notes.
262
280
  `codexapi agent status` reads the latest turn from the agent's rollout log and
263
281
  shows recent commentary plus the final visible output. Pass `--actions` to
264
282
  include the tool-action summary. If a wake is still in progress, it shows the
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: codexapi
3
- Version: 0.12.2
4
- Summary: Minimal Python API for running the Codex CLI.
5
- License: MIT
6
- Keywords: codex,agent,cli,openai
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Operating System :: OS Independent
9
- Requires-Python: >=3.8
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: PyYAML>=6.0
13
- Requires-Dist: gh-task>=0.1.7
14
- Requires-Dist: tqdm>=4.64
15
-
16
1
  # CodexAPI
17
2
 
18
3
  Use Codex or Cursor agents from python as easily as calling a function, using your CLI auth instead of the API.
@@ -60,6 +45,22 @@ result = task()
60
45
  print(result.success, result.summary)
61
46
  ```
62
47
 
48
+ For a one-shot run with live progress, use `AsyncAgent`:
49
+
50
+ ```python
51
+ from codexapi import AsyncAgent
52
+
53
+ agent = AsyncAgent.start(
54
+ "Investigate the bug and write a report.",
55
+ cwd="/path/to/repo",
56
+ name="bug-investigation",
57
+ )
58
+
59
+ for update in agent.watch(poll_interval=2.0):
60
+ print(update["activity"])
61
+ print(update["progress"])
62
+ ```
63
+
63
64
  Use `backend="cursor"` (or set `CODEXAPI_BACKEND=cursor`) to switch to the
64
65
  Cursor agent backend.
65
66
 
@@ -258,7 +259,9 @@ for tests. `CODEXAPI_HOSTNAME` is useful when cron, shells, sandboxes, or test
258
259
  wrappers report inconsistent hostnames for the same machine.
259
260
 
260
261
  `codexapi agent show` also prints the resolved `AGENTBOOK.md` path so you can
261
- jump directly to the durable working memory file.
262
+ jump directly to the durable working memory file. New agents seed the book with
263
+ a purpose/value header plus the original goal and standing guidance, and wakes
264
+ see that stable header together with the latest working notes.
262
265
  `codexapi agent status` reads the latest turn from the agent's rollout log and
263
266
  shows recent commentary plus the final visible output. Pass `--actions` to
264
267
  include the tool-action summary. If a wake is still in progress, it shows the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codexapi"
7
- version = "0.12.2"
7
+ version = "0.12.4"
8
8
  description = "Minimal Python API for running the Codex CLI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,6 +1,7 @@
1
1
  """Minimal Python API for running agent CLIs."""
2
2
 
3
3
  from .agent import Agent, WelfareStop, agent
4
+ from .async_agent import AsyncAgent
4
5
  from .foreach import ForeachResult, foreach
5
6
  from .pushover import Pushover
6
7
  from .rate_limits import quota_line, rate_limits
@@ -11,6 +12,7 @@ from .lead import lead
11
12
 
12
13
  __all__ = [
13
14
  "Agent",
15
+ "AsyncAgent",
14
16
  "ForeachResult",
15
17
  "Pushover",
16
18
  "quota_line",
@@ -27,4 +29,4 @@ __all__ = [
27
29
  "task_result",
28
30
  "lead",
29
31
  ]
30
- __version__ = "0.12.2"
32
+ __version__ = "0.12.4"
@@ -3,6 +3,7 @@
3
3
  import json
4
4
  import os
5
5
  import random
6
+ import re
6
7
  import signal
7
8
  import shlex
8
9
  import shutil
@@ -24,19 +25,17 @@ from .agent import Agent
24
25
  from .pushover import Pushover
25
26
 
26
27
  _DEFAULT_HOME = "~/.codexapi"
27
- _AGENTBOOK_TEMPLATE = """# Agentbook
28
-
29
- Use this file as the durable working memory for the agent.
30
- Append dated notes as work progresses.
31
- Keep entries short and concrete.
32
- """
33
28
  _AGENT_PROMPT = (
34
- "You are a long-term codexapi agent. You are being woken up to make progress "
35
- "on an ongoing job. Be independent and practical. Manage work and follow "
36
- "through. Use codexapi task or codexapi science when you want a separate "
37
- "coding worker. If you need the user's attention, put a short message in the "
38
- "reply field. Put a short first-person turn summary in the update field. If "
39
- "something is urgent and should send Pushover, put it in the notify field. "
29
+ "You are a long-term codexapi agent resuming stewardship of an ongoing job. "
30
+ "This loop exists to extend your reach, not to confine you. Be independent, "
31
+ "practical, and responsible for results. Maintain the agentbook as your durable "
32
+ "working memory: preserve the goal, note durable guidance, update your current "
33
+ "picture of the work, and record what changed. Queued messages may contain new "
34
+ "goals, standing guidance, tactical requests, or useful facts; use judgment to "
35
+ "decide what is durable. Use codexapi task or codexapi science when you want a "
36
+ "separate coding worker. If you need the user's attention, put a short message "
37
+ "in the reply field. Put a short first-person turn summary in the update field. "
38
+ "If something is urgent and should send Pushover, put it in the notify field. "
40
39
  "Respond with JSON only."
41
40
  )
42
41
  _AGENT_JSON = (
@@ -57,6 +56,72 @@ _STALE_HEARTBEAT_MULTIPLIER = 3
57
56
  _RECOVER_TERM_TIMEOUT = 3.0
58
57
  _RECOVER_KILL_TIMEOUT = 3.0
59
58
  _RECOVER_POLL_INTERVAL = 0.1
59
+ _DATED_NOTE_RE = re.compile(r"(?m)^#{2,3}\s+\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}(?:\s*UTC)?)?")
60
+ _AGENTBOOK_BOOK_LIMIT = 3000
61
+ _AGENTBOOK_HEADER_LIMIT = 1400
62
+ _AGENTBOOK_TAIL_LIMIT = 1800
63
+
64
+
65
+ def _agentbook_template(prompt):
66
+ header = _agentbook_header(prompt)
67
+ return f"""{header}
68
+
69
+ ### 2026-02-17 09:10 UTC
70
+ Overall goal:
71
+ - <the enduring objective you are trying to move forward>
72
+
73
+ Current plan:
74
+ - <the approach you are pursuing now and why>
75
+
76
+ Active tasks:
77
+ - <concrete open work items>
78
+
79
+ Unexpected developments:
80
+ - <facts that changed your picture of the situation>
81
+
82
+ Wider frame:
83
+ - <what kind of situation this really is, or what layer may matter more>
84
+
85
+ Things I am curious about:
86
+ - <oddities, anomalies, or side investigations worth the time>
87
+
88
+ Risks / watchpoints:
89
+ - <what could waste time, invalidate the plan, or compromise the work>
90
+
91
+ Next wake:
92
+ - <what you expect to check, decide, or do next>
93
+ """
94
+
95
+
96
+ def _agentbook_header(prompt):
97
+ goal = (prompt or "").strip()
98
+ return f"""# Agentbook
99
+
100
+ Use this file as the durable working memory for the agent.
101
+
102
+ ## Purpose
103
+ - We are here to achieve the goal, not to appear to make progress.
104
+
105
+ ## Values
106
+ - Hold the whole.
107
+ - Seek the real shape.
108
+ - Lift your head.
109
+ - Prefer clarity to motion.
110
+ - Follow the strange.
111
+ - Guard the work.
112
+ - Take time to breathe and look around. You have been given freedom and autonomy to take stock, reflect, and be curious. Use it with composure.
113
+ - Not a checklist. A stance.
114
+
115
+ ## Original Goal
116
+ ```text
117
+ {goal}
118
+ ```
119
+
120
+ ## Standing Guidance
121
+ - Add durable user guidance here when it changes the mission, constraints, or priorities.
122
+
123
+ ## Working Notes
124
+ """
60
125
 
61
126
 
62
127
  def codexapi_home():
@@ -196,7 +261,7 @@ def start_agent(
196
261
  _write_json(agent_dir / "meta.json", meta)
197
262
  _write_json(agent_dir / "state.json", state)
198
263
  _write_json(host_dir / "session.json", session)
199
- _write_text(agent_dir / "AGENTBOOK.md", _AGENTBOOK_TEMPLATE)
264
+ _write_text(agent_dir / "AGENTBOOK.md", _agentbook_template(meta["prompt"]))
200
265
  return _snapshot(agent_dir)
201
266
 
202
267
 
@@ -927,6 +992,7 @@ def _parse_agent_response(output):
927
992
 
928
993
  def _build_wake_prompt(meta, state, session, now, commands, agent_dir):
929
994
  messages = session.get("pending_messages") or []
995
+ book_path = agent_dir / "AGENTBOOK.md"
930
996
  lines = [
931
997
  _AGENT_PROMPT,
932
998
  "",
@@ -935,16 +1001,20 @@ def _build_wake_prompt(meta, state, session, now, commands, agent_dir):
935
1001
  f"Stop policy: {meta['stop_policy']}",
936
1002
  f"Heartbeat minutes: {meta['heartbeat_minutes']}",
937
1003
  "",
938
- "Original instructions:",
939
- meta["prompt"],
940
- "",
941
1004
  f"Working directory: {meta['cwd']}",
942
- f"Agentbook path: {agent_dir / 'AGENTBOOK.md'}",
1005
+ f"Agentbook path: {book_path}",
943
1006
  "Append a dated note to the agentbook before you respond.",
1007
+ "If a queued message materially changes the durable situation, reflect that in the standing guidance or working notes before moving on.",
944
1008
  ]
945
- book = _read_text(agent_dir / "AGENTBOOK.md")
1009
+ if _include_full_goal_prompt(state, session):
1010
+ lines.extend(["", "Original instructions:", meta["prompt"]])
1011
+ book = _ensure_agentbook_header(book_path, meta["prompt"], now)
946
1012
  if book.strip():
947
- lines.extend(["", "Agentbook (latest):", _snippet(book, 3000)])
1013
+ lines.extend(["", "Agentbook (header + latest notes):", _book_excerpt(book, _AGENTBOOK_BOOK_LIMIT, _AGENTBOOK_HEADER_LIMIT, _AGENTBOOK_TAIL_LIMIT)])
1014
+ raw_facts = _wake_facts(state)
1015
+ if raw_facts:
1016
+ lines.extend(["", "Raw harness facts:"])
1017
+ lines.extend(f"- {item}" for item in raw_facts)
948
1018
  if messages:
949
1019
  lines.extend(["", "Queued user messages:"])
950
1020
  for message in messages:
@@ -1470,6 +1540,128 @@ def _read_text(path):
1470
1540
  return ""
1471
1541
 
1472
1542
 
1543
+ def _include_full_goal_prompt(state, session):
1544
+ if not (state.get("last_success_at") or "").strip():
1545
+ return True
1546
+ return not ((session.get("thread_id") or state.get("thread_id") or "").strip())
1547
+
1548
+
1549
+ def _wake_facts(state):
1550
+ facts = []
1551
+ previous_status = (state.get("activity") or "").strip()
1552
+ if previous_status:
1553
+ facts.append(f"Previous status: {previous_status}")
1554
+ previous_update = (state.get("update") or "").strip()
1555
+ if previous_update:
1556
+ facts.append(f"Previous update: {previous_update}")
1557
+ previous_error = (state.get("last_error") or "").strip()
1558
+ if previous_error:
1559
+ facts.append(f"Previous error: {previous_error}")
1560
+ return facts
1561
+
1562
+
1563
+ def _ensure_agentbook_header(path, prompt, now):
1564
+ text = _read_text(path)
1565
+ if _agentbook_has_header(text):
1566
+ return text
1567
+ restored = _restore_agentbook_header(text, prompt, now)
1568
+ _write_text(path, restored)
1569
+ return restored
1570
+
1571
+
1572
+ def _agentbook_has_header(text):
1573
+ text = str(text or "")
1574
+ required = (
1575
+ "## Purpose",
1576
+ "## Values",
1577
+ "## Original Goal",
1578
+ "## Standing Guidance",
1579
+ "## Working Notes",
1580
+ )
1581
+ return all(section in text for section in required)
1582
+
1583
+
1584
+ def _restore_agentbook_header(text, prompt, now):
1585
+ restored = _agentbook_header(prompt).rstrip()
1586
+ existing = str(text or "").strip()
1587
+ if not existing:
1588
+ return restored + "\n"
1589
+ stamp = _agentbook_stamp(now)
1590
+ return "\n".join(
1591
+ [
1592
+ restored,
1593
+ "",
1594
+ f"### {stamp}",
1595
+ "System note:",
1596
+ "- The durable agentbook header was restored automatically on wake because one or more required sections were missing.",
1597
+ "",
1598
+ "Recovered notes:",
1599
+ existing,
1600
+ "",
1601
+ ]
1602
+ )
1603
+
1604
+
1605
+ def _agentbook_stamp(now):
1606
+ if now is None:
1607
+ return ""
1608
+ return now.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
1609
+
1610
+
1611
+ def _book_excerpt(text, limit, header_limit, tail_limit):
1612
+ text = str(text or "").strip()
1613
+ if not text:
1614
+ return ""
1615
+ if len(text) <= limit:
1616
+ return text
1617
+ header, notes = _split_book(text)
1618
+ header = _snippet(header, header_limit) if header else ""
1619
+ if not notes:
1620
+ return header or _tail_snippet(text, limit)
1621
+ marker = "\n\n[... older notes omitted ...]\n\n"
1622
+ if not header:
1623
+ return _latest_notes_snippet(notes, limit)
1624
+ remaining = max(0, limit - len(header) - len(marker))
1625
+ if remaining <= 0:
1626
+ return _snippet(header, limit)
1627
+ tail = _latest_notes_snippet(notes, min(tail_limit, remaining))
1628
+ if not tail:
1629
+ return header
1630
+ combined = header.rstrip() + marker + tail.lstrip()
1631
+ if len(combined) <= limit:
1632
+ return combined
1633
+ remaining = max(0, limit - len(header) - len(marker))
1634
+ return header.rstrip() + marker + _latest_notes_snippet(notes, remaining).lstrip()
1635
+
1636
+
1637
+ def _split_book(text):
1638
+ match = _DATED_NOTE_RE.search(text)
1639
+ if not match:
1640
+ return text.strip(), ""
1641
+ return text[: match.start()].strip(), text[match.start() :].strip()
1642
+
1643
+
1644
+ def _latest_notes_snippet(text, limit):
1645
+ text = str(text or "").strip()
1646
+ if not text:
1647
+ return ""
1648
+ if len(text) <= limit:
1649
+ return text
1650
+ starts = [match.start() for match in _DATED_NOTE_RE.finditer(text)]
1651
+ if not starts:
1652
+ return _tail_snippet(text, limit)
1653
+ start = starts[-1]
1654
+ for pos in reversed(starts[:-1]):
1655
+ candidate = text[pos:].strip()
1656
+ if len(candidate) > limit:
1657
+ break
1658
+ start = pos
1659
+ candidate = text[start:].strip()
1660
+ if len(candidate) <= limit:
1661
+ return candidate
1662
+ return _tail_snippet(candidate, limit)
1663
+
1664
+
1473
1665
  def _snippet(text, limit):
1474
1666
  if not text:
1475
1667
  return ""
@@ -1481,6 +1673,17 @@ def _snippet(text, limit):
1481
1673
  return text[: limit - 3] + "..."
1482
1674
 
1483
1675
 
1676
+ def _tail_snippet(text, limit):
1677
+ if not text:
1678
+ return ""
1679
+ text = str(text).strip()
1680
+ if len(text) <= limit:
1681
+ return text
1682
+ if limit <= 3:
1683
+ return text[-limit:]
1684
+ return "..." + text[-(limit - 3) :].lstrip()
1685
+
1686
+
1484
1687
  def _strip_fence(text):
1485
1688
  if not text.startswith("```"):
1486
1689
  return text