codexapi 0.6.2__tar.gz → 0.6.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.
- {codexapi-0.6.2/src/codexapi.egg-info → codexapi-0.6.4}/PKG-INFO +23 -2
- {codexapi-0.6.2 → codexapi-0.6.4}/README.md +22 -1
- {codexapi-0.6.2 → codexapi-0.6.4}/pyproject.toml +1 -1
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/__init__.py +3 -1
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/cli.py +38 -1
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/pushover.py +1 -1
- codexapi-0.6.4/src/codexapi/watch.py +279 -0
- {codexapi-0.6.2 → codexapi-0.6.4/src/codexapi.egg-info}/PKG-INFO +23 -2
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/SOURCES.txt +1 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/LICENSE +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/setup.cfg +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/__main__.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/agent.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/foreach.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/gh_integration.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/ralph.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/rate_limits.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/science.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/task.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/taskfile.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi/welfare.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/dependency_links.txt +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/entry_points.txt +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/src/codexapi.egg-info/requires.txt +0 -0
- {codexapi-0.6.2 → codexapi-0.6.4}/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.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Minimal Python API for running the Codex CLI.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: codex,agent,cli,openai
|
|
@@ -126,6 +126,18 @@ codexapi run --thread-id THREAD_ID --print-thread-id "Continue where we left off
|
|
|
126
126
|
|
|
127
127
|
Use `--no-yolo` to run Codex with `--full-auto` instead.
|
|
128
128
|
|
|
129
|
+
Watch mode periodically ticks a long-running agent session with the current time
|
|
130
|
+
and prints JSON status updates. The agent controls the loop by setting
|
|
131
|
+
`continue` to true/false in its JSON response. Each tick expects JSON keys:
|
|
132
|
+
`status` (one line), `continue` (bool), and optional `comments` (string). If the
|
|
133
|
+
JSON is invalid, watch asks the agent once to retry before stopping with an
|
|
134
|
+
error. When `~/.pushover` is configured, watch sends a notification when it
|
|
135
|
+
stops.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
codexapi watch 5 "Run the benchmark and wait for results."
|
|
139
|
+
```
|
|
140
|
+
|
|
129
141
|
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
130
142
|
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
131
143
|
`.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
|
|
@@ -154,7 +166,8 @@ Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
|
|
|
154
166
|
Line 1 is your user or group key, line 2 is the app API token. When this file
|
|
155
167
|
exists, Science will send a notification whenever it detects a new best result,
|
|
156
168
|
including the metric values and percent improvement. Task runs will also send a
|
|
157
|
-
✅/❌ notification with the task summary.
|
|
169
|
+
✅/❌ notification with the task summary. Watch runs send a notification when the
|
|
170
|
+
loop stops.
|
|
158
171
|
|
|
159
172
|
Run a task file across a list file:
|
|
160
173
|
|
|
@@ -189,6 +202,14 @@ the same conversation and returns only the agent's message.
|
|
|
189
202
|
- `welfare` (bool): when true, append welfare stop instructions to each prompt
|
|
190
203
|
and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
|
|
191
204
|
|
|
205
|
+
### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
|
|
206
|
+
|
|
207
|
+
Runs a long-lived agent session and periodically "ticks" it with the current
|
|
208
|
+
local time and a reminder of `prompt`. Each tick expects JSON with keys:
|
|
209
|
+
`status` (one line), `continue` (bool), and optional `comments` (string). If the
|
|
210
|
+
JSON is invalid, watch asks the agent once to retry. The loop stops when
|
|
211
|
+
`continue` is false and sends a Pushover notification (when configured).
|
|
212
|
+
|
|
192
213
|
### `task(prompt, check=None, max_iterations=10, cwd=None, yolo=True, flags=None, progress=False, set_up=None, tear_down=None, on_success=None, on_failure=None) -> str`
|
|
193
214
|
|
|
194
215
|
Runs a task with checker-driven retries and returns the success summary.
|
|
@@ -111,6 +111,18 @@ codexapi run --thread-id THREAD_ID --print-thread-id "Continue where we left off
|
|
|
111
111
|
|
|
112
112
|
Use `--no-yolo` to run Codex with `--full-auto` instead.
|
|
113
113
|
|
|
114
|
+
Watch mode periodically ticks a long-running agent session with the current time
|
|
115
|
+
and prints JSON status updates. The agent controls the loop by setting
|
|
116
|
+
`continue` to true/false in its JSON response. Each tick expects JSON keys:
|
|
117
|
+
`status` (one line), `continue` (bool), and optional `comments` (string). If the
|
|
118
|
+
JSON is invalid, watch asks the agent once to retry before stopping with an
|
|
119
|
+
error. When `~/.pushover` is configured, watch sends a notification when it
|
|
120
|
+
stops.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
codexapi watch 5 "Run the benchmark and wait for results."
|
|
124
|
+
```
|
|
125
|
+
|
|
114
126
|
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
115
127
|
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
116
128
|
`.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
|
|
@@ -139,7 +151,8 @@ Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
|
|
|
139
151
|
Line 1 is your user or group key, line 2 is the app API token. When this file
|
|
140
152
|
exists, Science will send a notification whenever it detects a new best result,
|
|
141
153
|
including the metric values and percent improvement. Task runs will also send a
|
|
142
|
-
✅/❌ notification with the task summary.
|
|
154
|
+
✅/❌ notification with the task summary. Watch runs send a notification when the
|
|
155
|
+
loop stops.
|
|
143
156
|
|
|
144
157
|
Run a task file across a list file:
|
|
145
158
|
|
|
@@ -174,6 +187,14 @@ the same conversation and returns only the agent's message.
|
|
|
174
187
|
- `welfare` (bool): when true, append welfare stop instructions to each prompt
|
|
175
188
|
and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
|
|
176
189
|
|
|
190
|
+
### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
|
|
191
|
+
|
|
192
|
+
Runs a long-lived agent session and periodically "ticks" it with the current
|
|
193
|
+
local time and a reminder of `prompt`. Each tick expects JSON with keys:
|
|
194
|
+
`status` (one line), `continue` (bool), and optional `comments` (string). If the
|
|
195
|
+
JSON is invalid, watch asks the agent once to retry. The loop stops when
|
|
196
|
+
`continue` is false and sends a Pushover notification (when configured).
|
|
197
|
+
|
|
177
198
|
### `task(prompt, check=None, max_iterations=10, cwd=None, yolo=True, flags=None, progress=False, set_up=None, tear_down=None, on_success=None, on_failure=None) -> str`
|
|
178
199
|
|
|
179
200
|
Runs a task with checker-driven retries and returns the success summary.
|
|
@@ -7,6 +7,7 @@ from .rate_limits import quota_line, rate_limits
|
|
|
7
7
|
from .ralph import Ralph
|
|
8
8
|
from .science import Science
|
|
9
9
|
from .task import Task, TaskFailed, TaskResult, task, task_result
|
|
10
|
+
from .watch import watch
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"Agent",
|
|
@@ -24,5 +25,6 @@ __all__ = [
|
|
|
24
25
|
"foreach",
|
|
25
26
|
"task",
|
|
26
27
|
"task_result",
|
|
28
|
+
"watch",
|
|
27
29
|
]
|
|
28
|
-
__version__ = "0.6.
|
|
30
|
+
__version__ = "0.6.4"
|
|
@@ -19,6 +19,7 @@ from .science import Science
|
|
|
19
19
|
from .task import DEFAULT_MAX_ITERATIONS, TaskFailed, task
|
|
20
20
|
from .taskfile import TaskFile, load_task_file, task_def_uses_item
|
|
21
21
|
from .rate_limits import quota_line
|
|
22
|
+
from .watch import watch
|
|
22
23
|
|
|
23
24
|
_SESSION_ID_RE = re.compile(
|
|
24
25
|
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
@@ -1039,6 +1040,32 @@ def main(argv=None):
|
|
|
1039
1040
|
"--flags",
|
|
1040
1041
|
help="Additional raw CLI flags to pass to Codex (quoted as needed).",
|
|
1041
1042
|
)
|
|
1043
|
+
|
|
1044
|
+
watch_parser = subparsers.add_parser(
|
|
1045
|
+
"watch",
|
|
1046
|
+
help="Periodically tick an agent for long-running work.",
|
|
1047
|
+
)
|
|
1048
|
+
watch_parser.add_argument(
|
|
1049
|
+
"minutes",
|
|
1050
|
+
type=int,
|
|
1051
|
+
help="Tick interval in minutes (integer, >= 1).",
|
|
1052
|
+
)
|
|
1053
|
+
watch_parser.add_argument(
|
|
1054
|
+
"prompt",
|
|
1055
|
+
nargs="?",
|
|
1056
|
+
help="Prompt to send. Use '-' or omit to read from stdin.",
|
|
1057
|
+
)
|
|
1058
|
+
watch_parser.add_argument("--cwd", help="Working directory for the Codex session.")
|
|
1059
|
+
watch_parser.add_argument(
|
|
1060
|
+
"--no-yolo",
|
|
1061
|
+
action="store_false",
|
|
1062
|
+
dest="yolo",
|
|
1063
|
+
help="Disable --yolo and use --full-auto.",
|
|
1064
|
+
)
|
|
1065
|
+
watch_parser.add_argument(
|
|
1066
|
+
"--flags",
|
|
1067
|
+
help="Additional raw CLI flags to pass to Codex (quoted as needed).",
|
|
1068
|
+
)
|
|
1042
1069
|
run_parser.add_argument(
|
|
1043
1070
|
"--thread-id",
|
|
1044
1071
|
help="Resume an existing Codex thread id.",
|
|
@@ -1474,7 +1501,7 @@ def main(argv=None):
|
|
|
1474
1501
|
|
|
1475
1502
|
prompt_source = None
|
|
1476
1503
|
prompt = None
|
|
1477
|
-
if args.command in ("run", "ralph"):
|
|
1504
|
+
if args.command in ("run", "ralph", "watch"):
|
|
1478
1505
|
prompt_source = args.prompt
|
|
1479
1506
|
elif args.command == "science":
|
|
1480
1507
|
prompt_source = args.task
|
|
@@ -1509,6 +1536,16 @@ def main(argv=None):
|
|
|
1509
1536
|
args.ralph_fresh,
|
|
1510
1537
|
)()
|
|
1511
1538
|
return
|
|
1539
|
+
if args.command == "watch":
|
|
1540
|
+
if args.minutes < 1:
|
|
1541
|
+
raise SystemExit("watch minutes must be >= 1.")
|
|
1542
|
+
try:
|
|
1543
|
+
watch(args.minutes, prompt, args.cwd, args.yolo, args.flags)
|
|
1544
|
+
except KeyboardInterrupt:
|
|
1545
|
+
raise SystemExit(130)
|
|
1546
|
+
except Exception as exc:
|
|
1547
|
+
raise SystemExit(str(exc) or "watch failed") from None
|
|
1548
|
+
return
|
|
1512
1549
|
if args.command == "task":
|
|
1513
1550
|
if args.project:
|
|
1514
1551
|
raise SystemExit("task --project already handled earlier.")
|
|
@@ -15,7 +15,7 @@ _PUSHOVER_URL = "https://api.pushover.net/1/messages.json"
|
|
|
15
15
|
_MAX_MESSAGE = 1024
|
|
16
16
|
|
|
17
17
|
_STARTUP_MESSAGE = (
|
|
18
|
-
"Pushover user and app keys read, notifications for task
|
|
18
|
+
"Pushover user and app keys read, notifications for task/science/watch enabled."
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Periodic watch loop for long-running Codex work.
|
|
2
|
+
|
|
3
|
+
watch keeps a single Codex thread alive and periodically "ticks" it with the
|
|
4
|
+
current time and a reminder of the original instructions. Each tick expects a
|
|
5
|
+
small JSON status payload so the loop can decide whether to continue.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from .agent import Agent
|
|
14
|
+
from .pushover import Pushover
|
|
15
|
+
|
|
16
|
+
_JSON_INSTRUCTIONS = (
|
|
17
|
+
"Respond with JSON only (no markdown/backticks/extra text).\n"
|
|
18
|
+
"Return a single JSON object with keys:\n"
|
|
19
|
+
" status: string (one line)\n"
|
|
20
|
+
" continue: boolean\n"
|
|
21
|
+
" comments: string (optional)\n"
|
|
22
|
+
"To stop this watch loop, set continue to false."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def watch(minutes, prompt, cwd=None, yolo=True, flags=None):
|
|
27
|
+
"""Run a periodic watch loop.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
minutes: Tick interval in whole minutes (>= 1).
|
|
31
|
+
prompt: The original instruction prompt.
|
|
32
|
+
cwd: Optional working directory for the Codex session.
|
|
33
|
+
yolo: Whether to pass --yolo to Codex.
|
|
34
|
+
flags: Additional raw CLI flags to pass to Codex.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The last parsed JSON status object.
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(minutes, int):
|
|
40
|
+
raise TypeError("minutes must be an integer")
|
|
41
|
+
if minutes < 1:
|
|
42
|
+
raise ValueError("minutes must be >= 1")
|
|
43
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
44
|
+
raise ValueError("prompt must be a non-empty string")
|
|
45
|
+
|
|
46
|
+
interval = minutes * 60
|
|
47
|
+
session = Agent(cwd, yolo, None, flags)
|
|
48
|
+
pushover = Pushover()
|
|
49
|
+
pushover.ensure_ready()
|
|
50
|
+
title = _format_title(prompt)
|
|
51
|
+
|
|
52
|
+
last_sent = None
|
|
53
|
+
last_result = None
|
|
54
|
+
tick = 0
|
|
55
|
+
|
|
56
|
+
while True:
|
|
57
|
+
tick += 1
|
|
58
|
+
sent_at = time.monotonic()
|
|
59
|
+
elapsed = None if last_sent is None else sent_at - last_sent
|
|
60
|
+
last_sent = sent_at
|
|
61
|
+
|
|
62
|
+
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
63
|
+
message = _build_tick_prompt(prompt, now, elapsed, tick)
|
|
64
|
+
output = session(message)
|
|
65
|
+
try:
|
|
66
|
+
result = _parse_status(output)
|
|
67
|
+
except ValueError as exc:
|
|
68
|
+
print(
|
|
69
|
+
f"[watch {tick} {now}] Invalid JSON from agent, requesting retry: {exc}",
|
|
70
|
+
file=sys.stderr,
|
|
71
|
+
)
|
|
72
|
+
retry_prompt = _json_retry_prompt(prompt, tick, str(exc), output)
|
|
73
|
+
retry_output = session(retry_prompt)
|
|
74
|
+
try:
|
|
75
|
+
result = _parse_status(retry_output)
|
|
76
|
+
except ValueError as exc2:
|
|
77
|
+
details = _format_json_double_failure(
|
|
78
|
+
str(exc),
|
|
79
|
+
output,
|
|
80
|
+
str(exc2),
|
|
81
|
+
retry_output,
|
|
82
|
+
)
|
|
83
|
+
pushover.send(title, f"Watch stopped (invalid JSON).\n{details}")
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
"Agent was unable to provide valid JSON output after retry.\n"
|
|
86
|
+
+ details
|
|
87
|
+
) from None
|
|
88
|
+
last_result = result
|
|
89
|
+
_print_status(now, elapsed, tick, result)
|
|
90
|
+
|
|
91
|
+
if not result["continue"]:
|
|
92
|
+
pushover.send(title, _format_stop_message(tick, now, result))
|
|
93
|
+
return last_result
|
|
94
|
+
|
|
95
|
+
next_tick = sent_at + interval
|
|
96
|
+
sleep_seconds = next_tick - time.monotonic()
|
|
97
|
+
if sleep_seconds > 0:
|
|
98
|
+
time.sleep(sleep_seconds)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_tick_prompt(prompt, now, elapsed, tick):
|
|
102
|
+
lines = [
|
|
103
|
+
f"Tick {tick}.",
|
|
104
|
+
f"Local time now: {now}",
|
|
105
|
+
]
|
|
106
|
+
if elapsed is not None:
|
|
107
|
+
lines.append(
|
|
108
|
+
"Time since last tick: "
|
|
109
|
+
f"{_format_minutes_seconds(elapsed)} ({int(round(elapsed))}s)"
|
|
110
|
+
)
|
|
111
|
+
lines.extend(
|
|
112
|
+
[
|
|
113
|
+
"",
|
|
114
|
+
"A reminder: your instructions are:",
|
|
115
|
+
prompt.strip(),
|
|
116
|
+
"",
|
|
117
|
+
_JSON_INSTRUCTIONS,
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
return "\n".join(lines).strip()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _format_minutes_seconds(seconds):
|
|
124
|
+
if seconds is None:
|
|
125
|
+
return ""
|
|
126
|
+
seconds = int(round(seconds))
|
|
127
|
+
if seconds < 0:
|
|
128
|
+
seconds = 0
|
|
129
|
+
minutes, seconds = divmod(seconds, 60)
|
|
130
|
+
return f"{minutes}m{seconds:02d}s"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_status(output):
|
|
134
|
+
text = _maybe_strip_code_fence(str(output or "").strip())
|
|
135
|
+
data = _try_parse_json(text)
|
|
136
|
+
if data is None:
|
|
137
|
+
snippet = text[:200].replace("\n", "\\n")
|
|
138
|
+
raise ValueError(f"Invalid JSON response. Snippet: {snippet}")
|
|
139
|
+
if not isinstance(data, dict):
|
|
140
|
+
raise ValueError("Status JSON must be an object.")
|
|
141
|
+
|
|
142
|
+
status = data.get("status")
|
|
143
|
+
cont = data.get("continue")
|
|
144
|
+
comments = data.get("comments")
|
|
145
|
+
|
|
146
|
+
if not isinstance(status, str):
|
|
147
|
+
raise ValueError("Status JSON missing string 'status'.")
|
|
148
|
+
if not isinstance(cont, bool):
|
|
149
|
+
raise ValueError("Status JSON missing boolean 'continue'.")
|
|
150
|
+
if comments is None:
|
|
151
|
+
comments = ""
|
|
152
|
+
if not isinstance(comments, str):
|
|
153
|
+
raise ValueError("Status JSON missing string 'comments'.")
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"status": _single_line(status),
|
|
157
|
+
"continue": cont,
|
|
158
|
+
"comments": comments,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _json_retry_prompt(prompt, tick, error, output):
|
|
163
|
+
snippet = _snippet(output, 600)
|
|
164
|
+
lines = [
|
|
165
|
+
f"Your last message (tick {tick}) was not valid JSON.",
|
|
166
|
+
f"Error: {error}",
|
|
167
|
+
"",
|
|
168
|
+
"Here is your previous output (truncated):",
|
|
169
|
+
snippet,
|
|
170
|
+
"",
|
|
171
|
+
"Please try again and respond with JSON only.",
|
|
172
|
+
"",
|
|
173
|
+
"A reminder: your instructions are:",
|
|
174
|
+
prompt.strip(),
|
|
175
|
+
"",
|
|
176
|
+
_JSON_INSTRUCTIONS,
|
|
177
|
+
]
|
|
178
|
+
return "\n".join(lines).strip()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _format_title(prompt):
|
|
182
|
+
text = _single_line(prompt).strip() or "codexapi watch"
|
|
183
|
+
if len(text) > 60:
|
|
184
|
+
text = text[:57] + "..."
|
|
185
|
+
return f"Watch: {text}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _format_stop_message(tick, now, result):
|
|
189
|
+
status = _single_line(result.get("status") or "").strip()
|
|
190
|
+
header = f"Watch stopped at tick {tick} ({now})."
|
|
191
|
+
if status:
|
|
192
|
+
header = f"{header} {status}"
|
|
193
|
+
comments = (result.get("comments") or "").strip()
|
|
194
|
+
if comments:
|
|
195
|
+
return f"{header}\n{comments}"
|
|
196
|
+
return header
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _format_json_failure(error, output):
|
|
200
|
+
snippet = _snippet(output, 600)
|
|
201
|
+
return "\n".join(
|
|
202
|
+
[
|
|
203
|
+
f"Error: {error}",
|
|
204
|
+
"",
|
|
205
|
+
"Last output (truncated):",
|
|
206
|
+
snippet,
|
|
207
|
+
]
|
|
208
|
+
).strip()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _format_json_double_failure(error_1, output_1, error_2, output_2):
|
|
212
|
+
first = _format_json_failure(error_1, output_1)
|
|
213
|
+
second = _format_json_failure(error_2, output_2)
|
|
214
|
+
return "\n".join(
|
|
215
|
+
[
|
|
216
|
+
"First attempt:",
|
|
217
|
+
first,
|
|
218
|
+
"",
|
|
219
|
+
"Second attempt:",
|
|
220
|
+
second,
|
|
221
|
+
]
|
|
222
|
+
).strip()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _snippet(text, limit):
|
|
226
|
+
text = str(text or "").strip()
|
|
227
|
+
if not text:
|
|
228
|
+
return "(empty)"
|
|
229
|
+
if len(text) <= limit:
|
|
230
|
+
return text
|
|
231
|
+
return text[:limit].rstrip() + "..."
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _maybe_strip_code_fence(text):
|
|
235
|
+
if not text.startswith("```"):
|
|
236
|
+
return text
|
|
237
|
+
lines = text.splitlines()
|
|
238
|
+
if not lines:
|
|
239
|
+
return text
|
|
240
|
+
if lines[0].startswith("```"):
|
|
241
|
+
lines = lines[1:]
|
|
242
|
+
if lines and lines[-1].strip() == "```":
|
|
243
|
+
lines = lines[:-1]
|
|
244
|
+
return "\n".join(lines).strip()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _try_parse_json(text):
|
|
248
|
+
if not text:
|
|
249
|
+
return None
|
|
250
|
+
try:
|
|
251
|
+
return json.loads(text)
|
|
252
|
+
except json.JSONDecodeError:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
start = text.find("{")
|
|
256
|
+
end = text.rfind("}")
|
|
257
|
+
if start == -1 or end == -1 or end <= start:
|
|
258
|
+
return None
|
|
259
|
+
try:
|
|
260
|
+
return json.loads(text[start : end + 1])
|
|
261
|
+
except json.JSONDecodeError:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _single_line(text):
|
|
266
|
+
return " ".join(text.replace("\r", " ").split())
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _print_status(now, elapsed, tick, result):
|
|
270
|
+
delta = ""
|
|
271
|
+
if elapsed is not None:
|
|
272
|
+
delta = f" +{_format_minutes_seconds(elapsed)}"
|
|
273
|
+
status = result.get("status", "")
|
|
274
|
+
cont = result.get("continue")
|
|
275
|
+
line = f"[watch {tick} {now}{delta}] {status} (continue={cont})".rstrip()
|
|
276
|
+
print(line)
|
|
277
|
+
comments = result.get("comments") or ""
|
|
278
|
+
if comments.strip():
|
|
279
|
+
print(comments.rstrip())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: codexapi
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Minimal Python API for running the Codex CLI.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: codex,agent,cli,openai
|
|
@@ -126,6 +126,18 @@ codexapi run --thread-id THREAD_ID --print-thread-id "Continue where we left off
|
|
|
126
126
|
|
|
127
127
|
Use `--no-yolo` to run Codex with `--full-auto` instead.
|
|
128
128
|
|
|
129
|
+
Watch mode periodically ticks a long-running agent session with the current time
|
|
130
|
+
and prints JSON status updates. The agent controls the loop by setting
|
|
131
|
+
`continue` to true/false in its JSON response. Each tick expects JSON keys:
|
|
132
|
+
`status` (one line), `continue` (bool), and optional `comments` (string). If the
|
|
133
|
+
JSON is invalid, watch asks the agent once to retry before stopping with an
|
|
134
|
+
error. When `~/.pushover` is configured, watch sends a notification when it
|
|
135
|
+
stops.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
codexapi watch 5 "Run the benchmark and wait for results."
|
|
139
|
+
```
|
|
140
|
+
|
|
129
141
|
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
130
142
|
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
131
143
|
`.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
|
|
@@ -154,7 +166,8 @@ Optional Pushover notifications: create `~/.pushover` with two non-empty lines.
|
|
|
154
166
|
Line 1 is your user or group key, line 2 is the app API token. When this file
|
|
155
167
|
exists, Science will send a notification whenever it detects a new best result,
|
|
156
168
|
including the metric values and percent improvement. Task runs will also send a
|
|
157
|
-
✅/❌ notification with the task summary.
|
|
169
|
+
✅/❌ notification with the task summary. Watch runs send a notification when the
|
|
170
|
+
loop stops.
|
|
158
171
|
|
|
159
172
|
Run a task file across a list file:
|
|
160
173
|
|
|
@@ -189,6 +202,14 @@ the same conversation and returns only the agent's message.
|
|
|
189
202
|
- `welfare` (bool): when true, append welfare stop instructions to each prompt
|
|
190
203
|
and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
|
|
191
204
|
|
|
205
|
+
### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
|
|
206
|
+
|
|
207
|
+
Runs a long-lived agent session and periodically "ticks" it with the current
|
|
208
|
+
local time and a reminder of `prompt`. Each tick expects JSON with keys:
|
|
209
|
+
`status` (one line), `continue` (bool), and optional `comments` (string). If the
|
|
210
|
+
JSON is invalid, watch asks the agent once to retry. The loop stops when
|
|
211
|
+
`continue` is false and sends a Pushover notification (when configured).
|
|
212
|
+
|
|
192
213
|
### `task(prompt, check=None, max_iterations=10, cwd=None, yolo=True, flags=None, progress=False, set_up=None, tear_down=None, on_success=None, on_failure=None) -> str`
|
|
193
214
|
|
|
194
215
|
Runs a task with checker-driven retries and returns the success summary.
|
|
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
|