Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veryfront",
"version": "0.1.1118",
"version": "0.1.1119",
"license": "Apache-2.0",
"nodeModulesDir": "auto",
"minimumDependencyAge": {
Expand Down
58 changes: 58 additions & 0 deletions src/agent/ag-ui/runtime-chat-stream-encoder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,64 @@ describe("agent/ag-ui-runtime-chat-stream-encoder", () => {
);
});

it("preserves providerExecuted on runtime tool lifecycle events", () => {
const encoder = createAgUiRuntimeChatStreamEncoder({
responseMessageId: "msg-1",
});

assertEquals(
encoder.encode({
type: "tool-input-start",
toolCallId: "tool-provider-fetch",
toolName: "web_fetch",
providerExecuted: true,
}),
[
{ type: "start-step" },
{
type: "tool-input-start",
toolCallId: "tool-provider-fetch",
toolName: "web_fetch",
providerExecuted: true,
},
],
);
assertEquals(
encoder.encode({
type: "tool-input-available",
toolCallId: "tool-provider-fetch",
toolName: "web_fetch",
input: { url: "https://example.com/docs" },
providerExecuted: true,
}),
[
{
type: "tool-input-available",
toolCallId: "tool-provider-fetch",
toolName: "web_fetch",
input: { url: "https://example.com/docs" },
providerExecuted: true,
},
],
);
assertEquals(
encoder.encode({
type: "tool-output-error",
toolCallId: "tool-provider-fetch",
errorText: "provider failed",
providerExecuted: true,
}),
[
{
type: "tool-output-error",
toolCallId: "tool-provider-fetch",
errorText: "provider failed",
providerExecuted: true,
},
],
);
});

it("emits text events with the response message id and block content id", () => {
const encoder = createAgUiRuntimeChatStreamEncoder({
responseMessageId: "msg-1",
Expand Down
46 changes: 42 additions & 4 deletions src/agent/ag-ui/runtime-chat-stream-encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type ToolPart = {
toolName: string;
inputText: string;
input: Record<string, unknown>;
providerExecuted?: boolean;
};

type PendingToolDelta = {
Expand Down Expand Up @@ -98,6 +99,11 @@ function getStringField(event: AgUiRuntimeStreamEvent, key: string): string | un
return typeof value === "string" ? value : undefined;
}

function getBooleanField(event: AgUiRuntimeStreamEvent, key: string): boolean | undefined {
const value = event[key];
return typeof value === "boolean" ? value : undefined;
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Expand Down Expand Up @@ -380,17 +386,26 @@ export function createAgUiRuntimeChatStreamEncoder(
if (!toolCallId || !toolName) {
return events;
}
const providerExecuted = getBooleanField(event, "providerExecuted");
const toolPart = toolParts.get(toolCallId);
if (!toolPart) {
toolParts.set(toolCallId, {
toolName,
inputText: "",
input: {},
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
} else if (providerExecuted !== undefined) {
toolPart.providerExecuted = providerExecuted;
}
if (!emittedToolInputStartIds.has(toolCallId)) {
emittedToolInputStartIds.add(toolCallId);
events.push({ type: "tool-input-start", toolCallId, toolName });
events.push({
type: "tool-input-start",
toolCallId,
toolName,
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
}
const pendingEvents = flushPendingToolDeltas(toolCallId);
const existingToolPart = toolParts.get(toolCallId);
Expand Down Expand Up @@ -453,29 +468,40 @@ export function createAgUiRuntimeChatStreamEncoder(
? parsedPendingInput
: inputRecord
: inputRecord;
const providerExecuted = getBooleanField(event, "providerExecuted");

if (existingToolPart) {
existingToolPart.toolName = toolName;
existingToolPart.inputText = pendingInputText;
existingToolPart.input = resolvedInputRecord;
if (providerExecuted !== undefined) {
existingToolPart.providerExecuted = providerExecuted;
}
} else {
toolParts.set(toolCallId, {
toolName,
inputText: pendingInputText,
input: resolvedInputRecord,
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
}

if (!emittedToolInputStartIds.has(toolCallId)) {
emittedToolInputStartIds.add(toolCallId);
events.push({ type: "tool-input-start", toolCallId, toolName });
events.push({
type: "tool-input-start",
toolCallId,
toolName,
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
}
events.push(...flushPendingToolDeltas(toolCallId));
events.push({
type: "tool-input-available",
toolCallId,
toolName,
input: resolvedInputRecord,
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
return events;
}
Expand All @@ -485,7 +511,13 @@ export function createAgUiRuntimeChatStreamEncoder(
if (!toolCallId) {
return events;
}
events.push({ type: "tool-output-available", toolCallId, output: event.output });
const providerExecuted = getBooleanField(event, "providerExecuted");
events.push({
type: "tool-output-available",
toolCallId,
output: event.output,
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
return events;
}
case "tool-output-error": {
Expand All @@ -495,7 +527,13 @@ export function createAgUiRuntimeChatStreamEncoder(
return events;
}
const errorText = getStringField(event, "errorText") ?? "Tool execution failed";
events.push({ type: "tool-output-error", toolCallId, errorText });
const providerExecuted = getBooleanField(event, "providerExecuted");
events.push({
type: "tool-output-error",
toolCallId,
errorText,
...(providerExecuted !== undefined ? { providerExecuted } : {}),
});
return events;
}
case "data": {
Expand Down
3 changes: 2 additions & 1 deletion src/agent/hosted/chat-execution-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ describe("agent/hosted-chat-execution-runtime", () => {
assertEquals(terminalStates, [{ status: "completed" }]);
});

it("completes response finalization with provider-native web tool input still open", async () => {
it("completes response finalization with provider-owned web tool input still open", async () => {
let streamOptions: HostedChatRuntimeToUiMessageStreamOptions | undefined;
const terminalStates: HostedLifecycleTerminalState[] = [];
const runtime = createHostedChatExecutionRuntime({
Expand Down Expand Up @@ -927,6 +927,7 @@ describe("agent/hosted-chat-execution-runtime", () => {
toolCallId: "srvtoolu-fetch",
input: { url: "https://veryfront.com/docs/agent/create-agent" },
state: "input-available",
providerExecuted: true,
},
],
}),
Expand Down
121 changes: 121 additions & 0 deletions src/agent/hosted/chat-runtime-tool-assembly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,127 @@ Deno.test("prepareHostedChatRuntimeToolAssembly keeps source provider tools insi
assertStringIncludes(toolAssembly.systemInstructions, "- sleep");
});

Deno.test("prepareHostedChatRuntimeToolAssembly falls back to local web_fetch when OpenAI lacks provider-native web_fetch", async () => {
const taskContext: HostedChatRuntimeToolAssemblyContext = {
authToken: "token",
projectId: "project-1",
model: "openai/gpt-5.4-nano",
};

const toolAssembly = await prepareHostedChatRuntimeToolAssembly({
sourceIntegrationPolicy: unrestrictedSourceIntegrationPolicy,
taskContext,
instructions: "Base instructions",
localTools: {
sleep: localTool("Sleep"),
web_fetch: localTool("Fetch a URL"),
write_file: localTool("Write a file"),
},
apiUrl: "https://api.example.com",
apiMcpUrl: "https://api.example.com/mcp",
allowedToolNames: ["sleep", "write_file"],
allowedProviderToolNames: ["web_fetch"],
createRemoteToolSource: remoteSourceFromConfig,
preloadLatestConversationUserText: false,
});

assertEquals(toolAssembly.localToolNames, ["sleep", "web_fetch", "write_file"]);
assertEquals(toolAssembly.providerToolNames, []);
assertEquals(taskContext.availableToolNames, ["sleep", "web_fetch", "write_file"]);
assertExists(toolAssembly.runtimeTools.web_fetch);
assertStringIncludes(toolAssembly.systemInstructions, "- web_fetch");
});

Deno.test("prepareHostedChatRuntimeToolAssembly denies OpenAI local web_fetch fallback when direct allowed tools exclude it", async () => {
const taskContext: HostedChatRuntimeToolAssemblyContext = {
authToken: "token",
projectId: "project-1",
model: "openai/gpt-5.4-nano",
};

const toolAssembly = await prepareHostedChatRuntimeToolAssembly({
sourceIntegrationPolicy: unrestrictedSourceIntegrationPolicy,
taskContext,
instructions: "Base instructions",
localTools: {
sleep: localTool("Sleep"),
web_fetch: localTool("Fetch a URL"),
},
apiUrl: "https://api.example.com",
apiMcpUrl: "https://api.example.com/mcp",
allowedToolNames: ["sleep"],
sourceProviderToolNames: ["web_fetch"],
createRemoteToolSource: remoteSourceFromConfig,
preloadLatestConversationUserText: false,
});

assertEquals(toolAssembly.localToolNames, ["sleep"]);
assertEquals(toolAssembly.providerToolNames, []);
assertEquals(taskContext.availableToolNames, ["sleep"]);
assertEquals(toolAssembly.runtimeTools.web_fetch, undefined);
});

Deno.test("prepareHostedChatRuntimeToolAssembly keeps empty provider allowlist as local web_fetch fallback denial", async () => {
const taskContext: HostedChatRuntimeToolAssemblyContext = {
authToken: "token",
projectId: "project-1",
model: "openai/gpt-5.4-nano",
};

const toolAssembly = await prepareHostedChatRuntimeToolAssembly({
sourceIntegrationPolicy: unrestrictedSourceIntegrationPolicy,
taskContext,
instructions: "Base instructions",
localTools: {
sleep: localTool("Sleep"),
web_fetch: localTool("Fetch a URL"),
},
apiUrl: "https://api.example.com",
apiMcpUrl: "https://api.example.com/mcp",
allowedToolNames: ["sleep"],
allowedProviderToolNames: [],
sourceProviderToolNames: ["web_fetch"],
createRemoteToolSource: remoteSourceFromConfig,
preloadLatestConversationUserText: false,
});

assertEquals(toolAssembly.localToolNames, ["sleep"]);
assertEquals(toolAssembly.providerToolNames, []);
assertEquals(taskContext.availableToolNames, ["sleep"]);
assertEquals(toolAssembly.runtimeTools.web_fetch, undefined);
});

Deno.test("prepareHostedChatRuntimeToolAssembly does not duplicate Anthropic provider-native web_fetch with the local fallback", async () => {
const taskContext: HostedChatRuntimeToolAssemblyContext = {
authToken: "token",
projectId: "project-1",
model: "anthropic/claude-sonnet-4-6",
};

const toolAssembly = await prepareHostedChatRuntimeToolAssembly({
sourceIntegrationPolicy: unrestrictedSourceIntegrationPolicy,
taskContext,
instructions: "Base instructions",
localTools: {
sleep: localTool("Sleep"),
web_fetch: localTool("Fetch a URL"),
},
apiUrl: "https://api.example.com",
apiMcpUrl: "https://api.example.com/mcp",
allowedToolNames: ["sleep"],
allowedProviderToolNames: ["web_fetch"],
sourceProviderToolNames: ["web_fetch"],
createRemoteToolSource: remoteSourceFromConfig,
preloadLatestConversationUserText: false,
});

assertEquals(toolAssembly.localToolNames, ["sleep"]);
assertEquals(toolAssembly.providerToolNames, ["web_fetch"]);
assertEquals(taskContext.availableToolNames, ["sleep", "web_fetch"]);
assertEquals(toolAssembly.runtimeTools.web_fetch, undefined);
assertStringIncludes(toolAssembly.systemInstructions, "- web_fetch");
});

Deno.test("prepareHostedChatRuntimeToolAssembly preloads default research artifacts", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = () =>
Expand Down
Loading
Loading