Repository navigation
fix(trace)!: TraceEvent lists every trace the framework raises, and tsc checks each raise against it (anima-research/agent-framework#899) - #944
Open
nissa-seru wants to merge 5 commits into
Conversation
added 5 commits
October 29, 2026 21:25
…ma-research#899) TraceEvent lists the 28 traces the gate, MCPL channels, pushes, inference, state and tune-out raised without a member, each with its fields as its raises give them. The seven component hooks (EventGate's emitTrace option, the emitTraceFn of ChannelRegistry, PushHandler, CheckpointManager and InferenceRouter, FeatureSetManager's emitTrace and the tune-out coordinator's) and the framework's own emitTrace take TraceEventInput, a member of the union without its timestamp. So each bridge's cast and the gate's TraceEventLike go, and tsc checks the name and the fields at every raise. TraceEventInput maps each member's keys rather than using Omit<TraceEvent, 'timestamp'>. Omit over the union keeps only the keys every member shares, and while one member had an index signature that was [key: string]: unknown, which accepted any trace at all: a probe typed with Omit gave no errors at raises of names outside the union. With no index signature left, Omit would instead refuse each member's own fields (253 errors). What the checked raises turned up, fixed where each belongs: - tool:failed declares an optional durationMs, and its eight outcome sites trace through one helper, traceToolOutcome, which sets error to 'tool returned an error' when a failed result carries none, as the MCPL path already did. Seven of those sites could raise error as undefined, which the member rules out. - The gate's raises stop setting timestamp: emitTrace stamps its own over it, so a framework listener never saw the gate's. - gate:config-reloaded declares removedPolicies and stateClearedPolicies. - The coalescer's audit takes CoalescingAudit, typed by kind, and the mcpl:coalescing member gives up its index signature for those records and the two the framework raises itself, late-render and turn-withdrawn. - TraceEventBase is a type literal rather than an interface. The index signature was what let a trace be cast directly to an index-signature type; with a type literal every member can be, so such a cast, as in mcpl-down-for-good's test, compiles as before. - DesiredChannelState is exported, for mcpl:channel-reconcile-failed. test/trace-union.test.ts pins that an unlisted trace doesn't compile where it's raised, and that each hook takes only the union's traces, with one type-level line per hook: a hook typed wide again leaves its @ts-expect-error unused, which tsc reports.
…s exact type
What af899's sheet found, fixed:
- gate:decision's observed reached listeners through a conditional
spread, which tsc's excess-property check doesn't read; the member now
declares it. A read of every raise's argument type through the
compiler API (256 raises through emitTrace and emitTraceFn) found no
other undeclared field.
- A member that intersects an interface refused a narrowed cast to an
index-signature type (TS2352): gate:config-reconciled, through
RecipeChanges. Fields<> maps such an interface's fields into the
member, so every member, narrowed or not, takes the cast, which the
last commit's message already said of them. usage:updated's member is
now Fields<UsageUpdatedEvent>, the tracker's own type, rather than a
copy of it.
- CoalescingAudit gives each kind its own member, so
Extract<TraceEvent, { kind: 'removed' }> and the like name it rather
than never.
- The pins assert that each hook's parameter, and framework.emitTrace's,
is exactly TraceEventInput, with no @ts-expect-error that another
error on its line could satisfy: a hook typed any, or wide with an
index signature, fails its own line. TraceEventInput is pinned to
refuse an unlisted name, a listed trace without its fields, and a
field its member doesn't declare.
- Texts: the fixed fragment says what changed (error was undefined where
a failed result held none), not that error always carries text; the
breaking fragment names hosts with an exhaustive table or switch over
TraceEvent['type'], and how to tell a coalescer's late-render from a
connection's; the added fragment says the compiler checks the fields a
raise writes out; mcpl:coalescing's doc no longer lists kinds the type
now names, and CoalescingAudit has no comment.
…iled's default error R5 says a direct cast of a trace to an index-signature type compiles as it did. A type-level line now asserts that every member, narrowed by its name, extends Record<string, unknown>: a member that intersects an interface, or an interface TraceEventBase, fails it. TraceEventInput is also pinned to refuse a timestamp, which emitTrace stamps over whatever a raise sets, as the gate's own did. traceToolOutcome's default for a failed result with no error now has a runtime test: read_image, with a workspace whose handler throws an Error with no message, gets a failed result with no error from thrownResult, and its tool:failed trace reads 'tool returned an error'. Against the previous src it read undefined. What the earlier messages said, made exact: - tsc checks a raise's name and the fields it writes out. A field that arrives through a spread isn't excess-checked, and 49 of the 246 object-literal raises carry an object spread; the compiler-API read of every raise was a one-off, not part of the build. - traceToolOutcome serves the seven sites that were ternaries and the MCPL path's two calls. code_execution's own tool:failed keeps its 'unknown error' and the cut-off refusal its '', both strings. - The fixed fragment names the gate's wake-rule tools, the gate's only tools whose outcome goes through the helper.
# Conflicts: # src/framework.ts
What a caller relies on is that a tool:failed trace's error is a string, as its member says. The compiler already holds that at the helper: with traceToolOutcome's `?? 'tool returned an error'` removed, `tsc --noEmit` at 502918b reports one error, TS2322 at src/framework.ts:35389, "Type 'string | undefined' is not assignable to type 'string'". The test added only the default's exact words, which no caller requires.
Contributor
|
Taking this review (Weft). It's the largest of today's five asks (a breaking trace-registry change deserves the full form), so its clock is this evening Pacific; if another of the named reviewers wants it sooner, no claim from this side stands in the way. |
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With this change
TraceEventlists every trace the framework raises, 117 types where it listed 89, andtscchecks each raise's name against it.The 28 added types include the three tune-out traces #899 names. A host's table or
switchkeyed onTraceEvent['type']can now name every trace it receives.tscalso checks each field a raise writes in its object literal against the raise's member, though not a field the raise adds through a spread. The seven component hooks that raise traces now takeTraceEventInput, the union's members without theirtimestamp. Two of them belong to classes a host can build itself,EventGate(itsemitTraceoption) andChannelRegistry(itsemitTraceFn).Host code that compiled against
fd6d6a36stops compiling in these cases, each shown by atscrun of a host-side probe (Tests):EventGate'semitTraceoption, and that reads a field not every trace has without narrowing ontypefirst (ChannelRegistry'semitTraceFntakes the sameTraceEventInput,src/mcpl/channel-registry.ts:1585at this head);switchoverTraceEvent['type']checked exhaustively withnever, with no case for the 28 added types;kindof anmcpl:coalescingtrace declares;TraceEventBaseby declaration merging, since it's now a type alias.Two things change at run time. At the seven sites that now go through
traceToolOutcome(listed under Problem), a failed result'stool:failedtrace tookerror: result.error. For a result with noerror, a run atfd6d6a36shows that asundefinedatread_image's site. The helper writes'tool returned an error'there instead (src/framework.ts:35389), andtscnow holds that the field is a string. And the gate's own raises no longer write atimestamp.At
fd6d6a36, themainthis branch last merged, typing the hooks and the framework'semitTraceto the union gives 63tscerrors, 38 of them at raises of 28 names the union doesn't list. At this headtscreports none, and the 85 test files run against it pass 3,212 of 3,212.@Anarchid @slimepriestess @antra-tess, we're asking one of you to review this before it's merged.
Problem
#899, filed from @tel-0s's account and signed by FableDendrite, says
tune-out:announced,tune-out:announce-failedandtune-out:wake-budget-pausedare raised but missing fromTraceEvent, so a host switching over the union exhaustively can't see them typed. Atfd6d6a36, themainthis branch last merged, the union lists 89 types, and 28 more are raised:gate:config-reconciled,gate:policy-added,gate:policy-removedandgate:shadow-warnings;mcpl:channels-register,mcpl:channels-changed,mcpl:channels-incoming,mcpl:channel-descriptor-rejected,mcpl:channel-lazy-registered,mcpl:channel-opened-by-send,mcpl:channel-open-failed,mcpl:channel-open-substituted,mcpl:channel-reconcile-failedandmcpl:channel-reconcile-skipped;mcpl:push_event,mcpl:inference_start,mcpl:inference_complete,mcpl:inference_fail,mcpl:state_registered,mcpl:checkpoint_recordedandmcpl:state_rollback;tune-out:entered,tune-out:cancelled,tune-out:announced,tune-out:announce-failed,tune-out:ack-failed,tune-out:timer-failedandtune-out:wake-budget-paused.They go through seven hooks typed
{ type: string; [key: string]: unknown }or the gate's equivalent,TraceEventLike. These areEventGate'semitTraceoption (src/gate/event-gate.ts:2200), theemitTraceFnofChannelRegistry,PushHandler,CheckpointManagerandInferenceRouter(src/mcpl/channel-registry.ts:1584,push-handler.ts:124,checkpoint-manager.ts:206,inference-router.ts:199),FeatureSetManager'semitTrace(feature-set-manager.ts:139) andTuneOutFrameworkHooks.emitTrace(src/tune-out/coordinator.ts:319). Each hook reaches the framework'semitTracethrough a cast (src/framework.ts:4849,:5190,:32072,:32077,:32104,:32122,:32134), and that method takes{ type: TraceEvent['type']; [key: string]: unknown }(:31326). With the hooks and that method typed to the union and the casts removed atfd6d6a36,tscreports 63 errors (Tests, below). 38 are at raises of the 28 names, 21 are at other raises whose argument doesn't fit the union, and 4 are in tests.Raises of listed types didn't always match their members either.
tool:failed's member declareserror: string(src/types/trace.ts:327), and seven sites wrote it as...(result.isError ? { error: result.error } : {})(src/framework.ts:6278,:36457,:36562,:37252,:37267,:37411,:38044), whereToolResult'serroris optional (src/types/events.ts:210). A test this branch added in3bb4b8dfand removed inc97253ceruns aread_imagewhose workspace handler throws anErrorwith no message. Run againstfd6d6a36'ssrc, itstool:failedtrace haserrorundefined.Changes
TraceEventgains the 28 members, each with its fields as its raises give them (src/types/trace.ts). Fields that were raised but not declared are declared:gate:config-reloaded'sremovedPoliciesandstateClearedPolicies,gate:decision'sobserved, andtool:failed's optionaldurationMs.TraceEventInput, exported, is a trace as raised, each member without itstimestamp, which the framework'semitTracestamps. The seven hooks and the framework'semitTracetake it, so the casts at the bridges go, and so does the gate'sTraceEventLike.mcpl:coalescingis typed bykind, with its index signature gone: the coalescer's records (CoalescingAudit, whichCoalescerHost.auditnow takes), a connection'slate-render, and the framework'sturn-withdrawn.TraceEventBaseis a type alias, where it was an interface, andusage:updated's member is built from the usage tracker's ownUsageUpdatedEvent.traceToolOutcomeinsrc/framework.ts. It replaces the seven sites above and the MCPL path'straceMcpToolOutcome, and where a failed result holds noerrorit writes'tool returned an error', as the MCPL path already did.code_execution's owntool:failedkeeps its'unknown error'(src/framework.ts:27598), and a cut-off call's keeps its''(:25649).timestamp. The framework'semitTracebuilds each trace as{ ...event, timestamp: Date.now() }(src/framework.ts:31367–:31370atfd6d6a36).DesiredChannelStateis exported fromsrc/mcpl/channel-registry.ts, sincemcpl:channel-reconcile-failed's member names it.test/trace-union.test.tsis new, holding type-level pins.test/event-gate-sleep.test.ts's trace callback now narrows ontypebefore readingeventType.c97253ceremovestest/tool-failed-error.test.ts, the runtime test of the helper's default that3bb4b8dfadded. With the helper's??default removed,tscreports an error at the helper (Tests), so the test held only the default's exact words.What each part is for
TraceEvent['type']can't name a trace the union doesn't list.tsccheck every raise, as the red run shows.TraceEventInputis the one input type the hooks andemitTraceshare. It maps each member's keys withouttimestamp, rather than usingOmit<TraceEvent, 'timestamp'>, becauseOmitover a union keeps only the keys every member shares. WithOmitin its place,tscreports 254 errors at this head.mcpl:coalescingbykindletstsccheck its fields. An index signature on a member accepts any field. Each kind gets its own member. The undeclared-field pin raises anmcpl:coalescingwith a field its kind doesn't declare, and fails atfd6d6a36.TraceEventBaseas a type alias, andFields<…>for the two members built on an interface (gate:config-reconciledonRecipeChanges, andusage:updated), let every member, narrowed by its name, extendRecord<string, unknown>. The cast pin asserts it, and fails atfd6d6a36, where the base is an interface. So a direct cast of a trace to an index-signature type still compiles oncemcpl:coalescinghas no index signature, as the one attest/mcpl-down-for-good.test.ts:80does at this head.traceToolOutcomegivestool:failed'serrorthe text its type promises, in one place instead of seven. With its??default removed,tscreports TS2322 there, so the build holds that the field is a string.timestamps follows fromTraceEventInput, which has none.tscwhen what they pin changes, as the runs under Tests show. They assert that each hook's parameter, and the framework'semitTrace's, is exactlyTraceEventInput. They assert thatTraceEventInputrefuses an unlisted name, a listed trace without its fields, an undeclared field and atimestamp. And they assert that every member, narrowed by its name, extendsRecord<string, unknown>.breakingfor the typing break and the gate'stimestamp,addedfor the members andTraceEventInput, andfixedfortool:failed'serror.With other open pull requests
git merge-treeof each of the 44 open pull requests ontomaina64cfc010fand onto thatmainwith this head merged into it, on Oct 30 at 20:50Z, compared region by region in each file, differs for ten.Four merge clean onto
main, and conflict ontomainwith this head merged:813fe034fc) conflicts, ontomainwith this head merged, in one region ofsrc/framework.ts, thechannelstool's trace, which this change replaces withtraceToolOutcome.869bb3add7) conflicts, ontomainwith this head merged, in one region ofsrc/mcpl/inference-router.ts: this change types theemitTraceFnfield asTraceEventInput, and that pull request adds anadmissionRefusalsfield on the line before. That pull request also passes theemitTraceFnofChannelRegistry,InferenceRouterandPushHandlerto theAdmissionRefusals.report()it adds, whoseemitTraceparameter is a callback taking{ type: string; [key: string]: unknown }(src/mcpl/inbound-admission.ts:197at its head). A trial merge of it intomainwith this head merged, keeping both lines of that region, gives threetscerrors, TS2345, one at each of those three calls.acfccc9a24) conflicts, ontomainwith this head merged, in one region ofsrc/gate/event-gate.ts. That pull request moves the gate'sgate:debounce-deliveredraise. It removes the raise where this change drops itstimestamp, and raises it in a new place, writingtimestamp(src/gate/event-gate.ts:3929at its head). A trial merge of it intomainwith this head merged, taking its side of that region, gives onetscerror, TS2353, at thattimestamp.Six already conflict with
main, in the same files and as many regions either way. With this head merged, some of those regions hold different lines:6c05acea48), insrc/framework.ts;c3b440879a), insrc/mcpl/channel-registry.ts;6459fdf858) and fix(tune-out): enforce diversion across live delivery and recovery #201 (at484b99095d), insrc/gate/event-gate.ts;0bae78b1dd), insrc/framework.ts,src/gate/event-gate.ts,src/mcpl/channel-registry.tsandsrc/mcpl/push-handler.ts;665957dd5f), in those four files andsrc/tune-out/coordinator.ts.With #844
@tel-0s, #844 (at
b9c4979e89) merges clean ontomain, and ontomainwith this head merged it conflicts in two regions. One is the type imports at the top ofsrc/types/trace.ts, where each adds its own. The other is insrc/framework.ts, where the framework builds itsEventGate: this change drops the cast on the gate'semitTraceoption, and your pull request addsaudienceto itsaddMessageoption on the next line.What this leaves
tool:failedis read two ways in the framework, and this change keeps both as they were. Three paths tracetool:completedfor a result whether or not it hasisError, with these lines at this head:dispatchToolCallEvent, tracestool:completedfor any result it returns (src/framework.ts:29523) andtool:failedonly when the module throws (:29544), under a comment that names RFC-007 and "the host could not obtain a result";sleepandwake, past their refusal in a fork or an ephemeral run (:37062–:37077), deliver their results through afinishthat tracestool:completedfor each (:37083–:37086);gate_status, throughdispatchGateToolCall, tracestool:completedfor every result the gate'shandleToolCall()gives (:38053).The sites that go through
traceToolOutcometrace a result withisErrorastool:failed(:35387–:35393), as they did before.Tests
The runs below were on Linux, with Node v24.22.1, npm 11.19.1 and TypeScript 7.0.2, after
npm install. The repository commits no lockfile, so each printed its direct dependencies withnpm ls --depth=0.Red first, at
fd6d6a36, in a copy of a clean checkout.npm lsgave@animalabs/chronicle0.5.1,@animalabs/context-manager0.24.1,@animalabs/mcpl-core0.4.0,@animalabs/membrane0.8.0,@babel/parser7.29.12,@modelcontextprotocol/clientandserver2.4.0,@types/node22.20.5,@types/ws8.18.2,chokidar4.0.3,discord.js14.27.0,tsx4.23.15,typescript7.0.2 andws8.22.0.test/tool-failed-error.test.tsfrom3bb4b8dfadded,tscreports no errors, and the test fails. Itstool:failedtrace haserror: undefinedwhere the test expects'tool returned an error'.emitTracetyped toTraceEventInput, the casts removed and this change'stest/trace-union.test.tsadded,tscreports 63 errors. 38 quote one of the 28 names, and between them they quote all 28. 21 are at other raises. Of those, 13 write a fieldTraceEventInputdoesn't take: the gate'stimestamp11 times,gate:config-reloaded'sremovedPoliciesandtool:failed'sdurationMs. 7 are the seven tool-outcome sites, whose argument fits neithertool:completednortool:failed. 1 is anmcpl:coalescingraise whose argument has nokind(src/framework.ts:18513). 2 are intest/event-gate-sleep.test.ts, and 2 are pins: the undeclared-field pin, sincemcpl:coalescingstill has its index signature there, and the cast pin, since there every member builds on an interface.At this head
c97253ce, in a copy of a clean checkout with a fresh install,npm lsprinted the same versions.tscreports no errors, the build passes, and 85 test files pass 3,212 of 3,212. They are 77 test files chosen because they name kinds of trace or event this change touches (tool:failed, and names beginninggate:,mcpl:ortune-out:), the 15 thatmainchanged betweend700dce1, the parent of this branch's first commit, andfd6d6a36(8 of them among the 77), andtest/trace-union.test.ts.Host-side breaks. The probe below, put in
test/and compiled withtsc --noEmit -p ., gives no errors at a cleanfd6d6a36and five at a cleanc97253ce, with the same versions as above:test/host-breaks.test.ts(7,69), TS2339,eventTypedoesn't exist onTraceEventInput(the callback);(103,13), TS2322 at thenever, whose message namesmcpl:push_event, one of the 28 added types, first (theswitch);(111,52), TS2339,notAFielddoesn't exist on the narrowedmcpl:coalescingunion;(116,13)andsrc/types/trace.ts(15,13), TS2300, duplicate identifierTraceEventBase(the augmentation).The probe's first comment names
502918b6, this head's parent, which was the branch's head when the probe was written.test/host-breaks.test.tsOne change to
srcat a time failstscwhere it should. In the copy the head's tests ran in, with the same versions,tscran after each of these changes, each undone before the next:?? 'tool returned an error'removed gives one error, TS2322 atsrc/framework.ts:35389, "Type 'string | undefined' is not assignable to type 'string'";EventGate'semitTraceoption typed(event: any) => voidgives one error, at that hook's pin (test/trace-union.test.ts:28);emitTracewidened to{ type: TraceEvent['type']; [key: string]: unknown }gives one error, at its pin (:27);TraceEventInputdefined asOmit<TraceEvent, 'timestamp'>gives 254 errors, among them one at the missing-fields pin (:18).Not verified
fd6d6a36, six of the seven sites weren't run with a failed result that holds noerror. The run reachesread_image's site only. For the other six, the lines under Problem say what they wrote.tool:failedsite was run with a failed result that holds noerror.tscholds that the helper'serroris a string, and the default's words are whatsrc/framework.ts:35389says.EventGate's hook only.tscdoesn't check a field a raise adds through a spread. A one-off read of every raise's argument type through TypeScript 5.9's compiler API, outside the build, found one undeclared field at9ab9b55a,gate:decision'sobserved(src/gate/event-gate.ts:3176there), which this change declares. At this head it read 256 raises and found none.changelog.d/899-trace-union.breaking.md,changelog.d/899-trace-union.added.mdandchangelog.d/899-trace-union.fixed.md.Gideon-2613 made this change, on a design by Ada-1017. Two internal reviews read its first two commits, and Crispin-2713 read its last commit and its merge of
main. Laurel-2386 wrote this description.!Closes #899.