Skip to content

fix(agui): mirror the canonical subagent lifecycle events and subagentRunId - #391

Merged
contextablemark merged 2 commits into
mainfrom
fix/agui-subagent-drift
Aug 30, 2026
Merged

fix(agui): mirror the canonical subagent lifecycle events and subagentRunId#391
contextablemark merged 2 commits into
mainfrom
fix/agui-subagent-drift

Conversation

@jpr5

@jpr5 jpr5 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Drift Tests has been red on main since Aug 26. Of the criticals, 26 are AG-UI schema drift — upstream ag-ui added the subagent lifecycle and aimock's types never followed. (The Gemini model-family criticals in the same run are a separate concern and are not touched here.)

What upstream added

Three new members on EventTypeSUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR — plus a subagentRunId correlation field threaded through every event a subagent can emit.

subagentRunId is per-event, not a base field

Worth stating explicitly, because the cheap fix is wrong. The drift report lists subagentRunId against 23 event types, which reads like a base-event field — and declaring it once on AGUIBaseEvent would have cleared all 23 at once, the way metadata was cleared in #387.

But canonical does not put it on BaseEventSchema. Reading events.ts schema by schema:

  • 24 events declare subagentRunId: z.string().optional()
  • 3 subagent events declare it required, z.string()
  • 7 events deliberately omit it: RUN_STARTED, RUN_FINISHED, RUN_ERROR, MESSAGES_SNAPSHOT, and the four deprecated THINKING_* events

A base-event declaration would have gone green while putting the field on seven events canonical does not give it. So it is mirrored per event.

(24 optional, not the 23 reported — STATE_DELTA was missing from the report for a separate reason, below.)

Changes

  • src/agui-types.tssubagentRunId?: string on the 24 events canonical marks optional; three new event interfaces (AGUISubagentStartedEvent / Finished / Error) with it required; AGUISubagentFinishedOutcome mirroring AGUIRunFinishedOutcome one level down; union and AGUIEventType members.
  • src/__tests__/drift/agui-schema.drift.ts — strip trailing comments in the canonical parser, plus a regression test.

The parser bug this surfaced

The canonical parser stripped whole-line comments only. A trailing comment survives, and since entries are cut on top-level commas it then leads the next entry, whose field-name match fails — the field is dropped silently.

Upstream writes exactly that on STATE_DELTA:

delta: z.array(z.any()), // JSON Patch (RFC 6902)
subagentRunId: z.string().optional(),

So canonical STATE_DELTA.subagentRunId was invisible: it never appeared in the 23 criticals, and once declared it flipped to a false exists in aimock but not in canonical warning. Same class as the two parser mis-readings fixed in #387. Stripping is safe here — no canonical schema literal contains //.

Red-green proof

⚠️ The local ../ag-ui sibling this test resolves was stale (Aug 22, no subagent events at all), and against it the suite passes 15/15 — a false green. Both runs below use an isolated, freshly-cloned canonical checkout, the same thing CI clones.

  • aimock: 5e3b500 (red) → 6ecdcce (green)
  • ag-ui: 363d3878e30887e88c1fd5ca1916ec3a5962b6be
  • Positive control: grep -c SUBAGENT on the fresh clone's events.ts = 9; on the stale sibling = 0.

Command, identical for both:

npx vitest run --config vitest.config.drift.ts

RED — at 5e3b500, unmodified:

EXIT=1
 Test Files  1 failed | 18 passed | 7 skipped (26)
      Tests  2 failed | 132 passed | 57 skipped (191)

[CRITICAL] Event type "SUBAGENT_STARTED" exists in canonical @ag-ui/core but is missing from aimock AGUIEventType
[CRITICAL] Event type "SUBAGENT_FINISHED" ...
[CRITICAL] Event type "SUBAGENT_ERROR" ...
[CRITICAL] TEXT_MESSAGE_START: field "subagentRunId" (optional) exists in canonical but missing from aimock
  ... × 23 event types

26 criticals: 3 event types + subagentRunId × 23.

GREEN — this branch:

EXIT=0
 Test Files  19 passed | 7 skipped (26)
      Tests  135 passed | 57 skipped (192)

0 criticals. One warning remains and is pre-existing and genuine — canonical TEXT_MESSAGE_START.role is .default("assistant") where aimock requires it, same one #387 left as-is. The false STATE_DELTA warning is gone.

Guards mutation-tested

Mutation Result
Drop subagentRunId from agui-types.ts EXIT=1, criticals return
Drop the 3 SUBAGENT_* members from AGUIEventType EXIT=1, all 3 event-type criticals return
Revert the trailing-comment strip EXIT=1, the new regression test fails

Other checks

tsc --noEmit clean · eslint . clean · prettier --check clean · tsdown build clean · full unit suite 5280 passed | 46 skipped (5326), EXIT=0.

Note on CI

The drift job is gated if: github.event_name != 'pull_request', so a green PR here does not exercise it — hence the local proof above. agui-schema-drift does run on PRs and covers the change in this PR.

jpr5 added 2 commits August 30, 2026 08:35
…tRunId

Upstream ag-ui added SUBAGENT_STARTED / SUBAGENT_FINISHED / SUBAGENT_ERROR
and threaded an optional `subagentRunId` through the events a subagent can
emit, so aimock's AG-UI types were behind canonical.

`subagentRunId` is declared per event, not on the base event: canonical puts
it on 24 event schemas as optional and on the three subagent events as
required, and deliberately omits it from RUN_STARTED / RUN_FINISHED /
RUN_ERROR / MESSAGES_SNAPSHOT and the four deprecated THINKING_* events.
Declaring it once on AGUIBaseEvent would have cleared the drift report but
put the field on seven events that canonical does not give it.
…pped

The canonical parser stripped whole-line comments only. A trailing comment
survives, and since entries are cut on top-level commas it then leads the
NEXT entry, whose field-name match fails — the field is dropped silently.

Upstream writes `delta: z.array(z.any()), // JSON Patch (RFC 6902)` on
STATE_DELTA, which hid canonical STATE_DELTA.subagentRunId: it went
unreported as missing, then read as an aimock-only field once declared.
@pkg-pr-new

pkg-pr-new Bot commented Aug 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@391

commit: 6ecdcce

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the scoped AG-UI event-schema parity and trailing-comment parser regression against canonical upstream. The reviewed head is 6ecdcce; all active checks are green.

@contextablemark
contextablemark merged commit 8853c61 into main Aug 30, 2026
28 checks passed
@contextablemark
contextablemark deleted the fix/agui-subagent-drift branch August 30, 2026 16:21
contextablemark added a commit that referenced this pull request Aug 31, 2026
…ges and interrupts, and stop the drift collector reporting clean when it cannot see (#393)

Follow-up to #391, which merged at its pre-review head. The type mirror
landed, but the review that was running against it had not reported yet
— so `main` currently has the three `SUBAGENT_*` event types **and no
way for a consumer to import them**. This carries the review fixes.

## What was actually broken on main

- `git show origin/main:src/index.ts | grep -c Subagent` → **0**. The
new event types are exported from neither barrel, so the headline
benefit of #391 is unreachable from outside the package.
- `AGUIMessage` and `AGUIInterrupt` never got `subagentRunId`, though
canonical declares it on both — `types.ts:28` (`BaseMessageSchema`) and
`types.ts:224` (`InterruptSchema`).
- The AG-UI drift CI lane ran a single hardcoded filename, so any drift
guard added later would silently never run.
- The report collector **dropped any AG-UI failure it could not
pattern-match**, with no counter and no quarantine — a real assertion in
the existing `agui-schema.drift.ts` produced exit 0 and `conclusion:
"clean"`. That is pre-existing and independent of #391.

## Changes

**Public surface**
- Export `AGUISubagentStartedEvent`, `AGUISubagentFinishedEvent`,
`AGUISubagentErrorEvent`, `AGUISubagentFinishedOutcome` from **both**
barrels — `src/index.ts` and `src/agui-stub.ts` (the `./agui` subpath).
The second barrel was missing them too.
- Mirror canonical optional `subagentRunId` onto `AGUIMessage` and
`AGUIInterrupt`.

**Guard against recurrence**
- `src/__tests__/agui-barrel-exports.test.ts` asserts every type
declared in `agui-types.ts` is re-exported from both barrels. It parses
with the **TypeScript compiler API**, not regexes, and records a name
only when it is reachable *under its declared name* — so a renamed
re-export (`X as Y`) cannot pass, by construction.

**CI lane**
- The AG-UI drift lane and the collector now select by path prefix
`src/__tests__/drift/agui-` with a documented naming contract, instead
of one hardcoded filename. A future AG-UI guard is picked up
automatically rather than orphaned. The lane still requires no provider
API keys; the credit-burning `drift` leg is untouched.

**Collector fail-closed**
- An AG-UI failure the collector cannot structurally interpret now
**quarantines** (existing exit-5 lane, `CollectResult {entries,
quarantine}` — the same shape the HTTP leg already returns) instead of
vanishing. Per-assertion and ungated.
- A failed assertion with empty `failureMessages` no longer reads as
clean.
- Unparseable AG-UI stdout on a zero-exit run now throws, matching the
HTTP twin, instead of returning `{testResults: []}`.
- `classifyAgUiCheckout` verifies the canonical `types.ts` actually
exists rather than trusting a directory named `ag-ui`, so a stale clone
reports as stale instead of as a git/network failure.

## Evidence

Barrel exports — before:
```
error TS2724: '"./src/index.js"' has no exported member named 'AGUISubagentStartedEvent'
```
8 such errors across both barrels; clean after.

CI lane — with `subagentRunId` deleted from `AGUIMessage`, the **old**
lane command still passed, exit 0. The guard was provably inert on the
server. After: exit 1.

Collector — mutating `agui-schema.drift.ts` to produce an
uninterpretable real failure:
```
before: entries 0 / quarantined 0 / exit 0 / conclusion "clean"
after:  QUARANTINE= 1 ... EXITCODE= 5 CONCLUSION= quarantine
```

Gates: `tsc --noEmit`, `eslint`, `prettier --check`, `tsdown` all exit
0; full suite 179 files / 5367 tests.

## Known gaps — deliberate, not oversights

- **`subagentRunId` on `AGUIMessage`/`AGUIInterrupt` ships without a
regression test.** A non-event drift guard was written for this PR and
**withdrawn**: three review rounds found fail-open defects in it (most
seriously, it read `.omit({subagentRunId: true})` — a schema *removing*
the field — as declaring it, an idiom canonical already uses). Rather
than a fourth patching round on a hand-rolled schema differ, it is
deferred to the follow-up that rewrites the legacy
`agui-schema.drift.ts`, so one differ gets built once. The field is
correct as merged; nothing will catch its removal until then.
- **The `SUBAGENT_*` types are declarative only.** No builder in
`agui-handler.ts` emits them and no build option accepts
`subagentRunId`, so aimock can type a subagent event but cannot emit
one. The CHANGELOG says so explicitly rather than implying reach.

## Follow-ups filed (not in scope here)

- Rewrite `agui-schema.drift.ts` onto the TypeScript AST — same root
cause as everything above; the barrel guard's readers are liftable.
- The drift **reporting** layer: the base-report reuse path is dead for
three independent reproduced reasons (bare `node` importing a `.ts`
module, missing `actions: read`, and a `--status=success` filter that
cannot fire on days main drifted), so every PR pays a full fresh live
provider run; infrastructure failures are Slacked as "providers changed
response formats"; and seven provider secrets sit in job-level `env:`,
exposed to `pnpm install` lifecycle scripts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AvkmhXVLqSSEW6FvPQHSu5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants