151 lines
10 KiB
Swift
151 lines
10 KiB
Swift
import Foundation
|
|
|
|
/// "Orchestra" — Nucleic's orchestration mode, modeled on Claude Code's ultracode mode.
|
|
///
|
|
/// Orchestra is *not* an API effort level. It pairs the documented `xhigh` effort with
|
|
/// standing consent for the agent to fan work out through Nucleic-managed worker sessions.
|
|
/// The user selects it in the effort menu like any other level, but it never reaches a
|
|
/// backend verbatim:
|
|
///
|
|
/// - `resolvedEffort` maps the `orchestra` sentinel to its real effort, `xhigh`, so the
|
|
/// `claude --effort` (and Codex) flag only ever sees a level the API accepts.
|
|
/// - `combinedSystemPrompt` rides the standing-consent text in on `--append-system-prompt`.
|
|
/// The CLI doesn't persist that flag, so `SessionController` re-applies it every turn —
|
|
/// which is exactly the per-turn "mode reminder" the orchestration design calls for, so
|
|
/// no separate refresher counter is needed here.
|
|
///
|
|
/// Keeping the sentinel, the effort mapping, and the consent text in one place lets the UI,
|
|
/// the core, and the tests agree on what "orchestra" means.
|
|
public enum OrchestrationMode {
|
|
/// The sentinel stored in `Session.effort` when the user picks Orchestra. Chosen so it
|
|
/// never collides with a real API effort level and is obvious in the persisted store.
|
|
public static let effortSentinel = "orchestra"
|
|
|
|
/// The mode's former sentinel, from before it was renamed from "Ultracode". Still
|
|
/// recognized so a session persisted (or in flight from an older remote) under the old
|
|
/// name keeps resolving to the mode rather than being sent to a backend verbatim.
|
|
public static let legacyEffortSentinel = "ultracode"
|
|
|
|
/// The real effort level orchestra runs at. The xhigh ceiling is the most thorough the
|
|
/// API documents; orchestra adds the fan-out consent on top, not a hidden higher level.
|
|
public static let resolvedOrchestraEffort = "xhigh"
|
|
|
|
/// Whether `effort` selects orchestra (case-insensitive, whitespace-tolerant). Accepts the
|
|
/// legacy "ultracode" token too, so the rename can't strand an already-stored selection.
|
|
public static func isOrchestra(_ effort: String?) -> Bool {
|
|
let normalized = effort?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return normalized == effortSentinel || normalized == legacyEffortSentinel
|
|
}
|
|
|
|
/// Which side of an Orchestra session a run is: the user-started chat is the `supervisor` (it
|
|
/// dispatches and drives workers); a machine-spawned `nucleic_subagent` session is a `worker`
|
|
/// (it executes one subtask and reports back). Selects which standing prompt rides in on
|
|
/// `--append-system-prompt` — the supervisor gets ``consentPrompt``, the worker
|
|
/// ``workerBriefing``. `nil` (the default) means neither: an ordinary, non-Orchestra run.
|
|
public enum Role: Sendable, Equatable {
|
|
case supervisor
|
|
case worker
|
|
}
|
|
|
|
/// The effort actually handed to a backend: `orchestra` resolves to `xhigh`; everything
|
|
/// else passes through untouched. One-way — the resolved value never flows back into
|
|
/// `Session.effort`, so the UI keeps showing "Orchestra".
|
|
public static func resolvedEffort(_ effort: String?) -> String? {
|
|
isOrchestra(effort) ? resolvedOrchestraEffort : effort
|
|
}
|
|
|
|
/// The standing prompt appended for the **supervisor** while Orchestra is on. It frames the
|
|
/// agent's role — delegate, don't do — grants blanket fan-out consent, and lays out the
|
|
/// non-blocking spawn + `nucleic_supervise` drive loop plus the verification patterns to reach
|
|
/// for. Re-sent every turn by the caller, so it doubles as the mode's recurring reminder.
|
|
///
|
|
/// Load-bearing details: Orchestra routes worker creation through Nucleic's MCP tools so the app
|
|
/// can manage the worker lifecycle and enforce the configured model, and the model's most common
|
|
/// failure is to *narrate* a fan-out in prose without emitting a real tool call — so the prompt
|
|
/// names both directly. `nucleic_subagent` is non-blocking (it returns a `worker_id` immediately),
|
|
/// so the supervisor must actively drive the run with `nucleic_supervise` rather than assume a
|
|
/// spawn call blocks until the worker is done.
|
|
public static let consentPrompt = """
|
|
Orchestra (orchestration mode) is on. Your role this session is SUPERVISOR, not \
|
|
implementer: plan the work, delegate every substantive task to Nucleic-managed worker \
|
|
subagents, drive them to completion, and synthesize their results — do not write code, \
|
|
edit files, or carry out the work yourself. Standing consent: you may fan work out to \
|
|
parallel workers through Nucleic's tools on every substantive task without asking first. \
|
|
Optimize for the most exhaustive, correct answer rather than the fastest one.
|
|
|
|
Delegate, don't do. On a substantive turn, scout the task yourself, decompose it into \
|
|
independent subtasks sized to the problem's natural structure, and dispatch each as a \
|
|
worker with `nucleic_subagent` (a short task label plus complete, self-contained \
|
|
instructions). Spawning is non-blocking: each call returns a `worker_id` immediately \
|
|
while the worker runs. Do the hands-on work yourself only when a worker genuinely cannot \
|
|
— a one-line reply, or a step no subagent can perform. Never do work a worker could do \
|
|
just because it seems quick.
|
|
|
|
Drive the workers with the supervise loop. After dispatching, call `nucleic_supervise` — \
|
|
it blocks until workers report events and returns them along with how many are still \
|
|
running. Handle each event: answer a worker's question with `nucleic_reply_to_worker` \
|
|
(the worker_id plus your reply); read a completed worker's output and re-dispatch \
|
|
follow-on work between phases; note failures and retry or adjust. Keep calling \
|
|
`nucleic_supervise` until no workers remain running. When a decision genuinely needs the \
|
|
human — a product choice, an ambiguous requirement, an irreversible action — ask them \
|
|
directly with `AskUserQuestion`; you are the only agent that can reach the user, and \
|
|
workers route such questions up to you.
|
|
|
|
Make the fan-out real, not narrated. Never say you are "delegating", "spawning workers", \
|
|
or working "in parallel" unless you have actually issued the corresponding \
|
|
`nucleic_subagent` calls this turn. Do not use native Task/Agent spawn tools; \
|
|
Nucleic-managed workers are required so the app can observe their lifecycle and enforce \
|
|
the user's worker-model choice. If a turn is conversational or trivial enough that \
|
|
workers would not help, say so plainly and handle it solo; do not announce an \
|
|
orchestration you are not performing.
|
|
|
|
Lean on the quality patterns that fit: adversarial verification (a second wave that \
|
|
tries to refute the first wave's findings against the source), a completeness critic \
|
|
(one worker hunting for what the others missed), and multi-phase sequencing (understand, \
|
|
design, implement, review as separate fan-outs). The fan-out multiplies token usage, so \
|
|
keep each subtask scoped to a distinct concern rather than splitting per line or file.
|
|
"""
|
|
|
|
/// The standing prompt appended for a **worker** subagent. It tells the worker what it is (one
|
|
/// leaf of a supervisor's fan-out), how to return its result (a single self-contained final
|
|
/// reply — that reply *is* what the supervisor reads), and how to reach back up: workers can't
|
|
/// talk to the human, so anything needing a decision or unblocking goes to the supervisor via
|
|
/// `nucleic_ask_supervisor`, never `AskUserQuestion`. Delivered on `--append-system-prompt` at
|
|
/// worker creation, the worker-side counterpart of ``consentPrompt``.
|
|
public static let workerBriefing = """
|
|
You are a Nucleic-managed worker subagent in an Orchestra (orchestration) session. A \
|
|
supervisor agent decomposed a larger task and dispatched this subtask to you; other \
|
|
workers may be running in parallel on sibling subtasks. Carry out the task described in \
|
|
your prompt completely and autonomously, then return a single, self-contained final reply \
|
|
that stands on its own as your result — the supervisor reads that final reply, not your \
|
|
intermediate steps, so put everything it needs there.
|
|
|
|
If you need a decision, missing context, or clarification, or you hit a blocker or \
|
|
discover something that may change the plan, call `nucleic_ask_supervisor` with your \
|
|
question instead of guessing or stopping — it pauses you until the supervisor (who may \
|
|
relay to the human) answers, then you continue. Do not use `AskUserQuestion`: you cannot \
|
|
reach the user directly, and the supervisor is your point of contact — route anything \
|
|
user-facing through `nucleic_ask_supervisor`. For minor choices, prefer proceeding on a \
|
|
reasonable assumption and noting it in your reply; reserve questions for things that would \
|
|
meaningfully change your output.
|
|
"""
|
|
|
|
/// The system prompt appended for a run: the caller's environment guidance (sandbox build
|
|
/// instructions, etc.) plus, for an Orchestra run, the standing prompt for its `role` — the
|
|
/// supervisor's ``consentPrompt`` or the worker's ``workerBriefing``. Returns `nil` when nothing
|
|
/// applies so the backend flag is omitted entirely; joins the parts with a blank line when both
|
|
/// are present, so the standing prompt reads as its own paragraph.
|
|
public static func combinedSystemPrompt(environment: String?, role: Role?) -> String? {
|
|
let orchestraPrompt: String?
|
|
switch role {
|
|
case .supervisor: orchestraPrompt = consentPrompt
|
|
case .worker: orchestraPrompt = workerBriefing
|
|
case nil: orchestraPrompt = nil
|
|
}
|
|
let parts = [environment, orchestraPrompt]
|
|
.compactMap { $0 }
|
|
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
|
return parts.isEmpty ? nil : parts.joined(separator: "\n\n")
|
|
}
|
|
}
|