Builds on #93 (agents see app events only) and #94 (name glob filter on every surface).
Why
Tools are discoverable: an app registers each one with a description and schema, and an agent reads them with appduct tools or appduct_list_tools before calling anything. Events are not. An app posts them with postEvent(name, payload) and nothing tells the daemon which names exist or what their payloads look like. So an agent that wants to wait for an outcome has to guess the name for appduct_wait_for_event, read the app's source, or drain the buffer hoping the event already fired. docs/TOOLS.md tells app authors to postEvent("checkout_completed", { orderId }), yet the daemon cannot tell an agent that name exists.
Expected outcome
An app declares the events it posts, and agents list them the way they list tools.
App side. registerEvent({ name, description, payloadSchema? }) in React Native, Swift and Kotlin, returning a disposer, with the same lifecycle as registerTool: declared at any time while the app runs, removed on dispose, re-sent in full after a resume. payloadSchema follows the tool schema rules (Standard Schema or raw JSON Schema; the daemon never inspects it). Declaration is advisory: an undeclared postEvent still flows to agents. In development the SDK warns when a posted name is undeclared or its payload fails the declared schema; production posts as-is.
Wire and daemon. An event registry per session beside the tool registry, fed by an event_registry_snapshot / event_registry_delta pair with the same strict-validation-or-close rule as the tool registry, and an events.list RPC with the usual selector semantics. Registry changes emit an internal kind that goes to the log from #93, not to agents.
Agent surface. One new MCP tool, appduct_list_events({ selector?, name?, limit?, offset? }), returning one signature per declared event (checkout_completed { orderId: string } plus its description) with total. name takes #94's glob; an exact name that matches one declared event returns its full payload schema as well. CLI: appduct events --list [--name <glob>], same output as one signature line per event, or the full schema for an exact name. The shipped skill tells agents to list events before waiting on one, and the writing-tools reference tells app authors to declare each event they post.
Constraints and non-goals
- Declaration must not become an allowlist: every app using
postEvent today keeps working unchanged. An SDK from before this change connects and works with no event registry.
- The daemon does not validate posted payloads against the declared schema.
- Non-goal: grouping events the way tools are grouped.
- Non-goal: a separate describe tool; the exact-name form of the list covers it.
- Non-goal: deriving a catalog from names already seen in the buffer.
How we know it is done
- After
registerEvent({ name: "checkout_completed", description: "...", payloadSchema }), events.list returns that event with its description and schema; after the disposer runs, it is gone.
appduct_list_events returns signature lines with total; with name: "cart.*" only matching events; with an exact name matching one event, that event's full payload schema.
appduct events --list prints one signature line per declared event, --name <glob> narrows it, and an exact name prints the full schema; --json carries the same data.
- A session that resumes re-sends its event registry, and
events.list matches the app's current declarations afterwards.
- A
postEvent with an undeclared name still reaches appduct_events; in development the SDK logs a warning naming the event.
- An app built with an SDK that has no
registerEvent connects and events.list returns an empty registry, no error.
- Swift and Kotlin expose
registerEvent with the same fields, and the conformance fixtures cover the snapshot and delta frames.
- The shipped skill and
docs/TOOLS.md describe declaring and listing events.
Alternatives considered
- Declare events on the tool that emits them (
registerTool({ emits })): an event posted outside any tool has nowhere to be declared.
- A static list passed to
connect(): cannot change while the app runs, does not fit code-split apps.
- Names seen so far in the retention buffer as the catalog: misses anything not fired yet and carries no description or schema.
- Allowlist like tools: breaks every existing
postEvent caller until it declares.
Slices
Work in order. #126 and #127 can run in parallel.
Builds on #93 (agents see app events only) and #94 (
nameglob filter on every surface).Why
Tools are discoverable: an app registers each one with a description and schema, and an agent reads them with
appduct toolsorappduct_list_toolsbefore calling anything. Events are not. An app posts them withpostEvent(name, payload)and nothing tells the daemon which names exist or what their payloads look like. So an agent that wants to wait for an outcome has to guess the name forappduct_wait_for_event, read the app's source, or drain the buffer hoping the event already fired.docs/TOOLS.mdtells app authors topostEvent("checkout_completed", { orderId }), yet the daemon cannot tell an agent that name exists.Expected outcome
An app declares the events it posts, and agents list them the way they list tools.
App side.
registerEvent({ name, description, payloadSchema? })in React Native, Swift and Kotlin, returning a disposer, with the same lifecycle asregisterTool: declared at any time while the app runs, removed on dispose, re-sent in full after a resume.payloadSchemafollows the tool schema rules (Standard Schema or raw JSON Schema; the daemon never inspects it). Declaration is advisory: an undeclaredpostEventstill flows to agents. In development the SDK warns when a posted name is undeclared or its payload fails the declared schema; production posts as-is.Wire and daemon. An event registry per session beside the tool registry, fed by an
event_registry_snapshot/event_registry_deltapair with the same strict-validation-or-close rule as the tool registry, and anevents.listRPC with the usual selector semantics. Registry changes emit an internal kind that goes to the log from #93, not to agents.Agent surface. One new MCP tool,
appduct_list_events({ selector?, name?, limit?, offset? }), returning one signature per declared event (checkout_completed { orderId: string }plus its description) withtotal.nametakes #94's glob; an exactnamethat matches one declared event returns its full payload schema as well. CLI:appduct events --list [--name <glob>], same output as one signature line per event, or the full schema for an exact name. The shipped skill tells agents to list events before waiting on one, and the writing-tools reference tells app authors to declare each event they post.Constraints and non-goals
postEventtoday keeps working unchanged. An SDK from before this change connects and works with no event registry.How we know it is done
registerEvent({ name: "checkout_completed", description: "...", payloadSchema }),events.listreturns that event with its description and schema; after the disposer runs, it is gone.appduct_list_eventsreturns signature lines withtotal; withname: "cart.*"only matching events; with an exact name matching one event, that event's full payload schema.appduct events --listprints one signature line per declared event,--name <glob>narrows it, and an exact name prints the full schema;--jsoncarries the same data.events.listmatches the app's current declarations afterwards.postEventwith an undeclared name still reachesappduct_events; in development the SDK logs a warning naming the event.registerEventconnects andevents.listreturns an empty registry, no error.registerEventwith the same fields, and the conformance fixtures cover the snapshot and delta frames.docs/TOOLS.mddescribe declaring and listing events.Alternatives considered
registerTool({ emits })): an event posted outside any tool has nowhere to be declared.connect(): cannot change while the app runs, does not fit code-split apps.postEventcaller until it declares.Slices
Work in order. #126 and #127 can run in parallel.
appduct events lsappduct_list_eventsMCP toolregisterEventto the Swift SDKregisterEventto the Kotlin SDKregisterEventto the React Native SDK