Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/agent/ag-ui/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,10 @@ describe("agent/ag-ui-encoder", () => {
data: { runStartedAtUtc: "2026-07-19T07:30:00.000Z" },
}),
[{
event: "Custom",
event: "RuntimeEventRecorded",
payload: {
name: "veryfront.runtime_context",
runtime: "veryfront",
kind: "runtime_context",
value: { runStartedAtUtc: "2026-07-19T07:30:00.000Z" },
},
}],
Expand Down
95 changes: 93 additions & 2 deletions src/agent/ag-ui/native-run-events.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import "#veryfront/schemas/_test-setup.ts";
import { assertEquals, assertExists } from "#veryfront/testing/assert.ts";
import { assertEquals, assertExists, assertThrows } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import {
buildChildRunStatusChangedEvent,
buildDocumentCitedEvent,
buildFileAttachedEvent,
buildInputRequestLifecycleEvent,
buildNativeRunEventFrame,
buildRuntimeEventRecordedEvent,
buildToolCallStatusChangedEvent,
buildUrlCitedEvent,
isNativeRunEventName,
Expand Down Expand Up @@ -57,6 +58,7 @@ describe("agent/ag-ui-native-run-events", () => {
"UrlCited",
"DocumentCited",
"FileAttached",
"RuntimeEventRecorded",
]);
assertEquals(
Object.values(nativeRunEventTypes),
Expand All @@ -69,7 +71,7 @@ describe("agent/ag-ui-native-run-events", () => {
);
});

it("accepts the six legacy custom names and rejects everything else", () => {
it("accepts the seven legacy custom names and rejects everything else", () => {
for (
const name of [
"tool-call-status",
Expand All @@ -78,6 +80,7 @@ describe("agent/ag-ui-native-run-events", () => {
"source-url",
"source-document",
"file",
"veryfront.runtime_context",
]
) {
assertEquals(isNativeRunEventName(name), true, name);
Expand Down Expand Up @@ -423,6 +426,55 @@ describe("agent/ag-ui-native-run-events", () => {
}
});

it("builds both emission shapes for a runtime event recorded", () => {
const runtimeContext = {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
};
assertEquals(
buildRuntimeEventRecordedEvent({
runtime: "veryfront",
kind: "runtime_context",
value: runtimeContext,
}),
{
live: {
event: "RuntimeEventRecorded",
payload: { runtime: "veryfront", kind: "runtime_context", value: runtimeContext },
},
durable: {
runtime: "veryfront",
kind: "runtime_context",
value: runtimeContext,
type: "RUNTIME_EVENT_RECORDED",
},
},
);
});

it("rejects a runtime event recorded with an invalid runtime, kind, or value", () => {
// `runtime`/`kind` are typed `string`, which accepts empty text, and
// `value` is typed `unknown`, which accepts `undefined`; but the API
// catalog's RUNTIME_EVENT_RECORDED variant requires non-empty strings
// and a JSON value. This builder has callers outside this module's own
// dispatcher (e.g. a future codex-runtime producer), so it must reject
// those on its own rather than trusting the caller's static types.
assertThrows(() =>
buildRuntimeEventRecordedEvent({ runtime: "", kind: "runtime_context", value: {} })
);
assertThrows(() =>
buildRuntimeEventRecordedEvent({ runtime: "veryfront", kind: "", value: {} })
);
assertThrows(() =>
buildRuntimeEventRecordedEvent({
runtime: "veryfront",
kind: "runtime_context",
value: undefined,
})
);
});

it("routes every legacy name through the dispatcher", () => {
assertEquals(
buildNativeRunEventFrame({
Expand Down Expand Up @@ -461,6 +513,40 @@ describe("agent/ag-ui-native-run-events", () => {
})?.live.event,
"FileAttached",
);
assertEquals(
buildNativeRunEventFrame({
name: "veryfront.runtime_context",
value: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
}),
{
live: {
event: "RuntimeEventRecorded",
payload: {
runtime: "veryfront",
kind: "runtime_context",
value: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
},
},
durable: {
runtime: "veryfront",
kind: "runtime_context",
value: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
type: "RUNTIME_EVENT_RECORDED",
},
},
);
});

it("returns null for a name or value that has no native frame", () => {
Expand Down Expand Up @@ -494,5 +580,10 @@ describe("agent/ag-ui-native-run-events", () => {
null,
"a file-change value is FILES_CHANGED on the legacy path, never FileAttached",
);
assertEquals(
buildNativeRunEventFrame({ name: "veryfront.runtime_context", value: null }),
null,
"a non-record value cannot become a RuntimeEventRecorded payload",
);
});
});
56 changes: 55 additions & 1 deletion src/agent/ag-ui/native-run-events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getJsonValueSchema, getNonEmptyStringSchema } from "#veryfront/schemas/index.ts";

/**
* Native run event vocabulary shared by every emission path.
*
* Veryfront Code used to wrap these seven occurrences in an AG-UI `Custom`
* Veryfront Code used to wrap these eight occurrences in an AG-UI `Custom`
* frame and let the API translate the custom name back into a type. The API
* now accepts the native names, so this module owns the list once: the wire
* name a live SSE frame carries, the stored type a durable record carries, and
Expand Down Expand Up @@ -45,6 +47,11 @@ export const NATIVE_RUN_EVENTS = [
legacyCustomName: "source-document",
},
{ wireName: "FileAttached", storedType: "FILE_ATTACHED", legacyCustomName: "file" },
{
wireName: "RuntimeEventRecorded",
storedType: "RUNTIME_EVENT_RECORDED",
legacyCustomName: "veryfront.runtime_context",
},
] as const satisfies readonly NativeRunEventDefinition[];

type NativeRunEventEntry = (typeof NATIVE_RUN_EVENTS)[number];
Expand Down Expand Up @@ -78,6 +85,7 @@ export const nativeRunEventTypes = {
urlCited: "URL_CITED",
documentCited: "DOCUMENT_CITED",
fileAttached: "FILE_ATTACHED",
runtimeEventRecorded: "RUNTIME_EVENT_RECORDED",
} as const;

/** Stored type for each native wire name, for SSE readers. */
Expand Down Expand Up @@ -136,6 +144,7 @@ const CHILD_RUN_STATUS_CHANGED = NATIVE_RUN_EVENTS[3];
const URL_CITED = NATIVE_RUN_EVENTS[4];
const DOCUMENT_CITED = NATIVE_RUN_EVENTS[5];
const FILE_ATTACHED = NATIVE_RUN_EVENTS[6];
const RUNTIME_EVENT_RECORDED = NATIVE_RUN_EVENTS[7];

// Keep application fields from overriding the native type or transport timing
// when an open custom value becomes a flat native payload.
Expand Down Expand Up @@ -330,6 +339,39 @@ export function buildFileAttachedEvent(source: Record<string, unknown>): NativeR
return toFrame(FILE_ATTACHED, payload);
}

/**
* Fields the API catalog's `RUNTIME_EVENT_RECORDED` variant requires:
* `runtime` and `kind` non-empty strings, `value` any JSON value but never
* `undefined`. This is the catch-all diagnostics type for a runtime-native
* event with no AG-UI equivalent (the API catalog's own description
* mentions codex thread/session events as a future producer), so unlike the
* other seven builders this one does not derive its shape from a fixed
* source chunk -- the caller supplies the catalog fields directly.
*/
export interface RuntimeEventRecordedInput {
runtime: string;
kind: string;
value: unknown;
}

/**
* Build the runtime event recorded frames.
*
* Validates against the catalog's own constraints -- `runtime`/`kind` non-empty
* strings, `value` a bounded JSON value -- rather than trusting the caller's
* static `string`/`unknown` types, since this builder (unlike the other seven)
* has callers outside this module's own dispatcher that supply the catalog
* fields directly.
*/
export function buildRuntimeEventRecordedEvent(
input: RuntimeEventRecordedInput,
): NativeRunEventFrame {
const runtime = getNonEmptyStringSchema().parse(input.runtime);
const kind = getNonEmptyStringSchema().parse(input.kind);
const value = getJsonValueSchema().parse(input.value);
return toFrame(RUNTIME_EVENT_RECORDED, { runtime, kind, value });
}

/** Routing input for one custom event name and its value. */
export interface NativeRunEventRoutingInput {
name: string;
Expand Down Expand Up @@ -389,6 +431,18 @@ export function buildNativeRunEventFrame(
readString(record.mediaType)
? buildFileAttachedEvent(record)
: null;
case "veryfront.runtime_context":
// The one producer (runtime/index.ts's #streamWithinTurn) always sends
// the whole AgentRunRuntimeContext snapshot as the chunk's `data`, so
// `record` (already guarded non-null above) is the payload's `value`
// field wholesale; `runtime`/`kind` are this producer's own constants,
// not read off the value, since this legacy name only ever carried the
// context object itself.
return buildRuntimeEventRecordedEvent({
runtime: "veryfront",
kind: "runtime_context",
value: record,
});
default:
return null;
}
Expand Down
44 changes: 44 additions & 0 deletions src/agent/conversation/legacy-run-read-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
buildDocumentCitedEvent,
buildFileAttachedEvent,
buildInputRequestLifecycleEvent,
buildRuntimeEventRecordedEvent,
buildToolCallStatusChangedEvent,
buildUrlCitedEvent,
NATIVE_RUN_EVENTS,
Expand Down Expand Up @@ -1569,6 +1570,27 @@ describe("conversation run lifecycle read adapter", () => {
value: { type: "file", mediaType: "text/plain", path: "notes.txt" },
},
},
{
description: "RUNTIME_EVENT_RECORDED",
native: buildRuntimeEventRecordedEvent({
runtime: "veryfront",
kind: "runtime_context",
value: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
}).durable,
customTwin: {
type: "CUSTOM",
name: "veryfront.runtime_context",
value: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
},
},
];

it("covers every native type with a legacy reconstruction case", () => {
Expand Down Expand Up @@ -1597,6 +1619,28 @@ describe("conversation run lifecycle read adapter", () => {
assertEquals(customFramesFor(1, native), customFramesFor(1, customTwin));
});

it(
"does not reconstruct a non-veryfront RUNTIME_EVENT_RECORDED as the runtime_context twin",
() => {
// RUNTIME_EVENT_RECORDED is the API catalog's generic diagnostics
// shape and has other producers (e.g. the codex runtime) with other
// runtime/kind pairs. Only the exact veryfront/runtime_context pair
// may unwrap to the legacy `veryfront.runtime_context` CUSTOM twin;
// every other pair must surface as its own generic custom record
// instead of a false runtime_context.
const native = buildRuntimeEventRecordedEvent({
runtime: "codex",
kind: "stderr",
value: { line: "boom" },
}).durable;

assertEquals(
customFramesFor(2, { ...native, ...v2Envelope(1, "codex-runtime-event") }),
[{ type: "custom", name: "codex.stderr", data: { line: "boom" } }],
);
},
);

it("reads a TOOL_CALL_STATUS_CHANGED durable record as its CUSTOM twin on the version 1 reader", () => {
const { native, customTwin } = cases.find((entry) =>
entry.description === "TOOL_CALL_STATUS_CHANGED"
Expand Down
18 changes: 17 additions & 1 deletion src/agent/conversation/legacy-run-read-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const DURABLE_ENVELOPE_KEYS = [
*/
function readNativeAsLegacyCustom(
event: Record<string, unknown>,
): { name: string; value: Record<string, unknown> } | null {
): { name: string; value: unknown } | null {
const definition = typeof event.type === "string"
? NATIVE_STORED_TYPE_TO_LEGACY.get(event.type)
: undefined;
Expand All @@ -65,6 +65,22 @@ function readNativeAsLegacyCustom(
for (const key of DURABLE_ENVELOPE_KEYS) {
delete value[key];
}
if (definition.storedType === "RUNTIME_EVENT_RECORDED") {
// The legacy `veryfront.runtime_context` CUSTOM twin carried the bare
// AgentRunRuntimeContext object as its value, produced only for the
// veryfront/runtime_context pair; the native payload wraps that same
// object as `{ runtime, kind, value }` to match the API catalog's
// generic diagnostics shape (RUNTIME_EVENT_RECORDED has other producers
// with other runtimes/kinds, e.g. the codex runtime, so the wrapper is
// required there). Only that exact pair unwraps to the legacy twin here,
// the same way the citation/file case below restores a field the native
// payload dropped; any other runtime/kind becomes its own generic custom
// record instead of a false veryfront.runtime_context.
if (value.runtime === "veryfront" && value.kind === "runtime_context") {
return { name: definition.legacyCustomName, value: value.value };
}
return { name: `${String(value.runtime)}.${String(value.kind)}`, value: value.value };
}
if (
definition.storedType === "INPUT_REQUEST_CREATED" ||
definition.storedType === "INPUT_REQUEST_UPDATED"
Expand Down
26 changes: 26 additions & 0 deletions src/agent/conversation/run-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,32 @@ describe("agent/conversation-run-events", () => {
);
});

it("encodes native runtime event chunks as native durable records", () => {
const encoder = new ConversationRunEventEncoder();
assertEquals(
encoder.encode({
type: "data-veryfront.runtime_context",
data: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
}),
[
{
type: conversationRunEventTypes.runtimeEventRecorded,
runtime: "veryfront",
kind: "runtime_context",
value: {
currentTimeUtc: "2026-09-09T00:00:00.000Z",
currentDateUtc: "2026-09-09",
runStartedAtUtc: "2026-09-09T00:00:00.000Z",
},
},
],
);
});

it("keeps state chunks and unknown data names custom", () => {
const encoder = new ConversationRunEventEncoder();
assertEquals(
Expand Down
Loading
Loading