Skip to content

fix(agent): clock the AG-UI encoder so headless runs carry elapsedMs - #3502

Merged
kojiwakayama merged 1 commit into
mainfrom
fix/agui-encoder-elapsed
Aug 9, 2026
Merged

kojiwakayama merged 1 commit into
mainfrom
fix/agui-encoder-elapsed

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Root cause

Persisted run events for hosted and scheduled runs have never carried elapsedMs. The emitter is AgUiBrowserEncoderState (src/agent/ag-ui/browser-encoder.ts): the runtime streams its AG-UI events as SSE to veryfront-api, which persists them into agent_run_event verbatim. That state holds run-scoped stepCount and activeStepName but had no clock at all.

Confirmed the field survives ingest: the API's AG-UI payload schemas are .passthrough(), and append-external-agent-run-events.ts stores payload: event. So a framework-side stamp reaches the column with no API change.

How it was identified

The two symptoms are independent, and together they fingerprint the encoder:

step names elapsedMs encoder
advance absent one shared, unclocked ← production
repeat step-1 absent a fresh encoder per chunk
advance present one shared, clocked — the goal

Production (runs at 10:26, 10:31, 11:00 on 2026-08-09, read from the prod primary) shows advance + absent: payload ? 'elapsedMs' false on every event, stepName running step-1 through step-5, each once as STEP_STARTED and once as STEP_FINISHED.

run-chunk-mirror has been clocked since #3483, so it would stamp. The hosted adapter fixed in #3497 rebuilds its encoder per chunk, so it would repeat step-1. Neither matched — which is what pointed at a third emitter.

Ruled out, so this is not a stale deploy: v0.1.1220 genuinely contains the clocked chunk mirror, and the pod that executed the 11:00 run (10.192.6.132, veryfront-server-6f45c6d8c6-pct7c) reports VERYFRONT_VERSION 0.1.1220.

Despite its name, this encoder is the durable path for headless runs — there is no browser attached to a scheduled run. That is exactly what veryfront-issue-inbox#413 flags, and the name misled two prior fixes: #3483 and #3497.

Why clocked by default

This state is built at four composition roots — ag-ui-sse, runtime-event-encoder, chunk-encoder-bridge, and through browser-chunk-encoder. An opt-in clock only has to be missed at one of them to lose elapsedMs for every run, which is precisely how it was missed twice. So the default stamps, and callers asserting exact payloads pass nowMs: null explicitly.

Tests

  • A deterministic clock stamps run-relative elapsed that accrues across events (25ms then 400ms) and leaves the rest of the payload untouched.
  • A separate test pins that the default is clocked. Reverting the default to opt-in fails it with the default state must stamp elapsedMs — verified.

Verification

  • src/agent, src/internal-agents, src/chat, src/runtime, src/server: 1407 passed. The single agent-stream.handler failure is pre-existing — confirmed by stashing this change and reproducing it identically on a clean tree.
  • fmt, lint, deno task typecheck clean; docs/api-reference regenerated with the CI-pinned Deno 2.7.7.

Follow-up

#3497 remains a valid fix for a real per-chunk encoder defect on the top-level hosted adapter; it is simply not the path production takes. Its description has been corrected on the record.

Summary by CodeRabbit

  • New Features

    • Added optional elapsed-time metadata to browser-encoded events, reporting run-relative elapsedMs values.
    • Added configuration to provide a custom clock or disable timestamp tracking.
    • Extended encoding integrations to support the new timing options.
  • Bug Fixes

    • Ensured elapsed times are non-negative and consistently rounded.
  • Documentation

    • Updated API reference line references for AG-UI functionality.

Persisted run events for hosted and scheduled runs have never carried
`elapsedMs`. The emitter is `AgUiBrowserEncoderState`: the runtime streams
its AG-UI events as SSE to veryfront-api, which persists them into
`agent_run_event` verbatim (the payload schemas are `.passthrough()`, so
added fields survive). That state holds run-scoped `stepCount` and
`activeStepName` but had no clock at all.

The two symptoms fingerprint the encoder, and they are independent:

  step names advance + no elapsedMs -> one shared encoder, unclocked
  step names repeat step-1         -> a fresh encoder per chunk

Production shows the first. `run-chunk-mirror` has been clocked since
#3483, so it would stamp; the hosted adapter fixed in #3497 rebuilds its
encoder per chunk, so it would repeat `step-1`. Neither matched, which is
what identified this third emitter. Despite its name it is the durable
path for headless runs -- there is no browser attached to a scheduled run,
which is what veryfront-issue-inbox#413 already flags. That name misled
both #3483 and #3497.

Clocked by default, rather than an opt-in clock. This state is built at
four composition roots (`ag-ui-sse`, `runtime-event-encoder`,
`chunk-encoder-bridge`, and through `browser-chunk-encoder`), and an
opt-in clock only has to be missed at one of them to lose `elapsedMs` for
every run -- which is precisely how it was missed twice. Callers that
assert exact payloads pass `nowMs: null`.

Tests cover both halves: a deterministic clock stamps run-relative elapsed
that accrues across events without disturbing the rest of the payload, and
a separate test pins that the default is clocked, which fails if the
default ever reverts to opt-in.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7db92863-1232-494f-838c-fa3b0f06ac5d

📥 Commits

Reviewing files that changed from the base of the PR and between f659dd5 and 395dcf7.

📒 Files selected for processing (12)
  • docs/api-reference/veryfront/agent.md
  • src/agent/ag-ui/browser-chunk-encoder.test.ts
  • src/agent/ag-ui/browser-chunk-encoder.ts
  • src/agent/ag-ui/browser-encoder.test.ts
  • src/agent/ag-ui/browser-encoder.ts
  • src/agent/ag-ui/chat-ui-chunk-browser-encoder.test.ts
  • src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts
  • src/agent/ag-ui/chunk-encoder-bridge.ts
  • src/agent/ag-ui/runtime-event-encoder.test.ts
  • src/agent/ag-ui/runtime-event-encoder.ts
  • src/internal-agents/ag-ui-sse.test.ts
  • src/internal-agents/ag-ui-sse.ts

📝 Walkthrough

Walkthrough

AG-UI encoders now support optional run-relative elapsedMs stamping. Callers can inject a clock or disable timing with nowMs: null. Tests cover timing behavior and deterministic unstamped output. API source references were refreshed.

Changes

AG-UI timing support

Layer / File(s) Summary
Timing state and event stamping
src/agent/ag-ui/browser-encoder.ts
Encoder state now supports an optional clock and start timestamp. Runtime and finalization payloads receive rounded, non-negative elapsedMs values when timing is enabled.
Clock option propagation
src/agent/ag-ui/runtime-event-encoder.ts, src/agent/ag-ui/chunk-encoder-bridge.ts, src/agent/ag-ui/browser-chunk-encoder.ts, src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts, src/internal-agents/ag-ui-sse.ts
Encoder entry points accept and forward the optional nowMs configuration.
Timing validation and API references
src/agent/ag-ui/*.test.ts, src/internal-agents/ag-ui-sse.test.ts, docs/api-reference/veryfront/agent.md
Tests cover default stamping, elapsed-time accumulation, opt-out behavior, and deterministic unstamped output. API source links now reference current declarations and implementations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EncoderState
  participant RuntimeEventMapper
  participant AGUIEvent
  EncoderState->>RuntimeEventMapper: provide run start and clock
  RuntimeEventMapper->>AGUIEvent: emit payload with elapsedMs
Loading

Possibly related PRs

Suggested reviewers: kwakayama

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds timing to the AG-UI encoder, but issue [#3483] identifies ConversationRunEventEncoder as the persisted event emitter. Update the persisted ConversationRunEventEncoder path, or provide evidence that the AG-UI encoder changes satisfy the issue's current persistence requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: adding elapsed-time clocking to the AG-UI encoder for headless runs.
Out of Scope Changes check ✅ Passed The changes remain within AG-UI encoder timing, option propagation, tests, and related documentation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agui-encoder-elapsed

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 395dcf7768

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export function createStreamTransformState(
options: AgUiBrowserEncoderStateOptions = {},
): StreamTransformState {
return createAgUiBrowserEncoderState(options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve elapsedMs through the headless SSE formatter

In the headless path checked in src/internal-agents/run-stream.ts, this state feeds mapped events into formatAgUiEvent, but that formatter parses known events through the closed object schemas in buildAgUiEventPayloadSchemas. Those schemas do not declare elapsedMs, and the registered schema validator strips unknown object keys by default, so the newly stamped field is removed before the SSE reaches persistence. Hosted and scheduled runs therefore still store events without elapsedMs; add the field to the wire schemas or make them passthrough, and cover the formatted SSE output.

Useful? React with 👍 / 👎.

@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 91b6bc3 Aug 9, 2026
31 checks passed
@kojiwakayama
kojiwakayama deleted the fix/agui-encoder-elapsed branch August 9, 2026 12:40
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.

1 participant