115 lines
4.8 KiB
Markdown
115 lines
4.8 KiB
Markdown
# Tool Description & Push Delivery (`describe` + `blocks` + APNs)
|
|
|
|
_Normative for approval rendering on the client side._
|
|
|
|
> How the app receives and displays **what** it is approving, and how the push notification stays
|
|
> lightweight. Primarily concerns **approvals** (tool arguments are hard to read as raw JSON);
|
|
> clarifications already carry a human-written `question` from the LLM.
|
|
>
|
|
> This file defines the **wire contract** (what the client receives). How the blocks are
|
|
> produced on the agent side (built-in vs MCP, templates) is in the agent's tool description
|
|
> infrastructure.
|
|
|
|
---
|
|
|
|
## 1. Two Representations, One Item
|
|
|
|
Every `approvals[]` entry in an `inbox_update` ([payloads.md §3.1](payloads.md)) carries **two**
|
|
views of the same tool call:
|
|
|
|
| Field | What it is | Used for |
|
|
|-------|-----------|----------|
|
|
| `summary` | Short human string, generated by `describe(Short)` on the agent (e.g. *"Send an email to mario@acme.com"*) | Card row, **push notification**, conversation log |
|
|
| `blocks` | **Structured** parameter description, generated by `describe_view(args)` | Detail screen (forms/tables/diffs) |
|
|
| `arguments` | Raw args (tool's JSON) | Final fallback / debug |
|
|
|
|
`summary` is the source of truth for "narrow" surfaces (notification, badge); `blocks` for the
|
|
detail screen. `arguments` remain as a safety net.
|
|
|
|
---
|
|
|
|
## 2. `blocks` Schema (wire)
|
|
|
|
A tool call is described as a **list of typed blocks**. The vocabulary is **small and stable**:
|
|
tools are unlimited, block types are not. Each client maps types to its native widgets *once*,
|
|
and it works for any present and future tool.
|
|
|
|
```json
|
|
{
|
|
"v": 1,
|
|
"summary": "Send an email to to@mail.com",
|
|
"blocks": [
|
|
{ "type": "key_value", "key": "Email-To", "value": "to@mail.com", "value_type": "email" },
|
|
{ "type": "field", "label": "Subject", "value": "Q3 Estimate", "value_type": "string" },
|
|
{ "type": "block", "label": "Body", "value": "…body…", "value_type": "text" }
|
|
]
|
|
}
|
|
```
|
|
|
|
Example for `write_file`:
|
|
|
|
```json
|
|
{
|
|
"v": 1,
|
|
"summary": "Write /path/x.rs",
|
|
"blocks": [
|
|
{ "type": "key_value", "key": "File", "value": "/path/x.rs", "value_type": "path" },
|
|
{ "type": "block", "label": "Diff", "value": "= same\n- old line\n+ new line", "value_type": "diff" }
|
|
]
|
|
}
|
|
```
|
|
|
|
**`type`** = layout hint (three values suffice):
|
|
|
|
| `type` | Rendering |
|
|
|--------|-----------|
|
|
| `key_value` | Compact `key: value` row |
|
|
| `field` | Labelled field, value on one line |
|
|
| `block` | Extended content with label (multi-line / dedicated viewer) |
|
|
|
|
**`value_type`** = value semantics → widget + formatting:
|
|
|
|
`string` · `text` · `markdown` · `code` · `diff` · `command` · `email` · `url` · `path` · `json` · `number` · `boolean` · `datetime` · `secret`
|
|
|
|
Rendering notes:
|
|
- `diff` → diff viewer (green/red). `command` / `code` → monospace. `email` / `url` → tappable.
|
|
- `secret` → **masked by default** (e.g. API key in args).
|
|
- Block fields: `key` (for `key_value`) or `label` (for `field`/`block`), `value`, `value_type`.
|
|
|
|
---
|
|
|
|
## 3. Delivery Model: Lightweight Push, Detail via WS
|
|
|
|
**Principle: the push never carries `blocks`/`arguments`. Only the `summary`.**
|
|
|
|
| Channel | What travels |
|
|
|---------|-------------|
|
|
| **WS** (live or `inbox_request` on tap) | **Complete** `inbox_update`: `summary` + `blocks` + `arguments` |
|
|
| **APNs/FCM push** | A minimal `notification` (kind §3.2 in payloads.md) with `body` = `summary` (`describe`), **no blocks/args** |
|
|
|
|
Flow: the app receives the notification with the `summary` line → user taps → app opens WS and
|
|
sends `inbox_request` ([payloads.md §4.6](payloads.md)) → receives the complete `inbox_update`
|
|
with `blocks` → shows the detail screen.
|
|
|
|
### Why (zero-trust + size)
|
|
|
|
- The relay is **zero-trust**: it cannot read the encrypted content, so it **cannot** extract
|
|
`summary` from the `inbox_update` blob. It is the **agent** that emits, for the push, a
|
|
lightweight `notification` payload (E2E, decrypted by the NSE) containing only the `summary`.
|
|
- This way the push **always** fits under the content-in-push threshold (3500B b64,
|
|
[server.md §5](server.md)) → notification always readable, without the content-vs-wake juggling
|
|
needed for rich payloads.
|
|
- The `aps.alert` in plaintext ("Skald / Action required") remains the generic fallback visible
|
|
to Apple; the real text (`summary`) is in the encrypted blob that the NSE replaces.
|
|
|
|
---
|
|
|
|
## 4. Forward-Compat & Degradation
|
|
|
|
- `v` at the top of `blocks`. An unknown `type`/`value_type` is not an error: the client
|
|
degrades to `key_value` / raw string (never crash).
|
|
- If `blocks` is absent (older agent, or tool with no description), the client shows `summary`
|
|
and optionally raw `arguments`. Fully backward-compatible.
|
|
- Non-form surfaces (e.g. Telegram) **flatten** blocks to text (`key: value`, diff in monospace):
|
|
every block is linearisable by construction.
|