C5 follow-up: derive RpcMethod from zod enum (single source of truth)

The C4→C5 drift bug (hotfix 12556de) was caused by the same RPC
method list being declared twice — once as a zod enum in
worker/entry/types.ts for runtime validation, and once as a TS
union type in runtime-bridge.ts for compile-time handler wiring.
The two drifted when C4 added `invokeInternalTool`: the TS union
grew, the handler switch added a case, but the zod enum was
forgotten, so the runtime gate rejected envelopes the handler had
already registered.

This commit collapses the two declarations into one:

- Extract `workerRpcMethodSchema = z.enum([...])` in
  worker/entry/types.ts as the canonical list.
- Export `WorkerRpcMethod = z.infer<typeof workerRpcMethodSchema>`.
- Replace the hand-written `RpcMethod` union in runtime-bridge.ts
  with `type RpcMethod = WorkerRpcMethod` plus an explicit import.
- Reuse the schema inside the `rpc-request` variant of
  `workerMessageSchema` so adding a method requires exactly one
  edit instead of three.
- Add an exhaustiveness `default: { const _exhaustive: never = ... }`
  guard to `handleRpc`'s switch. If the zod enum widens without a
  matching case, TypeScript fails the build at the `never`
  assignment — drift becomes a compile-time error, not a runtime
  ZodError fired at the user.

typecheck clean, 529 unit tests green, 8/8 eval green.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 14:52:08 -04:00
1 parent 12556deb05
commit 43337e535b
2 files changed
+42 -15

No files matched your search

+23 -8
View File
@@ -8,6 +8,28 @@ import type { RuntimeBridge, RuntimeMessageInjector } from "../../agent-runtime"
const runStatusSchema = z.enum(["queued", "running", "succeeded", "failed", "cancelled", "timed_out", "interrupted"]);
/**
* Single source of truth for worker → main RPC method names.
*
* This constant is consumed by both the zod validation schema in this
* file (the runtime gate at `handleWorkerMessage`) and the `RpcMethod`
* TypeScript union exported from `agent-runtime/runtime-bridge.ts` via
* `z.infer`. Adding a new internal RPC requires touching exactly one
* place — this literal list — which eliminates the C4→C5 hotfix class
* of bug where the two definitions silently drifted and the zod gate
* rejected a newly-minted method the handler had already registered.
*/
export const workerRpcMethodSchema = z.enum([
"persistMessage",
"prepareRun",
"beforeToolCall",
"afterToolCall",
"transformContext",
"invokeInternalTool",
]);
export type WorkerRpcMethod = z.infer<typeof workerRpcMethodSchema>;
export const workerMessageSchema = z.union([
z.object({
type: z.literal("run-log"),
@@ -47,14 +69,7 @@ export const workerMessageSchema = z.union([
type: z.literal("rpc-request"),
rpcId: z.string(),
runId: z.string().uuid(),
method: z.enum([
"persistMessage",
"prepareRun",
"beforeToolCall",
"afterToolCall",
"transformContext",
"invokeInternalTool",
]),
method: workerRpcMethodSchema,
payload: z.unknown(),
}),
z.object({