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
26 changes: 13 additions & 13 deletions docs/api-reference/veryfront/agent.md

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions docs/guides/multi-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,14 @@ Break the task down. Use agent_researcher to gather facts, then agent_writer to
produce the final copy.
```

Set `delegates: []` when an agent must not delegate. Hosted runtimes retain the
legacy generic `invoke_agent` tool only for older definitions where
`delegates` is absent; direct runtimes do not add it automatically.
Self-delegation and delegate ids that cannot form a valid provider tool name
are rejected with explicit diagnostics. Declare direct tools by name when
using `delegates`; `tools: true` is intentionally rejected because it would
hide the agent's capability boundary.
Set `delegates: []` when an agent must not delegate. `invoke_agent` is the
generic platform tool for dynamic agent selection. Enable it explicitly in a
direct runtime with `tools: { invoke_agent: true }`; hosted runtimes expose it
when generic delegation is allowed and `delegates` is absent. Self-delegation
and delegate ids that cannot form a valid provider tool name are rejected with
explicit diagnostics. Declare direct tools by name when using `delegates`;
`tools: true` is intentionally rejected because it would hide the agent's
capability boundary.

Hosted nested delegation carries trusted invocation lineage from parent to
child runs. The root conversation and run stay stable, the immediate parent is
Expand Down
3 changes: 3 additions & 0 deletions src/agent/composition/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ async function runAgentAsStreamingTool(
agent: Agent,
input: string,
sourceIntegrationPolicy: SourceIntegrationPolicyManifest | undefined,
abortSignal?: AbortSignal,
): Promise<AgentResponse> {
const execute = async (): Promise<AgentResponse> => {
let finalResponse: AgentResponse | undefined;
const stream = await agent.stream({
input,
abortSignal,
onFinish: (response) => {
finalResponse = response;
},
Expand Down Expand Up @@ -74,6 +76,7 @@ export function agentAsTool(agent: Agent, description: string): Tool {
agent,
input,
getRuntimeSourceIntegrationPolicyFromContext(context),
context?.abortSignal,
);

setActiveSpanAttributes({
Expand Down
70 changes: 59 additions & 11 deletions src/agent/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { tool, toolRegistry } from "#veryfront/tool";
import { defineSchema } from "#veryfront/schemas/index.ts";
import { VeryfrontError } from "#veryfront/errors";
import { getEffectiveAgentSystem } from "./runtime/effective-agent-system.ts";
import { getAvailableTools } from "./runtime/tool-helpers.ts";
import { agentRegistry } from "./composition/index.ts";
import { agent } from "./factory.ts";
import type { AgentConfig, AgentResponse } from "./types.ts";
Expand Down Expand Up @@ -375,18 +376,65 @@ description: Excluded skill
assertEquals(toolRegistry.has("agent_ingestion-agent"), false);
});

it("materializes explicitly requested invoke_agent for direct runtimes", async () => {
const assistant = agent({
id: "generic-orchestrator",
system: "Invoke registered specialist agents.",
skills: [],
tools: { invoke_agent: true },
});

const definitions = await getAvailableTools(assistant.config.tools, {
callerAgentId: assistant.id,
includeIntegrationTools: false,
});

assertEquals(definitions.map((definition) => definition.name), ["invoke_agent"]);
});

it("suppresses generic invoke_agent when delegates are explicitly scoped", async () => {
for (
const [id, delegates, expectedTools] of [
["empty-delegate-scope", [], []],
["fixed-delegate-scope", ["ingestion-agent"], ["agent_ingestion-agent"]],
] as const
) {
const assistant = agent({
id,
system: "Delegate only within the explicit scope.",
skills: [],
delegates: [...delegates],
tools: { invoke_agent: true },
});

const definitions = await getAvailableTools(assistant.config.tools, {
callerAgentId: assistant.id,
includeIntegrationTools: false,
});

assertEquals(definitions.map((definition) => definition.name), [...expectedTools]);
}
});

it("rejects delegates combined with the implicit all-tools selector", () => {
assertThrows(
() =>
agent({
id: "broad-orchestrator",
system: "Delegate specialist work.",
delegates: ["ingestion-agent"],
tools: true,
}),
Error,
"cannot combine delegates with tools: true",
);
for (
const [id, delegates] of [
["empty-broad-orchestrator", []],
["broad-orchestrator", ["ingestion-agent"]],
] as const
) {
assertThrows(
() =>
agent({
id,
system: "Delegate specialist work.",
delegates: [...delegates],
tools: true,
}),
Error,
"cannot combine delegates with tools: true",
);
}
});

it("uses the default system prompt before an available skill catalog", async () => {
Expand Down
23 changes: 17 additions & 6 deletions src/agent/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import {
} from "#veryfront/security/input-validation/limits.ts";
import { DEFAULT_MAX_BODY_SIZE_BYTES } from "#veryfront/utils/constants/index.ts";
import { ensureBuiltinSchemaValidator } from "#veryfront/extensions/builtin-schema-validator.ts";
import { buildAgentDelegateTools } from "./runtime/agent-delegation.ts";
import {
buildAgentDelegateTools,
createInvokeAgentTool,
INVOKE_AGENT_TOOL_ID,
} from "./runtime/agent-delegation.ts";
import { normalizeAgentDelegateIds } from "./runtime/agent-delegation-names.ts";
import { buildAgentCallContext } from "./runtime/call-context.ts";
import type { RuntimeSkillDefinition } from "./runtime/skill-metadata.ts";
Expand Down Expand Up @@ -309,6 +313,11 @@ function resolveToolsConfiguration(input: {

if (config.tools !== true) {
const configuredTools = { ...(config.tools ?? {}) };
if (delegates !== undefined) {
delete configuredTools[INVOKE_AGENT_TOOL_ID];
} else if (configuredTools[INVOKE_AGENT_TOOL_ID] === true) {
configuredTools[INVOKE_AGENT_TOOL_ID] = createInvokeAgentTool({ selfId: id });
}
for (const registration of SKILL_TOOL_REGISTRATIONS) {
if (!exposeSkillTools) {
delete configuredTools[registration.id];
Expand All @@ -328,17 +337,19 @@ function resolveToolsConfiguration(input: {
merged = hasConfiguredTools || config.tools !== undefined ? configuredTools : undefined;
}

if (delegates?.length) {
if (delegates !== undefined) {
if (merged === true) {
throw INVALID_ARGUMENT.create({
detail: `Agent "${id}" cannot combine delegates with tools: true. ` +
"Declare the required tools by name so delegate capabilities remain explicit.",
});
}
merged = {
...(merged ?? {}),
...buildAgentDelegateTools({ delegates, selfId: id }),
};
if (delegates.length > 0) {
merged = {
...(merged ?? {}),
...buildAgentDelegateTools({ delegates, selfId: id }),
};
}
}

return merged;
Expand Down
4 changes: 2 additions & 2 deletions src/agent/hosted/cloud-agent-chat-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ export function buildLocalTools(
}),
);
} else {
// Agents authored before declarative delegates retain the legacy hosted
// child-fork tool. Explicit scoped delegate bindings opt out.
// Generic invoke_agent remains the platform tool for dynamic agent
// selection. Explicit scoped delegate bindings opt into fixed targets.
tools.invoke_agent = createInvokeAgentTool(
context,
taskContext,
Expand Down
8 changes: 4 additions & 4 deletions src/agent/hosted/cloud-agent-child-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,20 +453,20 @@ export function buildHostedDelegateTools(
});
}

/** The shape of a hosted delegation binding (scoped vs. legacy). */
/** The shape of a hosted delegation binding (fixed-target vs. generic). */
export type HostedDelegationBinding =
| { kind: "scoped"; delegateIds: string[] }
| { kind: "legacy" };
| { kind: "generic" };

/**
* Resolves the delegation binding from an agent config. Agents with an explicit
* `delegates` list use scoped delegation; all others fall back to legacy invoke_agent.
* `delegates` list use scoped delegation; all others use generic invoke_agent.
*/
export function resolveHostedDelegationBinding(
agentConfig: RuntimeAgentMarkdownDefinition | undefined,
): HostedDelegationBinding {
if (agentConfig?.delegates !== undefined) {
return { kind: "scoped", delegateIds: agentConfig.delegates };
}
return { kind: "legacy" };
return { kind: "generic" };
}
4 changes: 2 additions & 2 deletions src/agent/hosted/veryfront-cloud-agent-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ Deno.test("hosted generic invocation is only replaced by explicit delegates", ()
"get_file",
],
}),
{ kind: "legacy" },
{ kind: "generic" },
);
assertEquals(
veryfrontCloudAgentServiceInternals.resolveHostedDelegationBinding({
Expand All @@ -760,7 +760,7 @@ Deno.test("hosted generic invocation is only replaced by explicit delegates", ()
skills: ["legacy-workflow"],
tools: ["get_file"],
}),
{ kind: "legacy" },
{ kind: "generic" },
);
assertEquals(
veryfrontCloudAgentServiceInternals.resolveHostedDelegationBinding({
Expand Down
Loading