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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 52 additions & 52 deletions docs/api-reference/veryfront/agent.md

Large diffs are not rendered by default.

32 changes: 30 additions & 2 deletions docs/guides/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,16 +355,44 @@ 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
<runtime_context>
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.
</runtime_context>
```

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 named `veryfront.runtime_context` for durable replay and diagnostics.

## Dynamic system prompts

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 () => {
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()}`;
},
});
```
Expand Down
13 changes: 13 additions & 0 deletions src/agent/ag-ui/browser-encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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",
Expand Down
65 changes: 61 additions & 4 deletions src/agent/factory-call-context.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -37,6 +38,9 @@ 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<AgentConfig, "model" | "resolveModelTransport">,
context?: Record<string, unknown>,
mode: "generate" | "stream" = "generate",
observeStreamBody?: (body: string) => void,
): Promise<string> {
let observed = "";
const model: ModelRuntime = {
Expand All @@ -52,7 +56,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;
Expand All @@ -63,7 +68,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();
observeStreamBody?.(await response.text());
} else {
await assistant.generate({ input: "Where does this project live?", context });
}

return observed;
}
Expand All @@ -90,14 +103,58 @@ 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<runtime_context>\ncurrent_date_utc: 2025-07-14\n</runtime_context>",
skills: false,
}, { scheduleId: "schedule-1" });

assertEquals(prompt.includes("2025-07-14"), false);
assertEquals(prompt.match(/<runtime_context>/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"));
let streamBody = "";
const prompt = await captureFactorySystemPrompt(
{
id: "browser-agent",
system: "Answer with the current date.",
environmentContext:
"<date_time>\nBrowser timezone: America/Los_Angeles\nBrowser date: 2025-07-14\n</date_time>",
skills: false,
},
undefined,
"stream",
(body) => {
streamBody = body;
},
);

assertStringIncludes(prompt, "Browser timezone: America/Los_Angeles");
assertStringIncludes(prompt, "current_date_utc: 2026-07-19");
assertEquals(
prompt.indexOf("<environment_context>") < prompt.indexOf("<runtime_context>"),
true,
);
assertStringIncludes(streamBody, '"type":"data-veryfront.runtime_context"');
assertStringIncludes(streamBody, '"runStartedAtUtc":"2026-07-19T07:30:00.000Z"');
});

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<runtime_context>"), true);
});

it("renders skills through the shared runtime skills block", async () => {
Expand Down
48 changes: 43 additions & 5 deletions src/agent/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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";
Expand Down Expand Up @@ -794,6 +800,7 @@ export class AgentRuntime {
},
): Promise<AgentResponse> {
throwIfAborted(abortSignal);
const runRuntimeContext = captureAgentRunRuntimeContext();
const transport = await this.resolveModelTransport(context, modelOverride, "generate");
const requestedModel = transport.requestedModel;
const resolvedModelString = transport.resolvedModelString;
Expand All @@ -804,6 +811,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);
Expand Down Expand Up @@ -832,6 +841,7 @@ export class AgentRuntime {
projectId: tryGetCacheKeyContext()?.projectId,
},
context,
runRuntimeContext,
supportsToolCalling,
resolvedModelString,
transport.languageModel,
Expand Down Expand Up @@ -867,6 +877,11 @@ export class AgentRuntime {
maxOutputTokensOverride?: number,
abortSignal?: AbortSignal,
): Promise<ReadableStream<Uint8Array>> {
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;
Expand Down Expand Up @@ -949,6 +964,10 @@ export class AgentRuntime {
model: resolvedModelString,
},
});
sendSSE(controller, encoder, {
type: "data-veryfront.runtime_context",
data: runRuntimeContext,
});
inFlight = chain.execute(
agentContext,
() =>
Expand All @@ -962,6 +981,7 @@ export class AgentRuntime {
textPartId,
toolContext,
context,
runRuntimeContext,
supportsToolCalling,
resolvedModelString,
languageModel,
Expand Down Expand Up @@ -1029,6 +1049,7 @@ export class AgentRuntime {
messages: Message[],
toolContextBase: ToolExecutionContext | undefined,
runtimeContext: Record<string, unknown> | undefined,
runRuntimeContext: AgentRunRuntimeContext,
supportsToolCalling: boolean,
modelString?: string,
resolvedModel?: ModelRuntime,
Expand Down Expand Up @@ -1181,9 +1202,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,
Expand Down Expand Up @@ -1298,7 +1323,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,
),
};
}

Expand Down Expand Up @@ -1629,7 +1657,9 @@ export class AgentRuntime {
toolCalls,
status: this.status,
usage: totalUsage,
metadata: { warning: `Max steps (${maxSteps}) reached` },
metadata: withAgentRunRuntimeContextMetadata(runRuntimeContext, {
warning: `Max steps (${maxSteps}) reached`,
}),
};
});
}
Expand All @@ -1652,6 +1682,7 @@ export class AgentRuntime {
textPartId: string | undefined,
toolContextBase: Record<string, unknown> | undefined,
runtimeContext: Record<string, unknown> | undefined,
runRuntimeContext: AgentRunRuntimeContext,
supportsToolCalling: boolean,
modelString?: string,
resolvedModel?: ModelRuntime,
Expand Down Expand Up @@ -1772,10 +1803,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,
),
Expand Down Expand Up @@ -2250,7 +2285,10 @@ export class AgentRuntime {
toolCalls,
status: "completed",
usage: totalUsage,
metadata: finalFinishReason ? { finishReason: finalFinishReason } : undefined,
metadata: withAgentRunRuntimeContextMetadata(
runRuntimeContext,
finalFinishReason ? { finishReason: finalFinishReason } : undefined,
),
};
}

Expand Down
36 changes: 33 additions & 3 deletions src/agent/runtime/provider-transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
/<runtime_context>[\s\S]*<\/runtime_context>/,
"<runtime_context>\nserver-authored UTC snapshot\n</runtime_context>",
),
}
: message
),
};
}

describe("agent provider transport hooks", () => {
afterEach(() => {
if (originalLogLevel === undefined) Deno.env.delete("LOG_LEVEL");
Expand Down Expand Up @@ -152,11 +171,22 @@ describe("agent provider transport hooks", () => {
}

assertEquals(contexts.length, 2);
assertEquals(contexts[0], contexts[1]);
assertEquals(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: [
{ role: "system", content: "Follow the same instructions." },
{
role: "system",
content:
"Follow the same instructions.\n\n<runtime_context>\nserver-authored UTC snapshot\n</runtime_context>",
},
{
role: "user",
content: [{ type: "text", text: "Use the same normalized input." }],
Expand Down
Loading