refactor(v2): bind op definitions to models and require zod payload schemas#1593
Conversation
Colocate persisted/transient op payload shapes with their op definitions via interface-merge into #/wire/types (PersistedOpMap / TransientOpMap), replacing the per-service WireRecordMap augmentations. defineOp apply handlers now infer payloads from the map, and taskService restores delivered-notification marks by iterating getRecords() on restore instead of a wireRecord restore hook.
…chemas - ModelDef.defineOp(type, opts) replaces defineOp(model, type, opts); the standalone function stays as the internal primitive - schema (zod, before apply) is required and is the payload type's single source of truth; apply/toEvent payload and return types are inferred, so per-op payload interfaces are deleted and consumers use PayloadOf<typeof op> - Op registries (PersistedOpMap/TransientOpMap) now map op types to typeof the op; classification stays key-level to avoid type cycles - Every op is registered: 12 transient ops carry persist: false, 32 persisted ops ride the wire log
🦋 Changeset detectedLatest commit: 4821912 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29a298a09f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| /** | ||
| * Zod schema for the payload — the payload type's single source of truth | ||
| * (`P` is inferred from it). Stored on the descriptor so wire boundaries | ||
| * (replay of `wire.jsonl`, record export) can validate payloads against the | ||
| * Op's declared shape. Not consulted by `dispatch` / `replay` themselves. |
There was a problem hiding this comment.
Move new inline comments to the file header
packages/agent-core-v2/AGENTS.md requires comments to live only in the top-of-file /** */ block and says never to place them beside functions, methods, or statements. This new property-level JSDoc adds inline API documentation inside OpDescriptor; please fold this detail into the module header or remove it so new v2 code follows the documented comment convention.
Useful? React with 👍 / 👎.
Related Issue
None — the problem is explained below.
Problem
In the experimental v2 engine (
packages/agent-core-v2, behind a flag), every wire op's payload was declared twice: once as a hand-written TypeScript interface and again in thePersistedOpMap/TransientOpMapregistries, so the two could drift. Op definitions were also detached from their models (defineOp(SomeModel, ...)as a free function), and roughly a third of all ops were missing from the registries entirely, leaving replay classification implicit.What changed
SomeModel.defineOp('type', { ... })(the standalonedefineOpremains as the internal primitive). Payload and return types ofapply/toEventare inferred, so per-op payload interfaces were deleted; the few consumers that named them now usePayloadOf<typeof op>.schema(zod, declared beforeapply) is now the payload type's single source of truth and is stored on the op descriptor for future wire-boundary validation;dispatch/replaydo not consult it. Complex domain types (ContextMessage,LoopRecordedEvent, etc.) usez.custom<T>()rather than being restated.typeofthe op, and payload lookup (OpPayload/ModelReducers) recovers the payload from the op's own type. Registry classification is key-level only, which keeps the whole graph free of type cycles.PersistedOpMap, 12 transient ops (all withpersist: false) inTransientOpMap— the map membership now enforces the persistence policy at the type level.Verified:
tsc --noEmitclean,lint:domainOK, oxlint 0 errors, full suite 3140/3140 passing. The type-level guarantees (payload inference, persistence constraints, literal-type guards, registry recovery) are covered by the compile-time assertions intest/lint/op-uniqueness.test.ts.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update. (Internal experimental engine; no user-facing docs affected.)