UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

754 lines (753 loc) • 32.9 kB
/** * Async delegation (N2) — spawn a background worker, collect it later, in any * order. * * Delegation through `registerAgentTool` is synchronous: the supervising * agent's loop blocks on the worker, so four investigations cost four times * one investigation and the supervisor sits idle while each runs. This module * keeps the same worker machinery and changes only WHEN the caller waits. * `spawnDelegate` records the job and returns a `workerId` immediately; * `collectDelegates` hands back whichever worker finished FIRST, which has * nothing to do with which was spawned first. * * Everything underneath already existed and is reused, not re-implemented: * * - `runIsolatedAgent` gives the worker a fresh session on a worker instance * that SHARES the host's tool registry (so live MCP connections are reused), * plus waste detection, honest stop reasons and continuation handles; * - the process-wide delegation pool in `agentToolRegistrar` bounds * concurrency — one pool, not a second one competing with the first; * - `bankArtifact` (N3) writes each worker's FULL report to a file, so the * conversation carries a bounded summary and a read-back call rather than * a report that has been truncated into uselessness; * - the checklist's `delegatesPending` / `delegatesReady` counters (N1) are * fed from here, which is how the model learns "a worker finished" from any * `tasks_list` — no polling loop, and no change to the core generate loop. * * @module agent/backgroundDelegation */ import { z } from "zod"; import { logger } from "../utils/logger.js"; import { DELEGATION_RESULT_CONTENT_CHARS, acquireDelegationSlot, raiseDelegationPoolCapacity, resolveDelegationDepth, tryAcquireDelegationSlot, } from "./agentToolRegistrar.js"; import { buildMechanicalDigest, runIsolatedAgent, } from "./isolatedAgentRunner.js"; import { resolveChecklistSessionId, setChecklistDelegateCountsSource, } from "./taskChecklist.js"; /** * Every outstanding job in the process, keyed by workerId. * * Module-level for the same reason the checklist is: a job outlives the tool * call that spawned it and must survive compaction, which rewrites messages * and cannot touch a module map. Claimed jobs are deleted on the spot, so this * holds only work that has not been accounted for. */ const jobs = new Map(); const hostSettings = new WeakMap(); /** * Default depth ceiling: a background worker does not spawn background * workers. Its own delegates would outlive it with nobody left to collect * them — an orphan that burns pool slots and is never read. */ const DEFAULT_DELEGATE_MAX_DEPTH = 1; /** How long a spawned worker waits for a pool slot before giving up. */ const DEFAULT_DELEGATE_POOL_QUEUE_TIMEOUT_MS = 120_000; /** How long a collect waits when the caller names no bound. */ const DEFAULT_COLLECT_WAIT_MS = 300_000; /** Grace period for cancelled workers to unwind before `cancelDelegates` returns. */ const CANCEL_SETTLE_GRACE_MS = 15_000; /** * Per-record evidence kept for a delegated worker (default is ~8 KB). * * Raised because these records are BANKED, not sent to a model: the banked * report is the run's evidence, and evidence that was cut at 8 KB before it * ever reached the file is evidence nobody can recover. */ const DELEGATE_CAPTURE_RESULT_CHARS = 100_000; /** Preview cut into the conversation from each banked report. */ const REPORT_PREVIEW_CHARS = 600; /** Longest label derived from a task string. */ const LABEL_MAX_CHARS = 60; let workerCounter = 0; let settleCounter = 0; let checklistCountsInstalled = false; // ── Small helpers ────────────────────────────────────────────────────────── function asRecord(value) { return value && typeof value === "object" ? value : undefined; } function errorMessage(error) { return error instanceof Error ? error.message : String(error); } /** Matches `agentToolRegistrar`'s convention: the recovery step is IN the text. */ function refusal(message) { return { isError: true, error: message }; } function bounded(text, maxChars) { return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text; } function firstLine(text, maxChars) { const line = text.split("\n", 1)[0]?.trim() ?? ""; return bounded(line, maxChars); } function jsonBlock(value) { try { return JSON.stringify(value, null, 2) ?? String(value); } catch (error) { return `[unserializable: ${errorMessage(error)}]`; } } /** A timer that never keeps the process alive for a collect nobody awaits. */ function afterMs(ms) { return new Promise((resolve) => { const timer = setTimeout(resolve, Math.max(0, ms)); timer.unref?.(); }); } function whenAborted(signal) { if (signal.aborted) { return Promise.resolve(); } return new Promise((resolve) => { signal.addEventListener("abort", () => resolve(), { once: true }); }); } function deferredOutcome() { let resolve = () => undefined; const promise = new Promise((settle) => { resolve = settle; }); return { promise, resolve }; } // ── Settings ─────────────────────────────────────────────────────────────── function settingsFor(host) { return (hostSettings.get(host) ?? { maxDepth: DEFAULT_DELEGATE_MAX_DEPTH, poolQueueTimeoutMs: DEFAULT_DELEGATE_POOL_QUEUE_TIMEOUT_MS, defaultCollectWaitMs: DEFAULT_COLLECT_WAIT_MS, }); } /** * Feed the task checklist's delegate counters, once per process. * * Keyed by session alone because that is the signature the checklist offers — * and the right key anyway: a checklist and the workers a run spawned belong * to the same session, which is exactly what `delegatesPending` reports. */ function installChecklistCounts() { if (checklistCountsInstalled) { return; } setChecklistDelegateCountsSource(countsForSession); checklistCountsInstalled = true; } /** * Apply registration options for one host: depth ceiling, queue wait, and a * raise (never a lowering) of the shared delegation pool. */ export function configureDelegation(host, options = {}) { const settings = { maxDepth: options.maxDepth ?? DEFAULT_DELEGATE_MAX_DEPTH, poolQueueTimeoutMs: options.poolQueueTimeoutMs ?? DEFAULT_DELEGATE_POOL_QUEUE_TIMEOUT_MS, defaultCollectWaitMs: DEFAULT_COLLECT_WAIT_MS, ...(options.spawnDefaults !== undefined && { spawnDefaults: options.spawnDefaults, }), }; hostSettings.set(host, settings); if (options.maxConcurrent !== undefined) { raiseDelegationPoolCapacity(options.maxConcurrent); } installChecklistCounts(); return settings; } // ── Counting ─────────────────────────────────────────────────────────────── function isReady(job) { return job.phase === "ready" && job.outcome !== undefined; } function outstandingFor(host, sessionId) { return [...jobs.values()].filter((job) => job.host === host && job.sessionId === sessionId && job.phase !== "claimed"); } function tally(candidates) { let pending = 0; let ready = 0; for (const job of candidates) { if (isReady(job)) { ready += 1; } else { pending += 1; } } return { pending, ready }; } /** Counts across every host for one session — what the checklist reads. */ function countsForSession(sessionId) { return tally([...jobs.values()].filter((job) => job.sessionId === sessionId && job.phase !== "claimed")); } /** * Outstanding workers for a host's session: `pending` are still running or * queued, `ready` finished and are waiting to be claimed. This is what feeds * every `ChecklistToolResult`, so the model learns a worker landed from any * `tasks_list` call. */ export function delegateCounts(host, sessionId) { const session = sessionId ?? resolveChecklistSessionId(host); return tally(outstandingFor(host, session)); } // ── Report assembly ──────────────────────────────────────────────────────── function renderRecords(records) { if (records.length === 0) { return "(no tools were called)"; } return records .map((record, index) => `[${index + 1}] ${record.toolName}(${jsonBlock(record.params)}) → ` + `${record.isError ? "ERROR" : "ok"} (${record.durationMs}ms)\n` + record.resultText) .join("\n\n"); } /** * The worker's complete report, as it goes to disk. * * Everything the run produced is here — narrative, structured data, a digest, * and every tool execution record in full. The bounded thing is the SUMMARY * that goes into the conversation; this is the thing the summary points at, so * cutting it would defeat the point of banking it. */ function buildReportBody(job, outcome) { const header = [ `workerId: ${job.workerId}`, `label: ${job.label}`, `status: ${outcome.status}`, outcome.stopReason ? `stopReason: ${outcome.stopReason}` : "", `durationMs: ${outcome.durationMs}`, `toolCalls: ${outcome.toolExecutions.length}`, outcome.handle ? `continuationHandle: ${outcome.handle}` : "", ] .filter(Boolean) .join("\n"); return [ "# Delegated worker report", header, `## Task\n${job.task}`, `## Report\n${outcome.content?.trim() || "(the worker produced no narrative)"}`, outcome.data !== undefined ? `## Structured data\n${jsonBlock(outcome.data)}` : "", outcome.wasteSignals?.length ? `## Waste signals\n${outcome.wasteSignals.join("\n")}` : "", outcome.extractionError ? `## Extraction errors\n${outcome.extractionError}` : "", `## Tool execution digest\n${jsonBlock(buildMechanicalDigest(outcome.toolExecutions))}`, `## Tool executions (${outcome.toolExecutions.length}, complete)\n${renderRecords(outcome.toolExecutions)}`, ] .filter(Boolean) .join("\n\n"); } /** * Bank the report and hand back the pointer. * * A banking failure is reported in the reference rather than thrown: losing * the file must not also lose the worker's outcome, and the caller is told in * so many words that the read-back is not available and why. */ async function bankReport(job, body) { const label = `delegate:${job.label}`; try { return await job.host.bankArtifact(body, { kind: "worker-report", label, sessionId: job.sessionId, previewChars: REPORT_PREVIEW_CHARS, }); } catch (error) { const message = errorMessage(error); logger.warn("[BackgroundDelegation] Banking the worker report failed", { workerId: job.workerId, error: message, }); return { artifactId: "", label, kind: "worker-report", sizeBytes: Buffer.byteLength(body, "utf-8"), preview: bounded(body, REPORT_PREVIEW_CHARS), readBackHint: `The full report could NOT be banked (${message}), so there is nothing to ` + "read back — what survived is this preview. Re-run the task if you need the rest.", }; } } // ── Settling ─────────────────────────────────────────────────────────────── function markSettled(job, outcome) { job.outcome = outcome; job.settledOrder = ++settleCounter; job.phase = "ready"; logger.debug("[BackgroundDelegation] Worker settled", { workerId: job.workerId, status: outcome.status, durationMs: outcome.durationMs, artifactId: outcome.report.artifactId, }); return outcome; } async function settleRun(job, outcome, startedAt) { const report = await bankReport(job, buildReportBody(job, outcome)); const summary = outcome.content?.trim() || `Worker ${job.workerId} finished with status "${outcome.status}" and ` + `${outcome.toolExecutions.length} tool calls but wrote no narrative; ` + "the banked report holds the evidence."; return markSettled(job, { workerId: job.workerId, label: job.label, status: outcome.status, ok: outcome.status === "completed" || outcome.status === "partial", summary: bounded(summary, DELEGATION_RESULT_CONTENT_CHARS), report, durationMs: Date.now() - startedAt, toolCallsUsed: outcome.toolExecutions.length, ...(outcome.wasteSignals?.length && { wasteSignals: outcome.wasteSignals }), ...(outcome.handle && { handle: outcome.handle }), // A cancelled worker still runs to a real outcome (the runner unwinds and // reports what it had). Say WHY it is short, or the supervisor reads a // truncated investigation as a completed one. ...(job.cancelled && { error: `Worker ${job.workerId} was cancelled by the caller.`, }), }); } /** * A worker that never produced an outcome — cancelled, refused a pool slot, or * broken. It still settles into a claimable outcome with a banked report: a * failure the supervisor cannot see is worse than a failure it can. */ async function settleFailure(job, reason, startedAt) { const body = [ "# Delegated worker report", `workerId: ${job.workerId}\nlabel: ${job.label}\nstatus: error`, `## Task\n${job.task}`, `## Failure\n${reason}`, ].join("\n\n"); const report = await bankReport(job, body); return markSettled(job, { workerId: job.workerId, label: job.label, status: "error", ok: false, summary: bounded(reason, DELEGATION_RESULT_CONTENT_CHARS), report, durationMs: Date.now() - startedAt, toolCallsUsed: 0, error: reason, }); } // ── Running one job ──────────────────────────────────────────────────────── function buildDefinition(job, options) { const instructions = [ "You are a background worker. A supervising agent delegated exactly one task to you " + "and is doing other work while you run.", "Use your tools to gather real evidence, then finish with a SELF-CONTAINED report: " + "what you did, what you found, and the evidence for each finding. The supervisor " + "cannot see your tool calls — whatever is not in your report did not happen.", "Stay inside your task. Do not start adjacent work you were not asked for.", options.scope ? `Scope — what you may look at:\n${options.scope}` : "", options.context ? `Context from the supervisor:\n${options.context}` : "", ] .filter(Boolean) .join("\n\n"); return { id: `delegate-${job.workerId}`, name: job.label, description: `Background worker: ${firstLine(job.task, LABEL_MAX_CHARS)}`, instructions, ...(options.provider && { provider: options.provider }), ...(options.model && { model: options.model }), ...(options.tools?.length && { tools: options.tools }), ...(options.maxSteps !== undefined && { maxSteps: options.maxSteps }), }; } function buildRunOptions(job, options) { const overrides = { ...(options.budgetMs !== undefined && { turnTimeoutMs: options.budgetMs }), ...(options.maxSteps !== undefined && { maxSteps: options.maxSteps }), }; return { abortSignal: job.controller.signal, // Depth travels so a worker's own delegation attempts hit the ceiling. // The caller's sessionId deliberately does NOT: the worker gets its own // session (runIsolatedAgent stamps the run id), so its checklist and its // delegate counters stay separate from the supervisor's. toolContext: { agentDepth: job.depth + 1, delegateWorkerId: job.workerId, }, capture: { maxResultChars: DELEGATE_CAPTURE_RESULT_CHARS }, ...(Object.keys(overrides).length > 0 && { overrides }), }; } /** * The detached body of one delegation. Never rejects: every path ends in a * claimable outcome, because a job that vanishes is a job the supervisor waits * on forever. */ async function runJob(job, options, settings, preAcquired) { const startedAt = Date.now(); let release = preAcquired; try { if (!release) { const queued = acquireDelegationSlot(settings.poolQueueTimeoutMs); const raced = await Promise.race([ queued.then((grant) => ({ kind: "granted", grant }), (error) => ({ kind: "queue-timeout", error })), whenAborted(job.controller.signal).then(() => ({ kind: "aborted", })), ]); if (raced.kind === "granted") { release = raced.grant; } else { // Whatever we stopped waiting for may still be granted — hand it // straight back rather than leaking a slot nobody will ever use. void queued.then((grant) => grant()).catch(() => undefined); return raced.kind === "aborted" ? await settleFailure(job, `Worker ${job.workerId} was cancelled while waiting for a delegation slot.`, startedAt) : await settleFailure(job, `Worker ${job.workerId} never got a delegation slot: ${errorMessage(raced.error)}. The pool was saturated for ${settings.poolQueueTimeoutMs}ms.`, startedAt); } } if (job.controller.signal.aborted) { return await settleFailure(job, `Worker ${job.workerId} was cancelled before it started.`, startedAt); } job.phase = "running"; const outcome = await runIsolatedAgent(job.host, buildDefinition(job, options), job.task, buildRunOptions(job, options)); return await settleRun(job, outcome, startedAt); } catch (error) { return await settleFailure(job, job.cancelled ? `Worker ${job.workerId} was cancelled: ${errorMessage(error)}` : `Worker ${job.workerId} failed: ${errorMessage(error)}`, startedAt); } finally { release?.(); job.detachParent?.(); } } // ── Public API ───────────────────────────────────────────────────────────── function depthRefusal(depth, maxDepth) { return (`Delegation depth limit reached (${depth}/${maxDepth}). Complete this investigation ` + "yourself with your own tools instead of delegating further."); } /** * Start a background worker and return its handle immediately — before the * pool is waited on, and long before the worker finishes. * * The worker runs through `runIsolatedAgent`, so it gets a fresh session on a * worker instance sharing this host's tool registry (live MCP connections * included), waste detection, and an honest stop reason. Its full report is * banked to a file when it settles; `collectDelegates` hands back the bounded * summary and the pointer. * * @throws when the task is empty or the caller is already at the depth ceiling */ export async function spawnDelegate(host, options) { const task = options.task?.trim() ?? ""; if (!task) { throw new Error("delegate_task needs a task: state, in one or two sentences, exactly what the " + "worker should investigate and what it should report back."); } const settings = settingsFor(host); const depth = options.depth ?? resolveDelegationDepth(host); if (depth >= settings.maxDepth) { throw new Error(depthRefusal(depth, settings.maxDepth)); } installChecklistCounts(); workerCounter += 1; const workerId = `w${workerCounter}`; const controller = new AbortController(); // Chain the parent, and remember how to unchain: one long-lived run signal // with many delegates hung off it would otherwise accumulate a listener per // worker for the life of the run. let detachParent; const parentSignal = options.abortSignal; if (parentSignal) { if (parentSignal.aborted) { controller.abort(); } else { const onParentAbort = () => controller.abort(); parentSignal.addEventListener("abort", onParentAbort, { once: true }); detachParent = () => parentSignal.removeEventListener("abort", onParentAbort); } } // Ask for a slot without queueing, so the handle can say truthfully whether // this worker is running or waiting. const immediate = tryAcquireDelegationSlot(); const settled = deferredOutcome(); const job = { workerId, host, sessionId: options.sessionId ?? resolveChecklistSessionId(host), label: options.label?.trim() || firstLine(task, LABEL_MAX_CHARS) || workerId, task, depth, phase: immediate ? "running" : "queued", spawnedAt: Date.now(), settledOrder: 0, controller, ...(detachParent && { detachParent }), settled: settled.promise, cancelled: false, }; jobs.set(workerId, job); // Detached on purpose: this is the whole point of the primitive. void runJob(job, options, settings, immediate).then(settled.resolve); logger.debug("[BackgroundDelegation] Worker spawned", { workerId, sessionId: job.sessionId, queued: immediate === undefined, depth, }); return { workerId, spawnedAt: job.spawnedAt, queued: immediate === undefined, }; } function claim(job) { const outcome = job.outcome; if (!outcome) { return undefined; } job.phase = "claimed"; jobs.delete(job.workerId); return outcome; } function bySettleOrder(left, right) { return left.settledOrder - right.settledOrder; } /** * Claim finished workers. * * `{ mode: "any" }` returns the first worker to FINISH — spawn order is * irrelevant. `{ mode: "all" }` returns every outstanding worker in completion * order. `{ workerId }` waits for one named worker. Each outcome is claimed * exactly once and then dropped, so two collects never report the same work * twice. * * `waitMs: 0` polls (whatever is ready right now); omitting it waits up to the * runtime default. `timedOut` says work was still outstanding when the call * returned — the signal to come back later, not an error. * * @throws when a named workerId is unknown to this host and session */ export async function collectDelegates(host, request) { const settings = settingsFor(host); const sessionId = request.sessionId ?? resolveChecklistSessionId(host); const waitMs = request.waitMs ?? settings.defaultCollectWaitMs; if ("workerId" in request) { const job = jobs.get(request.workerId); if (!job || job.host !== host || job.sessionId !== sessionId) { const known = outstandingFor(host, sessionId).map((j) => j.workerId); throw new Error(known.length > 0 ? `No outstanding worker "${request.workerId}". Outstanding workers are ${known.join(", ")} — collect one of those, or use { mode: "any" }.` : `No outstanding worker "${request.workerId}": every worker has already been ` + "collected. Do not collect again; work with the results you have."); } if (!isReady(job)) { await Promise.race([job.settled, afterMs(waitMs)]); } const outcome = isReady(job) ? claim(job) : undefined; const counts = tally(outstandingFor(host, sessionId)); return { completed: outcome ? [outcome] : [], ...counts, timedOut: outcome === undefined, }; } const mode = request.mode; let ready = outstandingFor(host, sessionId).filter(isReady); if (mode === "any") { if (ready.length === 0) { const unsettled = outstandingFor(host, sessionId).filter((job) => !isReady(job)); if (unsettled.length > 0) { await Promise.race([ ...unsettled.map((job) => job.settled), afterMs(waitMs), ]); ready = outstandingFor(host, sessionId).filter(isReady); } } const first = [...ready].sort(bySettleOrder)[0]; const outcome = first ? claim(first) : undefined; const counts = tally(outstandingFor(host, sessionId)); return { completed: outcome ? [outcome] : [], ...counts, timedOut: outcome === undefined && counts.pending > 0, }; } const unsettled = outstandingFor(host, sessionId).filter((job) => !isReady(job)); if (unsettled.length > 0) { await Promise.race([ Promise.all(unsettled.map((job) => job.settled)), afterMs(waitMs), ]); } const completed = outstandingFor(host, sessionId) .filter(isReady) .sort(bySettleOrder) .map(claim) .filter((outcome) => outcome !== undefined); const counts = tally(outstandingFor(host, sessionId)); return { completed, ...counts, timedOut: counts.pending > 0 }; } /** * Cancel background workers: one by id, or every outstanding worker this host * spawned. Cancelled workers still settle into a claimable outcome saying they * were cancelled — silence would leave the supervisor waiting on a worker that * is never coming back. * * @returns how many workers were cancelled */ export async function cancelDelegates(host, workerId) { const targets = [...jobs.values()].filter((job) => job.host === host && job.phase !== "claimed" && !job.controller.signal.aborted && (workerId === undefined || job.workerId === workerId)); for (const job of targets) { job.cancelled = true; job.controller.abort(); } if (targets.length > 0) { // Bounded: a worker that ignores its abort must not hang the caller. await Promise.race([ Promise.all(targets.map((job) => job.settled)), afterMs(CANCEL_SETTLE_GRACE_MS), ]); } logger.debug("[BackgroundDelegation] Workers cancelled", { cancelled: targets.length, ...(workerId && { workerId }), }); return targets.length; } // ── Model-facing tools ───────────────────────────────────────────────────── const SPAWN_SCHEMA = z.object({ task: z .string() .describe("Exactly what this worker must investigate and report back. Self-contained: " + "the worker cannot see your conversation."), scope: z .string() .optional() .describe("What the worker may look at — files, directories, systems."), context: z .string() .optional() .describe("A brief slice of background the worker needs. Keep it short; do not paste whole documents."), tools: z .array(z.string()) .optional() .describe("Restrict the worker to these tool names."), model: z.string().optional().describe("Model override for this worker."), }); const COLLECT_SCHEMA = z.object({ mode: z .enum(["any", "all"]) .optional() .describe('"any" (default) returns the first worker to finish; "all" waits for every outstanding worker.'), workerId: z .string() .optional() .describe("Collect one specific worker instead of using mode."), waitMs: z .number() .optional() .describe("How long to wait, in milliseconds. 0 returns only what is already finished."), }); /** * `delegate_task` and `collect_results`, bound to `host`. Register them with * `host.registerTool()` (see `NeuroLink.registerDelegationTools()`), never on * the tool registry directly: only the "user-defined" category reaches the * LLM's tool schema. */ export function createDelegationTools(host) { return { delegate_task: { name: "delegate_task", description: "Hand one self-contained task to a background worker and get a workerId back " + "IMMEDIATELY — the worker runs while you keep working. Delegate the big, " + "separable investigations; do the small ones yourself. Collect the results later " + "with collect_results; they come back in whatever order the workers finish.", inputSchema: SPAWN_SCHEMA, execute: async (params, executionContext) => { const parsed = SPAWN_SCHEMA.safeParse(params ?? {}); if (!parsed.success) { return refusal("delegate_task expects { task, scope?, context?, tools?, model? } with task a " + "non-empty string. Call it again with the task spelled out."); } const contextRecord = asRecord(executionContext); const sessionId = resolveChecklistSessionId(host, executionContext); // Registration-time defaults under the model's arguments: the model's own // `model` wins, `provider` can only come from here (the schema has none). const defaults = settingsFor(host).spawnDefaults; try { const handle = await spawnDelegate(host, { ...(defaults?.provider !== undefined && { provider: defaults.provider, }), ...(defaults?.model !== undefined && { model: defaults.model }), ...parsed.data, sessionId, depth: resolveDelegationDepth(host, contextRecord), }); const counts = delegateCounts(host, sessionId); const result = { ...handle, pending: counts.pending, ready: counts.ready, }; return result; } catch (error) { return refusal(errorMessage(error)); } }, }, collect_results: { name: "collect_results", description: "Claim finished background workers. Each result carries a bounded summary and a " + "read-back call for the worker's FULL report, which was banked to a file. Results " + "are returned in completion order, not the order you spawned them, and each one is " + "handed out exactly once. pending/ready tell you what is still outstanding.", inputSchema: COLLECT_SCHEMA, execute: async (params, executionContext) => { const parsed = COLLECT_SCHEMA.safeParse(params ?? {}); if (!parsed.success) { return refusal('collect_results expects { mode?: "any" | "all", workerId?, waitMs? }. ' + "Call it again with no arguments to take the next finished worker."); } const sessionId = resolveChecklistSessionId(host, executionContext); const { mode, workerId, waitMs } = parsed.data; try { return await collectDelegates(host, workerId ? { workerId, sessionId, ...(waitMs !== undefined && { waitMs }), } : { mode: mode ?? "any", sessionId, ...(waitMs !== undefined && { waitMs }), }); } catch (error) { return refusal(errorMessage(error)); } }, }, }; }