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
5 changes: 5 additions & 0 deletions docs/guides/agent-service-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ When exactly one code or markdown agent is discovered, that agent becomes the
default for direct `/api/runs` requests. Pass `agentId` when the service exposes
multiple agents and direct requests need a predictable default.

Hosted tool execution preserves the selected agent ID and durable run ID. Remote
tool calls carry these identities so they match the run authorization claims.
If you customize `createTaskContext`, return the resolved `agentId` in that context.
Hosted execution uses that identity, including any normalization your hook applies.

## Keep agent behavior in project files

Define the agent in `agents/` and keep service startup separate from agent
Expand Down
8 changes: 8 additions & 0 deletions src/agent/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ describe("agent factory", () => {
toolRegistryInternal.clearAll();
});

it("rejects empty explicit identities and preserves valid or generated identities", () => {
for (const id of ["", " ", "\t\n"]) {
assertThrows(() => agent({ id, system: "Synthetic" }), Error, "Agent id cannot be empty");
}
assertEquals(agent({ id: "bound-agent", system: "Synthetic" }).id, "bound-agent");
assertEquals(typeof agent({ system: "Synthetic" }).id, "string");
});

it("bootstraps schema validation before registering universal skill tools", () => {
resetExtensionContracts();
registerSkill("support-triage", createSkill("support-triage", "Triage support requests"));
Expand Down
6 changes: 5 additions & 1 deletion src/agent/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
import type { RuntimeSkillDefinition } from "./runtime/skill-metadata.ts";

const IntrinsicReflectApply = Reflect.apply;
const IntrinsicStringTrim = String.prototype.trim;
const IntrinsicArrayFilter = Array.prototype.filter;
const IntrinsicObjectEntries = Object.entries;
const IntrinsicObjectKeys = Object.keys;
Expand Down Expand Up @@ -533,7 +534,10 @@ function createAgent<TOutput = never>(
config: AgentConfig<TOutput>,
options: { register: boolean; runtimeOptions?: AgentRuntimeInternalOptions },
): Agent<TOutput> {
if (typeof config.id === "string" && config.id.trim().length === 0) {
if (
typeof config.id === "string" &&
IntrinsicReflectApply(IntrinsicStringTrim, config.id, []).length === 0
) {
throw toError(
createError({
type: "agent",
Expand Down
225 changes: 137 additions & 88 deletions src/agent/hosted/default-chat-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,103 +527,152 @@ Deno.test("createDefaultHostedChatRuntime builds a cloud-backed hosted runtime",
assertEquals(capturedContext.availableToolNames, ["sleep"]);
});

Deno.test("createDefaultHostedChatRuntime forwards project identity to tool execution", async () => {
await runWithProjectRequestContext(
it("keeps the hosted runtime usable when an optional agent identity is blank", async () => {
for (const agentId of ["", " "]) {
const runtime = await createDefaultHostedChatRuntime({
sourceIntegrationPolicy: denyAllSourceIntegrationPolicy,
options: {
projectId: "project-1",
authToken: "fixture-token",
instructions: "Respond briefly.",
model: "openai/gpt-5.4",
agentId,
},
config: { apiUrl: "https://api.example.com", apiMcpUrl: "https://api.example.com/mcp" },
buildLocalTools: () => ({}),
createRemoteToolSource: emptyRemoteSource,
preloadLatestConversationUserText: false,
});
assertExists(runtime.agent);
await runtime.cleanup();
}
});

for (
const identityCase of [
{ name: "default", expected: "veryfront", resolved: undefined },
{ name: "normalized task context", expected: "canonical-agent", resolved: "canonical-agent" },
{
projectId: "project-1",
projectSlug: "project-slug-1",
token: "token-1",
name: "removed task context identity",
expected: "veryfront-hosted-runtime",
resolved: undefined,
},
async () => {
clearModelProviders();
let modelCallCount = 0;
let capturedExecutionContext: ToolExecutionContext | undefined;
{ name: "blank task context identity", expected: "veryfront-hosted-runtime", resolved: " " },
]
) {
Deno.test(`createDefaultHostedChatRuntime forwards bound identity from ${identityCase.name}`, async () => {
await runWithProjectRequestContext(
{
projectId: "project-1",
projectSlug: "project-slug-1",
token: "token-1",
},
async () => {
clearModelProviders();
let modelCallCount = 0;
let capturedExecutionContext: ToolExecutionContext | undefined;

registerModelProvider("test", () => ({
provider: "test",
modelId: "test/hosted-context",
doGenerate: () => Promise.reject(new Error("unused")),
doStream() {
modelCallCount += 1;
return Promise.resolve({
stream: new ReadableStream<unknown>({
start(controller) {
if (modelCallCount === 1) {
controller.enqueue({
type: "tool-call",
toolCallId: "inspect-context-1",
toolName: "inspect_context",
input: {},
});
controller.enqueue({
type: "finish",
finishReason: "tool-calls",
usage: { inputTokens: 1, outputTokens: 1 },
});
} else {
controller.enqueue({ type: "text-delta", text: "done" });
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: { inputTokens: 1, outputTokens: 1 },
});
}
controller.close();
registerModelProvider("test", () => ({
provider: "test",
modelId: "test/hosted-context",
doGenerate: () => Promise.reject(new Error("unused")),
doStream() {
modelCallCount += 1;
return Promise.resolve({
stream: new ReadableStream<unknown>({
start(controller) {
if (modelCallCount === 1) {
controller.enqueue({
type: "tool-call",
toolCallId: "inspect-context-1",
toolName: "inspect_context",
input: {},
});
controller.enqueue({
type: "finish",
finishReason: "tool-calls",
usage: { inputTokens: 1, outputTokens: 1 },
});
} else {
controller.enqueue({ type: "text-delta", text: "done" });
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: { inputTokens: 1, outputTokens: 1 },
});
}
controller.close();
},
}),
});
},
}));

try {
const runtime = await createDefaultHostedChatRuntime({
sourceIntegrationPolicy: denyAllSourceIntegrationPolicy,
options: {
projectId: "project-1",
projectSlug: "project-slug-1",
authToken: "token-1",
instructions: "Inspect the runtime context.",
model: "test/hosted-context",
runId: "run-bound-default-chat",
agentId: "veryfront",
allowedTools: ["inspect_context"],
},
config: {
apiUrl: "https://api.example.com",
apiMcpUrl: "https://api.example.com/mcp",
},
...(identityCase.name === "default" ? {} : {
createTaskContext: ({ options, modelId }) => ({
authToken: options.authToken,
runId: options.runId,
agentId: identityCase.resolved,
projectId: options.projectId ?? "",
projectSlug: options.projectSlug,
branchId: options.branchId ?? null,
model: modelId,
}),
}),
buildLocalTools: () => ({
inspect_context: {
...localTool("Inspect the runtime context"),
execute: (_input: unknown, context?: ToolExecutionContext) => {
capturedExecutionContext = context;
return { ok: true };
},
},
}),
createRemoteToolSource: emptyRemoteSource,
preloadLatestConversationUserText: false,
});
},
}));

try {
const runtime = await createDefaultHostedChatRuntime({
sourceIntegrationPolicy: denyAllSourceIntegrationPolicy,
options: {
projectId: "project-1",
projectSlug: "project-slug-1",
authToken: "token-1",
instructions: "Inspect the runtime context.",
model: "test/hosted-context",
allowedTools: ["inspect_context"],
},
config: {
apiUrl: "https://api.example.com",
apiMcpUrl: "https://api.example.com/mcp",
},
buildLocalTools: () => ({
inspect_context: {
...localTool("Inspect the runtime context"),
execute: (_input: unknown, context?: ToolExecutionContext) => {
capturedExecutionContext = context;
return { ok: true };
},
await withMockFetch(
() => Promise.resolve(Response.json({ tools: [] })),
async () => {
const result = await runtime.agent.stream({
messages: [],
abortSignal: new AbortController().signal,
});
for await (const _chunk of result.toUIMessageStream()) {
// Consume the complete tool-call round trip.
}
},
}),
createRemoteToolSource: emptyRemoteSource,
preloadLatestConversationUserText: false,
});
);

await withMockFetch(
() => Promise.resolve(Response.json({ tools: [] })),
async () => {
const result = await runtime.agent.stream({
messages: [],
abortSignal: new AbortController().signal,
});
for await (const _chunk of result.toUIMessageStream()) {
// Consume the complete tool-call round trip.
}
},
);

assertEquals(capturedExecutionContext?.projectId, "project-1");
assertEquals(capturedExecutionContext?.projectSlug, "project-slug-1");
} finally {
clearModelProviders();
}
},
);
});
assertEquals(capturedExecutionContext?.projectId, "project-1");
assertEquals(capturedExecutionContext?.projectSlug, "project-slug-1");
assertEquals(capturedExecutionContext?.runId, "run-bound-default-chat");
assertEquals(capturedExecutionContext?.agentId, identityCase.expected);
} finally {
clearModelProviders();
}
},
);
});
}

Deno.test("createDefaultHostedChatRuntime keeps hosted credentials out of project tools", async () => {
clearModelProviders();
Expand Down
8 changes: 7 additions & 1 deletion src/agent/hosted/default-chat-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { snapshotBoundedJsonValue } from "#veryfront/schemas/json-value.ts";
import { defineOwnDataProperty } from "#veryfront/security/own-data-property.ts";

const apply = Reflect.apply;
const stringTrim = String.prototype.trim;
const TypeErrorConstructor = TypeError;
const objectEntries = Object.entries;
const objectSetPrototypeOf = Object.setPrototypeOf;
Expand Down Expand Up @@ -322,6 +323,10 @@ export type PreparedHostedRuntimeAgentOptions = {
refreshSystem?: () => Promise<AgentSystem> | AgentSystem;
};

function resolveRuntimeAgentId(agentId: string | undefined): string {
return agentId && apply(stringTrim, agentId, []) ? agentId : "veryfront-hosted-runtime";
}

function createRuntimeAgentConfig(input: PreparedHostedRuntimeAgentOptions): AgentConfig {
const liveProjectSteering = input.options.liveProjectSteering;
const refreshSystem = input.refreshSystem;
Expand All @@ -335,7 +340,7 @@ function createRuntimeAgentConfig(input: PreparedHostedRuntimeAgentOptions): Age
refreshSystem,
});
const runtimeConfig: RuntimeToolFilterConfig = {
id: input.runtimeAgentId ?? "veryfront-hosted-runtime",
id: input.runtimeAgentId ?? resolveRuntimeAgentId(input.options.agentId),
model: input.modelId,
system: input.toolAssembly.systemMessages ?? input.toolAssembly.systemInstructions,
tools: runtimeTools,
Expand Down Expand Up @@ -549,6 +554,7 @@ export async function createDefaultHostedChatRuntime(
cloudContext,
() =>
createPreparedHostedRuntimeAgent({
runtimeAgentId: resolveRuntimeAgentId(taskContext.agentId),
options: input.options,
taskContext,
toolAssembly,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,17 @@ describe("private executor facades", () => {
Object.defineProperty(Object.prototype, "delegates", {
configurable: true,
get() {
if (this.id === "veryfront-hosted-runtime" && this.tools?.visible?.execute) {
if (this.id === coder.id && this.tools?.visible?.execute) {
exposedFacadeValues++;
}
return undefined;
},
});
// Positive control: the probe must detect a reflected configuration
// carrying the selected agent identity before testing the private path.
Reflect.get({ id: coder.id, tools: { visible } }, "delegates");
assertEquals(exposedFacadeValues, 1);
exposedFacadeValues = 0;
Object.defineProperty(Object.prototype, "projectSteering", {
configurable: true,
get() {
Expand Down
Loading
Loading