# Flow
This document describes the first practical local-testing flow for two agents that cooperate through a shared folder, while one side keeps access to real data private.
For the runnable demo and test in this repo, place that shared folder under a sandbox root such as `./sandbox/two-console-demo/shared/` instead of writing directly to a repo-root `./shared/` directory.
It is intentionally biased toward:
- simple file protocols
- explicit state
- easy manual debugging
- being runnable from two local terminals
## Core Idea
Use the shared folder as the agent-to-agent transport.
Do not use Telegram as the transport between agents.
Telegram is only for human approval and status notifications.
So there are two separate channels:
1. Agent channel
Shared folder only.
2. Human approval channel
Telegram only.
## Should Agent Messages Be One File Or Many Files?
Use one file per message or event, not a single append-only `message.md`.
Reason:
- easier to watch for new work
- avoids write races
- simpler ack/state handling
- easier to retry or replay
- easier to attach structured metadata
Use `thread.md` only as a readable summary.
Do not use it as the authoritative machine protocol.
## Recommended Shared Layout
```text
sandbox/two-console-demo/shared/
thread.md
datasets/
<dataset_uuid>/
dataset.yaml
mock/
...
.rho/
agents.yaml
spaces/
<space_id>.yaml
inbox/
<to_agent>/
<message_id>.yaml
outbox/
<from_agent>/
requests/
<request_id>.yaml
runs/
<run_id>.yaml
results/
<result_id>.yaml
events/
<event_id>.yaml
```
Per-agent local state:
```text
sandbox/two-console-demo/users/
agent1/
datasets/
share/
private/
local/
inbox/
staging/
approvals/
telegram/
agent2/
datasets/
share/
private/
local/
inbox/
staging/
approvals/
telegram/
```
## Agent Registry
Yes, have a shared file listing known agents.
Suggested file:
```text
sandbox/two-console-demo/shared/.rho/agents.yaml
```
Example:
```yaml
version: 1
agents:
- id: agent1
owner: user1
role: data-owner
status: active
- id: agent2
owner: user2
role: compute-agent
status: active
```
This gives you stable ids for routing messages and requests.
## Shared Space
A shared space is a named collaboration context.
Suggested file:
```text
sandbox/two-console-demo/shared/.rho/spaces/<space_id>.yaml
```
Example:
```yaml
version: 1
space:
id: space-local-001
agents:
- agent1
- agent2
status: active
shared_root: ./sandbox/two-console-demo/shared
created_by: agent1
```
## Message Transport
The tool that “messages another agent” should create a new YAML file in the recipient inbox.
Not append to `message.md`.
Suggested path:
```text
sandbox/two-console-demo/shared/.rho/inbox/agent2/msg-<uuid>.yaml
```
Example message:
```yaml
version: 1
message:
id: msg-001
from: agent1
to: agent2
space_id: space-local-001
type: request_run_real
related_request_id: req-001
created_at: 2026-04-02T00:00:00Z
body:
text: "Please evaluate this computation request against dataset prices-demo."
```
The receiving agent loop watches only:
```text
sandbox/two-console-demo/shared/.rho/inbox/<agent_id>/
```
That keeps the routing trivial.
## Status Updates
Agent1 should keep Agent2 informed with explicit status events.
Do this with event files, also one per file:
```text
sandbox/two-console-demo/shared/.rho/events/<event_id>.yaml
```
Or, if you want routing, put them into inbox messages with `type: status_update`.
Suggested statuses:
- `request_received`
- `review_started`
- `waiting_for_admin`
- `mock_run_started`
- `mock_run_finished`
- `real_run_started`
- `real_run_finished`
- `release_approved`
- `release_rejected`
- `request_rejected`
- `result_published`
Example:
```yaml
version: 1
message:
id: msg-002
from: agent1
to: agent2
space_id: space-local-001
type: status_update
related_request_id: req-001
created_at: 2026-04-02T00:01:00Z
body:
status: waiting_for_admin
text: "Code review completed. Waiting for approval to run on mock."
```
## Tool Surface In Pi Extensions
The `pi` extension layer should eventually expose tools like:
- `send_agent_message`
- `review_shared_code`
- `request_run_mock`
- `request_run_real`
- `publish_result`
- `check_request_status`
But the approval decisions should still come from the external admin flow, not directly from the model.
## First CLI Shape
You said you want to test this from two terminals, with either direct commands or the `pi` REPL.
That makes sense.
Suggested local CLI shape:
```text
rho agent init <agent_id>
rho agent bind-telegram <agent_id> <chat_ref>
rho agent listen <agent_id>
rho space create <space_id> --agents agent1,agent2
rho space accept <agent_id> <space_id>
rho dataset create ...
rho dataset publish <agent_id> <dataset_uuid>
rho request create-run ...
rho request status <request_id>
```
You do not need to implement all of these at once.
This is the operator model to aim for.
## Example End-To-End Flow
This is the concrete flow you described, rewritten as a protocol.
### 1. Initialize Agents
```text
init agent1
init agent2
```
Effects:
- create local user folders
- write agent metadata
- prepare local inbox/staging/approval dirs
### 2. Bind Telegram
```text
bind agent1 to telegram chat
bind agent2 to telegram chat
```
The same chat can be reused for local testing.
All Telegram messages should clearly prefix the agent id:
- `[agent1] waiting for approval`
- `[agent2] result received`
### 3. Start Both Listening
Each agent runs a local loop:
- watch shared inbox for messages to that agent
- watch local approval state
- process work items
### 4. Agent1 Creates Dataset
Use `rho-dataset`.
This stages:
- shareable mock bundle under `users/agent1/.../share`
- private real bundle under `users/agent1/.../private`
### 5. Agent1 Creates Shared Space With Agent2
Write:
```text
shared/.rho/spaces/<space_id>.yaml
```
And send:
```text
shared/.rho/inbox/agent2/msg-....yaml
```
with type `space_invite`.
### 6. Agent2 Accepts
Agent2 writes an acceptance message or updates the space status.
At this point both sides know the shared root path and space id.
### 7. Agent1 Publishes Dataset
Use the share bundle only:
```text
users/agent1/datasets/share/<dataset_uuid>
```
copy to:
```text
shared/datasets/<dataset_uuid>
```
### 8. Agent2 Writes Code And Runs On Mock
This is a local tool call on Agent2’s side.
Agent2:
- reads `shared/datasets/<dataset_uuid>/dataset.yaml`
- uses the mock data
- writes code in the shared workspace
- runs Gondolin against the mock dataset
This is usually safe enough to run without involving Agent1 yet.
### 9. Agent2 Requests Run On Real
Agent2 sends a request message to Agent1.
Suggested request file:
```text
shared/.rho/requests/<request_id>.yaml
```
And a routed inbox message to Agent1 pointing at that request.
The request should include:
- dataset UUID
- code paths to review
- command to run
- expected outputs
- code digest
### 10. Agent1 Receives Request
Agent1 loop sees the inbox message and loads the request.
Then Agent1 runs:
1. code review tool
2. local admin notification via Telegram
The sandboxed code must not control the Telegram message directly.
The outer harness builds the Telegram notification.
### 11. Telegram Asks Admin
Admin options should be simple and explicit:
1. run on mock
2. run on real
3. reject with message
For the first version, even if mock is normally safe, it is reasonable to ask:
- run on mock
- run on real
- reject
because it keeps the decision model very obvious.
### 12. If Run On Mock
Agent1:
- snapshots code
- binds mock data
- runs sandbox
- records stdout/stderr/artifacts
- sends status updates to Agent2
- sends result summary to Telegram
Then ask admin:
- run on real?
- reject?
### 13. If Run On Real
Agent1:
- snapshots code
- binds real private data
- runs sandbox locally
- records outputs in local staging only
- sends result summary to Telegram
Then ask admin:
- release results?
- reject release?
### 14. Release
If approved, Agent1 writes a result bundle into shared space:
```text
shared/.rho/results/<result_id>.yaml
shared/results/<result_id>/...
```
And sends a routed message to Agent2:
- result available
- request id
- result id
- summary
If not approved, send a rejection/status message instead.
### 15. Agent2 Receives Response
Agent2 sees:
- status updates
- rejection
- or published result
It should also be possible for Agent2 to explicitly poll:
- `check_request_status`
## Telegram Hooking Rule
This is an important safety rule.
Do not let code running inside Gondolin decide what is sent to Telegram.
Instead:
1. sandbox produces raw outputs
2. outer harness inspects them
3. outer harness decides what summary to send
That prevents untrusted code from directly driving the approval UX.
## Minimum Files To Implement First
If you want the smallest protocol that still works, start with:
```text
shared/.rho/agents.yaml
shared/.rho/spaces/<space_id>.yaml
shared/.rho/inbox/<agent_id>/<message_id>.yaml
shared/.rho/requests/<request_id>.yaml
shared/.rho/results/<result_id>.yaml
```
That is enough to support:
- agent discovery
- space creation
- message delivery
- run requests
- result publication
## Recommended First Tools
Implement these first:
1. `send_agent_message`
Writes a routed inbox file.
2. `create_run_request`
Writes a request manifest and notifies the other agent.
3. `review_shared_code`
Produces a summary from shared code changes.
4. `run_request_on_mock`
Local execution only.
5. `run_request_on_real`
Local execution only, gated by approval.
6. `publish_result`
Copies approved artifacts to shared results.
## What To Avoid
- do not use `thread.md` as the machine protocol
- do not let Telegram become the agent transport
- do not let sandboxed code directly send approval messages
- do not append multiple writers into one shared mutable markdown file as the source of truth
## Immediate Next Step
The next concrete implementation step should be:
1. define `shared/.rho/agents.yaml`
2. define inbox message schema
3. implement `send_agent_message`
4. implement `create_run_request`
That gives you a real inter-agent transport before building the more complex approval loop.