Skip to content

Commit 849143b

Browse files
Kasper Jungeclaude
authored andcommitted
docs: cut API page — remove multi-run management, Slack example, verbose tables
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3271bc4 commit 849143b

1 file changed

Lines changed: 26 additions & 263 deletions

File tree

docs/api.md

Lines changed: 26 additions & 263 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
---
2-
description: Use ralphify as a Python library — run loops programmatically, listen to events, manage multiple runs, and discover primitives without the CLI.
2+
description: Use ralphify as a Python library — run loops programmatically, listen to events, and discover primitives without the CLI.
33
---
44

55
# Python API
66

7-
Ralphify can be used as a Python library, not just a CLI. This is useful when you want to:
7+
Ralphify can be used as a Python library. This is useful when you want to embed the loop in a larger automation pipeline, react to events programmatically, or script runs with more control than the CLI provides.
88

9-
- Embed the loop in a larger automation pipeline
10-
- Build custom orchestration on top of ralphify
11-
- Listen to events and react programmatically (e.g. send Slack alerts on failures)
12-
- Script runs with more control than the CLI provides
9+
All public API is available from the top-level `ralphify` package.
1310

1411
## Quick start
1512

@@ -26,58 +23,28 @@ state = RunState(run_id="my-run")
2623
run_loop(config, state)
2724
```
2825

29-
This runs the same loop as `ralph run -n 3`, using `RALPH.md` as the prompt. When the loop finishes, `state` contains the results.
26+
This runs the same loop as `ralph run -n 3`. When the loop finishes, `state` contains the results.
3027

31-
## Core function
28+
## `run_loop(config, state, emitter=None)`
3229

33-
### `run_loop(config, state, emitter=None)`
34-
35-
The main loop. Discovers primitives, assembles prompts, pipes them to the agent, runs checks, and repeats.
36-
37-
```python
38-
from ralphify import run_loop, RunConfig, RunState, NullEmitter
39-
40-
config = RunConfig(
41-
command="claude",
42-
args=["-p", "--dangerously-skip-permissions"],
43-
prompt_file="RALPH.md",
44-
max_iterations=5,
45-
stop_on_error=True,
46-
timeout=300,
47-
log_dir="ralph_logs",
48-
)
49-
state = RunState(run_id="build-features")
50-
51-
run_loop(config, state)
52-
53-
print(f"Completed: {state.completed}")
54-
print(f"Failed: {state.failed}")
55-
print(f"Total: {state.total}")
56-
```
30+
The main loop. Discovers primitives, assembles prompts, pipes them to the agent, runs checks, and repeats. Blocks until the loop finishes.
5731

5832
| Parameter | Type | Description |
5933
|---|---|---|
6034
| `config` | `RunConfig` | All settings for the run |
6135
| `state` | `RunState` | Observable state — counters, status, control methods |
6236
| `emitter` | `EventEmitter | None` | Event listener. `None` uses `NullEmitter` (silent) |
6337

64-
The function blocks until the loop finishes (iteration limit reached, stop requested, error, or `KeyboardInterrupt`).
65-
66-
## Configuration
67-
68-
### `RunConfig`
38+
## `RunConfig`
6939

70-
A dataclass with all run settings. Fields match the CLI options.
40+
Fields match the CLI options:
7141

7242
```python
73-
from pathlib import Path
74-
from ralphify import RunConfig
75-
7643
config = RunConfig(
7744
command="claude",
7845
args=["-p", "--dangerously-skip-permissions"],
7946
prompt_file="RALPH.md",
80-
prompt_text=None, # Ad-hoc prompt text (overrides prompt_file)
47+
prompt_text=None, # Ad-hoc prompt (overrides prompt_file)
8148
prompt_name=None, # Named ralph from .ralphify/ralphs/
8249
max_iterations=10,
8350
delay=2.0,
@@ -88,102 +55,45 @@ config = RunConfig(
8855
)
8956
```
9057

91-
| Field | Type | Default | CLI equivalent |
92-
|---|---|---|---|
93-
| `command` | `str` || `ralph.toml [agent] command` |
94-
| `args` | `list[str]` || `ralph.toml [agent] args` |
95-
| `prompt_file` | `str` || `ralph.toml [agent] ralph` or `-f` |
96-
| `prompt_text` | `str | None` | `None` | `-p` |
97-
| `prompt_name` | `str | None` | `None` | `ralph run <name>` |
98-
| `max_iterations` | `int | None` | `None` | `-n` |
99-
| `delay` | `float` | `0` | `-d` / `--delay` |
100-
| `timeout` | `float | None` | `None` | `-t` / `--timeout` |
101-
| `stop_on_error` | `bool` | `False` | `-s` / `--stop-on-error` |
102-
| `log_dir` | `str | None` | `None` | `-l` / `--log-dir` |
103-
| `project_root` | `Path` | `Path(".")` | Working directory |
104-
105-
`RunConfig` is mutable — you can change fields mid-run (e.g. increase `max_iterations`), and the loop picks up changes at the next iteration boundary.
58+
`RunConfig` is mutable — you can change fields mid-run, and the loop picks up changes at the next iteration boundary.
10659

107-
### `RunState`
60+
## `RunState`
10861

109-
Observable state for a running loop. Created with a `run_id` and updated by the engine as iterations execute.
62+
Observable state for a running loop:
11063

11164
```python
112-
from ralphify import RunState
113-
11465
state = RunState(run_id="my-run")
66+
run_loop(config, state)
11567

116-
# After run_loop() finishes:
11768
print(state.status) # RunStatus.COMPLETED
118-
print(state.iteration) # 5 (last iteration number)
11969
print(state.completed) # 4
12070
print(state.failed) # 1
121-
print(state.timed_out) # 0 (subset of failed)
122-
print(state.total) # 5 (completed + failed)
123-
print(state.started_at) # datetime (UTC)
71+
print(state.total) # 5
12472
```
12573

126-
| Property / Field | Type | Description |
127-
|---|---|---|
128-
| `run_id` | `str` | Unique identifier for this run |
129-
| `status` | `RunStatus` | Current lifecycle status |
130-
| `iteration` | `int` | Current iteration number (1-indexed) |
131-
| `completed` | `int` | Iterations that succeeded |
132-
| `failed` | `int` | Iterations that failed (includes timed out) |
133-
| `timed_out` | `int` | Iterations that timed out (subset of `failed`) |
134-
| `total` | `int` | `completed + failed` |
135-
| `started_at` | `datetime | None` | UTC timestamp when the run started |
74+
### Control methods
13675

137-
#### Control methods
138-
139-
`RunState` provides thread-safe methods to control the loop from another thread:
76+
Thread-safe methods for controlling the loop from another thread:
14077

14178
```python
14279
state.request_stop() # Stop after current iteration
14380
state.request_pause() # Pause between iterations
14481
state.request_resume() # Resume a paused loop
14582
state.request_reload() # Re-discover primitives before next iteration
146-
147-
state.stop_requested # bool — whether stop was requested
148-
state.paused # bool — whether currently paused
14983
```
15084

151-
These are useful when running the loop in a background thread (see [Multi-run management](#multi-run-management) below).
152-
153-
### `RunStatus`
154-
155-
Enum representing the lifecycle of a run.
156-
157-
| Value | Description |
158-
|---|---|
159-
| `PENDING` | Created but not started |
160-
| `RUNNING` | Loop is executing iterations |
161-
| `PAUSED` | Paused between iterations |
162-
| `STOPPED` | Stopped by user request |
163-
| `COMPLETED` | Reached iteration limit or finished naturally |
164-
| `FAILED` | Crashed with an exception |
165-
16685
## Event system
16786

168-
The loop emits structured events so you can observe progress without coupling to the engine internals.
169-
170-
### Listening to events
171-
172-
Implement the `EventEmitter` protocol — a single `emit(event)` method:
87+
The loop emits structured events. Implement the `EventEmitter` protocol (a single `emit(event)` method) to listen:
17388

17489
```python
175-
from ralphify import Event, EventEmitter, EventType, RunConfig, RunState, run_loop
90+
from ralphify import Event, EventType, RunConfig, RunState, run_loop
17691

17792

17893
class MyEmitter:
179-
"""Custom event listener that prints iteration results."""
180-
18194
def emit(self, event: Event) -> None:
18295
if event.type == EventType.ITERATION_COMPLETED:
183-
duration = event.data["duration_formatted"]
184-
print(f"Iteration {event.data['iteration']} completed ({duration})")
185-
elif event.type == EventType.ITERATION_FAILED:
186-
print(f"Iteration {event.data['iteration']} failed (exit {event.data['returncode']})")
96+
print(f"Iteration {event.data['iteration']} completed")
18797
elif event.type == EventType.CHECK_FAILED:
18898
print(f" Check '{event.data['name']}' failed")
18999

@@ -193,181 +103,34 @@ state = RunState(run_id="observed-run")
193103
run_loop(config, state, emitter=MyEmitter())
194104
```
195105

196-
### `Event`
197-
198-
Every event has these fields:
199-
200-
| Field | Type | Description |
201-
|---|---|---|
202-
| `type` | `EventType` | What happened |
203-
| `run_id` | `str` | Which run produced this event |
204-
| `data` | `dict` | Event-specific data |
205-
| `timestamp` | `datetime` | UTC timestamp |
206-
207-
Use `event.to_dict()` to serialize for JSON transport.
208-
209-
### `EventType`
210-
211-
Events cover the full run lifecycle: `RUN_STARTED`, `RUN_STOPPED`, `RUN_PAUSED`, `RUN_RESUMED`, iteration events (`ITERATION_STARTED`, `ITERATION_COMPLETED`, `ITERATION_FAILED`, `ITERATION_TIMED_OUT`), check events (`CHECKS_STARTED`, `CHECK_PASSED`, `CHECK_FAILED`, `CHECKS_COMPLETED`), prompt assembly (`CONTEXTS_RESOLVED`, `PROMPT_ASSEMBLED`), and streaming (`AGENT_ACTIVITY`, `LOG_MESSAGE`).
212-
213-
Each event's `data` dict contains relevant fields (iteration number, exit codes, durations, output text, etc.). Inspect `event.data.keys()` or see the `EventType` enum in source for the full schema.
214-
215-
### Built-in emitters
216-
217-
| Emitter | Description | Use case |
218-
|---|---|---|
219-
| `NullEmitter` | Discards all events | Tests, silent runs |
220-
| `QueueEmitter` | Pushes events into a `queue.Queue` | Async consumers, UI layers |
221-
| `FanoutEmitter` | Broadcasts to multiple emitters | Combining logging + monitoring |
222-
223-
```python
224-
from ralphify import QueueEmitter, FanoutEmitter
225-
226-
# Queue for async consumption
227-
q_emitter = QueueEmitter()
228-
229-
# Combine multiple listeners
230-
fanout = FanoutEmitter([q_emitter, MyEmitter()])
231-
232-
run_loop(config, state, emitter=fanout)
233-
234-
# Drain events from the queue
235-
while not q_emitter.queue.empty():
236-
event = q_emitter.queue.get()
237-
print(event.to_dict())
238-
```
239-
240-
## Multi-run management
241-
242-
`RunManager` orchestrates concurrent runs in background threads.
243-
244-
```python
245-
from ralphify import RunManager, RunConfig
246-
247-
manager = RunManager()
248-
249-
# Create and start a run
250-
config = RunConfig(
251-
command="claude",
252-
args=["-p", "--dangerously-skip-permissions"],
253-
prompt_file="RALPH.md",
254-
max_iterations=5,
255-
)
256-
managed = manager.create_run(config)
257-
manager.start_run(managed.state.run_id)
258-
259-
# Check status
260-
print(managed.state.status) # RunStatus.RUNNING
261-
print(managed.state.completed) # 2
262-
263-
# Control the run
264-
manager.pause_run(managed.state.run_id)
265-
manager.resume_run(managed.state.run_id)
266-
manager.stop_run(managed.state.run_id)
267-
268-
# List all runs
269-
for run in manager.list_runs():
270-
print(f"{run.state.run_id}: {run.state.status.value}")
271-
```
106+
Each `Event` has `type` (`EventType`), `run_id`, `data` (dict), and `timestamp`. Use `event.to_dict()` to serialize.
272107

273-
`RunManager` provides `create_run(config)`, `start_run(run_id)`, `stop_run(run_id)`, `pause_run(run_id)`, `resume_run(run_id)`, `list_runs()`, and `get_run(run_id)`. Each run is wrapped in a `ManagedRun` with `config`, `state`, `emitter` (QueueEmitter), and `thread` fields. Use `managed.add_listener(emitter)` to register additional event listeners before starting.
108+
Built-in emitters: `NullEmitter` (silent), `QueueEmitter` (pushes to a `queue.Queue`), `FanoutEmitter` (broadcasts to multiple emitters).
274109

275110
## Primitive discovery
276111

277-
Discover checks, contexts, instructions, and ralphs without running the loop.
112+
Discover checks, contexts, instructions, and ralphs without running the loop:
278113

279114
```python
280115
from pathlib import Path
281116
from ralphify import discover_checks, discover_contexts, discover_instructions, discover_ralphs
282117

283118
root = Path(".")
284119

285-
checks = discover_checks(root)
286-
for check in checks:
120+
for check in discover_checks(root):
287121
print(f"Check: {check.name}, command: {check.command}, enabled: {check.enabled}")
288122

289-
contexts = discover_contexts(root)
290-
for ctx in contexts:
123+
for ctx in discover_contexts(root):
291124
print(f"Context: {ctx.name}, command: {ctx.command}")
292-
293-
instructions = discover_instructions(root)
294-
for inst in instructions:
295-
print(f"Instruction: {inst.name}, enabled: {inst.enabled}")
296-
297-
ralphs = discover_ralphs(root)
298-
for ralph in ralphs:
299-
print(f"Ralph: {ralph.name}, description: {ralph.description}")
300125
```
301126

302-
### Running primitives
127+
Run discovered primitives directly:
303128

304129
```python
305130
from ralphify import run_all_checks, run_all_contexts
306131

307-
# Run all checks and get results
308132
enabled_checks = [c for c in discover_checks(root) if c.enabled]
309133
results = run_all_checks(enabled_checks, root)
310134
for r in results:
311-
print(f" {r.check.name}: {'PASS' if r.passed else 'FAIL'} (exit {r.exit_code})")
312-
313-
# Run all contexts and get output
314-
enabled_contexts = [c for c in discover_contexts(root) if c.enabled]
315-
context_results = run_all_contexts(enabled_contexts, root)
316-
for cr in context_results:
317-
print(f" {cr.context.name}: {len(cr.output)} chars of output")
135+
print(f" {r.check.name}: {'PASS' if r.passed else 'FAIL'}")
318136
```
319-
320-
### Resolving ralphs
321-
322-
```python
323-
from ralphify import resolve_ralph_name
324-
325-
# Look up a named ralph and get the path to its RALPH.md
326-
ralph_path = resolve_ralph_name("docs", Path("."))
327-
if ralph_path:
328-
print(f"Found: {ralph_path}")
329-
else:
330-
print("Named ralph 'docs' not found")
331-
```
332-
333-
## Example: Slack notification on failure
334-
335-
A practical example combining the API with external integrations:
336-
337-
```python
338-
import requests
339-
from ralphify import Event, EventType, RunConfig, RunState, run_loop
340-
341-
342-
class SlackNotifier:
343-
"""Send a Slack message when a run finishes with failures."""
344-
345-
def __init__(self, webhook_url: str):
346-
self.webhook_url = webhook_url
347-
348-
def emit(self, event: Event) -> None:
349-
if event.type != EventType.RUN_STOPPED:
350-
return
351-
failed = event.data.get("failed", 0)
352-
if failed == 0:
353-
return
354-
total = event.data.get("total", 0)
355-
completed = event.data.get("completed", 0)
356-
requests.post(self.webhook_url, json={
357-
"text": f"Ralph loop finished: {completed}/{total} passed, {failed} failed."
358-
})
359-
360-
361-
config = RunConfig(
362-
command="claude",
363-
args=["-p", "--dangerously-skip-permissions"],
364-
prompt_file="RALPH.md",
365-
max_iterations=10,
366-
)
367-
state = RunState(run_id="monitored-run")
368-
369-
notifier = SlackNotifier("https://hooks.slack.com/services/YOUR/WEBHOOK/URL")
370-
run_loop(config, state, emitter=notifier)
371-
```
372-
373-
All public API is available from the top-level `ralphify` package (e.g. `from ralphify import run_loop, RunConfig, RunState`).

0 commit comments

Comments
 (0)