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
6 changes: 3 additions & 3 deletions docs/api-reference/veryfront/provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const model = resolveModel("veryfront-cloud/openai/gpt-5.4-nano");

### `registerModelProvider(name, factory)`

Register a custom model provider factory for the current project.
Register a custom model provider factory for the active project scope or application bootstrap.

**Returns:** `void`
**Returns:** `ModelProviderRegistrationDisposer`

### `resolveModel(modelString)`

Expand Down Expand Up @@ -85,7 +85,7 @@ Clear all registered model providers (for testing).
| `hasModelProvider` | Check if a model provider is registered (project-scoped or shared). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/model-registry.ts#L272) |
| `markCurrentVeryfrontCloudBillingGroupUsed` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/context.ts#L35) |
| `normalizeVeryfrontCloudModelId` | Normalizes Veryfront Cloud model ID. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts#L198) |
| `registerModelProvider` | Register a custom model provider factory for the current project. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/model-registry.ts#L61) |
| `registerModelProvider` | Register a custom model provider factory for the active project scope or application bootstrap. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/model-registry.ts#L61) |
| `resolveModel` | Resolve a "provider/model" string to a framework-compatible model runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/model-registry.ts#L228) |
| `resolveVeryfrontCloudGatewayModelId` | Resolves Veryfront Cloud gateway model ID. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts#L272) |
| `resolveVeryfrontCloudModelId` | Resolves Veryfront Cloud model ID. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/provider/veryfront-cloud/model-catalog.ts#L246) |
Expand Down
9 changes: 8 additions & 1 deletion docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ For providers not covered by env vars, use `registerModelProvider()`:
```ts
import { registerModelProvider } from "veryfront/provider";

registerModelProvider("ollama", (id) => {
const unregisterOllama = registerModelProvider("ollama", (id) => {
// Return a framework-compatible model runtime for this model ID.
// Prefer built-in providers when possible; custom registration is an
// advanced interop surface for non-standard backends. The runtime must
Expand All @@ -207,6 +207,13 @@ The factory receives the model ID and must return a framework-compatible model
runtime with the generation surface the framework expects, including
`doGenerate()` and `doStream()`.

Registration inside a project source context is isolated to that project.
Registration during application bootstrap, outside a project context, becomes
the default for every project unless a project registers an override. The
returned disposer removes only the registration created by that call.
Call `unregisterOllama()` during application teardown when the registration is
no longer needed.

## Direct model resolution

For cases outside the agent system:
Expand Down
2 changes: 0 additions & 2 deletions scripts/lint/test-typecheck-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
"src/agent/hosted/response-stream.test.ts",
"src/agent/memory/memory.test.ts",
"src/agent/middleware/cost-tracking/tracker.test.ts",
"src/agent/runtime/load-tools-tool.test.ts",
"src/agent/runtime/search-tools-tool.test.ts",
"src/build/bundler/code-splitter/manifest-builder.test.ts",
"src/build/production-build/build/build-cleanup.test.ts",
"src/build/production-build/manifest.test.ts",
Expand Down
3 changes: 2 additions & 1 deletion src/agent/hosted/agent-project-steering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ Deno.test("createHostedAgentProjectSteering registers the built-in schema valida
assertEquals(tryResolve<SchemaValidator>("SchemaValidator"), undefined);

const baseDir = writeAgentDefinition({ rootDir, agentId: "writer" });
createHostedAgentProjectSteering({
const steering = createHostedAgentProjectSteeringPublic({
baseDir,
agentId: "writer",
getApiUrl: () => "https://api.example.com",
});

assertEquals(typeof tryResolve<SchemaValidator>("SchemaValidator")?.object, "function");
assertEquals(steering.getAgentConfig().id, "writer");
});
});

Expand Down
52 changes: 52 additions & 0 deletions src/agent/hosted/project-remote-tool-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,58 @@ Deno.test("createHostedProjectRemoteToolSource fails closed on claimed but uncon
assertEquals(switchCount, 0);
});

Deno.test("createHostedProjectRemoteToolSource rejects unconfirmed claimed navigation success", async () => {
let switchCount = 0;
const source = createHostedProjectRemoteToolSource({
source: createRemoteSource({
tools: [navigationTool("studio_open_project")],
execute: () => ({
structuredContent: {
success: true,
project_id: "different-project",
slug: "different-project",
},
}),
}),
projectScopedRemoteToolOptions: {
projectNavigationToolNames: ["studio_open_project"],
},
onProjectSwitch: () => {
switchCount += 1;
},
});

const result = await source.executeTool("studio_open_project", {
project_reference: "requested-project",
});

assertEquals(result, createUnconfirmedProjectContextSwitchResult());
assertEquals(switchCount, 0);
});

Deno.test("createHostedProjectRemoteToolSource preserves upstream navigation failure", async () => {
const failure = { structuredContent: { success: false, message: "not found" } };
const source = createHostedProjectRemoteToolSource({
source: createRemoteSource({
tools: [navigationTool("studio_open_project")],
execute: () => failure,
}),
projectScopedRemoteToolOptions: {
projectNavigationToolNames: ["studio_open_project"],
},
onProjectSwitch: () => {
throw new Error("unexpected project switch");
},
});

assertEquals(
await source.executeTool("studio_open_project", {
project_reference: "missing-project",
}),
failure,
);
});

Deno.test("createHostedProjectRemoteToolSource skips mutation callbacks for failed results", async () => {
let mutationCount = 0;
const source = createHostedProjectRemoteToolSource({
Expand Down
3 changes: 1 addition & 2 deletions src/agent/hosted/project-steering-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ Deno.test("hosted project steering uses the bounded transport by default", async
);
await Promise.resolve();

assert(error instanceof RangeError);
assertStringIncludes(error.message, "Project file response may contain at most");
assertStringIncludes(String(error), "Project files list response exceeds");
assertEquals(cancelled, true);
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/agent/hosted/project-steering-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "../runtime/load-skill-tool.ts";
import type { MutableAgentProjectContext } from "../project/context.ts";
import {
createRuntimeProjectFilesClient,
createStrictRuntimeProjectFilesClient,
type RuntimeProjectFilesClient,
type RuntimeProjectFilesClientOptions,
type RuntimeProjectFilesFetch,
Expand Down Expand Up @@ -122,7 +122,7 @@ function createProjectFilesClientOptions(
function createDefaultProjectFilesClient(
options: HostedProjectSteeringAdapterOptions,
): RuntimeProjectFilesClient {
return createRuntimeProjectFilesClient(createProjectFilesClientOptions(options));
return createStrictRuntimeProjectFilesClient(createProjectFilesClientOptions(options));
}

function createDefaultProjectSkillLoader(
Expand Down
65 changes: 48 additions & 17 deletions src/agent/runtime/agent-runtime-step.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe("agent/runtime-step", () => {
toolDefinition("get_release"),
toolDefinition("load_skill"),
],
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "generate",
remoteToolSources: undefined,
Expand Down Expand Up @@ -74,7 +74,7 @@ describe("agent/runtime-step", () => {
config: { model: "anthropic/claude-opus-4-6", system: "Base", tools: true } as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
getAvailableTools: async () => [toolDefinition("create_release")],
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "generate",
providerToolNames: ["web_search"],
Expand Down Expand Up @@ -104,7 +104,7 @@ describe("agent/runtime-step", () => {
config: { model: "auto", system: "Base", __vfToolLoadingMode: "eager" } as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
getAvailableTools: async () => [],
isLocalModel: true,
supportsToolCalling: false,
messages: [],
mode: "generate",
remoteToolSources: undefined,
Expand Down Expand Up @@ -138,7 +138,7 @@ describe("agent/runtime-step", () => {
assertEquals(options?.remoteToolContext?.allowedSkillIds, ["selected"]);
return [];
},
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "generate",
remoteToolSources: undefined,
Expand Down Expand Up @@ -179,7 +179,7 @@ describe("agent/runtime-step", () => {
allowedRemoteToolNames: ["remote_allowed"],
config,
forwardedRemoteToolDefinitions: [toolDefinition("forwarded_remote")],
isLocalModel: false,
supportsToolCalling: true,
messages,
mode: "generate",
remoteToolSources: [remoteSource],
Expand Down Expand Up @@ -249,7 +249,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -287,7 +287,7 @@ describe("agent/runtime-step", () => {
assertEquals(options?.includeSkillTools, false);
return [toolDefinition("ordinary_tool")];
},
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -320,7 +320,7 @@ describe("agent/runtime-step", () => {
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
getAvailableTools: async () => [],
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: undefined,
Expand All @@ -342,15 +342,15 @@ describe("agent/runtime-step", () => {
assertEquals(prepared.toolContext.__vfSourceIntegrationPolicy, sourceIntegrationPolicy);
});

it("uses the canonical deferred exposure contract for local models", async () => {
it("does not load tools for runtimes that declare tool calling unsupported", async () => {
const prepared = await prepareAgentRuntimeStep({
agentId: "agent_1",
activeSkillPolicy: undefined,
activeSkillToolAvailability: undefined,
allowedRemoteToolNames: undefined,
config: { model: "local/test", system: "Local", tools: true } as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: true,
supportsToolCalling: false,
messages: [],
mode: "stream",
remoteToolSources: [],
Expand All @@ -359,7 +359,7 @@ describe("agent/runtime-step", () => {
systemPrompt: "Local",
toolContextBase: undefined,
getAvailableTools: async () => {
throw new Error("local model should not load tools");
throw new Error("tool-incompatible runtime should not load tools");
},
resolveRuntimeState: async () => ({ systemPrompt: "Local", context: undefined }),
});
Expand All @@ -368,6 +368,37 @@ describe("agent/runtime-step", () => {
assertEquals(prepared.toolContext, {});
});

it("loads tools for server-local runtimes that declare tool calling support", async () => {
let loaded = false;
const prepared = await prepareAgentRuntimeStep({
agentId: "agent_1",
activeSkillPolicy: undefined,
activeSkillToolAvailability: undefined,
allowedRemoteToolNames: undefined,
config: { model: "local/test", system: "Local", tools: true } as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: [],
runtimeContext: undefined,
step: 0,
systemPrompt: "Local",
toolContextBase: undefined,
getAvailableTools: async () => {
loaded = true;
return [toolDefinition("local_lookup")];
},
resolveRuntimeState: async () => ({ systemPrompt: "Local", context: undefined }),
});

assertEquals(loaded, true);
assertEquals(
prepared.toolExposurePlan.authorized.map((tool) => tool.name),
["local_lookup"],
);
});

it("hides intake tools but keeps delegation tools after submitted form input", async () => {
const messages: Message[] = [{
id: "tool_result_1",
Expand All @@ -394,7 +425,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages,
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -448,7 +479,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages,
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -489,7 +520,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -551,7 +582,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages,
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -593,7 +624,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: [],
Expand Down Expand Up @@ -634,7 +665,7 @@ describe("agent/runtime-step", () => {
__vfToolLoadingMode: "eager",
} as AgentConfig,
forwardedRemoteToolDefinitions: undefined,
isLocalModel: false,
supportsToolCalling: true,
messages: [],
mode: "stream",
remoteToolSources: [],
Expand Down
24 changes: 13 additions & 11 deletions src/agent/runtime/agent-runtime-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface PrepareAgentRuntimeStepInput {
excludedToolNames?: ReadonlySet<string>;
forwardedRemoteToolDefinitions: ToolDefinition[] | undefined;
getAvailableTools: RuntimeStepToolLoader;
isLocalModel: boolean;
supportsToolCalling: boolean;
messages: Message[];
mode: AgentRuntimeStepMode;
providerToolNames?: readonly string[];
Expand Down Expand Up @@ -131,16 +131,18 @@ export async function prepareAgentRuntimeStep(
toolContext.activeSkillToolAvailability = input.activeSkillToolAvailability;
}

let tools = input.isLocalModel ? [] : await input.getAvailableTools(input.config.tools, {
callerAgentId: input.agentId,
includeSkillTools: shouldIncludeSkillTools(input.config),
allowedRemoteToolNames: input.allowedRemoteToolNames,
forwardedRemoteToolDefinitions: input.forwardedRemoteToolDefinitions,
remoteToolSources: input.remoteToolSources,
remoteToolContext: toolContext,
sourceIntegrationPolicy: input.sourceIntegrationPolicy,
strictConfiguredToolsOnly: input.strictConfiguredToolsOnly,
});
let tools = input.supportsToolCalling
? await input.getAvailableTools(input.config.tools, {
callerAgentId: input.agentId,
includeSkillTools: shouldIncludeSkillTools(input.config),
allowedRemoteToolNames: input.allowedRemoteToolNames,
forwardedRemoteToolDefinitions: input.forwardedRemoteToolDefinitions,
remoteToolSources: input.remoteToolSources,
remoteToolContext: toolContext,
sourceIntegrationPolicy: input.sourceIntegrationPolicy,
strictConfiguredToolsOnly: input.strictConfiguredToolsOnly,
})
: [];

if (input.activeSkillPolicy || input.activeSkillToolAvailability) {
tools = filterToolsForSkill(
Expand Down
Loading