ramure 0.0.1__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.
- ramure-0.0.1/.gitignore +4 -0
- ramure-0.0.1/PKG-INFO +207 -0
- ramure-0.0.1/README.md +198 -0
- ramure-0.0.1/cli-plan.md +134 -0
- ramure-0.0.1/code-smells.md +169 -0
- ramure-0.0.1/considerations +59 -0
- ramure-0.0.1/examples/new/01_simple.py +26 -0
- ramure-0.0.1/examples/new/03_concurrent.py +40 -0
- ramure-0.0.1/examples/new/04_spawn_observe.py +50 -0
- ramure-0.0.1/examples/new/05_attach_endpoints.py +62 -0
- ramure-0.0.1/pyproject.toml +29 -0
- ramure-0.0.1/ramure/__init__.py +49 -0
- ramure-0.0.1/ramure/agent.py +68 -0
- ramure-0.0.1/ramure/cli.py +168 -0
- ramure-0.0.1/ramure/context.py +18 -0
- ramure-0.0.1/ramure/control.py +139 -0
- ramure-0.0.1/ramure/extension.py +10 -0
- ramure-0.0.1/ramure/extension.ts +377 -0
- ramure-0.0.1/ramure/helpers/__init__.py +17 -0
- ramure-0.0.1/ramure/helpers/agent.py +91 -0
- ramure-0.0.1/ramure/helpers/schema.py +81 -0
- ramure-0.0.1/ramure/log.py +169 -0
- ramure-0.0.1/ramure/machines.py +130 -0
- ramure-0.0.1/ramure/notes +3 -0
- ramure-0.0.1/ramure/process.py +359 -0
- ramure-0.0.1/ramure/runtime.py +262 -0
- ramure-0.0.1/ramure/server.py +142 -0
- ramure-0.0.1/ramure/stream.py +81 -0
- ramure-0.0.1/ramure/types.py +55 -0
- ramure-0.0.1/spec-replication.md +192 -0
- ramure-0.0.1/tests/__init__.py +0 -0
- ramure-0.0.1/tests/helpers.py +146 -0
- ramure-0.0.1/tests/test_context_runtime.py +391 -0
- ramure-0.0.1/tests/test_control.py +126 -0
- ramure-0.0.1/tests/test_e2e.py +151 -0
- ramure-0.0.1/tests/test_event_log.py +109 -0
- ramure-0.0.1/tests/test_events.py +98 -0
- ramure-0.0.1/tests/test_logging.py +1 -0
- ramure-0.0.1/tests/test_machines.py +19 -0
- ramure-0.0.1/tests/test_process.py +657 -0
- ramure-0.0.1/tests/test_schema.py +26 -0
- ramure-0.0.1/tests/test_spawn_readiness.py +126 -0
- ramure-0.0.1/uv.lock +225 -0
- ramure-0.0.1/wasm-2026-report.md +52 -0
ramure-0.0.1/.gitignore
ADDED
ramure-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ramure
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A multi-agent orchestration runtime
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: typer>=0.12
|
|
7
|
+
Requires-Dist: websockets>=13.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# ramure
|
|
11
|
+
|
|
12
|
+
A lightweight async multi-agent orchestration library. Agents run as [pi](https://github.com/mariozechner/pi-coding-agent) instances in tmux sessions, coordinated by Python process functions.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import asyncio
|
|
18
|
+
from ramure import LocalImage, agent, agent_process, done, wait
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@agent_process(image=LocalImage(), timeout=30)
|
|
22
|
+
async def summarize(text: str) -> str:
|
|
23
|
+
worker = await agent("worker")
|
|
24
|
+
|
|
25
|
+
@worker.on("finish")
|
|
26
|
+
async def on_finish(summary: str) -> str:
|
|
27
|
+
"""Call this with your summary when done."""
|
|
28
|
+
done(summary)
|
|
29
|
+
return "Done."
|
|
30
|
+
|
|
31
|
+
await worker.send(f"Summarize this text, then call finish:\n\n{text}")
|
|
32
|
+
return await wait()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
asyncio.run(summarize("The quick brown fox jumped over the lazy dog. " * 20))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Core concepts
|
|
39
|
+
|
|
40
|
+
### Processes
|
|
41
|
+
|
|
42
|
+
The unit of composition is a **process function** — an async function decorated with `@agent_process` that creates agents, wires them up, and returns a result.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
@agent_process
|
|
46
|
+
async def build_and_review(spec: str) -> str:
|
|
47
|
+
builder = await agent("builder")
|
|
48
|
+
auditor = await agent("auditor")
|
|
49
|
+
connect(builder, auditor)
|
|
50
|
+
|
|
51
|
+
@builder.on("submit")
|
|
52
|
+
async def on_submit(code: str):
|
|
53
|
+
await auditor.send(f"Review:\n{code}")
|
|
54
|
+
return "Submitted"
|
|
55
|
+
|
|
56
|
+
@auditor.on("approve")
|
|
57
|
+
async def on_approve(code: str):
|
|
58
|
+
done(code)
|
|
59
|
+
return "Approved"
|
|
60
|
+
|
|
61
|
+
@auditor.on("reject")
|
|
62
|
+
async def on_reject(feedback: str):
|
|
63
|
+
await builder.send(f"Fix: {feedback}")
|
|
64
|
+
return "Sent back"
|
|
65
|
+
|
|
66
|
+
await builder.send(f"Implement: {spec}")
|
|
67
|
+
await auditor.send("Review the builder's work.")
|
|
68
|
+
return await wait()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **Root process** (no active runtime): creates a `Runtime` and websocket server, tears them down on return.
|
|
72
|
+
- **Nested process** (runtime already active): creates a child scope, inherits the runtime.
|
|
73
|
+
- `done(value)` / `fail(reason)` signal completion from tool handlers.
|
|
74
|
+
- `await wait()` blocks until `done()` or `fail()` is called.
|
|
75
|
+
- Agents are cleaned up automatically when their owning process returns.
|
|
76
|
+
|
|
77
|
+
### Composition
|
|
78
|
+
|
|
79
|
+
Processes compose by calling each other:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
@agent_process(image=LocalImage())
|
|
83
|
+
async def main():
|
|
84
|
+
code = await write_code("fibonacci function")
|
|
85
|
+
review = await review_code(code)
|
|
86
|
+
return code
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Concurrent fan-out with `asyncio.gather`:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
@agent_process(image=LocalImage())
|
|
93
|
+
async def main():
|
|
94
|
+
results = await asyncio.gather(
|
|
95
|
+
research("Rust"),
|
|
96
|
+
research("Python"),
|
|
97
|
+
)
|
|
98
|
+
return results
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Observation and retry
|
|
102
|
+
|
|
103
|
+
`spawn()` runs a process in the background and returns a handle with an event stream:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
@agent_process(image=LocalImage())
|
|
107
|
+
async def main():
|
|
108
|
+
handle = spawn(flaky_task, "write a haiku")
|
|
109
|
+
|
|
110
|
+
async for event in handle.events:
|
|
111
|
+
if event.type == "failed":
|
|
112
|
+
handle = spawn(flaky_task, "write a haiku")
|
|
113
|
+
if event.type == "done":
|
|
114
|
+
return event.data
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Processes can emit custom events with `emit(type, data)`. Agent event logs are also async-iterable via `agent.events`.
|
|
118
|
+
|
|
119
|
+
### Endpoints
|
|
120
|
+
|
|
121
|
+
A process can expose endpoints to its parent. The parent can call them
|
|
122
|
+
directly, or attach them to an agent as tools. A child's agents are
|
|
123
|
+
available to the parent via ``handle.agents`` without any extra step.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
@agent_process
|
|
127
|
+
async def worker_pool():
|
|
128
|
+
@expose
|
|
129
|
+
async def submit_task(task: str) -> str:
|
|
130
|
+
w = await agent(f"worker-{uuid.uuid4().hex[:8]}")
|
|
131
|
+
await w.send(f"Do: {task}")
|
|
132
|
+
return w.name
|
|
133
|
+
|
|
134
|
+
return await wait()
|
|
135
|
+
|
|
136
|
+
handle = spawn(worker_pool)
|
|
137
|
+
name = await handle.call("submit_task", task="build a server")
|
|
138
|
+
worker = handle.agents[name]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
To let an agent consume a process's endpoints, attach the handle:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
@agent_process
|
|
145
|
+
async def main():
|
|
146
|
+
pool = spawn(worker_pool)
|
|
147
|
+
dispatcher = await agent("dispatcher")
|
|
148
|
+
await pool.attach(dispatcher) # all endpoints as tools
|
|
149
|
+
# or: await pool.attach(dispatcher, only=["submit_task"], prefix="pool_")
|
|
150
|
+
await dispatcher.send("Use submit_task to delegate jobs.")
|
|
151
|
+
return await wait()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Endpoints run inside the child process's scope, so calls to `emit`,
|
|
155
|
+
`done`, and `fail` inside an endpoint affect the child.
|
|
156
|
+
|
|
157
|
+
## API
|
|
158
|
+
|
|
159
|
+
### Decorator
|
|
160
|
+
|
|
161
|
+
- `@agent_process(image=, timeout=, log_dir=)` — wrap an async function as a process
|
|
162
|
+
|
|
163
|
+
### Ambient functions
|
|
164
|
+
|
|
165
|
+
- `await agent(name, system_prompt=, image=, machine=)` — create an agent
|
|
166
|
+
- `await machine(image=)` — spawn a standalone machine
|
|
167
|
+
- `connect(a, b, direction=)` — allow agents to message/send files
|
|
168
|
+
- `done(result)` — signal process success
|
|
169
|
+
- `fail(reason)` — signal process failure
|
|
170
|
+
- `await wait()` — block until `done()` or `fail()`
|
|
171
|
+
- `emit(type, data)` — emit a process event
|
|
172
|
+
- `spawn(fn, *args, **kwargs)` — run a process in background, returns `ProcessHandle`
|
|
173
|
+
- `@expose` — register an async function as an endpoint callable via `handle.call()` or attachable via `handle.attach()`
|
|
174
|
+
- `current_runtime()` — access the runtime (rarely needed)
|
|
175
|
+
|
|
176
|
+
### Agent methods
|
|
177
|
+
|
|
178
|
+
- `agent.on(tool_name)` — decorator to register a tool handler
|
|
179
|
+
- `agent.send(message)` — send a message to the agent
|
|
180
|
+
- `agent.exec(command)` — run a shell command on the agent's machine
|
|
181
|
+
- `agent.events` — async-iterable log of raw agent events
|
|
182
|
+
|
|
183
|
+
### ProcessHandle
|
|
184
|
+
|
|
185
|
+
- `handle.events` — async-iterable stream of process events
|
|
186
|
+
- `handle.agents` — dict of the child's agents
|
|
187
|
+
- `await handle.call(name, **kwargs)` — call an endpoint
|
|
188
|
+
- `await handle.attach(agent, only=, prefix=)` — register endpoints as tools on an agent
|
|
189
|
+
- `handle.cancel()` — cancel the process
|
|
190
|
+
|
|
191
|
+
## CLI
|
|
192
|
+
|
|
193
|
+
Running an `@agent_process` opens a Unix socket at
|
|
194
|
+
`~/.ramure/runtimes/{execution_id}.sock` and writes a per-run log tree
|
|
195
|
+
under `~/.ramure/logs/{execution_id}/`. The `ramure` CLI uses these:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
ramure ls # live runs
|
|
199
|
+
ramure status [--id <prefix>] # agents, machines, connections
|
|
200
|
+
ramure send <agent> <msg> [--id <prefix>]
|
|
201
|
+
ramure connect <agent> [--id <prefix>] # tmux attach
|
|
202
|
+
ramure ssh <agent> [--id <prefix>] # shell on the agent's machine
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`--id` takes an execution-id prefix. Omit when there's one live run.
|
|
206
|
+
All commands require the run to be live (socket present). Finished-run
|
|
207
|
+
logs are at `~/.ramure/logs/{execution_id}/` — read them directly.
|
ramure-0.0.1/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# ramure
|
|
2
|
+
|
|
3
|
+
A lightweight async multi-agent orchestration library. Agents run as [pi](https://github.com/mariozechner/pi-coding-agent) instances in tmux sessions, coordinated by Python process functions.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import asyncio
|
|
9
|
+
from ramure import LocalImage, agent, agent_process, done, wait
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@agent_process(image=LocalImage(), timeout=30)
|
|
13
|
+
async def summarize(text: str) -> str:
|
|
14
|
+
worker = await agent("worker")
|
|
15
|
+
|
|
16
|
+
@worker.on("finish")
|
|
17
|
+
async def on_finish(summary: str) -> str:
|
|
18
|
+
"""Call this with your summary when done."""
|
|
19
|
+
done(summary)
|
|
20
|
+
return "Done."
|
|
21
|
+
|
|
22
|
+
await worker.send(f"Summarize this text, then call finish:\n\n{text}")
|
|
23
|
+
return await wait()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
asyncio.run(summarize("The quick brown fox jumped over the lazy dog. " * 20))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Core concepts
|
|
30
|
+
|
|
31
|
+
### Processes
|
|
32
|
+
|
|
33
|
+
The unit of composition is a **process function** — an async function decorated with `@agent_process` that creates agents, wires them up, and returns a result.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
@agent_process
|
|
37
|
+
async def build_and_review(spec: str) -> str:
|
|
38
|
+
builder = await agent("builder")
|
|
39
|
+
auditor = await agent("auditor")
|
|
40
|
+
connect(builder, auditor)
|
|
41
|
+
|
|
42
|
+
@builder.on("submit")
|
|
43
|
+
async def on_submit(code: str):
|
|
44
|
+
await auditor.send(f"Review:\n{code}")
|
|
45
|
+
return "Submitted"
|
|
46
|
+
|
|
47
|
+
@auditor.on("approve")
|
|
48
|
+
async def on_approve(code: str):
|
|
49
|
+
done(code)
|
|
50
|
+
return "Approved"
|
|
51
|
+
|
|
52
|
+
@auditor.on("reject")
|
|
53
|
+
async def on_reject(feedback: str):
|
|
54
|
+
await builder.send(f"Fix: {feedback}")
|
|
55
|
+
return "Sent back"
|
|
56
|
+
|
|
57
|
+
await builder.send(f"Implement: {spec}")
|
|
58
|
+
await auditor.send("Review the builder's work.")
|
|
59
|
+
return await wait()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **Root process** (no active runtime): creates a `Runtime` and websocket server, tears them down on return.
|
|
63
|
+
- **Nested process** (runtime already active): creates a child scope, inherits the runtime.
|
|
64
|
+
- `done(value)` / `fail(reason)` signal completion from tool handlers.
|
|
65
|
+
- `await wait()` blocks until `done()` or `fail()` is called.
|
|
66
|
+
- Agents are cleaned up automatically when their owning process returns.
|
|
67
|
+
|
|
68
|
+
### Composition
|
|
69
|
+
|
|
70
|
+
Processes compose by calling each other:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
@agent_process(image=LocalImage())
|
|
74
|
+
async def main():
|
|
75
|
+
code = await write_code("fibonacci function")
|
|
76
|
+
review = await review_code(code)
|
|
77
|
+
return code
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Concurrent fan-out with `asyncio.gather`:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
@agent_process(image=LocalImage())
|
|
84
|
+
async def main():
|
|
85
|
+
results = await asyncio.gather(
|
|
86
|
+
research("Rust"),
|
|
87
|
+
research("Python"),
|
|
88
|
+
)
|
|
89
|
+
return results
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Observation and retry
|
|
93
|
+
|
|
94
|
+
`spawn()` runs a process in the background and returns a handle with an event stream:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
@agent_process(image=LocalImage())
|
|
98
|
+
async def main():
|
|
99
|
+
handle = spawn(flaky_task, "write a haiku")
|
|
100
|
+
|
|
101
|
+
async for event in handle.events:
|
|
102
|
+
if event.type == "failed":
|
|
103
|
+
handle = spawn(flaky_task, "write a haiku")
|
|
104
|
+
if event.type == "done":
|
|
105
|
+
return event.data
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Processes can emit custom events with `emit(type, data)`. Agent event logs are also async-iterable via `agent.events`.
|
|
109
|
+
|
|
110
|
+
### Endpoints
|
|
111
|
+
|
|
112
|
+
A process can expose endpoints to its parent. The parent can call them
|
|
113
|
+
directly, or attach them to an agent as tools. A child's agents are
|
|
114
|
+
available to the parent via ``handle.agents`` without any extra step.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
@agent_process
|
|
118
|
+
async def worker_pool():
|
|
119
|
+
@expose
|
|
120
|
+
async def submit_task(task: str) -> str:
|
|
121
|
+
w = await agent(f"worker-{uuid.uuid4().hex[:8]}")
|
|
122
|
+
await w.send(f"Do: {task}")
|
|
123
|
+
return w.name
|
|
124
|
+
|
|
125
|
+
return await wait()
|
|
126
|
+
|
|
127
|
+
handle = spawn(worker_pool)
|
|
128
|
+
name = await handle.call("submit_task", task="build a server")
|
|
129
|
+
worker = handle.agents[name]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To let an agent consume a process's endpoints, attach the handle:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
@agent_process
|
|
136
|
+
async def main():
|
|
137
|
+
pool = spawn(worker_pool)
|
|
138
|
+
dispatcher = await agent("dispatcher")
|
|
139
|
+
await pool.attach(dispatcher) # all endpoints as tools
|
|
140
|
+
# or: await pool.attach(dispatcher, only=["submit_task"], prefix="pool_")
|
|
141
|
+
await dispatcher.send("Use submit_task to delegate jobs.")
|
|
142
|
+
return await wait()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Endpoints run inside the child process's scope, so calls to `emit`,
|
|
146
|
+
`done`, and `fail` inside an endpoint affect the child.
|
|
147
|
+
|
|
148
|
+
## API
|
|
149
|
+
|
|
150
|
+
### Decorator
|
|
151
|
+
|
|
152
|
+
- `@agent_process(image=, timeout=, log_dir=)` — wrap an async function as a process
|
|
153
|
+
|
|
154
|
+
### Ambient functions
|
|
155
|
+
|
|
156
|
+
- `await agent(name, system_prompt=, image=, machine=)` — create an agent
|
|
157
|
+
- `await machine(image=)` — spawn a standalone machine
|
|
158
|
+
- `connect(a, b, direction=)` — allow agents to message/send files
|
|
159
|
+
- `done(result)` — signal process success
|
|
160
|
+
- `fail(reason)` — signal process failure
|
|
161
|
+
- `await wait()` — block until `done()` or `fail()`
|
|
162
|
+
- `emit(type, data)` — emit a process event
|
|
163
|
+
- `spawn(fn, *args, **kwargs)` — run a process in background, returns `ProcessHandle`
|
|
164
|
+
- `@expose` — register an async function as an endpoint callable via `handle.call()` or attachable via `handle.attach()`
|
|
165
|
+
- `current_runtime()` — access the runtime (rarely needed)
|
|
166
|
+
|
|
167
|
+
### Agent methods
|
|
168
|
+
|
|
169
|
+
- `agent.on(tool_name)` — decorator to register a tool handler
|
|
170
|
+
- `agent.send(message)` — send a message to the agent
|
|
171
|
+
- `agent.exec(command)` — run a shell command on the agent's machine
|
|
172
|
+
- `agent.events` — async-iterable log of raw agent events
|
|
173
|
+
|
|
174
|
+
### ProcessHandle
|
|
175
|
+
|
|
176
|
+
- `handle.events` — async-iterable stream of process events
|
|
177
|
+
- `handle.agents` — dict of the child's agents
|
|
178
|
+
- `await handle.call(name, **kwargs)` — call an endpoint
|
|
179
|
+
- `await handle.attach(agent, only=, prefix=)` — register endpoints as tools on an agent
|
|
180
|
+
- `handle.cancel()` — cancel the process
|
|
181
|
+
|
|
182
|
+
## CLI
|
|
183
|
+
|
|
184
|
+
Running an `@agent_process` opens a Unix socket at
|
|
185
|
+
`~/.ramure/runtimes/{execution_id}.sock` and writes a per-run log tree
|
|
186
|
+
under `~/.ramure/logs/{execution_id}/`. The `ramure` CLI uses these:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
ramure ls # live runs
|
|
190
|
+
ramure status [--id <prefix>] # agents, machines, connections
|
|
191
|
+
ramure send <agent> <msg> [--id <prefix>]
|
|
192
|
+
ramure connect <agent> [--id <prefix>] # tmux attach
|
|
193
|
+
ramure ssh <agent> [--id <prefix>] # shell on the agent's machine
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`--id` takes an execution-id prefix. Omit when there's one live run.
|
|
197
|
+
All commands require the run to be live (socket present). Finished-run
|
|
198
|
+
logs are at `~/.ramure/logs/{execution_id}/` — read them directly.
|
ramure-0.0.1/cli-plan.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# CLI plan for interacting with live ramure runtimes
|
|
2
|
+
|
|
3
|
+
## What the old CLI does, what's relevant
|
|
4
|
+
|
|
5
|
+
Ignoring everything that's about "remote server, auth, billing, devboxes,
|
|
6
|
+
repos, PRs, SSH":
|
|
7
|
+
|
|
8
|
+
| Old command | What it does | Analog in new ramure |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `ramure execution ls` | list running executions | `ramure ls` — list live runtimes |
|
|
11
|
+
| `ramure execution status <slug>` | show status (agents, connections, machines) | `ramure status <id>` |
|
|
12
|
+
| `ramure execution activity <slug> -n 50` | recent events | `ramure tail <id> [--agent X] [-n 50]` |
|
|
13
|
+
| `ramure execution stop <slug>` | stop a run | no need for this |
|
|
14
|
+
| `ramure execution send <slug> <msg> -a <agent>` | send a message to an agent | `ramure send <id> <agent> <msg>` |
|
|
15
|
+
| `ramure execution ssh <slug> -a <agent>` | open shell on agent VM | needed, there can be remote machines |
|
|
16
|
+
| `ramure execution connect <slug> -a <agent>` | resume the pi session | needed |
|
|
17
|
+
| `ramure tool <name> key=value` (inside VM) | call a tool | **interesting** — see below |
|
|
18
|
+
| `ramure tools` (inside VM) | list tools for current agent | same context |
|
|
19
|
+
| `ramure exec program.py` | launch a new run | **unnecessary** — `python program.py` does it. |
|
|
20
|
+
| `ramure apply <slug>` | apply diff to local | not our problem |
|
|
21
|
+
| `ramure auth / init / devbox` | auth, repo setup | not our problem |
|
|
22
|
+
|
|
23
|
+
So the "translation" of the old CLI into the new world is actually quite
|
|
24
|
+
small: `ls`, `status`, `tail`, `stop`, `send`. Five commands. SSH/connect
|
|
25
|
+
are replaced by `tmux attach`, which is better — more direct, no key
|
|
26
|
+
management.
|
|
27
|
+
|
|
28
|
+
## The interesting remnant: `ramure tool <name>`
|
|
29
|
+
|
|
30
|
+
This is different from everything else. In the old model, an agent
|
|
31
|
+
running inside a VM could shell out to the `ramure` CLI to call a tool
|
|
32
|
+
on itself — it was a way to invoke runtime-registered Python handlers
|
|
33
|
+
from bash. Equivalent in the new world would be: if you ssh'd into a
|
|
34
|
+
ramure agent's machine (or cd'd into its tmux), you could type
|
|
35
|
+
`ramure tool submit diff=... summary=...` to call the tool handler.
|
|
36
|
+
|
|
37
|
+
That's an actual capability to think about: **invoking tools from
|
|
38
|
+
outside the LLM.** In the old code it existed because agents could run
|
|
39
|
+
bash tools that exercised Python handlers. In the new code it could be
|
|
40
|
+
done, but it's not obvious we want it — the whole point of tool handlers
|
|
41
|
+
|
|
42
|
+
is that the LLM calls them.
|
|
43
|
+
|
|
44
|
+
I'd shelve this.
|
|
45
|
+
|
|
46
|
+
no need for this
|
|
47
|
+
|
|
48
|
+
## What's worth stealing from the old structure
|
|
49
|
+
|
|
50
|
+
A few concrete things the old CLI got right:
|
|
51
|
+
|
|
52
|
+
1. **Subcommand grouping by noun.** `ramure execution {ls, status, tail,
|
|
53
|
+
stop, send}`, `ramure devbox {create, ls, snapshot}`. Discoverable,
|
|
54
|
+
future-proof. For us: probably just flat (`ramure ls`, `ramure tail`,
|
|
55
|
+
`ramure send`), because there's only one noun (execution/run).
|
|
56
|
+
|
|
57
|
+
2. **`--agent` defaults to "first" or "builder".** Instead of erroring
|
|
58
|
+
when omitted. Saves typing for the common single-agent case. We
|
|
59
|
+
should do the same: if a command needs an agent and only one exists,
|
|
60
|
+
use it; if none specified and multiple exist, list them and ask.
|
|
61
|
+
|
|
62
|
+
3. **`ls` with `--all` to include stopped runs.** Assumes history. In
|
|
63
|
+
our world "stopped" isn't really a thing — the socket goes away. But
|
|
64
|
+
we do have the JSONL logs. `ramure ls --all` could list
|
|
65
|
+
`~/.ramure/runtimes/*.sock` (live) + `~/.ramure/logs/*/` (finished).
|
|
66
|
+
Probably overkill for now.
|
|
67
|
+
|
|
68
|
+
4. **Event formatting via a shared `format_event`.** The old CLI had
|
|
69
|
+
`ramure/display.py` with one function that turns a LogEntry into a
|
|
70
|
+
pretty line. We want this for `tail`. Important detail because the
|
|
71
|
+
events we log are the same ones we'd display, and
|
|
72
|
+
format-once-used-many avoids a bunch of inconsistency.
|
|
73
|
+
|
|
74
|
+
5. **Execution slugs instead of UUIDs.** Old used `{verb-noun-hash}`
|
|
75
|
+
slugs, so you could tab-complete or remember them. Our UUIDs are
|
|
76
|
+
awful to type. Worth considering slug generation on
|
|
77
|
+
`runtime.start()` — or at least displaying a prefix (`ramure ls`
|
|
78
|
+
showing first 8 chars, and accepting prefixes when you refer to
|
|
79
|
+
them).
|
|
80
|
+
|
|
81
|
+
6. **Streaming semantics in `exec`.** The old `exec` command streamed
|
|
82
|
+
events to stdout while the run was live. Ours doesn't need a `exec`
|
|
83
|
+
command since you run Python directly, but: when the user runs
|
|
84
|
+
`python program.py`, they're staring at whatever the program prints.
|
|
85
|
+
They may also want the log stream. A `ramure tail <id>` in another
|
|
86
|
+
terminal covers this. Separate concerns, not the same tool.
|
|
87
|
+
|
|
88
|
+
## Revised plan
|
|
89
|
+
|
|
90
|
+
Given what I saw in the old CLI, the minimal useful surface is:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
ramure ls list live runtimes
|
|
94
|
+
ramure status [<id>] show execution metadata (agents, etc.)
|
|
95
|
+
ramure tail [<id>] [--agent X] [-n N] stream or recent events
|
|
96
|
+
ramure send [<id>] [--agent X] <message> inject a message
|
|
97
|
+
ramure stop [<id>] stop a run (soft: emit runtime-shutdown; fallback SIGTERM)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Plus:
|
|
101
|
+
- `[<id>]` accepts prefixes (`ramure tail a3f1`).
|
|
102
|
+
- `[<id>]` is optional when only one runtime is live.
|
|
103
|
+
- `--agent` defaults to the only agent, or errors with list.
|
|
104
|
+
- Event formatting lives in a `ramure/display.py` that's imported by
|
|
105
|
+
`tail` and `status`.
|
|
106
|
+
- Logs go to `~/.ramure/logs/{execution_id}/*.jsonl` by default (same
|
|
107
|
+
base as the socket, so discovery is symmetrical).
|
|
108
|
+
|
|
109
|
+
The transport question: **let's do registry + loopback WebSocket, not
|
|
110
|
+
Unix socket.** Reason: the server already speaks WS, adding Unix would
|
|
111
|
+
mean a second dispatcher. Registry file at
|
|
112
|
+
`~/.ramure/runtimes/{execution_id}.json` has
|
|
113
|
+
`{pid, server_url, program, started_at}`. CLI reads it. On `close`,
|
|
114
|
+
delete it. On startup, scan for stale entries and clean them up.
|
|
115
|
+
|
|
116
|
+
Slight downside vs. Unix sockets: a stale JSON is harder to detect than
|
|
117
|
+
a stale socket (you need to check the pid). But you'd check the pid
|
|
118
|
+
anyway for `stop`, so the code is there.
|
|
119
|
+
|
|
120
|
+
## Suggested build order
|
|
121
|
+
|
|
122
|
+
1. **Registry file** (`runtime.py`): write on start, delete on close.
|
|
123
|
+
~15 lines.
|
|
124
|
+
2. **Default `log_dir`** to `~/.ramure/logs/` if not set. ~3 lines.
|
|
125
|
+
3. **`ramure/display.py`**: `format_event(entry) -> str | None`. Moved
|
|
126
|
+
or copied from old CLI if patterns match, rewritten if not.
|
|
127
|
+
4. **`ramure/cli.py`**: Typer (or argparse — Typer is fine, small dep)
|
|
128
|
+
with `ls`, `status`, `tail`, `send`, `stop`. Uses the registry and
|
|
129
|
+
the WS protocol.
|
|
130
|
+
5. **Entry point** in `pyproject.toml`: `ramure = "ramure.cli:app"`.
|
|
131
|
+
|
|
132
|
+
Probably ~300 lines of CLI total, plus ~50 in runtime.py.
|
|
133
|
+
|
|
134
|
+
how does the loopback websocket work? how does it get all these things we need?
|