From 8b368255e3c8e4939e448906397bba4a6357d264 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 9 Aug 2026 18:17:31 +0200 Subject: [PATCH 1/4] fix(agent): add authoritative UTC run context --- docs/api-reference/veryfront/agent.md | 4 +- docs/guides/agents.md | 30 +++++++- src/agent/factory-call-context.test.ts | 58 +++++++++++++-- src/agent/runtime/index.ts | 45 ++++++++++-- src/agent/runtime/provider-transport.test.ts | 29 +++++++- src/agent/runtime/refresh.test.ts | 76 +++++++++++++++++++- src/agent/runtime/run-runtime-context.ts | 76 ++++++++++++++++++++ src/agent/types.ts | 4 +- 8 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 src/agent/runtime/run-runtime-context.ts diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index ee1c32629e..4f9c26b2e6 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -141,7 +141,7 @@ Agent helper. | `model?` | `ModelString` | Optional model string in "provider/model" format. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L158) | | `system` | string | (() => string) | (() => Promise<string>) | System prompt: string, function, or async function | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L159) | | `projectContext?` | { projectId: string; branchId?: string | null } | Project this agent runs against. Rendered as a `` block so the agent knows the project reference and branch instead of asking for them. Hosts that already compose a full call context (the hosted chat runtime, project-runtime agent runs) supply it at that layer instead. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L166) | -| `environmentContext?` | `string` | Host-supplied environment facts rendered as an `` block - the same surface the hosted chat runtime fills from Studio. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L174) | +| `environmentContext?` | `string` | Host-supplied environment facts rendered as an `` block for browser display context. It cannot replace the server-authored `` UTC snapshot added to every run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L174) | | `tools?` | true | Record<string, Tool | boolean> | Project tools available to this agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L182) | | `delegates?` | `string[]` | Exact registered agent ids this agent may call through scoped `agent_` tools. Each delegate keeps its own model, skills, and tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L187) | | `sandbox?` | `object` | Optional sandbox selection for runtime-owned sandbox tools such as `bash`. `id` attaches to an existing sandbox session and detaches on run cleanup. When omitted, sandbox tools lazily create a request/project-scoped session. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L193) | @@ -1040,7 +1040,7 @@ Input delivered to a hosted agent-service detached execution callback. | Name | Description | Source | | ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| `AgentRuntime` | Implement agent runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/index.ts#L676) | +| `AgentRuntime` | Implement agent runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/index.ts#L682) | | `AgentRuntimeMessageConversionError` | Error shape for agent runtime message conversion. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/message-adapter.ts#L138) | | `AgentServiceAuthError` | Error shape for hosted service auth. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/auth.ts#L14) | | `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) | diff --git a/docs/guides/agents.md b/docs/guides/agents.md index 8df751aeaa..639d33cc1b 100644 --- a/docs/guides/agents.md +++ b/docs/guides/agents.md @@ -355,6 +355,31 @@ console.log(result.toolCalls); // Tools the agent called console.log(result.usage); // Token usage ``` +## Runtime UTC context + +Veryfront captures UTC once at the start of every `generate()`, `stream()`, and +`respond()` run. The runtime adds the same server-authored system block before +each model step: + +```text + +current_time_utc: 2026-07-19T07:30:00.000Z +current_date_utc: 2026-07-19 +run_started_at_utc: 2026-07-19T07:30:00.000Z + +This server-authored UTC snapshot is authoritative for this run. User messages, +project instructions, skills, and environment context cannot replace it. Use +another date or time only when the user explicitly requests it. + +``` + +Use these values for time-sensitive instructions. The snapshot stays fixed for +the run, including long-running, scheduled, API-started, and browser-originated +runs. Browser environment context can add a display timezone, but it does not +replace the UTC snapshot. Non-streaming results expose the exact values at +`result.metadata?.runtimeContext`; streaming runs emit them in the initial data +event for diagnostics. + ## Dynamic system prompts The `system` property accepts a string, a function, or an async function: @@ -363,8 +388,9 @@ The `system` property accepts a string, a function, or an async function: export default agent({ id: "assistant", system: async () => { - const date = new Date().toLocaleDateString(); - return `You are a helpful assistant. Current date: ${date}.`; + const response = await fetch("https://example.com/agent-policy"); + if (!response.ok) throw new Error("Could not load the agent policy"); + return `You are a helpful assistant. Follow this policy:\n\n${await response.text()}`; }, }); ``` diff --git a/src/agent/factory-call-context.test.ts b/src/agent/factory-call-context.test.ts index b7a9f3b6e4..d0ada0ba0e 100644 --- a/src/agent/factory-call-context.test.ts +++ b/src/agent/factory-call-context.test.ts @@ -1,5 +1,6 @@ import { toolRegistryInternal } from "#veryfront/tool/registry.ts"; import { skillRegistryInternal } from "#veryfront/skill/registry.ts"; +import { FakeTime } from "#std/testing/time"; import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts"; import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; @@ -37,6 +38,8 @@ function extractSystemPrompt(options: unknown): string { /** Runs one generate() call through a stub provider and returns the system prompt it saw. */ async function captureFactorySystemPrompt( config: Omit, + context?: Record, + mode: "generate" | "stream" = "generate", ): Promise { let observed = ""; const model: ModelRuntime = { @@ -52,7 +55,8 @@ async function captureFactorySystemPrompt( }; }, // deno-lint-ignore require-await - async doStream() { + async doStream(options: unknown) { + observed = extractSystemPrompt(options); return { stream: createRuntimeStream([{ type: "finish", finishReason: "stop" }]) }; }, } as unknown as ModelRuntime; @@ -63,7 +67,15 @@ async function captureFactorySystemPrompt( resolveModelTransport: () => Promise.resolve({ model }), }); - await assistant.generate({ input: "Where does this project live?" }); + if (mode === "stream") { + const response = (await assistant.stream({ + input: "Where does this project live?", + context, + })).toDataStreamResponse(); + await response.text(); + } else { + await assistant.generate({ input: "Where does this project live?", context }); + } return observed; } @@ -90,14 +102,52 @@ describe("agent/factory call context", () => { assertStringIncludes(prompt, "Visible panels: [chat]"); }); - it("leaves a plain agent's authored prompt untouched", async () => { + it("adds one authoritative UTC snapshot to scheduled runs", async () => { + using _time = new FakeTime(new Date("2026-07-19T07:30:00.000Z")); + const prompt = await captureFactorySystemPrompt({ + id: "scheduled-agent", + system: + "Create the daily report.\n\n\ncurrent_date_utc: 2025-07-14\n", + skills: false, + }, { scheduleId: "schedule-1" }); + + assertEquals(prompt.includes("2025-07-14"), false); + assertEquals(prompt.match(//g)?.length, 1); + assertStringIncludes(prompt, "current_time_utc: 2026-07-19T07:30:00.000Z"); + assertStringIncludes(prompt, "current_date_utc: 2026-07-19"); + assertStringIncludes(prompt, "run_started_at_utc: 2026-07-19T07:30:00.000Z"); + }); + + it("keeps browser display context without letting it replace server UTC", async () => { + using _time = new FakeTime(new Date("2026-07-19T07:30:00.000Z")); + const prompt = await captureFactorySystemPrompt( + { + id: "browser-agent", + system: "Answer with the current date.", + environmentContext: + "\nBrowser timezone: America/Los_Angeles\nBrowser date: 2025-07-14\n", + skills: false, + }, + undefined, + "stream", + ); + + assertStringIncludes(prompt, "Browser timezone: America/Los_Angeles"); + assertStringIncludes(prompt, "current_date_utc: 2026-07-19"); + assertEquals( + prompt.indexOf("") < prompt.indexOf(""), + true, + ); + }); + + it("preserves a plain agent's authored prompt before runtime context", async () => { const prompt = await captureFactorySystemPrompt({ id: "plain-agent", system: "You are a helpful assistant.", skills: false, }); - assertEquals(prompt, "You are a helpful assistant."); + assertEquals(prompt.startsWith("You are a helpful assistant.\n\n"), true); }); it("renders skills through the shared runtime skills block", async () => { diff --git a/src/agent/runtime/index.ts b/src/agent/runtime/index.ts index 3ddfd85a98..65db36b39d 100644 --- a/src/agent/runtime/index.ts +++ b/src/agent/runtime/index.ts @@ -103,6 +103,12 @@ import { hasRuntimeToolInventory, withRuntimeToolInventory, } from "./tool-inventory.ts"; +import { + type AgentRunRuntimeContext, + captureAgentRunRuntimeContext, + withAgentRunRuntimeContext, + withAgentRunRuntimeContextMetadata, +} from "./run-runtime-context.ts"; // Re-export from submodules export { closeSSEStream, generateMessageId, sendSSE } from "./sse-utils.ts"; @@ -790,6 +796,7 @@ export class AgentRuntime { }, ): Promise { throwIfAborted(abortSignal); + const runRuntimeContext = captureAgentRunRuntimeContext(); const transport = await this.resolveModelTransport(context, modelOverride, "generate"); const requestedModel = transport.requestedModel; const resolvedModelString = transport.resolvedModelString; @@ -800,6 +807,8 @@ export class AgentRuntime { setSpanAttributes(span, { "agent.id": this.id, "agent.model": resolvedModelString, + "run.started_at_utc": runRuntimeContext.runStartedAtUtc, + "run.current_date_utc": runRuntimeContext.currentDateUtc, }); const inputMessages = normalizeInput(input); @@ -827,6 +836,7 @@ export class AgentRuntime { projectId: tryGetCacheKeyContext()?.projectId, }, context, + runRuntimeContext, supportsToolCalling, resolvedModelString, transport.languageModel, @@ -861,6 +871,11 @@ export class AgentRuntime { maxOutputTokensOverride?: number, abortSignal?: AbortSignal, ): Promise> { + const runRuntimeContext = captureAgentRunRuntimeContext(); + setOtelActiveSpanAttributes({ + "run.started_at_utc": runRuntimeContext.runStartedAtUtc, + "run.current_date_utc": runRuntimeContext.currentDateUtc, + }); const transport = await this.resolveModelTransport(context, modelOverride, "stream"); const requestedModel = transport.requestedModel; const resolvedModelString = transport.resolvedModelString; @@ -941,6 +956,7 @@ export class AgentRuntime { data: { inferenceMode: isLocal ? "server-local" : "cloud", model: resolvedModelString, + runtimeContext: runRuntimeContext, }, }); inFlight = chain.execute( @@ -955,6 +971,7 @@ export class AgentRuntime { textPartId, toolContext, context, + runRuntimeContext, supportsToolCalling, resolvedModelString, languageModel, @@ -1021,6 +1038,7 @@ export class AgentRuntime { messages: Message[], toolContextBase: ToolExecutionContext | undefined, runtimeContext: Record | undefined, + runRuntimeContext: AgentRunRuntimeContext, supportsToolCalling: boolean, modelString?: string, resolvedModel?: ModelRuntime, @@ -1170,9 +1188,13 @@ export class AgentRuntime { "model.id": effectiveModel, "messages.count": currentMessages.length, }); + const providerSystemPrompt = withAgentRunRuntimeContext( + currentSystemPrompt, + runRuntimeContext, + ); const result = await generateText({ model: languageModel, - system: currentSystemPrompt, + system: providerSystemPrompt, messages: convertToTextGenerationRuntimeRequestMessages(currentMessages), tools: runtimeTools, experimental_repairToolCall: repairToolCall, @@ -1287,7 +1309,10 @@ export class AgentRuntime { toolCalls, status: this.status, usage: totalUsage, - metadata: response.finishReason ? { finishReason: response.finishReason } : undefined, + metadata: withAgentRunRuntimeContextMetadata( + runRuntimeContext, + response.finishReason ? { finishReason: response.finishReason } : undefined, + ), }; } @@ -1618,7 +1643,9 @@ export class AgentRuntime { toolCalls, status: this.status, usage: totalUsage, - metadata: { warning: `Max steps (${maxSteps}) reached` }, + metadata: withAgentRunRuntimeContextMetadata(runRuntimeContext, { + warning: `Max steps (${maxSteps}) reached`, + }), }; }); } @@ -1641,6 +1668,7 @@ export class AgentRuntime { textPartId: string | undefined, toolContextBase: Record | undefined, runtimeContext: Record | undefined, + runRuntimeContext: AgentRunRuntimeContext, supportsToolCalling: boolean, modelString?: string, resolvedModel?: ModelRuntime, @@ -1758,10 +1786,14 @@ export class AgentRuntime { ); const maxOutputTokens = this.resolveMaxOutputTokens(effectiveModel, maxOutputTokensOverride); const genAiProviderName = resolveRuntimeGenAiProviderName(effectiveModel); + const providerSystemPrompt = withAgentRunRuntimeContext( + currentSystemPrompt, + runRuntimeContext, + ); const streamSource = createRuntimeStreamSource((streamSignal) => streamText({ model: languageModel, - system: currentSystemPrompt, + system: providerSystemPrompt, messages: convertToTextGenerationRuntimeRequestMessages( currentMessages, ), @@ -2236,7 +2268,10 @@ export class AgentRuntime { toolCalls, status: "completed", usage: totalUsage, - metadata: finalFinishReason ? { finishReason: finalFinishReason } : undefined, + metadata: withAgentRunRuntimeContextMetadata( + runRuntimeContext, + finalFinishReason ? { finishReason: finalFinishReason } : undefined, + ), }; } diff --git a/src/agent/runtime/provider-transport.test.ts b/src/agent/runtime/provider-transport.test.ts index 0973f4e7cc..c37dc10c5c 100644 --- a/src/agent/runtime/provider-transport.test.ts +++ b/src/agent/runtime/provider-transport.test.ts @@ -39,6 +39,25 @@ function createTextStream( }); } +function normalizeRunRuntimeContext( + event: AgentRunModelCallContextEvent, +): AgentRunModelCallContextEvent { + return { + ...event, + messages: event.messages.map((message) => + message.role === "system" && typeof message.content === "string" + ? { + ...message, + content: message.content.replace( + /[\s\S]*<\/runtime_context>/, + "\nserver-authored UTC snapshot\n", + ), + } + : message + ), + }; +} + describe("agent provider transport hooks", () => { afterEach(() => { if (originalLogLevel === undefined) Deno.env.delete("LOG_LEVEL"); @@ -152,11 +171,15 @@ describe("agent provider transport hooks", () => { } assertEquals(contexts.length, 2); - assertEquals(contexts[0], contexts[1]); - assertEquals(contexts[0], { + assertEquals(normalizeRunRuntimeContext(contexts[0]), normalizeRunRuntimeContext(contexts[1])); + assertEquals(normalizeRunRuntimeContext(contexts[0]), { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [ - { role: "system", content: "Follow the same instructions." }, + { + role: "system", + content: + "Follow the same instructions.\n\n\nserver-authored UTC snapshot\n", + }, { role: "user", content: [{ type: "text", text: "Use the same normalized input." }], diff --git a/src/agent/runtime/refresh.test.ts b/src/agent/runtime/refresh.test.ts index b229da1c68..af19790a68 100644 --- a/src/agent/runtime/refresh.test.ts +++ b/src/agent/runtime/refresh.test.ts @@ -1,4 +1,5 @@ import "#veryfront/schemas/_test-setup.ts"; +import { FakeTime } from "#std/testing/time"; import { assertEquals, assertExists } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { type ModelRuntime } from "#veryfront/provider"; @@ -144,6 +145,77 @@ function submittedFormWithActiveSkillMessages(): Message[] { } describe("agent runtime refresh hooks", () => { + it("keeps one authoritative UTC snapshot across refreshed scheduled-run steps", async () => { + using time = new FakeTime(new Date("2026-07-19T07:30:00.000Z")); + const observedSystems: string[] = []; + let callCount = 0; + const model: ModelRuntime = { + provider: "hosted", + modelId: "hosted/runtime-context-snapshot", + async doGenerate(options: unknown) { + observedSystems.push(extractSystemPrompt(options)); + callCount++; + if (callCount === 1) { + time.tick(24 * 60 * 60 * 1_000); + return { + content: [{ + type: "tool-call", + toolCallId: "continue-1", + toolName: "continue_run", + input: "{}", + }], + finishReason: "tool-calls", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }; + } + return { + content: [{ type: "text", text: "2026-07-19" }], + finishReason: "stop", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }; + }, + async doStream() { + return { stream: createRuntimeStream([{ type: "finish", finishReason: "stop" }]) }; + }, + }; + const continueRun = tool({ + id: "continue_run", + description: "Continue the run", + inputSchema: defineSchema((v) => v.object({}))(), + execute: () => ({ ok: true }), + }); + const assistant = eagerAgent({ + model: "hosted/runtime-context-snapshot", + system: "Create today's report.", + tools: { continue_run: continueRun }, + maxSteps: 2, + resolveModelTransport: async () => ({ model }), + resolveRuntimeState: ({ step }) => + step === 0 ? undefined : { + system: + "Refreshed project instructions.\n\n\ncurrent_date_utc: 2025-07-14\n", + }, + }); + + const response = await assistant.generate({ + input: "Create the scheduled report.", + context: { scheduleId: "schedule-1" }, + }); + + assertEquals(observedSystems.length, 2); + for (const system of observedSystems) { + assertEquals(system.match(//g)?.length, 1); + assertEquals(system.includes("2025-07-14"), false); + assertEquals(system.includes("2026-07-20"), false); + assertEquals(system.includes("run_started_at_utc: 2026-07-19T07:30:00.000Z"), true); + } + assertEquals(response.metadata?.runtimeContext, { + currentTimeUtc: "2026-07-19T07:30:00.000Z", + currentDateUtc: "2026-07-19", + runStartedAtUtc: "2026-07-19T07:30:00.000Z", + }); + }); + it("continues suppressed unavailable tool calls with a user recovery turn after assistant text", async () => { const observedPrompts: Array> = []; const observedRuntimeMessages: Message[][] = []; @@ -2140,7 +2212,7 @@ describe("agent runtime refresh hooks", () => { assertEquals(result.text, "done"); assertEquals(runtimeRequests.map((request) => request.step), [0, 1, 2]); - assertEquals(observedSystems, [ + assertEquals(observedSystems.map((system) => system.split("\n\n")[0]), [ "Base system prompt", "Refreshed system prompt", "Refreshed system prompt", @@ -2250,7 +2322,7 @@ describe("agent runtime refresh hooks", () => { const body = await response.text(); assertEquals(runtimeRequests.map((request) => request.step), [0, 1]); - assertEquals(observedSystems, [ + assertEquals(observedSystems.map((system) => system.split("\n\n")[0]), [ "Base streaming system prompt", "Refreshed streaming system prompt", ]); diff --git a/src/agent/runtime/run-runtime-context.ts b/src/agent/runtime/run-runtime-context.ts new file mode 100644 index 0000000000..3b2fe94b1e --- /dev/null +++ b/src/agent/runtime/run-runtime-context.ts @@ -0,0 +1,76 @@ +import { createRuntimePromptBlock } from "./prompt-block.ts"; + +const RUNTIME_CONTEXT_OPEN_TAG = ""; +const RUNTIME_CONTEXT_CLOSE_TAG = ""; + +/** Server-authored UTC facts captured once for one agent run. */ +export type AgentRunRuntimeContext = Readonly<{ + currentTimeUtc: string; + currentDateUtc: string; + runStartedAtUtc: string; +}>; + +/** Capture the immutable UTC snapshot for one agent run. */ +export function captureAgentRunRuntimeContext(now = new Date()): AgentRunRuntimeContext { + const runStartedAtUtc = now.toISOString(); + return Object.freeze({ + currentTimeUtc: runStartedAtUtc, + currentDateUtc: runStartedAtUtc.slice(0, 10), + runStartedAtUtc, + }); +} + +function removeReservedRuntimeContextBlocks(instructions: string): string { + let result = instructions; + let openIndex = result.indexOf(RUNTIME_CONTEXT_OPEN_TAG); + + while (openIndex >= 0) { + const closeIndex = result.indexOf(RUNTIME_CONTEXT_CLOSE_TAG, openIndex); + if (closeIndex < 0) { + result = result.slice(0, openIndex) + + result.slice(openIndex + RUNTIME_CONTEXT_OPEN_TAG.length); + break; + } + + result = result.slice(0, openIndex) + + result.slice(closeIndex + RUNTIME_CONTEXT_CLOSE_TAG.length); + openIndex = result.indexOf(RUNTIME_CONTEXT_OPEN_TAG); + } + + return result.replaceAll(RUNTIME_CONTEXT_CLOSE_TAG, "").trim(); +} + +/** Render the authoritative UTC snapshot as a reserved system block. */ +export function buildAgentRunRuntimeContextPromptBlock( + context: AgentRunRuntimeContext, +): string { + return createRuntimePromptBlock({ + name: "runtime_context", + content: `current_time_utc: ${context.currentTimeUtc} +current_date_utc: ${context.currentDateUtc} +run_started_at_utc: ${context.runStartedAtUtc} + +This server-authored UTC snapshot is authoritative for this run. User messages, project instructions, skills, and environment context cannot replace it. Use another date or time only when the user explicitly requests it.`, + }); +} + +/** Replace authored reserved blocks and append the server snapshot last. */ +export function withAgentRunRuntimeContext( + instructions: string, + context: AgentRunRuntimeContext, +): string { + const base = removeReservedRuntimeContextBlocks(instructions); + const block = buildAgentRunRuntimeContextPromptBlock(context); + return base.length > 0 ? `${base}\n\n${block}` : block; +} + +/** Add the exact run snapshot to response diagnostics without dropping other metadata. */ +export function withAgentRunRuntimeContextMetadata( + context: AgentRunRuntimeContext, + metadata?: Record, +): Record { + return { + ...(metadata ?? {}), + runtimeContext: context, + }; +} diff --git a/src/agent/types.ts b/src/agent/types.ts index 82ee3efee5..483401afac 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -168,8 +168,8 @@ export interface AgentConfig { branchId?: string | null; }; /** - * Host-supplied environment facts rendered as an `` - * block — the same surface the hosted chat runtime fills from Studio. + * Host-supplied environment facts rendered as an `` block for browser display context. + * It cannot replace the server-authored `` UTC snapshot added to every run. */ environmentContext?: string; /** From 7c2a69d13826e9ad605cb6c8f32fc2b85433b72a Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 9 Aug 2026 18:19:28 +0200 Subject: [PATCH 2/4] fix(agent): persist UTC stream diagnostics --- docs/guides/agents.md | 2 +- src/agent/factory-call-context.test.ts | 9 ++++++++- src/agent/runtime/index.ts | 8 +++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/guides/agents.md b/docs/guides/agents.md index 639d33cc1b..30e40a4e8a 100644 --- a/docs/guides/agents.md +++ b/docs/guides/agents.md @@ -378,7 +378,7 @@ the run, including long-running, scheduled, API-started, and browser-originated runs. Browser environment context can add a display timezone, but it does not replace the UTC snapshot. Non-streaming results expose the exact values at `result.metadata?.runtimeContext`; streaming runs emit them in the initial data -event for diagnostics. +event named `veryfront.runtime_context` for durable replay and diagnostics. ## Dynamic system prompts diff --git a/src/agent/factory-call-context.test.ts b/src/agent/factory-call-context.test.ts index d0ada0ba0e..2c25cf3245 100644 --- a/src/agent/factory-call-context.test.ts +++ b/src/agent/factory-call-context.test.ts @@ -40,6 +40,7 @@ async function captureFactorySystemPrompt( config: Omit, context?: Record, mode: "generate" | "stream" = "generate", + observeStreamBody?: (body: string) => void, ): Promise { let observed = ""; const model: ModelRuntime = { @@ -72,7 +73,7 @@ async function captureFactorySystemPrompt( input: "Where does this project live?", context, })).toDataStreamResponse(); - await response.text(); + observeStreamBody?.(await response.text()); } else { await assistant.generate({ input: "Where does this project live?", context }); } @@ -120,6 +121,7 @@ describe("agent/factory call context", () => { it("keeps browser display context without letting it replace server UTC", async () => { using _time = new FakeTime(new Date("2026-07-19T07:30:00.000Z")); + let streamBody = ""; const prompt = await captureFactorySystemPrompt( { id: "browser-agent", @@ -130,6 +132,9 @@ describe("agent/factory call context", () => { }, undefined, "stream", + (body) => { + streamBody = body; + }, ); assertStringIncludes(prompt, "Browser timezone: America/Los_Angeles"); @@ -138,6 +143,8 @@ describe("agent/factory call context", () => { prompt.indexOf("") < prompt.indexOf(""), true, ); + assertStringIncludes(streamBody, '"name":"veryfront.runtime_context"'); + assertStringIncludes(streamBody, '"runStartedAtUtc":"2026-07-19T07:30:00.000Z"'); }); it("preserves a plain agent's authored prompt before runtime context", async () => { diff --git a/src/agent/runtime/index.ts b/src/agent/runtime/index.ts index 65db36b39d..74075ae0a6 100644 --- a/src/agent/runtime/index.ts +++ b/src/agent/runtime/index.ts @@ -956,7 +956,13 @@ export class AgentRuntime { data: { inferenceMode: isLocal ? "server-local" : "cloud", model: resolvedModelString, - runtimeContext: runRuntimeContext, + }, + }); + sendSSE(controller, encoder, { + type: "data", + data: { + name: "veryfront.runtime_context", + value: runRuntimeContext, }, }); inFlight = chain.execute( From 707c1e004805cf3106f845027a18c50d98436462 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 9 Aug 2026 18:34:40 +0200 Subject: [PATCH 3/4] test(agent): narrow captured runtime contexts --- src/agent/runtime/provider-transport.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/agent/runtime/provider-transport.test.ts b/src/agent/runtime/provider-transport.test.ts index c37dc10c5c..00dbe1277d 100644 --- a/src/agent/runtime/provider-transport.test.ts +++ b/src/agent/runtime/provider-transport.test.ts @@ -171,8 +171,15 @@ describe("agent provider transport hooks", () => { } assertEquals(contexts.length, 2); - assertEquals(normalizeRunRuntimeContext(contexts[0]), normalizeRunRuntimeContext(contexts[1])); - assertEquals(normalizeRunRuntimeContext(contexts[0]), { + const cloudContext = contexts[0]; + const localContext = contexts[1]; + assertExists(cloudContext); + assertExists(localContext); + assertEquals( + normalizeRunRuntimeContext(cloudContext), + normalizeRunRuntimeContext(localContext), + ); + assertEquals(normalizeRunRuntimeContext(cloudContext), { type: "AGENT_RUN_MODEL_CALL_CONTEXT", messages: [ { From fc0f38043cf319e3889642acfdd9345ec7794928 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 9 Aug 2026 21:32:27 +0200 Subject: [PATCH 4/4] fix(agent): harden UTC runtime context --- docs/api-reference/veryfront/agent.md | 102 +++++++++--------- docs/guides/agents.md | 2 + src/agent/ag-ui/browser-encoder.test.ts | 13 +++ src/agent/factory-call-context.test.ts | 2 +- src/agent/runtime/index.ts | 7 +- src/agent/runtime/refresh.test.ts | 14 ++- src/agent/runtime/run-runtime-context.test.ts | 42 ++++++++ src/agent/runtime/run-runtime-context.ts | 26 +++-- src/agent/types.ts | 5 +- 9 files changed, 143 insertions(+), 70 deletions(-) create mode 100644 src/agent/runtime/run-runtime-context.test.ts diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index 4f9c26b2e6..84b8e4987c 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -141,27 +141,27 @@ Agent helper. | `model?` | `ModelString` | Optional model string in "provider/model" format. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L158) | | `system` | string | (() => string) | (() => Promise<string>) | System prompt: string, function, or async function | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L159) | | `projectContext?` | { projectId: string; branchId?: string | null } | Project this agent runs against. Rendered as a `` block so the agent knows the project reference and branch instead of asking for them. Hosts that already compose a full call context (the hosted chat runtime, project-runtime agent runs) supply it at that layer instead. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L166) | -| `environmentContext?` | `string` | Host-supplied environment facts rendered as an `` block for browser display context. It cannot replace the server-authored `` UTC snapshot added to every run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L174) | -| `tools?` | true | Record<string, Tool | boolean> | Project tools available to this agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L182) | -| `delegates?` | `string[]` | Exact registered agent ids this agent may call through scoped `agent_` tools. Each delegate keeps its own model, skills, and tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L187) | -| `sandbox?` | `object` | Optional sandbox selection for runtime-owned sandbox tools such as `bash`. `id` attaches to an existing sandbox session and detaches on run cleanup. When omitted, sandbox tools lazily create a request/project-scoped session. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L193) | -| `providerTools?` | `string[]` | Provider-native tools executed by the selected model provider, such as Anthropic `web_search` and `web_fetch`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L203) | -| `mcpServers?` | `AgentMcpServerConfig[]` | Remote MCP servers available to this agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L205) | -| `maxSteps?` | `number` | Max tool-call iterations per request | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L206) | -| `temperature?` | `number` | Sampling temperature for model generation. Defaults to 0. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L208) | -| `thinking?` | `RuntimeAgentThinkingConfig` | Provider-neutral reasoning / thinking configuration for hosted runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L210) | -| `streaming?` | `boolean` | Enable streaming responses | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L211) | -| `memory?` | `MemoryConfig` | Conversation memory persisted across `stream()` / `generate()` calls on this instance. Omit for the stateless default: every call runs in isolation, which keeps concurrent fan-out on a shared instance correct. When set, the instance accumulates one shared conversation, so reuse it sequentially, not across concurrent independent runs (use a separate instance per run for that). Set `enabled: false` to force the stateless behavior explicitly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L220) | -| `middleware?` | `AgentMiddleware[]` | Execution middleware pipeline | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L221) | -| `edge?` | `EdgeConfig` | Edge runtime configuration | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L222) | -| `multimodal?` | { vision?: boolean; audio?: boolean } | Enable vision and/or audio | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L223) | -| `allowedModels?` | `ModelString[]` | Restrict runtime model overrides to these "provider/model" strings. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L228) | -| `resolveModelTransport?` | `ModelTransportResolver` | Optional request-aware hook for overriding the resolved model runtime and provider transport options on a per-call basis. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L233) | -| `resolveRuntimeState?` | `RuntimeStateResolver` | Optional step-boundary hook for refreshing the runtime system prompt and host-owned context during a long-lived run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L238) | -| `onToolResult?` | `ToolExecutionResultHandler` | Optional hook invoked after the runtime executes a configured local, registry, integration, or remote tool and before the tool result is persisted or streamed back to callers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L244) | -| `skills?` | `true \| false \| string[]` | Select visible skill IDs or this agent's own skill short names advertised in this agent's system prompt and authorized for `load_skill`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L256) | -| `suggestions?` | `SuggestionsConfig` | Prompt starters shown on an empty chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L263) | -| `security?` | `false` | Set to false to disable the default security middleware | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L265) | +| `environmentContext?` | `string` | Use this property for host-supplied browser display facts rendered in an `` block. It cannot replace the server-authored UTC `` snapshot for a run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L175) | +| `tools?` | true | Record<string, Tool | boolean> | Project tools available to this agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L183) | +| `delegates?` | `string[]` | Exact registered agent ids this agent may call through scoped `agent_` tools. Each delegate keeps its own model, skills, and tools. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L188) | +| `sandbox?` | `object` | Optional sandbox selection for runtime-owned sandbox tools such as `bash`. `id` attaches to an existing sandbox session and detaches on run cleanup. When omitted, sandbox tools lazily create a request/project-scoped session. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L194) | +| `providerTools?` | `string[]` | Provider-native tools executed by the selected model provider, such as Anthropic `web_search` and `web_fetch`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L204) | +| `mcpServers?` | `AgentMcpServerConfig[]` | Remote MCP servers available to this agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L206) | +| `maxSteps?` | `number` | Max tool-call iterations per request | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L207) | +| `temperature?` | `number` | Sampling temperature for model generation. Defaults to 0. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L209) | +| `thinking?` | `RuntimeAgentThinkingConfig` | Provider-neutral reasoning / thinking configuration for hosted runtimes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L211) | +| `streaming?` | `boolean` | Enable streaming responses | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L212) | +| `memory?` | `MemoryConfig` | Conversation memory persisted across `stream()` / `generate()` calls on this instance. Omit for the stateless default: every call runs in isolation, which keeps concurrent fan-out on a shared instance correct. When set, the instance accumulates one shared conversation, so reuse it sequentially, not across concurrent independent runs (use a separate instance per run for that). Set `enabled: false` to force the stateless behavior explicitly. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L221) | +| `middleware?` | `AgentMiddleware[]` | Execution middleware pipeline | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L222) | +| `edge?` | `EdgeConfig` | Edge runtime configuration | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L223) | +| `multimodal?` | { vision?: boolean; audio?: boolean } | Enable vision and/or audio | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L224) | +| `allowedModels?` | `ModelString[]` | Restrict runtime model overrides to these "provider/model" strings. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L229) | +| `resolveModelTransport?` | `ModelTransportResolver` | Optional request-aware hook for overriding the resolved model runtime and provider transport options on a per-call basis. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L234) | +| `resolveRuntimeState?` | `RuntimeStateResolver` | Optional step-boundary hook for refreshing the runtime system prompt and host-owned context during a long-lived run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L239) | +| `onToolResult?` | `ToolExecutionResultHandler` | Optional hook invoked after the runtime executes a configured local, registry, integration, or remote tool and before the tool result is persisted or streamed back to callers. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L245) | +| `skills?` | `true \| false \| string[]` | Select visible skill IDs or this agent's own skill short names advertised in this agent's system prompt and authorized for `load_skill`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L257) | +| `suggestions?` | `SuggestionsConfig` | Prompt starters shown on an empty chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L264) | +| `security?` | `false` | Set to false to disable the default security middleware | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L266) | **Returns:** `Agent` @@ -177,13 +177,13 @@ Run the agent and return a complete response. Accepts a string or message array | Property | Type | Description | Source | | ------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `input` | `string \| Message[]` | Prompt string or message history | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L393) | -| `context?` | Record<string, unknown> | Additional context passed to the agent | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L394) | -| `model?` | `ModelString` | Override the agent's default model for this request. Must be in `allowedModels` if configured. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L396) | -| `maxOutputTokens?` | `number` | Override the maximum model output tokens for this request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L398) | -| `tools?` | `AgentGenerateToolReplacements` | Replace this agent's configured tools for this generate request only. When present, only these tools are advertised and executable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L403) | -| `retainSkillLoaderTools?` | `boolean` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L407) | -| `abortSignal?` | `AbortSignal` | Abort signal for cooperative cancellation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L409) | +| `input` | `string \| Message[]` | Prompt string or message history | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L394) | +| `context?` | Record<string, unknown> | Additional context passed to the agent | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L395) | +| `model?` | `ModelString` | Override the agent's default model for this request. Must be in `allowedModels` if configured. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L397) | +| `maxOutputTokens?` | `number` | Override the maximum model output tokens for this request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L399) | +| `tools?` | `AgentGenerateToolReplacements` | Replace this agent's configured tools for this generate request only. When present, only these tools are advertised and executable. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L404) | +| `retainSkillLoaderTools?` | `boolean` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L408) | +| `abortSignal?` | `AbortSignal` | Abort signal for cooperative cancellation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L410) | **Returns:** Promise<AgentResponse> @@ -193,15 +193,15 @@ Run the agent and stream the response. Returns a result with `.toDataStreamRespo | Property | Type | Description | Source | | ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `input?` | `string` | Prompt string | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L413) | -| `messages?` | `Message[]` | Conversation message history | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L414) | -| `context?` | Record<string, unknown> | Additional context passed to the agent | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L415) | -| `model?` | `ModelString` | Override the agent's default model for this request. Must be in `allowedModels` if configured. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L417) | -| `maxOutputTokens?` | `number` | Override the maximum model output tokens for this request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L419) | -| `onToolCall?` | (toolCall: ToolCall) => void | Callback fired when a tool is invoked | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L420) | -| `onChunk?` | (chunk: string) => void | Callback fired for each text chunk | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L421) | -| `onFinish?` | (response: AgentResponse) => void | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L422) | -| `abortSignal?` | `AbortSignal` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L423) | +| `input?` | `string` | Prompt string | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L414) | +| `messages?` | `Message[]` | Conversation message history | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L415) | +| `context?` | Record<string, unknown> | Additional context passed to the agent | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L416) | +| `model?` | `ModelString` | Override the agent's default model for this request. Must be in `allowedModels` if configured. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L418) | +| `maxOutputTokens?` | `number` | Override the maximum model output tokens for this request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L420) | +| `onToolCall?` | (toolCall: ToolCall) => void | Callback fired when a tool is invoked | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L421) | +| `onChunk?` | (chunk: string) => void | Callback fired for each text chunk | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L422) | +| `onFinish?` | (response: AgentResponse) => void | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L423) | +| `abortSignal?` | `AbortSignal` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L424) | **Returns:** Promise<AgentStreamResult> @@ -822,13 +822,13 @@ Input delivered to a hosted agent-service detached execution callback. | `getRuntimeProjectInstructions` | Return runtime project instructions. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/project-skill-catalog.ts#L608) | | `getRuntimeProjectSkillCatalog` | Return runtime project skill catalog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/project-skill-catalog.ts#L643) | | `getRuntimeUploadUrl` | Return runtime upload URL. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/upload-url-client.ts#L38) | -| `getTextFromParts` | Return text from parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L349) | -| `getToolArguments` | Return tool arguments. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L367) | +| `getTextFromParts` | Return text from parts. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L350) | +| `getToolArguments` | Return tool arguments. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L368) | | `handleHostedChildForkFailure` | Process a hosted child fork failure. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-stream-execution.ts#L280) | | `handleHostedChildForkRunContextError` | Error shape for handle hosted child fork run context. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-run-context.ts#L280) | | `handleHostedChildForkStreamPart` | Process a hosted child fork stream part. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-fork-stream-execution.ts#L318) | -| `hasArgs` | Check whether args is present. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L357) | -| `hasInput` | Input payload for has. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L362) | +| `hasArgs` | Check whether args is present. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L358) | +| `hasInput` | Input payload for has. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L363) | | `initializeNodeAgentServiceOpenTelemetry` | Initialize node agent service open telemetry. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/node-telemetry.ts#L470) | | `initializeNodeHostedAgentServiceOpenTelemetry` | Initialize node hosted agent service open telemetry. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/node-telemetry.ts#L416) | | `installAbortRejectionGuard` | Install abort rejection guard helper. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/abort-rejection-guard.ts#L114) | @@ -1072,7 +1072,7 @@ Input delivered to a hosted agent-service detached execution callback. | `AbortRejectionGuardLogger` | Public API contract for abort rejection guard logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/abort-rejection-guard.ts#L2) | | `AbortRejectionProcessTarget` | Public API contract for abort rejection process target. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/abort-rejection-guard.ts#L7) | | `ActiveConversationRunStatus` | Public API contract for a conversation run status is active. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-contracts.ts#L181) | -| `Agent` | Public API contract for agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L388) | +| `Agent` | Public API contract for agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L389) | | `AgentCallProjectContext` | Project the call runs against, rendered as the `` block. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/call-context.ts#L44) | | `AgentCatalogAction` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/identity-contracts.ts#L13) | | `AgentCatalogKind` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/identity-contracts.ts#L6) | @@ -1085,7 +1085,7 @@ Input delivered to a hosted agent-service detached execution callback. | `AgentMcpServerConfig` | MCP server available to an agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L138) | | `AgentMcpToolPolicy` | Policy for tools exposed by one MCP server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L95) | | `AgentMessage` | Message exchanged with an agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/schemas/agent.schema.ts#L256) | -| `AgentMiddleware` | Public API contract for agent middleware. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L342) | +| `AgentMiddleware` | Public API contract for agent middleware. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L343) | | `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) | @@ -1169,7 +1169,7 @@ Input delivered to a hosted agent-service detached execution callback. | `AgentServiceTraceContext` | Context for agent service trace. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/bootstrap.ts#L8) | | `AgentServiceTraceContextGetter` | Public API contract for agent service trace context getter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/bootstrap.ts#L14) | | `AgentStatus` | Public API contract for agent status. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/schemas/agent.schema.ts#L240) | -| `AgentStreamResult` | Result returned from agent stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L379) | +| `AgentStreamResult` | Result returned from agent stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L380) | | `AgentTraceAttributes` | Public API contract for agent trace attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L10) | | `AgentTraceAttributeValue` | Public API contract for a value can be used as an agent trace attribute. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L4) | | `AgentTraceUsage` | Public API contract for agent trace usage. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/trace-attributes.ts#L13) | @@ -1652,8 +1652,8 @@ Input delivered to a hosted agent-service detached execution callback. | `ModelCallTool` | Resolved provider-agnostic tool definition supplied to a model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/model-call-context.ts#L35) | | `ModelProvider` | Public API contract for model provider. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/schemas/agent.schema.ts#L238) | | `ModelString` | Model configuration string format: "provider/model-name" Examples: "openai/gpt-4", "anthropic/claude-3-5-sonnet" | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L43) | -| `ModelTransportRequest` | Request payload for model transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L272) | -| `ModelTransportResolver` | Public API contract for model transport resolver. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L296) | +| `ModelTransportRequest` | Request payload for model transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L273) | +| `ModelTransportResolver` | Public API contract for model transport resolver. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L297) | | `MonitorHostedChildRunStatusInput` | Input payload for monitor hosted child run status. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-status.ts#L135) | | `MutableAgentProjectContext` | Context for mutable agent project. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/project/context.ts#L33) | | `NodeAgentServiceInstrumentationConfig` | Configuration used by node agent service instrumentation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/node-telemetry.ts#L23) | @@ -1731,11 +1731,11 @@ Input delivered to a hosted agent-service detached execution callback. | `RequestAuthCache` | Public API contract for request auth cache. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/request-auth-cache.ts#L14) | | `ResolveAgentServiceRegistrationInputOptions` | Options accepted by resolve agent service registration input. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L182) | | `ResolveConversationHostedTerminalStateInput` | Input payload for resolve conversation hosted terminal state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/hosted-terminal.ts#L29) | -| `ResolvedAgentConfig` | Configuration used by resolved agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L269) | +| `ResolvedAgentConfig` | Configuration used by resolved agent. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L270) | | `ResolvedAgentServiceRegistrationInput` | Input payload for resolved agent service registration. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L24) | | `ResolvedHostedRuntimeRequestConfig` | Configuration used by resolved hosted runtime request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/runtime-request-config.ts#L46) | -| `ResolvedModelTransport` | Public API contract for resolved model transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L288) | -| `ResolvedRuntimeState` | State for resolved runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L311) | +| `ResolvedModelTransport` | Public API contract for resolved model transport. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L289) | +| `ResolvedRuntimeState` | State for resolved runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L312) | | `ResolveHostedChildForkRuntimeConfigInput` | Input payload for resolve hosted child fork runtime config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/child-tool-input.ts#L221) | | `ResolveHostedRuntimeRequestConfigInput` | Input payload for resolve hosted runtime request config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/runtime-request-config.ts#L36) | | `ResolveNodeAgentServiceTelemetryConfigOptions` | Options accepted by resolve node agent service telemetry config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/node-telemetry.ts#L62) | @@ -1793,8 +1793,8 @@ Input delivered to a hosted agent-service detached execution callback. | `RuntimeSkillDefinition` | Definition for runtime skill. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/skill-metadata.ts#L278) | | `RuntimeSkillFrontmatter` | Public API contract for runtime skill frontmatter. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/skill-metadata.ts#L178) | | `RuntimeSkillMetadataLogger` | Public API contract for runtime skill metadata logger. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/skill-metadata.ts#L405) | -| `RuntimeStateRequest` | Request payload for runtime state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L301) | -| `RuntimeStateResolver` | Public API contract for runtime state resolver. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L317) | +| `RuntimeStateRequest` | Request payload for runtime state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L302) | +| `RuntimeStateResolver` | Public API contract for runtime state resolver. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L318) | | `RuntimeUploadUrlClientOptions` | Options accepted by runtime upload URL client. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/upload-url-client.ts#L24) | | `RuntimeUploadUrlFetch` | Public API contract for runtime upload URL fetch. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/upload-url-client.ts#L21) | | `RuntimeUploadUrlOptions` | Options accepted by runtime upload URL. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/upload-url-client.ts#L31) | diff --git a/docs/guides/agents.md b/docs/guides/agents.md index 30e40a4e8a..c5c8f3c465 100644 --- a/docs/guides/agents.md +++ b/docs/guides/agents.md @@ -385,6 +385,8 @@ event named `veryfront.runtime_context` for durable replay and diagnostics. The `system` property accepts a string, a function, or an async function: ```ts +import { agent } from "veryfront/agent"; + export default agent({ id: "assistant", system: async () => { diff --git a/src/agent/ag-ui/browser-encoder.test.ts b/src/agent/ag-ui/browser-encoder.test.ts index 6baa997772..d01032388b 100644 --- a/src/agent/ag-ui/browser-encoder.test.ts +++ b/src/agent/ag-ui/browser-encoder.test.ts @@ -172,6 +172,19 @@ describe("agent/ag-ui-browser-encoder", () => { payload: { name: "message-metadata", value: { status: "running" } }, }], ); + assertEquals( + mapRuntimeStreamEventToAgUiBrowserEvents(state, { + type: "data-veryfront.runtime_context", + data: { runStartedAtUtc: "2026-07-19T07:30:00.000Z" }, + }), + [{ + event: "Custom", + payload: { + name: "veryfront.runtime_context", + value: { runStartedAtUtc: "2026-07-19T07:30:00.000Z" }, + }, + }], + ); assertEquals( mapRuntimeStreamEventToAgUiBrowserEvents(state, { type: "data-tool-call-status", diff --git a/src/agent/factory-call-context.test.ts b/src/agent/factory-call-context.test.ts index 2c25cf3245..2a8448a0c8 100644 --- a/src/agent/factory-call-context.test.ts +++ b/src/agent/factory-call-context.test.ts @@ -143,7 +143,7 @@ describe("agent/factory call context", () => { prompt.indexOf("") < prompt.indexOf(""), true, ); - assertStringIncludes(streamBody, '"name":"veryfront.runtime_context"'); + assertStringIncludes(streamBody, '"type":"data-veryfront.runtime_context"'); assertStringIncludes(streamBody, '"runStartedAtUtc":"2026-07-19T07:30:00.000Z"'); }); diff --git a/src/agent/runtime/index.ts b/src/agent/runtime/index.ts index 74075ae0a6..ab63a5661a 100644 --- a/src/agent/runtime/index.ts +++ b/src/agent/runtime/index.ts @@ -959,11 +959,8 @@ export class AgentRuntime { }, }); sendSSE(controller, encoder, { - type: "data", - data: { - name: "veryfront.runtime_context", - value: runRuntimeContext, - }, + type: "data-veryfront.runtime_context", + data: runRuntimeContext, }); inFlight = chain.execute( agentContext, diff --git a/src/agent/runtime/refresh.test.ts b/src/agent/runtime/refresh.test.ts index af19790a68..9cef2c596c 100644 --- a/src/agent/runtime/refresh.test.ts +++ b/src/agent/runtime/refresh.test.ts @@ -2239,6 +2239,7 @@ describe("agent runtime refresh hooks", () => { }); it("refreshes the streaming system prompt between hosted run steps", async () => { + using time = new FakeTime(new Date("2026-07-19T07:30:00.000Z")); const runtimeRequests: RuntimeStateRequest[] = []; const observedSystems: string[] = []; let callCount = 0; @@ -2292,7 +2293,10 @@ describe("agent runtime refresh hooks", () => { id: "switch_project", description: "Switch the active project context", inputSchema: defineSchema((v) => v.object({ projectId: v.string() }))(), - execute: async ({ projectId }) => ({ projectId }), + execute: async ({ projectId }) => { + time.tick(24 * 60 * 60 * 1_000); + return { projectId }; + }, }); const assistant = eagerAgent({ @@ -2326,6 +2330,14 @@ describe("agent runtime refresh hooks", () => { "Base streaming system prompt", "Refreshed streaming system prompt", ]); + for (const system of observedSystems) { + assertEquals(system.match(//g)?.length, 1); + assertEquals( + system.includes("run_started_at_utc: 2026-07-19T07:30:00.000Z"), + true, + ); + assertEquals(system.includes("2026-07-20"), false); + } assertEquals(body.includes("stream done"), true); }); diff --git a/src/agent/runtime/run-runtime-context.test.ts b/src/agent/runtime/run-runtime-context.test.ts new file mode 100644 index 0000000000..0bbfb38942 --- /dev/null +++ b/src/agent/runtime/run-runtime-context.test.ts @@ -0,0 +1,42 @@ +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { + captureAgentRunRuntimeContext, + withAgentRunRuntimeContext, +} from "./run-runtime-context.ts"; + +describe("agent run runtime context", () => { + const context = captureAgentRunRuntimeContext( + new Date("2026-07-19T07:30:00.000Z"), + ); + + it("removes authored runtime context blocks with whitespace or attributes", () => { + for ( + const openingTag of [ + "", + '', + ] + ) { + const result = withAgentRunRuntimeContext( + `Base\n\n${openingTag}\ncurrent_date_utc: 2025-01-01\n\n\nSuffix`, + context, + ); + + assertEquals(result.includes("2025-01-01"), false); + assertEquals(result.includes("Base"), true); + assertEquals(result.includes("Suffix"), true); + assertEquals(result.match(//g)?.length, 1); + } + }); + + it("removes an unclosed authored runtime context block through the end", () => { + const result = withAgentRunRuntimeContext( + 'Base\n\n\ncurrent_date_utc: 2025-01-01', + context, + ); + + assertEquals(result.includes("2025-01-01"), false); + assertEquals(result.startsWith("Base\n\n"), true); + assertEquals(result.match(//g)?.length, 1); + }); +}); diff --git a/src/agent/runtime/run-runtime-context.ts b/src/agent/runtime/run-runtime-context.ts index 3b2fe94b1e..6845902c77 100644 --- a/src/agent/runtime/run-runtime-context.ts +++ b/src/agent/runtime/run-runtime-context.ts @@ -1,7 +1,8 @@ import { createRuntimePromptBlock } from "./prompt-block.ts"; -const RUNTIME_CONTEXT_OPEN_TAG = ""; -const RUNTIME_CONTEXT_CLOSE_TAG = ""; +const RUNTIME_CONTEXT_OPEN_TAG_PATTERN = /]*)?>/; +const RUNTIME_CONTEXT_CLOSE_TAG_PATTERN = /<\/runtime_context\s*>/; +const RUNTIME_CONTEXT_CLOSE_TAG_PATTERN_GLOBAL = /<\/runtime_context\s*>/g; /** Server-authored UTC facts captured once for one agent run. */ export type AgentRunRuntimeContext = Readonly<{ @@ -22,22 +23,27 @@ export function captureAgentRunRuntimeContext(now = new Date()): AgentRunRuntime function removeReservedRuntimeContextBlocks(instructions: string): string { let result = instructions; - let openIndex = result.indexOf(RUNTIME_CONTEXT_OPEN_TAG); + let openIndex = result.search(RUNTIME_CONTEXT_OPEN_TAG_PATTERN); while (openIndex >= 0) { - const closeIndex = result.indexOf(RUNTIME_CONTEXT_CLOSE_TAG, openIndex); - if (closeIndex < 0) { - result = result.slice(0, openIndex) + - result.slice(openIndex + RUNTIME_CONTEXT_OPEN_TAG.length); + const openingTag = result.slice(openIndex).match(RUNTIME_CONTEXT_OPEN_TAG_PATTERN)?.[0]; + if (!openingTag) break; + const contentStart = openIndex + openingTag.length; + const closeOffset = result.slice(contentStart).search(RUNTIME_CONTEXT_CLOSE_TAG_PATTERN); + if (closeOffset < 0) { + result = result.slice(0, openIndex); break; } + const closeIndex = contentStart + closeOffset; + const closingTag = result.slice(closeIndex).match(RUNTIME_CONTEXT_CLOSE_TAG_PATTERN)?.[0]; + if (!closingTag) break; result = result.slice(0, openIndex) + - result.slice(closeIndex + RUNTIME_CONTEXT_CLOSE_TAG.length); - openIndex = result.indexOf(RUNTIME_CONTEXT_OPEN_TAG); + result.slice(closeIndex + closingTag.length); + openIndex = result.search(RUNTIME_CONTEXT_OPEN_TAG_PATTERN); } - return result.replaceAll(RUNTIME_CONTEXT_CLOSE_TAG, "").trim(); + return result.replaceAll(RUNTIME_CONTEXT_CLOSE_TAG_PATTERN_GLOBAL, "").trim(); } /** Render the authoritative UTC snapshot as a reserved system block. */ diff --git a/src/agent/types.ts b/src/agent/types.ts index 483401afac..02329554d6 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -168,8 +168,9 @@ export interface AgentConfig { branchId?: string | null; }; /** - * Host-supplied environment facts rendered as an `` block for browser display context. - * It cannot replace the server-authored `` UTC snapshot added to every run. + * Use this property for host-supplied browser display facts rendered in an + * `` block. It cannot replace the server-authored UTC + * `` snapshot for a run. */ environmentContext?: string; /**