Merge nucleic/dapper-breezy-heron-eqsl into dev
This commit is contained in:
@@ -189,8 +189,70 @@ enum ModelCatalog {
|
||||
|
||||
static let fallbackModel = "claude-opus-5"
|
||||
static let fallbackEffort = EffortLadder.fallbackEffort
|
||||
static let fallbackSupervisorModel = "claude-fable-5"
|
||||
static let fallbackOrchestraWorkerModel = "claude-sonnet-5"
|
||||
|
||||
// MARK: - Orchestra supervisor & worker selection
|
||||
|
||||
/// One "Supervisor model" choice: a model paired with the effort it conducts at. Orchestra used
|
||||
/// to pin every supervisor to `xhigh`; the two levels make that selectable without adding a
|
||||
/// second control, since the only supervisor efforts worth offering are the top two.
|
||||
///
|
||||
/// The list is deliberately short. A supervisor plans, delegates, and synthesizes — long-horizon
|
||||
/// reasoning work — so it is offered only the two models that lead that lane (Fable 5 on the
|
||||
/// Claude side, GPT-5.6 Sol on the GPT side), one per provider, rather than the whole catalog.
|
||||
struct SupervisorChoice: Hashable, Identifiable {
|
||||
let model: String
|
||||
/// The API effort this choice conducts at — `xhigh` for Deep, `max` for Max.
|
||||
let effort: String
|
||||
/// The level's product name, shown in parentheses after the model.
|
||||
let levelName: String
|
||||
|
||||
var id: String { "\(model)|\(effort)" }
|
||||
var displayName: String { "\(ModelCatalog.displayName(model)) (\(levelName))" }
|
||||
}
|
||||
|
||||
/// The supervisor choices, Claude lane first (matching `models` order).
|
||||
static let supervisorChoices: [SupervisorChoice] = [
|
||||
SupervisorChoice(model: "claude-fable-5", effort: "xhigh", levelName: "Deep"),
|
||||
SupervisorChoice(model: "claude-fable-5", effort: "max", levelName: "Max"),
|
||||
SupervisorChoice(model: "gpt-5.6-sol", effort: "xhigh", levelName: "Deep"),
|
||||
SupervisorChoice(model: "gpt-5.6-sol", effort: "max", levelName: "Max"),
|
||||
]
|
||||
|
||||
/// Fable 5 at `xhigh` — the model Orchestra already defaulted to, at the effort it already ran,
|
||||
/// so an untouched install behaves exactly as before.
|
||||
static let fallbackSupervisorChoice = supervisorChoices[0]
|
||||
static let fallbackSupervisorModel = fallbackSupervisorChoice.model
|
||||
static let fallbackSupervisorEffort = fallbackSupervisorChoice.effort
|
||||
|
||||
/// The choice matching a stored (model, effort) pair. Falls back to the default for anything
|
||||
/// unrecognized — including a model stored before the list was shortened (an install that had
|
||||
/// picked Opus 5 as its supervisor lands on Fable 5 (Deep) rather than on an empty picker).
|
||||
static func supervisorChoice(model: String?, effort: String?) -> SupervisorChoice {
|
||||
if let exact = supervisorChoices.first(where: { $0.model == model && $0.effort == effort }) {
|
||||
return exact
|
||||
}
|
||||
// A known model whose stored effort is no longer offered keeps the model, at Deep.
|
||||
if let sameModel = supervisorChoices.first(where: { $0.model == model }) { return sameModel }
|
||||
return fallbackSupervisorChoice
|
||||
}
|
||||
|
||||
/// The **Intelligent** worker option: classify each worker's task and let the Intelligence
|
||||
/// router pick the model for it, rather than pinning one SKU for the whole fan-out. Stored as
|
||||
/// `OrchestrationMode.intelligentWorkerModel`, which is not a real SKU and is resolved to a
|
||||
/// concrete model at spawn (`AppStore.orchestraWorkerSelection`).
|
||||
static let intelligentWorkerModel = OrchestrationMode.intelligentWorkerModel
|
||||
|
||||
/// Named for the routing it does, not for "the app picks" — "Auto" is already taken by the
|
||||
/// single reasoning mode the ACP agents expose (see `effortDisplayName`) and by Auto-approve.
|
||||
static let intelligentWorkerDisplayName = "Intelligent"
|
||||
|
||||
static let intelligentWorkerBlurb =
|
||||
"Classifies each worker's task and routes it to the model that suits it, so a quick fix "
|
||||
+ "runs cheap and a hard subtask runs strong."
|
||||
|
||||
/// Workers default to Intelligent: a fan-out's subtasks vary far more in difficulty than one
|
||||
/// chat's turns do, so per-task routing is a better default than any single pinned SKU.
|
||||
static let fallbackOrchestraWorkerModel = intelligentWorkerModel
|
||||
/// How many Orchestra workers the supervisor may run at once by default. The fan-out
|
||||
/// multiplies token usage, so this bounds a single supervisor's concurrent workers rather
|
||||
/// than letting an over-eager fan-out spawn an unlimited wave. `0` means "unlimited".
|
||||
@@ -268,6 +330,9 @@ enum ModelCatalog {
|
||||
static let defaultModelKey = "nucleic.defaultModel"
|
||||
static let defaultEffortKey = "nucleic.defaultEffort"
|
||||
static let defaultSupervisorModelKey = "nucleic.defaultSupervisorModel"
|
||||
/// The supervisor's effort — the "(Deep)" / "(Max)" half of the supervisor choice. Kept as its
|
||||
/// own key rather than folded into the model string so both halves stay independently readable.
|
||||
static let defaultSupervisorEffortKey = "nucleic.defaultSupervisorEffort"
|
||||
static let defaultOrchestraWorkerModelKey = "nucleic.defaultOrchestraWorkerModel"
|
||||
static let defaultOrchestraMaxWorkersKey = "nucleic.defaultOrchestraMaxConcurrentWorkers"
|
||||
static let defaultAutoKey = "nucleic.defaultAuto"
|
||||
@@ -291,6 +356,15 @@ enum ModelCatalog {
|
||||
static var storedDefaultSupervisorModel: String {
|
||||
UserDefaults.standard.string(forKey: defaultSupervisorModelKey) ?? fallbackSupervisorModel
|
||||
}
|
||||
static var storedDefaultSupervisorEffort: String {
|
||||
UserDefaults.standard.string(forKey: defaultSupervisorEffortKey) ?? fallbackSupervisorEffort
|
||||
}
|
||||
/// The stored supervisor choice, normalized through ``supervisorChoice(model:effort:)`` so a
|
||||
/// value from before the list was shortened resolves to a real entry.
|
||||
static var storedSupervisorChoice: SupervisorChoice {
|
||||
supervisorChoice(
|
||||
model: storedDefaultSupervisorModel, effort: storedDefaultSupervisorEffort)
|
||||
}
|
||||
static var storedDefaultOrchestraWorkerModel: String {
|
||||
UserDefaults.standard.string(forKey: defaultOrchestraWorkerModelKey) ?? fallbackOrchestraWorkerModel
|
||||
}
|
||||
|
||||
@@ -179,9 +179,13 @@ struct NucleicApp: App {
|
||||
store.chatDockBounceHook = { DockBounce.shared.setPending($0) }
|
||||
store.defaultModel = ModelCatalog.storedDefaultModel
|
||||
store.defaultEffort = ModelCatalog.storedDefaultEffort
|
||||
store.defaultSupervisorModel = ModelCatalog.storedDefaultSupervisorModel
|
||||
// Normalized through `storedSupervisorChoice` so a supervisor stored before the list was
|
||||
// shortened (or with an effort no longer offered) launches on a real entry.
|
||||
store.defaultSupervisorModel = ModelCatalog.storedSupervisorChoice.model
|
||||
store.defaultSupervisorEffort = ModelCatalog.storedSupervisorChoice.effort
|
||||
store.defaultOrchestraWorkerModel = ModelCatalog.storedDefaultOrchestraWorkerModel
|
||||
store.defaultOrchestraMaxConcurrentWorkers = ModelCatalog.storedDefaultOrchestraMaxWorkers
|
||||
store.defaultIntelligenceLevel = ModelCatalog.storedDefaultIntelligenceLevel
|
||||
store.defaultAuto = ModelCatalog.storedDefaultAuto
|
||||
store.defaultAutoShip = ModelCatalog.storedDefaultAutoShip
|
||||
store.summarizeToolCalls = TranscriptDisplay.storedSummarizeToolCalls
|
||||
|
||||
@@ -2392,6 +2392,7 @@ private struct AgentsSettingsTab: View {
|
||||
@AppStorage(ModelCatalog.defaultModelKey) private var defaultModel = ModelCatalog.fallbackModel
|
||||
@AppStorage(ModelCatalog.defaultEffortKey) private var defaultEffort = ModelCatalog.fallbackEffort
|
||||
@AppStorage(ModelCatalog.defaultSupervisorModelKey) private var defaultSupervisorModel = ModelCatalog.fallbackSupervisorModel
|
||||
@AppStorage(ModelCatalog.defaultSupervisorEffortKey) private var defaultSupervisorEffort = ModelCatalog.fallbackSupervisorEffort
|
||||
@AppStorage(ModelCatalog.defaultOrchestraWorkerModelKey) private var defaultOrchestraWorkerModel = ModelCatalog.fallbackOrchestraWorkerModel
|
||||
@AppStorage(ModelCatalog.defaultOrchestraMaxWorkersKey) private var defaultOrchestraMaxWorkers = ModelCatalog.fallbackOrchestraMaxWorkers
|
||||
@AppStorage(ModelCatalog.defaultAutoKey) private var defaultAuto = false
|
||||
@@ -2491,20 +2492,31 @@ private struct AgentsSettingsTab: View {
|
||||
}
|
||||
|
||||
Section("Orchestra") {
|
||||
Picker("Supervisor model", selection: $defaultSupervisorModel) {
|
||||
ForEach(ModelCatalog.models, id: \.self) { sku in
|
||||
let badge = ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text("")
|
||||
Text("\(ModelCatalog.displayName(sku))\(badge)")
|
||||
.tag(sku)
|
||||
// Model + effort in one control: the supervisor conducts (plan, delegate,
|
||||
// synthesize), so only the two long-horizon leaders are offered, each at the top
|
||||
// two efforts. The two halves persist to their own keys — see `supervisorBinding`.
|
||||
Picker("Supervisor model", selection: supervisorBinding) {
|
||||
ForEach(ModelCatalog.supervisorChoices) { choice in
|
||||
Text(choice.displayName).tag(choice)
|
||||
}
|
||||
}
|
||||
.help("The model that plans the work and drives the workers. Deep runs at xhigh effort; Max runs at max.")
|
||||
Picker("Worker model", selection: $defaultOrchestraWorkerModel) {
|
||||
ForEach(ModelCatalog.models, id: \.self) { sku in
|
||||
let badge = ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text("")
|
||||
Text("\(ModelCatalog.displayName(sku))\(badge)")
|
||||
.tag(sku)
|
||||
// Intelligent first, above the fixed SKUs it chooses among.
|
||||
Text(ModelCatalog.intelligentWorkerDisplayName)
|
||||
.tag(ModelCatalog.intelligentWorkerModel)
|
||||
Divider()
|
||||
// Providers set apart by a separator, as in the composer's model menu.
|
||||
ForEach(Array(ModelCatalog.modelGroups.enumerated()), id: \.offset) { index, group in
|
||||
if index > 0 { Divider() }
|
||||
ForEach(group, id: \.self) { sku in
|
||||
let badge = ModelCatalog.contextBadge(for: sku).map { Text(" \($0)").foregroundColor(.secondary) } ?? Text("")
|
||||
Text("\(ModelCatalog.displayName(sku))\(badge)")
|
||||
.tag(sku)
|
||||
}
|
||||
}
|
||||
}
|
||||
.help(ModelCatalog.intelligentWorkerBlurb)
|
||||
Picker("Max concurrent workers", selection: $defaultOrchestraMaxWorkers) {
|
||||
ForEach(ModelCatalog.orchestraMaxWorkerChoices, id: \.self) { count in
|
||||
Text(count <= 0 ? "Unlimited" : "\(count)").tag(count)
|
||||
@@ -2597,6 +2609,7 @@ private struct AgentsSettingsTab: View {
|
||||
}
|
||||
.onChange(of: defaultEffort) { _, _ in pushDefaults() }
|
||||
.onChange(of: defaultSupervisorModel) { _, _ in pushDefaults() }
|
||||
.onChange(of: defaultSupervisorEffort) { _, _ in pushDefaults() }
|
||||
.onChange(of: defaultOrchestraWorkerModel) { _, _ in pushDefaults() }
|
||||
.onChange(of: defaultOrchestraMaxWorkers) { _, _ in pushDefaults() }
|
||||
.onChange(of: defaultAuto) { _, _ in pushDefaults() }
|
||||
@@ -2656,12 +2669,31 @@ private struct AgentsSettingsTab: View {
|
||||
? "*" : ExternalAgentIntegrationSettings.encodedModels(models)
|
||||
}
|
||||
|
||||
/// The supervisor picker's selection, projected over the two stored halves (model + effort) so
|
||||
/// the control is one row while each half keeps its own well-named key. Reads through
|
||||
/// `supervisorChoice(model:effort:)`, so a value stored before the list was shortened resolves
|
||||
/// to a real entry instead of leaving the picker blank.
|
||||
private var supervisorBinding: Binding<ModelCatalog.SupervisorChoice> {
|
||||
Binding(
|
||||
get: {
|
||||
ModelCatalog.supervisorChoice(
|
||||
model: defaultSupervisorModel, effort: defaultSupervisorEffort)
|
||||
},
|
||||
set: { choice in
|
||||
defaultSupervisorModel = choice.model
|
||||
defaultSupervisorEffort = choice.effort
|
||||
})
|
||||
}
|
||||
|
||||
private func pushDefaults() {
|
||||
store.defaultModel = defaultModel
|
||||
store.defaultEffort = defaultEffort
|
||||
store.defaultSupervisorModel = defaultSupervisorModel
|
||||
store.defaultSupervisorEffort = defaultSupervisorEffort
|
||||
store.defaultOrchestraWorkerModel = defaultOrchestraWorkerModel
|
||||
store.defaultOrchestraMaxConcurrentWorkers = defaultOrchestraMaxWorkers
|
||||
store.defaultIntelligenceLevel =
|
||||
IntelligenceLevel(rawValue: defaultIntelligenceLevel) ?? .fallback
|
||||
store.defaultAuto = defaultAuto
|
||||
}
|
||||
|
||||
|
||||
@@ -774,7 +774,20 @@ public final class AppStore: ConflictArbiter {
|
||||
public var defaultModel: String?
|
||||
public var defaultEffort: String?
|
||||
public var defaultSupervisorModel: String?
|
||||
/// The effort an Orchestra supervisor runs at — the "(Deep)" / "(Max)" half of the supervisor
|
||||
/// choice in Settings. Stored on the session as a qualified sentinel
|
||||
/// (`OrchestrationMode.qualifiedEffortSentinel`) so the mode stays Orchestra while the
|
||||
/// underlying API effort varies. `nil` keeps the historical fixed `xhigh`.
|
||||
public var defaultSupervisorEffort: String?
|
||||
/// The worker model for Orchestra fan-out — a SKU, or
|
||||
/// `OrchestrationMode.intelligentWorkerModel` for **Intelligent**, which classifies each
|
||||
/// worker's task and routes it (see `orchestraWorkerSelection`).
|
||||
public var defaultOrchestraWorkerModel: String?
|
||||
/// The Intelligence level routed workers are resolved at (set by the app from Settings). Level
|
||||
/// is the ambition/budget knob the classifier's purpose is combined with — Orchestra's own
|
||||
/// thoroughness lives in the supervisor and the fan-out, so workers route at the user's
|
||||
/// ordinary default rather than being forced to the top of the ladder.
|
||||
public var defaultIntelligenceLevel: IntelligenceLevel = .fallback
|
||||
/// Cap on how many Orchestra workers one supervisor may run concurrently (set by the app from
|
||||
/// Settings). `<= 0` means unlimited. Enforced by `spawnOrchestraSubagent`, which parks a
|
||||
/// worker's `nucleic_subagent` call until a slot frees, so an over-eager fan-out queues rather
|
||||
@@ -7010,8 +7023,13 @@ public final class AppStore: ConflictArbiter {
|
||||
// supervisor instead. Gated to Control projects, where Orchestra actually runs
|
||||
// (`orchestraActive`); the supervisor default falls back to the ordinary model if unset.
|
||||
let resolvedModel: String?
|
||||
// The supervisor's *effort* is configurable alongside its model ("Fable 5 (Max)"), so an
|
||||
// Orchestra chat is stored with the qualified sentinel rather than the bare one. `xhigh`
|
||||
// still stores bare, so the default selection produces the same value it always did.
|
||||
var resolvedEffort = effort
|
||||
if OrchestrationMode.isOrchestra(effort ?? defaultEffort), project.isNucleicControlled {
|
||||
resolvedModel = defaultSupervisorModel ?? model ?? defaultModel
|
||||
resolvedEffort = OrchestrationMode.qualifiedEffortSentinel(defaultSupervisorEffort)
|
||||
} else {
|
||||
resolvedModel = model ?? defaultModel
|
||||
}
|
||||
@@ -7094,7 +7112,7 @@ public final class AppStore: ConflictArbiter {
|
||||
parentSessionID: nvrsionBranch != nil ? nil : parentSession?.id,
|
||||
spawnedBySessionID: spawnedBy,
|
||||
rootSpawnedBySessionID: spawnedByRoot,
|
||||
model: resolvedModel, effort: effort ?? defaultEffort,
|
||||
model: resolvedModel, effort: resolvedEffort ?? defaultEffort,
|
||||
routedPurpose: routing?.purpose.rawValue,
|
||||
routedLevel: routing?.level.rawValue,
|
||||
routedReason: routing?.reason,
|
||||
@@ -7290,6 +7308,42 @@ public final class AppStore: ConflictArbiter {
|
||||
return created
|
||||
}
|
||||
|
||||
/// The model + effort one worker should run at, and the routing provenance when the Intelligence
|
||||
/// router chose them.
|
||||
///
|
||||
/// With a SKU pinned in Settings, every worker runs it. With **Intelligent**, the worker's own
|
||||
/// task is classified (`HeuristicPurposeClassifier` — deterministic and sub-millisecond, so it's
|
||||
/// safe on this synchronous spawn path, unlike the model-backed layers a composer can await)
|
||||
/// and `IntelligenceRouter` picks the model and effort that suit
|
||||
/// *that* subtask: a typo fix lands on a cheap model, a backend feature on a strong one. That is
|
||||
/// the whole point of the fan-out's cost story — the expensive model supervises, and each worker
|
||||
/// costs what its own task deserves.
|
||||
///
|
||||
/// `fallbackEffort` is what a pinned-SKU worker runs at (Orchestra's `xhigh` for a supervised
|
||||
/// worker, the parent's effort for a blocking subagent); a routed worker uses the router's effort
|
||||
/// instead. Always returns a concrete SKU — the Intelligent sentinel must never reach a backend.
|
||||
func orchestraWorkerSelection(
|
||||
task: String, prompt: String, parent: Session, fallbackEffort: String?
|
||||
) -> (model: String?, effort: String?, routing: RoutingNote?) {
|
||||
guard OrchestrationMode.isIntelligentWorkerModel(defaultOrchestraWorkerModel) else {
|
||||
return (defaultOrchestraWorkerModel ?? parent.model, fallbackEffort, nil)
|
||||
}
|
||||
// The short label plus the full instructions: the label carries the supervisor's own summary
|
||||
// of the subtask, which is often the clearest statement of what the work *is*.
|
||||
let verdict = HeuristicPurposeClassifier.classify([task, prompt].joined(separator: "\n"))
|
||||
let resolution = IntelligenceRouter.route(
|
||||
purpose: verdict.purpose,
|
||||
level: defaultIntelligenceLevel,
|
||||
connected: connectedProviders,
|
||||
limits: intelligenceRoutingLimits,
|
||||
codexPro: isCodexPro,
|
||||
fallback: (
|
||||
parent.model ?? defaultModel ?? OrchestrationMode.intelligentWorkerFallbackModel,
|
||||
fallbackEffort ?? EffortLadder.fallbackEffort
|
||||
))
|
||||
return (resolution.model, resolution.effort, RoutingNote(resolution))
|
||||
}
|
||||
|
||||
/// Spawn one Orchestra worker as a first-class Nucleic session — **non-blocking**. It creates the
|
||||
/// worker session (so the supervisor gets a `worker_id` to track immediately), registers it with
|
||||
/// the supervisor's coordinator, and hands the actual run to a detached task; it does NOT wait for
|
||||
@@ -7308,7 +7362,10 @@ public final class AppStore: ConflictArbiter {
|
||||
guard !prompt.isEmpty else {
|
||||
return .denied(message: "Worker prompt is required.")
|
||||
}
|
||||
let workerModel = defaultOrchestraWorkerModel ?? parent.model
|
||||
let selection = orchestraWorkerSelection(
|
||||
task: request.task, prompt: prompt, parent: parent,
|
||||
fallbackEffort: OrchestrationMode.resolvedOrchestraEffort)
|
||||
let workerModel = selection.model
|
||||
let title = Self.orchestraWorkerTitle(for: request.task)
|
||||
let coordinator = orchestraCoordinator(for: parent.id)
|
||||
let workerID: SessionID
|
||||
@@ -7318,7 +7375,9 @@ public final class AppStore: ConflictArbiter {
|
||||
// immediately with a real `worker_id`.
|
||||
workerID = try await createSession(
|
||||
in: project, title: title, prompt: prompt,
|
||||
model: workerModel, effort: OrchestrationMode.resolvedOrchestraEffort,
|
||||
model: workerModel,
|
||||
effort: selection.effort ?? OrchestrationMode.resolvedOrchestraEffort,
|
||||
routing: selection.routing,
|
||||
useWorktree: !project.nvrsionActive, auto: true, autoShip: false,
|
||||
spawnedBy: parent.id, startRun: false)
|
||||
} catch {
|
||||
@@ -7467,13 +7526,18 @@ public final class AppStore: ConflictArbiter {
|
||||
guard !prompt.isEmpty else {
|
||||
return .denied(message: "Subagent prompt is required.")
|
||||
}
|
||||
let workerModel = defaultOrchestraWorkerModel ?? parent.model
|
||||
// Same worker-model resolution as the Orchestra spawn, so an Intelligent selection routes a
|
||||
// blocking subagent too — and, critically, so the sentinel is never passed through as a SKU.
|
||||
let selection = orchestraWorkerSelection(
|
||||
task: request.task, prompt: prompt, parent: parent, fallbackEffort: parent.effort)
|
||||
let workerModel = selection.model
|
||||
let title = Self.orchestraWorkerTitle(for: request.task)
|
||||
let workerID: SessionID
|
||||
do {
|
||||
workerID = try await createSession(
|
||||
in: project, title: title, prompt: prompt,
|
||||
model: workerModel, effort: parent.effort,
|
||||
model: workerModel, effort: selection.effort ?? parent.effort,
|
||||
routing: selection.routing,
|
||||
useWorktree: !project.nvrsionActive, auto: true, autoShip: false,
|
||||
spawnedBy: parent.id, startRun: false)
|
||||
} catch {
|
||||
|
||||
@@ -26,15 +26,50 @@ public enum OrchestrationMode {
|
||||
/// 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.
|
||||
/// The effort orchestra runs at when the sentinel carries no qualifier — the level every
|
||||
/// Orchestra session used before the supervisor's effort became selectable, so an unqualified
|
||||
/// (or legacy) stored selection keeps behaving exactly as it did.
|
||||
public static let resolvedOrchestraEffort = "xhigh"
|
||||
|
||||
/// Separates the sentinel from the supervisor's chosen effort: `"orchestra:max"`. The mode is
|
||||
/// still Orchestra — only the underlying API effort differs — so the qualifier rides on the
|
||||
/// sentinel rather than becoming a second `Session` column: every existing reader keeps
|
||||
/// working (`isOrchestra` is true, the UI still shows "Orchestra"), and the choice persists
|
||||
/// and transfers with the session for free. `xhigh` is stored bare, so the common case
|
||||
/// produces exactly the string it always did.
|
||||
public static let effortQualifierSeparator: Character = ":"
|
||||
|
||||
/// The value to store in `Session.effort` for a supervisor running at `effort`. Falls back to
|
||||
/// the bare sentinel for a nil/unknown effort or for `xhigh` (the unqualified default).
|
||||
public static func qualifiedEffortSentinel(_ effort: String?) -> String {
|
||||
guard let normalized = effort?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
|
||||
normalized != resolvedOrchestraEffort,
|
||||
EffortLadder.efforts.contains(normalized) || normalized == EffortLadder.proEffort
|
||||
else { return effortSentinel }
|
||||
return "\(effortSentinel)\(effortQualifierSeparator)\(normalized)"
|
||||
}
|
||||
|
||||
/// The sentinel and its optional effort qualifier, or nil when `effort` isn't an Orchestra
|
||||
/// selection at all. Case-insensitive and whitespace-tolerant, like the raw comparison it
|
||||
/// replaced.
|
||||
private static func parse(_ effort: String?) -> (base: String, qualifier: String?)? {
|
||||
guard let normalized = effort?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
|
||||
!normalized.isEmpty
|
||||
else { return nil }
|
||||
let parts = normalized.split(
|
||||
separator: effortQualifierSeparator, maxSplits: 1, omittingEmptySubsequences: false)
|
||||
let base = String(parts[0])
|
||||
guard base == effortSentinel || base == legacyEffortSentinel else { return nil }
|
||||
let qualifier = parts.count > 1 ? String(parts[1]) : nil
|
||||
return (base, (qualifier?.isEmpty ?? true) ? nil : qualifier)
|
||||
}
|
||||
|
||||
/// Whether `effort` selects orchestra (case-insensitive, whitespace-tolerant). Accepts the
|
||||
/// legacy "ultracode" token too, so the rename can't strand an already-stored selection.
|
||||
/// legacy "ultracode" token too, so the rename can't strand an already-stored selection, and
|
||||
/// the qualified form (`"orchestra:max"`), so choosing a supervisor effort never reads as
|
||||
/// "not Orchestra" — which would drop the mode's tools and prompt.
|
||||
public static func isOrchestra(_ effort: String?) -> Bool {
|
||||
let normalized = effort?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return normalized == effortSentinel || normalized == legacyEffortSentinel
|
||||
parse(effort) != nil
|
||||
}
|
||||
|
||||
/// Which side of an Orchestra session a run is: the user-started chat is the `supervisor` (it
|
||||
@@ -47,11 +82,37 @@ public enum OrchestrationMode {
|
||||
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".
|
||||
/// The effort actually handed to a backend: an Orchestra selection resolves to its qualifier
|
||||
/// (`"orchestra:max"` → `max`) or to `xhigh` when unqualified; everything else passes through
|
||||
/// untouched. One-way — the resolved value never flows back into `Session.effort`, so the UI
|
||||
/// keeps showing "Orchestra". An unrecognized qualifier resolves to `xhigh` rather than being
|
||||
/// forwarded, so a hand-edited or future-versioned store value can't reach a backend as a
|
||||
/// bogus `--effort`.
|
||||
public static func resolvedEffort(_ effort: String?) -> String? {
|
||||
isOrchestra(effort) ? resolvedOrchestraEffort : effort
|
||||
guard let parsed = parse(effort) else { return effort }
|
||||
guard let qualifier = parsed.qualifier,
|
||||
EffortLadder.efforts.contains(qualifier) || qualifier == EffortLadder.proEffort
|
||||
else { return resolvedOrchestraEffort }
|
||||
return qualifier
|
||||
}
|
||||
|
||||
// MARK: - Worker model selection
|
||||
|
||||
/// The sentinel stored in the worker-model setting for **Intelligent**: instead of pinning one
|
||||
/// SKU for every worker, classify each worker's task and let `IntelligenceRouter` pick the model
|
||||
/// (and effort) that suits it. Deliberately not a real SKU — `BackendID.forModel` doesn't know
|
||||
/// it, so it must be resolved to a concrete model before any spawn (see
|
||||
/// `AppStore.orchestraWorkerSelection`); it must never reach a backend as `--model`.
|
||||
public static let intelligentWorkerModel = "nucleic-intelligent"
|
||||
|
||||
/// The model a routed worker falls back to when the router can't resolve one — no provider
|
||||
/// probed yet at launch, or every candidate is quota-blocked. Matches the fixed worker default
|
||||
/// Orchestra shipped with, so the fallback is the behavior users already knew.
|
||||
public static let intelligentWorkerFallbackModel = "claude-sonnet-5"
|
||||
|
||||
/// Whether the worker-model setting selects Intelligent (classifier-chosen) rather than a SKU.
|
||||
public static func isIntelligentWorkerModel(_ model: String?) -> Bool {
|
||||
model?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == intelligentWorkerModel
|
||||
}
|
||||
|
||||
/// The standing prompt appended for the **supervisor** while Orchestra is on. It frames the
|
||||
|
||||
@@ -252,6 +252,81 @@ struct OrchestraWorkerSlotTests {
|
||||
#expect(store.orchestraWorkersRunning == 0) // the slot came back
|
||||
}
|
||||
|
||||
// MARK: - Worker model selection (Intelligent)
|
||||
|
||||
/// With **Intelligent** selected, each worker is routed from its own task rather than pinned to
|
||||
/// one SKU — and, load-bearing, the sentinel is resolved to a real model. Passing it through as
|
||||
/// `--model` would hand the backend a SKU no provider knows.
|
||||
@Test func intelligentWorkerModelRoutesPerTaskAndNeverLeaksTheSentinel() async throws {
|
||||
let repo = try await GitTestRepo()
|
||||
defer { repo.cleanup() }
|
||||
let store = makeStore(repo: repo) { _ in ScriptedBackend { _, _ in } }
|
||||
store.defaultOrchestraWorkerModel = OrchestrationMode.intelligentWorkerModel
|
||||
// Both lanes connected, so routing can pick either provider's candidate.
|
||||
let project = try #require(
|
||||
await store.addProject(name: "demo", rootPath: repo.root, defaultBranch: "main"))
|
||||
let parentID = try await store.createSession(in: project, title: "supervisor", prompt: "")
|
||||
let parent = try #require(await store.liveSnapshot(parentID)).session
|
||||
|
||||
// A one-line typo fix and a substantial backend task must not resolve to the same thing on a
|
||||
// per-task router — that difference IS the feature.
|
||||
let quick = store.orchestraWorkerSelection(
|
||||
task: "fix typo", prompt: "Fix the typo in the README heading.",
|
||||
parent: parent, fallbackEffort: OrchestrationMode.resolvedOrchestraEffort)
|
||||
let heavy = store.orchestraWorkerSelection(
|
||||
task: "build the API",
|
||||
prompt: "Implement the server endpoint, its data model, and the algorithm behind it.",
|
||||
parent: parent, fallbackEffort: OrchestrationMode.resolvedOrchestraEffort)
|
||||
|
||||
for selection in [quick, heavy] {
|
||||
let model = try #require(selection.model)
|
||||
#expect(model != OrchestrationMode.intelligentWorkerModel)
|
||||
#expect(BackendID.forModel(model) != nil) // a real, runnable SKU
|
||||
#expect(selection.routing != nil) // provenance recorded for "why this model"
|
||||
}
|
||||
#expect(quick.routing?.purpose == .quickFix)
|
||||
#expect(heavy.routing?.purpose == .backendImpl)
|
||||
|
||||
// A pinned SKU is passed straight through, with the caller's effort and no routing note.
|
||||
store.defaultOrchestraWorkerModel = "claude-haiku-4-5"
|
||||
let pinned = store.orchestraWorkerSelection(
|
||||
task: "fix typo", prompt: "Fix the typo.", parent: parent, fallbackEffort: "xhigh")
|
||||
#expect(pinned.model == "claude-haiku-4-5")
|
||||
#expect(pinned.effort == "xhigh")
|
||||
#expect(pinned.routing == nil)
|
||||
}
|
||||
|
||||
/// A spawned worker actually runs the routed model — the selection reaches the created session,
|
||||
/// not just the return value.
|
||||
@Test func spawnedWorkerSessionUsesTheRoutedModel() async throws {
|
||||
let repo = try await GitTestRepo()
|
||||
defer { repo.cleanup() }
|
||||
let store = makeStore(repo: repo) { _ in ScriptedBackend { _, _ in } }
|
||||
store.defaultOrchestraWorkerModel = OrchestrationMode.intelligentWorkerModel
|
||||
|
||||
let project = try #require(
|
||||
await store.addProject(name: "demo", rootPath: repo.root, defaultBranch: "main"))
|
||||
let parentID = try await store.createSession(in: project, title: "supervisor", prompt: "")
|
||||
let parent = try #require(await store.liveSnapshot(parentID)).session
|
||||
|
||||
let result = await store.spawnOrchestraSubagent(
|
||||
OrchestraSubagentRequest(task: "fix typo", prompt: "Fix the typo in the heading."),
|
||||
parent: parent, project: project)
|
||||
guard case .spawned(let workerID, _, let model) = result else {
|
||||
Issue.record("expected .spawned, got \(result)")
|
||||
return
|
||||
}
|
||||
let reported = try #require(model)
|
||||
#expect(reported != OrchestrationMode.intelligentWorkerModel)
|
||||
let worker = try #require(await store.liveSnapshot(workerID)).session
|
||||
#expect(worker.model == reported)
|
||||
// Routing provenance rides onto the session, so its info panel can say why this model.
|
||||
#expect(worker.routedPurpose == PromptPurpose.quickFix.rawValue)
|
||||
#expect(worker.routedReason?.isEmpty == false)
|
||||
|
||||
await store.teardownOrchestra(supervisor: parentID)
|
||||
}
|
||||
|
||||
// MARK: - Plan delegation
|
||||
|
||||
/// Submitting a plan spawns a real worker session for EVERY task in EVERY batch at once — batches
|
||||
|
||||
@@ -27,6 +27,59 @@ struct OrchestrationModeTests {
|
||||
#expect(OrchestrationMode.resolvedEffort("Orchestra") == "xhigh")
|
||||
}
|
||||
|
||||
// MARK: - Supervisor effort qualifier
|
||||
|
||||
/// The supervisor's effort rides on the sentinel ("orchestra:max"). The load-bearing part is that
|
||||
/// it still reads as Orchestra — a qualified value that failed `isOrchestra` would silently drop
|
||||
/// the mode's tools and standing prompt.
|
||||
@Test func qualifiedSentinelStaysOrchestraAndCarriesItsEffort() {
|
||||
#expect(OrchestrationMode.isOrchestra("orchestra:max"))
|
||||
#expect(OrchestrationMode.isOrchestra(" Orchestra:MAX ")) // trimmed, case-insensitive
|
||||
#expect(OrchestrationMode.resolvedEffort("orchestra:max") == "max")
|
||||
#expect(OrchestrationMode.resolvedEffort("orchestra:xhigh") == "xhigh")
|
||||
// The legacy sentinel takes a qualifier too, so an old session that gets re-saved is fine.
|
||||
#expect(OrchestrationMode.isOrchestra("ultracode:max"))
|
||||
#expect(OrchestrationMode.resolvedEffort("ultracode:max") == "max")
|
||||
}
|
||||
|
||||
/// `xhigh` — what Orchestra always ran at — stores bare, so the default selection produces
|
||||
/// exactly the value it always did and nothing downstream sees a new shape.
|
||||
@Test func defaultSupervisorEffortStoresTheBareSentinel() {
|
||||
#expect(OrchestrationMode.qualifiedEffortSentinel("xhigh") == "orchestra")
|
||||
#expect(OrchestrationMode.qualifiedEffortSentinel(nil) == "orchestra")
|
||||
#expect(OrchestrationMode.qualifiedEffortSentinel("max") == "orchestra:max")
|
||||
// Round-trips: what we store resolves back to what was chosen.
|
||||
for effort in ["low", "medium", "high", "xhigh", "max"] {
|
||||
let stored = OrchestrationMode.qualifiedEffortSentinel(effort)
|
||||
#expect(OrchestrationMode.isOrchestra(stored))
|
||||
#expect(OrchestrationMode.resolvedEffort(stored) == effort)
|
||||
}
|
||||
}
|
||||
|
||||
/// A qualifier that isn't a real effort resolves to xhigh rather than being forwarded — a
|
||||
/// hand-edited or future-versioned store value must never reach a backend as a bogus `--effort`.
|
||||
@Test func unknownQualifierFallsBackToXhigh() {
|
||||
#expect(OrchestrationMode.resolvedEffort("orchestra:bogus") == "xhigh")
|
||||
#expect(OrchestrationMode.resolvedEffort("orchestra:") == "xhigh")
|
||||
#expect(OrchestrationMode.qualifiedEffortSentinel("bogus") == "orchestra")
|
||||
// And a non-Orchestra effort that merely contains a colon is left alone.
|
||||
#expect(OrchestrationMode.isOrchestra("high:max") == false)
|
||||
#expect(OrchestrationMode.resolvedEffort("high:max") == "high:max")
|
||||
}
|
||||
|
||||
// MARK: - Intelligent worker sentinel
|
||||
|
||||
/// The Intelligent sentinel is not a SKU: it must be recognizable, and it must not be mistaken
|
||||
/// for a model (it is resolved to a real one before any spawn).
|
||||
@Test func intelligentWorkerSentinelIsRecognizedAndIsNotASKU() {
|
||||
#expect(OrchestrationMode.isIntelligentWorkerModel(OrchestrationMode.intelligentWorkerModel))
|
||||
#expect(OrchestrationMode.isIntelligentWorkerModel(" Nucleic-Intelligent "))
|
||||
#expect(OrchestrationMode.isIntelligentWorkerModel("claude-sonnet-5") == false)
|
||||
#expect(OrchestrationMode.isIntelligentWorkerModel(nil) == false)
|
||||
// No provider claims it — which is exactly why it must never be passed through as `--model`.
|
||||
#expect(BackendID.forModel(OrchestrationMode.intelligentWorkerModel) == nil)
|
||||
}
|
||||
|
||||
@Test func passesThroughRealEffortLevels() {
|
||||
for level in ["low", "medium", "high", "xhigh", "max"] {
|
||||
#expect(OrchestrationMode.resolvedEffort(level) == level)
|
||||
|
||||
Reference in New Issue
Block a user