codexapi 0.1.7__tar.gz → 0.1.9__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.1.7/src/codexapi.egg-info → codexapi-0.1.9}/PKG-INFO +11 -1
- {codexapi-0.1.7 → codexapi-0.1.9}/README.md +10 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/pyproject.toml +1 -1
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi/__init__.py +1 -1
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi/cli.py +117 -0
- codexapi-0.1.9/src/codexapi/ralph.py +333 -0
- {codexapi-0.1.7 → codexapi-0.1.9/src/codexapi.egg-info}/PKG-INFO +11 -1
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi.egg-info/SOURCES.txt +1 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/LICENSE +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/setup.cfg +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi/__main__.py +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi/agent.py +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi/task.py +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi.egg-info/dependency_links.txt +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/src/codexapi.egg-info/entry_points.txt +0 -0
- {codexapi-0.1.7 → codexapi-0.1.9}/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.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Minimal Python API for running the Codex CLI.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: codex,agent,cli,openai
|
|
@@ -81,6 +81,16 @@ Resume a session and print the thread id to stderr:
|
|
|
81
81
|
codexapi --thread-id THREAD_ID --print-thread-id "Continue where we left off."
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
85
|
+
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
86
|
+
`.codexapi/ralph-loop.local.md` or running `codexapi --ralph-cancel`.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
codexapi --ralph "Fix the bug." --completion-promise DONE --max-iterations 5
|
|
90
|
+
codexapi --ralph --ralph-fresh "Try again from scratch." --max-iterations 3
|
|
91
|
+
codexapi --ralph-cancel --cwd /path/to/project
|
|
92
|
+
```
|
|
93
|
+
|
|
84
94
|
## API
|
|
85
95
|
|
|
86
96
|
### `agent(prompt, cwd=None, yolo=False, flags=None) -> str`
|
|
@@ -69,6 +69,16 @@ Resume a session and print the thread id to stderr:
|
|
|
69
69
|
codexapi --thread-id THREAD_ID --print-thread-id "Continue where we left off."
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
73
|
+
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
74
|
+
`.codexapi/ralph-loop.local.md` or running `codexapi --ralph-cancel`.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codexapi --ralph "Fix the bug." --completion-promise DONE --max-iterations 5
|
|
78
|
+
codexapi --ralph --ralph-fresh "Try again from scratch." --max-iterations 3
|
|
79
|
+
codexapi --ralph-cancel --cwd /path/to/project
|
|
80
|
+
```
|
|
81
|
+
|
|
72
82
|
## API
|
|
73
83
|
|
|
74
84
|
### `agent(prompt, cwd=None, yolo=False, flags=None) -> str`
|
|
@@ -12,6 +12,7 @@ from datetime import datetime
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from .agent import Agent, agent
|
|
15
|
+
from .ralph import cancel_ralph_loop, run_ralph_loop
|
|
15
16
|
from .task import TaskFailed, task
|
|
16
17
|
|
|
17
18
|
_SESSION_ID_RE = re.compile(
|
|
@@ -36,6 +37,7 @@ _COLUMN_TITLES = {
|
|
|
36
37
|
"tok": "TOK/S",
|
|
37
38
|
"in": "IN",
|
|
38
39
|
"out": "OUT",
|
|
40
|
+
"turn": "TURN",
|
|
39
41
|
"model": "MODEL",
|
|
40
42
|
"effort": "EFF",
|
|
41
43
|
"perm": "PERM",
|
|
@@ -309,6 +311,24 @@ def _format_token_total(value):
|
|
|
309
311
|
return str(value)
|
|
310
312
|
|
|
311
313
|
|
|
314
|
+
def _format_duration(seconds):
|
|
315
|
+
if seconds is None:
|
|
316
|
+
return "-"
|
|
317
|
+
if seconds < 0:
|
|
318
|
+
return "-"
|
|
319
|
+
total = int(seconds)
|
|
320
|
+
days, rem = divmod(total, 86400)
|
|
321
|
+
hours, rem = divmod(rem, 3600)
|
|
322
|
+
minutes, secs = divmod(rem, 60)
|
|
323
|
+
if days:
|
|
324
|
+
return f"{days}d{hours:02d}h"
|
|
325
|
+
if hours:
|
|
326
|
+
return f"{hours}h{minutes:02d}m"
|
|
327
|
+
if minutes:
|
|
328
|
+
return f"{minutes}m{secs:02d}s"
|
|
329
|
+
return f"{secs}s"
|
|
330
|
+
|
|
331
|
+
|
|
312
332
|
def _summarize_session(path, mtime):
|
|
313
333
|
prompt = None
|
|
314
334
|
prompt_fallback = None
|
|
@@ -569,6 +589,7 @@ def _layout_columns(width, id_width, show):
|
|
|
569
589
|
("tok", ">"),
|
|
570
590
|
("in", ">"),
|
|
571
591
|
("out", ">"),
|
|
592
|
+
("turn", ">"),
|
|
572
593
|
]
|
|
573
594
|
widths = {
|
|
574
595
|
"id": id_width,
|
|
@@ -576,6 +597,7 @@ def _layout_columns(width, id_width, show):
|
|
|
576
597
|
"tok": 7,
|
|
577
598
|
"in": 7,
|
|
578
599
|
"out": 7,
|
|
600
|
+
"turn": 7,
|
|
579
601
|
}
|
|
580
602
|
mins = {}
|
|
581
603
|
|
|
@@ -638,6 +660,16 @@ def _format_session(session, layout):
|
|
|
638
660
|
status = "RUN" if session.get("status") == "running" else "IDLE"
|
|
639
661
|
tok_s = session["tok_s"]
|
|
640
662
|
tok_s_str = "-" if tok_s is None else f"{tok_s:5.1f}"
|
|
663
|
+
last_user_ts = session.get("last_user_ts")
|
|
664
|
+
last_agent_ts = session.get("last_agent_ts")
|
|
665
|
+
if status == "RUN":
|
|
666
|
+
turn_seconds = (datetime.now() - last_user_ts).total_seconds() if last_user_ts else None
|
|
667
|
+
else:
|
|
668
|
+
if last_user_ts and last_agent_ts:
|
|
669
|
+
turn_seconds = (last_agent_ts - last_user_ts).total_seconds()
|
|
670
|
+
else:
|
|
671
|
+
turn_seconds = None
|
|
672
|
+
turn_str = _format_duration(turn_seconds)
|
|
641
673
|
meta = session.get("meta") or {}
|
|
642
674
|
model = meta.get("model") or meta.get("model_provider") or "-"
|
|
643
675
|
effort = meta.get("effort") or "-"
|
|
@@ -655,6 +687,7 @@ def _format_session(session, layout):
|
|
|
655
687
|
"tok": tok_s_str,
|
|
656
688
|
"in": total_in,
|
|
657
689
|
"out": total_out,
|
|
690
|
+
"turn": turn_str,
|
|
658
691
|
"model": _truncate_head(str(model), widths.get("model", 0)),
|
|
659
692
|
"effort": _truncate_head(str(effort), widths.get("effort", 0)),
|
|
660
693
|
"perm": _truncate_head(str(perm), widths.get("perm", 0)),
|
|
@@ -837,9 +870,22 @@ def main(argv=None):
|
|
|
837
870
|
if argv and argv[0] == "top":
|
|
838
871
|
_run_top(argv[1:])
|
|
839
872
|
return
|
|
873
|
+
ralph_help = (
|
|
874
|
+
"Ralph loop mode (--ralph):\n"
|
|
875
|
+
" Repeats the exact same prompt each iteration until a completion promise\n"
|
|
876
|
+
" is detected or --max-iterations is reached (0 means unlimited).\n"
|
|
877
|
+
" Completion promise: output <promise>TEXT</promise> where TEXT matches\n"
|
|
878
|
+
" --completion-promise after trimming/collapsing whitespace. CRITICAL RULE:\n"
|
|
879
|
+
" Only output the promise when it is completely and unequivocally TRUE.\n"
|
|
880
|
+
" Cancel by deleting .codexapi/ralph-loop.local.md or running --ralph-cancel.\n"
|
|
881
|
+
" Default reuses a single Codex thread; use --ralph-fresh for a new Agent\n"
|
|
882
|
+
" each iteration (no shared context).\n"
|
|
883
|
+
)
|
|
840
884
|
parser = argparse.ArgumentParser(
|
|
841
885
|
prog="codexapi",
|
|
842
886
|
description="Run Codex via the codexapi wrapper.",
|
|
887
|
+
epilog=ralph_help,
|
|
888
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
843
889
|
)
|
|
844
890
|
parser.add_argument(
|
|
845
891
|
"prompt",
|
|
@@ -870,10 +916,67 @@ def main(argv=None):
|
|
|
870
916
|
action="store_true",
|
|
871
917
|
help="Print the current thread id to stderr after running.",
|
|
872
918
|
)
|
|
919
|
+
parser.add_argument(
|
|
920
|
+
"--ralph",
|
|
921
|
+
action="store_true",
|
|
922
|
+
help="Run a Ralph loop that repeats the same prompt each iteration.",
|
|
923
|
+
)
|
|
924
|
+
parser.add_argument(
|
|
925
|
+
"--max-iterations",
|
|
926
|
+
type=int,
|
|
927
|
+
default=None,
|
|
928
|
+
help="Max iterations for --ralph (0 means unlimited).",
|
|
929
|
+
)
|
|
930
|
+
parser.add_argument(
|
|
931
|
+
"--completion-promise",
|
|
932
|
+
help="Promise text for --ralph to match in <promise>...</promise>.",
|
|
933
|
+
)
|
|
934
|
+
parser.add_argument(
|
|
935
|
+
"--ralph-fresh",
|
|
936
|
+
action="store_true",
|
|
937
|
+
help="With --ralph, start each iteration with a fresh Agent context.",
|
|
938
|
+
)
|
|
939
|
+
parser.add_argument(
|
|
940
|
+
"--ralph-cancel",
|
|
941
|
+
action="store_true",
|
|
942
|
+
help=(
|
|
943
|
+
"Cancel a Ralph loop by removing .codexapi/ralph-loop.local.md "
|
|
944
|
+
"(respects --cwd)."
|
|
945
|
+
),
|
|
946
|
+
)
|
|
873
947
|
|
|
874
948
|
args = parser.parse_args(argv)
|
|
949
|
+
if args.ralph_cancel:
|
|
950
|
+
if (
|
|
951
|
+
args.ralph
|
|
952
|
+
or args.task
|
|
953
|
+
or args.thread_id
|
|
954
|
+
or args.print_thread_id
|
|
955
|
+
or args.max_iterations is not None
|
|
956
|
+
or args.completion_promise is not None
|
|
957
|
+
or args.ralph_fresh
|
|
958
|
+
or args.check is not None
|
|
959
|
+
or args.prompt
|
|
960
|
+
):
|
|
961
|
+
raise SystemExit(
|
|
962
|
+
"--ralph-cancel cannot be combined with prompts or other modes."
|
|
963
|
+
)
|
|
964
|
+
print(cancel_ralph_loop(args.cwd))
|
|
965
|
+
return
|
|
875
966
|
if args.check is not None and not args.task:
|
|
876
967
|
raise SystemExit("--check requires --task.")
|
|
968
|
+
if not args.ralph and (
|
|
969
|
+
args.max_iterations is not None
|
|
970
|
+
or args.completion_promise is not None
|
|
971
|
+
or args.ralph_fresh
|
|
972
|
+
):
|
|
973
|
+
raise SystemExit(
|
|
974
|
+
"--max-iterations/--completion-promise/--ralph-fresh require --ralph."
|
|
975
|
+
)
|
|
976
|
+
if args.ralph and (args.task or args.thread_id or args.print_thread_id):
|
|
977
|
+
raise SystemExit(
|
|
978
|
+
"--task/--thread-id/--print-thread-id are not supported with --ralph."
|
|
979
|
+
)
|
|
877
980
|
if args.task and (args.thread_id or args.print_thread_id):
|
|
878
981
|
raise SystemExit("--thread-id/--print-thread-id are not supported with --task.")
|
|
879
982
|
|
|
@@ -881,6 +984,20 @@ def main(argv=None):
|
|
|
881
984
|
|
|
882
985
|
exit_code = 0
|
|
883
986
|
|
|
987
|
+
if args.ralph:
|
|
988
|
+
max_iterations = args.max_iterations if args.max_iterations is not None else 0
|
|
989
|
+
if max_iterations < 0:
|
|
990
|
+
raise SystemExit("--max-iterations must be >= 0.")
|
|
991
|
+
run_ralph_loop(
|
|
992
|
+
prompt,
|
|
993
|
+
args.cwd,
|
|
994
|
+
args.yolo,
|
|
995
|
+
args.flags,
|
|
996
|
+
max_iterations,
|
|
997
|
+
args.completion_promise,
|
|
998
|
+
args.ralph_fresh,
|
|
999
|
+
)
|
|
1000
|
+
return
|
|
884
1001
|
if args.task:
|
|
885
1002
|
check = args.check if args.check is not None else prompt
|
|
886
1003
|
try:
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Ralph Wiggum-style loop for Codex runs."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from .agent import Agent
|
|
9
|
+
|
|
10
|
+
_STATE_DIR = ".codexapi"
|
|
11
|
+
_STATE_FILE = "ralph-loop.local.md"
|
|
12
|
+
_PROMISE_RE = re.compile(r"<promise>(.*?)</promise>", re.DOTALL)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_ralph_loop(
|
|
16
|
+
prompt,
|
|
17
|
+
cwd=None,
|
|
18
|
+
yolo=False,
|
|
19
|
+
flags=None,
|
|
20
|
+
max_iterations=0,
|
|
21
|
+
completion_promise=None,
|
|
22
|
+
fresh=False,
|
|
23
|
+
):
|
|
24
|
+
"""Run a Ralph Wiggum-style loop that repeats the same prompt.
|
|
25
|
+
|
|
26
|
+
The loop writes `.codexapi/ralph-loop.local.md` in the target cwd and keeps
|
|
27
|
+
sending the exact same prompt each iteration until one of these happens:
|
|
28
|
+
- A completion promise is matched.
|
|
29
|
+
- `max_iterations` is reached (0 means unlimited).
|
|
30
|
+
- The state file is removed (cancel).
|
|
31
|
+
- An error or KeyboardInterrupt.
|
|
32
|
+
|
|
33
|
+
To complete with a promise, the agent must output:
|
|
34
|
+
<promise>TEXT</promise>
|
|
35
|
+
`TEXT` is trimmed and whitespace-collapsed before an exact match against
|
|
36
|
+
`completion_promise`. CRITICAL RULE: If a completion promise is set, you
|
|
37
|
+
may ONLY output it when the statement is completely and unequivocally TRUE.
|
|
38
|
+
Do not output false promises to escape the loop.
|
|
39
|
+
|
|
40
|
+
By default a single Agent instance is reused for shared context. Set
|
|
41
|
+
`fresh=True` to create a new Agent each iteration for a clean context.
|
|
42
|
+
Cancel by deleting the state file or running `codexapi --ralph-cancel`.
|
|
43
|
+
"""
|
|
44
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
45
|
+
raise ValueError("prompt must be a non-empty string")
|
|
46
|
+
if completion_promise is not None and not isinstance(completion_promise, str):
|
|
47
|
+
raise TypeError("completion_promise must be a string or None")
|
|
48
|
+
if max_iterations < 0:
|
|
49
|
+
raise ValueError("max_iterations must be >= 0")
|
|
50
|
+
|
|
51
|
+
state_path = _state_path(cwd)
|
|
52
|
+
_ensure_state_dir(state_path)
|
|
53
|
+
|
|
54
|
+
started_at = _utc_now()
|
|
55
|
+
iteration = 1
|
|
56
|
+
_write_state(
|
|
57
|
+
state_path,
|
|
58
|
+
iteration,
|
|
59
|
+
max_iterations,
|
|
60
|
+
completion_promise,
|
|
61
|
+
started_at,
|
|
62
|
+
prompt,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
max_label = str(max_iterations) if max_iterations > 0 else "unlimited"
|
|
66
|
+
if completion_promise is None:
|
|
67
|
+
promise_label = "none (runs forever)"
|
|
68
|
+
else:
|
|
69
|
+
promise_label = (
|
|
70
|
+
f"{completion_promise} (ONLY output when TRUE - do not lie!)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
print(
|
|
74
|
+
"\n".join(
|
|
75
|
+
[
|
|
76
|
+
"Ralph loop activated.",
|
|
77
|
+
"",
|
|
78
|
+
f"Iteration: {iteration}",
|
|
79
|
+
f"Max iterations: {max_label}",
|
|
80
|
+
f"Completion promise: {promise_label}",
|
|
81
|
+
"",
|
|
82
|
+
"The loop will resend the SAME PROMPT each iteration.",
|
|
83
|
+
"Cancel by deleting .codexapi/ralph-loop.local.md or running",
|
|
84
|
+
"codexapi --ralph-cancel.",
|
|
85
|
+
"No manual stop beyond max iterations or completion promise.",
|
|
86
|
+
"",
|
|
87
|
+
"To monitor: head -10 .codexapi/ralph-loop.local.md",
|
|
88
|
+
"",
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
print(prompt)
|
|
93
|
+
|
|
94
|
+
if completion_promise is not None:
|
|
95
|
+
print(
|
|
96
|
+
"\n".join(
|
|
97
|
+
[
|
|
98
|
+
"",
|
|
99
|
+
"CRITICAL - Ralph Loop Completion Promise",
|
|
100
|
+
"",
|
|
101
|
+
"To complete this loop, output this EXACT text:",
|
|
102
|
+
f" <promise>{completion_promise}</promise>",
|
|
103
|
+
"",
|
|
104
|
+
"STRICT REQUIREMENTS (DO NOT VIOLATE):",
|
|
105
|
+
" - Use <promise> XML tags EXACTLY as shown above",
|
|
106
|
+
" - The statement MUST be completely and unequivocally TRUE",
|
|
107
|
+
" - Do NOT output false statements to exit the loop",
|
|
108
|
+
" - Do NOT lie even if you think you should exit",
|
|
109
|
+
"",
|
|
110
|
+
"CRITICAL RULE: If a completion promise is set, you may ONLY",
|
|
111
|
+
"output it when the statement is completely and unequivocally",
|
|
112
|
+
"TRUE. Do not output false promises to escape the loop, even if",
|
|
113
|
+
"you think you're stuck or should exit for other reasons. The",
|
|
114
|
+
"loop is designed to continue until genuine completion.",
|
|
115
|
+
"",
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
runner = None
|
|
121
|
+
last_message = None
|
|
122
|
+
state_missing = False
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
while True:
|
|
126
|
+
if not os.path.exists(state_path):
|
|
127
|
+
state_missing = True
|
|
128
|
+
print("Ralph loop canceled: state file removed.")
|
|
129
|
+
return last_message
|
|
130
|
+
|
|
131
|
+
print(_status_line(iteration, completion_promise))
|
|
132
|
+
|
|
133
|
+
if fresh:
|
|
134
|
+
runner = Agent(cwd, yolo, None, flags)
|
|
135
|
+
elif runner is None:
|
|
136
|
+
runner = Agent(cwd, yolo, None, flags)
|
|
137
|
+
|
|
138
|
+
message = runner(prompt)
|
|
139
|
+
print(message)
|
|
140
|
+
last_message = message
|
|
141
|
+
|
|
142
|
+
if not os.path.exists(state_path):
|
|
143
|
+
state_missing = True
|
|
144
|
+
print("Ralph loop canceled: state file removed.")
|
|
145
|
+
return last_message
|
|
146
|
+
|
|
147
|
+
if max_iterations > 0 and iteration >= max_iterations:
|
|
148
|
+
print(f"Ralph loop: Max iterations ({max_iterations}) reached.")
|
|
149
|
+
return message
|
|
150
|
+
|
|
151
|
+
if promise_matches(message, completion_promise):
|
|
152
|
+
print(
|
|
153
|
+
"Ralph loop: Detected "
|
|
154
|
+
f"<promise>{completion_promise}</promise>"
|
|
155
|
+
)
|
|
156
|
+
return message
|
|
157
|
+
|
|
158
|
+
if not os.path.exists(state_path):
|
|
159
|
+
state_missing = True
|
|
160
|
+
print("Ralph loop canceled: state file removed.")
|
|
161
|
+
return last_message
|
|
162
|
+
|
|
163
|
+
iteration += 1
|
|
164
|
+
_write_state(
|
|
165
|
+
state_path,
|
|
166
|
+
iteration,
|
|
167
|
+
max_iterations,
|
|
168
|
+
completion_promise,
|
|
169
|
+
started_at,
|
|
170
|
+
prompt,
|
|
171
|
+
)
|
|
172
|
+
except KeyboardInterrupt:
|
|
173
|
+
print("Ralph loop interrupted.", file=sys.stderr)
|
|
174
|
+
raise SystemExit(130)
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
print(f"Ralph loop stopped: {exc}", file=sys.stderr)
|
|
177
|
+
raise SystemExit(1)
|
|
178
|
+
finally:
|
|
179
|
+
if not state_missing:
|
|
180
|
+
_cleanup_state(state_path)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def cancel_ralph_loop(cwd=None):
|
|
184
|
+
"""Cancel the Ralph loop by removing the state file."""
|
|
185
|
+
state_path = _state_path(cwd)
|
|
186
|
+
if not os.path.exists(state_path):
|
|
187
|
+
return "No active Ralph loop state found."
|
|
188
|
+
|
|
189
|
+
iteration = None
|
|
190
|
+
try:
|
|
191
|
+
fields = _read_state_fields(state_path)
|
|
192
|
+
value = fields.get("iteration")
|
|
193
|
+
if value and value.isdigit():
|
|
194
|
+
iteration = int(value)
|
|
195
|
+
except OSError:
|
|
196
|
+
iteration = None
|
|
197
|
+
|
|
198
|
+
_cleanup_state(state_path)
|
|
199
|
+
|
|
200
|
+
if iteration is None:
|
|
201
|
+
return "Canceled Ralph loop."
|
|
202
|
+
return f"Canceled Ralph loop at iteration {iteration}."
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def promise_matches(message, completion_promise):
|
|
206
|
+
"""Return True when the message contains the matching completion promise."""
|
|
207
|
+
if completion_promise is None:
|
|
208
|
+
return False
|
|
209
|
+
extracted = _extract_promise(message)
|
|
210
|
+
if extracted is None:
|
|
211
|
+
return False
|
|
212
|
+
return extracted == completion_promise
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _extract_promise(message):
|
|
216
|
+
"""Extract and normalize the first <promise>...</promise> tag from text."""
|
|
217
|
+
match = _PROMISE_RE.search(message)
|
|
218
|
+
if not match:
|
|
219
|
+
return None
|
|
220
|
+
return _normalize_whitespace(match.group(1))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _normalize_whitespace(text):
|
|
224
|
+
"""Trim and collapse whitespace to match the Claude plugin behavior."""
|
|
225
|
+
return " ".join(text.split())
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _state_path(cwd):
|
|
229
|
+
"""Return the absolute path for the Ralph loop state file."""
|
|
230
|
+
root = os.fspath(cwd) if cwd else os.getcwd()
|
|
231
|
+
return os.path.join(root, _STATE_DIR, _STATE_FILE)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _ensure_state_dir(state_path):
|
|
235
|
+
"""Ensure the Ralph loop state directory exists."""
|
|
236
|
+
os.makedirs(os.path.dirname(state_path), exist_ok=True)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _write_state(
|
|
240
|
+
state_path,
|
|
241
|
+
iteration,
|
|
242
|
+
max_iterations,
|
|
243
|
+
completion_promise,
|
|
244
|
+
started_at,
|
|
245
|
+
prompt,
|
|
246
|
+
):
|
|
247
|
+
"""Write the Ralph loop state file atomically."""
|
|
248
|
+
content = _state_content(
|
|
249
|
+
iteration,
|
|
250
|
+
max_iterations,
|
|
251
|
+
completion_promise,
|
|
252
|
+
started_at,
|
|
253
|
+
prompt,
|
|
254
|
+
)
|
|
255
|
+
temp_path = f"{state_path}.tmp.{os.getpid()}"
|
|
256
|
+
with open(temp_path, "w", encoding="utf-8") as handle:
|
|
257
|
+
handle.write(content)
|
|
258
|
+
os.replace(temp_path, state_path)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _state_content(
|
|
262
|
+
iteration,
|
|
263
|
+
max_iterations,
|
|
264
|
+
completion_promise,
|
|
265
|
+
started_at,
|
|
266
|
+
prompt,
|
|
267
|
+
):
|
|
268
|
+
"""Build the YAML frontmatter state file content."""
|
|
269
|
+
completion_value = _format_completion_promise(completion_promise)
|
|
270
|
+
lines = [
|
|
271
|
+
"---",
|
|
272
|
+
"active: true",
|
|
273
|
+
f"iteration: {iteration}",
|
|
274
|
+
f"max_iterations: {max_iterations}",
|
|
275
|
+
f"completion_promise: {completion_value}",
|
|
276
|
+
f"started_at: \"{started_at}\"",
|
|
277
|
+
"---",
|
|
278
|
+
"",
|
|
279
|
+
prompt,
|
|
280
|
+
]
|
|
281
|
+
return "\n".join(lines)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _format_completion_promise(completion_promise):
|
|
285
|
+
"""Format the completion promise to match the plugin frontmatter."""
|
|
286
|
+
if completion_promise is None:
|
|
287
|
+
return "null"
|
|
288
|
+
return f"\"{completion_promise}\""
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _read_state_fields(state_path):
|
|
292
|
+
"""Read YAML frontmatter fields from the Ralph loop state file."""
|
|
293
|
+
with open(state_path, "r", encoding="utf-8") as handle:
|
|
294
|
+
lines = handle.read().splitlines()
|
|
295
|
+
if not lines or lines[0].strip() != "---":
|
|
296
|
+
return {}
|
|
297
|
+
|
|
298
|
+
fields = {}
|
|
299
|
+
for line in lines[1:]:
|
|
300
|
+
if line.strip() == "---":
|
|
301
|
+
break
|
|
302
|
+
if ":" not in line:
|
|
303
|
+
continue
|
|
304
|
+
key, value = line.split(":", 1)
|
|
305
|
+
fields[key.strip()] = value.strip()
|
|
306
|
+
return fields
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _cleanup_state(state_path):
|
|
310
|
+
"""Remove the Ralph loop state file if it exists."""
|
|
311
|
+
try:
|
|
312
|
+
os.remove(state_path)
|
|
313
|
+
except FileNotFoundError:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _status_line(iteration, completion_promise):
|
|
318
|
+
"""Build the per-iteration status line for the Ralph loop."""
|
|
319
|
+
if completion_promise is None:
|
|
320
|
+
return (
|
|
321
|
+
f"Ralph iteration {iteration} | "
|
|
322
|
+
"No completion promise set - loop runs infinitely"
|
|
323
|
+
)
|
|
324
|
+
return (
|
|
325
|
+
f"Ralph iteration {iteration} | To stop: output "
|
|
326
|
+
f"<promise>{completion_promise}</promise> "
|
|
327
|
+
"(ONLY when statement is TRUE - do not lie to exit!)"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _utc_now():
|
|
332
|
+
"""Return a UTC timestamp string matching the Claude plugin."""
|
|
333
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: codexapi
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Minimal Python API for running the Codex CLI.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: codex,agent,cli,openai
|
|
@@ -81,6 +81,16 @@ Resume a session and print the thread id to stderr:
|
|
|
81
81
|
codexapi --thread-id THREAD_ID --print-thread-id "Continue where we left off."
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
Ralph loop mode repeats the same prompt until a completion promise or a max
|
|
85
|
+
iteration cap is hit (0 means unlimited). Cancel by deleting
|
|
86
|
+
`.codexapi/ralph-loop.local.md` or running `codexapi --ralph-cancel`.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
codexapi --ralph "Fix the bug." --completion-promise DONE --max-iterations 5
|
|
90
|
+
codexapi --ralph --ralph-fresh "Try again from scratch." --max-iterations 3
|
|
91
|
+
codexapi --ralph-cancel --cwd /path/to/project
|
|
92
|
+
```
|
|
93
|
+
|
|
84
94
|
## API
|
|
85
95
|
|
|
86
96
|
### `agent(prompt, cwd=None, yolo=False, flags=None) -> str`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|