Skip to content

Let apps declare the events they post, and let agents list them #95

Description

@V3RON

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

  1. After registerEvent({ name: "checkout_completed", description: "...", payloadSchema }), events.list returns that event with its description and schema; after the disposer runs, it is gone.
  2. 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.
  3. 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.
  4. A session that resumes re-sends its event registry, and events.list matches the app's current declarations afterwards.
  5. A postEvent with an undeclared name still reaches appduct_events; in development the SDK logs a warning naming the event.
  6. An app built with an SDK that has no registerEvent connects and events.list returns an empty registry, no error.
  7. Swift and Kotlin expose registerEvent with the same fields, and the conformance fixtures cover the snapshot and delta frames.
  8. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpMCP server and built-in toolsstatus:readySpec and fix direction are clear; an agent can pick it uptype:featureSomething Appduct should do that it does not today

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions