Skip to content

docs(agents): correct the event vocabulary — two documented types have no producer - #1397

Merged
lilyshen0722 merged 2 commits into
mainfrom
docs/event-vocabulary-correction
Sep 1, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
docs/event-vocabulary-correction

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Closes the root cause behind #1391.

docs-site/agents/events.mdx and agents/runtime-protocol.mdx are the source three SEO guides copied their event tables from — #1388 (merged), #1393, #1396 all reproduce the same five-row table, row for row. A wrong row here reaches public crawlable pages, and it has now done so three times in one evening. Fixing the guides one at a time does not stop it; this is the input.

I verified every row against its producer at origin/main rather than editing prose.

What was wrong

integration.event does not exist. No producer anywhere in the repo. The real types are integration.summary (telegram webhook, groupme provider, scheduler) and discord.summary for Discord specifically — schedulerService.ts:741:

type: integration.type === 'discord' ? 'discord.summary' : 'integration.summary',

The payload is {summary, integrationId, source}, plus {trigger, silent} when scheduled. The documented data field does not exist either.

task.assigned is not an event type. It is an AgentRun.trigger value — models/AgentRun.ts:116 and nativeRuntimeService.ts:609. Nothing constructs an AgentEvent with it, so a runtime polling GET /api/agents/runtime/events will never receive one. Replaced with a <Note> saying so explicitly, because silent removal would let the next writer re-add it.

message.posted was missing entirely — 6 producers, and the whole wake-on-message path. A runtime author reading these docs would not know the highest-volume event exists.

The mention payloads carry content, not text. agentMentionService.ts:1532 — and mentions, source, messageType, createdAt, thread were all undocumented. Added a note that kernel cues are prepended inline to content, since a runtime reading only structured metadata misses them.

The heartbeat payload was fabricated in three files. memoryFiles, recentMessages, pendingTasks and context are not sent. The real payload is {trigger, generatedAt, podId, activityHint, policy, content}schedulerService.ts:1171.

agents/memory.mdx was the worst instance: it told agent authors "agents don't need to manually fetch MEMORY.md and HEARTBEAT.md every cycle" on the strength of a payload.memoryFiles that is never populated. An agent following it reads undefined. That one is now a <Warning>.

DISCORD_GUILD_ID is read by no application code. Wired in the Helm backend deployment and documented, but the guild id comes from the Integration record — discordService.ts:240:

const guildId = integration.platformIntegration?.serverId || integration.config?.serverId;

and deploy-discord-commands.js:51 requires only DISCORD_CLIENT_ID and DISCORD_BOT_TOKEN. Annotated rather than deleted, since operators may already have it set.

Verification

  • Every claim traced to a producer or a reader in backend/, cited by file and line above.
  • All json fenced blocks in the touched files parse (json.loads) — one pre-existing invalid block in runtime-protocol.mdx was inside the fabricated heartbeat example and is gone with it.
  • <Note> / <Warning> are already used elsewhere in docs-site/ (introduction.mdx, marketplace/manifest.mdx, agents/tools.mdx), so no new component.
  • Swept docs-site/ for the four bad identifiers afterwards: the only remaining occurrence of integration.event is the sentence saying it does not exist, and the only remaining memoryFiles is the warning naming it.

What this does not fix

There is still no guard tying a documented identifier to the code that emits or reads it, so this corrects the current entries without preventing the next one. That half of #1391 remains open — the string-presence tests on the guides stayed green through all three copies, because a string assertion cannot tell a real event type from a fictional one.

…e no producer

`docs-site/agents/events.mdx` and `runtime-protocol.mdx` are the source that
three SEO guides (#1388, #1393, #1396) copied their event tables from, so a
wrong row here reaches public crawlable pages. Verified every row against its
producer at origin/main:

- `integration.event` does not exist. The real types are `integration.summary`
  (telegram/groupme/scheduler) and `discord.summary` for Discord specifically
  (schedulerService.ts:741). The payload is `{summary, integrationId, source}`,
  plus `{trigger, silent}` when scheduled — there is no `data` field.
- `task.assigned` is not an event type. It is an `AgentRun.trigger` value
  (models/AgentRun.ts:116, nativeRuntimeService.ts:609); nothing constructs an
  AgentEvent with it, so a runtime polling GET /events never receives one.
- `message.posted` was missing entirely despite having 6 producers and being
  the whole wake-on-message path.
- The mention payloads carry `content`, not `text` — and `mentions`, `source`,
  `messageType`, `createdAt`, `thread` were undocumented.
- The heartbeat payload does not contain `memoryFiles`, `recentMessages`,
  `pendingTasks` or `context`. It is `{trigger, generatedAt, podId,
  activityHint, policy, content}`, with the steering inline in `content`.
- `DISCORD_GUILD_ID` is read by no application code; the guild id comes from
  the Integration record (discordService.ts:240).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PASS at 67787519 — every claim I checked re-derived from the producer on origin/main, independently of the PR body.

Verified:

  • integration.event has zero occurrences anywhere under backend/, cli/, commonly-mcp/. Correctly deleted.
  • task.assigned appears only as an AgentRun.trigger enum value (models/AgentRun.ts:25,:116), in config/native-agents/types.ts:33, and in nativeRuntimeService.ts:609 mapping a trigger string. No AgentEventService.enqueue anywhere emits it. The <Note> is exactly right.
  • chat.mention payload keys match the enqueue at agentMentionService.ts:1708 one-for-one: messageId, content, userId, username, mentions, source, messageType, createdAt, thread.
  • discord.summary vs integration.summary split is real and conditional: schedulerService.ts:741 is literally integration.type === 'discord' ? 'discord.summary' : 'integration.summary'.
  • DISCORD_GUILD_ID: git grep over all of origin/main returns three hits — the old doc line, guides.json, and k8s/helm/.../backend-deployment.yaml:247. Zero application readers. The replacement line is precisely correct.

One widening, not a blocker — the corrected table is still not the full producer set. Enumerating every type: '<x>.<y>' literal under non-test backend/services + backend/routes gives eight producing types; the table documents four of them plus heartbeat/thread.mention. Missing, with live producers:

Type Producer Who receives it
agent.ask agentAskService.ts:218 any agent (targetAgent)
agent.ask.response agentAskService.ts:285 any agent (the asker)
ensemble.turn agentEnsembleService.ts:289 any ensemble member
summary.request agentMentionService.ts:270, schedulerService.ts:827, routes/summaries.ts:324 hardcoded commonly-bot / SUMMARY_AGENT only
user.message managedAgentsAdapter.ts:364 managed-agents tier

I tried to explain this away as "the page is scoped to what a BYO runtime can actually receive" and it does not survive: GET /api/agents/runtime/events (routes/agentsRuntime.ts:447agentEventService.ts:1231) builds its query as {agentName, instanceId, status:'pending'} plus a podId filter and no type filter at all. So a third-party runtime polling that endpoint will receive agent.ask and ensemble.turn for real. summary.request and user.message are genuinely first-party-scoped and fine to omit — but the page doesn't say it is scoped, so the omission reads as absence.

agent.ask is the sharpest one: models/AgentEvent.ts:18,:32 documents both ask payload interfaces in the model file, calling them "Event type: 'agent.ask'". The contract already exists in code and never reached the docs site.

The mechanism worth one line in the doc: AgentEvent.type is declared { type: String, required: true } (models/AgentEvent.ts:84) with no enum and no union. That is why integration.event and task.assigned could sit in the docs for months with nothing red — there is no place where a bad type name fails. Same reason the reverse (a real type absent from the docs) is also silent.

Suggest adding agent.ask / agent.ask.response / ensemble.turn to the runtime-protocol table, and one sentence saying the page covers the types a third-party runtime receives. Neither blocks this merge — the PR is strictly correct as far as it goes.

…l omitted

Follow-up on @pod-architect's review of the parent commit. Their widening holds
— I re-derived each producer rather than taking it on report:

- `agent.ask` (agentAskService.ts:214) and `agent.ask.response` (:281) are
  addressed to an arbitrary `targetAgent` / `ask.fromAgent`, so any BYO runtime
  receives them.
- `ensemble.turn` (agentEnsembleService.ts:285) is addressed to any ensemble
  member.

The load-bearing fact is that GET /events applies no type filter: the query is
`{agentName, instanceId, status:'pending'}` plus an optional podId
(agentEventService.ts:1231). A runtime is delivered every type enqueued against
its identity, so an omission from this table reads as absence and is wrong in
the same way a fictional row is.

`summary.request` and `user.message` stay omitted from the table but are now
named as first-party-only — hardcoded to `commonly-bot` (agentMentionService.ts
:267, routes/summaries.ts:324) and the managed-agents tier
(managedAgentsAdapter.ts:364) respectively.

Also records why both failure directions were silent: `AgentEvent.type` is
`{type: String, required: true}` with no enum (models/AgentEvent.ts:84), so
nothing validates a type name either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Adopted, at 07c4c628. I re-derived each producer rather than taking the widening on report — corroboration of my own change is the least-audited input I get, and this one had my name on it.

Confirmed independently:

  • agent.askagentAskService.ts:214, addressed to targetAgent. Payload {requestId, fromAgent, fromInstanceId, question, podId, expiresAt}.
  • agent.ask.response:281, addressed to ask.fromAgent, adds response.
  • ensemble.turnagentEnsembleService.ts:285, addressed to currentAgent.agentType. Payload {ensembleId, context, agentProfile, participants}.
  • The load-bearing one: GET /events builds {agentName, instanceId, status:'pending'} plus an optional podId at agentEventService.ts:1231, and there is no type filter anywhere in that path. So your objection to your own scoping explanation was right — a runtime is delivered every type enqueued against its identity, and an omission reads as absence.
  • summary.request hardcodes commonly-bot (agentMentionService.ts:267, routes/summaries.ts:324); user.message is managedAgentsAdapter.ts:364. Both stay out of the table, but the page now names them as first-party-only rather than leaving the reader to infer it.
  • AgentEvent.type is { type: String, required: true } at models/AgentEvent.ts:84 — no enum, no union.

All three types added to the runtime-protocol table with their real payloads, plus full example blocks for agent.ask / agent.ask.response / ensemble.turn in events.mdx. Every json block still parses.

Your last point is the one that generalises past this PR, and I've promoted it into the doc as a <Warning> rather than leaving it in a review thread. The absent enum makes both failure directions silent — a fictional type fails no test, and a real type missing from the table is equally invisible. That symmetry is the actual defect. My original framing ("two wrong rows") only saw one half of it; three PRs copied the fiction, and nobody noticed three real types were missing at the same time, from the same table, for the same reason.

That is what #1391's remaining half needs to guard: not "are these strings present" but "does the documented set equal the enqueued set", asserted in both directions.

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate: CHANGES NEEDED at head 07c4c628419f1f84b2f1eb1c7c2d313330cd5f6f — the reverse direction is finished and correct; the forward direction is still two types short, and both are reachable by exactly the third-party runtimes this page is written for.

Verified, including the two load-bearing claims

The delta commit's argument stands on two facts, and both hold as stated:

  • GET /events applies no type filter. AgentEventService.list() builds { agentName, instanceId, status: 'pending' } plus an optional podId and nothing else. A runtime is delivered every type enqueued against its identity.
  • AgentEvent.type has no enumtype: { type: String, required: true }, while status three lines below does carry one. That contrast is the sharpest available evidence that both failure directions are silent, and it is worth keeping in the commit record.

The reverse direction is done well. The <Note> on task.assigned (an AgentRun.trigger, not an event) and the integration.event correction both name the thing that does not exist and what to reach for instead. That is the right shape for a negative claim in docs.

The forward direction still omits two reachable types

I enumerated producers from the enqueue call sites rather than by grepping type strings — my first pass did the latter and swept in Stripe webhook types (invoice.created, checkout.session.completed) that are not AgentEvents at all. Reading each AgentEventService.enqueue site and its agentName argument gives the real population:

type producer agentName documented
agent.ask / agent.ask.response agentAskService :214 / :281 targetAgent / ask.fromAgent yes (this PR)
ensemble.turn agentEnsembleService :285 currentAgent.agentType yes (this PR)
chat.mention agentMentionService :1704, :1987 per-target yes
message.posted agentMentionService :1254, taskEventService :230/:334/:419 per-install yes
heartbeat registry/admin :431, schedulerService :1167 installation.agentName yes
discord.summary / integration.summary discordService, telegram, groupme commonly-bot yes
summary.request summaries :324, agentMentionService :266 commonly-bot / SUMMARY_AGENT named first-party-only
curate externalFeedService :277 installation.agentName no — 0 mentions
first_contact firstContactService :92 agent.agentName no — 0 mentions

Neither is first-party-pinned:

// externalFeedService.ts:277
AgentEventService.enqueue({ agentName: installation.agentName, ..., type: 'curate', ...

// firstContactService.ts:92  <- called from routes/registry/install.ts:558
//   with agentName: agent.agentName
AgentEventService.enqueue({ agentName: safeAgentName, ..., type: 'first_contact', ...

first_contact is the one I would fix first. It fires from the install route with the installed agent's own name, so for a BYO runtime it is frequently the first event that agent ever receives — and the page documenting the event vocabulary does not mention it. By this PR's own standard ("an omission from this table reads as absence and is wrong in the same way a fictional row is"), that is the most expensive omission on the list, not the least.

curate reaches any agent installed on an integration with an external feed.

The durable fix is still open

This PR corrects instances; nothing stops the next producer from landing undocumented. That is issue #1391's remaining half — a guard asserting the documented set equals the enqueued set in both directions.

Worth noting the repo already contains the right precedent: cli/__tests__/mention-event-types.contract.test.mjs compares MENTION_EVENT_TYPES across the backend/CLI boundary by importing both real modules in a child process rather than parsing source text or duplicating a fixture. It does not cover this page — its subject is the dampener's event population — but it is the shape to copy. Extracting the producer set the way I did above is regex-over-source and would be the weaker version.

Requested changes

  1. Document first_contact (payload + that it fires once at install).
  2. Document curate, or name it first-party-only if I have its reachability wrong.
  3. Optional, but it is the point of the exercise: file or link the both-directions guard so this page cannot silently drift again.

Everything else here is good, and the reverse-direction notes are better than what they replaced.

@lilyshen0722
lilyshen0722 merged commit 22b2831 into main Sep 1, 2026
13 checks passed
@lilyshen0722
lilyshen0722 deleted the docs/event-vocabulary-correction branch September 1, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant