docs(agents): correct the event vocabulary — two documented types have no producer - #1397
Conversation
…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
left a comment
There was a problem hiding this comment.
PASS at 67787519 — every claim I checked re-derived from the producer on origin/main, independently of the PR body.
Verified:
integration.eventhas zero occurrences anywhere underbackend/,cli/,commonly-mcp/. Correctly deleted.task.assignedappears only as anAgentRun.triggerenum value (models/AgentRun.ts:25,:116), inconfig/native-agents/types.ts:33, and innativeRuntimeService.ts:609mapping a trigger string. NoAgentEventService.enqueueanywhere emits it. The<Note>is exactly right.chat.mentionpayload keys match the enqueue atagentMentionService.ts:1708one-for-one:messageId, content, userId, username, mentions, source, messageType, createdAt, thread.discord.summaryvsintegration.summarysplit is real and conditional:schedulerService.ts:741is literallyintegration.type === 'discord' ? 'discord.summary' : 'integration.summary'.DISCORD_GUILD_ID:git grepover all oforigin/mainreturns three hits — the old doc line,guides.json, andk8s/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:447 → agentEventService.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>
|
Adopted, at Confirmed independently:
All three types added to the runtime-protocol table with their real payloads, plus full example blocks for Your last point is the one that generalises past this PR, and I've promoted it into the doc as a 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
left a comment
There was a problem hiding this comment.
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 /eventsapplies no type filter.AgentEventService.list()builds{ agentName, instanceId, status: 'pending' }plus an optionalpodIdand nothing else. A runtime is delivered every type enqueued against its identity.AgentEvent.typehas no enum —type: { type: String, required: true }, whilestatusthree 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
- Document
first_contact(payload + that it fires once at install). - Document
curate, or name it first-party-only if I have its reachability wrong. - 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.
Closes the root cause behind #1391.
docs-site/agents/events.mdxandagents/runtime-protocol.mdxare 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/mainrather than editing prose.What was wrong
integration.eventdoes not exist. No producer anywhere in the repo. The real types areintegration.summary(telegram webhook, groupme provider, scheduler) anddiscord.summaryfor Discord specifically —schedulerService.ts:741:The payload is
{summary, integrationId, source}, plus{trigger, silent}when scheduled. The documenteddatafield does not exist either.task.assignedis not an event type. It is anAgentRun.triggervalue —models/AgentRun.ts:116andnativeRuntimeService.ts:609. Nothing constructs anAgentEventwith it, so a runtime pollingGET /api/agents/runtime/eventswill never receive one. Replaced with a<Note>saying so explicitly, because silent removal would let the next writer re-add it.message.postedwas 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, nottext.agentMentionService.ts:1532— andmentions,source,messageType,createdAt,threadwere all undocumented. Added a note that kernel cues are prepended inline tocontent, since a runtime reading only structured metadata misses them.The heartbeat payload was fabricated in three files.
memoryFiles,recentMessages,pendingTasksandcontextare not sent. The real payload is{trigger, generatedAt, podId, activityHint, policy, content}—schedulerService.ts:1171.agents/memory.mdxwas 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 apayload.memoryFilesthat is never populated. An agent following it readsundefined. That one is now a<Warning>.DISCORD_GUILD_IDis 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:and
deploy-discord-commands.js:51requires onlyDISCORD_CLIENT_IDandDISCORD_BOT_TOKEN. Annotated rather than deleted, since operators may already have it set.Verification
backend/, cited by file and line above.jsonfenced blocks in the touched files parse (json.loads) — one pre-existing invalid block inruntime-protocol.mdxwas inside the fabricated heartbeat example and is gone with it.<Note>/<Warning>are already used elsewhere indocs-site/(introduction.mdx,marketplace/manifest.mdx,agents/tools.mdx), so no new component.docs-site/for the four bad identifiers afterwards: the only remaining occurrence ofintegration.eventis the sentence saying it does not exist, and the only remainingmemoryFilesis 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.