Skip to content

Commit 51b603b

Browse files
fix(events): freeze the exported payload tables through and mark the required canonical.payload a minor bump (review)
1 parent 6361fd7 commit 51b603b

3 files changed

Lines changed: 22 additions & 5 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
"agent-bundle": patch
2+
"agent-bundle": minor
33
---
44

5-
Give every event route a canonical, per-family `canonical.payload` beside the raw `native` envelope (`AgentEventRouteProps<E>`): the fields at least two hosts report — `toolName`, `toolInput`, `toolUseId`, `toolResponse`, `sessionId` (Claude and Codex `session_id`, Cursor `conversation_id`), `transcriptPath`, `cwd`, `model`, `permissionMode`, `agentId`/`agentType`, `agentTranscriptPath`, `prompt`, `reason`, `source`, `trigger`, `error`/`isInterrupt`, `lastAssistantMessage`, and `reentry` (Claude and Codex `stop_hook_active`, Cursor `loop_count > 0`) — each delivered as `{ value, nativeKey }` naming the host key it was read from, and absent when the host did not send it, never fabricated. Cursor's `tool_output` JSON string is parsed into `toolResponse` (kept as the string when it is not valid JSON). Type a route to its family (`AgentEventRouteProps<'tool/after'>`) and `payload` narrows to that family's fields, in the route and in the generated `.agent-bundle/routes.d.ts` that `renderRoute` reads; `AgentEventCanonicalIdentity<E>` gains the same parameter. The per-family table ships as `agentEventPayloadFields`, the per-host key table as `agentEventPayloadNativeKeys` (with `AgentEventPayload`, `AgentEventPayloadField`, `AgentEventPayloadFieldName`, `AgentEventPayloadNativeKey`, and `agentEventPayloadFieldKinds`), and each pinned capability table mirrors its host's mapping under `hooks.eventRoutes.<event>.payload`, so the generated events reference documents field × host → native key per family. `agent-bundle/test` gains `createEventRouteInput(event, native, { host })`, which validates a host envelope and builds the `{ canonical, native }` input the harness takes, payload included; the Workbench Lifecycles view lists the mapped payload beside the canonical identity. Additive: `native` is unchanged, `idempotencyKey` still hashes only the envelope, and the bare `AgentEventRouteProps` keeps working with every field optional; only code that constructs `AgentEventCanonicalIdentity` by hand must add `payload`. (#466)
5+
Give every event route a canonical, per-family `canonical.payload` beside the raw `native` envelope (`AgentEventRouteProps<E>`): the fields at least two hosts report — `toolName`, `toolInput`, `toolUseId`, `toolResponse`, `sessionId` (Claude and Codex `session_id`, Cursor `conversation_id`), `transcriptPath`, `cwd`, `model`, `permissionMode`, `agentId`/`agentType`, `agentTranscriptPath`, `prompt`, `reason`, `source`, `trigger`, `error`/`isInterrupt`, `lastAssistantMessage`, and `reentry` (Claude and Codex `stop_hook_active`, Cursor `loop_count > 0`) — each delivered as `{ value, nativeKey }` naming the host key it was read from, and absent when the host did not send it, never fabricated. Cursor's `tool_output` JSON string is parsed into `toolResponse` (kept as the string when it is not valid JSON). Type a route to its family (`AgentEventRouteProps<'tool/after'>`) and `payload` narrows to that family's fields, in the route and in the generated `.agent-bundle/routes.d.ts` that `renderRoute` reads; `AgentEventCanonicalIdentity<E>` gains the same parameter. The per-family table ships as `agentEventPayloadFields`, the per-host key table as `agentEventPayloadNativeKeys` (with `AgentEventPayload`, `AgentEventPayloadField`, `AgentEventPayloadFieldName`, `AgentEventPayloadNativeKey`, and `agentEventPayloadFieldKinds`), and each pinned capability table mirrors its host's mapping under `hooks.eventRoutes.<event>.payload`, so the generated events reference documents field × host → native key per family. `agent-bundle/test` gains `createEventRouteInput(event, native, { host })`, which validates a host envelope and builds the `{ canonical, native }` input the harness takes, payload included; the Workbench Lifecycles view lists the mapped payload beside the canonical identity. `native` is unchanged, `idempotencyKey` still hashes only the envelope, and a route typed with the bare `AgentEventRouteProps` keeps working with every field optional. Breaking for one shape of consumer code: `payload` is a required property of `AgentEventCanonicalIdentity`, so a test or harness that constructs the identity by hand no longer compiles until it adds one — build the input with `createEventRouteInput` instead. (#466)

‎packages/agent-bundle/src/routes/events.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { deepFreeze } from '../core/freeze.ts';
12
import type { JsonValue } from '../core/strict-json.ts';
23

34
/** The event-route families admitted by the recorded #97 v1/G10 decision. */
@@ -95,7 +96,7 @@ export type AgentEventPayloadFieldKind =
9596
| 'string-array'
9697
| 'trigger';
9798

98-
export const agentEventPayloadFieldKinds = Object.freeze({
99+
export const agentEventPayloadFieldKinds = deepFreeze({
99100
agentId: 'string',
100101
agentTranscriptPath: 'nullable-string',
101102
agentType: 'string',
@@ -139,8 +140,10 @@ const taskFields = [...sessionFields, 'taskId', 'taskSubject', 'taskDescription'
139140
* The canonical payload fields of every event-route family, in the order the
140141
* payload object carries them. This is the one per-family table; the types
141142
* ({@link AgentEventPayload}) and the runtime projection derive from it.
143+
* Frozen through (the family arrays share instances), so a consumer holding
144+
* the export cannot change what later invocations project.
142145
*/
143-
export const agentEventPayloadFields = Object.freeze({
146+
export const agentEventPayloadFields = deepFreeze({
144147
'agent/idle': [...sessionFields, 'teammateName', 'teamName'],
145148
'agent/start': threeHostFields,
146149
'agent/stop': [...threeHostFields, 'agentTranscriptPath', 'reentry', 'lastAssistantMessage'],
@@ -282,7 +285,7 @@ const cursorTool = [...cursorSession, 'cwd', 'toolName', 'toolInput', 'toolUseId
282285
*/
283286
export const agentEventPayloadNativeKeys: Readonly<
284287
Record<AgentEventPayloadHost, Readonly<Partial<Record<CanonicalAgentEvent, NativeKeyTable>>>>
285-
> = Object.freeze({
288+
> = deepFreeze({
286289
claude: Object.freeze({
287290
'agent/idle': pick(standardKeys, [...claudeSession, 'teammateName', 'teamName']),
288291
'agent/start': pick(standardKeys, claudeSession),

‎packages/agent-bundle/tests/event-payload.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,17 @@ it('admits a family field only when at least two supporting hosts report it, or
253253
expect(used.has(field as AgentEventPayloadFieldName), `${field} belongs to a family`).toBe(true);
254254
}
255255
});
256+
257+
it('exports the tables frozen through, so a consumer cannot change what later invocations project', () => {
258+
for (const fields of Object.values(agentEventPayloadFields)) {
259+
expect(Object.isFrozen(fields)).toBe(true);
260+
}
261+
for (const host of hosts) {
262+
for (const mapping of Object.values(agentEventPayloadNativeKeys[host])) {
263+
expect(Object.isFrozen(mapping)).toBe(true);
264+
for (const entry of Object.values(mapping)) expect(Object.isFrozen(entry)).toBe(true);
265+
}
266+
}
267+
expect(() => (agentEventPayloadFields['tool/before'] as unknown as string[]).push('reentry')).toThrow(TypeError);
268+
expect(Object.isFrozen(agentEventPayloadFieldKinds)).toBe(true);
269+
});

0 commit comments

Comments
 (0)