/** * TypeBox schemas for subagent tool parameters */ import { Type } from "typebox"; function keepTopLevelParameterDescriptions(schema: T): T { return pruneNestedDescriptions(schema, []) as T; } function pruneNestedDescriptions(value: unknown, path: string[]): unknown { if (!value || typeof value !== "object") return value; const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value)); for (const key of Reflect.ownKeys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor) continue; if (key === "description" && !isTopLevelParameterDescription(path)) continue; if ("value" in descriptor) { const nextPath = typeof key === "string" ? [...path, key] : path; descriptor.value = pruneNestedDescriptions(descriptor.value, nextPath); } Object.defineProperty(result, key, descriptor); } return result; } function isTopLevelParameterDescription(path: string[]): boolean { return path.length === 2 && path[0] === "properties"; } const SkillOverride = Type.Unsafe({ anyOf: [ { type: "array", items: { type: "string" } }, { type: "boolean" }, { type: "string" }, ], description: "Skill name(s) to make available (comma-separated), array of strings, or boolean (false disables, true uses default)", }); const OutputOverride = Type.Unsafe({ anyOf: [ { type: "string" }, { type: "boolean" }, ], description: "Output filename/path (string), or false to disable file output", }); const OutputModeOverride = Type.String({ enum: ["inline", "file-only"], description: "Return saved output inline (default) or only a concise file reference. file-only requires output to be a path.", }); const ReadsOverride = Type.Unsafe({ anyOf: [ { type: "array", items: { type: "string" } }, { type: "boolean" }, ], description: "Files to read before running (array of filenames), or false to disable", }); const JsonSchemaObject = Type.Unsafe({ type: "object", additionalProperties: true, description: "JSON Schema object for strict structured output. Non-object roots are rejected.", }); const AcceptanceEvidenceKinds = [ "changed-files", "tests-added", "commands-run", "validation-output", "residual-risks", "no-staged-files", "diff-summary", "review-findings", "manual-notes", ]; const AcceptanceOverride = Type.Unsafe({ anyOf: [ { type: "string", enum: ["auto", "attested", "checked"] }, { type: "string", enum: ["reviewed"], deprecated: true, description: "Invalid as an explicit policy. Recognized only so preflight can explain that reviewed is an achieved status.", }, { type: "boolean", enum: [false] }, { type: "object", additionalProperties: true }, ], description: `Optional acceptance policy. For commentator/read-only calls, omit acceptance. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(", ")}. Evidence levels end at verified; use acceptance.review.required for review. Omitted means auto-inferred unless agentContract compatibility behavior is enabled.`, }); const AgentContractOverride = Type.Object({ version: Type.Integer({ enum: [1], description: "Enable compatibility behavior for this run/child." }), }, { additionalProperties: false, description: "Compatibility behavior. Omit for the default behavior." }); const ChainGateOverride = Type.String({ enum: ["execution", "acceptance"], description: "For chain steps with agentContract, choose whether the chain advances on execution success or acceptance success. Defaults to execution.", }); const TurnBudgetOverride = Type.Object({ maxTurns: Type.Integer({ minimum: 1 }), graceTurns: Type.Optional(Type.Integer({ minimum: 0 })), }, { additionalProperties: false, description: "Optional assistant-turn budget. At maxTurns the child is asked to wrap up; after graceTurns additional assistant turns it is aborted and partial output is returned." }); const ToolBudgetBlock = Type.Unsafe({ anyOf: [ { type: "array", minItems: 1, items: { type: "string", minLength: 1 } }, { type: "string", enum: ["*"] }, ], }); const ToolBudgetOverride = Type.Object({ soft: Type.Optional(Type.Integer({ minimum: 1 })), hard: Type.Integer({ minimum: 1 }), block: Type.Optional(ToolBudgetBlock), }, { additionalProperties: false, description: "Optional child tool-call budget. soft nudges the child; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize." }); const UsageBudgetLimitOverride = Type.Object({ soft: Type.Optional(Type.Number({ exclusiveMinimum: 0 })), hard: Type.Number({ exclusiveMinimum: 0 }), }, { additionalProperties: false }); const UsageBudgetOverride = Type.Object({ tokens: Type.Optional(UsageBudgetLimitOverride), costUsd: Type.Optional(UsageBudgetLimitOverride), }, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." }); // Parallel task item (within a parallel step) export const ParallelTaskSchema = Type.Object({ agent: Type.String(), task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })), phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })), label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })), as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })), outputSchema: Type.Optional(JsonSchemaObject), cwd: Type.Optional(Type.String()), count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })), output: Type.Optional(OutputOverride), outputMode: Type.Optional(OutputModeOverride), reads: Type.Optional(ReadsOverride), progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })), skill: Type.Optional(SkillOverride), model: Type.Optional(Type.String({ description: "Override model for this task" })), toolBudget: Type.Optional(ToolBudgetOverride), acceptance: Type.Optional(AcceptanceOverride), agentContract: Type.Optional(AgentContractOverride), gateOn: Type.Optional(ChainGateOverride), }); export const DynamicExpandSchema = Type.Object({ from: Type.Object({ output: Type.String({ description: "Prior named structured output to expand from." }), path: Type.String({ description: "JSON Pointer into the structured output, e.g. /items." }), }, { additionalProperties: false }), item: Type.Optional(Type.String({ description: "Template variable name for each item. Defaults to item." })), key: Type.Optional(Type.String({ description: "JSON Pointer relative to each item for stable child ids." })), maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required fanout bound unless configured globally." })), onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Empty input behavior. Defaults to skip." })), }, { additionalProperties: false }); export const DynamicParallelTemplateSchema = Type.Object({ agent: Type.String(), task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })), phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })), label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })), outputSchema: Type.Optional(JsonSchemaObject), cwd: Type.Optional(Type.String()), output: Type.Optional(OutputOverride), outputMode: Type.Optional(OutputModeOverride), reads: Type.Optional(ReadsOverride), progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })), skill: Type.Optional(SkillOverride), model: Type.Optional(Type.String({ description: "Override model for this task" })), toolBudget: Type.Optional(ToolBudgetOverride), acceptance: Type.Optional(AcceptanceOverride), agentContract: Type.Optional(AgentContractOverride), gateOn: Type.Optional(ChainGateOverride), }, { additionalProperties: false }); export const DynamicCollectSchema = Type.Object({ as: Type.String({ description: "Safe output name for the ordered collected result array." }), outputSchema: Type.Optional(JsonSchemaObject), }, { additionalProperties: false }); // Flattened so chain steps do not need an object-shape anyOf/oneOf union. export const ChainItem = Type.Object({ checkpoint: Type.Optional(Type.String({ description: "Approval checkpoint name. Pauses the chain without launching a child until approve-checkpoint or reject-checkpoint is called." })), message: Type.Optional(Type.String({ description: "Optional approval message shown while the checkpoint is paused." })), agent: Type.Optional(Type.String({ description: "Sequential step agent name" })), task: Type.Optional(Type.String({ description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps." })), phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })), label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })), as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })), outputSchema: Type.Optional(JsonSchemaObject), cwd: Type.Optional(Type.String()), output: Type.Optional(OutputOverride), outputMode: Type.Optional(OutputModeOverride), reads: Type.Optional(ReadsOverride), progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })), skill: Type.Optional(SkillOverride), model: Type.Optional(Type.String({ description: "Override model for this step" })), toolBudget: Type.Optional(ToolBudgetOverride), acceptance: Type.Optional(AcceptanceOverride), agentContract: Type.Optional(AgentContractOverride), gateOn: Type.Optional(ChainGateOverride), parallel: Type.Optional(Type.Unsafe({ anyOf: [ Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }), DynamicParallelTemplateSchema, ], description: "Static parallel tasks array, or a single dynamic fanout child template when expand/collect are present.", })), expand: Type.Optional(DynamicExpandSchema), collect: Type.Optional(DynamicCollectSchema), concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })), failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })), worktree: Type.Optional(Type.Boolean({ description: "Create isolated git worktrees for each parallel task." })), }, { description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, {expand, parallel: {...}, collect} for dynamic fanout, or {checkpoint: name, message?} for an approval pause.", additionalProperties: false, }); // Runtime mission handlers validate these untrusted nested objects loudly. Keeping // their provider schema shallow avoids repeating a full durable-record schema in // every tool request. const MissionLaunchOverride = Type.Unsafe({ anyOf: [ { type: "object", additionalProperties: true }, { type: "boolean", enum: [false] }, ], }); const MissionUpdateOverride = Type.Unsafe({ type: "object", additionalProperties: true }); const ControlOverrides = Type.Object({ enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })), needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "No-observed-activity window before a run needs attention" })), activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by elapsed ms (default: 240000)" })), activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by assistant turns (disabled by default)" })), activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by total tokens (disabled by default)" })), failedToolAttemptsBeforeAttention: Type.Optional(Type.Integer({ minimum: 1, description: "Consecutive mutating-tool failures before escalating to needs_attention (default: 3)" })), notifyOn: Type.Optional(Type.Array(Type.String({ enum: ["active_long_running", "needs_attention"] }), { description: "Control event types that should notify the parent/orchestrator. Defaults to active_long_running and needs_attention.", })), notifyChannels: Type.Optional(Type.Array(Type.String({ enum: ["event", "async", "intercom"] }), { description: "Notification channels to use when available. Defaults to event, async, and intercom.", })), }); const SubagentParamsSchema = Type.Object({ agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })), task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })), // Management action (when present, tool operates in management mode) action: Type.Optional(Type.String({ description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task} or {workflowScript}); use it only for management/control actions." })), name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })), id: Type.Optional(Type.String({ description: "Run id or prefix for status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run." })), runId: Type.Optional(Type.String({ description: "Target run ID for interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls." })), dir: Type.Optional(Type.String({ description: "Async run directory for action='status', action='stop', action='resume', or action='steer'." })), handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })), index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })), view: Type.Optional(Type.String({ enum: ["fleet", "transcript"], description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.", })), lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })), message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })), steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls; extension RPC steering forces false so callers retain exact child ownership." })), additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })), scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })), target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })), focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })), thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' (off/minimal/low/medium/high/xhigh/max, inherit, or false for off)." })), schedule: Type.Optional(Type.String({ deprecated: true, description: "Removed one-shot schedule field. Use action='schedule.create' with at." })), scheduleName: Type.Optional(Type.String({ deprecated: true, description: "Removed schedule display field. Use name." })), at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })), every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })), on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })), timezone: Type.Optional(Type.String({ description: "IANA timezone reserved for a later calendar schedule slice." })), overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })), catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })), missionId: Type.Optional(Type.String({ description: "Mission id." })), mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission." })), missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: summary, labels, decisions, artifacts, or delivery receipts." })), missionStatus: Type.Optional(Type.String({ description: "Mission status." })), missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })), runMode: Type.Optional(Type.String({ description: "Attached run mode." })), runStatus: Type.Optional(Type.String({ description: "Attached run status." })), summary: Type.Optional(Type.String({ description: "Mission close summary." })), // Chain identifier for management (can't reuse 'chain' — that's the execution array) chainName: Type.Optional(Type.String({ description: "Chain name for get/update/delete management actions" })), // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields) config: Type.Optional(Type.Unsafe({ anyOf: [ { type: "object", additionalProperties: true }, { type: "string" }, ], description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain." })), workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript orchestration. Starts asynchronously by default; pass async:false for a small foreground run. Use await runs.run(key, {agent, task, worktree?}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Set worktree:true at workflow or child level for a separate managed worktree per child; child fields override workflow defaults. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })), chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "terminal", "milestones", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; background and other-repo workflows stay quieter with terminal/milestone summaries." })), worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives a direct single child or each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })), step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })), context: Type.Optional(Type.String({ enum: ["fresh", "fork"], description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.", })), async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })), timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout for foreground and async/background runs; foreground defaults to 30m absent call/agent. Alias maxRuntimeMs." })), maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs; foreground defaults to 30m absent call/agent." })), turnBudget: Type.Optional(TurnBudgetOverride), toolBudget: Type.Optional(ToolBudgetOverride), usageBudget: Type.Optional(UsageBudgetOverride), agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })), cwd: Type.Optional(Type.String({ description: "Execution cwd, or target project directory for project.open/status/close." })), artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })), includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })), share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })), sessionDir: Type.Optional( Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }), ), // Clarification TUI clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution. Explicit clarify: true keeps the run foreground for the clarify UI; omitted clarify can still run in the background when async: true is set." })), control: Type.Optional(ControlOverrides), // Solo agent overrides output: Type.Optional(Type.Unsafe({ anyOf: [ { type: "string" }, { type: "boolean" }, ], description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.", })), outputMode: Type.Optional(OutputModeOverride), skill: Type.Optional(SkillOverride), model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })), outputSchema: Type.Optional(JsonSchemaObject), agentContract: Type.Optional(AgentContractOverride), acceptance: Type.Optional(AcceptanceOverride), }); export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema); const SubagentWaitParamsSchema = Type.Object({ id: Type.Optional(Type.String({ description: "Async run or remembered detached foreground run id/prefix to wait for one specific run. Omit to wait across every active async run started in this session.", })), nonBlocking: Type.Optional(Type.Boolean({ description: "When true, resolve id to one exact run, persist a wake subscription, and return immediately. The originating session is woken on completion, failure, attention, reconciliation failure, or timeout. Requires id and cannot be combined with all.", })), all: Type.Optional(Type.Boolean({ description: "Wait for ALL active runs to finish. Default false: return as soon as the first run finishes, so a fleet manager can spawn a replacement and wait again. Ignored when id targets a single run.", })), timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).", })), }); export const SubagentWaitParams = keepTopLevelParameterDescriptions(SubagentWaitParamsSchema);