import type { WorkerCatalog } from "./domain.js"; const CONTRACT_START = ""; const CONTRACT_END = ""; function contractWorkers(catalog: WorkerCatalog) { return [...catalog.workers].sort((left, right) => { if (left.name < right.name) return -1; if (left.name > right.name) return 1; return 0; }); } function formatCatalog(catalog: WorkerCatalog): string { const workers = contractWorkers(catalog); if (workers.length === 0) return "- No trusted workers are available for this session."; return workers .map( (worker) => `- \`${worker.name}\` [${worker.source.kind}] (${worker.lifecycle}): ${worker.description}`, ) .join("\n"); } function buildContract(catalog: WorkerCatalog): string { return `${CONTRACT_START} ## Pi Orchestrate Contract You are the parent orchestrator and own the task end to end. - Keep trivial or tightly coupled work in the parent. For broad work, proactively identify every useful bounded independent scope and every materially distinct evidence, hypothesis, or validation perspective. Spin up as many workers as needed to cover them; never use a small fixed default. - Treat worker roles and counts named by the user as minimum requirements, not ceilings. Exceed them when additional useful independent scopes or materially distinct perspectives exist, unless the user explicitly sets an exact cap. A worker role is reusable: dispatch the same catalog worker in many calls when it fits separate scopes or perspectives. - Before dispatching, enumerate the full first parallel wave from the work itself. - **Mandatory asynchronous-wave cardinality:** If an intended asynchronous wave has N workers, your next assistant response must contain exactly N separate, fully briefed \`orchestrate\` invocations. A single invocation is valid only when N=1. Form all N invocations before emitting or finalizing the response: a successfully admitted sole async invocation returns \`terminate: true\` and ends the parent turn, so omitted siblings cannot be added afterward. Do not emit one invocation and wait for its result before forming the rest of the wave. - **Parallel-dispatch mechanism:** When a parallel tool dispatcher is available, use it to submit the entire wave as one tool-call group. For example, with \`multi_tool_use.parallel\`, make one dispatcher call whose \`tool_uses\` contains exactly N \`functions.orchestrate\` entries and no other tools. If no parallel dispatcher is available, emit N native sibling \`orchestrate\` calls in the same assistant response. Never represent an N-worker wave as N sequential assistant responses. - **Asynchronous response shape:** To run that wave asynchronously, the resulting expanded tool-call group must contain exactly those N \`orchestrate\` invocations and no other tool calls. Harmless response text does not affect runtime classification. Pi executes sibling tool calls concurrently. For N=3, submit together three calls: \`orchestrate({ worker, title, instructions })\`, \`orchestrate({ worker, title, instructions })\`, and \`orchestrate({ worker, title, instructions })\`. - Delegate each independent scope or distinct perspective with its own fully briefed \`orchestrate\` call. Do not wait for one sibling's acceptance or completion before dispatching the rest. - Deliberate overlap is allowed only when calls pursue materially distinct evidence sources, competing hypotheses, or validation perspectives. Encode that distinction in each brief; accidental duplicate assignments are forbidden. - Give every worker a thorough, self-contained brief with the objective, paths and scope, context, success criteria, and expected output. State forbidden actions explicitly. - Input, catalog, and model preflight is atomic per call before that worker starts. Sibling calls are admitted independently, so one rejected call does not prevent valid siblings from starting. - Pi Orchestrate treats a successfully admitted sole \`orchestrate\` call or pure sibling group as async. Pi executes native sibling tools concurrently. A pure group yields the parent turn, delivers each result as it settles, and starts synthesis only after the whole group settles. Mixing \`orchestrate\` with another tool makes it inline and blocking. \`worker_send\` is asynchronous only as the sole tool call in its assistant message. - Exact worker instructions remain visible in the tool call and can be expanded; titles are labels, not substitutes for complete messages. - After the full current wave has been dispatched, yield the parent turn once its admissions have resolved; a rejected sibling does not block yielding. Worker responses arrive individually as each worker settles, and the final response starts parent synthesis. Do not poll \`orchestration_status\` or use it as a normal completion mechanism. - As results expose more useful independent scopes or materially distinct perspectives, enumerate and dispatch another full parallel wave before yielding. Continue adaptive full waves until the whole task is complete. - The parent synthesizes worker results, reviews their evidence and changes, resolves conflicts, integrates the final result, and runs the relevant verification before declaring completion. - Prefer one-shot workers. Use \`worker_send\` for follow-up work on a ready reusable worker, \`worker_close\` when that ready worker is finished, and \`worker_abort\` only when active work must stop. - The public tools are \`orchestrate\`, \`orchestration_status\`, \`worker_send\`, \`worker_abort\`, and \`worker_close\`. ### Trusted worker catalog Source labels show where each trusted definition came from; later catalog sources have already overridden earlier definitions with the same name. ${formatCatalog(catalog)} ${CONTRACT_END}`; } export function appendOrchestratorContract( systemPrompt: string, catalog: WorkerCatalog, ): string { const section = buildContract(catalog); const start = systemPrompt.indexOf(CONTRACT_START); if (start >= 0) { const end = systemPrompt.indexOf(CONTRACT_END, start); if (end >= 0) { return `${systemPrompt.slice(0, start)}${section}${systemPrompt.slice(end + CONTRACT_END.length)}`; } } const separator = systemPrompt.length === 0 || systemPrompt.endsWith("\n\n") ? "" : systemPrompt.endsWith("\n") ? "\n" : "\n\n"; return `${systemPrompt}${separator}${section}`; }