Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions docs/api-reference/veryfront/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | <code>string &#124; (() =&gt; string) &#124; (() =&gt; Promise&lt;string&gt;)</code> | System prompt: string, function, or async function | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L159) |
| `projectContext?` | <code>&#123; projectId: string; branchId?: string &#124; null &#125;</code> | Project this agent runs against. Rendered as a `<project_context>` 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 `<environment_context>` 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 `<environment_context>` block for browser display context. It cannot replace the server-authored `<runtime_context>` UTC snapshot added to every run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/types.ts#L174) |
| `tools?` | <code>true &#124; Record&lt;string, Tool &#124; boolean&gt;</code> | 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_<id>` 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) |
Expand Down Expand Up @@ -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) |
Expand Down
30 changes: 28 additions & 2 deletions docs/guides/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<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 for diagnostics.

## Dynamic system prompts

The `system` property accepts a string, a function, or an async function:
Expand All @@ -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()}`;
},
});
```
Expand Down
58 changes: 54 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,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<AgentConfig, "model" | "resolveModelTransport">,
context?: Record<string, unknown>,
mode: "generate" | "stream" = "generate",
): Promise<string> {
let observed = "";
const model: ModelRuntime = {
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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<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"));
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",
);

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

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
45 changes: 40 additions & 5 deletions src/agent/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -790,6 +796,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 @@ -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);
Expand Down Expand Up @@ -827,6 +836,7 @@ export class AgentRuntime {
projectId: tryGetCacheKeyContext()?.projectId,
},
context,
runRuntimeContext,
supportsToolCalling,
resolvedModelString,
transport.languageModel,
Expand Down Expand Up @@ -861,6 +871,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 @@ -941,6 +956,7 @@ export class AgentRuntime {
data: {
inferenceMode: isLocal ? "server-local" : "cloud",
model: resolvedModelString,
runtimeContext: runRuntimeContext,
Comment thread
kojiwakayama marked this conversation as resolved.
Outdated
},
});
inFlight = chain.execute(
Expand All @@ -955,6 +971,7 @@ export class AgentRuntime {
textPartId,
toolContext,
context,
runRuntimeContext,
supportsToolCalling,
resolvedModelString,
languageModel,
Expand Down Expand Up @@ -1021,6 +1038,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 @@ -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,
Expand Down Expand Up @@ -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,
),
};
}

Expand Down Expand Up @@ -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`,
}),
};
});
}
Expand All @@ -1641,6 +1668,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 @@ -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,
),
Expand Down Expand Up @@ -2236,7 +2268,10 @@ export class AgentRuntime {
toolCalls,
status: "completed",
usage: totalUsage,
metadata: finalFinishReason ? { finishReason: finalFinishReason } : undefined,
metadata: withAgentRunRuntimeContextMetadata(
runRuntimeContext,
finalFinishReason ? { finishReason: finalFinishReason } : undefined,
),
};
}

Expand Down
29 changes: 26 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,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<runtime_context>\nserver-authored UTC snapshot\n</runtime_context>",
},
{
role: "user",
content: [{ type: "text", text: "Use the same normalized input." }],
Expand Down
Loading
Loading