Skip to content

Commit 202ec68

Browse files
fix(events): freeze the exported payload tables through and mark the required canonical.payload a minor bump (review)
1 parent 39719a7 commit 202ec68

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. */
@@ -103,7 +104,7 @@ export type AgentEventPayloadFieldKind =
103104
| 'string-array'
104105
| 'trigger';
105106

106-
export const agentEventPayloadFieldKinds = Object.freeze({
107+
export const agentEventPayloadFieldKinds = deepFreeze({
107108
agentId: 'string',
108109
agentTranscriptPath: 'nullable-string',
109110
agentType: 'string',
@@ -153,8 +154,10 @@ const modelSwitchFields = [...sessionFields, 'fromModel', 'toModel', 'requestedM
153154
* The canonical payload fields of every event-route family, in the order the
154155
* payload object carries them. This is the one per-family table; the types
155156
* ({@link AgentEventPayload}) and the runtime projection derive from it.
157+
* Frozen through (the family arrays share instances), so a consumer holding
158+
* the export cannot change what later invocations project.
156159
*/
157-
export const agentEventPayloadFields = Object.freeze({
160+
export const agentEventPayloadFields = deepFreeze({
158161
'agent/idle': [...sessionFields, 'teammateName', 'teamName'],
159162
'agent/start': threeHostFields,
160163
'agent/stop': [...threeHostFields, 'agentTranscriptPath', 'reentry', 'lastAssistantMessage'],
@@ -301,7 +304,7 @@ const cursorTool = [...cursorSession, 'cwd', 'toolName', 'toolInput', 'toolUseId
301304
*/
302305
export const agentEventPayloadNativeKeys: Readonly<
303306
Record<AgentEventPayloadHost, Readonly<Partial<Record<CanonicalAgentEvent, NativeKeyTable>>>>
304-
> = Object.freeze({
307+
> = deepFreeze({
305308
claude: Object.freeze({
306309
'agent/idle': pick(standardKeys, [...claudeSession, 'teammateName', 'teamName']),
307310
'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
@@ -302,3 +302,17 @@ it('admits a family field only when at least two supporting hosts report it, or
302302
expect(used.has(field as AgentEventPayloadFieldName), `${field} belongs to a family`).toBe(true);
303303
}
304304
});
305+
306+
it('exports the tables frozen through, so a consumer cannot change what later invocations project', () => {
307+
for (const fields of Object.values(agentEventPayloadFields)) {
308+
expect(Object.isFrozen(fields)).toBe(true);
309+
}
310+
for (const host of hosts) {
311+
for (const mapping of Object.values(agentEventPayloadNativeKeys[host])) {
312+
expect(Object.isFrozen(mapping)).toBe(true);
313+
for (const entry of Object.values(mapping)) expect(Object.isFrozen(entry)).toBe(true);
314+
}
315+
}
316+
expect(() => (agentEventPayloadFields['tool/before'] as unknown as string[]).push('reentry')).toThrow(TypeError);
317+
expect(Object.isFrozen(agentEventPayloadFieldKinds)).toBe(true);
318+
});

0 commit comments

Comments
 (0)