Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/protocols/a2a-fleet-delegation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# A2A Fleet And Delegation

Native A2A pairing makes peers discoverable. The fleet layer turns those peers
into a small, durable operator network: inspect who is available, delegate work,
poll task state, and keep a local transcript of what was asked and what came
back.

## Commands

```sh
maestro a2a fleet [--json] [--registry <path>] [--tasks <path>]
maestro a2a delegate <peer> <text> [--role <role>] [--cwd <path>] [--wait]
maestro a2a tasks [peer] [--json] [--refresh]
maestro a2a wait <peer> <task-id>
```

`fleet` reads the native peer registry, fetches each peer Agent Card when
reachable, and joins the result with the local task ledger. It never prints
bearer token values. Peers that cannot be reached are still shown with their
registry URL and a bounded error.

`delegate` sends a normal A2A `message:send` request with Maestro delegation
metadata: origin, peer name, role, and working directory. The resulting task is
recorded in the local ledger before optional waiting begins.

`tasks` reads the durable ledger and can refresh known task IDs from their
registered peers. This gives the operator a single place to see outstanding work
across the Mac mini, dev desktop, and local Maestro instances.

## Files

The peer registry remains:

```text
~/.maestro/a2a/peers.json
```

The task ledger defaults to:

```text
~/.maestro/a2a/tasks.json
```

`MAESTRO_A2A_TASKS_FILE` overrides the ledger path. `CODEX_A2A_TASKS_FILE` is
accepted as a migration alias.

## Acceptance Tests

Before this feature, the following tests fail:

```sh
npm run test:fast -- test/cli/commands/a2a-fleet-delegation.test.ts test/cli-tui/commands/a2a-handlers.test.ts
cargo test -p maestro-tui commands::registry::tests::a2a_command_parses_peer_actions
```

After implementation, they must pass and prove:

- `maestro a2a delegate <peer> <text> --wait` sends real HTTP+JSON A2A traffic,
records the task, updates the final state, and stores a transcript.
- `maestro a2a fleet --json` shows peer health, Agent Card capabilities, and the
peer's most recent ledger task without leaking token values.
- `maestro a2a tasks --json` reads the ledger and can be used as a fleet task
view.
- TypeScript and Rust TUIs both recognize `/a2a fleet`, `/a2a tasks`, and
`/a2a delegate`.

7 changes: 4 additions & 3 deletions docs/protocols/a2a-peer-pairing.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ bash scripts/smoke-maestro-a2a-tmux.sh
```

The smoke launches two local Maestro peers in tmux, exchanges native pairing
codes, accepts each peer into isolated registries, then verifies `send --wait`
and explicit `wait`. See [A2A tmux smoke](./a2a-tmux-smoke.md) for the harness
contract and troubleshooting knobs.
codes, accepts each peer into isolated registries, delegates work into a durable
task ledger, then verifies `fleet`, `tasks`, `send`, and explicit `wait`. See
[A2A tmux smoke](./a2a-tmux-smoke.md) for the harness contract and
troubleshooting knobs.

## TUI Surface

Expand Down
19 changes: 13 additions & 6 deletions docs/protocols/a2a-tmux-smoke.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# A2A tmux smoke

`scripts/smoke-maestro-a2a-tmux.sh` is the local end-to-end smoke for native
Maestro A2A pairing. It launches two local Maestro control-plane peers in tmux,
uses the TypeScript `maestro a2a` CLI to exchange pairing codes, stores each
peer in an isolated registry, then verifies both `send --wait` and explicit
`wait`.
Maestro A2A pairing and delegation. It launches two local Maestro control-plane
peers in tmux, uses the TypeScript `maestro a2a` CLI to exchange pairing codes,
stores each peer in an isolated registry, delegates work into a durable task
ledger, then verifies fleet health, task listing, `send`, and explicit `wait`.

Run it from the repo root:

Expand All @@ -19,6 +19,9 @@ entrypoint it drives is:
bun run a2a -- offer ...
bun run a2a -- accept ...
bun run a2a -- peers
bun run a2a -- delegate ...
bun run a2a -- fleet ...
bun run a2a -- tasks ...
bun run a2a -- send ...
bun run a2a -- wait ...
```
Expand All @@ -29,7 +32,10 @@ bun run a2a -- wait ...
- Pairing codes are generated from each peer's Agent Card.
- Each side accepts the other side into an isolated
`MAESTRO_A2A_PEERS_FILE` registry.
- Peer A can send to peer B and block with bounded `send --wait`.
- Peer A can delegate work to peer B and block with bounded `delegate --wait`.
- Fleet output joins live Agent Card health with the durable local task ledger.
- Task output reads the recorded delegated task without resolving or printing
bearer token values.
- Peer B can send to peer A, parse the returned task id, and complete a bounded
explicit `wait`.

Expand All @@ -47,7 +53,8 @@ MAESTRO_A2A_TMUX_READY_TIMEOUT_SECONDS=180 bash scripts/smoke-maestro-a2a-tmux.s

By default the script kills the tmux session on exit. Set
`MAESTRO_A2A_TMUX_KEEP_SESSION=1` to inspect the peer panes after a failure.
Logs and temporary peer registries are written under `tmp/a2a-tmux-smoke/`.
Logs, temporary peer registries, and task ledgers are written under
`tmp/a2a-tmux-smoke/`.

## Expected output

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"smoke:codex-app-server-live": "node scripts/smoke-codex-app-server-live.mjs",
"smoke:event-bus": "tsx scripts/smoke-maestro-event-bus.ts",
"smoke:a2a-local": "tsx scripts/smoke-maestro-a2a-local.ts",
"smoke:a2a-tmux": "bash scripts/smoke-maestro-a2a-tmux.sh",
"a2a": "tsx src/cli.ts a2a",
"a2a:codex-bridge": "python3 scripts/codex-a2a-bridge.py",
"a2a:peer": "tsx src/cli.ts a2a",
Expand Down
23 changes: 22 additions & 1 deletion packages/tui-rs/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3371,27 +3371,48 @@ Add the required fields and retry.",
[
"## A2A peer pairing",
"",
"/a2a fleet",
"/a2a peers",
"/a2a tasks [peer]",
"/a2a accept <pairing-code>",
"/a2a delegate <peer> <text>",
"/a2a send <peer> <text>",
"",
"Native pairing codes are shared with the TypeScript CLI/TUI. Use `maestro a2a offer` to create a code from a running peer.",
"Native pairing codes, fleet views, and delegation ledgers are shared with the TypeScript CLI/TUI.",
]
.join("\n"),
);
}
A2aAction::Fleet => {
self.state.add_system_message(
"A2A fleet inspection uses the shared Maestro peer registry. Run `maestro a2a fleet` for live health and task summaries until the Rust fleet reader is wired into this view."
.to_string(),
);
}
A2aAction::Peers => {
self.state.add_system_message(
"A2A peer listing uses the shared Maestro peer registry. Run `maestro a2a peers` for the current registry until the Rust registry reader is wired into this view."
.to_string(),
);
}
A2aAction::Tasks { peer } => {
let scope = peer.as_deref().unwrap_or("all peers");
self.state.add_system_message(format!(
"A2A task ledger requested for {scope}. Run `maestro a2a tasks` for the current durable ledger until the Rust task reader is wired into this view."
));
}
A2aAction::Accept { code } => {
self.state.add_system_message(format!(
"A2A pairing code captured ({} chars). Run `maestro a2a accept <code>` or use the TypeScript TUI `/a2a accept <code>` to persist it in the shared registry.",
code.len()
));
}
A2aAction::Delegate { peer, text } => {
self.state.add_system_message(format!(
"A2A delegation prepared for `{peer}` ({} chars). Run `maestro a2a delegate {peer} <text> --wait` while the Rust delegation controller is connected to the shared A2A client.",
text.len()
));
}
A2aAction::Send { peer, text } => {
self.state.add_system_message(format!(
"A2A send request prepared for `{peer}` ({} chars). Run `maestro a2a send {peer} <text> --wait` while the Rust send controller is connected to the shared A2A client.",
Expand Down
34 changes: 31 additions & 3 deletions packages/tui-rs/src/commands/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,13 +511,30 @@ fn parse_a2a_action(raw: &str) -> Result<A2aAction, CommandError> {
.unwrap_or_default();
match subcommand.as_str() {
"" | "help" => Ok(A2aAction::Help),
"fleet" => Ok(A2aAction::Fleet),
"peers" | "list" => Ok(A2aAction::Peers),
"tasks" => Ok(A2aAction::Tasks {
peer: tokens.get(1).cloned(),
}),
"accept" => {
let code = tokens
.get(1)
.ok_or_else(|| CommandError::new("Usage: /a2a accept <pairing-code>"))?;
Ok(A2aAction::Accept { code: code.clone() })
}
"delegate" => {
let peer = tokens
.get(1)
.ok_or_else(|| CommandError::new("Usage: /a2a delegate <peer> <text>"))?;
let text = tokens.get(2..).unwrap_or(&[]).join(" ");
if text.trim().is_empty() {
return Err(CommandError::new("Usage: /a2a delegate <peer> <text>"));
}
Ok(A2aAction::Delegate {
peer: peer.clone(),
text,
})
}
"send" => {
let peer = tokens
.get(1)
Expand All @@ -533,7 +550,7 @@ fn parse_a2a_action(raw: &str) -> Result<A2aAction, CommandError> {
}
_ => Err(
CommandError::new(format!("Unknown A2A subcommand: {subcommand}"))
.with_hint("Usage: /a2a [peers|accept <code>|send <peer> <text>]"),
.with_hint("Usage: /a2a [fleet|peers|tasks|accept <code>|delegate <peer> <text>|send <peer> <text>]"),
),
}
}
Expand Down Expand Up @@ -754,15 +771,15 @@ pub fn build_command_registry() -> CommandRegistry {
registry.register(
Command::new(
"a2a",
"Pair and inspect A2A peer agents",
"Pair, inspect, and delegate to A2A peer agents",
CommandCategory::Tools,
Box::new(|ctx| {
Ok(CommandOutput::Action(CommandAction::A2a(parse_a2a_action(
&ctx.raw_args,
)?)))
}),
)
.usage("/a2a [peers|accept <code>|send <peer> <text>]"),
.usage("/a2a [fleet|peers|tasks|accept <code>|delegate <peer> <text>|send <peer> <text>]"),
);

// Queue command
Expand Down Expand Up @@ -1959,6 +1976,17 @@ mod tests {
fn a2a_command_parses_peer_actions() {
let registry = build_command_registry();

assert!(registry.execute("/a2a fleet", "/tmp", None, None).is_ok());
assert!(registry.execute("/a2a tasks", "/tmp", None, None).is_ok());
assert!(registry
.execute(
"/a2a delegate mac-mini run workspace smoke",
"/tmp",
None,
None,
)
.is_ok());

match registry
.execute("/a2a peers", "/tmp", None, None)
.expect("a2a peers should parse")
Expand Down
6 changes: 6 additions & 0 deletions packages/tui-rs/src/commands/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,16 @@ pub enum McpAction {
pub enum A2aAction {
/// Show native A2A pairing help.
Help,
/// Show A2A fleet health.
Fleet,
/// List paired peers.
Peers,
/// List delegated A2A tasks.
Tasks { peer: Option<String> },
/// Accept a pairing code.
Accept { code: String },
/// Delegate work to a peer.
Delegate { peer: String, text: String },
/// Send a message to a peer.
Send { peer: String, text: String },
}
Expand Down
32 changes: 26 additions & 6 deletions scripts/smoke-maestro-a2a-tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ WORK_DIR="${MAESTRO_A2A_TMUX_WORK_DIR:-"$ROOT_DIR/tmp/a2a-tmux-smoke"}"
LOG_DIR="$WORK_DIR/logs"
REGISTRY_A="$WORK_DIR/peer-a-registry.json"
REGISTRY_B="$WORK_DIR/peer-b-registry.json"
TASKS_A="$WORK_DIR/peer-a-tasks.json"
TASKS_B="$WORK_DIR/peer-b-tasks.json"
READY_TIMEOUT_SECONDS="${MAESTRO_A2A_TMUX_READY_TIMEOUT_SECONDS:-120}"
KEEP_SESSION="${MAESTRO_A2A_TMUX_KEEP_SESSION:-0}"

Expand Down Expand Up @@ -74,7 +76,7 @@ require_cmd curl

cd "$ROOT_DIR"
mkdir -p "$LOG_DIR"
rm -f "$REGISTRY_A" "$REGISTRY_B"
rm -f "$REGISTRY_A" "$REGISTRY_B" "$TASKS_A" "$TASKS_B"

if tmux has-session -t "$SESSION_NAME" >/dev/null 2>&1; then
echo "tmux session already exists: $SESSION_NAME" >&2
Expand Down Expand Up @@ -108,11 +110,29 @@ a2a_cli "$REGISTRY_B" accept "$CODE_A" --name peer-a --default
a2a_cli "$REGISTRY_A" peers
a2a_cli "$REGISTRY_B" peers

echo "sending peer-a -> peer-b with bounded wait"
SEND_A_TO_B="$(a2a_cli "$REGISTRY_A" send peer-b "hello from tmux peer A" --wait --max-wait-ms 30000 --interval-ms 250 --timeout-ms 3000)"
echo "$SEND_A_TO_B"
if ! grep -q "tmux peer B received the A2A message" <<<"$SEND_A_TO_B"; then
echo "peer-a -> peer-b response did not include expected peer B text" >&2
echo "delegating peer-a -> peer-b with bounded wait and durable ledger"
DELEGATE_A_TO_B="$(a2a_cli "$REGISTRY_A" delegate peer-b "run the tmux A2A smoke from peer A" --role background-worker --cwd "$ROOT_DIR" --wait --tasks "$TASKS_A" --max-wait-ms 30000 --interval-ms 250 --timeout-ms 3000)"
echo "$DELEGATE_A_TO_B"
if ! grep -q "tmux peer B received the A2A message" <<<"$DELEGATE_A_TO_B"; then
echo "peer-a -> peer-b delegation response did not include expected peer B text" >&2
exit 1
fi

echo "checking fleet and delegated task views"
FLEET_A="$(a2a_cli "$REGISTRY_A" fleet --json --tasks "$TASKS_A" --timeout-ms 3000)"
echo "$FLEET_A"
if ! grep -q '"status": "online"' <<<"$FLEET_A"; then
echo "fleet output did not show peer-b online" >&2
exit 1
fi
if ! grep -q '"lastTask"' <<<"$FLEET_A"; then
echo "fleet output did not include the delegated task summary" >&2
exit 1
fi
TASKS_A_OUT="$(a2a_cli "$REGISTRY_A" tasks --json --tasks "$TASKS_A")"
echo "$TASKS_A_OUT"
if ! grep -q '"state": "TASK_STATE_COMPLETED"' <<<"$TASKS_A_OUT"; then
echo "task ledger did not record completed delegation" >&2
exit 1
fi

Expand Down
Loading
Loading