Skip to content
Closed
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
30 changes: 15 additions & 15 deletions docs/api-reference/veryfront/agent.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/api-reference/veryfront/embedding.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const { POST, GET, DELETE } = createUploadHandler(store, {
| `ragStore` | Creates a persistent RAG store with lazy embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/rag-store.ts#L212) |
| `registerEmbeddingProvider` | Register an embedding provider factory. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L25) |
| `resolveEmbeddingModel` | Resolve a "provider/model" string to an embedding runtime instance. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/resolve.ts#L116) |
| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1265) |
| `similarity` | Compute cosine similarity between two numeric vectors. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/runtime/runtime-bridge.ts#L1286) |
| `vectorStore` | Creates an in-memory vector store with integrated embedding and similarity search. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/embedding/vector-store.ts#L46) |

### Types
Expand Down
2 changes: 2 additions & 0 deletions src/agent/runtime/chat-stream-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export interface ChatStreamState {
accumulatedText: string;
reasoningParts: StreamingReasoningPart[];
finishReason: string | null;
providerMetadata?: Record<string, unknown>;
toolCalls: Map<string, StreamingToolCall>;
toolResults: StreamingToolResult[];
suppressedToolCalls: { id: string; name: string }[];
Expand Down Expand Up @@ -1395,6 +1396,7 @@ export function processStreamInternal(
closeTextSegment();
closeReasoningSegment();
state.finishReason = typedPart.finishReason ?? null;
state.providerMetadata = typedPart.providerMetadata;
if (state.finishReason) {
setActiveSpanAttributes({
"gen_ai.response.finish_reasons": [state.finishReason],
Expand Down
1 change: 1 addition & 0 deletions src/agent/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ function buildGeneratedAssistantMessage(
...metadata,
role: "assistant",
parts,
...(response.providerMetadata ? { providerOptions: response.providerMetadata } : {}),
};
}

Expand Down
270 changes: 265 additions & 5 deletions src/agent/runtime/provider-transport.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import "#veryfront/schemas/_test-setup.ts";
import { clearModelProviders, type ModelRuntime, registerModelProvider } from "#veryfront/provider";
import { defineSchema } from "#veryfront/schemas/index.ts";
import { assertEquals, assertExists, assertStringIncludes } from "#veryfront/testing/assert.ts";
import { afterEach, describe, it } from "#veryfront/testing/bdd.ts";
import { clearModelProviders, type ModelRuntime, registerModelProvider } from "#veryfront/provider";
import { agent } from "../factory.ts";
import type { AgentRunModelCallContextEvent } from "../../runtime/model-call-context.ts";
import { runWithRunEventSink } from "../../runtime/run-event-sink-context.ts";
import type { ModelTransportRequest } from "../types.ts";
import { tool } from "#veryfront/tool";
import {
__registerLogRecordEmitter,
__resetLoggerConfigForTests,
__resetLogRecordEmitterForTests,
type LogEntry,
} from "#veryfront/utils/logger/logger.ts";
import { createGoogleModelRuntime } from "../../../extensions/ext-llm-google/src/google-provider.ts";
import type { AgentRunModelCallContextEvent } from "../../runtime/model-call-context.ts";
import { runWithRunEventSink } from "../../runtime/run-event-sink-context.ts";
import { agent } from "../factory.ts";
import type { ModelTransportRequest } from "../types.ts";

const originalLogLevel = Deno.env.get("LOG_LEVEL");

Expand Down Expand Up @@ -39,6 +42,17 @@ function createTextStream(
});
}

function createRuntimeStream(parts: unknown[]) {
return new ReadableStream<unknown>({
start(controller) {
for (const part of parts) {
controller.enqueue(part);
}
controller.close();
},
});
}

function normalizeRunRuntimeContext(
event: AgentRunModelCallContextEvent,
): Pick<AgentRunModelCallContextEvent, "type" | "messages"> {
Expand Down Expand Up @@ -131,6 +145,252 @@ describe("agent provider transport hooks", () => {
});
});

it("replays provider metadata on the generate tool-result leg", async () => {
const providerMetadata = {
google: {
rawAssistantParts: [{
functionCall: { id: "lookup-1", name: "lookup", args: { value: "alpha" } },
thoughtSignature: "signed-tool-turn-generate",
}],
},
};
let callCount = 0;
let replayedProviderMetadata: unknown;
const modelCallContexts: AgentRunModelCallContextEvent[] = [];
const transportModel: ModelRuntime = {
provider: "hosted",
modelId: "hosted/provider-metadata-generate",
async doGenerate(options: unknown) {
callCount++;
if (callCount === 1) {
return {
content: [{
type: "tool-call",
toolCallId: "lookup-1",
toolName: "lookup",
input: '{"value":"alpha"}',
}],
finishReason: "tool-calls",
providerMetadata,
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
};
}

const prompt = (options as {
prompt?: Array<{ role?: string; providerMetadata?: unknown }>;
}).prompt ?? [];
replayedProviderMetadata = prompt.find((message) => message.role === "assistant")
?.providerMetadata;
return {
content: [{ type: "text", text: "generate replay complete" }],
finishReason: "stop",
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
};
},
async doStream() {
return { stream: createRuntimeStream([{ type: "finish", finishReason: "stop" }]) };
},
};
const lookup = tool({
id: "lookup",
description: "Look up a value",
inputSchema: defineSchema((v) => v.object({ value: v.string() }))(),
execute: ({ value }) => ({ value }),
});
const assistant = agent(
{
model: "hosted/provider-metadata-generate",
system: "Use the lookup tool.",
tools: { lookup },
maxSteps: 2,
resolveModelTransport: () => ({ model: transportModel }),
__vfToolLoadingMode: "eager",
} as Parameters<typeof agent>[0],
);

const result = await runWithRunEventSink(
(event) => {
modelCallContexts.push(event as AgentRunModelCallContextEvent);
},
() => assistant.generate({ input: "Look up alpha." }),
);

assertEquals(result.text, "generate replay complete");
assertEquals(callCount, 2);
assertEquals(replayedProviderMetadata, providerMetadata);
assertEquals(modelCallContexts.length, 2);
const replayContextAssistant = modelCallContexts[1]?.messages.find((message) =>
message.role === "assistant"
);
assertEquals(
replayContextAssistant && "providerMetadata" in replayContextAssistant,
false,
);
});

it("replays provider metadata without exposing it on the streamed response", async () => {
const providerMetadata = {
google: {
rawAssistantParts: [{
functionCall: { id: "lookup-1", name: "lookup", args: { value: "beta" } },
thoughtSignature: "signed-tool-turn-stream",
}],
},
};
let callCount = 0;
let replayedProviderMetadata: unknown;
const transportModel: ModelRuntime = {
provider: "hosted",
modelId: "hosted/provider-metadata-stream",
async doGenerate() {
return {
content: [{ type: "text", text: "unused" }],
finishReason: "stop",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
};
},
async doStream(options: unknown) {
callCount++;
if (callCount === 1) {
return {
stream: createRuntimeStream([
{ type: "tool-input-start", id: "lookup-1", toolName: "lookup" },
{
type: "tool-input-delta",
id: "lookup-1",
delta: '{"value":"beta"}',
},
{ type: "tool-input-end", id: "lookup-1" },
{
type: "tool-call",
toolCallId: "lookup-1",
toolName: "lookup",
input: { value: "beta" },
},
{ type: "finish", finishReason: "tool-calls", providerMetadata },
]),
};
}

const prompt = (options as {
prompt?: Array<{ role?: string; providerMetadata?: unknown }>;
}).prompt ?? [];
replayedProviderMetadata = prompt.find((message) => message.role === "assistant")
?.providerMetadata;
return {
stream: createRuntimeStream([
{ type: "text-delta", text: "stream replay complete" },
{ type: "finish", finishReason: "stop" },
]),
};
},
};
const lookup = tool({
id: "lookup",
description: "Look up a value",
inputSchema: defineSchema((v) => v.object({ value: v.string() }))(),
execute: ({ value }) => ({ value }),
});
const assistant = agent(
{
model: "hosted/provider-metadata-stream",
system: "Use the lookup tool.",
tools: { lookup },
maxSteps: 2,
resolveModelTransport: () => ({ model: transportModel }),
__vfToolLoadingMode: "eager",
} as Parameters<typeof agent>[0],
);

const response = (await assistant.stream({ input: "Look up beta." })).toDataStreamResponse();
const body = await response.text();

assertStringIncludes(body, "stream replay complete");
assertEquals(body.includes("signed-tool-turn-stream"), false);
assertEquals(callCount, 2);
assertEquals(replayedProviderMetadata, providerMetadata);
});

it("replays the exact signed Gemini tool call before its function response", async () => {
const encoder = new TextEncoder();
const rawAssistantParts = [{
functionCall: { id: "lookup-1", name: "lookup", args: { value: "gamma" } },
thoughtSignature: "gemini-3-signed-tool-turn",
}];
const requestBodies: Array<Record<string, unknown>> = [];
const googleRuntime = createGoogleModelRuntime({
apiKey: "test-google-key",
baseURL: "https://example.google.test/v1beta",
fetch: (_input, init) => {
requestBodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
const responseParts = requestBodies.length === 1
? [
encoder.encode(
`data: ${
JSON.stringify({
candidates: [{ content: { role: "model", parts: rawAssistantParts } }],
})
}\n\n`,
),
encoder.encode(
'data: {"candidates":[{"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}\n\n',
),
]
: [
encoder.encode(
'data: {"candidates":[{"content":{"role":"model","parts":[{"text":"wire replay complete"}]}}]}\n\n',
),
encoder.encode(
'data: {"candidates":[{"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":2,"candidatesTokenCount":1,"totalTokenCount":3}}\n\n',
),
];
return Promise.resolve(
new Response(
ReadableStream.from([...responseParts, encoder.encode("data: [DONE]\n\n")]),
{ status: 200, headers: { "content-type": "text/event-stream" } },
),
);
},
}, "gemini-3.5-flash");
const lookup = tool({
id: "lookup",
description: "Look up a value",
inputSchema: defineSchema((v) => v.object({ value: v.string() }))(),
execute: ({ value }) => ({ value }),
});
const assistant = agent(
{
model: "google-ai-studio/gemini-3.5-flash",
system: "Use the lookup tool.",
tools: { lookup },
maxSteps: 2,
resolveModelTransport: () => ({ model: googleRuntime }),
__vfToolLoadingMode: "eager",
} as Parameters<typeof agent>[0],
);

const response = (await assistant.stream({ input: "Look up gamma." })).toDataStreamResponse();
const body = await response.text();

assertStringIncludes(body, "wire replay complete");
assertEquals(body.includes("gemini-3-signed-tool-turn"), false);
assertEquals(requestBodies.length, 2);
const continuationContents = requestBodies[1]?.contents as unknown[] | undefined;
assertEquals(continuationContents?.slice(-2), [
{ role: "model", parts: rawAssistantParts },
{
role: "user",
parts: [{
functionResponse: {
id: "lookup-1",
name: "lookup",
response: { result: { value: "gamma" } },
},
}],
},
]);
});

it("records equivalent context through cloud and server-local runtime paths", async () => {
const contexts: AgentRunModelCallContextEvent[] = [];

Expand Down
2 changes: 2 additions & 0 deletions src/agent/runtime/runtime-tool-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface RuntimeGenerateTextResult {
toolResults?: RuntimeGenerateToolResult[];
usage?: RuntimeGenerateUsage;
finishReason?: string | null;
providerMetadata?: Record<string, unknown>;
}

export interface RuntimeRepairToolCall {
Expand Down Expand Up @@ -143,6 +144,7 @@ export type RuntimeStreamPart =
| {
type: "finish";
finishReason?: string | null;
providerMetadata?: Record<string, unknown>;
totalUsage?: {
inputTokens?: number;
outputTokens?: number;
Expand Down
5 changes: 5 additions & 0 deletions src/agent/runtime/streamed-assistant-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {

describe("agent/streamed-assistant-message", () => {
it("builds an assistant message from completed stream state", () => {
const providerMetadata = {
google: { rawAssistantParts: [{ thoughtSignature: "signed-turn" }] },
};
const state: ChatStreamState = {
accumulatedText: "Final answer",
reasoningParts: [
Expand All @@ -17,6 +20,7 @@ describe("agent/streamed-assistant-message", () => {
{ id: "reasoning_redacted", text: "", redactedData: "redacted_1" },
],
finishReason: "tool-calls",
providerMetadata,
toolCalls: new Map([
[
"call_1",
Expand Down Expand Up @@ -52,6 +56,7 @@ describe("agent/streamed-assistant-message", () => {
id: "msg_fixed",
role: "assistant",
timestamp: 123,
providerOptions: providerMetadata,
parts: [
{ type: "reasoning", text: "internal note", signature: "sig_1" },
{ type: "reasoning", redactedData: "redacted_1" },
Expand Down
6 changes: 5 additions & 1 deletion src/agent/runtime/streamed-assistant-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export function isPersistedReasoningPart(part: StreamingReasoningPart): boolean
}

export function buildStreamedAssistantMessage(
state: Pick<ChatStreamState, "accumulatedText" | "reasoningParts" | "toolCalls">,
state: Pick<
ChatStreamState,
"accumulatedText" | "reasoningParts" | "toolCalls" | "providerMetadata"
>,
identity: StreamedAssistantMessageIdentity,
options: { preserveRecoverablePlaceholderToolCalls?: boolean } = {},
): Message {
Expand Down Expand Up @@ -71,5 +74,6 @@ export function buildStreamedAssistantMessage(
role: "assistant",
parts,
timestamp: identity.timestamp,
...(state.providerMetadata ? { providerOptions: state.providerMetadata } : {}),
};
}
Loading
Loading