UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

668 lines (667 loc) • 45.9 kB
import { Type } from "typebox"; import { defineTool, errorResult } from "../core/tools.js"; import { governanceBaselineError, governanceBaselineProblem } from "./governance-baseline-validity.js"; import { egressText, egressThrownText, boundedRedactedSummary } from "../core/untrusted-egress.js"; import { withDelegationProvenance } from "../core/tool-policy.js"; import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "../agents/launch-receipt-contract.js"; import { startWorkflow } from "./workflow.js"; import { WORKFLOW_AGENT_PARKED_ERROR_CODE } from "./workflow-types.js"; import { checkpointStoreOfSeat } from "../core/checkpoint-seat.js"; import { buildWorkflowPrimitives } from "./workflow-primitives.js"; import { stripSpawnTurnQuestionFace } from "../core/ask-question.js"; import { parentSpecSeatOfCtx } from "../core/parent-spec-seat.js"; import { CLAMP_SEATS, CLAMP_SET, assertClampRecord, clampRecordError, clampSourceOf, foldClampSources } from "../core/child-clamp-seats.js"; import { assertNoRetiredKeys } from "../core/retired-keys.js"; const RETIRED_RUN_WORKFLOW_TOOL_DEPS_KEYS = { principal: "the host run's principal rides `RunWorkflowToolDeps.parentSpecSeat.principal` (the parent SPEC seat the host's prepare mints), never a seat of its own", oneShot: "the host run's one-shot fact rides `RunWorkflowToolDeps.parentSpecSeat.oneShot` (only `true` travels), never a seat of its own", builtinWorkflows: "the engine ships no built-in named workflows — `{name}` resolves only the deployment's `scriptStore`; register any named workflow there, or pass `script` inline", parentExcludeTools: "the host run's tool selector layers ride `RunWorkflowToolDeps.parentToolCeiling` (`ToolExecuteContext.toolSelector.layers`, the host's prepare-time selector) and bound every agent the workflow spawns as its ceiling — an exclusion list is no longer handed down by name", ...Object.fromEntries(CLAMP_SEATS.filter((seat) => CLAMP_SET[seat].carrier === "clamp").map((seat) => [ `parent${seat.charAt(0).toUpperCase()}${seat.slice(1)}`, `the host's tighten-only clamps ride ONE getter, \`RunWorkflowToolDeps.parentClamps\` (every clamp row, present keys) — return \`${seat}\` from it`, ])), }; import { parseWorkflowMeta, splitWorkflowMeta, workflowScriptReadsClockOrRandom } from "./workflow-meta.js"; import { mergeWorkflowArgs, normalizeStringArg } from "./workflow-script-store.js"; import { workflowSizeGuidelineSection } from "./workflow-size-guideline.js"; import { toolFace } from "../core/tool-catalog-entries.js"; import { thrownText } from "../core/thrown-value.js"; export const RUN_WORKFLOW_TOOL_NAME = "Workflow"; function stripCodeFences(s) { const t = s.trim(); const m = /^```[A-Za-z]*\r?\n([\s\S]*?)\r?\n?```$/.exec(t); return m ? m[1] : s; } const DEFAULTS = { maxScriptChars: 100_000, maxAgents: 50, totalTimeoutMs: 600_000, maxResultChars: 100_000, maxLogChars: 10_000, }; function completionSummary(name, run) { const cleanName = name?.replace(/[^\p{L}\p{N} _.:/-]/gu, "").replace(/\s+/g, " ").trim().slice(0, 60); const done = run.agents.filter((a) => a.status === "completed").length; const failed = run.agents.filter((a) => a.status === "failed").length; const secs = run.endedAt !== undefined ? Math.max(0, Math.round((run.endedAt - run.startedAt) / 1000)) : undefined; const elapsed = secs === undefined ? "" : secs >= 60 ? ` in ${Math.floor(secs / 60)}m${secs % 60}s` : ` in ${secs}s`; const resumeClause = run.resume === undefined ? "" : run.resume.journalEntries === 0 ? "; resume found NO journal entries — nothing replayed, the whole script re-ran live" : `; resume: replayed ${run.resume.replayed} of ${run.resume.journalEntries} journaled results`; return (`workflow${cleanName ? ` "${cleanName}"` : ""} completed${elapsed} — agents ${done}/${run.agents.length} done` + `${failed > 0 ? ` (${failed} failed)` : ""}${resumeClause}; full result + per-agent rows via TaskOutput("${run.id}")`); } function completionIdFromRejection(err) { if (err === null || (typeof err !== "object" && typeof err !== "function")) return undefined; const run = err.workflowRun; return run?.completionId; } function workflowUsageBlock(run) { const agents = run.agents; return { ...run.stats, agent_count: agents.length, agents_done: agents.filter((a) => a.status === "completed").length, agents_error: agents.filter((a) => a.status === "failed").length, ...(agents.some((a) => a.status === "parked") ? { agents_parked: agents.filter((a) => a.status === "parked").length } : {}), agents_empty_result: agents.filter((a) => a.status === "completed" && (a.output === undefined || a.output === "")).length, agents_replayed: run.resume?.replayed ?? 0, ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}), ...(run.resume !== undefined ? { resume_journal_entries: run.resume.journalEntries } : {}), subagent_tokens: run.stats.tokens + run.stats.nested.tokens, tool_uses: agents.reduce((n, a) => n + (a.toolCalls ?? 0), 0), ...(run.endedAt !== undefined ? { duration_ms: run.endedAt - run.startedAt } : {}), }; } function workflowDiagnostics(runId, journalRef, resumeMiss) { const journalTeach = journalRef === undefined ? "" : journalRef.includes("://") ? ` Full journal: ${journalRef}.` : ` Full journal: ${journalRef} (a deployment data path — if Read is denied there, use the TaskOutput route above instead).`; const resumeMissTeach = resumeMiss ? ` NOTE: this was a resume, but the prior run's journal was not usable under this scope — verify the runId and that this deployment wires a journalStore; the results above are from a full live re-run.` : ""; return (`Per-agent rows (status / error / elapsed): TaskOutput({ task_id: "${runId}" }) — read them before assuming an empty or unexpected result.` + journalTeach + resumeMissTeach + ` To continue after editing the script: re-invoke with resumeFromRunId: "${runId}" (completed agents replay from the journal; only new/changed calls run).`); } const SCRIPT_ERROR_CODES = new Set([ "workflow.script_error", "workflow.parked_call_changed", "workflow.nesting", "workflow.model_not_allowed", "workflow.agent_schema", "workflow.max_agents", "workflow.result_too_large", "workflow.budget_exceeded", ]); function failureSummary(err, runId) { const e = err; const code = typeof e?.code === "string" ? e.code : undefined; if (code === WORKFLOW_AGENT_PARKED_ERROR_CODE) { const msg = typeof e?.message === "string" ? egressText("same-principal", e.message) : ""; return (`workflow parked (${code}): ${msg.length > 200 ? `${msg.slice(0, 200)}…` : msg} — it did not fail and is not finished` + (runId ? `; per-agent rows via TaskOutput("${runId}") (the parked row reads status "parked")` : "") + `; do not re-issue the same call — once the approval is decided, the deployment resumes this run${runId ? ` (resumeFromRunId: "${runId}")` : ""} and the script continues from the parked call`); } const guide = runId ? `; per-agent rows via TaskOutput("${runId}"); after fixing, re-invoke with resumeFromRunId: "${runId}" (completed agents replay from the journal)` : ""; if (code && SCRIPT_ERROR_CODES.has(code) && typeof e?.message === "string") { const msg = egressText("same-principal", e.message); return `workflow failed (${code}): ${msg.length > 200 ? `${msg.slice(0, 200)}…` : msg}${guide}`; } return `${code && code.startsWith("workflow.") ? `workflow failed (${code})` : "workflow failed"}${guide}`; } export function workflowWhenToUseText(m) { return (m.whenToUse || m.description) || undefined; } export function renderNamedWorkflowListing(entries) { if (entries.length === 0) return undefined; const lines = entries.map((e) => { const w = workflowWhenToUseText(e); return `- ${e.name}${w ? `: ${w}` : ""}`; }); return ("Named workflows available via {name} (registered defaults merge UNDER your `args`; a top-level key you pass wins wholesale):\n" + lines.join("\n")); } async function collectNamedWorkflowListings(store) { const byName = new Map(); try { const listed = await store?.list?.(); if (Array.isArray(listed)) { const rows = listed.filter((e) => Boolean(e) && typeof e.name === "string" && e.name.length > 0); for (const e of rows) byName.set(e.name, e); } } catch { } return [...byName.values()]; } export function createRunWorkflowTool(d) { assertNoRetiredKeys(d, RETIRED_RUN_WORKFLOW_TOOL_DEPS_KEYS, "RunWorkflowToolDeps"); if (d.parentClamps !== undefined && typeof d.parentClamps !== "function") { throw clampRecordError(`RunWorkflowToolDeps.parentClamps must be a getter returning the host's clamp source, got ${typeof d.parentClamps}`); } return mountRunWorkflowTool(d); } async function mountRunWorkflowTool(d) { if (d.scriptRunner.safeForUntrustedScripts !== true) { throw new Error("createRunWorkflowTool: refusing to mount run_workflow on a runner that is not safeForUntrustedScripts (design/98 fail-closed)"); } const lim = d.limits ?? {}; const maxScriptChars = lim.maxScriptChars ?? DEFAULTS.maxScriptChars; const childCaps = { perAgentMaxWalltimeMs: lim.perAgentMaxWalltimeMs, childMaxCostUsd: lim.childMaxCostUsd, childMaxTokens: lim.childMaxTokens, childMaxTurns: lim.childMaxTurns, }; const problem = governanceBaselineProblem(d.governanceBaseline); if (problem !== null) throw governanceBaselineError(problem); const FACE_LIST_FIELDS = ["allowTools", "excludeTools", "deferTools", "alwaysLoadTools"]; const dropNullFaces = (slot) => { let out = slot; for (const field of FACE_LIST_FIELDS) { if (slot[field] !== null) continue; out = { ...out }; delete out[field]; } return out; }; const dropNullishOnAsk = (slot) => { if (!("onAsk" in slot) || (slot.onAsk !== undefined && slot.onAsk !== null)) return slot; const { onAsk: _absent, ...rest } = slot; return rest; }; const sanitizedBaseline = (() => { const wt = d.governanceBaseline.worktreeBase; const { worktreeBase: _absentOverlay, ...rest } = d.governanceBaseline; return { ...rest, base: dropNullishOnAsk(dropNullFaces(d.governanceBaseline.base)), ...(wt === null || wt === undefined ? {} : { worktreeBase: dropNullishOnAsk(dropNullFaces(wt)) }), }; })(); const unionFaceList = (own, parent) => own === undefined ? [...new Set(parent)] : [...new Set([...own, ...parent])]; const withParentFace = (base, inherit) => ({ ...base, ...(d.parentDeferTools?.length ? { deferTools: unionFaceList(base.deferTools ?? inherit?.deferTools, d.parentDeferTools) } : {}), ...(d.parentAlwaysLoadTools?.length ? { alwaysLoadTools: unionFaceList(base.alwaysLoadTools ?? inherit?.alwaysLoadTools, d.parentAlwaysLoadTools) } : {}), ...(() => { const parent = d.parentRestoreGatedTools; if (parent !== true && !(Array.isArray(parent) && parent.length > 0)) return {}; const own = base.restoreGatedTools !== undefined ? base.restoreGatedTools : inherit?.restoreGatedTools; return { restoreGatedTools: parent === true || own === true ? true : own === undefined ? parent : Array.isArray(own) ? [...new Set([...own, ...parent])] : own, }; })(), }); const withParentProfile = (base) => d.parentPromptProfile !== undefined && base.promptProfile === undefined ? { ...base, promptProfile: d.parentPromptProfile } : base; const baselineWithParentFace = (d.parentDeferTools?.length ?? 0) > 0 || (d.parentAlwaysLoadTools?.length ?? 0) > 0 || d.parentPromptProfile !== undefined || d.parentRestoreGatedTools !== undefined ? { ...sanitizedBaseline, base: withParentProfile(withParentFace(sanitizedBaseline.base)), ...(sanitizedBaseline.worktreeBase !== undefined ? { worktreeBase: withParentFace(sanitizedBaseline.worktreeBase, sanitizedBaseline.base) } : {}), } : sanitizedBaseline; const governance = { baseline: baselineWithParentFace, models: d.models, caps: childCaps, onNotice: d.onNotice }; const namedWorkflowSection = renderNamedWorkflowListing(await collectNamedWorkflowListings(d.scriptStore)); const sizeGuidelineSection = workflowSizeGuidelineSection(d.sizeGuideline ?? lim.sizeGuideline); const structuredError = (message) => errorResult(JSON.stringify({ error: message })); return defineTool({ name: RUN_WORKFLOW_TOOL_NAME, ...toolFace("workflow"), approvalPreview: (raw) => { const a = (raw ?? {}); if (typeof a.name === "string" && a.name) return { form: "named", workflow: a.name, ...(a.args !== undefined ? { hasArgs: true } : {}) }; if (typeof a.scriptPath === "string" && a.scriptPath) return { form: "scriptPath", scriptPath: a.scriptPath, ...(a.args !== undefined ? { hasArgs: true } : {}) }; if (typeof a.script !== "string" || !a.script) return undefined; try { const meta = parseWorkflowMeta(a.script); return { form: "script", name: meta.name, description: meta.description, ...(meta.phases !== undefined ? { phases: meta.phases.map((ph) => ({ title: ph.title, ...(ph.detail !== undefined ? { detail: ph.detail } : {}) })) } : {}), scriptChars: a.script.length, ...(a.args !== undefined ? { hasArgs: true } : {}), }; } catch (e) { return { form: "script", parseError: thrownText(e).slice(0, 300), scriptChars: a.script.length }; } }, description: "Author and run your own deterministic JS WORKFLOW that spawns and coordinates sub-agents. Pass `script` " + "(begins with `export const meta = {...}`, then an async body) and optional `args` (a JSON value). Returns a " + "runId immediately; the workflow runs asynchronously and you are notified on completion. Use for a " + "SUBSTANTIAL task that genuinely decomposes.\n" + "ONLY call this tool when multi-agent orchestration was explicitly opted into: the user/operator asked " + "for a workflow or fan-out in their own words, a skill or task directive requests it, or your standing " + "instructions authorize self-orchestration. For any other task — even one that would clearly benefit " + "from parallelism — do NOT call this tool: delegate individual sub-agents instead, or briefly describe " + "what a workflow could do and ask first. A task merely LOOKING decomposable does not count.\n" + "Pick the MINIMAL primitive that fits — do NOT reach for parallel by default:\n" + "• agent(spec): ONE sub-agent (sequential when awaited). The DEFAULT — use it for any single coherent task. " + "Both forms work: agent({ objective: '…' }, { label: '…', phase: '…' }) and agent('prompt string', opts?). " + "ALWAYS pass a short kebab-case label naming what THIS agent does (e.g. { label: 'fix-lint-errors' }) — " + "label/phase go in OPTS (the second argument), never inside spec, where they are ignored; unlabeled " + "agents render as anonymous agent-N rows. " + "Do NOT split a task into parallel agents unless its parts are GENUINELY INDEPENDENT.\n" + "• parallel(thunks): run agents concurrently with a BARRIER (await ALL). ONLY for INDEPENDENT fan-out whose " + "results you need together. If the parts share state, depend on each other, or just 'feel like multiple " + "things', it is NOT parallel. A flat independent fan-out IS parallel — not workflow (and if those agents " + "WRITE the same repo, it is isolation, below).\n" + "• pipeline(items, ...stages): each item flows through the stages on its own chain (no barrier). Use for " + "per-item STAGED processing of many items (e.g. for each file: lint → test → report).\n" + "• agent(spec, { isolation: \"worktree\" }): run the agent in an ISOLATED git worktree. Use specifically when " + "concurrent agents WRITE THE SAME FILES/REPO and must not clobber each other — even as a parallel fan-out " + "(plain parallel would clobber); this takes precedence over plain parallel. A separate-working-copy need, " + "NOT merely 'several agents'.\n" + "• resumeFromRunId (this tool's arg): RESUME a prior/crashed run — replay its completed agents, run only new work " + "(a run that could park must have finished — a crashed park-capable run is decided through the deployment's approval channel, not resumed).\n" + "• scriptPath / name (this tool's args): when a script store is wired, every run persists its script and " + "returns its scriptPath — to iterate, EDIT that file and re-invoke with {scriptPath} instead of resending " + "the full script; {name} runs a saved workflow.\n" + "• budget: SCALE the number/depth of agents to a token/cost CEILING — use whenever the task names a budget. " + "Guard budget-driven loops on budget.total: with no budget set, budget.remaining() returns Infinity and a " + "while-loop runs straight into the agent cap — add a hard iteration cap or pass a budget.\n" + "\n" + "Environment facts (read before writing the script):\n" + "• Sub-agents may run in an isolated working directory — when a task concerns specific files/repos, put " + "ABSOLUTE paths in the agent's objective; never assume it sees your cwd.\n" + "• The script itself has NO host APIs: no require/import, no process/fs/fetch, no setTimeout (schedule " + "time-based work outside the workflow). Only agent/parallel/pipeline/phase/log/budget/args exist.\n" + "• Scripts must be DETERMINISTIC: `Date.now()`/`Math.random()`/argless `new Date()` throw (they would " + "break resume) — pass timestamps in via `args`, stamp results after the workflow returns, and for " + "randomness vary the agent prompt/label by index.\n" + "• schema: if an agent's result is consumed by CODE or a downstream stage (a scorer, a file writer, the " + "next pipeline stage, your own JSON.parse), pass opts.schema to FORCE structured output — prose transfer " + "silently loses fields. Only results a human/LLM reads as narrative may omit schema. Standard JSON Schema " + 'applies, unions included — a nullable field is {"type":["string","null"]} (or anyOf). Cross-agent ' + "assertions belong in the SCRIPT body: compare/validate agent results in plain JS between stages.\n" + "• RETURN SHAPE (differs from Claude Code): agent() ALWAYS returns the full TaskResult — never the bare " + "validated object. With schema, the validated object rides `r.structuredOutput` (read fields as " + "`r.structuredOutput.bugs`, NOT `r.bugs`); `r.result` keeps the text form. A schema'd agent that " + "completes in prose THROWS (fail-loud) instead of returning undefined.\n" + "• Research/audit workflows: document and file claims are HYPOTHESES, not evidence — add a VERIFY " + "stage whose agents EXECUTE (run the command, the test, the reproducer) and cross-check claims " + "against actual output before the synthesis stage reports them as fact.\n" + "• r.terminal.kind: agent() does NOT throw when the sub-agent FAILS — it RETURNS the result with r.terminal " + "({ kind: 'completed' } on success; kind 'failed' (with r.terminal.code / r.terminal.message) / 'blocked' (r.terminal.reason) / 'paused' otherwise, " + "with any failure text also in r.result). ALWAYS check r.terminal.kind before building on a result, and GATE later phases on it — e.g. " + "`const impl = await agent({ objective: '…' }, { label: 'implement-fix' }); if (impl.terminal.kind !== 'completed') return { failed: 'implement', why: impl.result }` " + "— running later phases on a failed prerequisite burns tokens on garbage.\n" + "• phase (two forms, both fine): bare marker — phase('Scan'); groups every later agent() under 'Scan' " + "until the next phase() call. Scoped — await phase('Fix', async () => { ...agents... }); groups just the " + "body (equivalently, pass { phase: 'Fix' } in an agent's opts). Give each stage a phase so progress " + "renders as stage groups; use the same titles you declared in meta.phases.\n" + "• spec.modelName works only when the deployment configured a workflow model allowlist, and its legal " + "values are the names on THAT allowlist which the deployment's model CATALOG KEYS also hold — a name " + "off the allowlist, and a listed name the catalog lacks, are both refused rather than approximated. " + "Generic tier words ('opus'/'sonnet'/'haiku'/'pro'/'flash'/'best') are not a separate namespace: on a " + "deployment that configured tier bindings they become ordinary catalog keys, but only the ones its " + "bindings actually resolve; on one that did not they are simply absent, unless its catalog happens to " + "declare that literal name. Either way the allowlist is the gate, so a tier word is legal here only " + "when this deployment allowlisted it, never as a shortcut around it. " + "It also goes on the SPEC (the first argument), never in an agent()'s options object; `model` is accepted " + "there as an alias for the same field. If unsure, omit it and the agent runs on the deployment's default " + "role.\n" + "\n" + "Don't race. After launching, you know nothing about what the workflow found. Never fabricate or predict its " + "results in any format — not as prose, summary, or structured output. When a completion notifier is wired " + "(the launch note will say so) END YOUR TURN and wait — the notification carries the result; poll " + "TaskOutput(task_id) only when the user asks for progress. If the user asks a follow-up before it lands, " + "give status, not a guess." + (namedWorkflowSection !== undefined ? `\n\n${namedWorkflowSection}` : "") + sizeGuidelineSection, parameters: Type.Object({ script: Type.Optional(Type.String({ description: "Self-contained workflow script. Must begin with `export const meta = { name, description, phases }` " + "(pure literal, no computed values) followed by the script body using agent()/parallel()/pipeline()/phase(). " + "`phases` is an ARRAY OF OBJECTS — e.g. `phases: [{ title: 'Scan', detail: 'grep the logs' }, { title: 'Fix' }]` " + "(one entry per phase() call, titles matched exactly; `detail`/`model` optional; never bare strings).", })), name: Type.Optional(Type.String({ description: "Name of a predefined workflow (a saved script in the deployment's workflow store — see the " + "named-workflow list in this tool's description). Resolves to a self-contained script; the " + "registration's default args merge under your `args`.", })), scriptPath: Type.Optional(Type.String({ description: "Path to a workflow script file persisted by a prior invocation (each run's tool result carries its " + "scriptPath when the deployment wires a script store). To iterate, edit that file and re-invoke with the " + "same `scriptPath` instead of re-sending the full script. Takes precedence over `script` and `name`.", })), args: Type.Optional(Type.Unknown()), resumeFromRunId: Type.Optional(Type.String({ description: "design/97 CORE-9: resume a PRIOR run of the SAME script — replays the longest unchanged prefix of " + "its agent results from the journal and runs only the first changed/new call + everything after live. " + "Requires a journal-backed deployment; omit for a fresh run.", })), }), execute: async (rawArgs, ctx) => { const rawScript = rawArgs.script; const rawName = rawArgs.name; const rawScriptPath = rawArgs.scriptPath; const scriptArgs = rawArgs.args; const rawResume = rawArgs.resumeFromRunId; const resumeFromRunId = typeof rawResume === "string" && rawResume.length > 0 ? rawResume : undefined; const sourceTaskId = ctx.taskId ?? d.sourceTaskId; const hostSeat = parentSpecSeatOfCtx(ctx) ?? d.parentSpecSeat; const detachedSeat = hostSeat !== undefined ? stripSpawnTurnQuestionFace(hostSeat) : undefined; const parentSpecSeat = detachedSeat?.spec; const principal = parentSpecSeat?.principal; const taskScope = ctx.principal ?? d.scope; let script; let registeredDefaultArgs; let registeredStringArgKey; if (typeof rawScriptPath === "string" && rawScriptPath.length > 0) { if (!d.scriptStore) return structuredError("scriptPath requires a deployment workflow script store — none is wired; pass `script` inline"); if (d.scriptStore.scopePartitioned !== true) { return structuredError("the wired workflow script store predates the scope-partition contract (scopePartitioned marker absent) — upgrade the store implementation"); } try { script = await d.scriptStore.load(rawScriptPath, taskScope ?? "default"); } catch (err) { return structuredError(`failed to read workflow script at scriptPath: ${egressThrownText("same-principal", err).slice(0, 300)}`); } } else if (typeof rawScript === "string" && rawScript.length > 0) { script = stripCodeFences(rawScript); } else if (typeof rawName === "string" && rawName.length > 0) { if (!d.scriptStore?.resolveName) { return structuredError("named workflows require a deployment workflow script store — none is wired; pass `script` inline"); } let resolved; try { resolved = await d.scriptStore.resolveName(rawName); } catch (err) { return structuredError(`failed to resolve workflow name: ${egressThrownText("same-principal", err).slice(0, 300)}`); } const resolvedShape = resolved; if (resolvedShape !== undefined && (resolvedShape === null || typeof resolvedShape !== "object")) { return structuredError("the wired workflow script store's resolveName returned the retired bare-string form — the resolution shape is now { script, defaultArgs? } (one shape); upgrade the store implementation"); } if (resolved === undefined) return structuredError(`unknown workflow name: ${JSON.stringify(rawName)}`); script = resolved.script; if ("defaultArgs" in resolved) registeredDefaultArgs = resolved.defaultArgs; if (typeof resolved.stringArgKey === "string" && resolved.stringArgKey.length > 0) { registeredStringArgKey = resolved.stringArgKey; } } else { return structuredError("Must provide script, name, or scriptPath"); } const effectiveArgs = mergeWorkflowArgs(normalizeStringArg(scriptArgs, registeredDefaultArgs, registeredStringArgKey), registeredDefaultArgs); if (script.length > maxScriptChars) { return structuredError(`script too large: ${script.length} chars > max ${maxScriptChars}`); } let meta; try { meta = parseWorkflowMeta(script); } catch (err) { return structuredError(thrownText(err)); } try { const { body } = splitWorkflowMeta(script); new Function(`return (async function(agent, parallel, pipeline, phase, log, budget, args) {\n${body}\n})`); if (workflowScriptReadsClockOrRandom(body)) { return structuredError("Workflow scripts must be deterministic: Date.now()/Math.random()/new Date() are unavailable (breaks resume). " + "Stamp results after the workflow returns, or pass timestamps via args."); } } catch (err) { return structuredError(`workflow script failed to compile: ${thrownText(err)}`); } const hostOnAsk = ctx.onAsk ?? d.parentOnAsk; const hostDurableApproval = ctx.durableApprovalForChildren ?? d.parentDurableApproval; const runGovernance = hostOnAsk !== undefined && governance.baseline.base.onAsk === undefined ? { ...governance, baseline: { ...governance.baseline, base: { ...governance.baseline.base, onAsk: withDelegationProvenance(hostOnAsk, { parentToolCallId: ctx.toolCallId, depth: 1 }), }, }, } : governance; const childParkSeat = ctx.checkpointStoreForChildren ?? d.parentCheckpointStore; const parkStore = checkpointStoreOfSeat(childParkSeat); const scriptFn = (wfCtx) => { const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, childParkSeat, () => { const host = d.parentClamps?.(); assertClampRecord(host, "RunWorkflowToolDeps.parentClamps()", "clamp"); return foldClampSources(clampSourceOf(ctx), host); }); return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result); }; if (ctx.signal?.aborted) { return structuredError("the launching tool call was aborted before the workflow could start — not started"); } let handle; const workflowTaskId = d.taskRegistry?.mintTaskId("workflow"); const autoModeReview = ctx.autoModeReview ?? d.autoModeReview?.(); const centerFromCtx = ctx.centerArtifactDigest !== undefined; const parentCenterArtifactDigest = centerFromCtx ? ctx.centerArtifactDigest : d.parentCenterArtifactDigest?.(); const parentCenterSourceRevision = centerFromCtx ? ctx.centerSourceRevision : d.parentCenterSourceRevision?.(); try { handle = startWorkflow(d.runner, scriptFn, { store: d.store, ...(parkStore !== undefined ? { checkpointStore: parkStore } : {}), ...(d.agents !== undefined ? { agents: d.agents } : {}), ...(d.builtinAgents !== undefined ? { builtinAgents: d.builtinAgents } : {}), name: meta.name, ...(meta.description !== undefined ? { description: meta.description } : {}), ...(meta.phases !== undefined ? { phases: meta.phases } : {}), ...(effectiveArgs !== undefined ? { effectiveArgs } : {}), ...(sourceTaskId !== undefined ? { sourceTaskId } : {}), ...(d.originatingSessionId !== undefined ? { originatingSessionId: d.originatingSessionId } : {}), ...((ctx.rootSessionId ?? d.rootSessionId ?? d.originatingSessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? d.rootSessionId ?? d.originatingSessionId } : {}), ...((ctx.placementRoot ?? d.placementRoot) !== undefined ? { placementRoot: ctx.placementRoot ?? d.placementRoot } : {}), ...(workflowTaskId !== undefined ? { runId: workflowTaskId } : {}), journalStore: d.journalStore, ...(resumeFromRunId !== undefined ? { resumeFromRunId } : {}), ...(d.onBackgroundChildEvent !== undefined ? { onBackgroundChildEvent: d.onBackgroundChildEvent } : {}), scope: taskScope ?? d.scope, budget: lim.budget, maxAgents: lim.maxAgents ?? DEFAULTS.maxAgents, totalTimeoutMs: lim.totalTimeoutMs ?? DEFAULTS.totalTimeoutMs, maxResultChars: lim.maxResultChars ?? DEFAULTS.maxResultChars, maxLogChars: lim.maxLogChars ?? DEFAULTS.maxLogChars, ...(d.parentCwd !== undefined ? { parentCwd: d.parentCwd } : {}), parentToolCallId: ctx.toolCallId, ...(sourceTaskId !== undefined ? { parentTaskId: sourceTaskId } : {}), ...((ctx.interactionPosture ?? d.parentInteractionPosture) !== undefined ? { interactionPosture: (ctx.interactionPosture ?? d.parentInteractionPosture) } : {}), ...(() => { if ("memoryCaptureOptedOut" in ctx) { return { parentMemoryCaptureState: () => { const o = ctx.memoryCaptureOptedOut ?? false; const i = ctx.memoryCaptureIndeterminate ?? false; const build = (optedOut, indeterminate) => ({ optedOut: optedOut === true, indeterminate: indeterminate === true, ...(ctx.memoryCaptureControlDir !== undefined ? { controlDir: ctx.memoryCaptureControlDir } : {}), ...(ctx.memoryCaptureAncestors !== undefined ? { ancestors: ctx.memoryCaptureAncestors } : {}), }); return o instanceof Promise || i instanceof Promise ? Promise.all([o, i]).then(([ov, iv]) => build(ov, iv)) : build(o, i); }, }; } return d.parentMemoryCaptureState !== undefined ? { parentMemoryCaptureState: d.parentMemoryCaptureState } : {}; })(), ...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}), ...(d.parentThinking !== undefined ? { defaultThinking: d.parentThinking } : {}), ...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}), ...(parentSpecSeat !== undefined ? { parentSpecSeat } : {}), ...(detachedSeat?.internalsFlag.questionFaceStripped === true || ctx.questionFaceStripped === true || d.parentQuestionFaceStripped === true ? { questionFaceStripped: true } : {}), ...(hostDurableApproval !== undefined && d.store !== undefined && parkStore !== undefined ? { defaultDurableApproval: { ...hostDurableApproval } } : {}), ...(resumeFromRunId !== undefined && d.parkedResume !== undefined && d.parkedResume(resumeFromRunId) !== undefined ? { parkedResume: d.parkedResume(resumeFromRunId) } : {}), ...(parentCenterArtifactDigest !== undefined ? { parentCenterArtifactDigest } : {}), ...(parentCenterSourceRevision !== undefined ? { parentCenterSourceRevision } : {}), ...((ctx.forwardEvent ?? d.forwardEvent) !== undefined ? { onForwardEvent: (ctx.forwardEvent ?? d.forwardEvent) } : {}), ...((ctx.inheritedGateForChildren ?? d.inheritedGateForChildren) !== undefined ? { inheritedGate: (ctx.inheritedGateForChildren ?? d.inheritedGateForChildren)() } : {}), ...(autoModeReview !== undefined ? { autoModeReview } : {}), }, { workflowDepth: d.workflowDepth, ...((ctx.toolSelector?.layers ?? d.parentToolCeiling) !== undefined ? { toolCeiling: (ctx.toolSelector?.layers ?? d.parentToolCeiling) } : {}) }); } catch (err) { return structuredError(thrownText(err)); } const runId = handle.runId; let persistedScriptPath; if (d.scriptStore && d.scriptStore.scopePartitioned === true) { try { persistedScriptPath = await d.scriptStore.persist(runId, script, taskScope ?? "default"); } catch { } } if (d.taskRegistry) { const taskOwner = ctx.taskId ?? d.taskOwner ?? d.sourceTaskId; d.taskRegistry.registerWorkflow({ runId, handle, store: d.store, description: meta.name ?? meta.description, toolUseId: ctx.toolCallId, ...(taskOwner !== undefined ? { owner: taskOwner } : {}), ...(taskScope !== undefined ? { scope: taskScope } : {}), ...(d.originatingSessionId !== undefined ? { originatingSessionId: d.originatingSessionId } : {}), onServedTerminal: () => { void Promise.resolve() .then(() => d.notifier?.ackServed?.({ runId, ...(d.originatingSessionId !== undefined ? { originatingSessionId: d.originatingSessionId } : {}), ...(sourceTaskId !== undefined ? { sourceTaskId } : {}), ...(principal !== undefined ? { principal } : {}), })) .catch(() => { }); }, }); } { let notified = false; const fire = (notification) => { if (notified) return; notified = true; try { d.taskNotification?.(notification, { priority: "next" }); } catch { } if (!d.notifier) return; void Promise.resolve() .then(() => d.notifier.notify({ runId, task_id: notification.task_id, task_type: "workflow", ...(notification.toolUseId !== undefined ? { toolUseId: notification.toolUseId } : {}), sourceTaskId, ...(d.originatingSessionId !== undefined ? { originatingSessionId: d.originatingSessionId } : {}), principal, status: notification.status === "completed" ? "completed" : "failed", summary: egressText("human", notification.summary), ...(notification.result !== undefined ? { result: egressText("human", notification.result) } : {}), ...(notification.usage !== undefined ? { usage: notification.usage } : {}), ...(notification.diagnostics !== undefined ? { diagnostics: egressText("human", notification.diagnostics) } : {}), ...(notification.completionId !== undefined ? { completionId: notification.completionId } : {}), })) .catch(() => { }); }; handle.done .then((res) => fire({ task_id: runId, task_type: "workflow", ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}), status: "completed", summary: completionSummary(meta.name, res.run), result: boundedRedactedSummary(res.result, 4000), usage: workflowUsageBlock(res.run), diagnostics: workflowDiagnostics(runId, d.journalStore?.locator?.(runId, res.run.scope), res.run.resume?.journalEntries === 0), completionId: res.run.completionId, }), (err) => fire({ task_id: runId, task_type: "workflow", ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}), status: "failed", summary: failureSummary(err, runId), diagnostics: workflowDiagnostics(runId, undefined), ...(completionIdFromRejection(err) !== undefined ? { completionId: completionIdFromRejection(err) } : {}), })) .catch(() => { }); } return { details: { type: "workflow-run", status: "backgrounded", workflowRunId: runId, task_id: runId }, content: JSON.stringify({ runId, task_id: runId, status: "started", ...(persistedScriptPath !== undefined ? { scriptPath: persistedScriptPath } : {}), handling: `${launchReceiptNoQuoteClause(" (the ids above, and scriptPath when present)")} ${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE}`, note: (() => { const pollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}" })` : undefined; const blockingPollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}", block: true })` : undefined; const journalRef = d.journalStore?.locator?.(runId, taskScope ?? d.scope ?? ""); const carries = pollExpr ? ` A terminal ${pollExpr} reply carries its result AND per-agent rows — read them${journalRef !== undefined ? ` (full journal: ${journalRef})` : ""} before assuming an empty or unexpected result.` : ""; if (parentSpecSeat?.oneShot === true) { return blockingPollExpr ? `This is a ONE-SHOT submission (e.g. headless \`-p\`) — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: ${blockingPollExpr}. If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.${carries}` : "This is a one-shot submission (e.g. headless `-p`) and no poll path is available — the workflow's outcome may not be recoverable after this turn ends."; } if (d.notifier) { return `The workflow runs asynchronously. You will be NOTIFIED when it completes (the notification carries the result). End your turn and wait — do not poll unless the user asks for progress.${carries}`; } if (d.taskNotification) { return `The workflow runs asynchronously. Its completion is injected into THIS session if it is still active; if you have ended your turn and hear nothing, ${pollExpr ? `poll ${pollExpr}` : "there is no poll path wired"}.${carries}`; } return pollExpr ? `The workflow runs asynchronously and no completion notifier is wired. Poll ${pollExpr}.${carries}` : "The workflow runs asynchronously; no completion notifier is wired and no status/poll path is available."; })(), }), }; }, }); }