docs(plan): A2A client implementation plan; spec revisions

This commit is contained in:
EugeneTes
2026-10-08 12:29:29 +00:00
parent b8be0f82ab
commit e70557725a
2 changed files with 2539 additions and 8 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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`.
- **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`
```json
@@ -47,15 +71,17 @@ The tool never includes keys in its results or errors.
## 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 |
|---|---|---|
| `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 |
| `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 |
| `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
@@ -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
(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
paths, cap `MaxFileBytes`), metadata `volcanic.hops`. Uses `SendStreamingMessage` when the card has `streaming`,
else `SendMessage` with `returnImmediately: true` followed by `GetTask` polling. Then, within `WaitSeconds`:
paths, cap `MaxFileBytes`), metadata `volcanic.hops`. Calls `SendMessage` with `returnImmediately: true`, then waits by
`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,
`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);
@@ -89,7 +115,7 @@ tool never throws into the loop.
Watch record, persisted in `<workspace>/.volcanic/a2a/pending.json`:
```json
{ "watchId": "…", "sessionScope": "telegram-111", "agent": "travel-bot", "taskId": "…", "contextId": "…",
{ "watchId": "…", "target": "111", "agent": "travel-bot", "taskId": "…", "contextId": "…",
"pushToken": "…", "startedAt": "…", "source": "push" }
```
@@ -138,8 +164,9 @@ message sends `hops + 1`. The server refuses above `MaxHops`.
## Telegram host
- References the extension, registers it host-wide with `TelegramA2AResultDelivery` and `A2AClientOptions` from config.
- Wires `OnPush` to the registry and calls `RestoreAsync` over allowed chats' workspaces at startup.
- With `A2AClient:Enabled`, adds the extension to each chat session (target = chat id), uses
`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,
source, age).