diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2685559d7..317916109 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/eval/__tests__/agent-bridge.test.ts b/packages/coding-agent/src/eval/__tests__/agent-bridge.test.ts index 30c4678c1..b5616e0df 100644 --- a/packages/coding-agent/src/eval/__tests__/agent-bridge.test.ts +++ b/packages/coding-agent/src/eval/__tests__/agent-bridge.test.ts @@ -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)); diff --git a/packages/coding-agent/src/eval/agent-bridge.ts b/packages/coding-agent/src/eval/agent-bridge.ts index 301709281..007cebfae 100644 --- a/packages/coding-agent/src/eval/agent-bridge.ts +++ b/packages/coding-agent/src/eval/agent-bridge.ts @@ -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 { 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); diff --git a/packages/coding-agent/src/prompts/tools/eval.md b/packages/coding-agent/src/prompts/tools/eval.md index fbe4204f7..81ea84b70 100644 --- a/packages/coding-agent/src/prompts/tools/eval.md +++ b/packages/coding-agent/src/prompts/tools/eval.md @@ -39,8 +39,8 @@ tool.(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, 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, agent } (parsed under `data` when `schema` set). {{#if js}} JS: options are ONE trailing object — agent(prompt, { agent, schema, handle }). {{/if}} {{/if}} diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index 72fe69ac5..355943ff9 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -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. diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index 85743ab9d..23fea8a56 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -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 = new Set(["task", "sonic"]); @@ -511,7 +508,8 @@ export class TaskTool implements AgentTool[1], theme: Theme) { @@ -566,13 +564,14 @@ export class TaskTool implements AgentTool, ): Promise> { 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 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 }, }; } diff --git a/packages/coding-agent/src/task/spawn-policy.test.ts b/packages/coding-agent/src/task/spawn-policy.test.ts new file mode 100644 index 000000000..e1fc772b2 --- /dev/null +++ b/packages/coding-agent/src/task/spawn-policy.test.ts @@ -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`"); + }); +}); diff --git a/packages/coding-agent/src/task/spawn-policy.ts b/packages/coding-agent/src/task/spawn-policy.ts new file mode 100644 index 000000000..225d72066 --- /dev/null +++ b/packages/coding-agent/src/task/spawn-policy.ts @@ -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(", "), + }; +} diff --git a/packages/coding-agent/src/task/types.ts b/packages/coding-agent/src/task/types.ts index 40faf390c..568981ca2 100644 --- a/packages/coding-agent/src/task/types.ts +++ b/packages/coding-agent/src/task/types.ts @@ -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(); + +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; diff --git a/packages/coding-agent/src/tools/__tests__/eval-description.test.ts b/packages/coding-agent/src/tools/__tests__/eval-description.test.ts new file mode 100644 index 000000000..86aa11b23 --- /dev/null +++ b/packages/coding-agent/src/tools/__tests__/eval-description.test.ts @@ -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(""); + }); +}); diff --git a/packages/coding-agent/src/tools/eval.ts b/packages/coding-agent/src/tools/eval.ts index b4b33c2cc..fb011648e 100644 --- a/packages/coding-agent/src/tools/eval.ts +++ b/packages/coding-agent/src/tools/eval.ts @@ -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 { 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. */