Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
06a5f0b
feat(agent): add the native run event vocabulary module
kojiwakayama Sep 9, 2026
30d4e4c
feat(agent): carry the tool name on tool input status telemetry
kojiwakayama Sep 9, 2026
f1a0435
feat(agent): emit native AG-UI frames on the live SSE path
kojiwakayama Sep 9, 2026
c427c13
feat(agent): write native durable records for the seven extension events
kojiwakayama Sep 9, 2026
d4cb636
feat(agent): append child run progress as CHILD_RUN_STATUS_CHANGED
kojiwakayama Sep 9, 2026
041dac1
feat(agent): read native run event frames in the parser and eval harness
kojiwakayama Sep 9, 2026
3ee902f
fix(agent): strip timing stamps when reading native records as legacy…
kojiwakayama Sep 9, 2026
d938f97
fix(chat): decode native run event frames in the AG-UI client
kojiwakayama Sep 9, 2026
c81157f
test(chat): cover the schemaless native decoder arms and pin the wire…
kojiwakayama Sep 9, 2026
cd581cf
test(agent): pin the native run event contract fixture
kojiwakayama Sep 9, 2026
8617f03
fix(agent): carry parentMessageId through the internal-agents AG-UI S…
kojiwakayama Sep 9, 2026
355d5e7
fix(agent): preserve tool-status fields and required native fields th…
kojiwakayama Sep 9, 2026
3688975
fix(agent): address native run events whole-branch review findings
kojiwakayama Sep 9, 2026
1746359
fix(agent): keep one native-run-event payload shared by live and dura…
kojiwakayama Sep 9, 2026
a712c54
docs(agent): fix stale cross-references and loosen ToolCallStatusChan…
kojiwakayama Sep 9, 2026
bcd714e
fix(agent): preserve child-run helper contracts and native event types
kojiwakayama Sep 10, 2026
f984cf5
fix(chat): accept tool status events without a tool name
kojiwakayama Sep 10, 2026
4d21b9d
fix(agent): reserve native event transport timing fields
kojiwakayama Sep 10, 2026
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
22 changes: 22 additions & 0 deletions docs/guides/memory-and-streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,28 @@ export async function GET() {
}
```

## Native run events

Veryfront emits native AG-UI events for tool status, input requests, child-run
status, citations, and attachments. Live SSE frames carry names such as
`ChildRunStatusChanged`; durable records use `CHILD_RUN_STATUS_CHANGED`. Readers
also accept the earlier `Custom` events. Tool-status frames can omit the tool name
or set it to `null`; the chat decoder preserves the status in either case. Native
event payloads reserve `type`, `elapsedMs`, and `emittedAt` for transport metadata.
Put application timing data in nested fields.

The public `buildInvokeAgentChildRunLifecycleCustomEvent` and
`buildInvokeAgentChildRunProgressEvents` helpers retain the `{ type: "CUSTOM",
name, value }` lifecycle shape. Their schemas and publisher callbacks keep that
contract. Veryfront converts these lifecycle events to native records when it
prepares them for durable publication.

An oversized tool-status, input-request, or child-run record becomes a
`conversation-run-event-omitted` marker. The marker records the original event
type and tool call ID when available; it does not retain the oversized payload.
You must keep lifecycle payloads within the run event size limit to preserve their
full content in stored history.

## Streaming

### Server-side streaming
Expand Down
27 changes: 15 additions & 12 deletions src/agent/ag-ui/chat-ui-chunk-encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ describe("agent/ag-ui-chat-ui-chunk-encoder", () => {
);
});

it("encodes source documents as renderable custom events", () => {
it("encodes source documents as native citation events", () => {
const encoder = createAgUiChatUiChunkEncoder({ timing: { nowMs: null, epochMs: null } });
const sourceDocument: ChatUiMessageChunk = {
type: "source-document",
Expand All @@ -239,15 +239,17 @@ describe("agent/ag-ui-chat-ui-chunk-encoder", () => {
};

assertEquals(encoder.encode(sourceDocument), [{
event: "Custom",
event: "DocumentCited",
payload: {
name: "source-document",
value: sourceDocument,
sourceId: "knowledge/knowledge-ingest-20260723131451088-source.md",
mediaType: "text/markdown",
title: "knowledge/knowledge-ingest-20260723131451088-source.md",
filename: "knowledge/knowledge-ingest-20260723131451088-source.md",
},
}]);
});

it("encodes source URLs as renderable custom events", () => {
it("encodes source URLs as native URL citation events", () => {
const encoder = createAgUiChatUiChunkEncoder({ timing: { nowMs: null, epochMs: null } });
const sourceUrl: ChatUiMessageChunk = {
type: "source-url",
Expand All @@ -257,15 +259,16 @@ describe("agent/ag-ui-chat-ui-chunk-encoder", () => {
};

assertEquals(encoder.encode(sourceUrl), [{
event: "Custom",
event: "UrlCited",
payload: {
name: "source-url",
value: sourceUrl,
sourceId: "web-1",
url: "https://example.com/reference",
title: "Reference",
},
}]);
});

it("encodes files as renderable custom events", () => {
it("encodes files as native file attachment events", () => {
const encoder = createAgUiChatUiChunkEncoder({ timing: { nowMs: null, epochMs: null } });
const file: ChatUiMessageChunk = {
type: "file",
Expand All @@ -274,10 +277,10 @@ describe("agent/ag-ui-chat-ui-chunk-encoder", () => {
};

assertEquals(encoder.encode(file), [{
event: "Custom",
event: "FileAttached",
payload: {
name: "file",
value: file,
url: "https://cdn.example.com/report.pdf",
mediaType: "application/pdf",
},
}]);
});
Expand Down
131 changes: 125 additions & 6 deletions src/agent/ag-ui/encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ describe("agent/ag-ui-encoder", () => {
},
{
event: "ToolCallStart",
payload: { toolCallId: "tool-1", toolCallName: "web_search" },
payload: {
toolCallId: "tool-1",
toolCallName: "web_search",
parentMessageId: "assistant-1",
},
},
],
);
Expand Down Expand Up @@ -279,13 +283,14 @@ describe("agent/ag-ui-encoder", () => {
assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "data-tool-call-status",
data: { toolCallId: "tool-1", status: "pending_input" },
data: { toolCallId: "tool-1", toolCallName: "create_file", status: "pending_input" },
}),
[{
event: "Custom",
event: "ToolCallStatusChanged",
payload: {
name: "tool-call-status",
value: { toolCallId: "tool-1", status: "pending_input" },
toolCallId: "tool-1",
status: "pending_input",
toolCallName: "create_file",
},
}],
);
Expand Down Expand Up @@ -323,6 +328,116 @@ describe("agent/ag-ui-encoder", () => {
);
});

it("emits native frames for citations, attachments, and lifecycle names", () => {
const state = createAgUiEncoderState({ nowMs: null, epochMs: null });

assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "source-url",
sourceId: "web-1",
url: "https://example.com/reference",
title: "Reference",
}),
[{
event: "UrlCited",
payload: {
sourceId: "web-1",
url: "https://example.com/reference",
title: "Reference",
},
}],
);
assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "source-document",
sourceId: "knowledge/report.md",
mediaType: "text/markdown",
title: "Report",
filename: "knowledge/report.md",
}),
[{
event: "DocumentCited",
payload: {
sourceId: "knowledge/report.md",
mediaType: "text/markdown",
title: "Report",
filename: "knowledge/report.md",
},
}],
);
assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "file",
url: "https://cdn.example.com/report.pdf",
mediaType: "application/pdf",
filename: "report.pdf",
}),
[{
event: "FileAttached",
payload: {
url: "https://cdn.example.com/report.pdf",
mediaType: "application/pdf",
filename: "report.pdf",
},
}],
);
assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "data-veryfront.input_request.lifecycle",
data: {
action: "created",
inputRequest: { id: "8f2f1f52-0f2a-4a3a-9b0f-0f2a4a3a9b0f", status: "open" },
},
}),
[{
event: "InputRequestCreated",
payload: {
inputRequest: { id: "8f2f1f52-0f2a-4a3a-9b0f-0f2a4a3a9b0f", status: "open" },
},
}],
);
assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "data-veryfront.invoke_agent.lifecycle",
data: {
toolCallId: "toolu_child_1",
childRunId: "run_child_1",
status: "running",
},
}),
[{
event: "ChildRunStatusChanged",
payload: {
toolCallId: "toolu_child_1",
childRunId: "run_child_1",
status: "running",
},
}],
);
});

it("keeps state chunks and unknown data names custom", () => {
const state = createAgUiEncoderState({ nowMs: null, epochMs: null });

assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, {
type: "data-state-delta",
data: [{ op: "replace", path: "/a", value: 1 }],
}),
[{
event: "Custom",
payload: {
name: "state-delta",
value: [{ op: "replace", path: "/a", value: 1 }],
},
}],
);
assertEquals(
mapRuntimeStreamEventToAgUiEvents(state, { type: "data-foo", data: { a: 1 } }),
[{ event: "Custom", payload: { name: "foo", value: { a: 1 } } }],
);
});

it("keeps provider-executed tools open until the provider returns a result", () => {
const state = createAgUiEncoderState({ nowMs: null, epochMs: null });

Expand Down Expand Up @@ -543,7 +658,11 @@ describe("agent/ag-ui-encoder", () => {
},
{
event: "ToolCallStart",
payload: { toolCallId: "tool-4", toolCallName: "web_search" },
payload: {
toolCallId: "tool-4",
toolCallName: "web_search",
parentMessageId: "assistant-3",
},
},
],
);
Expand Down
36 changes: 30 additions & 6 deletions src/agent/ag-ui/encoder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentResponse } from "../types.ts";
import { buildNativeRunEventFrame } from "./native-run-events.ts";

/** Event emitted for AG-UI runtime stream. */
export type AgUiRuntimeStreamEvent = Record<string, unknown> & { type: string };
Expand Down Expand Up @@ -717,6 +718,20 @@ export function mapRuntimeStreamEventToAgUiEvents(
);
}

/**
* The transport timing fields `stampAgUiEventTiming` writes onto a live
* event's own flat payload. Exported only so a reader that reconstructs a
* native frame's whole payload as legacy data -- e.g. the chat client's
* `stripAgUiTimingStamps` in `ag-ui.ts` -- can assert its strip list still
* covers every stamped field, instead of a third stamped field silently
* leaking into legacy `data` the way `elapsedMs`/`emittedAt` already did
* once on the durable-record read path (fixed in `legacy-run-read-adapter.ts`,
* commit 3ee902fb12) before this export existed to catch it in a test.
* `stampAgUiEventTiming` below keeps its own literals; this export adds no
* dependency to the runtime stamping path.
*/
export const AG_UI_EVENT_TIMING_STAMP_FIELDS = ["elapsedMs", "emittedAt"] as const;

export function stampAgUiEventTiming(
state: AgUiEncoderState,
events: AgUiEncodedEvent[],
Expand Down Expand Up @@ -786,15 +801,23 @@ function mapRuntimeStreamEventToAgUiEventsUnstamped(
}

state.sawVisibleOutput = true;
return [createCustomDataEvent(name, "data" in event ? event.data : null)];
const value = "data" in event ? event.data : null;
const native = buildNativeRunEventFrame({
name,
value,
parentMessageId: state.messageId,
});
return [native ? native.live : createCustomDataEvent(name, value)];
}

switch (event.type) {
case "source-document":
case "source-url":
case "file":
case "file": {
state.sawVisibleOutput = true;
return [createCustomDataEvent(event.type, event)];
const native = buildNativeRunEventFrame({ name: event.type, value: event });
return [native ? native.live : createCustomDataEvent(event.type, event)];
}

case "message-start":
getMessageId(state, event);
Expand Down Expand Up @@ -892,6 +915,7 @@ function mapRuntimeStreamEventToAgUiEventsUnstamped(
payload: {
toolCallId: event.toolCallId,
toolCallName: event.toolName,
...(state.messageId ? { parentMessageId: state.messageId } : {}),
},
});
return events;
Expand Down Expand Up @@ -1013,9 +1037,9 @@ function mapRuntimeStreamEventToAgUiEventsUnstamped(
];

default:
if (typeof event.type === "string" && event.type.startsWith("data-")) {
return [createCustomDataEvent(event.type.slice(5), event.data)];
}
// The `data-` guard at the top of this function already returns for
// any event.type starting with "data-", so that case can never reach
// here -- this was a second, unreachable copy of the native routing.
return [];
}
}
Expand Down
54 changes: 54 additions & 0 deletions src/agent/ag-ui/lifecycle-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,58 @@ describe("lifecycle AG-UI adapter", () => {
},
]);
});

it("emits native frames for tool status and custom lifecycle names", () => {
const adapter = createLifecycleAgUiAdapter({ messageId: "assistant-1" });

assertEquals(
adapter.encode({
class: "telemetry",
sequence: 1,
elapsedMs: 0,
event: {
type: "tool_input_status",
toolCallId: "tool-1",
toolCallName: "create_file",
status: "pending_input",
},
}),
[{
event: "ToolCallStatusChanged",
payload: {
toolCallId: "tool-1",
status: "pending_input",
toolCallName: "create_file",
parentMessageId: "assistant-1",
},
}],
);

assertEquals(
adapter.encode({
class: "semantic",
sequence: 2,
elapsedMs: 1,
event: {
type: "custom",
name: "source-url",
data: { type: "source-url", sourceId: "web-1", url: "https://example.com/a" },
},
}),
[{
event: "UrlCited",
payload: { sourceId: "web-1", url: "https://example.com/a" },
}],
);

assertEquals(
adapter.encode({
class: "semantic",
sequence: 3,
elapsedMs: 2,
event: { type: "custom", name: "state-delta", data: { ops: [] } },
}),
[{ event: "Custom", payload: { name: "state-delta", value: { ops: [] } } }],
);
});
});
Loading
Loading