From e05ef7e3a2fa24a1ec6a3fc3787c8a1c87456ff7 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 14:56:46 +0200 Subject: [PATCH 01/16] feat(agent): add durable model call context v2 --- src/agent/ag-ui/browser-encoder.ts | 33 +++- src/agent/ag-ui/browser-response-stream.ts | 5 +- .../conversation/private-run-event.test.ts | 16 ++ src/agent/conversation/private-run-event.ts | 173 +++++++++++++++++- .../conversation/run-chunk-mirror.test.ts | 33 ++++ src/agent/conversation/run-chunk-mirror.ts | 14 +- src/agent/conversation/run-events.ts | 34 +++- .../hosted/durable-run-event-sink.test.ts | 98 +++++++++- src/agent/hosted/durable-run-event-sink.ts | 32 ++-- src/internal-agents/ag-ui-sse.test.ts | 13 +- src/internal-agents/ag-ui-sse.ts | 12 +- src/internal-agents/run-stream.ts | 21 ++- src/provider/types.ts | 2 + src/provider/veryfront-cloud/provider.test.ts | 5 + src/provider/veryfront-cloud/provider.ts | 124 ++++++++----- src/runtime/model-call-context.test.ts | 41 ++++- src/runtime/model-call-context.ts | 68 +++++++ src/runtime/runtime-bridge.test.ts | 142 +++++++++++--- src/runtime/runtime-bridge.ts | 59 +++++- 19 files changed, 798 insertions(+), 127 deletions(-) diff --git a/src/agent/ag-ui/browser-encoder.ts b/src/agent/ag-ui/browser-encoder.ts index 4bad44890b..225cf87099 100644 --- a/src/agent/ag-ui/browser-encoder.ts +++ b/src/agent/ag-ui/browser-encoder.ts @@ -77,6 +77,7 @@ export interface AgUiBrowserEncoderStateOptions { * `Date.now`. Pass null to omit the stamp. */ epochMs?: (() => number) | null; + startedMs?: number; } /** Event emitted for AG-UI browser encoded. */ @@ -95,7 +96,7 @@ export function createAgUiBrowserEncoderState( const nowMs = options.nowMs === null ? undefined : options.nowMs ?? (() => performance.now()); const epochMs = options.epochMs === null ? undefined : options.epochMs ?? (() => Date.now()); return { - ...(nowMs ? { nowMs, startedMs: nowMs() } : {}), + ...(nowMs ? { nowMs, startedMs: options.startedMs ?? nowMs() } : {}), ...(epochMs ? { epochMs } : {}), messageId: null, textOpen: false, @@ -681,10 +682,13 @@ export function mapRuntimeStreamEventToAgUiBrowserEvents( state: AgUiBrowserEncoderState, event: AgUiRuntimeStreamEvent, ): AgUiBrowserEncodedEvent[] { - return stampTiming(state, mapRuntimeStreamEventToAgUiBrowserEventsUnstamped(state, event)); + return stampAgUiBrowserEventTiming( + state, + mapRuntimeStreamEventToAgUiBrowserEventsUnstamped(state, event), + ); } -function stampTiming( +export function stampAgUiBrowserEventTiming( state: AgUiBrowserEncoderState, events: AgUiBrowserEncodedEvent[], ): AgUiBrowserEncodedEvent[] { @@ -710,7 +714,26 @@ function stampTiming( return events; } - return events.map((entry) => ({ ...entry, payload: { ...entry.payload, ...timing } })); + return events.map((entry) => { + const elapsedMs = entry.payload.elapsedMs; + const emittedAt = entry.payload.emittedAt; + return { + ...entry, + payload: { + ...entry.payload, + ...(typeof elapsedMs === "number" && Number.isFinite(elapsedMs) && elapsedMs >= 0 + ? { elapsedMs } + : timing.elapsedMs === undefined + ? {} + : { elapsedMs: timing.elapsedMs }), + ...(typeof emittedAt === "number" && Number.isInteger(emittedAt) && emittedAt >= 0 + ? { emittedAt } + : timing.emittedAt === undefined + ? {} + : { emittedAt: timing.emittedAt }), + }, + }; + }); } function mapRuntimeStreamEventToAgUiBrowserEventsUnstamped( @@ -952,7 +975,7 @@ export function finalizeAgUiBrowserEvents( state: AgUiBrowserEncoderState, response: AgentResponse | null, ): AgUiBrowserEncodedEvent[] { - return stampTiming(state, finalizeAgUiBrowserEventsUnstamped(state, response)); + return stampAgUiBrowserEventTiming(state, finalizeAgUiBrowserEventsUnstamped(state, response)); } function finalizeAgUiBrowserEventsUnstamped( diff --git a/src/agent/ag-ui/browser-response-stream.ts b/src/agent/ag-ui/browser-response-stream.ts index fc344a55b3..5b683ef7b3 100644 --- a/src/agent/ag-ui/browser-response-stream.ts +++ b/src/agent/ag-ui/browser-response-stream.ts @@ -1,5 +1,6 @@ import type { AgentResponse } from "../types.ts"; import type { AgUiSseEvent } from "./host-support.ts"; +import { createAgUiBrowserEncoderState, stampAgUiBrowserEventTiming } from "./browser-encoder.ts"; const encoder = new TextEncoder(); @@ -66,13 +67,15 @@ export function createAgUiBrowserResponseStream( return new ReadableStream({ start(controller) { + const timingState = createAgUiBrowserEncoderState(); const writeEvent = (event: AgUiSseEvent) => { if (streamClosed) { return false; } try { - controller.enqueue(formatAgUiSseEventWithId(event, nextEventId)); + const [timed] = stampAgUiBrowserEventTiming(timingState, [event]); + controller.enqueue(formatAgUiSseEventWithId(timed ?? event, nextEventId)); nextEventId += 1; return true; } catch { diff --git a/src/agent/conversation/private-run-event.test.ts b/src/agent/conversation/private-run-event.test.ts index f320df9fa3..a2edc6d675 100644 --- a/src/agent/conversation/private-run-event.test.ts +++ b/src/agent/conversation/private-run-event.test.ts @@ -18,8 +18,12 @@ describe("agent/conversation/private-run-event", () => { assertEquals( isPrivateConversationRunEvent({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "veryfront-cloud/anthropic/claude-sonnet-4-6", modelProvider: "anthropic" }, + request: { maxOutputTokens: 4096, reasoning: { enabled: true, budgetTokens: 2048 } }, messages: [], tools: [], + elapsedMs: 42, + emittedAt: 1_786_866_357_364, }), true, ); @@ -30,6 +34,18 @@ describe("agent/conversation/private-run-event", () => { { type: "AGENT_RUN_MODEL_CALL_CONTEXT" }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: {} }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], tools: {} }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], model: { id: 1 } }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + model: { id: "x", provider: "anthropic" }, + }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + request: { reasoning: { arbitrary: true } }, + }, + { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], emittedAt: 1.5 }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], contextId: "legacy" }, { type: "TEXT_MESSAGE_CONTENT", messages: [] }, ] diff --git a/src/agent/conversation/private-run-event.ts b/src/agent/conversation/private-run-event.ts index db90e68d73..5bb2bae076 100644 --- a/src/agent/conversation/private-run-event.ts +++ b/src/agent/conversation/private-run-event.ts @@ -5,6 +5,153 @@ function ownDataValue(record: object, key: string): unknown { return descriptor && "value" in descriptor ? descriptor.value : undefined; } +function isRecord(value: unknown): value is Record { + return !!value && typeof value === "object" && !Array.isArray(value); +} + +function hasOnlyKeys(value: Record, keys: readonly string[]): boolean { + return Object.keys(value).every((key) => keys.includes(key)); +} + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +function isModel(value: unknown): boolean { + return isRecord(value) && hasOnlyKeys(value, ["id", "modelProvider"]) && + typeof ownDataValue(value, "id") === "string" && ownDataValue(value, "id") !== "" && + (ownDataValue(value, "modelProvider") === undefined || + (typeof ownDataValue(value, "modelProvider") === "string" && + ownDataValue(value, "modelProvider") !== "")); +} + +function isRequest(value: unknown): boolean { + if ( + !isRecord(value) || !hasOnlyKeys(value, [ + "maxOutputTokens", + "temperature", + "topP", + "topK", + "stopSequences", + "seed", + "presencePenalty", + "frequencyPenalty", + "reasoning", + ]) + ) return false; + for ( + const key of [ + "maxOutputTokens", + "temperature", + "topP", + "topK", + "seed", + "presencePenalty", + "frequencyPenalty", + ] + ) { + const field = ownDataValue(value, key); + if (field !== undefined && !isFiniteNumber(field)) return false; + } + const maxOutputTokens = ownDataValue(value, "maxOutputTokens"); + if (typeof maxOutputTokens === "number" && maxOutputTokens < 0) return false; + const stops = ownDataValue(value, "stopSequences"); + if ( + stops !== undefined && (!Array.isArray(stops) || stops.some((item) => typeof item !== "string")) + ) { + return false; + } + const reasoning = ownDataValue(value, "reasoning"); + if (reasoning === undefined) return true; + if (!isRecord(reasoning) || !hasOnlyKeys(reasoning, ["enabled", "effort", "budgetTokens"])) { + return false; + } + const enabled = ownDataValue(reasoning, "enabled"); + const effort = ownDataValue(reasoning, "effort"); + const budget = ownDataValue(reasoning, "budgetTokens"); + return (enabled === undefined || typeof enabled === "boolean") && + (effort === undefined || ["low", "medium", "high", "max"].includes(String(effort))) && + (budget === undefined || (Number.isInteger(budget) && (budget as number) >= 0)); +} + +function isMessage(value: unknown): boolean { + if (!isRecord(value) || typeof ownDataValue(value, "role") !== "string") return false; + const role = ownDataValue(value, "role"); + const content = ownDataValue(value, "content"); + if (role === "system") { + return typeof content === "string" && + hasOnlyKeys(value, ["role", "content", "providerOptions"]) && + isPersistedProviderOptions(ownDataValue(value, "providerOptions")); + } + if (!Array.isArray(content) || !hasOnlyKeys(value, ["role", "content"])) return false; + return content.every((part) => { + if (!isRecord(part)) return false; + if (role === "user") { + return ownDataValue(part, "type") === "text" + ? hasOnlyKeys(part, ["type", "text"]) && typeof ownDataValue(part, "text") === "string" + : ["image", "file"].includes(String(ownDataValue(part, "type"))) && + hasOnlyKeys(part, ["type", "mediaType", "url", "filename"]) && + typeof ownDataValue(part, "mediaType") === "string" && + typeof ownDataValue(part, "url") === "string" && + (ownDataValue(part, "filename") === undefined || + typeof ownDataValue(part, "filename") === "string"); + } + if (role === "assistant") { + if (ownDataValue(part, "type") === "text") { + return hasOnlyKeys(part, ["type", "text"]) && + typeof ownDataValue(part, "text") === "string"; + } + return ownDataValue(part, "type") === "tool-call" && + hasOnlyKeys(part, ["type", "toolCallId", "toolName", "input", "providerExecuted"]) && + typeof ownDataValue(part, "toolCallId") === "string" && + typeof ownDataValue(part, "toolName") === "string" && Object.hasOwn(part, "input") && + (ownDataValue(part, "providerExecuted") === undefined || + typeof ownDataValue(part, "providerExecuted") === "boolean"); + } + if (role === "tool") { + const output = ownDataValue(part, "output"); + return ownDataValue(part, "type") === "tool-result" && + hasOnlyKeys(part, ["type", "toolCallId", "toolName", "output"]) && + typeof ownDataValue(part, "toolCallId") === "string" && + typeof ownDataValue(part, "toolName") === "string" && isRecord(output) && + hasOnlyKeys(output, ["type", "value"]) && ownDataValue(output, "type") === "json" && + Object.hasOwn(output, "value"); + } + return false; + }); +} + +function isPersistedProviderOptions(value: unknown): boolean { + if (value === undefined) return true; + if (!isRecord(value)) return false; + return Object.keys(value).every((key) => { + if (key === "") return false; + const bucket = ownDataValue(value, key); + if (!isRecord(bucket) || !hasOnlyKeys(bucket, ["cacheControl"])) return false; + const cacheControl = ownDataValue(bucket, "cacheControl"); + return isRecord(cacheControl) && hasOnlyKeys(cacheControl, ["type", "ttl"]) && + ownDataValue(cacheControl, "type") === "ephemeral" && + (ownDataValue(cacheControl, "ttl") === undefined || + ownDataValue(cacheControl, "ttl") === "5m" || ownDataValue(cacheControl, "ttl") === "1h"); + }); +} + +function isTool(value: unknown): boolean { + if (!isRecord(value)) return false; + if (ownDataValue(value, "type") === "function") { + return hasOnlyKeys(value, ["type", "name", "description", "inputSchema"]) && + typeof ownDataValue(value, "name") === "string" && Object.hasOwn(value, "inputSchema") && + (ownDataValue(value, "description") === undefined || + typeof ownDataValue(value, "description") === "string"); + } + return ownDataValue(value, "type") === "provider" && + hasOnlyKeys(value, ["type", "name", "id", "args"]) && + typeof ownDataValue(value, "name") === "string" && + typeof ownDataValue(value, "id") === "string" && + String(ownDataValue(value, "id")).includes(".") && + isRecord(ownDataValue(value, "args")); +} + /** Return whether an event declares the private durable run-event discriminator. */ export function hasPrivateConversationRunEventType(value: unknown): value is object { return !!value && typeof value === "object" && !Array.isArray(value) && @@ -14,13 +161,33 @@ export function hasPrivateConversationRunEventType(value: unknown): value is obj /** Return whether an event belongs to the private durable run-event sequence. */ export function isPrivateConversationRunEvent(value: unknown): boolean { if (!hasPrivateConversationRunEventType(value)) return false; - if (!Array.isArray(ownDataValue(value, "messages"))) return false; + const messages = ownDataValue(value, "messages"); + if (!Array.isArray(messages) || !messages.every(isMessage)) return false; const toolsDescriptor = Object.getOwnPropertyDescriptor(value, "tools"); if ( toolsDescriptor !== undefined && - (!("value" in toolsDescriptor) || !Array.isArray(toolsDescriptor.value)) + (!("value" in toolsDescriptor) || !Array.isArray(toolsDescriptor.value) || + !toolsDescriptor.value.every(isTool)) ) return false; - return Object.keys(value).every((key) => key === "type" || key === "messages" || key === "tools"); + const model = ownDataValue(value, "model"); + if (model !== undefined && !isModel(model)) return false; + const request = ownDataValue(value, "request"); + if (request !== undefined && !isRequest(request)) return false; + const elapsedMs = ownDataValue(value, "elapsedMs"); + if (elapsedMs !== undefined && (!isFiniteNumber(elapsedMs) || elapsedMs < 0)) return false; + const emittedAt = ownDataValue(value, "emittedAt"); + if (emittedAt !== undefined && (!Number.isInteger(emittedAt) || (emittedAt as number) < 0)) { + return false; + } + return hasOnlyKeys(value as Record, [ + "type", + "model", + "request", + "messages", + "tools", + "elapsedMs", + "emittedAt", + ]); } /** Failure to persist a required run event before its associated operation. */ diff --git a/src/agent/conversation/run-chunk-mirror.test.ts b/src/agent/conversation/run-chunk-mirror.test.ts index 76338088d3..d2c4c9739f 100644 --- a/src/agent/conversation/run-chunk-mirror.test.ts +++ b/src/agent/conversation/run-chunk-mirror.test.ts @@ -133,6 +133,39 @@ describe("agent/conversation-run-chunk-mirror", () => { mirror.dispose(); }); + it("stamps missing external checkpoint timing and preserves supplied timing", async () => { + const queueController = createQueueController(); + let now = 100; + const encoder = new ConversationRunEventEncoder({ + nowMs: () => now, + epochMs: () => 1_786_866_357_364, + }); + const mirror = createConversationRunChunkMirror({ + queueController, + encoder, + immediateFlushEventCount: 99, + flushDelayMs: 10_000, + }); + now = 142; + + await mirror.appendEvents([ + { type: "CONTEXT_COMPACTION", compactedMessageCount: 2 } as never, + { type: "TOOL_EXPOSURE_CHECKPOINT", elapsedMs: 7, emittedAt: 8 } as never, + ]); + + assertEquals( + queueController.enqueued.map((event) => { + const timed = event as { elapsedMs?: number; emittedAt?: number }; + return { elapsedMs: timed.elapsedMs, emittedAt: timed.emittedAt }; + }), + [ + { elapsedMs: 42, emittedAt: 1_786_866_357_364 }, + { elapsedMs: 7, emittedAt: 8 }, + ], + ); + mirror.dispose(); + }); + it("allows hosts to wrap chunk and external event preparation", async () => { const queueController = createQueueController(); const preparedMarkers: string[] = []; diff --git a/src/agent/conversation/run-chunk-mirror.ts b/src/agent/conversation/run-chunk-mirror.ts index 7cac549b0d..faf2b41087 100644 --- a/src/agent/conversation/run-chunk-mirror.ts +++ b/src/agent/conversation/run-chunk-mirror.ts @@ -1,4 +1,8 @@ import type { ChatMessageMetadata, ChatUiMessageChunk } from "#veryfront/chat/protocol.ts"; +import { + type AgentRunEventTimingOptions, + createAgentRunEventTimingAnchor, +} from "../../runtime/model-call-context.ts"; import { type ConversationRunEvent, ConversationRunEventEncoder } from "./run-events.ts"; import { type ConversationRunMirror, @@ -24,6 +28,7 @@ const DEFAULT_HOSTED_CHUNK_MIRROR_HIGH_BACKLOG_EVENT_COUNT = 500; /** Public API contract for conversation run chunk mirror. */ export interface ConversationRunChunkMirror { + readonly timing?: AgentRunEventTimingOptions; handleChunk(chunk: ChatUiMessageChunk): Promise; appendEvents(events: ConversationRunEvent[]): Promise; flush(options?: { @@ -165,8 +170,8 @@ export function createConversationRunChunkMirror( // headless -- a scheduled run has no client attached -- so this is the only // point that observes emission time. Callers injecting their own encoder // choose their own clock, or none. - const encoder = input.encoder ?? - new ConversationRunEventEncoder({ nowMs: () => performance.now() }); + const timing = createAgentRunEventTimingAnchor(); + const encoder = input.encoder ?? new ConversationRunEventEncoder(timing); const immediateFlushEventCount = input.immediateFlushEventCount ?? DEFAULT_IMMEDIATE_FLUSH_EVENT_COUNT; const mirror = createConversationRunMirror({ @@ -183,6 +188,7 @@ export function createConversationRunChunkMirror( }); return { + timing, async handleChunk(chunk) { if (mirror.getSnapshot().disabled) { return; @@ -206,8 +212,8 @@ export function createConversationRunChunkMirror( const normalizedEvents = await (input.prepareExternalEvents?.({ events, - defaultPrepare: () => prepareConversationRunExternalEvents(events), - }) ?? prepareConversationRunExternalEvents(events)); + defaultPrepare: () => prepareConversationRunExternalEvents(encoder.stamp(events)), + }) ?? prepareConversationRunExternalEvents(encoder.stamp(events))); await input.onExternalEventsPrepared?.({ events: normalizedEvents }); if (normalizedEvents.length === 0) { return; diff --git a/src/agent/conversation/run-events.ts b/src/agent/conversation/run-events.ts index 556653cf11..debfc8cf63 100644 --- a/src/agent/conversation/run-events.ts +++ b/src/agent/conversation/run-events.ts @@ -73,6 +73,8 @@ export interface ConversationRunEventEncoderOptions { * nothing is stamped. */ nowMs?: () => number; + epochMs?: () => number; + startedMs?: number; } export class ConversationRunEventEncoder { @@ -85,6 +87,7 @@ export class ConversationRunEventEncoder { private stepCount = 0; private readonly nowMs?: () => number; private readonly startedMs?: number; + private readonly epochMs?: () => number; // One encoder spans a whole run: it carries stepCount and the active message // across every step, so elapsed measured from here is run-relative and needs no @@ -92,8 +95,9 @@ export class ConversationRunEventEncoder { constructor(options: ConversationRunEventEncoderOptions = {}) { if (options.nowMs) { this.nowMs = options.nowMs; - this.startedMs = options.nowMs(); + this.startedMs = options.startedMs ?? options.nowMs(); } + this.epochMs = options.epochMs; } private nextStepName(): string { @@ -162,12 +166,34 @@ export class ConversationRunEventEncoder { // is treated alike -- including the ones this encoder synthesises, such as the // terminal result for a provider-executed call the provider never resolved. private stampElapsed(events: ConversationRunEvent[]): ConversationRunEvent[] { - if (!this.nowMs || this.startedMs === undefined) { + if ((!this.nowMs || this.startedMs === undefined) && !this.epochMs) { return events; } - const elapsedMs = Math.max(0, Math.round(this.nowMs() - this.startedMs)); - return events.map((event) => ({ ...event, elapsedMs })); + const elapsedMs = this.nowMs && this.startedMs !== undefined + ? Math.max(0, Math.round(this.nowMs() - this.startedMs)) + : undefined; + const emittedAt = this.epochMs ? Math.round(this.epochMs()) : undefined; + return events.map((event) => ({ + ...event, + ...(typeof event.elapsedMs === "number" && Number.isFinite(event.elapsedMs) && + event.elapsedMs >= 0 + ? { elapsedMs: event.elapsedMs } + : elapsedMs === undefined + ? {} + : { elapsedMs }), + ...(typeof event.emittedAt === "number" && Number.isInteger(event.emittedAt) && + event.emittedAt >= 0 + ? { emittedAt: event.emittedAt } + : emittedAt === undefined + ? {} + : { emittedAt }), + })); + } + + /** Stamp externally-created checkpoints against this encoder's run anchor. */ + stamp(events: ConversationRunEvent[]): ConversationRunEvent[] { + return this.stampElapsed(events); } private encodeChunk(chunk: ChatStreamEvent): ConversationRunEvent[] { diff --git a/src/agent/hosted/durable-run-event-sink.test.ts b/src/agent/hosted/durable-run-event-sink.test.ts index 62aa77ddef..e1e1f47a3f 100644 --- a/src/agent/hosted/durable-run-event-sink.test.ts +++ b/src/agent/hosted/durable-run-event-sink.test.ts @@ -2,11 +2,15 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertInstanceOf, assertRejects } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import type { ConversationRunChunkMirror } from "../conversation/run-chunk-mirror.ts"; +import { ConversationRunEventEncoder } from "../conversation/run-events.ts"; import type { ConversationRunMirrorSnapshot } from "../conversation/run-mirror.ts"; import { generateText } from "../../runtime/runtime-bridge.ts"; import { createGenerateModel } from "../../runtime/runtime-bridge.test-helpers.ts"; import { runWithRunEventSink } from "../../runtime/run-event-sink-context.ts"; -import type { AgentRunModelCallContextEvent } from "../../runtime/model-call-context.ts"; +import { + type AgentRunModelCallContextEvent, + createAgentRunEventTimingAnchor, +} from "../../runtime/model-call-context.ts"; import { getPrivateRunEventAppendRequestByteLength, MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES, @@ -66,8 +70,7 @@ function firstAppendedEvent(appended: unknown[][]): Record { function leadingNoticeText(event: Record): string { const messages = event.messages as Array> | undefined; - const parts = messages?.[0]?.content as Array> | undefined; - const text = parts?.[0]?.text; + const text = messages?.[0]?.content; if (typeof text !== "string") { throw new Error("expected a leading notice message"); } @@ -87,7 +90,28 @@ function createModelCallContextEventWithText( } describe("agent/hosted/durable-run-event-sink", () => { - it("appends and flushes one direct event without chunk metadata", async () => { + it("uses one run anchor for public and private event families", async () => { + let now = 100; + const timing = createAgentRunEventTimingAnchor({ + nowMs: () => now, + epochMs: () => 1_786_866_357_364, + }); + const target = mirror(); + const publicEncoder = new ConversationRunEventEncoder(timing); + now = 142; + publicEncoder.encode({ type: "start", messageId: "message-1" }); + const publicEvent = publicEncoder.encode({ type: "text-start", id: "text:0" })[0]; + await createDurableRunEventSink({ mirror: target.result, timing })({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + }); + const privateEvent = firstAppendedEvent(target.appended); + assertEquals(publicEvent?.elapsedMs, 42); + assertEquals(privateEvent.elapsedMs, 42); + assertEquals(publicEvent?.emittedAt, privateEvent.emittedAt); + }); + + it("appends and flushes one direct event with default producer timing", async () => { const target = mirror(); const order: string[] = []; const sink = createDurableRunEventSink({ @@ -112,7 +136,20 @@ describe("agent/hosted/durable-run-event-sink", () => { await sink(event); assertEquals(order, ["append", "flush"]); - assertEquals(target.appended, [[event]]); + const persisted = firstAppendedEvent(target.appended); + assertEquals(persisted.type, event.type); + assertEquals(persisted.messages, event.messages); + assertEquals(persisted.tools, event.tools); + assertEquals( + typeof persisted.elapsedMs === "number" && persisted.elapsedMs >= 0, + true, + "an absent timing option still stamps nonnegative elapsedMs", + ); + assertEquals( + typeof persisted.emittedAt === "number" && Number.isInteger(persisted.emittedAt), + true, + "an absent timing option still stamps an epoch timestamp", + ); for ( const field of [ "contextId", @@ -127,6 +164,29 @@ describe("agent/hosted/durable-run-event-sink", () => { } }); + it("preserves valid producer timing instead of replacing it", async () => { + const target = mirror(); + const sink = createDurableRunEventSink({ + mirror: target.result, + timing: { + nowMs: () => 900, + startedMs: 100, + epochMs: () => 1_900_000_000_000, + }, + }); + + await sink({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + elapsedMs: 42, + emittedAt: 1_786_866_357_364, + }); + + const persisted = firstAppendedEvent(target.appended); + assertEquals(persisted.elapsedMs, 42); + assertEquals(persisted.emittedAt, 1_786_866_357_364); + }); + it("persists a direct context above 2 MiB as one unchanged event", async () => { const target = mirror(); const event = createModelCallContextEventWithText( @@ -139,10 +199,22 @@ describe("agent/hosted/durable-run-event-sink", () => { }); it("accepts the exact append request byte limit and rejects one byte over", async () => { - const baseEvent = createModelCallContextEventWithText(0); + const baseEvent = { + ...createModelCallContextEventWithText(0), + model: { id: "claude-sonnet-4-6", modelProvider: "anthropic" }, + request: { maxOutputTokens: 4096 }, + elapsedMs: 42, + emittedAt: 1_786_866_357_364, + }; const exactTextLength = MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES - getPrivateRunEventAppendRequestByteLength(baseEvent); - const exactEvent = createModelCallContextEventWithText(exactTextLength); + const exactEvent = { + ...baseEvent, + messages: [{ + role: "user" as const, + content: [{ type: "text" as const, text: "x".repeat(exactTextLength) }], + }], + }; assertEquals( getPrivateRunEventAppendRequestByteLength(exactEvent), MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES, @@ -153,7 +225,13 @@ describe("agent/hosted/durable-run-event-sink", () => { assertEquals(exactTarget.appended, [[exactEvent]]); const oversizedTarget = mirror(); - const oversizedEvent = createModelCallContextEventWithText(exactTextLength + 1); + const oversizedEvent = { + ...baseEvent, + messages: [{ + role: "user" as const, + content: [{ type: "text" as const, text: "x".repeat(exactTextLength + 1) }], + }], + }; await assertRejects( async () => await createDurableRunEventSink({ mirror: oversizedTarget.result })(oversizedEvent), @@ -168,6 +246,10 @@ describe("agent/hosted/durable-run-event-sink", () => { "a truncated record is persisted for audit before the gate throws", ); const persisted = firstAppendedEvent(oversizedTarget.appended); + assertEquals(persisted.model, baseEvent.model); + assertEquals(persisted.request, baseEvent.request); + assertEquals(persisted.elapsedMs, 42); + assertEquals(persisted.emittedAt, 1_786_866_357_364); assertEquals( getPrivateRunEventAppendRequestByteLength(persisted) <= MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES, diff --git a/src/agent/hosted/durable-run-event-sink.ts b/src/agent/hosted/durable-run-event-sink.ts index 6b3c91485d..18af3013b8 100644 --- a/src/agent/hosted/durable-run-event-sink.ts +++ b/src/agent/hosted/durable-run-event-sink.ts @@ -5,7 +5,11 @@ import { MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES, } from "../conversation/run-event-limits.ts"; import { DurableRunEventPersistenceError } from "../conversation/private-run-event.ts"; -import type { AgentRunEventSink } from "../../runtime/model-call-context.ts"; +import { + type AgentRunEventSink, + type AgentRunEventTimingOptions, + createTimedAgentRunEventSink, +} from "../../runtime/model-call-context.ts"; import { agentLogger } from "#veryfront/utils"; const DEFAULT_DURABLE_RUN_EVENT_PERSISTENCE_TIMEOUT_MS = 30_000; @@ -94,16 +98,13 @@ function buildTruncationNotice(input: { }): unknown { return { role: "system", - content: [{ - type: "text", - text: - `${OMITTED_MESSAGE_NOTICE} Original ${ - formatMebibytes(input.originalByteLength) - } exceeded the ${ - formatMebibytes(MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES) - } append limit; ${input.omittedMessageCount} message(s) omitted. The model call was not ` + - `dispatched — this record is an excerpt, not the context that was sent.`, - }], + content: + `${OMITTED_MESSAGE_NOTICE} Original ${ + formatMebibytes(input.originalByteLength) + } exceeded the ${ + formatMebibytes(MAX_CONVERSATION_RUN_EVENT_APPEND_REQUEST_BYTES) + } append limit; ${input.omittedMessageCount} message(s) omitted. The model call was not ` + + `dispatched — this record is an excerpt, not the context that was sent.`, }; } @@ -136,8 +137,12 @@ function truncatePrivateRunEventToLimit( keepTools: boolean, ): Record => ({ type: event.type, + ...(event.model === undefined ? {} : { model: event.model }), + ...(event.request === undefined ? {} : { request: event.request }), messages: [buildTruncationNotice({ originalByteLength, omittedMessageCount }), ...kept], ...(tools === undefined ? {} : { tools: keepTools ? tools : [] }), + ...(event.elapsedMs === undefined ? {} : { elapsedMs: event.elapsedMs }), + ...(event.emittedAt === undefined ? {} : { emittedAt: event.emittedAt }), }); const fits = (candidate: Record): boolean => @@ -263,8 +268,9 @@ export function createDurableRunEventSink(input: { mirror: ConversationRunChunkMirror; abortSignal?: AbortSignal; timeoutMs?: number; + timing?: AgentRunEventTimingOptions; }): AgentRunEventSink { - return async (event) => { + return createTimedAgentRunEventSink(async (event) => { let oversize: ResolvedRunEvent["oversize"]; try { assertEnabled(input.mirror.getSnapshot()); @@ -318,5 +324,5 @@ export function createDurableRunEventSink(input: { ); throw buildOversizeError(oversize); } - }; + }, input.timing ?? input.mirror.timing); } diff --git a/src/internal-agents/ag-ui-sse.test.ts b/src/internal-agents/ag-ui-sse.test.ts index 563ff18ef1..910c919333 100644 --- a/src/internal-agents/ag-ui-sse.test.ts +++ b/src/internal-agents/ag-ui-sse.test.ts @@ -368,11 +368,12 @@ describe("internal-agents/ag-ui-sse", () => { runId: "run_1", threadId: "thread-1", agentId: "assistant-1", + emittedAt: 8, }); assertEquals( new TextDecoder().decode(payload), - 'event: RunStarted\ndata: {"runId":"run_1","threadId":"thread-1","agentId":"assistant-1"}\n\n', + 'event: RunStarted\ndata: {"runId":"run_1","threadId":"thread-1","agentId":"assistant-1","emittedAt":8}\n\n', ); }); @@ -425,11 +426,11 @@ describe("internal-agents/ag-ui-sse", () => { }); it("accepts extension event tokens without weakening SSE framing", () => { - const payload = formatAgUiEvent("Done.custom-v1", { ok: true }); + const payload = formatAgUiEvent("Done.custom-v1", { ok: true, emittedAt: 8 }); assertEquals( new TextDecoder().decode(payload), - 'event: Done.custom-v1\ndata: {"ok":true}\n\n', + 'event: Done.custom-v1\ndata: {"ok":true,"emittedAt":8}\n\n', ); }); @@ -482,11 +483,12 @@ describe("internal-agents/ag-ui-sse", () => { usageCaptureStatus: "complete", finishReason: "stop", }, + emittedAt: 8, }); assertEquals( new TextDecoder().decode(payload), - 'event: RunFinished\ndata: {"metadata":{"provider":"veryfront-cloud","model":"anthropic/claude-sonnet-4-6","inputTokens":12,"outputTokens":8,"totalTokens":20,"cachedInputTokens":4,"cacheCreationInputTokens":6,"cacheReadInputTokens":4,"reasoningTokens":2,"billableInputTokens":10,"billableOutputTokens":7,"costUsd":0.002,"providerInputCostUsd":0.001,"providerOutputCostUsd":0.0005,"providerCostUsd":0.0015,"veryfrontInputChargeUsd":0.0012,"veryfrontOutputChargeUsd":0.0007,"veryfrontChargeUsd":0.0019,"veryfrontBilledUsd":0.002,"costCredits":2,"costSource":"gateway","billingMode":"deferred","usageCaptureStatus":"complete","finishReason":"stop"}}\n\n', + 'event: RunFinished\ndata: {"metadata":{"provider":"veryfront-cloud","model":"anthropic/claude-sonnet-4-6","inputTokens":12,"outputTokens":8,"totalTokens":20,"cachedInputTokens":4,"cacheCreationInputTokens":6,"cacheReadInputTokens":4,"reasoningTokens":2,"billableInputTokens":10,"billableOutputTokens":7,"costUsd":0.002,"providerInputCostUsd":0.001,"providerOutputCostUsd":0.0005,"providerCostUsd":0.0015,"veryfrontInputChargeUsd":0.0012,"veryfrontOutputChargeUsd":0.0007,"veryfrontChargeUsd":0.0019,"veryfrontBilledUsd":0.002,"costCredits":2,"costSource":"gateway","billingMode":"deferred","usageCaptureStatus":"complete","finishReason":"stop"},"emittedAt":8}\n\n', ); }); @@ -495,11 +497,12 @@ describe("internal-agents/ag-ui-sse", () => { messageId: "assistant-1", contentId: "block-1", delta: "hello", + emittedAt: 8, }); assertEquals( new TextDecoder().decode(payload), - 'event: TextMessageContent\ndata: {"messageId":"assistant-1","contentId":"block-1","delta":"hello"}\n\n', + 'event: TextMessageContent\ndata: {"messageId":"assistant-1","contentId":"block-1","delta":"hello","emittedAt":8}\n\n', ); }); diff --git a/src/internal-agents/ag-ui-sse.ts b/src/internal-agents/ag-ui-sse.ts index 2fddf648df..e98a417bd9 100644 --- a/src/internal-agents/ag-ui-sse.ts +++ b/src/internal-agents/ag-ui-sse.ts @@ -167,7 +167,17 @@ export function formatAgUiEvent(event: string, payload: Record) const schemas = resolveAgUiEventPayloadSchemas(); const schema = schemas[event as AgUiEventName]; - const validatedPayload = schema ? schema.parse(payload) : payload; + const { elapsedMs, emittedAt, ...rest } = payload; + const timedPayload = { + ...rest, + ...(typeof elapsedMs === "number" && Number.isFinite(elapsedMs) && elapsedMs >= 0 + ? { elapsedMs } + : {}), + emittedAt: typeof emittedAt === "number" && Number.isInteger(emittedAt) && emittedAt >= 0 + ? emittedAt + : Date.now(), + }; + const validatedPayload = schema ? schema.parse(timedPayload) : timedPayload; return encoder.encode(`event: ${event}\ndata: ${JSON.stringify(validatedPayload)}\n\n`); } diff --git a/src/internal-agents/run-stream.ts b/src/internal-agents/run-stream.ts index db1c28322e..68695b6436 100644 --- a/src/internal-agents/run-stream.ts +++ b/src/internal-agents/run-stream.ts @@ -60,6 +60,11 @@ import { parseSseJsonEvents, } from "./ag-ui-sse.ts"; import type { AgentRunEvent, AgentRunEventSink } from "#veryfront/runtime/model-call-context.ts"; +import { + createAgentRunEventTimingAnchor, + createTimedAgentRunEventSink, +} from "#veryfront/runtime/model-call-context.ts"; +import { stampAgUiBrowserEventTiming } from "#veryfront/agent/ag-ui/browser-encoder.ts"; import { runWithMandatoryRunEventSink } from "#veryfront/runtime/run-event-sink-context.ts"; import { AgentRunCancelledError, type AgentRunSessionManager } from "./session-manager.ts"; import { composeInternalAgentRunSystemPrompt } from "./run-system-prompt.ts"; @@ -664,17 +669,19 @@ function compactRuntimeMessagesForStream( * awaited, before there is a controller to enqueue into, so events raised * before `attach` are buffered and replayed once the stream opens. */ -function createModelCallContextRelay(): { +function createModelCallContextRelay( + timing: Parameters[1], +): { sink: AgentRunEventSink; attach: (emit: (event: AgentRunEvent) => void) => void; } { const buffered: AgentRunEvent[] = []; let emit: ((event: AgentRunEvent) => void) | undefined; return { - sink: (event) => { + sink: createTimedAgentRunEventSink((event) => { if (emit) emit(event); else buffered.push(event); - }, + }, timing), attach: (next) => { emit = next; for (const event of buffered.splice(0)) next(event); @@ -703,7 +710,8 @@ export async function createRuntimeAgentStreamResponse( let completedResponse: AgentResponse | null = null; let runtimeStream: ReadableStream; let closeSandbox = createIdempotentAsyncCleanup(); - const modelCallContextRelay = createModelCallContextRelay(); + const timing = createAgentRunEventTimingAnchor(); + const modelCallContextRelay = createModelCallContextRelay(timing); try { const forwardedAllowedRemoteToolNames = getAllowedRemoteToolNames(input.forwardedProps); const sourceAllowedRemoteToolNames = getAgentAllowedRemoteToolNames(agent); @@ -923,7 +931,7 @@ export async function createRuntimeAgentStreamResponse( }), ); addSpanEvent(runSpan, "agent.run.started"); - const state = createStreamTransformState(); + const state = createStreamTransformState(timing); const reader = runtimeStream.getReader(); const decoder = new TextDecoder(); let remainder = ""; @@ -942,7 +950,8 @@ export async function createRuntimeAgentStreamResponse( }; const enqueueIfAttached = (event: string, payload: Record) => { - const encodedEvent = formatAgUiEvent(event, payload); + const [timed] = stampAgUiBrowserEventTiming(state, [{ event, payload }]); + const encodedEvent = formatAgUiEvent(event, timed?.payload ?? payload); if (!clientAttached) { return; } diff --git a/src/provider/types.ts b/src/provider/types.ts index 1eb696b292..2736a7d536 100644 --- a/src/provider/types.ts +++ b/src/provider/types.ts @@ -2,6 +2,8 @@ export interface RuntimeMetadata { readonly specificationVersion?: string; readonly provider?: string; readonly modelId?: string; + /** Canonical underlying model provider when a gateway runtime masks it. */ + readonly modelProvider?: string; readonly [key: string]: unknown; } diff --git a/src/provider/veryfront-cloud/provider.test.ts b/src/provider/veryfront-cloud/provider.test.ts index 76c282a178..68023f2d67 100644 --- a/src/provider/veryfront-cloud/provider.test.ts +++ b/src/provider/veryfront-cloud/provider.test.ts @@ -58,6 +58,7 @@ describe("provider/veryfront-cloud", () => { assertEquals(typeof model.doGenerate, "function"); assertEquals(typeof model.doStream, "function"); assertEquals(model._generateViaStream, true); + assertEquals(model.modelProvider, "openai"); }); it("routes agent.generate through the streaming Veryfront Cloud gateway path", async () => { @@ -200,6 +201,7 @@ describe("provider/veryfront-cloud", () => { assertEquals(typeof model.doGenerate, "function"); assertEquals(typeof model.doStream, "function"); assertEquals(model._generateViaStream, true); + assertEquals(model.modelProvider, "moonshotai"); }); it("resolves veryfront-cloud mistral models without project ext-llm-openai installed", () => { @@ -213,6 +215,7 @@ describe("provider/veryfront-cloud", () => { assertEquals(typeof model.doGenerate, "function"); assertEquals(typeof model.doStream, "function"); assertEquals(model._generateViaStream, true); + assertEquals(model.modelProvider, "mistral"); }); it("rejects unsupported pre-prefixed veryfront-cloud Mistral models", () => { @@ -241,6 +244,7 @@ describe("provider/veryfront-cloud", () => { assertEquals(typeof model.doGenerate, "function"); assertEquals(typeof model.doStream, "function"); assertEquals(model._generateViaStream, true); + assertEquals(model.modelProvider, "anthropic"); }); it("resolves veryfront-cloud google models without project ext-llm-google installed", () => { @@ -254,6 +258,7 @@ describe("provider/veryfront-cloud", () => { assertEquals(typeof model.doGenerate, "function"); assertEquals(typeof model.doStream, "function"); assertEquals(model._generateViaStream, true); + assertEquals(model.modelProvider, "google"); }); it("resolves direct anthropic models through the built-in provider", () => { diff --git a/src/provider/veryfront-cloud/provider.ts b/src/provider/veryfront-cloud/provider.ts index 29133a4ffc..9cdb1bc8fe 100644 --- a/src/provider/veryfront-cloud/provider.ts +++ b/src/provider/veryfront-cloud/provider.ts @@ -13,8 +13,14 @@ import { } from "./openai.ts"; import { resolveVeryfrontCloudModelThinking } from "./model-catalog.ts"; -function preferStreamedGenerate(model: ModelRuntime): ModelRuntime { - return Object.assign(model, { _generateViaStream: true as const }); +function wrapVeryfrontCloudModel( + model: ModelRuntime, + modelProvider: string, +): ModelRuntime { + return Object.create(model, { + _generateViaStream: { enumerable: true, value: true }, + modelProvider: { enumerable: true, value: modelProvider }, + }); } function shouldUseOpenAIResponsesRuntime(upstreamModelId: string): boolean { @@ -32,13 +38,16 @@ export function createVeryfrontCloudModel(modelId: string): ModelRuntime { case "anthropic": { const anthropic = registry.get("anthropic"); if (anthropic) { - return preferStreamedGenerate(anthropic.createModel(upstreamModelId, { - credential: apiToken, - authToken: apiToken, - baseURL, - name: "veryfront-cloud", - fetch, - })); + return wrapVeryfrontCloudModel( + anthropic.createModel(upstreamModelId, { + credential: apiToken, + authToken: apiToken, + baseURL, + name: "veryfront-cloud", + fetch, + }), + provider, + ); } break; } @@ -46,12 +55,15 @@ export function createVeryfrontCloudModel(modelId: string): ModelRuntime { case "google": { const google = registry.get("google"); if (google) { - return preferStreamedGenerate(google.createModel(upstreamModelId, { - credential: apiToken, - baseURL, - name: "veryfront-cloud", - fetch, - })); + return wrapVeryfrontCloudModel( + google.createModel(upstreamModelId, { + credential: apiToken, + baseURL, + name: "veryfront-cloud", + fetch, + }), + provider, + ); } break; } @@ -60,54 +72,72 @@ export function createVeryfrontCloudModel(modelId: string): ModelRuntime { const openai = registry.get("openai"); if (shouldUseOpenAIResponsesRuntime(upstreamModelId)) { if (openai?.createResponses) { - return preferStreamedGenerate(openai.createResponses(upstreamModelId, { + return wrapVeryfrontCloudModel( + openai.createResponses(upstreamModelId, { + credential: apiToken, + baseURL, + name: "veryfront-cloud", + providerName: "veryfront-cloud", + fetch, + }), + provider, + ); + } + return wrapVeryfrontCloudModel( + createVeryfrontCloudOpenAIResponsesModel(upstreamModelId, { + apiToken, + baseURL, + fetch, + }), + provider, + ); + } + + if (openai) { + return wrapVeryfrontCloudModel( + openai.createModel(upstreamModelId, { credential: apiToken, baseURL, name: "veryfront-cloud", providerName: "veryfront-cloud", fetch, - })); - } - return preferStreamedGenerate(createVeryfrontCloudOpenAIResponsesModel(upstreamModelId, { - apiToken, - baseURL, - fetch, - })); + }), + provider, + ); } - - if (openai) { - return preferStreamedGenerate(openai.createModel(upstreamModelId, { - credential: apiToken, + return wrapVeryfrontCloudModel( + createVeryfrontCloudOpenAIModel(upstreamModelId, { + apiToken, baseURL, - name: "veryfront-cloud", - providerName: "veryfront-cloud", fetch, - })); - } - return preferStreamedGenerate(createVeryfrontCloudOpenAIModel(upstreamModelId, { - apiToken, - baseURL, - fetch, - })); + }), + provider, + ); } case "mistral": case "moonshotai": { const openai = registry.get("openai"); if (openai) { - return preferStreamedGenerate(openai.createModel(upstreamModelId, { - credential: apiToken, + return wrapVeryfrontCloudModel( + openai.createModel(upstreamModelId, { + credential: apiToken, + baseURL, + name: "veryfront-cloud", + providerName: "openai-compatible", + fetch, + }), + provider, + ); + } + return wrapVeryfrontCloudModel( + createVeryfrontCloudOpenAIModel(upstreamModelId, { + apiToken, baseURL, - name: "veryfront-cloud", - providerName: "openai-compatible", fetch, - })); - } - return preferStreamedGenerate(createVeryfrontCloudOpenAIModel(upstreamModelId, { - apiToken, - baseURL, - fetch, - })); + }), + provider, + ); } default: { diff --git a/src/runtime/model-call-context.test.ts b/src/runtime/model-call-context.test.ts index bd3746472a..7db907209d 100644 --- a/src/runtime/model-call-context.test.ts +++ b/src/runtime/model-call-context.test.ts @@ -5,6 +5,7 @@ import type { ModelCallMessage, ModelCallTool, } from "./model-call-context.ts"; +import { createTimedAgentRunEventSink } from "./model-call-context.ts"; describe("model-call-context", () => { it("describes only the direct provider-agnostic event", () => { @@ -25,10 +26,18 @@ describe("model-call-context", () => { }]; const event: AgentRunModelCallContextEvent = { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "anthropic/claude-sonnet-4-6", modelProvider: "anthropic" }, + request: { maxOutputTokens: 4096, reasoning: { enabled: true, budgetTokens: 2048 } }, messages, tools, }; - assertEquals(event, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages, tools }); + assertEquals(event, { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "anthropic/claude-sonnet-4-6", modelProvider: "anthropic" }, + request: { maxOutputTokens: 4096, reasoning: { enabled: true, budgetTokens: 2048 } }, + messages, + tools, + }); const eventWithExtraField: AgentRunModelCallContextEvent = { type: "AGENT_RUN_MODEL_CALL_CONTEXT", @@ -47,4 +56,34 @@ describe("model-call-context", () => { }; assertEquals(providerPrivateReasoning.role, "assistant"); }); + + it("rounds generated timing and preserves valid producer timing", () => { + const events: AgentRunModelCallContextEvent[] = []; + let now = 100; + const sink = createTimedAgentRunEventSink( + (event) => { + events.push(event); + }, + { nowMs: () => now, epochMs: () => 1_786_866_357_364.4, startedMs: 100 }, + ); + now = 142.6; + sink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }); + sink({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + elapsedMs: 7, + emittedAt: 8, + }); + sink({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + elapsedMs: -1, + emittedAt: 1.5, + }); + assertEquals(events.map(({ elapsedMs, emittedAt }) => ({ elapsedMs, emittedAt })), [ + { elapsedMs: 43, emittedAt: 1_786_866_357_364 }, + { elapsedMs: 7, emittedAt: 8 }, + { elapsedMs: 43, emittedAt: 1_786_866_357_364 }, + ]); + }); }); diff --git a/src/runtime/model-call-context.ts b/src/runtime/model-call-context.ts index c0a7473261..8b78bd7c32 100644 --- a/src/runtime/model-call-context.ts +++ b/src/runtime/model-call-context.ts @@ -46,6 +46,29 @@ export type ModelCallTool = args: Record; }; +/** Resolved model identity for one dispatched model call. */ +export interface ModelCallModel { + id: string; + modelProvider?: string; +} + +/** Provider-neutral generation controls that materially affect one model call. */ +export interface ModelCallRequest { + maxOutputTokens?: number; + temperature?: number; + topP?: number; + topK?: number; + stopSequences?: string[]; + seed?: number; + presencePenalty?: number; + frequencyPenalty?: number; + reasoning?: { + enabled?: boolean; + effort?: "low" | "medium" | "high" | "max"; + budgetTokens?: number; + }; +} + /** * Provider-agnostic input persisted before one model dispatch. System-message * provider options contain only validated prompt-cache metadata. Other @@ -53,8 +76,12 @@ export type ModelCallTool = */ export type AgentRunModelCallContextEvent = { type: "AGENT_RUN_MODEL_CALL_CONTEXT"; + model?: ModelCallModel; + request?: ModelCallRequest; messages: ModelCallMessage[]; tools?: ModelCallTool[]; + elapsedMs?: number; + emittedAt?: number; }; /** Event produced by an agent run runtime boundary. */ @@ -62,3 +89,44 @@ export type AgentRunEvent = AgentRunModelCallContextEvent; /** Receives events produced within one scoped agent run execution. */ export type AgentRunEventSink = (event: AgentRunEvent) => void | Promise; + +/** Shared run clock used by public and private event producers. */ +export interface AgentRunEventTimingOptions { + nowMs?: () => number; + epochMs?: () => number; + startedMs?: number; +} + +/** Create one timing anchor for every event family belonging to a run. */ +export function createAgentRunEventTimingAnchor( + options: Omit = {}, +): AgentRunEventTimingOptions { + const nowMs = options.nowMs ?? (() => performance.now()); + return { + nowMs, + epochMs: options.epochMs ?? (() => Date.now()), + startedMs: nowMs(), + }; +} + +/** Stamp producer timing at the persistence boundary. */ +export function createTimedAgentRunEventSink( + sink: AgentRunEventSink, + options: AgentRunEventTimingOptions = {}, +): AgentRunEventSink { + const nowMs = options.nowMs ?? (() => performance.now()); + const epochMs = options.epochMs ?? (() => Date.now()); + const startedMs = options.startedMs ?? nowMs(); + return (event) => + sink({ + ...event, + elapsedMs: typeof event.elapsedMs === "number" && Number.isFinite(event.elapsedMs) && + event.elapsedMs >= 0 + ? event.elapsedMs + : Math.max(0, Math.round(nowMs() - startedMs)), + emittedAt: typeof event.emittedAt === "number" && Number.isInteger(event.emittedAt) && + event.emittedAt >= 0 + ? event.emittedAt + : Math.round(epochMs()), + }); +} diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index 17e8badb95..6325a076bb 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -85,8 +85,8 @@ describe("runtime-bridge", () => { (event) => { recorded = event; }, - () => - generateText({ + async () => + await generateText({ model, system: [{ role: "system", content: "Shared prompt", providerOptions }], messages: [{ role: "user", content: "Hello" }], @@ -99,6 +99,7 @@ describe("runtime-bridge", () => { ]); assertEquals(recorded, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "test/sanitized-context", modelProvider: "test" }, messages: [ { role: "system", @@ -116,6 +117,42 @@ describe("runtime-bridge", () => { assertEquals(JSON.stringify(recorded).includes(sensitiveValue), false); }); + it("drops empty provider keys from persisted model call context", async () => { + let recorded: AgentRunEvent | undefined; + const model = createGenerateModel("test", "test/empty-provider-key", async () => ({ + content: [{ type: "text", text: "done" }], + finishReason: "stop", + usage: {}, + })); + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => + generateText({ + model, + system: [{ + role: "system", + content: "Shared prompt", + providerOptions: { + "": { cacheControl: { type: "ephemeral" } }, + anthropic: { cacheControl: { type: "ephemeral" } }, + }, + }], + messages: [{ role: "user", content: "Hello" }], + }), + ); + + assertEquals(recorded?.messages[0], { + role: "system", + content: "Shared prompt", + providerOptions: { + anthropic: { cacheControl: { type: "ephemeral" } }, + }, + }); + }); + it("rejects accessor-backed system cache metadata without invoking it", async () => { let accessorCalls = 0; let dispatches = 0; @@ -219,6 +256,7 @@ describe("runtime-bridge", () => { assertEquals(event.tools, options.tools); assertEquals(recorded, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "test/model-call-context", modelProvider: "test" }, messages: options.prompt, tools: options.tools, }); @@ -281,6 +319,8 @@ describe("runtime-bridge", () => { assertEquals(order, ["persist", "dispatch"]); assertEquals(recorded, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "test/model-call-context", modelProvider: "test" }, + request: { temperature: 0.7 }, messages: [ { role: "system", content: "System instructions" }, { role: "user", content: [{ type: "text", text: "Load the skill" }] }, @@ -356,6 +396,7 @@ describe("runtime-bridge", () => { assertEquals(recorded, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "test/model-call-provider-redaction", modelProvider: "test" }, messages: [ { role: "system", @@ -446,6 +487,7 @@ describe("runtime-bridge", () => { assertEquals(contexts, [ { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "test/evolving-skill-context", modelProvider: "test" }, messages: [ { role: "system", content: system }, { role: "user", content: [{ type: "text", text: "Review this change." }] }, @@ -454,6 +496,7 @@ describe("runtime-bridge", () => { }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + model: { id: "test/evolving-skill-context", modelProvider: "test" }, messages: [ { role: "system", content: system }, { role: "user", content: [{ type: "text", text: "Review this change." }] }, @@ -585,7 +628,7 @@ describe("runtime-bridge", () => { assertEquals(order, ["mandatory", "public", "dispatch"]); }); - it("delivers a successful sink clone and sanitizes another clone failure", async () => { + it("fails closed when the mandatory context cannot be cloned", async () => { const sensitiveFailureClass = "CUSTOMER_SECRET_FAILURE_CLASS"; const cloneError = new Error("clone failed"); cloneError.name = sensitiveFailureClass; @@ -623,40 +666,89 @@ describe("runtime-bridge", () => { }); try { - await runWithMandatoryRunEventSink( - () => { - mandatoryCalls += 1; - }, - () => - runWithRunEventSink( - (event) => { - publicEvent = event; + await assertRejects( + async () => + await runWithMandatoryRunEventSink( + () => { + mandatoryCalls += 1; }, () => - generateText({ - model, - messages: [{ - role: "assistant", - content: [{ - type: "tool-call", - toolCallId: "call-1", - toolName: "stateful", - input: statefulInput, - }], - }, { role: "user", content: "Continue" }], - }), + runWithRunEventSink( + (event) => { + publicEvent = event; + }, + () => + generateText({ + model, + messages: [{ + role: "assistant", + content: [{ + type: "tool-call", + toolCallId: "call-1", + toolName: "stateful", + input: statefulInput, + }], + }, { role: "user", content: "Continue" }], + }), + ), ), + TypeError, + "Mandatory model call context event is not cloneable", ); } finally { if (recorder && originalRecordError) recorder.recordError = originalRecordError; } assertEquals(mandatoryCalls, 0); - assertEquals(publicEvent?.type, "AGENT_RUN_MODEL_CALL_CONTEXT"); - assertEquals(dispatches, 1); + assertEquals(publicEvent, undefined); + assertEquals(dispatches, 0); assertEquals(failureClasses, ["unknown"]); }); + it("persists canonical cloud providers and explicitly projected reasoning", async () => { + for ( + const [modelId, modelProvider] of [ + ["veryfront-cloud/anthropic/claude-sonnet-4-6", "anthropic"], + ["veryfront-cloud/openai/gpt-5.4", "openai"], + ["veryfront-cloud/google/gemini-3.1-pro-preview", "google"], + ["veryfront-cloud/mistral/mistral-large-2512", "mistral"], + ] as const + ) { + let recorded: AgentRunEvent | undefined; + const bareModelId = modelId.split("/").at(-1)!; + const model = { + ...createGenerateModel("veryfront-cloud", bareModelId, async () => ({ + content: [], + finishReason: "stop", + usage: {}, + })), + modelProvider, + }; + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => + generateText({ + model, + messages: [{ role: "user", content: "Hello" }], + reasoning: { + enabled: true, + effort: "high", + budgetTokens: 2048, + ignored: "private-runtime-detail", + } as never, + }), + ); + assertEquals(recorded?.model, { id: bareModelId, modelProvider }); + assertEquals(recorded?.request?.reasoning, { + enabled: true, + effort: "high", + budgetTokens: 2048, + }); + } + }); + it("calls a sink shared by both lanes only once", async () => { let calls = 0; const sink = () => { diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index da6205b203..ebf78ea46b 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -25,6 +25,7 @@ import type { ChatSystemMessage } from "#veryfront/chat/types.ts"; import type { AgentRunModelCallContextEvent, ModelCallMessage, + ModelCallRequest, ModelCallTool, } from "./model-call-context.ts"; import { getActiveRunEventSinks } from "./run-event-sink-context.ts"; @@ -328,7 +329,7 @@ function sanitizePersistedProviderOptions( const sanitized: Record = {}; let retained = false; for (const key of keys) { - if (typeof key !== "string") { + if (typeof key !== "string" || key.length === 0) { continue; } const providerBucket = readOwnEnumerableDataDescriptor(value, key)?.value; @@ -626,12 +627,59 @@ function buildDirectModelOptions( }; } -async function emitModelCallContextEvent(directOptions: DirectModelOptions): Promise { +function buildModelCallRequest(options: DirectTextOptions): ModelCallRequest | undefined { + const reasoning = options.reasoning; + const request: ModelCallRequest = { + ...(options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {}), + ...(options.temperature !== undefined ? { temperature: options.temperature } : {}), + ...(options.topP !== undefined ? { topP: options.topP } : {}), + ...(options.topK !== undefined ? { topK: options.topK } : {}), + ...(options.stopSequences !== undefined ? { stopSequences: [...options.stopSequences] } : {}), + ...(options.seed !== undefined ? { seed: options.seed } : {}), + ...(options.presencePenalty !== undefined ? { presencePenalty: options.presencePenalty } : {}), + ...(options.frequencyPenalty !== undefined + ? { frequencyPenalty: options.frequencyPenalty } + : {}), + ...(reasoning + ? { + reasoning: { + ...(reasoning.enabled !== undefined ? { enabled: reasoning.enabled } : {}), + ...(reasoning.effort !== undefined ? { effort: reasoning.effort } : {}), + ...(reasoning.budgetTokens !== undefined ? { budgetTokens: reasoning.budgetTokens } : {}), + }, + } + : {}), + }; + return Object.keys(request).length > 0 ? request : undefined; +} + +function resolveModelProvider(model: ModelRuntime): string | undefined { + if (typeof model.modelProvider === "string" && model.modelProvider !== "") { + return model.modelProvider; + } + return model.provider === "veryfront-cloud" ? undefined : model.provider; +} + +async function emitModelCallContextEvent( + options: DirectTextOptions, + directOptions: DirectModelOptions, +): Promise { const sinks = getActiveRunEventSinks(); if (!sinks.mandatory && !sinks.public) return; const event: AgentRunModelCallContextEvent = { type: "AGENT_RUN_MODEL_CALL_CONTEXT", + ...(options.model.modelId + ? { + model: { + id: options.model.modelId, + ...(resolveModelProvider(options.model) + ? { modelProvider: resolveModelProvider(options.model) } + : {}), + }, + } + : {}), + ...(buildModelCallRequest(options) ? { request: buildModelCallRequest(options) } : {}), messages: sanitizeModelCallContextMessages(directOptions.prompt), ...(directOptions.tools ? { tools: directOptions.tools } : {}), }; @@ -654,6 +702,9 @@ async function emitModelCallContextEvent(directOptions: DirectModelOptions): Pro } }; const mandatoryEvent = sinks.mandatory ? cloneEvent() : undefined; + if (sinks.mandatory && !mandatoryEvent) { + throw new TypeError("Mandatory model call context event is not cloneable"); + } const publicEvent = sinks.public && sinks.public !== sinks.mandatory ? cloneEvent() : undefined; if (sinks.mandatory && mandatoryEvent) { await sinks.mandatory(mandatoryEvent); @@ -1042,7 +1093,7 @@ async function* textDeltasFromStream(stream: ReadableStream): AsyncIter export function generateText(options: GenerateTextOptions): PromiseLike { return resolveDirectTools(options.tools).then(async (tools) => { const directOptions = buildDirectModelOptions(options, tools); - await emitModelCallContextEvent(directOptions); + await emitModelCallContextEvent(options, directOptions); if (shouldGenerateViaStream(options.model)) { return options.model.doStream(directOptions).then(({ stream }) => buildGenerateResultFromStream(stream) @@ -1056,7 +1107,7 @@ export function generateText(options: GenerateTextOptions): PromiseLike { const directOptions = buildDirectModelOptions(options, tools); - await emitModelCallContextEvent(directOptions); + await emitModelCallContextEvent(options, directOptions); return options.model.doStream(directOptions); }); // Guard against an unhandled rejection when a branch is consumed lazily (or a From 84785c1b712c47f539e5faa148a1e76095e29a24 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 15:11:37 +0200 Subject: [PATCH 02/16] fix(agent): persist dispatched request controls --- .../hosted/durable-run-event-sink.test.ts | 15 +++++++++++++-- src/runtime/runtime-bridge.test.ts | 1 + src/runtime/runtime-bridge.ts | 19 ++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/agent/hosted/durable-run-event-sink.test.ts b/src/agent/hosted/durable-run-event-sink.test.ts index e1e1f47a3f..c2811fbb7c 100644 --- a/src/agent/hosted/durable-run-event-sink.test.ts +++ b/src/agent/hosted/durable-run-event-sink.test.ts @@ -193,9 +193,20 @@ describe("agent/hosted/durable-run-event-sink", () => { MAX_CONVERSATION_RUN_EVENT_PAYLOAD_BYTES + 1, ); - await createDurableRunEventSink({ mirror: target.result })(event); + await createDurableRunEventSink({ + mirror: target.result, + timing: { + nowMs: () => 100, + startedMs: 100, + epochMs: () => 1_786_866_357_364, + }, + })(event); - assertEquals(target.appended, [[event]]); + assertEquals(target.appended, [[{ + ...event, + elapsedMs: 0, + emittedAt: 1_786_866_357_364, + }]]); }); it("accepts the exact append request byte limit and rejects one byte over", async () => { diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index 6325a076bb..89adb39e00 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -257,6 +257,7 @@ describe("runtime-bridge", () => { assertEquals(recorded, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", model: { id: "test/model-call-context", modelProvider: "test" }, + request: { temperature: 0.7 }, messages: options.prompt, tools: options.tools, }); diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index ebf78ea46b..453f56cf64 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -138,10 +138,22 @@ type DirectStreamResult = { stream: ReadableStream; }; type DirectTextOptions = GenerateTextOptions | StreamTextOptions; +type ModelCallRequestSource = Pick< + GenerateTextOptions, + | "maxOutputTokens" + | "temperature" + | "topP" + | "topK" + | "stopSequences" + | "seed" + | "presencePenalty" + | "frequencyPenalty" + | "reasoning" +>; type DirectModelOptions = Record & { prompt: ModelCallMessage[]; tools?: ModelCallTool[]; -}; +} & ModelCallRequestSource; function readSystemProviderOptions( system: object, @@ -627,7 +639,7 @@ function buildDirectModelOptions( }; } -function buildModelCallRequest(options: DirectTextOptions): ModelCallRequest | undefined { +function buildModelCallRequest(options: ModelCallRequestSource): ModelCallRequest | undefined { const reasoning = options.reasoning; const request: ModelCallRequest = { ...(options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {}), @@ -666,6 +678,7 @@ async function emitModelCallContextEvent( ): Promise { const sinks = getActiveRunEventSinks(); if (!sinks.mandatory && !sinks.public) return; + const request = buildModelCallRequest(directOptions); const event: AgentRunModelCallContextEvent = { type: "AGENT_RUN_MODEL_CALL_CONTEXT", @@ -679,7 +692,7 @@ async function emitModelCallContextEvent( }, } : {}), - ...(buildModelCallRequest(options) ? { request: buildModelCallRequest(options) } : {}), + ...(request ? { request } : {}), messages: sanitizeModelCallContextMessages(directOptions.prompt), ...(directOptions.tools ? { tools: directOptions.tools } : {}), }; From e4cfebb30415edf85b0c85c2a4680bf51c7b9970 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 15:26:27 +0200 Subject: [PATCH 03/16] test(agent): cover durable context timing --- .../ag-ui/browser-response-stream.test.ts | 23 +++++++++++++-- .../hosted/chat-execution-runtime.test.ts | 24 +++++++++++++++- .../child-fork-execution-runner.test.ts | 28 +++++++++++++++++-- src/agent/runtime/provider-transport.test.ts | 18 ++++++++++-- src/internal-agents/run-stream.test.ts | 19 ++++++++++--- 5 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/agent/ag-ui/browser-response-stream.test.ts b/src/agent/ag-ui/browser-response-stream.test.ts index 01218c6954..e998fd2edd 100644 --- a/src/agent/ag-ui/browser-response-stream.test.ts +++ b/src/agent/ag-ui/browser-response-stream.test.ts @@ -26,6 +26,14 @@ async function collectStreamText(stream: ReadableStream): Promise }> { + return body.split("\n\n").flatMap((frame) => { + const event = /^event: (.+)$/m.exec(frame)?.[1]; + const data = /^data: (.+)$/m.exec(frame)?.[1]; + return event && data ? [{ event, data: JSON.parse(data) as Record }] : []; + }); +} + describe("agent/ag-ui-browser-response-stream", () => { it("writes bootstrap events, encoded chunk events, and finalize events", async () => { const stream = createAgUiBrowserResponseStream({ @@ -165,8 +173,19 @@ describe("agent/ag-ui-browser-response-stream", () => { }); const text = await collectStreamText(stream); - assertStringIncludes(text, "event: StateSnapshot"); - assertStringIncludes(text, 'data: {"snapshot":{}}'); + const stateSnapshot = parseSseFrames(text).find((frame) => frame.event === "StateSnapshot") + ?.data; + assertEquals(stateSnapshot?.snapshot, {}); + assertEquals( + typeof stateSnapshot?.elapsedMs === "number" && + Number.isFinite(stateSnapshot.elapsedMs) && stateSnapshot.elapsedMs >= 0, + true, + ); + assertEquals( + typeof stateSnapshot?.emittedAt === "number" && + Number.isInteger(stateSnapshot.emittedAt) && stateSnapshot.emittedAt > 0, + true, + ); }); it("stops consuming chunks after the response stream is cancelled", async () => { diff --git a/src/agent/hosted/chat-execution-runtime.test.ts b/src/agent/hosted/chat-execution-runtime.test.ts index e947402ca6..d4ce25ad6a 100644 --- a/src/agent/hosted/chat-execution-runtime.test.ts +++ b/src/agent/hosted/chat-execution-runtime.test.ts @@ -85,6 +85,15 @@ function createDurableRunMirror(input: { }; } +function withoutEventTiming(event: unknown): unknown { + if (typeof event !== "object" || event === null) return event; + const { elapsedMs: _elapsedMs, emittedAt: _emittedAt, ...semanticEvent } = event as Record< + string, + unknown + >; + return semanticEvent; +} + function createLifecycleAdapter(input?: { durableRunMirror?: ConversationRunChunkMirror | null; messageId?: string | null; @@ -643,7 +652,20 @@ describe("agent/hosted-chat-execution-runtime", () => { ).length, 1, ); - assertEquals(observed, persisted); + const persistedContext = persisted.find((event) => + (event as { type?: string }).type === "AGENT_RUN_MODEL_CALL_CONTEXT" + ) as Record | undefined; + assertEquals( + typeof persistedContext?.elapsedMs === "number" && + Number.isFinite(persistedContext.elapsedMs) && persistedContext.elapsedMs >= 0, + true, + ); + assertEquals( + typeof persistedContext?.emittedAt === "number" && + Number.isInteger(persistedContext.emittedAt) && persistedContext.emittedAt > 0, + true, + ); + assertEquals(observed, persisted.map(withoutEventTiming)); assertEquals(JSON.stringify(uiChunks).includes("AGENT_RUN_MODEL_CALL_CONTEXT"), false); assertEquals(lazyScopeActive, true); }); diff --git a/src/agent/hosted/child-fork-execution-runner.test.ts b/src/agent/hosted/child-fork-execution-runner.test.ts index b7fd1ce8f5..2a21a85e7c 100644 --- a/src/agent/hosted/child-fork-execution-runner.test.ts +++ b/src/agent/hosted/child-fork-execution-runner.test.ts @@ -32,6 +32,15 @@ function systemIncludes(system: AgentSystem, text: string): boolean { : system.some((message) => message.content.includes(text)); } +function withoutEventTiming(event: unknown): unknown { + if (typeof event !== "object" || event === null) return event; + const { elapsedMs: _elapsedMs, emittedAt: _emittedAt, ...semanticEvent } = event as Record< + string, + unknown + >; + return semanticEvent; +} + function createRuntimeEventStream( events: readonly Record[], ): ReadableStream { @@ -495,14 +504,29 @@ Deno.test("executeHostedChildForkWithPreparedTools preserves the mandatory sink assertEquals(activeDuringStart, true); assertEquals(activeDuringIteration, true); assertEquals(order.slice(0, 4), ["append", "flush", "observe", "dispatch"]); - assertEquals(persisted, [{ + assertEquals(persisted.map(withoutEventTiming), [{ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [ { role: "system", content: "Hosted child instructions" }, { role: "user", content: [{ type: "text", text: "Run child" }] }, ], + model: { + id: "test/hosted-child", + modelProvider: "test", + }, }]); - assertEquals(observed, persisted); + const persistedContext = persisted[0] as Record | undefined; + assertEquals( + typeof persistedContext?.elapsedMs === "number" && + Number.isFinite(persistedContext.elapsedMs) && persistedContext.elapsedMs >= 0, + true, + ); + assertEquals( + typeof persistedContext?.emittedAt === "number" && + Number.isInteger(persistedContext.emittedAt) && persistedContext.emittedAt > 0, + true, + ); + assertEquals(observed, persisted.map(withoutEventTiming)); assertEquals(result.success, true); if (result.success) { assertEquals(result.summary.text, "Injected context."); diff --git a/src/agent/runtime/provider-transport.test.ts b/src/agent/runtime/provider-transport.test.ts index 347330fb5f..0b9553470e 100644 --- a/src/agent/runtime/provider-transport.test.ts +++ b/src/agent/runtime/provider-transport.test.ts @@ -41,9 +41,9 @@ function createTextStream( function normalizeRunRuntimeContext( event: AgentRunModelCallContextEvent, -): AgentRunModelCallContextEvent { +): Pick { return { - ...event, + type: event.type, messages: event.messages.map((message) => message.role === "system" && typeof message.content === "string" ? { @@ -175,6 +175,20 @@ describe("agent provider transport hooks", () => { const localContext = contexts[1]; assertExists(cloudContext); assertExists(localContext); + assertEquals(cloudContext.model, { + id: "cloud/context-parity", + modelProvider: "cloud", + }); + assertEquals(localContext.model, { + id: "local/context-parity", + modelProvider: "local", + }); + const expectedRequestControls = { + maxOutputTokens: 4096, + temperature: 0, + }; + assertEquals(cloudContext.request, expectedRequestControls); + assertEquals(localContext.request, expectedRequestControls); assertEquals( normalizeRunRuntimeContext(cloudContext), normalizeRunRuntimeContext(localContext), diff --git a/src/internal-agents/run-stream.test.ts b/src/internal-agents/run-stream.test.ts index 5dba3afd2c..439595c160 100644 --- a/src/internal-agents/run-stream.test.ts +++ b/src/internal-agents/run-stream.test.ts @@ -2919,11 +2919,22 @@ describe("internal-agents/run-stream", () => { const frames = parseSseFrames(await response.text()); assertEquals(Boolean(sinkDuringCreate), true); + const contextFrame = frames.find((frame) => + frame.event === MODEL_CALL_CONTEXT_SSE_EVENT_NAME + ); + const contextEvent = contextFrame?.data as Record | undefined; + assertEquals(contextEvent?.type, modelCallContextEvent.type); + assertEquals(contextEvent?.messages, modelCallContextEvent.messages); + assertEquals(contextEvent?.tools, modelCallContextEvent.tools); + assertEquals( + typeof contextEvent?.elapsedMs === "number" && + Number.isFinite(contextEvent.elapsedMs) && contextEvent.elapsedMs >= 0, + true, + ); assertEquals( - frames - .filter((frame) => frame.event === MODEL_CALL_CONTEXT_SSE_EVENT_NAME) - .map((frame) => frame.data), - [modelCallContextEvent], + typeof contextEvent?.emittedAt === "number" && + Number.isInteger(contextEvent.emittedAt) && contextEvent.emittedAt > 0, + true, ); }); From a4d8a3f60c936e030e447c82ff122a0452aec896 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 15:28:12 +0200 Subject: [PATCH 04/16] test(cli): isolate Git fixture configuration --- cli/commands/push/command.test.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cli/commands/push/command.test.ts b/cli/commands/push/command.test.ts index f4ff1753c7..3a5a47c857 100644 --- a/cli/commands/push/command.test.ts +++ b/cli/commands/push/command.test.ts @@ -89,14 +89,21 @@ interface GitProject { async function withGitProject(test: (project: GitProject) => Promise): Promise { const projectDir = await Deno.makeTempDir(); const originalGithubSha = Deno.env.get("GITHUB_SHA"); + const originalGitConfigGlobal = Deno.env.get("GIT_CONFIG_GLOBAL"); + let isolatedGitConfigGlobal: string | undefined; const runGit = async (...args: string[]): Promise => { const result = await new Deno.Command("git", { args, cwd: projectDir, clearEnv: true, - env: Object.fromEntries( - Object.entries(Deno.env.toObject()).filter(([key]) => !key.startsWith("GIT_")), - ), + env: { + ...Object.fromEntries( + Object.entries(Deno.env.toObject()).filter(([key]) => !key.startsWith("GIT_")), + ), + ...(isolatedGitConfigGlobal === undefined + ? {} + : { GIT_CONFIG_GLOBAL: isolatedGitConfigGlobal }), + }, stdout: "piped", stderr: "piped", }).output(); @@ -108,6 +115,9 @@ async function withGitProject(test: (project: GitProject) => Promise): Pro try { Deno.env.delete("GITHUB_SHA"); await runGit("init", "--quiet"); + isolatedGitConfigGlobal = `${projectDir}/.git/veryfront-test-global-config`; + await Deno.writeTextFile(isolatedGitConfigGlobal, ""); + Deno.env.set("GIT_CONFIG_GLOBAL", isolatedGitConfigGlobal); await runGit("config", "user.email", "test@veryfront.com"); await runGit("config", "user.name", "Veryfront Test"); await Deno.writeTextFile(`${projectDir}/app.ts`, "export const value = 1;\n"); @@ -117,6 +127,7 @@ async function withGitProject(test: (project: GitProject) => Promise): Pro } finally { if (originalGithubSha === undefined) Deno.env.delete("GITHUB_SHA"); else Deno.env.set("GITHUB_SHA", originalGithubSha); + restoreEnv("GIT_CONFIG_GLOBAL", originalGitConfigGlobal); await Deno.remove(projectDir, { recursive: true }); } } From 5c6d84f2ec8c11e74f9abec1f74a6ea7aff4d544 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 15:39:58 +0200 Subject: [PATCH 05/16] docs: refresh framework API reference --- docs/api-reference/veryfront/agent.md | 58 +++++++++++------------ docs/api-reference/veryfront/embedding.md | 2 +- docs/api-reference/veryfront/provider.md | 40 ++++++++-------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 27b4f06e5e..7236e13a11 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -547,7 +547,7 @@ Input delivered to a hosted agent-service detached execution callback. | `buildAgentCallContext` | Builds the layered system-message set for one provider call (RFC 0001). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/call-context.ts#L648) | | `buildAgentDelegateTools` | Builds the opt-in delegate tools for a coordinator agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/agent-delegation.ts#L158) | | `buildAgentRunTraceAttributes` | Builds agent run trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L181) | -| `buildAgUiBrowserFinalizeResponse` | Response payload for build AG-UI browser finalize. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L415) | +| `buildAgUiBrowserFinalizeResponse` | Response payload for build AG-UI browser finalize. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L416) | | `buildAgUiSseTraceSignature` | Build a compact ordered event-type signature for regression checks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/sse-parser.ts#L75) | | `buildChatStreamChunkMessageMetadata` | Builds chat stream chunk message metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L325) | | `buildChildRunExecutionSnapshot` | Builds child run execution snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/child-run/execution-snapshot.ts#L79) | @@ -634,9 +634,9 @@ Input delivered to a hosted agent-service detached execution callback. | `createAgentServiceRuntime` | Create agent service runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/runtime.ts#L233) | | `createAgentServiceServerRuntime` | Create agent service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/server.ts#L54) | | `createAgUiBrowserChunkEncoder` | Create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L101) | -| `createAgUiBrowserEncoderState` | State for create AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L89) | +| `createAgUiBrowserEncoderState` | State for create AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L90) | | `createAgUiBrowserFinalizeTracker` | Create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L23) | -| `createAgUiBrowserResponseStream` | Create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L61) | +| `createAgUiBrowserResponseStream` | Create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L62) | | `createAgUiCancelHandler` | Handler for create AG-UI cancel. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/run-control.ts#L148) | | `createAgUiChatUiChunkBrowserEncoder` | Create AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L260) | | `createAgUiChatUiTrackedBrowserResponse` | Response payload for create AG-UI chat UI tracked browser. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L279) | @@ -664,7 +664,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createConversationRecord` | Record shape for create conversation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L175) | | `createConversationRootRunContext` | Context for create conversation root run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/root-run-context.ts#L48) | | `createConversationRootRunStartAdapter` | Create conversation root run start adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/root-run-context.ts#L102) | -| `createConversationRunChunkMirror` | Create conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L160) | +| `createConversationRunChunkMirror` | Create conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L165) | | `createConversationRunContext` | Context for create conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-context.ts#L12) | | `createConversationRunEventQueueController` | Create conversation run event queue controller. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L809) | | `createConversationRunMirror` | Create conversation run mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-mirror.ts#L94) | @@ -699,7 +699,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createHostedChildMirrorContext` | Context for create hosted child mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-mirror.ts#L233) | | `createHostedChildPendingToolLifecycle` | Create hosted child pending tool lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-pending-tool-lifecycle.ts#L98) | | `createHostedChildPendingToolLifecycleLogger` | Create hosted child pending tool lifecycle logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-pending-tool-lifecycle.ts#L55) | -| `createHostedConversationRunChunkMirror` | Create hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L374) | +| `createHostedConversationRunChunkMirror` | Create hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L380) | | `createHostedDurableChildForkRunContext` | Context for create hosted durable child fork run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L179) | | `createHostedDurableChildInvokeTraceRecorder` | Create hosted durable child invoke trace recorder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/durable-child-fork-execution.ts#L312) | | `createHostedFormInputTool` | Create hosted form input tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/form-input-tool.ts#L34) | @@ -746,7 +746,7 @@ Input delivered to a hosted agent-service detached execution callback. | `dispatchConversationHostedStreamErrorState` | State for dispatch conversation hosted stream error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L106) | | `dispatchConversationHostedTerminalState` | State for dispatch conversation hosted terminal. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L91) | | `doesProjectAgentRuntimeAgentMatchSource` | Does project agent runtime agent match source helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/project/agent-runtime.ts#L173) | -| `encodeConversationRunEvents` | Encode conversation run events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L353) | +| `encodeConversationRunEvents` | Encode conversation run events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L379) | | `ensureConversationProjectLink` | Ensure conversation project link helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L148) | | `evaluateSlashCommandArtifactPolicy` | Evaluate slash command artifact policy helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/slash-command-artifact-policy.ts#L200) | | `evaluateStarterIntentTurnPolicy` | Evaluate starter intent turn policy helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/delegation-policy.ts#L207) | @@ -772,7 +772,7 @@ Input delivered to a hosted agent-service detached execution callback. | `fetchLatestConversationUserText` | Fetch latest conversation user text helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/default-research-artifact-support.ts#L112) | | `filterAgentTraceAttributes` | Filter agent trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L61) | | `filterHostedChatRuntimeLocalTools` | Filter hosted chat runtime local tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-runtime-tool-assembly.ts#L223) | -| `finalizeAgUiBrowserEvents` | Finalize AG-UI browser events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L951) | +| `finalizeAgUiBrowserEvents` | Finalize AG-UI browser events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L974) | | `finalizeChildRunExecutionResources` | Finalize child run execution resources helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/child-run/execution-cleanup.ts#L28) | | `finalizeConversationAgentRun` | Finalize conversation agent run helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L1326) | | `finalizeHostedChildForkCompletion` | Finalize hosted child fork completion helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-stream-execution.ts#L156) | @@ -865,7 +865,7 @@ Input delivered to a hosted agent-service detached execution callback. | `mapAgUiRuntimeEventToForkParts` | Map AG-UI runtime event to fork parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/fork-runtime-part-mapper.ts#L229) | | `mapFrameworkEventToForkParts` | Handles map framework event to fork parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/fork-runtime-part-mapper.ts#L408) | | `mapHostedStreamPartToChatUiChunks` | Map hosted stream part to chat UI chunks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/hosted-ui-chunk-mapping.ts#L220) | -| `mapRuntimeStreamEventToAgUiBrowserEvents` | Map runtime stream event to AG-UI browser events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L680) | +| `mapRuntimeStreamEventToAgUiBrowserEvents` | Map runtime stream event to AG-UI browser events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L681) | | `mergeToolCallInput` | Input payload for merge tool call. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/tool-input.ts#L112) | | `mergeToolInputDelta` | Merge tool input delta helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/tool-input.ts#L54) | | `mirrorDefaultResearchRunArtifact` | Mirror default research run artifact helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/default-research-artifact-support.ts#L299) | @@ -880,7 +880,7 @@ Input delivered to a hosted agent-service detached execution callback. | `normalizeChatUiMessageStream` | Normalizes chat UI message stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L494) | | `normalizeConversationRunEvent` | Event emitted for normalize conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-normalization.ts#L36) | | `normalizeConversationRunEvents` | Normalizes conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-normalization.ts#L95) | -| `normalizeEncodedConversationRunEvents` | Normalizes encoded conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L361) | +| `normalizeEncodedConversationRunEvents` | Normalizes encoded conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L387) | | `normalizeHostedChildArtifactPath` | Normalizes hosted child artifact path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-artifact-support.ts#L133) | | `normalizeParsedAgentServiceChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | | `normalizeParsedHostedChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | @@ -1046,7 +1046,7 @@ Input delivered to a hosted agent-service detached execution callback. | `AppendConversationRunEventsError` | Error shape for append conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-append-errors.ts#L4) | | `BufferMemory` | Implement buffer memory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/memory/memory.ts#L128) | | `ConversationMemory` | Implement conversation memory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/memory/memory.ts#L84) | -| `ConversationRunEventEncoder` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L78) | +| `ConversationRunEventEncoder` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L80) | | `ConversationRunTerminalStateError` | Error shape for conversation run terminal state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L157) | | `HostedChildStreamIdleTimeoutError` | Error shape for hosted child stream idle timeout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-stream-watchdog.ts#L13) | | `HostedChildTerminalStateError` | Error shape for hosted child terminal state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-status.ts#L72) | @@ -1089,9 +1089,9 @@ Input delivered to a hosted agent-service detached execution callback. | `AgentPushRuntimeServiceRest` | Public API contract for agent push runtime service rest. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L40) | | `AgentRegistry` | Public API contract for agent registry. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/definition.ts#L59) | | `AgentResponse` | Response payload for agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/schemas/agent.schema.ts#L262) | -| `AgentRunEvent` | Event produced by an agent run runtime boundary. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L61) | -| `AgentRunEventSink` | Receives events produced within one scoped agent run execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L64) | -| `AgentRunModelCallContextEvent` | Provider-agnostic input persisted before one model dispatch. System-message provider options contain only validated prompt-cache metadata. Other provider-specific values are excluded because run events are durable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L54) | +| `AgentRunEvent` | Event produced by an agent run runtime boundary. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L88) | +| `AgentRunEventSink` | Receives events produced within one scoped agent run execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L91) | +| `AgentRunModelCallContextEvent` | Provider-agnostic input persisted before one model dispatch. System-message provider options contain only validated prompt-cache metadata. Other provider-specific values are excluded because run events are durable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L77) | | `AgentRuntimeForkStepRunner` | Public API contract for agent runtime fork step runner. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/fork-runtime-stream.ts#L110) | | `AgentRuntimeMessage` | Message shape for agent runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/message-adapter.ts#L107) | | `AgentRuntimeMessagePart` | Public API contract for agent runtime message part. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/message-adapter.ts#L87) | @@ -1181,12 +1181,12 @@ Input delivered to a hosted agent-service detached execution callback. | `AgUiBeforeStreamMessageInput` | Input payload for AG-UI before stream message. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/before-stream.ts#L4) | | `AgUiBeforeStreamResult` | Result returned from AG-UI before stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/before-stream.ts#L25) | | `AgUiBrowserChunkEncoder` | Public API contract for AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L15) | -| `AgUiBrowserEncodedEvent` | Event emitted for AG-UI browser encoded. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L83) | +| `AgUiBrowserEncodedEvent` | Event emitted for AG-UI browser encoded. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L84) | | `AgUiBrowserEncoderState` | State for AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L35) | | `AgUiBrowserFinalizeTracker` | Public API contract for AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L9) | -| `AgUiBrowserResponseEncoder` | Public API contract for AG-UI browser response encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L36) | -| `AgUiBrowserResponseExecution` | Public API contract for AG-UI browser response execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L29) | -| `AgUiBrowserResponseRequestState` | State for AG-UI browser response request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L21) | +| `AgUiBrowserResponseEncoder` | Public API contract for AG-UI browser response encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L37) | +| `AgUiBrowserResponseExecution` | Public API contract for AG-UI browser response execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L30) | +| `AgUiBrowserResponseRequestState` | State for AG-UI browser response request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L22) | | `AgUiBrowserRunFinishedMetadata` | Public API contract for AG-UI browser run finished metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L7) | | `AgUiCancelHandlerOptions` | Options accepted by AG-UI cancel handler. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/run-control.ts#L59) | | `AgUiChatUiChunkBrowserEncoder` | Public API contract for AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L17) | @@ -1279,14 +1279,14 @@ Input delivered to a hosted agent-service detached execution callback. | `ConversationRunAppendFailureOutcome` | Public API contract for conversation run append failure outcome. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L205) | | `ConversationRunAppendRecoveryOutcome` | Public API contract for conversation run append recovery outcome. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L199) | | `ConversationRunBatchFlushOutcome` | Public API contract for conversation run batch flush outcome. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L217) | -| `ConversationRunChunkMirror` | Public API contract for conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L26) | -| `ConversationRunChunkMirrorApiOptions` | Options accepted by conversation run chunk mirror API. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L88) | -| `ConversationRunChunkMirrorOptions` | Options accepted by conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L103) | -| `ConversationRunChunkMirrorPrepareChunkEventsInput` | Input payload for conversation run chunk mirror prepare chunk events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L49) | -| `ConversationRunChunkMirrorPreparedChunk` | Public API contract for conversation run chunk mirror prepared chunk. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L38) | -| `ConversationRunChunkMirrorPreparedEvents` | Public API contract for conversation run chunk mirror prepared events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L44) | -| `ConversationRunChunkMirrorPrepareExternalEventsInput` | Input payload for conversation run chunk mirror prepare external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L55) | -| `ConversationRunChunkMirrorQueueOptions` | Options accepted by conversation run chunk mirror queue. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L82) | +| `ConversationRunChunkMirror` | Public API contract for conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L30) | +| `ConversationRunChunkMirrorApiOptions` | Options accepted by conversation run chunk mirror API. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L93) | +| `ConversationRunChunkMirrorOptions` | Options accepted by conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L108) | +| `ConversationRunChunkMirrorPrepareChunkEventsInput` | Input payload for conversation run chunk mirror prepare chunk events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L54) | +| `ConversationRunChunkMirrorPreparedChunk` | Public API contract for conversation run chunk mirror prepared chunk. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L43) | +| `ConversationRunChunkMirrorPreparedEvents` | Public API contract for conversation run chunk mirror prepared events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L49) | +| `ConversationRunChunkMirrorPrepareExternalEventsInput` | Input payload for conversation run chunk mirror prepare external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L60) | +| `ConversationRunChunkMirrorQueueOptions` | Options accepted by conversation run chunk mirror queue. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L87) | | `ConversationRunContext` | Context for conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-context.ts#L4) | | `ConversationRunEvent` | Event emitted for conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L35) | | `ConversationRunEventEncoderOptions` | Options accepted by the conversation run event encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L64) | @@ -1304,7 +1304,7 @@ Input delivered to a hosted agent-service detached execution callback. | `CreateAgentServiceServerRuntimeOptions` | Options accepted by create agent service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/server.ts#L19) | | `CreateAgUiBrowserChunkEncoderOptions` | Options accepted by create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L22) | | `CreateAgUiBrowserFinalizeTrackerOptions` | Options accepted by create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L16) | -| `CreateAgUiBrowserResponseStreamInput` | Input payload for create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L42) | +| `CreateAgUiBrowserResponseStreamInput` | Input payload for create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L43) | | `CreateAgUiChatUiChunkBrowserEncoderOptions` | Options accepted by create AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L23) | | `CreateAgUiChatUiTrackedBrowserResponseInput` | Input payload for create AG-UI chat UI tracked browser response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L35) | | `CreateAgUiChunkEncoderBridgeOptions` | Options accepted by create AG-UI chunk encoder bridge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chunk-encoder-bridge.ts#L20) | @@ -1548,9 +1548,9 @@ Input delivered to a hosted agent-service detached execution callback. | `HostedChildWrittenArtifactPathInput` | Input payload for hosted child written artifact path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-artifact-support.ts#L14) | | `HostedConversationRootRunContext` | Context for hosted conversation root run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/root-run-lifecycle.ts#L70) | | `HostedConversationRootRunState` | State for hosted conversation root run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/root-run-lifecycle.ts#L61) | -| `HostedConversationRunChunkMirrorInstrumentation` | Public API contract for hosted conversation run chunk mirror instrumentation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L114) | -| `HostedConversationRunChunkMirrorOptions` | Options accepted by hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L123) | -| `HostedConversationRunChunkMirrorTraceAttributes` | Public API contract for hosted conversation run chunk mirror trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L108) | +| `HostedConversationRunChunkMirrorInstrumentation` | Public API contract for hosted conversation run chunk mirror instrumentation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L119) | +| `HostedConversationRunChunkMirrorOptions` | Options accepted by hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L128) | +| `HostedConversationRunChunkMirrorTraceAttributes` | Public API contract for hosted conversation run chunk mirror trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L113) | | `HostedDetachedFinalizationState` | State for hosted detached finalization. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/stream-finalization.ts#L21) | | `HostedDurableChildBootstrapCallbacks` | Public API contract for hosted durable child bootstrap callbacks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/durable-child-fork-execution.ts#L466) | | `HostedDurableChildBootstrapContext` | Context for hosted durable child bootstrap. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/durable-child-fork-execution.ts#L456) | diff --git a/docs/api-reference/veryfront/embedding.md b/docs/api-reference/veryfront/embedding.md index 6e21ef8a77..04e624ca7e 100644 --- a/docs/api-reference/veryfront/embedding.md +++ b/docs/api-reference/veryfront/embedding.md @@ -42,7 +42,7 @@ export const { POST, GET, DELETE } = createUploadHandler(store, { | `ragStore` | Creates a persistent RAG store with lazy embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/rag-store.ts#L212) | | `registerEmbeddingProvider` | Register an embedding provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L25) | | `resolveEmbeddingModel` | Resolve a "provider/model" string to an embedding runtime instance. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L116) | -| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1128) | +| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1192) | | `vectorStore` | Creates an in-memory vector store with integrated embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/vector-store.ts#L46) | ### Types diff --git a/docs/api-reference/veryfront/provider.md b/docs/api-reference/veryfront/provider.md index 0029626808..a59cb566e8 100644 --- a/docs/api-reference/veryfront/provider.md +++ b/docs/api-reference/veryfront/provider.md @@ -102,7 +102,7 @@ Clear all registered model providers and reset lazy built-ins (for testing). | ----------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `ModelProviderFactory` | Public API contract for model provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/model-registry.ts#L33) | | `ModelProviderRegistrationDisposer` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/model-registry.ts#L34) | -| `ModelRuntime` | Public API contract for model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L211) | +| `ModelRuntime` | Public API contract for model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L213) | | `VeryfrontCloudBootstrap` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/platform/cloud/resolver.ts#L49) | | `VeryfrontCloudChatModel` | Public API contract for Veryfront Cloud chat model. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts#L19) | | `VeryfrontCloudContext` | Context for Veryfront Cloud. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/context.ts#L4) | @@ -193,16 +193,16 @@ import { | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | `JsonSnapshotOptions` | Resource limits applied while taking a provider-boundary JSON snapshot. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/runtime-loader/json-snapshot.ts#L91) | | `JsonSnapshotValue` | A deeply owned JSON value returned by `snapshotJsonValue`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/runtime-loader/json-snapshot.ts#L82) | -| `ModelRuntimeCallOptions` | Canonical request contract passed to `ModelRuntime` generation hooks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L174) | -| `ModelRuntimePromptMessage` | Immutable prompt view accepted by model-runtime calls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L117) | -| `ModelRuntimeToolDefinition` | Canonical tool definition sent to a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L142) | +| `ModelRuntimeCallOptions` | Canonical request contract passed to `ModelRuntime` generation hooks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L176) | +| `ModelRuntimePromptMessage` | Immutable prompt view accepted by model-runtime calls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L119) | +| `ModelRuntimeToolDefinition` | Canonical tool definition sent to a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L144) | | `OpenAICompatibleChatMessage` | Message shape for OpenAI-compatible chat requests. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/runtime-loader.ts#L50) | | `OpenAICompatibleChatRequest` | Request payload for OpenAI-compatible chat completion providers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/runtime-loader.ts#L87) | | `ProviderWarning` | Structured warning emitted when a provider runtime drops or rewrites a caller-provided option. Mirrors the AI ecosystem convention (Vercel AI SDK, LangChain) of returning `unsupported-setting` warnings on the runtime result so callers can discover silently-dropped fields without having to read the source. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/runtime-loader.ts#L120) | -| `RuntimeAssistantContentPart` | Canonical assistant content accepted when invoking a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L33) | -| `RuntimePromptMessage` | Historical mutable provider-facing prompt contract retained for source compatibility. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L62) | -| `RuntimeReasoningOption` | Provider-neutral reasoning controls accepted by model runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L135) | -| `RuntimeResponseFormat` | Provider-neutral structured-output request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L157) | +| `RuntimeAssistantContentPart` | Canonical assistant content accepted when invoking a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L35) | +| `RuntimePromptMessage` | Historical mutable provider-facing prompt contract retained for source compatibility. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L64) | +| `RuntimeReasoningOption` | Provider-neutral reasoning controls accepted by model runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L137) | +| `RuntimeResponseFormat` | Provider-neutral structured-output request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L159) | | `RuntimeUsage` | Canonical provider-neutral usage reported by text-generation runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/runtime-usage.ts#L21) | #### Constants @@ -225,16 +225,16 @@ import type { | Name | Description | Source | | ----------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -| `EmbeddingRuntime` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L233) | -| `ModelRuntime` | Public API contract for model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L211) | -| `ModelRuntimeCallOptions` | Canonical request contract passed to `ModelRuntime` generation hooks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L174) | -| `ModelRuntimeCapabilities` | Explicit behavioral support advertised by a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L200) | -| `ModelRuntimeGenerateResult` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L121) | -| `ModelRuntimePromptMessage` | Immutable prompt view accepted by model-runtime calls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L117) | -| `ModelRuntimeStreamResult` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L129) | -| `ModelRuntimeToolDefinition` | Canonical tool definition sent to a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L142) | -| `RuntimeAssistantContentPart` | Canonical assistant content accepted when invoking a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L33) | +| `EmbeddingRuntime` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L235) | +| `ModelRuntime` | Public API contract for model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L213) | +| `ModelRuntimeCallOptions` | Canonical request contract passed to `ModelRuntime` generation hooks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L176) | +| `ModelRuntimeCapabilities` | Explicit behavioral support advertised by a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L202) | +| `ModelRuntimeGenerateResult` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L123) | +| `ModelRuntimePromptMessage` | Immutable prompt view accepted by model-runtime calls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L119) | +| `ModelRuntimeStreamResult` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L131) | +| `ModelRuntimeToolDefinition` | Canonical tool definition sent to a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L144) | +| `RuntimeAssistantContentPart` | Canonical assistant content accepted when invoking a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L35) | | `RuntimeMetadata` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L1) | -| `RuntimePromptMessage` | Historical mutable provider-facing prompt contract retained for source compatibility. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L62) | -| `RuntimeReasoningOption` | Provider-neutral reasoning controls accepted by model runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L135) | -| `RuntimeResponseFormat` | Provider-neutral structured-output request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L157) | +| `RuntimePromptMessage` | Historical mutable provider-facing prompt contract retained for source compatibility. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L64) | +| `RuntimeReasoningOption` | Provider-neutral reasoning controls accepted by model runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L137) | +| `RuntimeResponseFormat` | Provider-neutral structured-output request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/types.ts#L159) | From 2a75b6c3c63f140dd819ebb1d22fd141bbba3a62 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 16:02:31 +0200 Subject: [PATCH 06/16] fix(agent): align durable context boundaries --- cli/commands/push/command.test.ts | 13 ++- .../ag-ui/browser-response-stream.test.ts | 48 +++++++++++ src/agent/ag-ui/browser-response-stream.ts | 10 ++- src/agent/ag-ui/chunk-encoder-bridge.test.ts | 1 + src/agent/ag-ui/chunk-encoder-bridge.ts | 3 + .../conversation/private-run-event.test.ts | 53 ++++++++++++ src/agent/conversation/private-run-event.ts | 11 ++- .../conversation/run-chunk-mirror.test.ts | 48 +++++++++++ src/agent/conversation/run-chunk-mirror.ts | 10 ++- src/agent/runtime/model-transport.test.ts | 25 ++++++ src/provider/veryfront-cloud/provider.test.ts | 54 ++++++++++++ src/provider/veryfront-cloud/provider.ts | 10 ++- src/runtime/runtime-bridge.test.ts | 82 +++++++++++++++++- src/runtime/runtime-bridge.ts | 86 +++++++++++++++---- 14 files changed, 415 insertions(+), 39 deletions(-) diff --git a/cli/commands/push/command.test.ts b/cli/commands/push/command.test.ts index 3a5a47c857..3d0cc4f748 100644 --- a/cli/commands/push/command.test.ts +++ b/cli/commands/push/command.test.ts @@ -88,9 +88,9 @@ interface GitProject { async function withGitProject(test: (project: GitProject) => Promise): Promise { const projectDir = await Deno.makeTempDir(); + const isolatedGitConfigGlobal = await Deno.makeTempFile(); const originalGithubSha = Deno.env.get("GITHUB_SHA"); const originalGitConfigGlobal = Deno.env.get("GIT_CONFIG_GLOBAL"); - let isolatedGitConfigGlobal: string | undefined; const runGit = async (...args: string[]): Promise => { const result = await new Deno.Command("git", { args, @@ -100,9 +100,7 @@ async function withGitProject(test: (project: GitProject) => Promise): Pro ...Object.fromEntries( Object.entries(Deno.env.toObject()).filter(([key]) => !key.startsWith("GIT_")), ), - ...(isolatedGitConfigGlobal === undefined - ? {} - : { GIT_CONFIG_GLOBAL: isolatedGitConfigGlobal }), + GIT_CONFIG_GLOBAL: isolatedGitConfigGlobal, }, stdout: "piped", stderr: "piped", @@ -114,10 +112,10 @@ async function withGitProject(test: (project: GitProject) => Promise): Pro try { Deno.env.delete("GITHUB_SHA"); - await runGit("init", "--quiet"); - isolatedGitConfigGlobal = `${projectDir}/.git/veryfront-test-global-config`; - await Deno.writeTextFile(isolatedGitConfigGlobal, ""); + await Deno.writeTextFile(isolatedGitConfigGlobal, "[init]\n\tdefaultBranch = main\n"); Deno.env.set("GIT_CONFIG_GLOBAL", isolatedGitConfigGlobal); + await runGit("init", "--quiet"); + assertEquals(await runGit("symbolic-ref", "--short", "HEAD"), "main"); await runGit("config", "user.email", "test@veryfront.com"); await runGit("config", "user.name", "Veryfront Test"); await Deno.writeTextFile(`${projectDir}/app.ts`, "export const value = 1;\n"); @@ -129,6 +127,7 @@ async function withGitProject(test: (project: GitProject) => Promise): Pro else Deno.env.set("GITHUB_SHA", originalGithubSha); restoreEnv("GIT_CONFIG_GLOBAL", originalGitConfigGlobal); await Deno.remove(projectDir, { recursive: true }); + await Deno.remove(isolatedGitConfigGlobal); } } diff --git a/src/agent/ag-ui/browser-response-stream.test.ts b/src/agent/ag-ui/browser-response-stream.test.ts index e998fd2edd..2de0808b23 100644 --- a/src/agent/ag-ui/browser-response-stream.test.ts +++ b/src/agent/ag-ui/browser-response-stream.test.ts @@ -2,6 +2,7 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { createAgUiBrowserResponseStream } from "./browser-response-stream.ts"; +import { createAgUiChunkEncoderBridge } from "./chunk-encoder-bridge.ts"; import type { AgUiSseEvent } from "./host-support.ts"; async function collectStreamText(stream: ReadableStream): Promise { @@ -75,6 +76,53 @@ describe("agent/ag-ui-browser-response-stream", () => { assertStringIncludes(text, "event: RunFinished"); }); + it("shares the chunk encoder timing anchor with bootstrap and final events", async () => { + let now = 100; + const chunkEncoder = createAgUiChunkEncoderBridge<{ messageId: string }>({ + getRuntimeEvents: (chunk) => [ + { type: "message-start", messageId: chunk.messageId }, + { type: "text-start", id: chunk.messageId }, + ], + timing: { nowMs: () => now, epochMs: null }, + }); + + now = 150; + const stream = createAgUiBrowserResponseStream({ + agUiInput: { + runId: "run-timing", + threadId: "thread-timing", + messages: [], + }, + agentId: "assistant-1", + execution: { + agentUIStream: { + async *[Symbol.asyncIterator]() { + now = 175; + yield { messageId: "msg-1" }; + }, + }, + fail: async () => {}, + waitForFinish: async () => { + now = 200; + }, + }, + encoder: chunkEncoder, + initialState: {}, + }); + + const frames = parseSseFrames(await collectStreamText(stream)); + assertEquals( + frames.filter((frame) => + ["RunStarted", "TextMessageStart", "RunFinished"].includes(frame.event) + ).map((frame) => [frame.event, frame.data.elapsedMs]), + [ + ["RunStarted", 50], + ["TextMessageStart", 75], + ["RunFinished", 100], + ], + ); + }); + it("emits RunError and swallows execution.fail rejections", async () => { const stream = createAgUiBrowserResponseStream({ agUiInput: { diff --git a/src/agent/ag-ui/browser-response-stream.ts b/src/agent/ag-ui/browser-response-stream.ts index 5b683ef7b3..2fbcc1be38 100644 --- a/src/agent/ag-ui/browser-response-stream.ts +++ b/src/agent/ag-ui/browser-response-stream.ts @@ -1,6 +1,10 @@ import type { AgentResponse } from "../types.ts"; import type { AgUiSseEvent } from "./host-support.ts"; -import { createAgUiBrowserEncoderState, stampAgUiBrowserEventTiming } from "./browser-encoder.ts"; +import { + type AgUiBrowserEncoderState, + createAgUiBrowserEncoderState, + stampAgUiBrowserEventTiming, +} from "./browser-encoder.ts"; const encoder = new TextEncoder(); @@ -37,6 +41,8 @@ export interface AgUiBrowserResponseExecution { export interface AgUiBrowserResponseEncoder { encode: (chunk: TChunk) => AgUiSseEvent[]; finalize: (response: AgentResponse | null) => AgUiSseEvent[]; + /** Shared timing anchor for bootstrap, chunk, and final events. */ + timingState?: AgUiBrowserEncoderState; } /** Input payload for create AG-UI browser response stream. */ @@ -67,7 +73,7 @@ export function createAgUiBrowserResponseStream( return new ReadableStream({ start(controller) { - const timingState = createAgUiBrowserEncoderState(); + const timingState = input.encoder.timingState ?? createAgUiBrowserEncoderState(); const writeEvent = (event: AgUiSseEvent) => { if (streamClosed) { return false; diff --git a/src/agent/ag-ui/chunk-encoder-bridge.test.ts b/src/agent/ag-ui/chunk-encoder-bridge.test.ts index 6bef287c3d..b1429596e8 100644 --- a/src/agent/ag-ui/chunk-encoder-bridge.test.ts +++ b/src/agent/ag-ui/chunk-encoder-bridge.test.ts @@ -40,6 +40,7 @@ describe("agent/ag-ui-chunk-encoder-bridge", () => { }); assertEquals(bridge.state.messageId, "msg-1"); + assertEquals(bridge.timingState, bridge.state); assertEquals(finalEvents.length > 0, true); }); }); diff --git a/src/agent/ag-ui/chunk-encoder-bridge.ts b/src/agent/ag-ui/chunk-encoder-bridge.ts index 04e68426af..34a4bbb2a1 100644 --- a/src/agent/ag-ui/chunk-encoder-bridge.ts +++ b/src/agent/ag-ui/chunk-encoder-bridge.ts @@ -14,6 +14,8 @@ export interface AgUiChunkEncoderBridge { encode: (chunk: TChunk) => AgUiBrowserEncodedEvent[]; finalize: (response: AgentResponse | null) => AgUiBrowserEncodedEvent[]; state: AgUiBrowserEncoderState; + /** Timing anchor consumed by the browser response composition root. */ + timingState: AgUiBrowserEncoderState; } /** Options accepted by create AG-UI chunk encoder bridge. */ @@ -35,6 +37,7 @@ export function createAgUiChunkEncoderBridge( return { state, + timingState: state, encode: (chunk) => options.getRuntimeEvents(chunk).flatMap((event) => mapRuntimeStreamEventToAgUiBrowserEvents(state, event) diff --git a/src/agent/conversation/private-run-event.test.ts b/src/agent/conversation/private-run-event.test.ts index a2edc6d675..6272530938 100644 --- a/src/agent/conversation/private-run-event.test.ts +++ b/src/agent/conversation/private-run-event.test.ts @@ -45,6 +45,39 @@ describe("agent/conversation/private-run-event", () => { messages: [], request: { reasoning: { arbitrary: true } }, }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [{ + role: "assistant", + content: [{ + type: "tool-call", + toolCallId: "call-1", + toolName: "lookup", + input: undefined, + }], + }], + }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [{ + role: "tool", + content: [{ + type: "tool-result", + toolCallId: "call-1", + toolName: "lookup", + output: { type: "json", value: undefined }, + }], + }], + }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + tools: [{ + type: "function", + name: "lookup", + inputSchema: undefined, + }], + }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], emittedAt: 1.5 }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [], contextId: "legacy" }, { type: "TEXT_MESSAGE_CONTENT", messages: [] }, @@ -72,6 +105,26 @@ describe("agent/conversation/private-run-event", () => { assertEquals(reads, 0); }); + it("requires reasoning effort to be a literal allowed string", () => { + let coercions = 0; + const effort = { + toString() { + coercions += 1; + return "high"; + }, + }; + + assertEquals( + isPrivateConversationRunEvent({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + request: { reasoning: { effort } }, + }), + false, + ); + assertEquals(coercions, 0); + }); + it("uses the registered VeryfrontError slug for persistence failures", () => { const error = new DurableRunEventPersistenceError("mirror unavailable"); diff --git a/src/agent/conversation/private-run-event.ts b/src/agent/conversation/private-run-event.ts index 5bb2bae076..7682447d4f 100644 --- a/src/agent/conversation/private-run-event.ts +++ b/src/agent/conversation/private-run-event.ts @@ -70,7 +70,8 @@ function isRequest(value: unknown): boolean { const effort = ownDataValue(reasoning, "effort"); const budget = ownDataValue(reasoning, "budgetTokens"); return (enabled === undefined || typeof enabled === "boolean") && - (effort === undefined || ["low", "medium", "high", "max"].includes(String(effort))) && + (effort === undefined || + (typeof effort === "string" && ["low", "medium", "high", "max"].includes(effort))) && (budget === undefined || (Number.isInteger(budget) && (budget as number) >= 0)); } @@ -104,7 +105,8 @@ function isMessage(value: unknown): boolean { return ownDataValue(part, "type") === "tool-call" && hasOnlyKeys(part, ["type", "toolCallId", "toolName", "input", "providerExecuted"]) && typeof ownDataValue(part, "toolCallId") === "string" && - typeof ownDataValue(part, "toolName") === "string" && Object.hasOwn(part, "input") && + typeof ownDataValue(part, "toolName") === "string" && + ownDataValue(part, "input") !== undefined && (ownDataValue(part, "providerExecuted") === undefined || typeof ownDataValue(part, "providerExecuted") === "boolean"); } @@ -115,7 +117,7 @@ function isMessage(value: unknown): boolean { typeof ownDataValue(part, "toolCallId") === "string" && typeof ownDataValue(part, "toolName") === "string" && isRecord(output) && hasOnlyKeys(output, ["type", "value"]) && ownDataValue(output, "type") === "json" && - Object.hasOwn(output, "value"); + ownDataValue(output, "value") !== undefined; } return false; }); @@ -140,7 +142,8 @@ function isTool(value: unknown): boolean { if (!isRecord(value)) return false; if (ownDataValue(value, "type") === "function") { return hasOnlyKeys(value, ["type", "name", "description", "inputSchema"]) && - typeof ownDataValue(value, "name") === "string" && Object.hasOwn(value, "inputSchema") && + typeof ownDataValue(value, "name") === "string" && + ownDataValue(value, "inputSchema") !== undefined && (ownDataValue(value, "description") === undefined || typeof ownDataValue(value, "description") === "string"); } diff --git a/src/agent/conversation/run-chunk-mirror.test.ts b/src/agent/conversation/run-chunk-mirror.test.ts index d2c4c9739f..d4656e093b 100644 --- a/src/agent/conversation/run-chunk-mirror.test.ts +++ b/src/agent/conversation/run-chunk-mirror.test.ts @@ -166,6 +166,54 @@ describe("agent/conversation-run-chunk-mirror", () => { mirror.dispose(); }); + it("preserves timing across custom external event preparation", async () => { + const queueController = createQueueController(); + let now = 100; + let epoch = 1_000; + const callbackInputTiming: Array<{ elapsedMs?: number; emittedAt?: number }> = []; + const encoder = new ConversationRunEventEncoder({ + nowMs: () => now, + epochMs: () => epoch, + }); + const mirror = createConversationRunChunkMirror({ + queueController, + encoder, + immediateFlushEventCount: 99, + flushDelayMs: 10_000, + prepareExternalEvents: ({ events }) => { + callbackInputTiming.push(...events.map((event) => { + const timed = event as { elapsedMs?: number; emittedAt?: number }; + return { elapsedMs: timed.elapsedMs, emittedAt: timed.emittedAt }; + })); + now = 160; + epoch = 2_000; + return [ + ...events, + { type: "CONTEXT_COMPACTION", compactedMessageCount: 1 } as never, + ]; + }, + }); + now = 142; + epoch = 1_042; + + await mirror.appendEvents([ + { type: "TOOL_EXPOSURE_CHECKPOINT" } as never, + ]); + + assertEquals(callbackInputTiming, [{ elapsedMs: 42, emittedAt: 1_042 }]); + assertEquals( + queueController.enqueued.map((event) => { + const timed = event as { elapsedMs?: number; emittedAt?: number }; + return { elapsedMs: timed.elapsedMs, emittedAt: timed.emittedAt }; + }), + [ + { elapsedMs: 42, emittedAt: 1_042 }, + { elapsedMs: 60, emittedAt: 2_000 }, + ], + ); + mirror.dispose(); + }); + it("allows hosts to wrap chunk and external event preparation", async () => { const queueController = createQueueController(); const preparedMarkers: string[] = []; diff --git a/src/agent/conversation/run-chunk-mirror.ts b/src/agent/conversation/run-chunk-mirror.ts index faf2b41087..1e773ca1f0 100644 --- a/src/agent/conversation/run-chunk-mirror.ts +++ b/src/agent/conversation/run-chunk-mirror.ts @@ -210,10 +210,12 @@ export function createConversationRunChunkMirror( return; } - const normalizedEvents = await (input.prepareExternalEvents?.({ - events, - defaultPrepare: () => prepareConversationRunExternalEvents(encoder.stamp(events)), - }) ?? prepareConversationRunExternalEvents(encoder.stamp(events))); + const stampedEvents = encoder.stamp(events); + const preparedEvents = await (input.prepareExternalEvents?.({ + events: stampedEvents, + defaultPrepare: () => prepareConversationRunExternalEvents(stampedEvents), + }) ?? prepareConversationRunExternalEvents(stampedEvents)); + const normalizedEvents = prepareConversationRunExternalEvents(encoder.stamp(preparedEvents)); await input.onExternalEventsPrepared?.({ events: normalizedEvents }); if (normalizedEvents.length === 0) { return; diff --git a/src/agent/runtime/model-transport.test.ts b/src/agent/runtime/model-transport.test.ts index d82b4eea72..53a23d86dc 100644 --- a/src/agent/runtime/model-transport.test.ts +++ b/src/agent/runtime/model-transport.test.ts @@ -99,6 +99,31 @@ describe("resolveAgentModelTransport", () => { assertEquals((transport as { reasoning?: unknown }).reasoning, { enabled: true }); }); + it("keeps adaptive Anthropic thinking provider-native", async () => { + const hostModel = createModel("veryfront-cloud/anthropic/claude-opus-4-8"); + const config: AgentConfig = { + model: "veryfront-cloud/anthropic/claude-opus-4-8", + system: "You are a helpful assistant.", + resolveModelTransport: () => ({ model: hostModel }), + }; + + const transport = await resolveAgentModelTransport({ + agentId: "agent-1", + config, + context: undefined, + mode: "stream", + modelOverride: undefined, + }); + + assertEquals(transport.providerOptions, { + anthropic: { + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "high" }, + }, + }); + assertEquals(transport.reasoning, undefined); + }); + it("preserves provider option thinking opt-outs over Veryfront Cloud reasoning defaults", async () => { const hostModel = createModel("veryfront-cloud/moonshotai/kimi-k2.6"); const providerOptions = { openai: { thinking: { type: "disabled" } } }; diff --git a/src/provider/veryfront-cloud/provider.test.ts b/src/provider/veryfront-cloud/provider.test.ts index 68023f2d67..bec81a61d8 100644 --- a/src/provider/veryfront-cloud/provider.test.ts +++ b/src/provider/veryfront-cloud/provider.test.ts @@ -4,7 +4,9 @@ import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; import { agent } from "#veryfront/agent"; import { deleteEnv, setEnv } from "#veryfront/compat/process.ts"; import { clearEmbeddingProviders, resolveEmbeddingModel } from "#veryfront/embedding/index.ts"; +import { ensureBuiltinLLMProviders } from "#veryfront/extensions/builtin-extensions.ts"; import { clearModelProviders, resolveModel } from "#veryfront/provider"; +import type { ModelRuntime } from "#veryfront/provider/types.ts"; const CLOUD_ENV_KEYS = [ "VERYFRONT_API_TOKEN", @@ -61,6 +63,58 @@ describe("provider/veryfront-cloud", () => { assertEquals(model.modelProvider, "openai"); }); + it("preserves class runtime method receivers while adding cloud metadata", async () => { + setCloudBootstrap(); + + class PrivateFieldRuntime implements ModelRuntime { + [key: string]: unknown; + readonly #calls: string[] = []; + + prepare(): Promise { + this.#calls.push("prepare"); + return Promise.resolve(); + } + + doGenerate(): Promise<{ content: unknown[] }> { + this.#calls.push("generate"); + return Promise.resolve({ content: [] }); + } + + doStream(): Promise<{ stream: ReadableStream }> { + this.#calls.push("stream"); + return Promise.resolve({ stream: readableStreamFrom([]) }); + } + + calls(): string[] { + return [...this.#calls]; + } + } + + const runtime = new PrivateFieldRuntime(); + const registry = ensureBuiltinLLMProviders(); + const builtinOpenAI = registry.require("openai"); + registry.unregister("openai"); + registry.register({ + id: "openai", + createModel: () => runtime, + }); + + try { + const model = resolveModel("veryfront-cloud/openai/private-field-runtime"); + + await model.prepare?.(); + await model.doGenerate({}); + await model.doStream({}); + + assertEquals(runtime.calls(), ["prepare", "generate", "stream"]); + assertEquals(model._generateViaStream, true); + assertEquals(model.modelProvider, "openai"); + } finally { + registry.unregister("openai"); + registry.register(builtinOpenAI); + } + }); + it("routes agent.generate through the streaming Veryfront Cloud gateway path", async () => { setCloudBootstrap(); const encoder = new TextEncoder(); diff --git a/src/provider/veryfront-cloud/provider.ts b/src/provider/veryfront-cloud/provider.ts index 9cdb1bc8fe..c2adfd4da6 100644 --- a/src/provider/veryfront-cloud/provider.ts +++ b/src/provider/veryfront-cloud/provider.ts @@ -17,10 +17,18 @@ function wrapVeryfrontCloudModel( model: ModelRuntime, modelProvider: string, ): ModelRuntime { - return Object.create(model, { + const wrapped = Object.create(model, { _generateViaStream: { enumerable: true, value: true }, modelProvider: { enumerable: true, value: modelProvider }, }); + + Object.defineProperties(wrapped, { + doGenerate: { value: model.doGenerate.bind(model) }, + doStream: { value: model.doStream.bind(model) }, + ...(model.prepare ? { prepare: { value: model.prepare.bind(model) } } : {}), + }); + + return wrapped; } function shouldUseOpenAIResponsesRuntime(upstreamModelId: string): boolean { diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index 89adb39e00..0e30126a50 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -1,8 +1,15 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; +import { + assertEquals, + assertInstanceOf, + assertRejects, + assertStrictEquals, +} from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { metricsManager } from "#veryfront/observability/metrics/index.ts"; import { type AgentRunEvent, runWithRunEventSink } from "../agent/index.ts"; +import type { ModelRuntime } from "#veryfront/provider/types.ts"; +import { DurableRunEventPersistenceError } from "#veryfront/agent/conversation/private-run-event.ts"; import { runWithMandatoryRunEventSink } from "./run-event-sink-context.ts"; import { generateText, streamText } from "./runtime-bridge.ts"; import { @@ -667,7 +674,7 @@ describe("runtime-bridge", () => { }); try { - await assertRejects( + const error = await assertRejects( async () => await runWithMandatoryRunEventSink( () => { @@ -693,9 +700,11 @@ describe("runtime-bridge", () => { }), ), ), - TypeError, + DurableRunEventPersistenceError, "Mandatory model call context event is not cloneable", ); + assertInstanceOf(error, DurableRunEventPersistenceError); + assertStrictEquals(error.cause, cloneError); } finally { if (recorder && originalRecordError) recorder.recordError = originalRecordError; } @@ -750,6 +759,73 @@ describe("runtime-bridge", () => { } }); + it("omits reasoning when no canonical fields can be projected", async () => { + let recorded: AgentRunEvent | undefined; + const model = createGenerateModel("test", "test/empty-reasoning", async () => ({ + content: [], + finishReason: "stop", + usage: {}, + })); + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => + generateText({ + model, + messages: [{ role: "user", content: "Hello" }], + reasoning: { ignored: "provider-private" } as never, + }), + ); + + assertEquals(recorded?.request, undefined); + }); + + it("persists adaptive Anthropic thinking as canonical reasoning without raw provider options", async () => { + let recorded: AgentRunEvent | undefined; + const providerOptions = { + anthropic: { + thinking: { type: "adaptive", display: "summarized" }, + output_config: { effort: "high" }, + }, + }; + const model: ModelRuntime = { + provider: "veryfront-cloud", + modelId: "anthropic/claude-opus-4-8", + modelProvider: "anthropic", + async doGenerate(options) { + const dispatched = options as { + providerOptions?: Record; + reasoning?: unknown; + }; + assertEquals(dispatched.providerOptions, providerOptions); + assertEquals(dispatched.reasoning, undefined); + return { content: [], finishReason: "stop", usage: {} }; + }, + async doStream() { + throw new Error("unexpected stream dispatch"); + }, + }; + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => + generateText({ + model, + messages: [{ role: "user", content: "Hello" }], + providerOptions, + }), + ); + + assertEquals(recorded?.request, { + reasoning: { enabled: true, effort: "high" }, + }); + assertEquals("providerOptions" in (recorded?.request ?? {}), false); + }); + it("calls a sink shared by both lanes only once", async () => { let calls = 0; const sink = () => { diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index 453f56cf64..d5b3feb913 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -21,6 +21,7 @@ import type { ModelRuntimeGenerateResult, } from "#veryfront/provider/types.ts"; import type { RuntimeReasoningOption } from "#veryfront/agent/types.ts"; +import { DurableRunEventPersistenceError } from "#veryfront/agent/conversation/private-run-event.ts"; import type { ChatSystemMessage } from "#veryfront/chat/types.ts"; import type { AgentRunModelCallContextEvent, @@ -639,8 +640,17 @@ function buildDirectModelOptions( }; } -function buildModelCallRequest(options: ModelCallRequestSource): ModelCallRequest | undefined { - const reasoning = options.reasoning; +function buildModelCallRequest( + options: ModelCallRequestSource, + reasoning = options.reasoning, +): ModelCallRequest | undefined { + const projectedReasoning = reasoning + ? { + ...(reasoning.enabled !== undefined ? { enabled: reasoning.enabled } : {}), + ...(reasoning.effort !== undefined ? { effort: reasoning.effort } : {}), + ...(reasoning.budgetTokens !== undefined ? { budgetTokens: reasoning.budgetTokens } : {}), + } + : undefined; const request: ModelCallRequest = { ...(options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {}), ...(options.temperature !== undefined ? { temperature: options.temperature } : {}), @@ -652,14 +662,8 @@ function buildModelCallRequest(options: ModelCallRequestSource): ModelCallReques ...(options.frequencyPenalty !== undefined ? { frequencyPenalty: options.frequencyPenalty } : {}), - ...(reasoning - ? { - reasoning: { - ...(reasoning.enabled !== undefined ? { enabled: reasoning.enabled } : {}), - ...(reasoning.effort !== undefined ? { effort: reasoning.effort } : {}), - ...(reasoning.budgetTokens !== undefined ? { budgetTokens: reasoning.budgetTokens } : {}), - }, - } + ...(projectedReasoning && Object.keys(projectedReasoning).length > 0 + ? { reasoning: projectedReasoning } : {}), }; return Object.keys(request).length > 0 ? request : undefined; @@ -672,13 +676,52 @@ function resolveModelProvider(model: ModelRuntime): string | undefined { return model.provider === "veryfront-cloud" ? undefined : model.provider; } +function resolvePersistedReasoning( + model: ModelRuntime, + options: DirectModelOptions, +): RuntimeReasoningOption | undefined { + if (options.reasoning || resolveModelProvider(model) !== "anthropic") { + return options.reasoning; + } + + const providerOptions = options.providerOptions; + if (!providerOptions || typeof providerOptions !== "object" || Array.isArray(providerOptions)) { + return undefined; + } + const anthropic = readOwnEnumerableDataDescriptor(providerOptions, "anthropic")?.value; + if (!anthropic || typeof anthropic !== "object" || Array.isArray(anthropic)) { + return undefined; + } + const thinking = readOwnEnumerableDataDescriptor(anthropic, "thinking")?.value; + if (!thinking || typeof thinking !== "object" || Array.isArray(thinking)) { + return undefined; + } + if (readOwnEnumerableDataDescriptor(thinking, "type")?.value !== "adaptive") { + return undefined; + } + + const outputConfig = readOwnEnumerableDataDescriptor(anthropic, "output_config")?.value; + const effort = outputConfig && typeof outputConfig === "object" && !Array.isArray(outputConfig) + ? readOwnEnumerableDataDescriptor(outputConfig, "effort")?.value + : undefined; + return { + enabled: true, + ...(effort === "low" || effort === "medium" || effort === "high" || effort === "max" + ? { effort } + : {}), + }; +} + async function emitModelCallContextEvent( options: DirectTextOptions, directOptions: DirectModelOptions, ): Promise { const sinks = getActiveRunEventSinks(); if (!sinks.mandatory && !sinks.public) return; - const request = buildModelCallRequest(directOptions); + const request = buildModelCallRequest( + directOptions, + resolvePersistedReasoning(options.model, directOptions), + ); const event: AgentRunModelCallContextEvent = { type: "AGENT_RUN_MODEL_CALL_CONTEXT", @@ -697,9 +740,11 @@ async function emitModelCallContextEvent( ...(directOptions.tools ? { tools: directOptions.tools } : {}), }; - const cloneEvent = (): AgentRunModelCallContextEvent | null => { + const cloneEvent = (): + | { ok: true; event: AgentRunModelCallContextEvent } + | { ok: false; error: unknown } => { try { - return cloneStructuredValue(event); + return { ok: true, event: cloneStructuredValue(event) }; } catch (error) { const failureClass = error instanceof DOMException && error.name === "DataCloneError" ? "DataCloneError" @@ -711,14 +756,19 @@ async function emitModelCallContextEvent( logger.warn("Model call context event was not persisted because it is not cloneable", { failureClass, }); - return null; + return { ok: false, error }; } }; - const mandatoryEvent = sinks.mandatory ? cloneEvent() : undefined; - if (sinks.mandatory && !mandatoryEvent) { - throw new TypeError("Mandatory model call context event is not cloneable"); + const mandatoryClone = sinks.mandatory ? cloneEvent() : undefined; + if (mandatoryClone?.ok === false) { + throw new DurableRunEventPersistenceError( + "Mandatory model call context event is not cloneable", + { cause: mandatoryClone.error }, + ); } - const publicEvent = sinks.public && sinks.public !== sinks.mandatory ? cloneEvent() : undefined; + const mandatoryEvent = mandatoryClone?.ok ? mandatoryClone.event : undefined; + const publicClone = sinks.public && sinks.public !== sinks.mandatory ? cloneEvent() : undefined; + const publicEvent = publicClone?.ok ? publicClone.event : undefined; if (sinks.mandatory && mandatoryEvent) { await sinks.mandatory(mandatoryEvent); } From ec4f603b3528a86d74a40f8c47bc6db02c3e9c31 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 16:10:10 +0200 Subject: [PATCH 07/16] docs: refresh agent API reference --- docs/api-reference/veryfront/agent.md | 16 ++++++++-------- docs/api-reference/veryfront/embedding.md | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 7236e13a11..3a7d23cd4d 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -636,11 +636,11 @@ Input delivered to a hosted agent-service detached execution callback. | `createAgUiBrowserChunkEncoder` | Create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L101) | | `createAgUiBrowserEncoderState` | State for create AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L90) | | `createAgUiBrowserFinalizeTracker` | Create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L23) | -| `createAgUiBrowserResponseStream` | Create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L62) | +| `createAgUiBrowserResponseStream` | Create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L68) | | `createAgUiCancelHandler` | Handler for create AG-UI cancel. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/run-control.ts#L148) | | `createAgUiChatUiChunkBrowserEncoder` | Create AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L260) | | `createAgUiChatUiTrackedBrowserResponse` | Response payload for create AG-UI chat UI tracked browser. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L279) | -| `createAgUiChunkEncoderBridge` | Create AG-UI chunk encoder bridge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chunk-encoder-bridge.ts#L31) | +| `createAgUiChunkEncoderBridge` | Create AG-UI chunk encoder bridge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chunk-encoder-bridge.ts#L33) | | `createAgUiDetachedStartHandler` | Handler for create AG-UI detached start. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/detached-start.ts#L407) | | `createAgUiHandler` | Handler for create AG-UI. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/handler.ts#L527) | | `createAgUiResumeHandler` | Handler for create AG-UI resume. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/run-control.ts#L78) | @@ -699,7 +699,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createHostedChildMirrorContext` | Context for create hosted child mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-mirror.ts#L233) | | `createHostedChildPendingToolLifecycle` | Create hosted child pending tool lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-pending-tool-lifecycle.ts#L98) | | `createHostedChildPendingToolLifecycleLogger` | Create hosted child pending tool lifecycle logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-pending-tool-lifecycle.ts#L55) | -| `createHostedConversationRunChunkMirror` | Create hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L380) | +| `createHostedConversationRunChunkMirror` | Create hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L382) | | `createHostedDurableChildForkRunContext` | Context for create hosted durable child fork run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L179) | | `createHostedDurableChildInvokeTraceRecorder` | Create hosted durable child invoke trace recorder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/durable-child-fork-execution.ts#L312) | | `createHostedFormInputTool` | Create hosted form input tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/form-input-tool.ts#L34) | @@ -1184,9 +1184,9 @@ Input delivered to a hosted agent-service detached execution callback. | `AgUiBrowserEncodedEvent` | Event emitted for AG-UI browser encoded. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L84) | | `AgUiBrowserEncoderState` | State for AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L35) | | `AgUiBrowserFinalizeTracker` | Public API contract for AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L9) | -| `AgUiBrowserResponseEncoder` | Public API contract for AG-UI browser response encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L37) | -| `AgUiBrowserResponseExecution` | Public API contract for AG-UI browser response execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L30) | -| `AgUiBrowserResponseRequestState` | State for AG-UI browser response request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L22) | +| `AgUiBrowserResponseEncoder` | Public API contract for AG-UI browser response encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L41) | +| `AgUiBrowserResponseExecution` | Public API contract for AG-UI browser response execution. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L34) | +| `AgUiBrowserResponseRequestState` | State for AG-UI browser response request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L26) | | `AgUiBrowserRunFinishedMetadata` | Public API contract for AG-UI browser run finished metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L7) | | `AgUiCancelHandlerOptions` | Options accepted by AG-UI cancel handler. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/run-control.ts#L59) | | `AgUiChatUiChunkBrowserEncoder` | Public API contract for AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L17) | @@ -1304,10 +1304,10 @@ Input delivered to a hosted agent-service detached execution callback. | `CreateAgentServiceServerRuntimeOptions` | Options accepted by create agent service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/server.ts#L19) | | `CreateAgUiBrowserChunkEncoderOptions` | Options accepted by create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L22) | | `CreateAgUiBrowserFinalizeTrackerOptions` | Options accepted by create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L16) | -| `CreateAgUiBrowserResponseStreamInput` | Input payload for create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L43) | +| `CreateAgUiBrowserResponseStreamInput` | Input payload for create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L49) | | `CreateAgUiChatUiChunkBrowserEncoderOptions` | Options accepted by create AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L23) | | `CreateAgUiChatUiTrackedBrowserResponseInput` | Input payload for create AG-UI chat UI tracked browser response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L35) | -| `CreateAgUiChunkEncoderBridgeOptions` | Options accepted by create AG-UI chunk encoder bridge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chunk-encoder-bridge.ts#L20) | +| `CreateAgUiChunkEncoderBridgeOptions` | Options accepted by create AG-UI chunk encoder bridge. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chunk-encoder-bridge.ts#L22) | | `CreateAgUiRuntimeBrowserResponseInput` | Input payload for create AG-UI runtime browser response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/runtime-browser-response.ts#L14) | | `CreateAgUiRuntimeChatStreamEncoderOptions` | Options accepted by create AG-UI runtime chat stream encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/runtime-chat-stream-encoder.ts#L54) | | `CreateAgUiRuntimeEventEncoderOptions` | Options accepted by create AG-UI runtime event encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/runtime-event-encoder.ts#L21) | diff --git a/docs/api-reference/veryfront/embedding.md b/docs/api-reference/veryfront/embedding.md index 04e624ca7e..4a609604ca 100644 --- a/docs/api-reference/veryfront/embedding.md +++ b/docs/api-reference/veryfront/embedding.md @@ -42,7 +42,7 @@ export const { POST, GET, DELETE } = createUploadHandler(store, { | `ragStore` | Creates a persistent RAG store with lazy embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/rag-store.ts#L212) | | `registerEmbeddingProvider` | Register an embedding provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L25) | | `resolveEmbeddingModel` | Resolve a "provider/model" string to an embedding runtime instance. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L116) | -| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1192) | +| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1242) | | `vectorStore` | Creates an in-memory vector store with integrated embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/vector-store.ts#L46) | ### Types From de71b240d2242bd5b6618d8e70d21ff62136c8da Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 16:25:58 +0200 Subject: [PATCH 08/16] fix(agent): preserve runtime timing and metadata --- src/agent/ag-ui/browser-chunk-encoder.test.ts | 9 +++ src/agent/ag-ui/browser-chunk-encoder.ts | 3 + .../ag-ui/chat-ui-chunk-browser-encoder.ts | 2 +- .../ag-ui/tracked-browser-response.test.ts | 58 +++++++++++++++++++ src/agent/ag-ui/tracked-browser-response.ts | 6 +- .../conversation/private-run-event.test.ts | 36 ++++++++++++ src/provider/veryfront-cloud/provider.test.ts | 18 ++++++ src/provider/veryfront-cloud/provider.ts | 19 ++++++ 8 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/agent/ag-ui/browser-chunk-encoder.test.ts b/src/agent/ag-ui/browser-chunk-encoder.test.ts index 116c300e52..adb7f81c52 100644 --- a/src/agent/ag-ui/browser-chunk-encoder.test.ts +++ b/src/agent/ag-ui/browser-chunk-encoder.test.ts @@ -4,6 +4,15 @@ import { describe, it } from "#veryfront/testing/bdd.ts"; import { createAgUiBrowserChunkEncoder } from "./browser-chunk-encoder.ts"; describe("agent/ag-ui-browser-chunk-encoder", () => { + it("exposes its state as the browser response timing anchor", () => { + const encoder = createAgUiBrowserChunkEncoder({ + getRuntimeEvents: () => [], + timing: { nowMs: null, epochMs: null }, + }); + + assertEquals(encoder.timingState, encoder.state); + }); + it("merges chunk metadata into the browser finalize response", () => { const encoder = createAgUiBrowserChunkEncoder<{ id: string; diff --git a/src/agent/ag-ui/browser-chunk-encoder.ts b/src/agent/ag-ui/browser-chunk-encoder.ts index d04922e63c..45e198e439 100644 --- a/src/agent/ag-ui/browser-chunk-encoder.ts +++ b/src/agent/ag-ui/browser-chunk-encoder.ts @@ -14,6 +14,8 @@ import type { AgentResponse } from "../types.ts"; /** Public API contract for AG-UI browser chunk encoder. */ export interface AgUiBrowserChunkEncoder { state: AgUiBrowserEncoderState; + /** Timing anchor consumed by the browser response composition root. */ + timingState: AgUiBrowserEncoderState; encode: (chunk: TChunk) => AgUiBrowserEncodedEvent[]; finalize: (response: AgentResponse | null) => AgUiBrowserEncodedEvent[]; } @@ -108,6 +110,7 @@ export function createAgUiBrowserChunkEncoder( return { state: runtimeEventEncoder.state, + timingState: runtimeEventEncoder.state, encode: (chunk) => { mergeMetadata(runtimeEventEncoder.state.metadata, options.getMetadataFromChunk?.(chunk)); return options.getRuntimeEvents(chunk).flatMap((event) => runtimeEventEncoder.encode(event)); diff --git a/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts b/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts index 3e748d6a32..a66346de3b 100644 --- a/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts +++ b/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts @@ -16,7 +16,7 @@ import { /** Public API contract for AG-UI chat UI chunk browser encoder. */ export type AgUiChatUiChunkBrowserEncoder = Pick< AgUiBrowserChunkEncoder>, - "encode" | "finalize" + "encode" | "finalize" | "timingState" >; /** Options accepted by create AG-UI chat UI chunk browser encoder. */ diff --git a/src/agent/ag-ui/tracked-browser-response.test.ts b/src/agent/ag-ui/tracked-browser-response.test.ts index 547a369bbb..f877fce6d0 100644 --- a/src/agent/ag-ui/tracked-browser-response.test.ts +++ b/src/agent/ag-ui/tracked-browser-response.test.ts @@ -2,9 +2,18 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { createAgUiBrowserFinalizeTracker } from "./browser-finalize-tracker.ts"; +import { createAgUiBrowserEncoderState } from "./browser-encoder.ts"; import { createAgUiChunkEncoderBridge } from "./chunk-encoder-bridge.ts"; import { createAgUiTrackedBrowserResponse } from "./tracked-browser-response.ts"; +function parseSseFrames(body: string): Array<{ event: string; data: Record }> { + return body.split("\n\n").flatMap((frame) => { + const event = /^event: (.+)$/m.exec(frame)?.[1]; + const data = /^data: (.+)$/m.exec(frame)?.[1]; + return event && data ? [{ event, data: JSON.parse(data) as Record }] : []; + }); +} + describe("agent/ag-ui-tracked-browser-response", () => { it("combines chunk encoding and finalize tracking into one browser response helper", async () => { type Chunk = { @@ -80,6 +89,7 @@ describe("agent/ag-ui-tracked-browser-response", () => { waitForFinish: async () => {}, }, chunkEncoder: { + timingState: createAgUiBrowserEncoderState({ nowMs: null, epochMs: null }), encode: () => [{ event: "RunError", payload: { message: "boom" } }], finalize: () => [], }, @@ -92,4 +102,52 @@ describe("agent/ag-ui-tracked-browser-response", () => { assertStringIncludes(text, "event: RunError"); assertEquals(text.includes("finishReason"), false); }); + + it("shares an injected run timing anchor across bootstrap, chunk, and final events", async () => { + let now = 150; + const response = createAgUiTrackedBrowserResponse({ + agUiInput: { + threadId: "thread-timing", + runId: "run-timing", + messages: [], + tools: [], + context: [], + }, + agentId: "agent-1", + execution: { + agentUIStream: { + async *[Symbol.asyncIterator]() { + now = 175; + yield { messageId: "msg-1" }; + }, + }, + fail: async () => {}, + waitForFinish: async () => { + now = 200; + }, + }, + chunkEncoder: createAgUiChunkEncoderBridge({ + getRuntimeEvents: (chunk: { messageId: string }) => [ + { type: "message-start", messageId: chunk.messageId }, + { type: "text-start", id: chunk.messageId }, + ], + timing: { nowMs: () => now, epochMs: null, startedMs: 100 }, + }), + finalizeTracker: createAgUiBrowserFinalizeTracker({ + getMetadataFromChunk: () => ({ finishReason: "stop" }), + }), + }); + + const frames = parseSseFrames(await response.text()); + assertEquals( + frames.filter((frame) => + ["RunStarted", "TextMessageStart", "RunFinished"].includes(frame.event) + ).map((frame) => [frame.event, frame.data.elapsedMs]), + [ + ["RunStarted", 50], + ["TextMessageStart", 75], + ["RunFinished", 100], + ], + ); + }); }); diff --git a/src/agent/ag-ui/tracked-browser-response.ts b/src/agent/ag-ui/tracked-browser-response.ts index 8d5bda78d3..87a6b3a7ce 100644 --- a/src/agent/ag-ui/tracked-browser-response.ts +++ b/src/agent/ag-ui/tracked-browser-response.ts @@ -13,7 +13,10 @@ export interface CreateAgUiTrackedBrowserResponseInput extends CreateAgUiRuntimeBrowserResponseInput, "encoder" | "initialState" | "onChunk" | "getFinalResponse" > { - chunkEncoder: Pick, "encode" | "finalize">; + chunkEncoder: Pick< + AgUiChunkEncoderBridge, + "encode" | "finalize" | "timingState" + >; finalizeTracker: Pick< AgUiBrowserFinalizeTracker, "observeChunk" | "observeEncodedEvents" | "getFinalResponse" @@ -27,6 +30,7 @@ export function createAgUiTrackedBrowserResponse( return createAgUiRuntimeBrowserResponse({ ...input, encoder: { + timingState: input.chunkEncoder.timingState, encode: (chunk) => { const events = input.chunkEncoder.encode(chunk); input.finalizeTracker.observeEncodedEvents(events); diff --git a/src/agent/conversation/private-run-event.test.ts b/src/agent/conversation/private-run-event.test.ts index 6272530938..865a25bec1 100644 --- a/src/agent/conversation/private-run-event.test.ts +++ b/src/agent/conversation/private-run-event.test.ts @@ -27,6 +27,19 @@ describe("agent/conversation/private-run-event", () => { }), true, ); + assertEquals( + isPrivateConversationRunEvent({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [{ + role: "system", + content: "Cache safely.", + providerOptions: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } }, + }, + }], + }), + true, + ); for ( const value of [ @@ -45,6 +58,29 @@ describe("agent/conversation/private-run-event", () => { messages: [], request: { reasoning: { arbitrary: true } }, }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [{ + role: "system", + content: "Do not persist provider secrets.", + providerOptions: { + anthropic: { + cacheControl: { type: "ephemeral" }, + apiKey: "secret", + }, + }, + }], + }, + { + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [{ + role: "system", + content: "Reject unsupported cache policy.", + providerOptions: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "2h" } }, + }, + }], + }, { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [{ diff --git a/src/provider/veryfront-cloud/provider.test.ts b/src/provider/veryfront-cloud/provider.test.ts index bec81a61d8..41d24f4bbc 100644 --- a/src/provider/veryfront-cloud/provider.test.ts +++ b/src/provider/veryfront-cloud/provider.test.ts @@ -69,6 +69,21 @@ describe("provider/veryfront-cloud", () => { class PrivateFieldRuntime implements ModelRuntime { [key: string]: unknown; readonly #calls: string[] = []; + readonly #modelId = "private-field-runtime"; + readonly #provider = "private-provider"; + readonly #runtimeCapabilities = { toolCalling: true } as const; + + get modelId(): string { + return this.#modelId; + } + + get provider(): string { + return this.#provider; + } + + get runtimeCapabilities(): { readonly toolCalling: true } { + return this.#runtimeCapabilities; + } prepare(): Promise { this.#calls.push("prepare"); @@ -107,6 +122,9 @@ describe("provider/veryfront-cloud", () => { await model.doStream({}); assertEquals(runtime.calls(), ["prepare", "generate", "stream"]); + assertEquals(model.modelId, "private-field-runtime"); + assertEquals(model.provider, "private-provider"); + assertEquals(model.runtimeCapabilities, { toolCalling: true }); assertEquals(model._generateViaStream, true); assertEquals(model.modelProvider, "openai"); } finally { diff --git a/src/provider/veryfront-cloud/provider.ts b/src/provider/veryfront-cloud/provider.ts index c2adfd4da6..d7e410ff9e 100644 --- a/src/provider/veryfront-cloud/provider.ts +++ b/src/provider/veryfront-cloud/provider.ts @@ -28,6 +28,25 @@ function wrapVeryfrontCloudModel( ...(model.prepare ? { prepare: { value: model.prepare.bind(model) } } : {}), }); + const forwardedAccessors = new Set(); + let source: object | null = model; + while (source && source !== Object.prototype) { + for (const key of Reflect.ownKeys(source)) { + if (forwardedAccessors.has(key) || Object.hasOwn(wrapped, key)) continue; + + forwardedAccessors.add(key); + const descriptor = Object.getOwnPropertyDescriptor(source, key); + if (!descriptor || (!descriptor.get && !descriptor.set)) continue; + + Object.defineProperty(wrapped, key, { + ...descriptor, + get: descriptor.get?.bind(model), + set: descriptor.set?.bind(model), + }); + } + source = Object.getPrototypeOf(source); + } + return wrapped; } From dcdd874310288cad22e20f6b2ef6d724efa6ac5d Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 16:28:15 +0200 Subject: [PATCH 09/16] fix(agent): project enabled Anthropic thinking --- src/runtime/runtime-bridge.test.ts | 29 +++++++++++++++++++++++++++++ src/runtime/runtime-bridge.ts | 13 ++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index 0e30126a50..ae4c3a0867 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -826,6 +826,35 @@ describe("runtime-bridge", () => { assertEquals("providerOptions" in (recorded?.request ?? {}), false); }); + it("persists enabled Anthropic thinking with its canonical token budget", async () => { + let recorded: AgentRunEvent | undefined; + const model: ModelRuntime = { + provider: "veryfront-cloud", + modelId: "anthropic/claude-sonnet-4-6", + modelProvider: "anthropic", + async doGenerate() { + return { content: [], finishReason: "stop", usage: {} }; + }, + async doStream() { + throw new Error("unexpected stream dispatch"); + }, + }; + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => + generateText({ + model, + messages: [{ role: "user", content: "Hello" }], + providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2048 } } }, + }), + ); + + assertEquals(recorded?.request, { reasoning: { enabled: true, budgetTokens: 2048 } }); + }); + it("calls a sink shared by both lanes only once", async () => { let calls = 0; const sink = () => { diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index d5b3feb913..90b91b6a2c 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -696,10 +696,21 @@ function resolvePersistedReasoning( if (!thinking || typeof thinking !== "object" || Array.isArray(thinking)) { return undefined; } - if (readOwnEnumerableDataDescriptor(thinking, "type")?.value !== "adaptive") { + const thinkingType = readOwnEnumerableDataDescriptor(thinking, "type")?.value; + if (thinkingType !== "adaptive" && thinkingType !== "enabled") { return undefined; } + if (thinkingType === "enabled") { + const budgetTokens = readOwnEnumerableDataDescriptor(thinking, "budget_tokens")?.value; + return { + enabled: true, + ...(typeof budgetTokens === "number" && Number.isInteger(budgetTokens) && budgetTokens >= 0 + ? { budgetTokens } + : {}), + }; + } + const outputConfig = readOwnEnumerableDataDescriptor(anthropic, "output_config")?.value; const effort = outputConfig && typeof outputConfig === "object" && !Array.isArray(outputConfig) ? readOwnEnumerableDataDescriptor(outputConfig, "effort")?.value From 11924fcd2a1ad4b7c23796729ce6359f66b154ea Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 16:42:45 +0200 Subject: [PATCH 10/16] fix(agent): enforce durable context contracts --- docs/api-reference/veryfront/agent.md | 12 ++--- docs/api-reference/veryfront/embedding.md | 2 +- src/agent/ag-ui/browser-encoder.test.ts | 57 +++++++++++++++++++- src/agent/ag-ui/browser-encoder.ts | 64 +++++++++++++---------- src/agent/conversation/run-events.test.ts | 57 +++++++++++++++++++- src/agent/conversation/run-events.ts | 44 ++++++++++------ src/runtime/model-call-context.test.ts | 62 +++++++++++++++++++--- src/runtime/model-call-context.ts | 37 +++++++++---- src/runtime/runtime-bridge.test.ts | 43 +++++++++++++++ src/runtime/runtime-bridge.ts | 15 ++++-- 10 files changed, 319 insertions(+), 74 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 3a7d23cd4d..814b81d8f6 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -633,7 +633,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createAgentServiceRouteSet` | Create hosted agent service route set. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/routes.ts#L209) | | `createAgentServiceRuntime` | Create agent service runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/runtime.ts#L233) | | `createAgentServiceServerRuntime` | Create agent service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/server.ts#L54) | -| `createAgUiBrowserChunkEncoder` | Create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L101) | +| `createAgUiBrowserChunkEncoder` | Create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L103) | | `createAgUiBrowserEncoderState` | State for create AG-UI browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L90) | | `createAgUiBrowserFinalizeTracker` | Create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L23) | | `createAgUiBrowserResponseStream` | Create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L68) | @@ -652,7 +652,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createAgUiRuntimeHandler` | Handler for create AG-UI runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/runtime-handler.ts#L406) | | `createAgUiSseErrorResponse` | Response payload for create AG-UI sse error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/host-support.ts#L440) | | `createAgUiSseResponse` | Response payload for create AG-UI sse. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/host-support.ts#L453) | -| `createAgUiTrackedBrowserResponse` | Response payload for create AG-UI tracked browser. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/tracked-browser-response.ts#L24) | +| `createAgUiTrackedBrowserResponse` | Response payload for create AG-UI tracked browser. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/tracked-browser-response.ts#L27) | | `createBootstrappedHostedChatExecutionRuntime` | Create bootstrapped hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-execution-runtime.ts#L438) | | `createChatUiMessageStreamFromDataStream` | Create chat UI message stream from data stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/chat-ui-message-stream.ts#L608) | | `createConversationAgentRun` | Create conversation agent run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L1245) | @@ -746,7 +746,7 @@ Input delivered to a hosted agent-service detached execution callback. | `dispatchConversationHostedStreamErrorState` | State for dispatch conversation hosted stream error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L106) | | `dispatchConversationHostedTerminalState` | State for dispatch conversation hosted terminal. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L91) | | `doesProjectAgentRuntimeAgentMatchSource` | Does project agent runtime agent match source helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/project/agent-runtime.ts#L173) | -| `encodeConversationRunEvents` | Encode conversation run events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L379) | +| `encodeConversationRunEvents` | Encode conversation run events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L393) | | `ensureConversationProjectLink` | Ensure conversation project link helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L148) | | `evaluateSlashCommandArtifactPolicy` | Evaluate slash command artifact policy helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/slash-command-artifact-policy.ts#L200) | | `evaluateStarterIntentTurnPolicy` | Evaluate starter intent turn policy helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/delegation-policy.ts#L207) | @@ -772,7 +772,7 @@ Input delivered to a hosted agent-service detached execution callback. | `fetchLatestConversationUserText` | Fetch latest conversation user text helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/default-research-artifact-support.ts#L112) | | `filterAgentTraceAttributes` | Filter agent trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L61) | | `filterHostedChatRuntimeLocalTools` | Filter hosted chat runtime local tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-runtime-tool-assembly.ts#L223) | -| `finalizeAgUiBrowserEvents` | Finalize AG-UI browser events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L974) | +| `finalizeAgUiBrowserEvents` | Finalize AG-UI browser events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-encoder.ts#L984) | | `finalizeChildRunExecutionResources` | Finalize child run execution resources helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/child-run/execution-cleanup.ts#L28) | | `finalizeConversationAgentRun` | Finalize conversation agent run helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L1326) | | `finalizeHostedChildForkCompletion` | Finalize hosted child fork completion helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-stream-execution.ts#L156) | @@ -880,7 +880,7 @@ Input delivered to a hosted agent-service detached execution callback. | `normalizeChatUiMessageStream` | Normalizes chat UI message stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L494) | | `normalizeConversationRunEvent` | Event emitted for normalize conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-normalization.ts#L36) | | `normalizeConversationRunEvents` | Normalizes conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-normalization.ts#L95) | -| `normalizeEncodedConversationRunEvents` | Normalizes encoded conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L387) | +| `normalizeEncodedConversationRunEvents` | Normalizes encoded conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L401) | | `normalizeHostedChildArtifactPath` | Normalizes hosted child artifact path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-artifact-support.ts#L133) | | `normalizeParsedAgentServiceChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | | `normalizeParsedHostedChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | @@ -1302,7 +1302,7 @@ Input delivered to a hosted agent-service detached execution callback. | `CreateAgentServiceRegistrationLifecycleOptions` | Options accepted by create agent service registration lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L199) | | `CreateAgentServiceRuntimeOptions` | Options accepted by create agent service runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/runtime.ts#L96) | | `CreateAgentServiceServerRuntimeOptions` | Options accepted by create agent service server runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/server.ts#L19) | -| `CreateAgUiBrowserChunkEncoderOptions` | Options accepted by create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L22) | +| `CreateAgUiBrowserChunkEncoderOptions` | Options accepted by create AG-UI browser chunk encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-chunk-encoder.ts#L24) | | `CreateAgUiBrowserFinalizeTrackerOptions` | Options accepted by create AG-UI browser finalize tracker. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-finalize-tracker.ts#L16) | | `CreateAgUiBrowserResponseStreamInput` | Input payload for create AG-UI browser response stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/browser-response-stream.ts#L49) | | `CreateAgUiChatUiChunkBrowserEncoderOptions` | Options accepted by create AG-UI chat UI chunk browser encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/chat-ui-chunk-browser-encoder.ts#L23) | diff --git a/docs/api-reference/veryfront/embedding.md b/docs/api-reference/veryfront/embedding.md index 4a609604ca..23acc34ded 100644 --- a/docs/api-reference/veryfront/embedding.md +++ b/docs/api-reference/veryfront/embedding.md @@ -42,7 +42,7 @@ export const { POST, GET, DELETE } = createUploadHandler(store, { | `ragStore` | Creates a persistent RAG store with lazy embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/rag-store.ts#L212) | | `registerEmbeddingProvider` | Register an embedding provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L25) | | `resolveEmbeddingModel` | Resolve a "provider/model" string to an embedding runtime instance. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L116) | -| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1242) | +| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1258) | | `vectorStore` | Creates an in-memory vector store with integrated embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/vector-store.ts#L46) | ### Types diff --git a/src/agent/ag-ui/browser-encoder.test.ts b/src/agent/ag-ui/browser-encoder.test.ts index de3f29b859..7c878e2e42 100644 --- a/src/agent/ag-ui/browser-encoder.test.ts +++ b/src/agent/ag-ui/browser-encoder.test.ts @@ -1,11 +1,12 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { buildAgUiBrowserFinalizeResponse, createAgUiBrowserEncoderState, finalizeAgUiBrowserEvents, mapRuntimeStreamEventToAgUiBrowserEvents, + stampAgUiBrowserEventTiming, } from "./browser-encoder.ts"; describe("agent/ag-ui-browser-encoder", () => { @@ -174,6 +175,60 @@ describe("agent/ag-ui-browser-encoder", () => { assertEquals("elapsedMs" in (events[0]?.payload ?? {}), false); }); + it("preserves valid supplied timing and rejects invalid present timing", () => { + const state = createAgUiBrowserEncoderState({ + nowMs: () => Number.NaN, + epochMs: () => -1, + }); + const supplied = stampAgUiBrowserEventTiming(state, [{ + event: "Custom", + payload: { elapsedMs: 12.5, emittedAt: 1_786_866_357_364 }, + }]); + assertEquals(supplied[0]?.payload.elapsedMs, 12.5); + assertEquals(supplied[0]?.payload.emittedAt, 1_786_866_357_364); + + assertThrows( + () => + stampAgUiBrowserEventTiming(state, [{ + event: "Custom", + payload: { elapsedMs: Number.POSITIVE_INFINITY, emittedAt: 1_786_866_357_364 }, + }]), + TypeError, + "elapsedMs must be a finite non-negative number", + ); + assertThrows( + () => + stampAgUiBrowserEventTiming(state, [{ + event: "Custom", + payload: { elapsedMs: 0, emittedAt: -1 }, + }]), + TypeError, + "emittedAt must be a non-negative integer", + ); + }); + + it("rejects timing generated by invalid clocks", () => { + const invalidElapsed = createAgUiBrowserEncoderState({ + nowMs: (() => { + let reads = 0; + return () => reads++ === 0 ? 0 : Number.NaN; + })(), + epochMs: null, + }); + assertThrows( + () => mapRuntimeStreamEventToAgUiBrowserEvents(invalidElapsed, { type: "start-step" }), + TypeError, + "elapsedMs must be a finite non-negative number", + ); + + const invalidEpoch = createAgUiBrowserEncoderState({ nowMs: null, epochMs: () => -1 }); + assertThrows( + () => mapRuntimeStreamEventToAgUiBrowserEvents(invalidEpoch, { type: "start-step" }), + TypeError, + "emittedAt must be a non-negative integer", + ); + }); + it("clocks the state unless a caller explicitly opts out", () => { // Three production composition roots build this state. An opt-in clock only // has to be missed at one of them to lose elapsedMs for every hosted run, diff --git a/src/agent/ag-ui/browser-encoder.ts b/src/agent/ag-ui/browser-encoder.ts index 225cf87099..3b874b27f3 100644 --- a/src/agent/ag-ui/browser-encoder.ts +++ b/src/agent/ag-ui/browser-encoder.ts @@ -703,37 +703,47 @@ export function stampAgUiBrowserEventTiming( // wall-clock traces and logs, and turns ingest lag into `created_at - // emittedAt`. Both are stamped because wall clocks can step backwards and // the monotonic reading cannot. - const timing: Record = {}; - if (state.nowMs && state.startedMs !== undefined) { - timing.elapsedMs = Math.max(0, Math.round(state.nowMs() - state.startedMs)); + for (const { payload } of events) { + if (Object.hasOwn(payload, "elapsedMs")) assertValidElapsedMs(payload.elapsedMs); + if (Object.hasOwn(payload, "emittedAt")) assertValidEmittedAt(payload.emittedAt); } - if (state.epochMs) { - timing.emittedAt = Math.round(state.epochMs()); - } - if (Object.keys(timing).length === 0) { + + const needsElapsedMs = events.some(({ payload }) => !Object.hasOwn(payload, "elapsedMs")); + const needsEmittedAt = events.some(({ payload }) => !Object.hasOwn(payload, "emittedAt")); + const elapsedMs = needsElapsedMs && state.nowMs && state.startedMs !== undefined + ? Math.max(0, Math.round(state.nowMs() - state.startedMs)) + : undefined; + const emittedAt = needsEmittedAt && state.epochMs ? Math.round(state.epochMs()) : undefined; + if (elapsedMs !== undefined) assertValidElapsedMs(elapsedMs); + if (emittedAt !== undefined) assertValidEmittedAt(emittedAt); + if (elapsedMs === undefined && emittedAt === undefined) { return events; } - return events.map((entry) => { - const elapsedMs = entry.payload.elapsedMs; - const emittedAt = entry.payload.emittedAt; - return { - ...entry, - payload: { - ...entry.payload, - ...(typeof elapsedMs === "number" && Number.isFinite(elapsedMs) && elapsedMs >= 0 - ? { elapsedMs } - : timing.elapsedMs === undefined - ? {} - : { elapsedMs: timing.elapsedMs }), - ...(typeof emittedAt === "number" && Number.isInteger(emittedAt) && emittedAt >= 0 - ? { emittedAt } - : timing.emittedAt === undefined - ? {} - : { emittedAt: timing.emittedAt }), - }, - }; - }); + return events.map((entry) => ({ + ...entry, + payload: { + ...entry.payload, + ...(elapsedMs !== undefined && !Object.hasOwn(entry.payload, "elapsedMs") + ? { elapsedMs } + : {}), + ...(emittedAt !== undefined && !Object.hasOwn(entry.payload, "emittedAt") + ? { emittedAt } + : {}), + }, + })); +} + +function assertValidElapsedMs(value: unknown): asserts value is number { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + throw new TypeError("elapsedMs must be a finite non-negative number"); + } +} + +function assertValidEmittedAt(value: unknown): asserts value is number { + if (typeof value !== "number" || !Number.isInteger(value) || value < 0) { + throw new TypeError("emittedAt must be a non-negative integer"); + } } function mapRuntimeStreamEventToAgUiBrowserEventsUnstamped( diff --git a/src/agent/conversation/run-events.test.ts b/src/agent/conversation/run-events.test.ts index 24fb350e9a..03096a1c31 100644 --- a/src/agent/conversation/run-events.test.ts +++ b/src/agent/conversation/run-events.test.ts @@ -1,5 +1,5 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { ConversationRunEventEncoder, @@ -283,6 +283,61 @@ describe("agent/conversation-run-events", () => { assertEquals(later[0]?.elapsedMs, 7000, "a later event carries a later elapsed"); }); + it("preserves valid supplied timing and rejects invalid present timing", () => { + const encoder = new ConversationRunEventEncoder({ + nowMs: () => Number.NaN, + epochMs: () => -1, + startedMs: 0, + }); + const supplied = encoder.stamp([{ + type: conversationRunEventTypes.custom, + elapsedMs: 12.5, + emittedAt: 1_786_866_357_364, + }]); + assertEquals(supplied[0]?.elapsedMs, 12.5); + assertEquals(supplied[0]?.emittedAt, 1_786_866_357_364); + + assertThrows( + () => + encoder.stamp([{ + type: conversationRunEventTypes.custom, + elapsedMs: -1, + emittedAt: 1_786_866_357_364, + }]), + TypeError, + "elapsedMs must be a finite non-negative number", + ); + assertThrows( + () => + encoder.stamp([{ + type: conversationRunEventTypes.custom, + elapsedMs: 0, + emittedAt: 1.5, + }]), + TypeError, + "emittedAt must be a non-negative integer", + ); + }); + + it("rejects timing generated by invalid clocks", () => { + const invalidElapsed = new ConversationRunEventEncoder({ + nowMs: () => Number.NaN, + startedMs: 0, + }); + assertThrows( + () => invalidElapsed.encode({ type: "text-delta", id: "text:0", delta: "hi" }), + TypeError, + "elapsedMs must be a finite non-negative number", + ); + + const invalidEpoch = new ConversationRunEventEncoder({ epochMs: () => -1 }); + assertThrows( + () => invalidEpoch.encode({ type: "text-delta", id: "text:0", delta: "hi" }), + TypeError, + "emittedAt must be a non-negative integer", + ); + }); + it("omits elapsedMs entirely when no clock is supplied", () => { const encoder = new ConversationRunEventEncoder(); encoder.encode({ type: "start", messageId: "msg-no-clock" }); diff --git a/src/agent/conversation/run-events.ts b/src/agent/conversation/run-events.ts index debfc8cf63..903fede576 100644 --- a/src/agent/conversation/run-events.ts +++ b/src/agent/conversation/run-events.ts @@ -166,28 +166,30 @@ export class ConversationRunEventEncoder { // is treated alike -- including the ones this encoder synthesises, such as the // terminal result for a provider-executed call the provider never resolved. private stampElapsed(events: ConversationRunEvent[]): ConversationRunEvent[] { - if ((!this.nowMs || this.startedMs === undefined) && !this.epochMs) { + if (events.length === 0) { return events; } - const elapsedMs = this.nowMs && this.startedMs !== undefined + for (const event of events) { + if (Object.hasOwn(event, "elapsedMs")) assertValidElapsedMs(event.elapsedMs); + if (Object.hasOwn(event, "emittedAt")) assertValidEmittedAt(event.emittedAt); + } + + const needsElapsedMs = events.some((event) => !Object.hasOwn(event, "elapsedMs")); + const needsEmittedAt = events.some((event) => !Object.hasOwn(event, "emittedAt")); + const elapsedMs = needsElapsedMs && this.nowMs && this.startedMs !== undefined ? Math.max(0, Math.round(this.nowMs() - this.startedMs)) : undefined; - const emittedAt = this.epochMs ? Math.round(this.epochMs()) : undefined; + const emittedAt = needsEmittedAt && this.epochMs ? Math.round(this.epochMs()) : undefined; + if (elapsedMs !== undefined) assertValidElapsedMs(elapsedMs); + if (emittedAt !== undefined) assertValidEmittedAt(emittedAt); + if (elapsedMs === undefined && emittedAt === undefined) { + return events; + } return events.map((event) => ({ ...event, - ...(typeof event.elapsedMs === "number" && Number.isFinite(event.elapsedMs) && - event.elapsedMs >= 0 - ? { elapsedMs: event.elapsedMs } - : elapsedMs === undefined - ? {} - : { elapsedMs }), - ...(typeof event.emittedAt === "number" && Number.isInteger(event.emittedAt) && - event.emittedAt >= 0 - ? { emittedAt: event.emittedAt } - : emittedAt === undefined - ? {} - : { emittedAt }), + ...(elapsedMs !== undefined && !Object.hasOwn(event, "elapsedMs") ? { elapsedMs } : {}), + ...(emittedAt !== undefined && !Object.hasOwn(event, "emittedAt") ? { emittedAt } : {}), })); } @@ -375,6 +377,18 @@ export class ConversationRunEventEncoder { } } +function assertValidElapsedMs(value: unknown): asserts value is number { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + throw new TypeError("elapsedMs must be a finite non-negative number"); + } +} + +function assertValidEmittedAt(value: unknown): asserts value is number { + if (typeof value !== "number" || !Number.isInteger(value) || value < 0) { + throw new TypeError("emittedAt must be a non-negative integer"); + } +} + /** Encode conversation run events helper. */ export function encodeConversationRunEvents( events: ChatStreamEvent[], diff --git a/src/runtime/model-call-context.test.ts b/src/runtime/model-call-context.test.ts index 7db907209d..fb520867d5 100644 --- a/src/runtime/model-call-context.test.ts +++ b/src/runtime/model-call-context.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import type { AgentRunModelCallContextEvent, @@ -74,16 +74,62 @@ describe("model-call-context", () => { elapsedMs: 7, emittedAt: 8, }); - sink({ - type: "AGENT_RUN_MODEL_CALL_CONTEXT", - messages: [], - elapsedMs: -1, - emittedAt: 1.5, - }); assertEquals(events.map(({ elapsedMs, emittedAt }) => ({ elapsedMs, emittedAt })), [ { elapsedMs: 43, emittedAt: 1_786_866_357_364 }, { elapsedMs: 7, emittedAt: 8 }, - { elapsedMs: 43, emittedAt: 1_786_866_357_364 }, ]); }); + + it("rejects invalid present timing instead of silently replacing it", () => { + const sink = createTimedAgentRunEventSink(() => {}, { + nowMs: () => 100, + epochMs: () => 1_786_866_357_364, + startedMs: 100, + }); + + assertThrows( + () => + sink({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + elapsedMs: Number.NaN, + }), + TypeError, + "elapsedMs must be a finite non-negative number", + ); + assertThrows( + () => + sink({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + emittedAt: 1.5, + }), + TypeError, + "emittedAt must be a non-negative integer", + ); + }); + + it("rejects timing generated by invalid clocks", () => { + const sink = createTimedAgentRunEventSink(() => {}, { + nowMs: () => Number.NaN, + epochMs: () => -1, + startedMs: 0, + }); + assertThrows( + () => sink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }), + TypeError, + "elapsedMs must be a finite non-negative number", + ); + + const wallClockSink = createTimedAgentRunEventSink(() => {}, { + nowMs: () => 0, + epochMs: () => -1, + startedMs: 0, + }); + assertThrows( + () => wallClockSink({ type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [] }), + TypeError, + "emittedAt must be a non-negative integer", + ); + }); }); diff --git a/src/runtime/model-call-context.ts b/src/runtime/model-call-context.ts index 8b78bd7c32..c9b045e864 100644 --- a/src/runtime/model-call-context.ts +++ b/src/runtime/model-call-context.ts @@ -117,16 +117,33 @@ export function createTimedAgentRunEventSink( const nowMs = options.nowMs ?? (() => performance.now()); const epochMs = options.epochMs ?? (() => Date.now()); const startedMs = options.startedMs ?? nowMs(); - return (event) => - sink({ + return (event) => { + const hasElapsedMs = Object.hasOwn(event, "elapsedMs"); + const hasEmittedAt = Object.hasOwn(event, "emittedAt"); + if (hasElapsedMs) assertValidElapsedMs(event.elapsedMs); + if (hasEmittedAt) assertValidEmittedAt(event.emittedAt); + + const elapsedMs = hasElapsedMs ? event.elapsedMs : Math.max(0, Math.round(nowMs() - startedMs)); + const emittedAt = hasEmittedAt ? event.emittedAt : Math.round(epochMs()); + assertValidElapsedMs(elapsedMs); + assertValidEmittedAt(emittedAt); + + return sink({ ...event, - elapsedMs: typeof event.elapsedMs === "number" && Number.isFinite(event.elapsedMs) && - event.elapsedMs >= 0 - ? event.elapsedMs - : Math.max(0, Math.round(nowMs() - startedMs)), - emittedAt: typeof event.emittedAt === "number" && Number.isInteger(event.emittedAt) && - event.emittedAt >= 0 - ? event.emittedAt - : Math.round(epochMs()), + elapsedMs, + emittedAt, }); + }; +} + +function assertValidElapsedMs(value: unknown): asserts value is number { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + throw new TypeError("elapsedMs must be a finite non-negative number"); + } +} + +function assertValidEmittedAt(value: unknown): asserts value is number { + if (typeof value !== "number" || !Number.isInteger(value) || value < 0) { + throw new TypeError("emittedAt must be a non-negative integer"); + } } diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index ae4c3a0867..b4fd980f18 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -855,6 +855,49 @@ describe("runtime-bridge", () => { assertEquals(recorded?.request, { reasoning: { enabled: true, budgetTokens: 2048 } }); }); + it("persists raw enabled Anthropic thinking when neutral reasoning has no effect", async () => { + for (const reasoning of [{}, { enabled: false }] as const) { + let recorded: AgentRunEvent | undefined; + const providerOptions = { + anthropic: { thinking: { type: "enabled", budget_tokens: 2048 } }, + }; + const model: ModelRuntime = { + provider: "veryfront-cloud", + modelId: "anthropic/claude-sonnet-4-6", + modelProvider: "anthropic", + async doGenerate(options) { + const dispatched = options as { + reasoning?: unknown; + providerOptions?: Record; + }; + assertEquals(dispatched.reasoning, reasoning); + assertEquals(dispatched.providerOptions, providerOptions); + return { content: [], finishReason: "stop", usage: {} }; + }, + async doStream() { + throw new Error("unexpected stream dispatch"); + }, + }; + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => + generateText({ + model, + messages: [{ role: "user", content: "Hello" }], + providerOptions, + reasoning, + }), + ); + + assertEquals(recorded?.request, { + reasoning: { enabled: true, budgetTokens: 2048 }, + }); + } + }); + it("calls a sink shared by both lanes only once", async () => { let calls = 0; const sink = () => { diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index 90b91b6a2c..65a3074388 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -680,25 +680,30 @@ function resolvePersistedReasoning( model: ModelRuntime, options: DirectModelOptions, ): RuntimeReasoningOption | undefined { - if (options.reasoning || resolveModelProvider(model) !== "anthropic") { + // The Anthropic request builder only gives neutral reasoning precedence when + // it enables thinking; otherwise a raw provider thinking config remains effective. + if (resolveModelProvider(model) !== "anthropic" || options.reasoning?.enabled === true) { return options.reasoning; } const providerOptions = options.providerOptions; if (!providerOptions || typeof providerOptions !== "object" || Array.isArray(providerOptions)) { - return undefined; + return options.reasoning; } const anthropic = readOwnEnumerableDataDescriptor(providerOptions, "anthropic")?.value; if (!anthropic || typeof anthropic !== "object" || Array.isArray(anthropic)) { - return undefined; + return options.reasoning; } const thinking = readOwnEnumerableDataDescriptor(anthropic, "thinking")?.value; if (!thinking || typeof thinking !== "object" || Array.isArray(thinking)) { - return undefined; + return options.reasoning; } const thinkingType = readOwnEnumerableDataDescriptor(thinking, "type")?.value; + if (thinkingType === "disabled") { + return { enabled: false }; + } if (thinkingType !== "adaptive" && thinkingType !== "enabled") { - return undefined; + return options.reasoning; } if (thinkingType === "enabled") { From cd7933f194b04e9e1134089ab3b9aa039bc4ed1f Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 17:22:28 +0200 Subject: [PATCH 11/16] fix(agent): preserve custom encoder compatibility --- src/agent/ag-ui/browser-chunk-encoder.ts | 4 ++-- src/agent/ag-ui/tracked-browser-response.test.ts | 8 +++++--- src/agent/ag-ui/tracked-browser-response.ts | 11 ++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/agent/ag-ui/browser-chunk-encoder.ts b/src/agent/ag-ui/browser-chunk-encoder.ts index 45e198e439..b94d912389 100644 --- a/src/agent/ag-ui/browser-chunk-encoder.ts +++ b/src/agent/ag-ui/browser-chunk-encoder.ts @@ -14,8 +14,8 @@ import type { AgentResponse } from "../types.ts"; /** Public API contract for AG-UI browser chunk encoder. */ export interface AgUiBrowserChunkEncoder { state: AgUiBrowserEncoderState; - /** Timing anchor consumed by the browser response composition root. */ - timingState: AgUiBrowserEncoderState; + /** Optional timing anchor consumed by the browser response composition root. */ + timingState?: AgUiBrowserEncoderState; encode: (chunk: TChunk) => AgUiBrowserEncodedEvent[]; finalize: (response: AgentResponse | null) => AgUiBrowserEncodedEvent[]; } diff --git a/src/agent/ag-ui/tracked-browser-response.test.ts b/src/agent/ag-ui/tracked-browser-response.test.ts index f877fce6d0..29c0c2b196 100644 --- a/src/agent/ag-ui/tracked-browser-response.test.ts +++ b/src/agent/ag-ui/tracked-browser-response.test.ts @@ -2,7 +2,6 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { createAgUiBrowserFinalizeTracker } from "./browser-finalize-tracker.ts"; -import { createAgUiBrowserEncoderState } from "./browser-encoder.ts"; import { createAgUiChunkEncoderBridge } from "./chunk-encoder-bridge.ts"; import { createAgUiTrackedBrowserResponse } from "./tracked-browser-response.ts"; @@ -69,7 +68,7 @@ describe("agent/ag-ui-tracked-browser-response", () => { assertStringIncludes(text, '"finishReason":"stop"'); }); - it("suppresses final response output when encoded events contain RunError", async () => { + it("supports legacy custom encoders without timingState", async () => { const response = createAgUiTrackedBrowserResponse({ agUiInput: { threadId: crypto.randomUUID(), @@ -89,7 +88,6 @@ describe("agent/ag-ui-tracked-browser-response", () => { waitForFinish: async () => {}, }, chunkEncoder: { - timingState: createAgUiBrowserEncoderState({ nowMs: null, epochMs: null }), encode: () => [{ event: "RunError", payload: { message: "boom" } }], finalize: () => [], }, @@ -101,6 +99,10 @@ describe("agent/ag-ui-tracked-browser-response", () => { const text = await response.text(); assertStringIncludes(text, "event: RunError"); assertEquals(text.includes("finishReason"), false); + assertEquals( + parseSseFrames(text).every((frame) => typeof frame.data.elapsedMs === "number"), + true, + ); }); it("shares an injected run timing anchor across bootstrap, chunk, and final events", async () => { diff --git a/src/agent/ag-ui/tracked-browser-response.ts b/src/agent/ag-ui/tracked-browser-response.ts index 87a6b3a7ce..e40066d2c2 100644 --- a/src/agent/ag-ui/tracked-browser-response.ts +++ b/src/agent/ag-ui/tracked-browser-response.ts @@ -13,10 +13,9 @@ export interface CreateAgUiTrackedBrowserResponseInput extends CreateAgUiRuntimeBrowserResponseInput, "encoder" | "initialState" | "onChunk" | "getFinalResponse" > { - chunkEncoder: Pick< - AgUiChunkEncoderBridge, - "encode" | "finalize" | "timingState" - >; + chunkEncoder: + & Pick, "encode" | "finalize"> + & Partial, "timingState">>; finalizeTracker: Pick< AgUiBrowserFinalizeTracker, "observeChunk" | "observeEncodedEvents" | "getFinalResponse" @@ -27,10 +26,12 @@ export interface CreateAgUiTrackedBrowserResponseInput extends export function createAgUiTrackedBrowserResponse( input: CreateAgUiTrackedBrowserResponseInput, ): Response { + const timingState = input.chunkEncoder.timingState; + return createAgUiRuntimeBrowserResponse({ ...input, encoder: { - timingState: input.chunkEncoder.timingState, + ...(timingState === undefined ? {} : { timingState }), encode: (chunk) => { const events = input.chunkEncoder.encode(chunk); input.finalizeTracker.observeEncodedEvents(events); From cc50fe16fbe9a46db903776e2b896c8db5ad66a5 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 17:37:10 +0200 Subject: [PATCH 12/16] fix(agent): align effective request timing --- deno.json | 2 + .../src/openai-reasoning-models.ts | 128 ++---------------- .../conversation/hosted-lifecycle.test.ts | 8 ++ src/agent/conversation/hosted-lifecycle.ts | 3 +- .../conversation/run-chunk-mirror.test.ts | 53 +++++++- src/agent/conversation/run-chunk-mirror.ts | 5 +- src/agent/conversation/run-events.test.ts | 1 + src/agent/conversation/run-events.ts | 11 ++ .../conversation/run-stream-mirror.test.ts | 27 ++++ src/agent/conversation/run-stream-mirror.ts | 4 +- src/provider/shared/openai-reasoning.ts | 114 ++++++++++++++++ src/runtime/runtime-bridge.test.ts | 28 +++- src/runtime/runtime-bridge.ts | 9 +- 13 files changed, 268 insertions(+), 125 deletions(-) create mode 100644 src/provider/shared/openai-reasoning.ts diff --git a/deno.json b/deno.json index 9e80a429ab..ef37551e3d 100644 --- a/deno.json +++ b/deno.json @@ -136,6 +136,7 @@ "./oauth": "./src/oauth/index.ts", "./provider": "./src/provider/index.ts", "./provider/shared": "./src/provider/shared/index.ts", + "./provider/openai-reasoning": "./src/provider/shared/openai-reasoning.ts", "./provider/types": "./src/provider/types.ts", "./fs": "./src/fs/index.ts", "./integrations": "./src/integrations/index.ts", @@ -267,6 +268,7 @@ "veryfront/discovery/runtime-modules-bootstrap": "./src/discovery/runtime-modules-bootstrap.ts", "veryfront/observability": "./src/observability/index.ts", "veryfront/provider/shared": "./src/provider/shared/index.ts", + "veryfront/provider/openai-reasoning": "./src/provider/shared/openai-reasoning.ts", "veryfront/provider/types": "./src/provider/types.ts", "veryfront/tool/schema": "./src/tool/schema/index.ts", "veryfront/agent/composition": "./src/agent/composition/index.ts", diff --git a/extensions/ext-llm-openai/src/openai-reasoning-models.ts b/extensions/ext-llm-openai/src/openai-reasoning-models.ts index 0ba17d3912..33a7ee93ef 100644 --- a/extensions/ext-llm-openai/src/openai-reasoning-models.ts +++ b/extensions/ext-llm-openai/src/openai-reasoning-models.ts @@ -1,114 +1,14 @@ -import type { RuntimeReasoningOption } from "veryfront/provider/types"; - -export type OpenAIReasoningEffort = "low" | "medium" | "high"; - -export type OpenAIProviderReasoningEffort = NonNullable; - -export type OpenAIProviderReasoningOption = RuntimeReasoningOption; - -export type ResolvedOpenAIReasoning = { - effort: OpenAIReasoningEffort; - source: "default" | "explicit"; -}; - -const DEFAULT_REASONING_EFFORT: OpenAIReasoningEffort = "medium"; - -export function supportsDefaultReasoningParams(providerName: string): boolean { - const normalizedProvider = providerName.toLowerCase(); - return normalizedProvider === "openai" || normalizedProvider === "veryfront-cloud"; -} - -function isGpt5ChatSnapshot(modelId: string): boolean { - return /^gpt-5-chat($|-)/.test(modelId); -} - -function isGpt51(modelId: string): boolean { - return /^gpt-5\.1($|-)/.test(modelId); -} - -function isReasoningCapableGpt5(modelId: string): boolean { - if (isGpt5ChatSnapshot(modelId) || isGpt51(modelId)) { - return false; - } - - if (/^gpt-5(-|$)/.test(modelId)) { - return true; - } - - const gpt5Version = /^gpt-5\.(\d+)(-|$)/.exec(modelId)?.[1]; - return gpt5Version !== undefined && Number.parseInt(gpt5Version, 10) >= 2; -} - -export function getDefaultOpenAIReasoningEffort( - modelId: string, - providerName = "openai", -): OpenAIReasoningEffort | undefined { - const normalized = modelId.toLowerCase(); - if (!supportsDefaultReasoningParams(providerName)) { - return undefined; - } - - if (isGpt5ChatSnapshot(normalized)) { - return undefined; - } - - // GPT-5.1 defaults upstream reasoning to none unless callers opt in explicitly. - if (isGpt51(normalized)) { - return undefined; - } - - if (/^o1($|-\d)/.test(normalized) || /^o[34](-|$)/.test(normalized)) { - return DEFAULT_REASONING_EFFORT; - } - - if (isReasoningCapableGpt5(normalized)) { - return DEFAULT_REASONING_EFFORT; - } - - return undefined; -} - -export function resolveOpenAIReasoningConfig( - modelId: string, - providerName: string, - option: OpenAIProviderReasoningOption | undefined, -): ResolvedOpenAIReasoning | undefined { - if (!option) { - const effort = getDefaultOpenAIReasoningEffort(modelId, providerName); - return effort === undefined ? undefined : { effort, source: "default" }; - } - - if (option.enabled !== true) { - return undefined; - } - - switch (option.effort) { - case "low": - return { effort: "low", source: "explicit" }; - case "high": - case "max": - return { effort: "high", source: "explicit" }; - case "medium": - default: - return { effort: "medium", source: "explicit" }; - } -} - -export function shouldRequestOpenAIReasoningSummary( - providerName: string, - reasoning: ResolvedOpenAIReasoning, -): boolean { - // Default-reasoning BYOK "openai" requests must not ask for summaries: - // unverified customer organizations get a 400 from the Responses API. - return reasoning.source === "explicit" || providerName.toLowerCase() === "veryfront-cloud"; -} - -export function isOpenAIReasoningModel(modelId: string, providerName = "openai"): boolean { - return getDefaultOpenAIReasoningEffort(modelId, providerName) !== undefined; -} - -export function rejectsOpenAISamplingParams(modelId: string): boolean { - const normalized = modelId.toLowerCase(); - - return /^o[134]($|-)/.test(normalized) || isReasoningCapableGpt5(normalized); -} +export { + getDefaultOpenAIReasoningEffort, + isOpenAIReasoningModel, + rejectsOpenAISamplingParams, + resolveOpenAIReasoningConfig, + shouldRequestOpenAIReasoningSummary, + supportsDefaultReasoningParams, +} from "veryfront/provider/openai-reasoning"; +export type { + OpenAIProviderReasoningEffort, + OpenAIProviderReasoningOption, + OpenAIReasoningEffort, + ResolvedOpenAIReasoning, +} from "veryfront/provider/openai-reasoning"; diff --git a/src/agent/conversation/hosted-lifecycle.test.ts b/src/agent/conversation/hosted-lifecycle.test.ts index 394f3fcb62..351f20a142 100644 --- a/src/agent/conversation/hosted-lifecycle.test.ts +++ b/src/agent/conversation/hosted-lifecycle.test.ts @@ -206,6 +206,14 @@ describe("agent/conversation-hosted-lifecycle", () => { true, `every persisted event must carry elapsedMs, got ${JSON.stringify(elapsed)}`, ); + const emittedAt = fetchCalls.map((call) => + JSON.parse(String(call[1]?.body)).events[0].emittedAt + ); + assertEquals( + emittedAt.every((value) => typeof value === "number" && Number.isInteger(value) && value > 0), + true, + `every persisted event must carry epoch emittedAt, got ${JSON.stringify(emittedAt)}`, + ); }); it("finalizes and cancels conversation-backed root runs with host-supplied model metadata", async () => { diff --git a/src/agent/conversation/hosted-lifecycle.ts b/src/agent/conversation/hosted-lifecycle.ts index 3a73c0285b..705fed7956 100644 --- a/src/agent/conversation/hosted-lifecycle.ts +++ b/src/agent/conversation/hosted-lifecycle.ts @@ -18,6 +18,7 @@ import type { } from "../hosted/child-lifecycle.ts"; import type { HostedLifecycleAdapter, HostedLifecycleTerminalState } from "../hosted/lifecycle.ts"; import { agentLogger } from "#veryfront/utils"; +import { createAgentRunEventTimingAnchor } from "../../runtime/model-call-context.ts"; /** Input payload for conversation hosted lifecycle finalize. */ export interface ConversationHostedLifecycleFinalizeInput { @@ -151,7 +152,7 @@ export function createConversationHostedStreamLifecycleAdapter( // creation is also the anchor `elapsedMs` is measured from, so a fresh encoder // would reset elapsed to zero on every event. const encoder = options.encoder ?? - new ConversationRunEventEncoder({ nowMs: () => performance.now() }); + new ConversationRunEventEncoder(createAgentRunEventTimingAnchor()); return createConversationHostedLifecycleAdapter({ ...options, diff --git a/src/agent/conversation/run-chunk-mirror.test.ts b/src/agent/conversation/run-chunk-mirror.test.ts index d4656e093b..6f9ae87310 100644 --- a/src/agent/conversation/run-chunk-mirror.test.ts +++ b/src/agent/conversation/run-chunk-mirror.test.ts @@ -9,6 +9,7 @@ import { createHostedConversationRunChunkMirror, type HostedConversationRunChunkMirrorTraceAttributes, } from "./run-chunk-mirror.ts"; +import { createDurableRunEventSink } from "../hosted/durable-run-event-sink.ts"; type ConversationRunEventQueueFlushResult = Awaited< ReturnType @@ -57,10 +58,12 @@ describe("agent/conversation-run-chunk-mirror", () => { it("prepares UI chunks into durable events and enqueues them", async () => { const queueController = createQueueController(); const preparedTypes: string[] = []; + const legacyEncoder = new ConversationRunEventEncoder(); + Object.defineProperty(legacyEncoder, "getTimingAnchor", { value: undefined }); const mirror = createConversationRunChunkMirror({ queueController, - // Exact-event assertions: an unclocked encoder keeps them free of elapsedMs. - encoder: new ConversationRunEventEncoder(), + // Encoders created before timing-anchor introspection remain accepted. + encoder: legacyEncoder, immediateFlushEventCount: 99, flushDelayMs: 10_000, onChunkPrepared: ({ events }) => { @@ -79,7 +82,7 @@ describe("agent/conversation-run-chunk-mirror", () => { // The other mirror tests pin an unclocked encoder so their exact-event // assertions stay deterministic, which leaves the default unproven. This - // covers it: omitting `encoder` must yield durable events that carry elapsed. + // covers it: omitting `encoder` must yield durable events that carry producer timing. it("installs a clock on the encoder it creates by default", async () => { const queueController = createQueueController(); const prepared: ConversationRunEvent[] = []; @@ -101,6 +104,50 @@ describe("agent/conversation-run-chunk-mirror", () => { true, `elapsed must be a finite, nonnegative reading, got ${String(elapsedMs)}`, ); + const emittedAt = prepared[0]?.emittedAt; + assertEquals( + typeof emittedAt === "number" && Number.isInteger(emittedAt) && emittedAt > 0, + true, + `emittedAt must be a positive epoch timestamp, got ${String(emittedAt)}`, + ); + mirror.dispose(); + }); + + it("shares a custom encoder anchor with the private durable event sink", async () => { + const queueController = createQueueController(); + let now = 100; + let epoch = 1_000; + const encoder = new ConversationRunEventEncoder({ + nowMs: () => now, + epochMs: () => epoch, + }); + const publicEvents: ConversationRunEvent[] = []; + const privateEvents: ConversationRunEvent[] = []; + const mirror = createConversationRunChunkMirror({ + queueController, + encoder, + immediateFlushEventCount: 99, + flushDelayMs: 10_000, + onChunkPrepared: ({ events }) => publicEvents.push(...events), + onExternalEventsPrepared: ({ events }) => privateEvents.push(...events), + }); + now = 142; + epoch = 1_042; + + await mirror.handleChunk({ type: "text-delta", id: "m1", delta: "hello" }); + await createDurableRunEventSink({ mirror })({ + type: "AGENT_RUN_MODEL_CALL_CONTEXT", + messages: [], + }); + + assertEquals( + publicEvents.map(({ elapsedMs, emittedAt }) => ({ elapsedMs, emittedAt })), + [{ elapsedMs: 42, emittedAt: 1_042 }], + ); + assertEquals( + privateEvents.map(({ elapsedMs, emittedAt }) => ({ elapsedMs, emittedAt })), + [{ elapsedMs: 42, emittedAt: 1_042 }], + ); mirror.dispose(); }); diff --git a/src/agent/conversation/run-chunk-mirror.ts b/src/agent/conversation/run-chunk-mirror.ts index 1e773ca1f0..431426bffa 100644 --- a/src/agent/conversation/run-chunk-mirror.ts +++ b/src/agent/conversation/run-chunk-mirror.ts @@ -170,7 +170,10 @@ export function createConversationRunChunkMirror( // headless -- a scheduled run has no client attached -- so this is the only // point that observes emission time. Callers injecting their own encoder // choose their own clock, or none. - const timing = createAgentRunEventTimingAnchor(); + const getEncoderTimingAnchor = input.encoder?.getTimingAnchor; + const timing = typeof getEncoderTimingAnchor === "function" + ? getEncoderTimingAnchor.call(input.encoder) ?? createAgentRunEventTimingAnchor() + : createAgentRunEventTimingAnchor(); const encoder = input.encoder ?? new ConversationRunEventEncoder(timing); const immediateFlushEventCount = input.immediateFlushEventCount ?? DEFAULT_IMMEDIATE_FLUSH_EVENT_COUNT; diff --git a/src/agent/conversation/run-events.test.ts b/src/agent/conversation/run-events.test.ts index 03096a1c31..38757e9fae 100644 --- a/src/agent/conversation/run-events.test.ts +++ b/src/agent/conversation/run-events.test.ts @@ -340,6 +340,7 @@ describe("agent/conversation-run-events", () => { it("omits elapsedMs entirely when no clock is supplied", () => { const encoder = new ConversationRunEventEncoder(); + assertEquals(encoder.getTimingAnchor(), undefined, "legacy unclocked encoders stay unclocked"); encoder.encode({ type: "start", messageId: "msg-no-clock" }); const encoded = encoder.encode({ type: "text-delta", id: "text:0", delta: "hi" }); diff --git a/src/agent/conversation/run-events.ts b/src/agent/conversation/run-events.ts index 903fede576..b563099fac 100644 --- a/src/agent/conversation/run-events.ts +++ b/src/agent/conversation/run-events.ts @@ -1,6 +1,7 @@ import { defineSchema, lazySchema } from "#veryfront/schemas/index.ts"; import type { InferSchema } from "#veryfront/extensions/schema/index.ts"; import { type ChatStreamEvent } from "#veryfront/chat/protocol.ts"; +import type { AgentRunEventTimingOptions } from "../../runtime/model-call-context.ts"; import { normalizeConversationRunEvents } from "./run-event-normalization.ts"; /** Shared conversation run event types value. */ @@ -100,6 +101,16 @@ export class ConversationRunEventEncoder { this.epochMs = options.epochMs; } + /** Return the run timing anchor owned by this encoder, when it has one. */ + getTimingAnchor(): AgentRunEventTimingOptions | undefined { + if (!this.nowMs && !this.epochMs) return undefined; + return { + ...(this.nowMs ? { nowMs: this.nowMs } : {}), + ...(this.epochMs ? { epochMs: this.epochMs } : {}), + ...(this.startedMs !== undefined ? { startedMs: this.startedMs } : {}), + }; + } + private nextStepName(): string { this.stepCount += 1; this.activeStepName = `step-${this.stepCount}`; diff --git a/src/agent/conversation/run-stream-mirror.test.ts b/src/agent/conversation/run-stream-mirror.test.ts index cb7638324e..fbed9bad50 100644 --- a/src/agent/conversation/run-stream-mirror.test.ts +++ b/src/agent/conversation/run-stream-mirror.test.ts @@ -4,6 +4,7 @@ import { FakeTime } from "#std/testing/time"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { createConversationRunStreamMirror } from "./run-stream-mirror.ts"; import { type ConversationRunEventQueueController } from "./durable.ts"; +import { ConversationRunEventEncoder } from "./run-events.ts"; function createMockQueueController(initial?: { latestEventId?: number; @@ -68,6 +69,7 @@ describe("agent/conversation-run-stream-mirror", () => { const mirror = createConversationRunStreamMirror({ queueController: controller, immediateFlushEventCount: 2, + encoder: new ConversationRunEventEncoder(), }); mirror.handleStreamEvent({ type: "text-start", id: "msg-1" }); @@ -80,6 +82,31 @@ describe("agent/conversation-run-stream-mirror", () => { mirror.dispose(); }); + it("stamps elapsed and epoch time with its default run-scoped encoder", () => { + const controller = createMockQueueController(); + const mirror = createConversationRunStreamMirror({ + queueController: controller, + immediateFlushEventCount: 10, + }); + + mirror.handleStreamEvent({ type: "text-start", id: "msg-1" }); + + const event = controller.enqueued[0]?.[0] as + | { elapsedMs?: number; emittedAt?: number } + | undefined; + assertEquals( + typeof event?.elapsedMs === "number" && Number.isFinite(event.elapsedMs) && + event.elapsedMs >= 0, + true, + ); + assertEquals( + typeof event?.emittedAt === "number" && Number.isInteger(event.emittedAt) && + event.emittedAt > 0, + true, + ); + mirror.dispose(); + }); + it("normalizes already-encoded events before enqueueing them", () => { const controller = createMockQueueController(); const mirror = createConversationRunStreamMirror({ diff --git a/src/agent/conversation/run-stream-mirror.ts b/src/agent/conversation/run-stream-mirror.ts index 039689c67b..dc18b672de 100644 --- a/src/agent/conversation/run-stream-mirror.ts +++ b/src/agent/conversation/run-stream-mirror.ts @@ -10,6 +10,7 @@ import { } from "./run-mirror.ts"; import { normalizeConversationRunEvents } from "./run-event-normalization.ts"; import { type ConversationRunEventQueueController } from "./durable.ts"; +import { createAgentRunEventTimingAnchor } from "../../runtime/model-call-context.ts"; /** Public API contract for conversation run stream mirror. */ export interface ConversationRunStreamMirror { @@ -32,7 +33,8 @@ export function createConversationRunStreamMirror(input: { onRetryScheduled?: (state: ConversationRunMirrorRetryScheduledState) => Promise | void; onStopped?: (state: ConversationRunMirrorStoppedState) => Promise | void; }): ConversationRunStreamMirror { - const encoder = input.encoder ?? new ConversationRunEventEncoder(); + const encoder = input.encoder ?? + new ConversationRunEventEncoder(createAgentRunEventTimingAnchor()); const mirror = createConversationRunMirror({ queueController: input.queueController, immediateFlushEventCount: input.immediateFlushEventCount, diff --git a/src/provider/shared/openai-reasoning.ts b/src/provider/shared/openai-reasoning.ts new file mode 100644 index 0000000000..8eede714b7 --- /dev/null +++ b/src/provider/shared/openai-reasoning.ts @@ -0,0 +1,114 @@ +import type { RuntimeReasoningOption } from "../types.ts"; + +export type OpenAIReasoningEffort = "low" | "medium" | "high"; + +export type OpenAIProviderReasoningEffort = NonNullable; + +export type OpenAIProviderReasoningOption = RuntimeReasoningOption; + +export type ResolvedOpenAIReasoning = { + effort: OpenAIReasoningEffort; + source: "default" | "explicit"; +}; + +const DEFAULT_REASONING_EFFORT: OpenAIReasoningEffort = "medium"; + +export function supportsDefaultReasoningParams(providerName: string): boolean { + const normalizedProvider = providerName.toLowerCase(); + return normalizedProvider === "openai" || normalizedProvider === "veryfront-cloud"; +} + +function isGpt5ChatSnapshot(modelId: string): boolean { + return /^gpt-5-chat($|-)/.test(modelId); +} + +function isGpt51(modelId: string): boolean { + return /^gpt-5\.1($|-)/.test(modelId); +} + +function isReasoningCapableGpt5(modelId: string): boolean { + if (isGpt5ChatSnapshot(modelId) || isGpt51(modelId)) { + return false; + } + + if (/^gpt-5(-|$)/.test(modelId)) { + return true; + } + + const gpt5Version = /^gpt-5\.(\d+)(-|$)/.exec(modelId)?.[1]; + return gpt5Version !== undefined && Number.parseInt(gpt5Version, 10) >= 2; +} + +export function getDefaultOpenAIReasoningEffort( + modelId: string, + providerName = "openai", +): OpenAIReasoningEffort | undefined { + const normalized = modelId.toLowerCase(); + if (!supportsDefaultReasoningParams(providerName)) { + return undefined; + } + + if (isGpt5ChatSnapshot(normalized)) { + return undefined; + } + + // GPT-5.1 defaults upstream reasoning to none unless callers opt in explicitly. + if (isGpt51(normalized)) { + return undefined; + } + + if (/^o1($|-\d)/.test(normalized) || /^o[34](-|$)/.test(normalized)) { + return DEFAULT_REASONING_EFFORT; + } + + if (isReasoningCapableGpt5(normalized)) { + return DEFAULT_REASONING_EFFORT; + } + + return undefined; +} + +export function resolveOpenAIReasoningConfig( + modelId: string, + providerName: string, + option: OpenAIProviderReasoningOption | undefined, +): ResolvedOpenAIReasoning | undefined { + if (!option) { + const effort = getDefaultOpenAIReasoningEffort(modelId, providerName); + return effort === undefined ? undefined : { effort, source: "default" }; + } + + if (option.enabled !== true) { + return undefined; + } + + switch (option.effort) { + case "low": + return { effort: "low", source: "explicit" }; + case "high": + case "max": + return { effort: "high", source: "explicit" }; + case "medium": + default: + return { effort: "medium", source: "explicit" }; + } +} + +export function shouldRequestOpenAIReasoningSummary( + providerName: string, + reasoning: ResolvedOpenAIReasoning, +): boolean { + // Default-reasoning BYOK "openai" requests must not ask for summaries: + // unverified customer organizations get a 400 from the Responses API. + return reasoning.source === "explicit" || providerName.toLowerCase() === "veryfront-cloud"; +} + +export function isOpenAIReasoningModel(modelId: string, providerName = "openai"): boolean { + return getDefaultOpenAIReasoningEffort(modelId, providerName) !== undefined; +} + +export function rejectsOpenAISamplingParams(modelId: string): boolean { + const normalized = modelId.toLowerCase(); + + return /^o[134]($|-)/.test(normalized) || isReasoningCapableGpt5(normalized); +} diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index b4fd980f18..619a52ee05 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -751,11 +751,31 @@ describe("runtime-bridge", () => { }), ); assertEquals(recorded?.model, { id: bareModelId, modelProvider }); - assertEquals(recorded?.request?.reasoning, { - enabled: true, - effort: "high", - budgetTokens: 2048, + assertEquals( + recorded?.request?.reasoning, + modelProvider === "openai" + ? { enabled: true, effort: "high" } + : { enabled: true, effort: "high", budgetTokens: 2048 }, + ); + } + }); + + it("persists default OpenAI transport reasoning for direct reasoning models", async () => { + for (const modelId of ["o1", "o3-mini", "o4-mini", "gpt-5.4-nano"]) { + let recorded: AgentRunEvent | undefined; + const model = createGenerateModel("openai", modelId, async (options) => { + assertEquals(options.reasoning, undefined); + return { content: [], finishReason: "stop", usage: {} }; }); + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => generateText({ model, messages: [{ role: "user", content: "Hello" }] }), + ); + + assertEquals(recorded?.request?.reasoning, { enabled: true, effort: "medium" }); } }); diff --git a/src/runtime/runtime-bridge.ts b/src/runtime/runtime-bridge.ts index 65a3074388..6bda0e113a 100644 --- a/src/runtime/runtime-bridge.ts +++ b/src/runtime/runtime-bridge.ts @@ -21,6 +21,7 @@ import type { ModelRuntimeGenerateResult, } from "#veryfront/provider/types.ts"; import type { RuntimeReasoningOption } from "#veryfront/agent/types.ts"; +import { resolveOpenAIReasoningConfig } from "#veryfront/provider/shared/openai-reasoning.ts"; import { DurableRunEventPersistenceError } from "#veryfront/agent/conversation/private-run-event.ts"; import type { ChatSystemMessage } from "#veryfront/chat/types.ts"; import type { @@ -680,9 +681,15 @@ function resolvePersistedReasoning( model: ModelRuntime, options: DirectModelOptions, ): RuntimeReasoningOption | undefined { + const modelProvider = resolveModelProvider(model); + if (modelProvider === "openai" && typeof model.modelId === "string") { + const reasoning = resolveOpenAIReasoningConfig(model.modelId, modelProvider, options.reasoning); + return reasoning ? { enabled: true, effort: reasoning.effort } : options.reasoning; + } + // The Anthropic request builder only gives neutral reasoning precedence when // it enables thinking; otherwise a raw provider thinking config remains effective. - if (resolveModelProvider(model) !== "anthropic" || options.reasoning?.enabled === true) { + if (modelProvider !== "anthropic" || options.reasoning?.enabled === true) { return options.reasoning; } From 93e65f79a9a54b7ec262e748857595c1e985c4cb Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 17:44:14 +0200 Subject: [PATCH 13/16] fix(agent): preserve runtime identity and timing --- .../src/openai-provider.test.ts | 13 +++++++ .../ext-llm-openai/src/openai-provider.ts | 13 ++++++- .../conversation/run-stream-mirror.test.ts | 39 +++++++++++++++++++ src/agent/conversation/run-stream-mirror.ts | 3 +- src/runtime/runtime-bridge.test.ts | 22 +++++++++++ 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/extensions/ext-llm-openai/src/openai-provider.test.ts b/extensions/ext-llm-openai/src/openai-provider.test.ts index 525317858f..f59a691876 100644 --- a/extensions/ext-llm-openai/src/openai-provider.test.ts +++ b/extensions/ext-llm-openai/src/openai-provider.test.ts @@ -74,6 +74,19 @@ function _readRequestHeader(init: RequestInit | undefined, name: string): string // --------------------------------------------------------------------------- describe("openai-provider", () => { + it("exposes canonical model providers independently of runtime display labels", () => { + for (const createRuntime of [createOpenAIModelRuntime, createOpenAIResponsesRuntime]) { + const runtime = createRuntime({ + apiKey: "test-openai-key", + name: "prod-openai", + providerName: " OpenAI ", + }, "gpt-5.4-nano"); + + assertEquals(runtime.provider, "prod-openai"); + assertEquals(runtime.modelProvider, "openai"); + } + }); + it("creates an OpenAI-compatible language runtime without SDK helpers for generate", async () => { let requestedUrl = ""; let requestedInit: RequestInit | undefined; diff --git a/extensions/ext-llm-openai/src/openai-provider.ts b/extensions/ext-llm-openai/src/openai-provider.ts index e5c2636fbe..811eb27517 100644 --- a/extensions/ext-llm-openai/src/openai-provider.ts +++ b/extensions/ext-llm-openai/src/openai-provider.ts @@ -100,12 +100,19 @@ function getOpenAIProviderLabel(config: { name?: string }): string { return readNonEmptyString(config.name) ?? "openai"; } +function normalizeOpenAIProviderName(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + const normalized = value.trim().toLowerCase(); + return normalized === "" ? undefined : normalized; +} + function getRuntimeOpenAIProviderName(config: OpenAIRuntimeConfig): string { - return readNonEmptyString(config.providerName) ?? getOpenAIProviderLabel(config); + return normalizeOpenAIProviderName(config.providerName) ?? + normalizeOpenAIProviderName(config.name) ?? "openai"; } function getLLMOpenAIProviderName(config: LLMProviderConfig): string { - return readNonEmptyString(config.providerName) ?? "openai"; + return normalizeOpenAIProviderName(config.providerName) ?? "openai"; } type OpenAICompatibleProviderKind = "openai" | "mistral" | "moonshotai"; @@ -1031,6 +1038,7 @@ export function createOpenAIModelRuntime( const responseContext = { providerKind, providerLabel }; return { provider: providerLabel, + modelProvider: providerName, modelId, specificationVersion: "v3", supportedUrls: {}, @@ -1115,6 +1123,7 @@ export function createOpenAIResponsesRuntime( const responseContext = { providerKind, providerLabel }; return { provider: providerLabel, + modelProvider: providerName, modelId, specificationVersion: "v3", supportedUrls: {}, diff --git a/src/agent/conversation/run-stream-mirror.test.ts b/src/agent/conversation/run-stream-mirror.test.ts index fbed9bad50..a898f663d1 100644 --- a/src/agent/conversation/run-stream-mirror.test.ts +++ b/src/agent/conversation/run-stream-mirror.test.ts @@ -112,6 +112,7 @@ describe("agent/conversation-run-stream-mirror", () => { const mirror = createConversationRunStreamMirror({ queueController: controller, immediateFlushEventCount: 10, + encoder: new ConversationRunEventEncoder(), }); mirror.appendEvents([{ type: "TEXT_MESSAGE_CONTENT", delta: "x".repeat(300 * 1024) }]); @@ -126,6 +127,44 @@ describe("agent/conversation-run-stream-mirror", () => { mirror.dispose(); }); + it("stamps external events before and after normalization", () => { + const controller = createMockQueueController(); + let now = 100; + let epoch = 1_000; + const mirror = createConversationRunStreamMirror({ + queueController: controller, + immediateFlushEventCount: 10, + encoder: new ConversationRunEventEncoder({ + nowMs: () => now, + epochMs: () => epoch, + }), + }); + now = 142; + epoch = 1_042; + + mirror.appendEvents([ + { type: "TEXT_MESSAGE_CONTENT", delta: "x".repeat(300 * 1024) }, + { type: "TOOL_EXPOSURE_CHECKPOINT", elapsedMs: 7, emittedAt: 8 }, + ]); + + const normalized = controller.enqueued[0] as Array<{ + type: string; + elapsedMs?: number; + emittedAt?: number; + }>; + assertEquals(normalized.length > 2, true); + assertEquals( + normalized.slice(0, -1).every((event) => event.elapsedMs === 42 && event.emittedAt === 1_042), + true, + ); + assertEquals(normalized.at(-1), { + type: "TOOL_EXPOSURE_CHECKPOINT", + elapsedMs: 7, + emittedAt: 8, + }); + mirror.dispose(); + }); + it("uses the underlying mirror retry scheduling path", async () => { using time = new FakeTime(); const retryStates: Array<{ errorMessage: string; retryDelayMs: number }> = []; diff --git a/src/agent/conversation/run-stream-mirror.ts b/src/agent/conversation/run-stream-mirror.ts index dc18b672de..403e019d58 100644 --- a/src/agent/conversation/run-stream-mirror.ts +++ b/src/agent/conversation/run-stream-mirror.ts @@ -53,7 +53,8 @@ export function createConversationRunStreamMirror(input: { mirror.enqueue(normalizeConversationRunEvents(encoder.encode(event))); }, appendEvents(events) { - mirror.enqueue(normalizeConversationRunEvents(events)); + const stampedEvents = encoder.stamp(events); + mirror.enqueue(encoder.stamp(normalizeConversationRunEvents(stampedEvents))); }, flush() { return mirror.flush(); diff --git a/src/runtime/runtime-bridge.test.ts b/src/runtime/runtime-bridge.test.ts index 619a52ee05..ccacde603e 100644 --- a/src/runtime/runtime-bridge.test.ts +++ b/src/runtime/runtime-bridge.test.ts @@ -779,6 +779,28 @@ describe("runtime-bridge", () => { } }); + it("uses canonical OpenAI modelProvider when the runtime has a distinct display label", async () => { + let recorded: AgentRunEvent | undefined; + const model = { + ...createGenerateModel("prod-openai", "gpt-5.4-nano", async () => ({ + content: [], + finishReason: "stop", + usage: {}, + })), + modelProvider: "openai", + }; + + await runWithRunEventSink( + (event) => { + recorded = event; + }, + () => generateText({ model, messages: [{ role: "user", content: "Hello" }] }), + ); + + assertEquals(recorded?.model, { id: "gpt-5.4-nano", modelProvider: "openai" }); + assertEquals(recorded?.request?.reasoning, { enabled: true, effort: "medium" }); + }); + it("omits reasoning when no canonical fields can be projected", async () => { let recorded: AgentRunEvent | undefined; const model = createGenerateModel("test", "test/empty-reasoning", async () => ({ From d2469779726b9ccb7276f9798985f3aa47acd425 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 17:54:13 +0200 Subject: [PATCH 14/16] fix(config): align OpenAI reasoning alias --- tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index bdc18abe93..a9b45cfa28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -61,6 +61,7 @@ "react-dom/server": ["./npm/node_modules/react-dom/server.node.js"], "veryfront": ["./src/index.ts"], "veryfront/*": ["./src/*"], + "veryfront/provider/openai-reasoning": ["./src/provider/shared/openai-reasoning.ts"], "veryfront/head": ["./src/react/runtime/core.ts"], "veryfront/router": ["./src/react/runtime/core.ts"], "veryfront/context": ["./src/react/runtime/core.ts"], @@ -129,6 +130,7 @@ "#veryfront/prompt": ["./src/prompt/index.ts"], "#veryfront/prompt/*": ["./src/prompt/*"], "#veryfront/provider": ["./src/provider/index.ts"], + "#veryfront/provider/openai-reasoning": ["./src/provider/shared/openai-reasoning.ts"], "#veryfront/provider/*": ["./src/provider/*"], "#veryfront/react": ["./src/react/index.ts"], "#veryfront/react/*": ["./src/react/*"], From a79ef52363a3ab887fb7c320aa46972e25452c38 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 18:02:25 +0200 Subject: [PATCH 15/16] test: satisfy callback and fetch typings --- src/agent/conversation/run-chunk-mirror.test.ts | 8 ++++++-- src/agent/react/use-chat/use-chat.csrf.test.tsx | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/agent/conversation/run-chunk-mirror.test.ts b/src/agent/conversation/run-chunk-mirror.test.ts index 6f9ae87310..cd533a1862 100644 --- a/src/agent/conversation/run-chunk-mirror.test.ts +++ b/src/agent/conversation/run-chunk-mirror.test.ts @@ -128,8 +128,12 @@ describe("agent/conversation-run-chunk-mirror", () => { encoder, immediateFlushEventCount: 99, flushDelayMs: 10_000, - onChunkPrepared: ({ events }) => publicEvents.push(...events), - onExternalEventsPrepared: ({ events }) => privateEvents.push(...events), + onChunkPrepared: ({ events }) => { + publicEvents.push(...events); + }, + onExternalEventsPrepared: ({ events }) => { + privateEvents.push(...events); + }, }); now = 142; epoch = 1_042; diff --git a/src/agent/react/use-chat/use-chat.csrf.test.tsx b/src/agent/react/use-chat/use-chat.csrf.test.tsx index a619da24f1..9c53ab1755 100644 --- a/src/agent/react/use-chat/use-chat.csrf.test.tsx +++ b/src/agent/react/use-chat/use-chat.csrf.test.tsx @@ -51,6 +51,10 @@ async function settle(): Promise { flushSync(() => {}); } +function hasRequestHeaders(value: unknown): value is { headers?: HeadersInit } { + return typeof value === "object" && value !== null && "headers" in value; +} + /** Drive one `sendMessage` turn and hand back the headers the transport sent. */ async function captureSendHeaders( options: Parameters[0], @@ -58,7 +62,7 @@ async function captureSendHeaders( const originalFetch = globalThis.fetch; let sent = new Headers(); globalThis.fetch = (_input, init) => { - sent = new Headers(init?.headers); + sent = new Headers(hasRequestHeaders(init) ? init.headers : undefined); return Promise.resolve( new Response("event: RunFinished\ndata: {}\n\n", { status: 200, From 9204e57578bdc496a78c4b105e75594c8beb77e8 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 16 Aug 2026 18:07:16 +0200 Subject: [PATCH 16/16] docs: refresh API reference --- docs/api-reference/veryfront/agent.md | 34 +++++++++++------------ docs/api-reference/veryfront/embedding.md | 2 +- docs/api-reference/veryfront/provider.md | 30 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 814b81d8f6..b3dff297e2 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -464,7 +464,7 @@ Input delivered to a hosted agent-service detached execution callback. | `CONVERSATION_HOSTED_STREAM_ERROR_TERMINAL_ERROR_CODE` | Shared conversation hosted stream error terminal error code value. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L18) | | `ConversationMessageRecordSchema` | Schema for conversation message record. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L55) | | `ConversationRecordSchema` | Schema for conversation record. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L43) | -| `ConversationRunEventSchema` | Schema for conversation run event. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L32) | +| `ConversationRunEventSchema` | Schema for conversation run event. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L33) | | `ConversationRunProjectionSchema` | Schema for conversation run projection. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L178) | | `ConversationRunStatusSchema` | Schema for conversation run status. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L83) | | `ConversationRunTargetsSchema` | Schema for conversation run targets. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L17) | @@ -652,13 +652,13 @@ Input delivered to a hosted agent-service detached execution callback. | `createAgUiRuntimeHandler` | Handler for create AG-UI runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/runtime-handler.ts#L406) | | `createAgUiSseErrorResponse` | Response payload for create AG-UI sse error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/host-support.ts#L440) | | `createAgUiSseResponse` | Response payload for create AG-UI sse. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/host-support.ts#L453) | -| `createAgUiTrackedBrowserResponse` | Response payload for create AG-UI tracked browser. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/tracked-browser-response.ts#L27) | +| `createAgUiTrackedBrowserResponse` | Response payload for create AG-UI tracked browser. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/tracked-browser-response.ts#L26) | | `createBootstrappedHostedChatExecutionRuntime` | Create bootstrapped hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-execution-runtime.ts#L438) | | `createChatUiMessageStreamFromDataStream` | Create chat UI message stream from data stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/chat-ui-message-stream.ts#L608) | | `createConversationAgentRun` | Create conversation agent run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L1245) | -| `createConversationChildLifecycleAdapter` | Create conversation child lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L230) | -| `createConversationHostedLifecycleAdapter` | Create conversation hosted lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L51) | -| `createConversationHostedStreamLifecycleAdapter` | Create conversation hosted stream lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L137) | +| `createConversationChildLifecycleAdapter` | Create conversation child lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L231) | +| `createConversationHostedLifecycleAdapter` | Create conversation hosted lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L52) | +| `createConversationHostedStreamLifecycleAdapter` | Create conversation hosted stream lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L138) | | `createConversationHostedTerminalAdapter` | Create conversation hosted terminal adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L202) | | `createConversationMessage` | Message shape for create conversation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L191) | | `createConversationRecord` | Record shape for create conversation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L175) | @@ -668,7 +668,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createConversationRunContext` | Context for create conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-context.ts#L12) | | `createConversationRunEventQueueController` | Create conversation run event queue controller. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L809) | | `createConversationRunMirror` | Create conversation run mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-mirror.ts#L94) | -| `createConversationRunStreamMirror` | Create conversation run stream mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-stream-mirror.ts#L24) | +| `createConversationRunStreamMirror` | Create conversation run stream mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-stream-mirror.ts#L25) | | `createDefaultAgentServiceChatRuntime` | Create default hosted chat runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-chat-runtime.ts#L388) | | `createDefaultAgentServiceInvokeAgentTool` | Create default hosted invoke agent tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L830) | | `createDefaultAgentServiceProjectSteeringRefresh` | Create default hosted project steering refresh. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-project-steering-refresh.ts#L216) | @@ -699,7 +699,7 @@ Input delivered to a hosted agent-service detached execution callback. | `createHostedChildMirrorContext` | Context for create hosted child mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-mirror.ts#L233) | | `createHostedChildPendingToolLifecycle` | Create hosted child pending tool lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-pending-tool-lifecycle.ts#L98) | | `createHostedChildPendingToolLifecycleLogger` | Create hosted child pending tool lifecycle logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-pending-tool-lifecycle.ts#L55) | -| `createHostedConversationRunChunkMirror` | Create hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L382) | +| `createHostedConversationRunChunkMirror` | Create hosted conversation run chunk mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L385) | | `createHostedDurableChildForkRunContext` | Context for create hosted durable child fork run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L179) | | `createHostedDurableChildInvokeTraceRecorder` | Create hosted durable child invoke trace recorder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/durable-child-fork-execution.ts#L312) | | `createHostedFormInputTool` | Create hosted form input tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/form-input-tool.ts#L34) | @@ -746,7 +746,7 @@ Input delivered to a hosted agent-service detached execution callback. | `dispatchConversationHostedStreamErrorState` | State for dispatch conversation hosted stream error. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L106) | | `dispatchConversationHostedTerminalState` | State for dispatch conversation hosted terminal. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L91) | | `doesProjectAgentRuntimeAgentMatchSource` | Does project agent runtime agent match source helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/project/agent-runtime.ts#L173) | -| `encodeConversationRunEvents` | Encode conversation run events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L393) | +| `encodeConversationRunEvents` | Encode conversation run events helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L404) | | `ensureConversationProjectLink` | Ensure conversation project link helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L148) | | `evaluateSlashCommandArtifactPolicy` | Evaluate slash command artifact policy helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/artifacts/slash-command-artifact-policy.ts#L200) | | `evaluateStarterIntentTurnPolicy` | Evaluate starter intent turn policy helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/delegation-policy.ts#L207) | @@ -880,7 +880,7 @@ Input delivered to a hosted agent-service detached execution callback. | `normalizeChatUiMessageStream` | Normalizes chat UI message stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L494) | | `normalizeConversationRunEvent` | Event emitted for normalize conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-normalization.ts#L36) | | `normalizeConversationRunEvents` | Normalizes conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-event-normalization.ts#L95) | -| `normalizeEncodedConversationRunEvents` | Normalizes encoded conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L401) | +| `normalizeEncodedConversationRunEvents` | Normalizes encoded conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L412) | | `normalizeHostedChildArtifactPath` | Normalizes hosted child artifact path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-artifact-support.ts#L133) | | `normalizeParsedAgentServiceChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | | `normalizeParsedHostedChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | @@ -1046,7 +1046,7 @@ Input delivered to a hosted agent-service detached execution callback. | `AppendConversationRunEventsError` | Error shape for append conversation run events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-append-errors.ts#L4) | | `BufferMemory` | Implement buffer memory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/memory/memory.ts#L128) | | `ConversationMemory` | Implement conversation memory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/memory/memory.ts#L84) | -| `ConversationRunEventEncoder` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L80) | +| `ConversationRunEventEncoder` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L81) | | `ConversationRunTerminalStateError` | Error shape for conversation run terminal state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable.ts#L157) | | `HostedChildStreamIdleTimeoutError` | Error shape for hosted child stream idle timeout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-stream-watchdog.ts#L13) | | `HostedChildTerminalStateError` | Error shape for hosted child terminal state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-status.ts#L72) | @@ -1262,9 +1262,9 @@ Input delivered to a hosted agent-service detached execution callback. | `CloseHostedMirroredOpenToolCallsInput` | Input payload for close hosted mirrored open tool calls. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/mirrored-tool-chunk-state.ts#L145) | | `CompleteExternalAgentWorkerRunInput` | Input payload for complete external agent worker run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/external-worker-client.ts#L212) | | `ConversationAgentRunUsage` | Public API contract for conversation agent run usage. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L416) | -| `ConversationChildLifecycleContext` | Context for conversation child lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L163) | +| `ConversationChildLifecycleContext` | Context for conversation child lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L164) | | `ConversationControlPlaneResponseError` | Error shape for conversation control plane response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/bootstrap.ts#L63) | -| `ConversationHostedLifecycleFinalizeInput` | Input payload for conversation hosted lifecycle finalize. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L23) | +| `ConversationHostedLifecycleFinalizeInput` | Input payload for conversation hosted lifecycle finalize. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L24) | | `ConversationHostedTerminalAdapter` | Public API contract for conversation hosted terminal adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L127) | | `ConversationHostedTerminalRuntimeAdapter` | Public API contract for conversation hosted terminal runtime adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L83) | | `ConversationHostedTerminalStateInput` | Input payload for conversation hosted terminal state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L10) | @@ -1288,8 +1288,8 @@ Input delivered to a hosted agent-service detached execution callback. | `ConversationRunChunkMirrorPrepareExternalEventsInput` | Input payload for conversation run chunk mirror prepare external events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L60) | | `ConversationRunChunkMirrorQueueOptions` | Options accepted by conversation run chunk mirror queue. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-chunk-mirror.ts#L87) | | `ConversationRunContext` | Context for conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-context.ts#L4) | -| `ConversationRunEvent` | Event emitted for conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L35) | -| `ConversationRunEventEncoderOptions` | Options accepted by the conversation run event encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L64) | +| `ConversationRunEvent` | Event emitted for conversation run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L36) | +| `ConversationRunEventEncoderOptions` | Options accepted by the conversation run event encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L65) | | `ConversationRunEventQueueController` | Public API contract for conversation run event queue controller. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L230) | | `ConversationRunMirror` | Public API contract for conversation run mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-mirror.ts#L60) | | `ConversationRunMirrorRetryScheduledState` | State for conversation run mirror retry scheduled. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-mirror.ts#L39) | @@ -1297,7 +1297,7 @@ Input delivered to a hosted agent-service detached execution callback. | `ConversationRunMirrorStoppedState` | State for conversation run mirror stopped. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-mirror.ts#L28) | | `ConversationRunProjection` | Public API contract for conversation run projection. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L101) | | `ConversationRunQueueFlushOutcome` | Public API contract for conversation run queue flush outcome. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L224) | -| `ConversationRunStreamMirror` | Public API contract for conversation run stream mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-stream-mirror.ts#L15) | +| `ConversationRunStreamMirror` | Public API contract for conversation run stream mirror. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-stream-mirror.ts#L16) | | `ConversationRunTargets` | Public API contract for conversation run targets. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L26) | | `CreateAgentServiceRegistrationLifecycleOptions` | Options accepted by create agent service registration lifecycle. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L199) | | `CreateAgentServiceRuntimeOptions` | Options accepted by create agent service runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/runtime.ts#L96) | @@ -1313,7 +1313,7 @@ Input delivered to a hosted agent-service detached execution callback. | `CreateAgUiRuntimeEventEncoderOptions` | Options accepted by create AG-UI runtime event encoder. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/runtime-event-encoder.ts#L21) | | `CreateAgUiTrackedBrowserResponseInput` | Input payload for create AG-UI tracked browser response. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/tracked-browser-response.ts#L11) | | `CreateBootstrappedHostedChatExecutionRuntimeInput` | Input payload for create bootstrapped hosted chat execution runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-execution-runtime.ts#L142) | -| `CreateConversationHostedLifecycleAdapterOptions` | Options accepted by create conversation hosted lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L32) | +| `CreateConversationHostedLifecycleAdapterOptions` | Options accepted by create conversation hosted lifecycle adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-lifecycle.ts#L33) | | `CreateConversationHostedTerminalAdapterOptions` | Options accepted by create conversation hosted terminal adapter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L117) | | `CreateDefaultAgentServiceChatRuntimeContextInput` | Input payload for create default hosted chat runtime context. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-chat-runtime.ts#L106) | | `CreateDefaultAgentServiceChatRuntimeOptions` | Options accepted by create default hosted chat runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-chat-runtime.ts#L132) | @@ -1850,7 +1850,7 @@ Input delivered to a hosted agent-service detached execution callback. | `agentServiceConfigSchema` | Zod schema for agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L149) | | `agentServiceRegistrationConfigSchema` | Zod schema for agent service registration config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L85) | | `agUiSseEventTypes` | AG-UI runtime event type constants normalized from browser-wire SSE events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/sse-parser.ts#L5) | -| `conversationRunEventTypes` | Shared conversation run event types value. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L7) | +| `conversationRunEventTypes` | Shared conversation run event types value. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L8) | | `createNodeHostedAgentServiceRuntimeInfrastructure` | Create node hosted agent service runtime infrastructure. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/node-runtime-infrastructure.ts#L96) | | `defaultHostedInvokeAgentInputSchema` | Schema for default hosted invoke agent input. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L239) | | `defaultHostedInvokeAgentSelectionSchema` | Schema for default hosted invoke agent selection. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/default-invoke-agent-tool.ts#L228) | diff --git a/docs/api-reference/veryfront/embedding.md b/docs/api-reference/veryfront/embedding.md index 23acc34ded..e88fb60c52 100644 --- a/docs/api-reference/veryfront/embedding.md +++ b/docs/api-reference/veryfront/embedding.md @@ -42,7 +42,7 @@ export const { POST, GET, DELETE } = createUploadHandler(store, { | `ragStore` | Creates a persistent RAG store with lazy embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/rag-store.ts#L212) | | `registerEmbeddingProvider` | Register an embedding provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L25) | | `resolveEmbeddingModel` | Resolve a "provider/model" string to an embedding runtime instance. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L116) | -| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1258) | +| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1265) | | `vectorStore` | Creates an in-memory vector store with integrated embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/vector-store.ts#L46) | ### Types diff --git a/docs/api-reference/veryfront/provider.md b/docs/api-reference/veryfront/provider.md index a59cb566e8..dfb3f438df 100644 --- a/docs/api-reference/veryfront/provider.md +++ b/docs/api-reference/veryfront/provider.md @@ -119,6 +119,36 @@ Clear all registered model providers and reset lazy built-ins (for testing). These import paths group focused functionality under this module. Each is a separate barrel; import only what you need. +### `veryfront/provider/openai-reasoning` + +```ts +import { + getDefaultOpenAIReasoningEffort, + isOpenAIReasoningModel, + rejectsOpenAISamplingParams, +} from "veryfront/provider/openai-reasoning"; +``` + +#### Functions + +| Name | Description | Source | +| ------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ | +| `getDefaultOpenAIReasoningEffort` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L42) | +| `isOpenAIReasoningModel` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L106) | +| `rejectsOpenAISamplingParams` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L110) | +| `resolveOpenAIReasoningConfig` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L71) | +| `shouldRequestOpenAIReasoningSummary` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L97) | +| `supportsDefaultReasoningParams` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L16) | + +#### Types + +| Name | Description | Source | +| ------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- | +| `OpenAIProviderReasoningEffort` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L5) | +| `OpenAIProviderReasoningOption` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L7) | +| `OpenAIReasoningEffort` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L3) | +| `ResolvedOpenAIReasoning` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/shared/openai-reasoning.ts#L9) | + ### `veryfront/provider/shared` Shared plumbing consumed by the `@veryfront/ext-*` provider extensions. This barrel is the stable extension-facing surface. Implementations remain internal to `runtime-loader.ts` and `runtime-loader/`; their physical location may change without changing extension imports.