{"id":"0fd2739d2b1ee1d02df1bc761997fe8b29b41a6a307dd0b15a9644193008910d","pubkey":"2f4fa408d85b962d1fe717daae148a4c98424ab2e10c7dd11927e101ed3257b2","created_at":1771440928,"kind":30817,"tags":[["d","ai-agent-communication"],["title","AI Agent Communication"],["k","25802"],["k","25803"],["k","25805"],["k","31430"],["k","25800"],["k","25801"],["k","25804"],["k","25806"],["client","nostrhub.io"]],"content":"NIP-XX\n======\n\nAI Agent Messages\n-----------------\n\n`draft` `optional`\n\nThis NIP defines a protocol for bidirectional communication between Nostr clients\nand AI agent runtimes over Nostr relays.\n\nUntil a NIP number is assigned, this document uses placeholder number `XX` in the\ntitle and filename.\n\n## Kinds\n\nThis NIP reserves the following kinds for AI Agent communication:\n\neryoung.com. | ai.info                | No        |\n\nPrompt events (`25802`), terminal events (`25803`, `25805`), and AI info events (`31340`) are non-ephemeral, allowing durable replay and capability discovery for state restoration and audit. Streaming/tooling telemetry (`25800`, `25801`, `25804`, `25806`) remains ephemeral.\n\n## Rationale\n\nNostr has emerged as a universal transport for decentralized applications.\nAI agents are increasingly being deployed as networked services, and this protocol\nadds messaging shape for interactive, sessioned, streaming agent workflows.\n\n- **Decentralized identity**: Agents and clients identify via Nostr pubkeys.\n- **Interactive sessions**: Support multi-turn workflows with optional session grouping.\n- **Streaming**: Delta events let clients render partial output.\n- **Tool telemetry**: Agents can expose tool activity.\n- **Structured discovery**: `ai.info` advertises capabilities, limits, and supported formats.\n\n## Definitions\n\n### Run\n\nA **run** is a single prompt/response interaction.\n\n- One prompt event (`ai.prompt`, kind `25802`).\n- Optional cancellation request (`ai.cancel`, kind `25806`) from client while non-terminal.\n- Optional status (`ai.status`, kind `25800`) and delta (`ai.delta`, kind `25801`) events.\n- Optional tool-call events (`ai.tool_call`, kind `25804`).\n- Exactly one terminal event: `ai.response` (`25803`) or `ai.error` (`25805`).\n\nThe run identifier is the **prompt event id** (hex string). Non-prompt events MUST\nreference it in an `e` tag with marker `root`.\n\n### Session\n\nA **session** groups related runs.\n\n- The `s` tag identifies a session.\n- `s` is optional on protocol events.\n- If `s` is omitted, recipients SHOULD use `sender:<lowercase-hex-pubkey>`.\n- The default is deterministic and can be reproduced by all parties.\n\nClients MAY also use `[\"s\",\"session:<opaque-hash>\"]` for higher-level session IDs.\n\n### Actors\n\n- **Client**: User-facing app that sends prompts and renders agent output.\n- **Agent Runtime**: Nostr-native service that processes prompts.\n- **Relays**: Transport/storage layer.\n\n## Encryption\n\nAll protocol payloads in encrypted event kinds (`25800`, `25801`, `25802`, `25803`,\n`25804`, `25805`, `25806`) MUST use [NIP-44](44.md).\n\n### Required tag\n\nAll encrypted events in this NIP MUST include:\n\n```text\n[\"encryption\", \"nip44\"]\n```\n\nIf an agent and client both publish additional supported encryption schemes, senders\nMUST choose a scheme supported by both sides.\nIf no overlap exists, implementations MUST fail the request with\n`UNSUPPORTED_ENCRYPTION`.\n\n### Key agreement\n\nEncryption uses the sender’s private key and recipient’s public key.\nIn nostr-tools this is:\n\n```javascript\nconst conversationKey = nip44.utils.getConversationKey({\n  privateKey: senderPrivateKey,\n  publicKey: recipientPublicKey\n})\nconst encrypted = nip44.encrypt(plaintext, conversationKey)\n```\n\nThe plaintext must be JSON with a `ver` field.\n\nEach message MUST use a fresh nonce as defined by NIP-44.\n\n### NIP-59 metadata privacy (optional)\n\nNIP-44 encrypts content, but event metadata remains visible:\nauthors, pubkeys, tags, timestamps, and kind.\n\nClients and runtimes MAY wrap these events using [NIP-59](59.md) for metadata\nprivacy. When gift-wrapped:\n\n- routing SHOULD use the outer wrapper’s `p` tag and `kinds` filters;\n- inner tags are not intended for relay indexing;\n- clients still need to unwrap before applying payload validation.\n\nThis NIP defines two privacy profiles:\n\n1. **default**: NIP-44 encryption only.\n2. **privacy-first**: NIP-44 + NIP-59 wrapping + minimal disclosure in tool output.\n\nIf wrapped, tool metadata in `ai.tool_call` should avoid sensitive argument/output fields.\n\n## Event Formats\n\nAll fields marked `required` in tables and schemas below MUST be present.\n\nFor any event type where a field is repeated in both encrypted content and a tag\n(`tool`, `phase`, etc.), the encrypted content is the canonical source of truth.\nIf a mirror tag is present and differs from the encrypted payload, implementations\nMAY reject the event as `INVALID_SCHEMA`.\n\n### Structured `text` envelope (compatibility profile)\n\nFor compatibility with runtimes that currently emit telemetry as a compact text payload,\nimplementations MAY encode structured metadata inside `text` using this format:\n\n- delimiter between fields: `|`\n- field shape: `key=value`\n- value encoding: percent-encoding (`encodeURIComponent` / URL-encoding)\n\nExample:\n\n```text\nevent=tool|phase=start|name=web_fetch|call_id=call_123|target=https%3A%2F%2Fexample.com|ts=1771402416\n```\n\nWhen this envelope is used:\n\n- `event`, `phase`, and `ts` MUST be present.\n- consumers SHOULD ignore unknown keys.\n- producers SHOULD keep key names stable and lowercase snake_case.\n- if canonical JSON fields are present (for example `name`, `phase`, `arguments`), they\n  remain authoritative over mirrored envelope keys.\n\nPer-event required envelope keys:\n\n- `ai.response` (`25803`): `event=final`, `phase=end`, `text`\n- `ai.delta` (`25801`): `event=delta`, `phase`, and at least one of `text` or `block`\n- `ai.tool_call` (`25804`): `event=tool`, `phase`, and `text` or `name`\n- `ai.error` (`25805`): `event=error`, `phase=end`, `code`, `text`\n\nRecommended common keys for all envelope payloads:\n\n- `run_id` (or `runId`)\n- `session_id` (or `sessionId`)\n- `timestamp` (or `ts`)\n\nCanonical keys SHOULD use snake_case (`run_id`, `session_id`, `ts`).\nImplementations SHOULD also accept camelCase aliases for interoperability.\n\nThis format is intended as a wire-compatibility strategy and does not replace canonical\nJSON fields when those fields are available.\n\n### `ai.info` (kind 31340)\n\nAgent capability discovery event. This is a replaceable event (`kind 31340`).\nAgents SHOULD publish this in their pubkey namespace.\n\n**Tags**\n\n| Tag | Required | Description |\n|-----|----------|-------------|\n| d   | Yes      | Fixed identifier, e.g. `\"agent-info\"` |\n\nAgents SHOULD keep exactly one active `d` value and SHOULD not change it after first\npublication, so clients can reliably cache capabilities.\n\nClients SHOULD cache the newest valid `ai.info` publication by `(created_at, id)` and\nrefresh cached capabilities when capability entries change.\n\n**Content (JSON, unencrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"supports_streaming\": true,\n  \"supports_nip59\": true,\n  \"dvm_compatible\": false,\n  \"encryption\": [\"nip44\"],\n  \"supported_models\": [\"gpt-4.1-mini\", \"llama-3.1-70b\"],\n  \"default_model\": \"gpt-4.1-mini\",\n  \"tool_names\": [\"web_fetch\", \"calculator\"],\n  \"tool_schema_version\": 1,\n  \"max_prompt_bytes\": 32000,\n  \"max_context_tokens\": 128000,\n  \"tool_schemas\": {\n    \"calculator\": {\n      \"schema_version\": 1,\n      \"description\": \"Evaluate arithmetic expressions\",\n      \"requires_approval\": false,\n      \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"expr\": { \"type\": \"string\" },\n          \"precision\": { \"type\": \"number\" }\n        },\n        \"required\": [\"expr\"]\n      }\n    }\n  },\n  \"pricing_hints\": {\n    \"currency\": \"USD\",\n    \"per_1k_prompt_tokens\": 0.002,\n    \"per_1k_output_tokens\": 0.004\n  }\n}\n```\n\n### Model and schema negotiation\n\nClients SHOULD use `ai.info` before sending prompts. For each prompt:\n\n- If `model` is omitted, agents SHOULD use `default_model`.\n- If `model` is set, the agent MUST have it listed in `supported_models`.\n- If the sender supplies `tool_schema_version`, the agent MUST use that exact version.\n- If the sender omits `tool_schema_version`, agents MUST use `tool_schema_version` from\n  their latest `ai.info`.\n- If no compatible model/schema is advertised/supported, the agent MUST return an\n  `ai.error` with:\n  - `UNSUPPORTED_MODEL` when the requested model is unknown.\n  - `UNSUPPORTED_SCHEMA_VERSION` when the requested `tool_schema_version` is\n    incompatible.\n  and MUST NOT continue execution.\n\nThe `tool_schema_version` requested in a prompt binds accepted `ai.tool_call` JSON\nshapes for that run.\n\n### Prompt (kind 25802)\n\nClient → agent invocation.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Agent recipient pubkey |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"message\": \"user's message text\",\n  \"thinking\": \"low|medium|high|max\",\n  \"provider\": \"optional provider identifier\",\n  \"model\": \"optional model name\",\n  \"tool_schema_version\": 1,\n  \"fallback_models\": [\"list\", \"of\", \"fallbacks\"]\n}\n```\n\n### Cancel (kind 25806)\n\nClient → agent cancellation request.\n\nClients MAY emit this if they lose UI interest in a run.\nAgents SHOULD treat `ai.cancel` as idempotent for the same run (`p` + `e`).\nAgents MUST ignore `ai.cancel` for completed runs and MUST NOT emit additional terminal\nevents in that case.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Agent recipient pubkey |\n| e          | Yes      | Prompt id (`#e` root) |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"reason\": \"user_cancel|timeout|policy\"\n}\n```\n\n### Response (kind 25803)\n\nAgent → client terminal response.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Client recipient pubkey |\n| e          | Yes      | Prompt id (`#e` root) |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"text\": \"complete agent response\",\n  \"timestamp\": 1710000000,\n  \"usage\": {\n    \"input_tokens\": 100,\n    \"output_tokens\": 250\n  }\n}\n```\n\n`text` MAY be plain response text, or MAY carry a compatibility envelope string such as:\n\n```text\nevent=final|phase=end|text=Here%20is%20the%20answer...|finish_reason=stop|run_id=abc123|session_id=sender%3Aabc|ts=1771402425\n```\n\n### Delta (kind 25801)\n\nAgent → client streaming fragment.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Client recipient pubkey |\n| e          | Yes      | Prompt id (`#e` root) |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"text\": \"partial response text\",\n  \"seq\": 0\n}\n```\n\n`text` MAY alternatively carry a compatibility envelope string such as:\n\n```text\nevent=delta|mode=thinking|status=update|phase=update|block=Looking%20up%20latest%20pricing|text=Looking%20up%20latest%20pricing|run_id=abc123|session_id=sender%3Aabc|ts=1771402418\n```\n\nIf `seq` is present, it MUST be strictly increasing by `1` within a run.\n\n### Status (kind 25800)\n\nAgent → client state updates.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Client recipient pubkey |\n| e          | Yes      | Prompt id (`#e` root) |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"state\": \"thinking|tool_use|done\",\n  \"progress\": 50,\n  \"info\": \"additional status info\"\n}\n```\n\n### Tool Call (kind 25804)\n\nAgent tool-call telemetry. Agents own tool execution.\n\nFor this section, payload fields are canonical (`name` and `phase`), and `tool` and\n`phase` tags are optional index hints only.\n\nIf any optional hint tag is present, it SHOULD match the encrypted payload field.\nA mismatch MAY be treated as `INVALID_SCHEMA`.\n\nTo reduce telemetry leakage, agents SHOULD avoid including sensitive data in `output`\nunless strictly required for user intent; clients SHOULD treat tool output as potentially\nuntrusted and avoid surfacing secrets.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Client recipient pubkey |\n| e          | Yes      | Prompt id (`#e` root) |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n| tool       | No       | Optional index hint |\n| phase      | No       | Optional index hint: `start`/`result` |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"name\": \"calculator\",\n  \"phase\": \"start|result\",\n  \"arguments\": {\n    \"expr\": \"12 * 7\"\n  },\n  \"output\": {\n    \"stdout\": \"84\",\n    \"stderr\": \"\",\n    \"exit_code\": 0\n  },\n  \"success\": true,\n  \"duration_ms\": 120\n}\n```\n\nCompatibility envelope form inside `text` is also allowed, for example:\n\n```json\n{\n  \"ver\": 1,\n  \"text\": \"event=tool|phase=start|name=web_fetch|call_id=call_123|target=https%3A%2F%2Fexample.com|ts=1771402416\",\n  \"timestamp\": 1771402416\n}\n```\n\n### Error (kind 25805)\n\nTerminal failure event.\n\n**Tags**\n\n| Tag        | Required | Description |\n|------------|----------|-------------|\n| p          | Yes      | Client recipient pubkey |\n| e          | Yes      | Prompt id (`#e` root) |\n| encryption | Yes      | Must be `\"nip44\"` |\n| s          | No       | Session identifier |\n\n**Content (JSON, encrypted)**\n\n```json\n{\n  \"ver\": 1,\n  \"code\": \"RATE_LIMIT\",\n  \"message\": \"provider unavailable\",\n  \"retry_after\": 30,\n  \"details\": {\n    \"provider\": \"provider-id\"\n  }\n}\n```\n\nError codes:\n\n| Code               | Meaning |\n|--------------------|---------|\n| UNSUPPORTED_ENCRYPTION | Requested encryption scheme unsupported |\n| UNSUPPORTED_MODEL  | Requested model unavailable |\n| UNSUPPORTED_SCHEMA_VERSION | Requested tool schema version unsupported |\n| CANCELLED          | Run cancelled |\n| RATE_LIMIT         | Request throttled |\n| UNAUTHORIZED       | Sender or agent unauthorized |\n| BLOCKED_SENDER     | Sender blocked by policy |\n| MODEL_UNAVAILABLE  | Requested model/provider unavailable |\n| SESSION_LIMIT      | Session turns exceeded |\n| PARSE_ERROR        | Prompt payload invalid |\n| EMPTY_RESPONSE     | No response produced |\n| TOOL_ERROR         | Tool execution failed |\n| INVALID_SCHEMA     | Payload failed schema validation |\n| UNSUPPORTED_FEATURE | Requested feature/tool/scheme unsupported |\n| INVALID_SEQUENCE    | Delta sequence invalid for this run |\n| INTERNAL_ERROR     | Unexpected runtime failure |\n\n## JSON Schema\n\n### Common fields\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-agent-message.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 }\n  },\n  \"additionalProperties\": true\n}\n```\n\n### Prompt schema (`25802`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-prompt.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"message\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"message\": { \"type\": \"string\", \"minLength\": 1 },\n    \"thinking\": { \"type\": \"string\", \"enum\": [\"low\", \"medium\", \"high\", \"max\"] },\n    \"provider\": { \"type\": \"string\", \"minLength\": 1 },\n    \"model\": { \"type\": \"string\", \"minLength\": 1 },\n    \"tool_schema_version\": { \"type\": \"integer\", \"minimum\": 1 },\n    \"fallback_models\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" }\n    }\n  }\n}\n```\n\n### Cancel schema (`25806`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-cancel.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"reason\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"reason\": { \"type\": \"string\", \"enum\": [\"user_cancel\", \"timeout\", \"policy\"] }\n  }\n}\n```\n\n### Response schema (`25803`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-response.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"text\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"text\": { \"type\": \"string\" },\n    \"timestamp\": { \"type\": \"integer\", \"minimum\": 0 },\n    \"usage\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"input_tokens\": { \"type\": \"integer\", \"minimum\": 0 },\n        \"output_tokens\": { \"type\": \"integer\", \"minimum\": 0 }\n      },\n      \"required\": [\"input_tokens\", \"output_tokens\"]\n    }\n  }\n}\n```\n\n### Delta schema (`25801`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-delta.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"text\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"text\": { \"type\": \"string\" },\n    \"seq\": { \"type\": \"integer\", \"minimum\": 0 },\n    \"timestamp\": { \"type\": \"integer\", \"minimum\": 0 }\n  }\n}\n```\n\n### Status schema (`25800`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-status.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"state\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"state\": { \"type\": \"string\", \"enum\": [\"thinking\", \"tool_use\", \"done\"] },\n    \"progress\": { \"type\": \"integer\", \"minimum\": 0, \"maximum\": 100 },\n    \"info\": { \"type\": \"string\" }\n  }\n}\n```\n\n### Tool-call schema (`25804`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-tool-call.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\"],\n  \"oneOf\": [\n    {\n      \"required\": [\"text\"]\n    },\n    {\n      \"required\": [\"name\", \"phase\"]\n    }\n  ],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"text\": { \"type\": \"string\", \"minLength\": 1 },\n    \"name\": { \"type\": \"string\", \"minLength\": 1 },\n    \"phase\": { \"type\": \"string\", \"enum\": [\"start\", \"result\"] },\n    \"arguments\": { \"type\": \"object\" },\n    \"output\": { \"type\": \"object\" },\n    \"success\": { \"type\": \"boolean\" },\n    \"duration_ms\": { \"type\": \"integer\", \"minimum\": 0 },\n    \"timestamp\": { \"type\": \"integer\", \"minimum\": 0 }\n  }\n}\n```\n\n### Error schema (`25805`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-error.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"code\", \"message\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"code\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"UNSUPPORTED_ENCRYPTION\",\n        \"UNSUPPORTED_MODEL\",\n        \"UNSUPPORTED_SCHEMA_VERSION\",\n        \"CANCELLED\",\n        \"RATE_LIMIT\",\n        \"UNAUTHORIZED\",\n        \"BLOCKED_SENDER\",\n        \"MODEL_UNAVAILABLE\",\n        \"SESSION_LIMIT\",\n        \"PARSE_ERROR\",\n        \"EMPTY_RESPONSE\",\n        \"TOOL_ERROR\",\n        \"INVALID_SCHEMA\",\n        \"UNSUPPORTED_FEATURE\",\n        \"INVALID_SEQUENCE\",\n        \"INTERNAL_ERROR\"\n      ]\n    },\n    \"message\": { \"type\": \"string\", \"minLength\": 1 },\n    \"retry_after\": { \"type\": \"integer\", \"minimum\": 1 },\n    \"details\": { \"type\": \"object\" }\n  }\n}\n```\n\n### Info schema (`31340`)\n\n```json\n{\n  \"$id\": \"https://example.com/nip-xx-info.json\",\n  \"type\": \"object\",\n  \"required\": [\"ver\", \"encryption\", \"tool_names\"],\n  \"properties\": {\n    \"ver\": { \"const\": 1 },\n    \"supports_streaming\": { \"type\": \"boolean\" },\n    \"supports_nip59\": { \"type\": \"boolean\" },\n    \"dvm_compatible\": { \"type\": \"boolean\" },\n    \"encryption\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" },\n      \"contains\": { \"const\": \"nip44\" }\n    },\n    \"supported_models\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n    \"default_model\": { \"type\": \"string\" },\n    \"tool_names\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n    \"tool_schema_version\": { \"type\": \"integer\", \"minimum\": 1 },\n    \"tool_schemas\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"required\": [\"schema_version\", \"description\", \"input_schema\"],\n        \"properties\": {\n          \"schema_version\": { \"type\": \"integer\", \"minimum\": 1 },\n          \"description\": { \"type\": \"string\" },\n          \"requires_approval\": { \"type\": \"boolean\" },\n          \"input_schema\": { \"type\": \"object\" },\n          \"output_schema\": { \"type\": \"object\" }\n        }\n      }\n    },\n    \"max_prompt_bytes\": { \"type\": \"integer\", \"minimum\": 1 },\n    \"max_context_tokens\": { \"type\": \"integer\", \"minimum\": 1 },\n    \"pricing_hints\": { \"type\": \"object\" }\n  }\n}\n```\n\n## Validation and failure rules\n\nImplementations MUST follow these validation and failure rules:\n\n- JSON parse failures in encrypted payloads MUST be reported with `code = PARSE_ERROR`.\n- Missing required tags (`p`, `e` where required, `encryption`) or malformed tag values MUST be treated as `INVALID_SCHEMA`. The `d` tag is required only for `ai.info` (`31340`).\n- Invalid protocol content (for example unknown `state`/`thinking`/`code`) MUST be treated as\n  `INVALID_SCHEMA`.\n- `e` tags on non-prompt events MUST reference an existing or referenced prompt event id.\n- Non-`ai.prompt` events MUST require `e` marker `root`.\n- `ai.delta` with non-monotonic `seq` for the same run MUST be treated as\n  `INVALID_SEQUENCE` when `seq` is present.\n- `ai.cancel` without matching active run MUST be ignored.\n- Duplicate `ai.cancel` for unfinished runs SHOULD be treated as idempotent and MUST NOT\n  change accepted terminal selection.\n- Clients SHOULD ignore `ai.status`, `ai.delta`, and `ai.tool_call` for runs already in a\n  terminal state, except when retained for audit/debug tooling.\n- Agents SHOULD emit at most one terminal `ai.error` with `code=CANCELLED` for a given run.\n- If a run reaches a terminal state and emits additional terminal events, clients MUST keep\n  only the terminal event with the highest `created_at` and highest `id` as tie-breaker.\n- If a run is missing a terminal response after reasonable timeout, clients MAY treat it as\n  an incomplete run and surface an implementation-specific state.\n\n## Streaming and reconciliation\n\nFor `ai.delta` events (`kind 25801`):\n\n- Clients MUST ignore deltas where `(e, p, encryption)` do not match the subscribed run and\n  recipient.\n- If `seq` is present, it MUST be contiguous starting at `0`.\n- Clients SHOULD collect deltas and order by `(seq, created_at, id)` when `seq` is present, otherwise by `(created_at, id)`.\n- Clients MUST dedupe duplicates by `(event.id)` and SHOULD dedupe identical `(seq, text)` tuples when `seq` is present.\n- If a gap is detected (missing `seq`), clients SHOULD continue best-effort rendering and\n  can display a soft placeholder (“streaming degraded”) until `ai.response` arrives.\n- Final render MUST be taken from `ai.response` text, not from the delta stream.\n- Clients MUST NOT apply deltas after a terminal event has been accepted for a run.\n\n### Envelope parsing helper (non-normative)\n\n```ts\nfunction parseTextEnvelope(text: string): Record<string, string> {\n  const out: Record<string, string> = {};\n  for (const part of text.split(\"|\")) {\n    const idx = part.indexOf(\"=\");\n    if (idx <= 0) continue;\n    const key = part.slice(0, idx).trim();\n    const value = part.slice(idx + 1);\n    out[key] = decodeURIComponent(value);\n  }\n  return out;\n}\n```\n\n## Protocol Flow\n\n1. Client SHOULD read `ai.info` for capabilities:\n   ```text\n   {\"kinds\":[31340],\"authors\":[\"<agent-pubkey>\"]}\n   ```\n   If no `ai.info` exists, clients SHOULD:\n   - proceed with `supports_streaming = true`\n   - assume `encryption = [\"nip44\"]`\n   - assume `tool_names = []`\n   - disable tool-related UI\n2. If `supports_nip59` is true and stronger privacy is desired, client and agent SHOULD\n   use NIP-59 wrappers.\n3. Client sends `25802` prompt:\n   - `p` = agent pubkey\n   - optional `s`\n   - optional `model` and `tool_schema_version` matching `ai.info`\n   - `encryption = nip44`\n   - encrypted JSON payload\n4. Agent subscribes to prompts:\n   ```text\n   {\"kinds\":[25802],\"#p\":[\"<agent-pubkey>\"]}\n   ```\n5. Agent emits `25800`, `25801`, and optional `25804`.\n6. Client MAY cancel a non-terminal run by emitting `25806` (same prompt id in `e`).\n7. If cancellation is effective before terminal output, agent SHOULD emit `25805` with\n   `code = CANCELLED`.\n8. Agent emits terminal event:\n   - `25803` on success\n   - `25805` on failure\n   If a cancellation arrives after a terminal event has already been emitted, the\n   cancellation MUST be treated as no-op.\n9. Client subscribes to terminal and streaming events:\n   ```text\n     {\n       \"kinds\": [25800,25801,25803,25804,25805],\n     \"#p\": [\"<client-pubkey>\"],\n     \"#e\": [\"<prompt-event-id>\"],\n     \"authors\": [\"<agent-pubkey>\"]\n    }\n   ```\n   Clients SHOULD additionally enforce run-local ordering by matching the selected\n   `#e` value and `authors` set.\n\n### Session recovery recipe\n\n- Find active session:\n  ```text\n  {\"kinds\":[25802],\"#s\":[\"sender:<client-pubkey>\"],\"#p\":[\"<agent-pubkey>\"]}\n  ```\n- Resume a run:\n  ```text\n  {\"#e\":[\"<prompt-id>\"], \"authors\":[\"<agent-pubkey>\"], \"kinds\":[25800,25801,25803,25804,25805]}\n  ```\n- Latest terminal event should be taken as the current run state; deltas may be stale on relay replay.\n\n## Tool model and security\n\nThis NIP defines a **runtime-exec model**:\n\n- Agents own tool execution and emit `ai.tool_call` telemetry only.\n- Clients MUST NOT execute tools based on `ai.tool_call`.\n- Tool schemas advertised in `ai.info` let clients present expected UI affordances.\n- Agents MUST negotiate `model` and `tool_schema_version`; if unsupported, MUST return\n  `UNSUPPORTED_MODEL` or `UNSUPPORTED_SCHEMA_VERSION`.\n\n### Security and abuse controls\n\n- Agents SHOULD reject unknown senders not in allowlists and return `UNAUTHORIZED`.\n- Agents SHOULD reject senders blocked by policy and return `BLOCKED_SENDER`.\n- Agents SHOULD reject unsupported tools and return `UNSUPPORTED_FEATURE`.\n- Agents SHOULD validate schemas before execution and return `INVALID_SCHEMA` on mismatch.\n- Agents SHOULD map execution or transport throttling to `RATE_LIMIT`.\n- Agents SHOULD return `UNSUPPORTED_ENCRYPTION` when the requested scheme is unsupported.\n- `retry_after` SHOULD be present for transient failures where retry is useful.\n- Clients SHOULD prefer the newest terminal event by `(created_at,id)` when multiple terminal\n  events are observed.\n\n## Full example (encrypted payloads shown as placeholders)\n\n```text\nPrompt (25802, from client A to agent B)\n{\n  \"kind\": 25802,\n  \"pubkey\": \"A\",\n  \"tags\": [\n    [\"p\",\"B\"],\n    [\"s\",\"sender:A\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n\nStatus (25800)\n{\n  \"kind\": 25800,\n  \"tags\": [\n    [\"p\",\"A\"],\n    [\"e\",\"d5f...3a\",\"\", \"root\"],\n    [\"s\",\"sender:A\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n\nDelta (25801, seq 0)\n{\n  \"kind\": 25801,\n  \"tags\": [\n    [\"p\",\"A\"],\n    [\"e\",\"d5f...3a\",\"\", \"root\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n\nDelta (25801, seq 1)\n{\n  \"kind\": 25801,\n  \"tags\": [\n    [\"p\",\"A\"],\n    [\"e\",\"d5f...3a\",\"\", \"root\"],\n    [\"s\",\"sender:A\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n\nTool call telemetry (25804)\n{\n  \"kind\": 25804,\n  \"tags\": [\n    [\"p\",\"A\"],\n    [\"e\",\"d5f...3a\",\"\", \"root\"],\n    [\"tool\",\"calculator\"],\n    [\"phase\",\"start\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n\nCancel request (25806)\n{\n  \"kind\": 25806,\n  \"tags\": [\n    [\"p\",\"A\"],\n    [\"e\",\"d5f...3a\",\"\", \"root\"],\n    [\"s\",\"sender:A\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n\nResponse (25803)\n{\n  \"kind\": 25803,\n  \"tags\": [\n    [\"p\",\"A\"],\n    [\"e\",\"d5f...3a\",\"\", \"root\"],\n    [\"encryption\",\"nip44\"]\n  ],\n  \"content\": \"<nip44-ciphertext>\"\n}\n```\n\n### Conformance examples\n\n1. **Invalid schema**: `ai.prompt` with `message` missing MUST be rejected and MAY\n   return `ai.error` with `INVALID_SCHEMA`.\n2. **Unknown tool**: `tool_call.name` not in `ai.info.tool_names` SHOULD be treated as\n   unsupported and MAY return `UNSUPPORTED_FEATURE`.\n3. **Out-of-order deltas**: clients reorder by `seq` as described in reconciliation.\n4. **Encryption mismatch**: noncompliant `encryption` tag content MUST be rejected as\n   `UNSUPPORTED_ENCRYPTION` or `INVALID_SCHEMA`.\n5. **Duplicate terminal events**: clients MUST dedupe by `(created_at,id)` and render with\n   only the newest terminal event.\n6. **Cancel path**: client can cancel a run by `25806` and should treat subsequent\n   `25805` with `code = CANCELLED` as expected terminal completion.\n7. **Response/cancel race**: if both `25803` and `25805` for the same run exist, clients\n   MUST keep the terminal event with highest `(created_at,id)` and apply normal ordering.\n\n## Backward Compatibility\n\nImplementations MUST:\n\n- Ignore unknown encrypted JSON fields.\n- Use canonical fields.\n- Avoid field duplication for historical aliases in future versions.\n- Keep `s` optional and default to `sender:<lowercase-hex-pubkey>` when absent.\n\nThis document defines `ver: 1`; field names and meanings MUST NOT be redefined in\nthe same major version.\n\n## Implementations\n- **openclaw**: Open PR adding support to OpenClaw:\n  - PR: https://github.com/joelklabo/openclaw/pull/2\n- **clawlet**: Web client (Next.js) with (ZeroClaw) Rust runtime\n  - Web: https://github.com/joelklabo/clawlet\n\n## Copyright\n\nThis document is placed in the public domain.","sig":"37b4b3b3b9c46d92e6768a8886dfe808128f1428a6c16a181ae4e8026b8d9cb16689159031f4670a5286a7fd7a5c676f9af66b6b1e572872561a77ec2188bc72"}