fix(task): respected restricted spawn defaults
Resolved eval agent() and task tool defaults from the active spawn policy so restricted agents advertise and execute an allowed default. Fixes #3973
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed eval and task spawn defaults to respect restricted agent `spawns` lists. ([#3973](https://github.com/can1357/oh-my-pi/issues/3973))
|
||||
|
||||
## [16.2.11] - 2026-07-01
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -196,15 +196,29 @@ describe("runEvalAgent", () => {
|
||||
await expect(runEvalAgent({ prompt: "hello" }, { session: makeSession({ spawns: "" }) })).rejects.toThrow(
|
||||
"spawns disabled",
|
||||
);
|
||||
await expect(runEvalAgent({ prompt: "hello" }, { session: makeSession({ spawns: "reviewer" }) })).rejects.toThrow(
|
||||
"Allowed: reviewer",
|
||||
);
|
||||
await expect(
|
||||
runEvalAgent({ prompt: "hello", agent: "task" }, { session: makeSession({ spawns: "reviewer" }) }),
|
||||
).rejects.toThrow("Allowed: reviewer");
|
||||
await expect(
|
||||
runEvalAgent({ prompt: "hello" }, { session: makeSession({ depth: EVAL_AGENT_MAX_DEPTH }) }),
|
||||
).rejects.toThrow("maximum depth");
|
||||
expect(runSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("defaults to the first allowed spawn under restricted eval policies", async () => {
|
||||
mockAgents();
|
||||
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options =>
|
||||
singleResult(options, {
|
||||
output: options.agent.name,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runEvalAgent({ prompt: "hello" }, { session: makeSession({ spawns: "reviewer,task" }) });
|
||||
|
||||
expect(result.text).toBe("reviewer");
|
||||
expect(runSpy.mock.calls[0]?.[0].agent.name).toBe("reviewer");
|
||||
});
|
||||
|
||||
it("throws instead of spawning from plan mode", async () => {
|
||||
mockAgents();
|
||||
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
runIsolatedSubprocess,
|
||||
} from "../task/isolation-runner";
|
||||
import { AgentOutputManager } from "../task/output-manager";
|
||||
import { resolveSpawnPolicy } from "../task/spawn-policy";
|
||||
import type { AgentDefinition, AgentProgress, SingleResult } from "../task/types";
|
||||
import { type NestedRepoPatch, parseIsolationMode } from "../task/worktree";
|
||||
import type { ToolSession } from "../tools";
|
||||
@@ -39,7 +40,6 @@ export const EVAL_AGENT_BRIDGE_NAME = "__agent__";
|
||||
/** Hard recursion limit for eval-driven subagents. */
|
||||
export const EVAL_AGENT_MAX_DEPTH = 3;
|
||||
|
||||
const DEFAULT_AGENT_TYPE = "task";
|
||||
const DEFAULT_AGENT_LABEL = "EvalAgent";
|
||||
|
||||
const agentArgsSchema = type({
|
||||
@@ -139,14 +139,12 @@ function assertDepthAllowed(session: ToolSession): void {
|
||||
}
|
||||
|
||||
function assertSpawnAllowed(session: ToolSession, agentName: string): void {
|
||||
const parentSpawns = session.getSessionSpawns() ?? "*";
|
||||
if (parentSpawns === "*") return;
|
||||
if (parentSpawns === "") {
|
||||
throw new ToolError(`Cannot spawn '${agentName}'. Allowed: none (spawns disabled for this agent)`);
|
||||
const spawnPolicy = resolveSpawnPolicy(session.getSessionSpawns());
|
||||
if (!spawnPolicy.enabled) {
|
||||
throw new ToolError(`Cannot spawn '${agentName}'. Allowed: ${spawnPolicy.allowedErrorText}`);
|
||||
}
|
||||
const allowedSpawns = parentSpawns.split(",").map(spawn => spawn.trim());
|
||||
if (!allowedSpawns.includes(agentName)) {
|
||||
throw new ToolError(`Cannot spawn '${agentName}'. Allowed: ${parentSpawns}`);
|
||||
if (spawnPolicy.allowedAgents !== null && !spawnPolicy.allowedAgents.includes(agentName)) {
|
||||
throw new ToolError(`Cannot spawn '${agentName}'. Allowed: ${spawnPolicy.allowedErrorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +281,7 @@ function buildSubagentFailureMessage(agentName: string, result: SingleResult): s
|
||||
*/
|
||||
export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOptions): Promise<EvalAgentResult> {
|
||||
const parsed = parseAgentArgs(args);
|
||||
const agentName = parsed.agent ?? DEFAULT_AGENT_TYPE;
|
||||
const agentName = parsed.agent ?? resolveSpawnPolicy(options.session.getSessionSpawns()).defaultAgent;
|
||||
const structured = Object.hasOwn(parsed, "schema");
|
||||
|
||||
assertNotPlanMode(options.session);
|
||||
|
||||
@@ -39,8 +39,8 @@ tool.<name>(args) → unknown
|
||||
Invoke any session tool; `args` = its parameter object.
|
||||
completion(prompt, model?="default", system?=None, schema?=None) → str | dict
|
||||
Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → structured output, parsed object.
|
||||
{{#if spawns}}agent(prompt, agent?="task", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
||||
Run a subagent → final output. `agent` picks another discovered agent; `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
||||
{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, handle?=False) → str | dict
|
||||
Run a subagent → final output. `agent` picks another discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` as in completion(). Background via `local://` files named in the prompt. `handle` → DAG node dict { text, output, handle: "agent://<id>", id, agent } (parsed under `data` when `schema` set).
|
||||
{{#if js}} JS: options are ONE trailing object — agent(prompt, { agent, schema, handle }).
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -10,7 +10,7 @@ Execution blocks your turn: the call only returns once the work is completely fi
|
||||
- **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
|
||||
|
||||
# Inputs
|
||||
- `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `task` (the general-purpose worker) — omit it for the default worker instead of passing `agent: "task"`.
|
||||
- `agent` (optional): The base agent type to use (e.g., `explore`, `reviewer`). Defaults to `{{defaultAgent}}`{{#if defaultAgentIsGeneric}} (the general-purpose worker){{/if}} — omit it for the default worker instead of passing `agent: "{{defaultAgent}}"`.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
|
||||
{{#if batchEnabled}}
|
||||
- `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
|
||||
- `tasks[]`: Array of subagents to spawn.
|
||||
|
||||
@@ -30,6 +30,7 @@ import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type:
|
||||
import { truncateForPrompt } from "../tools/approval";
|
||||
import { isIrcEnabled } from "../tools/irc";
|
||||
import { formatBytes, formatDuration } from "../tools/render-utils";
|
||||
import { DEFAULT_SPAWN_AGENT, resolveSpawnPolicy } from "./spawn-policy";
|
||||
import {
|
||||
type AgentDefinition,
|
||||
type AgentProgress,
|
||||
@@ -187,17 +188,13 @@ function renderDescription(
|
||||
ircEnabled: boolean,
|
||||
parentSpawns: string,
|
||||
): string {
|
||||
const spawningDisabled = parentSpawns === "";
|
||||
const spawnPolicy = resolveSpawnPolicy(parentSpawns);
|
||||
const spawningDisabled = !spawnPolicy.enabled;
|
||||
let filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
|
||||
if (spawningDisabled) {
|
||||
filteredAgents = [];
|
||||
} else if (parentSpawns !== "*") {
|
||||
const allowed = new Set(
|
||||
parentSpawns
|
||||
.split(",")
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
} else if (spawnPolicy.allowedAgents !== null) {
|
||||
const allowed = new Set(spawnPolicy.allowedAgents);
|
||||
filteredAgents = filteredAgents.filter(a => allowed.has(a.name));
|
||||
}
|
||||
const renderedAgents = filteredAgents.map(agent => ({
|
||||
@@ -208,6 +205,9 @@ function renderDescription(
|
||||
return prompt.render(taskDescriptionTemplate, {
|
||||
agents: renderedAgents,
|
||||
spawningDisabled,
|
||||
defaultAgent: spawnPolicy.defaultAgent,
|
||||
defaultAgentIsGeneric: spawnPolicy.defaultAgent === DEFAULT_SPAWN_AGENT,
|
||||
allowedAgentsText: spawnPolicy.allowedPromptText,
|
||||
MAX_CONCURRENCY: maxConcurrency,
|
||||
isolationEnabled,
|
||||
batchEnabled,
|
||||
@@ -331,9 +331,6 @@ function spawnParamsFor(params: TaskParams, item: TaskItem): TaskParams {
|
||||
return spawn;
|
||||
}
|
||||
|
||||
/** Agent type spawned when a `task` call omits `agent`; mirrors the schema default in `getTaskSchema`. */
|
||||
const DEFAULT_TASK_AGENT = "task";
|
||||
|
||||
/** Generic worker agents whose output sharpens with a tailored `role` rather than the bare type. */
|
||||
const GENERIC_SPAWN_AGENTS: ReadonlySet<string> = new Set(["task", "sonic"]);
|
||||
|
||||
@@ -511,7 +508,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
|
||||
get parameters(): TaskToolSchemaInstance {
|
||||
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
||||
return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled() });
|
||||
const defaultAgent = resolveSpawnPolicy(this.session.getSessionSpawns()).defaultAgent;
|
||||
return getTaskSchema({ isolationEnabled, batchEnabled: this.#isBatchEnabled(), defaultAgent });
|
||||
}
|
||||
|
||||
renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
|
||||
@@ -566,13 +564,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
||||
): Promise<AgentToolResult<TaskToolDetails>> {
|
||||
const repaired = repairTaskParams(rawParams as TaskParams);
|
||||
// The schema defaults `agent` to `task` for model calls, but internal
|
||||
// callers and stale transcripts build params directly and bypass arktype.
|
||||
// Normalize once here so every downstream path sees the resolved agent.
|
||||
// Schema defaults run for model calls, but internal callers and stale
|
||||
// transcripts can bypass arktype. Normalize once so every downstream path
|
||||
// sees the session's actual default agent.
|
||||
const defaultAgent = resolveSpawnPolicy(this.session.getSessionSpawns()).defaultAgent;
|
||||
const params =
|
||||
typeof repaired.agent === "string" && repaired.agent.trim() !== ""
|
||||
? repaired
|
||||
: { ...repaired, agent: DEFAULT_TASK_AGENT };
|
||||
: { ...repaired, agent: defaultAgent };
|
||||
const batchEnabled = this.#isBatchEnabled();
|
||||
const validationError = validateShapeParams(batchEnabled, params) ?? validateSpawnParams(params, batchEnabled);
|
||||
if (validationError) {
|
||||
@@ -1189,18 +1188,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
}
|
||||
|
||||
// Check spawn restrictions from parent
|
||||
const parentSpawns = this.session.getSessionSpawns() ?? "*";
|
||||
const allowedSpawns = parentSpawns.split(",").map(s => s.trim());
|
||||
const isSpawnAllowed = (): boolean => {
|
||||
if (parentSpawns === "") return false; // Empty = deny all
|
||||
if (parentSpawns === "*") return true; // Wildcard = allow all
|
||||
return allowedSpawns.includes(agentName);
|
||||
};
|
||||
|
||||
if (!isSpawnAllowed()) {
|
||||
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
||||
const spawnPolicy = resolveSpawnPolicy(this.session.getSessionSpawns());
|
||||
const spawnAllowed =
|
||||
spawnPolicy.enabled &&
|
||||
(spawnPolicy.allowedAgents === null || spawnPolicy.allowedAgents.includes(agentName));
|
||||
if (!spawnAllowed) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${allowed}` }],
|
||||
content: [
|
||||
{ type: "text", text: `Cannot spawn '${agentName}'. Allowed: ${spawnPolicy.allowedErrorText}` },
|
||||
],
|
||||
details: { projectAgentsDir, results: [], totalDurationMs: Date.now() - startTime },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, describe, expect, it, vi } from "bun:test";
|
||||
import { Settings } from "../config/settings";
|
||||
import type { ToolSession } from "../tools";
|
||||
import * as taskDiscovery from "./discovery";
|
||||
import { TaskTool } from "./index";
|
||||
import type { AgentDefinition } from "./types";
|
||||
import { getTaskSchema } from "./types";
|
||||
|
||||
const factFinderAgent = {
|
||||
name: "fact-finder",
|
||||
description: "Find facts.",
|
||||
systemPrompt: "Find facts.",
|
||||
source: "project",
|
||||
} satisfies AgentDefinition;
|
||||
|
||||
const oracleAgent = {
|
||||
name: "oracle",
|
||||
description: "Answer hard questions.",
|
||||
systemPrompt: "Answer hard questions.",
|
||||
source: "bundled",
|
||||
} satisfies AgentDefinition;
|
||||
|
||||
function makeSession(spawns: string): ToolSession {
|
||||
const settings = Settings.isolated({
|
||||
"async.enabled": false,
|
||||
"task.batch": true,
|
||||
"task.isolation.mode": "none",
|
||||
});
|
||||
return {
|
||||
cwd: process.cwd(),
|
||||
hasUI: false,
|
||||
settings,
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => spawns,
|
||||
};
|
||||
}
|
||||
|
||||
describe("task spawn policy surfaces", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("uses the first allowed spawn as the schema default", () => {
|
||||
const schema = getTaskSchema({ isolationEnabled: false, batchEnabled: false, defaultAgent: "fact-finder" });
|
||||
const parsed = schema({ assignment: "check" });
|
||||
|
||||
expect(parsed).toEqual({ agent: "fact-finder", assignment: "check" });
|
||||
});
|
||||
|
||||
it("renders the restricted spawn default in the task description", async () => {
|
||||
vi.spyOn(taskDiscovery, "discoverAgents").mockResolvedValue({
|
||||
agents: [factFinderAgent, oracleAgent],
|
||||
projectAgentsDir: null,
|
||||
});
|
||||
|
||||
const tool = await TaskTool.create(makeSession("fact-finder,oracle"));
|
||||
const description = tool.description;
|
||||
|
||||
expect(description).toContain("Defaults to `fact-finder`");
|
||||
expect(description).toContain("Current spawn policy allows: `fact-finder`, `oracle`.");
|
||||
expect(description).not.toContain("Defaults to `task`");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/** Default agent used when a session has unrestricted spawning. */
|
||||
export const DEFAULT_SPAWN_AGENT = "task";
|
||||
|
||||
/** Spawn policy derived from a parent agent's `spawns` frontmatter. */
|
||||
export interface ResolvedSpawnPolicy {
|
||||
/** True when at least one subagent may be spawned. */
|
||||
enabled: boolean;
|
||||
/** Agent used when the caller omits the agent field. */
|
||||
defaultAgent: string;
|
||||
/** Explicitly allowed agents, or `null` when the policy is unrestricted. */
|
||||
allowedAgents: readonly string[] | null;
|
||||
/** Text used in spawn rejection messages. */
|
||||
allowedErrorText: string;
|
||||
/** Backtick-quoted explicit agents for prompt descriptions. */
|
||||
allowedPromptText?: string;
|
||||
}
|
||||
|
||||
/** Resolves spawn frontmatter into the default and prompt/error surfaces. */
|
||||
export function resolveSpawnPolicy(parentSpawns: string | boolean | null | undefined): ResolvedSpawnPolicy {
|
||||
let normalized: string;
|
||||
if (parentSpawns === false) {
|
||||
normalized = "";
|
||||
} else if (parentSpawns === true || parentSpawns === null || parentSpawns === undefined) {
|
||||
normalized = "*";
|
||||
} else {
|
||||
normalized = parentSpawns.trim();
|
||||
}
|
||||
|
||||
if (normalized === "*") {
|
||||
return {
|
||||
enabled: true,
|
||||
defaultAgent: DEFAULT_SPAWN_AGENT,
|
||||
allowedAgents: null,
|
||||
allowedErrorText: "*",
|
||||
};
|
||||
}
|
||||
|
||||
const allowedAgents = normalized
|
||||
.split(",")
|
||||
.map(spawn => spawn.trim())
|
||||
.filter(Boolean);
|
||||
if (allowedAgents.length === 0) {
|
||||
return {
|
||||
enabled: false,
|
||||
defaultAgent: DEFAULT_SPAWN_AGENT,
|
||||
allowedAgents,
|
||||
allowedErrorText: "none (spawns disabled for this agent)",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
defaultAgent: allowedAgents[0] ?? DEFAULT_SPAWN_AGENT,
|
||||
allowedAgents,
|
||||
allowedErrorText: allowedAgents.join(","),
|
||||
allowedPromptText: allowedAgents.map(agent => `\`${agent}\``).join(", "),
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Usage } from "@oh-my-pi/pi-ai";
|
||||
import { $env } from "@oh-my-pi/pi-utils";
|
||||
import { type } from "arktype";
|
||||
import { type BaseType, type } from "arktype";
|
||||
import type { AgentSessionEvent } from "../session/agent-session";
|
||||
import type { NestedRepoPatch } from "./worktree";
|
||||
|
||||
@@ -144,13 +144,84 @@ const ALL_TASK_SCHEMAS = [taskSchema, taskSchemaNoIsolation, taskSchemaBatch, ta
|
||||
type DynamicTaskSchema = (typeof ALL_TASK_SCHEMAS)[number];
|
||||
export type TaskSchema = typeof taskSchema;
|
||||
/** Active task tool parameter schema for the current isolation / batch flags */
|
||||
export type TaskToolSchemaInstance = DynamicTaskSchema;
|
||||
export type TaskToolSchemaInstance = DynamicTaskSchema | BaseType;
|
||||
|
||||
export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled: boolean }): DynamicTaskSchema {
|
||||
if (options.batchEnabled) {
|
||||
return options.isolationEnabled ? taskSchemaBatch : taskSchemaBatchNoIsolation;
|
||||
const TASK_AGENT_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
const taskSchemaCache = new Map<string, BaseType>();
|
||||
|
||||
function taskAgentSchemaRule(defaultAgent: string): string {
|
||||
const trimmed = defaultAgent.trim();
|
||||
if (TASK_AGENT_NAME_PATTERN.test(trimmed)) {
|
||||
return `string = '${trimmed}'`;
|
||||
}
|
||||
return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
|
||||
return "string";
|
||||
}
|
||||
|
||||
function createTaskSchema(options: {
|
||||
isolationEnabled: boolean;
|
||||
batchEnabled: boolean;
|
||||
defaultAgent: string;
|
||||
}): BaseType {
|
||||
const agent = taskAgentSchemaRule(options.defaultAgent);
|
||||
if (options.batchEnabled) {
|
||||
if (options.isolationEnabled) {
|
||||
return type.raw({
|
||||
agent,
|
||||
context: "string",
|
||||
tasks: taskItemSchemaIsolated.array(),
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
return type.raw({
|
||||
agent,
|
||||
context: "string",
|
||||
tasks: taskItemSchema.array(),
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
if (options.isolationEnabled) {
|
||||
return type.raw({
|
||||
agent,
|
||||
"id?": "string",
|
||||
"description?": "string",
|
||||
"role?": ROLE_INPUT_SCHEMA,
|
||||
assignment: "string",
|
||||
"isolated?": "boolean",
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
return type.raw({
|
||||
agent,
|
||||
"id?": "string",
|
||||
"description?": "string",
|
||||
"role?": ROLE_INPUT_SCHEMA,
|
||||
assignment: "string",
|
||||
"+": "delete",
|
||||
});
|
||||
}
|
||||
|
||||
export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled: boolean }): DynamicTaskSchema;
|
||||
export function getTaskSchema(options: {
|
||||
isolationEnabled: boolean;
|
||||
batchEnabled: boolean;
|
||||
defaultAgent: string;
|
||||
}): TaskToolSchemaInstance;
|
||||
export function getTaskSchema(options: {
|
||||
isolationEnabled: boolean;
|
||||
batchEnabled: boolean;
|
||||
defaultAgent?: string;
|
||||
}): TaskToolSchemaInstance {
|
||||
const defaultAgent = options.defaultAgent ?? "task";
|
||||
if (defaultAgent === "task") {
|
||||
if (options.batchEnabled) return options.isolationEnabled ? taskSchemaBatch : taskSchemaBatchNoIsolation;
|
||||
return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
|
||||
}
|
||||
const key = `${options.isolationEnabled ? "iso" : "flat"}:${options.batchEnabled ? "batch" : "single"}:${defaultAgent}`;
|
||||
const cached = taskSchemaCache.get(key);
|
||||
if (cached) return cached;
|
||||
const schema = createTaskSchema({ ...options, defaultAgent });
|
||||
taskSchemaCache.set(key, schema);
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +231,7 @@ export function getTaskSchema(options: { isolationEnabled: boolean; batchEnabled
|
||||
* transcripts using the flat form keep working under either setting.
|
||||
*/
|
||||
export interface TaskParams {
|
||||
/** Agent type to spawn; defaults to `"task"` (the general-purpose worker) when omitted. */
|
||||
/** Agent type to spawn; omitted values resolve from the session spawn policy. */
|
||||
agent?: string;
|
||||
/** Stable agent id (flat form); default = generated AdjectiveNoun. */
|
||||
id?: string;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { getEvalToolDescription } from "../eval";
|
||||
|
||||
describe("eval tool description", () => {
|
||||
it("advertises the first allowed spawn as the agent() default", () => {
|
||||
const description = getEvalToolDescription({ py: true, js: false, spawns: "fact-finder,oracle" });
|
||||
|
||||
expect(description).toContain('agent(prompt, agent?="fact-finder"');
|
||||
expect(description).toContain("omit it to use `fact-finder`");
|
||||
expect(description).toContain("Allowed agents: `fact-finder`, `oracle`.");
|
||||
});
|
||||
|
||||
it("omits agent() when spawning is disabled", () => {
|
||||
const description = getEvalToolDescription({ py: true, js: false, spawns: "" });
|
||||
|
||||
expect(description).not.toContain("agent(prompt");
|
||||
expect(description).not.toContain("<dag>");
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { defaultEvalSessionId } from "../eval/session-id";
|
||||
import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
|
||||
import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
|
||||
import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
|
||||
import { resolveSpawnPolicy } from "../task/spawn-policy";
|
||||
import { webpExclusionForModel } from "../utils/image-loading";
|
||||
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
||||
import type { ToolSession } from ".";
|
||||
@@ -164,13 +165,10 @@ export interface EvalToolDescriptionOptions {
|
||||
rb?: boolean;
|
||||
jl?: boolean;
|
||||
/**
|
||||
* Whether `agent()` is allowed in this session. Driven by the parent's
|
||||
* spawn policy (`getSessionSpawns`). Defaults to `true` for backward
|
||||
* compatibility — when the session forbids spawning, the prelude doc
|
||||
* omits the `agent()` entry so the model does not promise itself a
|
||||
* helper that will only ever throw "spawns disabled".
|
||||
* Parent spawn policy (`getSessionSpawns`). `true`/omitted means unrestricted,
|
||||
* `false`/`""` hides `agent()`, and a comma list drives the advertised default.
|
||||
*/
|
||||
spawns?: boolean;
|
||||
spawns?: boolean | string | null;
|
||||
}
|
||||
|
||||
export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
|
||||
@@ -178,8 +176,16 @@ export function getEvalToolDescription(options: EvalToolDescriptionOptions = {})
|
||||
const js = options.js ?? true;
|
||||
const rb = options.rb ?? false;
|
||||
const jl = options.jl ?? false;
|
||||
const spawns = options.spawns ?? true;
|
||||
return prompt.render(evalDescription, { py, js, rb, jl, spawns });
|
||||
const spawnPolicy = resolveSpawnPolicy(options.spawns ?? true);
|
||||
return prompt.render(evalDescription, {
|
||||
py,
|
||||
js,
|
||||
rb,
|
||||
jl,
|
||||
spawns: spawnPolicy.enabled,
|
||||
spawnDefaultAgent: spawnPolicy.defaultAgent,
|
||||
spawnAllowedAgentsText: spawnPolicy.allowedPromptText,
|
||||
});
|
||||
}
|
||||
|
||||
export interface EvalToolOptions {
|
||||
@@ -294,13 +300,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
||||
if (!this.session) return getEvalToolDescription();
|
||||
const backends = resolveEvalBackends(this.session);
|
||||
const sessionSpawns = this.session.getSessionSpawns?.() ?? "*";
|
||||
const spawnsAllowed = sessionSpawns !== "" && sessionSpawns !== null;
|
||||
return getEvalToolDescription({
|
||||
py: backends.python,
|
||||
js: backends.js,
|
||||
rb: backends.ruby,
|
||||
jl: backends.julia,
|
||||
spawns: spawnsAllowed,
|
||||
spawns: sessionSpawns,
|
||||
});
|
||||
}
|
||||
/** All reuse-chain examples; the `examples` getter filters by enabled languages. */
|
||||
|
||||
Reference in New Issue
Block a user