codexapi 0.6.2__tar.gz → 0.6.3__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.3}/PKG-INFO +16 -1
- {codexapi-0.6.2 → codexapi-0.6.3}/README.md +15 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/pyproject.toml +1 -1
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/__init__.py +3 -1
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/cli.py +36 -1
- codexapi-0.6.3/src/codexapi/watch.py +180 -0
- {codexapi-0.6.2 → codexapi-0.6.3/src/codexapi.egg-info}/PKG-INFO +16 -1
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi.egg-info/SOURCES.txt +1 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/LICENSE +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/setup.cfg +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/__main__.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/agent.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/foreach.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/gh_integration.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/pushover.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/ralph.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/rate_limits.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/science.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/task.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/taskfile.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi/welfare.py +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi.egg-info/dependency_links.txt +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi.egg-info/entry_points.txt +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/src/codexapi.egg-info/requires.txt +0 -0
- {codexapi-0.6.2 → codexapi-0.6.3}/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.3
|
|
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,14 @@ 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.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
codexapi watch 5 "Run the benchmark and wait for results."
|
|
135
|
+
```
|
|
136
|
+
|
|
129
137
|
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
130
138
|
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
131
139
|
`.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
|
|
@@ -189,6 +197,13 @@ the same conversation and returns only the agent's message.
|
|
|
189
197
|
- `welfare` (bool): when true, append welfare stop instructions to each prompt
|
|
190
198
|
and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
|
|
191
199
|
|
|
200
|
+
### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
|
|
201
|
+
|
|
202
|
+
Runs a long-lived agent session and periodically "ticks" it with the current
|
|
203
|
+
local time and a reminder of `prompt`. Each tick expects JSON with keys:
|
|
204
|
+
`status` (one line), `continue` (bool), and `comments` (string). The loop stops
|
|
205
|
+
when `continue` is false.
|
|
206
|
+
|
|
192
207
|
### `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
208
|
|
|
194
209
|
Runs a task with checker-driven retries and returns the success summary.
|
|
@@ -111,6 +111,14 @@ 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.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
codexapi watch 5 "Run the benchmark and wait for results."
|
|
120
|
+
```
|
|
121
|
+
|
|
114
122
|
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
115
123
|
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
116
124
|
`.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
|
|
@@ -174,6 +182,13 @@ the same conversation and returns only the agent's message.
|
|
|
174
182
|
- `welfare` (bool): when true, append welfare stop instructions to each prompt
|
|
175
183
|
and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
|
|
176
184
|
|
|
185
|
+
### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
|
|
186
|
+
|
|
187
|
+
Runs a long-lived agent session and periodically "ticks" it with the current
|
|
188
|
+
local time and a reminder of `prompt`. Each tick expects JSON with keys:
|
|
189
|
+
`status` (one line), `continue` (bool), and `comments` (string). The loop stops
|
|
190
|
+
when `continue` is false.
|
|
191
|
+
|
|
177
192
|
### `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
193
|
|
|
179
194
|
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.3"
|
|
@@ -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,14 @@ 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
|
+
return
|
|
1512
1547
|
if args.command == "task":
|
|
1513
1548
|
if args.project:
|
|
1514
1549
|
raise SystemExit("task --project already handled earlier.")
|
|
@@ -0,0 +1,180 @@
|
|
|
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 time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from .agent import Agent
|
|
13
|
+
|
|
14
|
+
_JSON_INSTRUCTIONS = (
|
|
15
|
+
"Respond with JSON only (no markdown/backticks/extra text).\n"
|
|
16
|
+
"Return a single JSON object with keys:\n"
|
|
17
|
+
" status: string (one line)\n"
|
|
18
|
+
" continue: boolean\n"
|
|
19
|
+
" comments: string\n"
|
|
20
|
+
"To stop this watch loop, set continue to false."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def watch(minutes, prompt, cwd=None, yolo=True, flags=None):
|
|
25
|
+
"""Run a periodic watch loop.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
minutes: Tick interval in whole minutes (>= 1).
|
|
29
|
+
prompt: The original instruction prompt.
|
|
30
|
+
cwd: Optional working directory for the Codex session.
|
|
31
|
+
yolo: Whether to pass --yolo to Codex.
|
|
32
|
+
flags: Additional raw CLI flags to pass to Codex.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The last parsed JSON status object.
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(minutes, int):
|
|
38
|
+
raise TypeError("minutes must be an integer")
|
|
39
|
+
if minutes < 1:
|
|
40
|
+
raise ValueError("minutes must be >= 1")
|
|
41
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
42
|
+
raise ValueError("prompt must be a non-empty string")
|
|
43
|
+
|
|
44
|
+
interval = minutes * 60
|
|
45
|
+
session = Agent(cwd, yolo, None, flags)
|
|
46
|
+
|
|
47
|
+
last_sent = None
|
|
48
|
+
last_result = None
|
|
49
|
+
tick = 0
|
|
50
|
+
|
|
51
|
+
while True:
|
|
52
|
+
tick += 1
|
|
53
|
+
sent_at = time.monotonic()
|
|
54
|
+
elapsed = None if last_sent is None else sent_at - last_sent
|
|
55
|
+
last_sent = sent_at
|
|
56
|
+
|
|
57
|
+
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
|
58
|
+
message = _build_tick_prompt(prompt, now, elapsed, tick)
|
|
59
|
+
output = session(message)
|
|
60
|
+
result = _parse_status(output)
|
|
61
|
+
last_result = result
|
|
62
|
+
_print_status(now, elapsed, tick, result)
|
|
63
|
+
|
|
64
|
+
if not result["continue"]:
|
|
65
|
+
return last_result
|
|
66
|
+
|
|
67
|
+
next_tick = sent_at + interval
|
|
68
|
+
sleep_seconds = next_tick - time.monotonic()
|
|
69
|
+
if sleep_seconds > 0:
|
|
70
|
+
time.sleep(sleep_seconds)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_tick_prompt(prompt, now, elapsed, tick):
|
|
74
|
+
lines = [
|
|
75
|
+
f"Tick {tick}.",
|
|
76
|
+
f"Local time now: {now}",
|
|
77
|
+
]
|
|
78
|
+
if elapsed is not None:
|
|
79
|
+
lines.append(
|
|
80
|
+
"Time since last tick: "
|
|
81
|
+
f"{_format_minutes_seconds(elapsed)} ({int(round(elapsed))}s)"
|
|
82
|
+
)
|
|
83
|
+
lines.extend(
|
|
84
|
+
[
|
|
85
|
+
"",
|
|
86
|
+
"A reminder: your instructions are:",
|
|
87
|
+
prompt.strip(),
|
|
88
|
+
"",
|
|
89
|
+
_JSON_INSTRUCTIONS,
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
return "\n".join(lines).strip()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _format_minutes_seconds(seconds):
|
|
96
|
+
if seconds is None:
|
|
97
|
+
return ""
|
|
98
|
+
seconds = int(round(seconds))
|
|
99
|
+
if seconds < 0:
|
|
100
|
+
seconds = 0
|
|
101
|
+
minutes, seconds = divmod(seconds, 60)
|
|
102
|
+
return f"{minutes}m{seconds:02d}s"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_status(output):
|
|
106
|
+
text = _maybe_strip_code_fence(str(output or "").strip())
|
|
107
|
+
data = _try_parse_json(text)
|
|
108
|
+
if data is None:
|
|
109
|
+
snippet = text[:200].replace("\n", "\\n")
|
|
110
|
+
raise ValueError(f"Invalid JSON response. Snippet: {snippet}")
|
|
111
|
+
if not isinstance(data, dict):
|
|
112
|
+
raise ValueError("Status JSON must be an object.")
|
|
113
|
+
|
|
114
|
+
status = data.get("status")
|
|
115
|
+
cont = data.get("continue")
|
|
116
|
+
comments = data.get("comments")
|
|
117
|
+
|
|
118
|
+
if not isinstance(status, str):
|
|
119
|
+
raise ValueError("Status JSON missing string 'status'.")
|
|
120
|
+
if not isinstance(cont, bool):
|
|
121
|
+
raise ValueError("Status JSON missing boolean 'continue'.")
|
|
122
|
+
if comments is None:
|
|
123
|
+
comments = ""
|
|
124
|
+
if not isinstance(comments, str):
|
|
125
|
+
raise ValueError("Status JSON missing string 'comments'.")
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"status": _single_line(status),
|
|
129
|
+
"continue": cont,
|
|
130
|
+
"comments": comments,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _maybe_strip_code_fence(text):
|
|
135
|
+
if not text.startswith("```"):
|
|
136
|
+
return text
|
|
137
|
+
lines = text.splitlines()
|
|
138
|
+
if not lines:
|
|
139
|
+
return text
|
|
140
|
+
if lines[0].startswith("```"):
|
|
141
|
+
lines = lines[1:]
|
|
142
|
+
if lines and lines[-1].strip() == "```":
|
|
143
|
+
lines = lines[:-1]
|
|
144
|
+
return "\n".join(lines).strip()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _try_parse_json(text):
|
|
148
|
+
if not text:
|
|
149
|
+
return None
|
|
150
|
+
try:
|
|
151
|
+
return json.loads(text)
|
|
152
|
+
except json.JSONDecodeError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
start = text.find("{")
|
|
156
|
+
end = text.rfind("}")
|
|
157
|
+
if start == -1 or end == -1 or end <= start:
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
return json.loads(text[start : end + 1])
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _single_line(text):
|
|
166
|
+
return " ".join(text.replace("\r", " ").split())
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _print_status(now, elapsed, tick, result):
|
|
170
|
+
delta = ""
|
|
171
|
+
if elapsed is not None:
|
|
172
|
+
delta = f" +{_format_minutes_seconds(elapsed)}"
|
|
173
|
+
status = result.get("status", "")
|
|
174
|
+
cont = result.get("continue")
|
|
175
|
+
line = f"[watch {tick} {now}{delta}] {status} (continue={cont})".rstrip()
|
|
176
|
+
print(line)
|
|
177
|
+
comments = result.get("comments") or ""
|
|
178
|
+
if comments.strip():
|
|
179
|
+
print(comments.rstrip())
|
|
180
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: codexapi
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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,14 @@ 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.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
codexapi watch 5 "Run the benchmark and wait for results."
|
|
135
|
+
```
|
|
136
|
+
|
|
129
137
|
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
130
138
|
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
131
139
|
`.codexapi/ralph-loop.local.md` or running `codexapi ralph --cancel`.
|
|
@@ -189,6 +197,13 @@ the same conversation and returns only the agent's message.
|
|
|
189
197
|
- `welfare` (bool): when true, append welfare stop instructions to each prompt
|
|
190
198
|
and raise `WelfareStop` if the agent outputs `MAKE IT STOP`.
|
|
191
199
|
|
|
200
|
+
### `watch(minutes, prompt, cwd=None, yolo=True, flags=None) -> dict`
|
|
201
|
+
|
|
202
|
+
Runs a long-lived agent session and periodically "ticks" it with the current
|
|
203
|
+
local time and a reminder of `prompt`. Each tick expects JSON with keys:
|
|
204
|
+
`status` (one line), `continue` (bool), and `comments` (string). The loop stops
|
|
205
|
+
when `continue` is false.
|
|
206
|
+
|
|
192
207
|
### `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
208
|
|
|
194
209
|
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
|
|
File without changes
|