docs(plan): A2A client implementation plan; spec revisions
This commit is contained in:
2504
docs/superpowers/plans/2026-10-08-a2a-client.md
Normal file
2504
docs/superpowers/plans/2026-10-08-a2a-client.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,30 @@ possibly after asking the user), and get results of long-running tasks back with
|
|||||||
- **`input-required`** is returned to the model with the `taskId`; the model answers with `send` and that `taskId`.
|
- **`input-required`** is returned to the model with the `taskId`; the model answers with `send` and that `taskId`.
|
||||||
- **Hops**: each outgoing message carries `metadata["volcanic.hops"]` = inbound hops + 1.
|
- **Hops**: each outgoing message carries `metadata["volcanic.hops"]` = inbound hops + 1.
|
||||||
|
|
||||||
|
## Revisions (2026-10-08, before implementation)
|
||||||
|
|
||||||
|
Decided with the owner after the server shipped; these override the sections below where they differ.
|
||||||
|
|
||||||
|
- **Off by default, its own switch**: configuration section `A2AClient` (env `A2AClient__…`), `Enabled = false`.
|
||||||
|
Independent of the server's `A2A__Enabled`. Off: the extension isn't added to any session, no watches are restored,
|
||||||
|
`/a2a` says the client is off.
|
||||||
|
- **Only the owner's own chat sessions get the `a2a` tool.** The Telegram host adds the extension per chat (like the
|
||||||
|
scheduler), never host-wide, so isolated A2A server sessions (driven by untrusted callers) can't call other agents
|
||||||
|
with the keys in `agents.json`. The hop counter still listens to `a2a.inbound` for hosts that add it elsewhere.
|
||||||
|
- **Settings**: `Enabled`, `WaitSeconds` (60), `WatchTimeout` (1 day), `CardTimeoutSeconds` (10), `MaxFileBytes` (10 MB).
|
||||||
|
The push base URL isn't a client setting: the host passes the server's `A2A:PublicUrl` only when the server is on
|
||||||
|
(the push receiver lives there). Without it, watches subscribe or poll.
|
||||||
|
- **Send = `returnImmediately` + wait.** `send` always calls `SendMessage` with `returnImmediately: true`, then waits up to
|
||||||
|
`WaitSeconds` by subscribing (streaming agents) or polling `GetTask`. A dropped streaming request would cancel the
|
||||||
|
remote run on servers that tie the run to the request (ours does), so the client never streams the send itself.
|
||||||
|
Agents that ignore `returnImmediately` simply answer late (the call is bounded by `WatchTimeout`).
|
||||||
|
- **Delivery contract**: `IA2AResultDelivery { Task<bool> DeliverAsync(target, text, ct); string WorkspaceFor(target);
|
||||||
|
IEnumerable<string> KnownTargets(); }` (targets = Telegram chat ids, like the scheduler). The default
|
||||||
|
`LiveSessionDelivery` delivers through the live session's `IExtensionApi.SendUserMessage(FollowUp)` and returns false
|
||||||
|
without one; undelivered results stay in `pending.json` and go out on that target's next session start.
|
||||||
|
- **The registry owns one HTTP handler** (`A2AWatchRegistry`, process-wide `Shared`); tests inject a handler that reaches
|
||||||
|
an in-process server.
|
||||||
|
|
||||||
## `agents.json`
|
## `agents.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -47,15 +71,17 @@ The tool never includes keys in its results or errors.
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Read from the same `A2A` section as the server (env `A2A__…`), passed through `A2AClientOptions`:
|
Section `A2AClient` (env `A2AClient__…`), bound to `A2AClientSettings` (the SDK already has an `A2AClientOptions`):
|
||||||
|
|
||||||
| Key | Default | Meaning |
|
| Key | Default | Meaning |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| `Enabled` | `false` | add the `a2a` tool to the owner's chats, restore watches |
|
||||||
| `WaitSeconds` | `60` | how long `send` blocks before handing over to a watch |
|
| `WaitSeconds` | `60` | how long `send` blocks before handing over to a watch |
|
||||||
| `WatchTimeout` | `1.00:00:00` | when a watch gives up |
|
| `WatchTimeout` | `1.00:00:00` | when a watch gives up |
|
||||||
| `PublicUrl` | – | our external base URL; required for push |
|
|
||||||
| `MaxFileBytes` | `10485760` | cap per file part, in and out |
|
|
||||||
| `CardTimeoutSeconds` | `10` | per-agent card fetch timeout |
|
| `CardTimeoutSeconds` | `10` | per-agent card fetch timeout |
|
||||||
|
| `MaxFileBytes` | `10485760` | cap per file part, in and out |
|
||||||
|
|
||||||
|
The push base URL comes from the server's `A2A:PublicUrl`, and only while `A2A:Enabled` is on.
|
||||||
|
|
||||||
## The `a2a` tool
|
## The `a2a` tool
|
||||||
|
|
||||||
@@ -65,8 +91,8 @@ Arguments: `action` (`agents` \| `send` \| `status` \| `cancel`), `agent`, `mess
|
|||||||
`supportsExtendedAgentCard`, the extended card. Returns per agent: name, description (entry override or card), skills
|
`supportsExtendedAgentCard`, the extended card. Returns per agent: name, description (entry override or card), skills
|
||||||
(id, name, description, examples), streaming/push capabilities, or `unreachable: <reason>`.
|
(id, name, description, examples), streaming/push capabilities, or `unreachable: <reason>`.
|
||||||
- **`send`** `{agent, message, contextId?, taskId?, files?}` — builds a message (text part, file parts read from workspace
|
- **`send`** `{agent, message, contextId?, taskId?, files?}` — builds a message (text part, file parts read from workspace
|
||||||
paths, cap `MaxFileBytes`), metadata `volcanic.hops`. Uses `SendStreamingMessage` when the card has `streaming`,
|
paths, cap `MaxFileBytes`), metadata `volcanic.hops`. Calls `SendMessage` with `returnImmediately: true`, then waits by
|
||||||
else `SendMessage` with `returnImmediately: true` followed by `GetTask` polling. Then, within `WaitSeconds`:
|
`SubscribeToTask` (card has `streaming`) or `GetTask` polling. Then, within `WaitSeconds`:
|
||||||
- terminal (`completed`, `failed`, `canceled`, `rejected`) → returns state, the agent's final text, the artifact list,
|
- terminal (`completed`, `failed`, `canceled`, `rejected`) → returns state, the agent's final text, the artifact list,
|
||||||
`contextId`, `taskId`. File artifacts are saved under `<workspace>/inbox/a2a/<agent>/` and returned as paths;
|
`contextId`, `taskId`. File artifacts are saved under `<workspace>/inbox/a2a/<agent>/` and returned as paths;
|
||||||
text artifacts are inlined (long ones saved to a file and returned as a path, by the tool's normal truncation);
|
text artifacts are inlined (long ones saved to a file and returned as a path, by the tool's normal truncation);
|
||||||
@@ -89,7 +115,7 @@ tool never throws into the loop.
|
|||||||
Watch record, persisted in `<workspace>/.volcanic/a2a/pending.json`:
|
Watch record, persisted in `<workspace>/.volcanic/a2a/pending.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "watchId": "…", "sessionScope": "telegram-111", "agent": "travel-bot", "taskId": "…", "contextId": "…",
|
{ "watchId": "…", "target": "111", "agent": "travel-bot", "taskId": "…", "contextId": "…",
|
||||||
"pushToken": "…", "startedAt": "…", "source": "push" }
|
"pushToken": "…", "startedAt": "…", "source": "push" }
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,8 +164,9 @@ message sends `hops + 1`. The server refuses above `MaxHops`.
|
|||||||
|
|
||||||
## Telegram host
|
## Telegram host
|
||||||
|
|
||||||
- References the extension, registers it host-wide with `TelegramA2AResultDelivery` and `A2AClientOptions` from config.
|
- With `A2AClient:Enabled`, adds the extension to each chat session (target = chat id), uses
|
||||||
- Wires `OnPush` to the registry and calls `RestoreAsync` over allowed chats' workspaces at startup.
|
`TelegramA2AResultDelivery`, and restores watches over the allowed chats at startup.
|
||||||
|
- When the server is on too, wires its `OnPush` to the registry and passes `A2A:PublicUrl` as the push base URL.
|
||||||
- `/a2a` additionally lists remote agents from `agents.json` (name, url, key masked) and active watches (agent, task,
|
- `/a2a` additionally lists remote agents from `agents.json` (name, url, key masked) and active watches (agent, task,
|
||||||
source, age).
|
source, age).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user