pantheon-opencode 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -95
- package/bin/pantheon-init.mjs +7 -0
- package/commands/pantheon-model.md +34 -0
- package/opencode.json +0 -2
- package/package.json +2 -2
- package/scripts/generate-preset-docs.mjs +239 -0
- package/scripts/install/model-picker.mjs +367 -2
- package/scripts/install/opencode.mjs +150 -137
- package/src/pantheon/chat-reminders.ts +4 -74
- package/src/pantheon/compaction-assert.ts +28 -28
- package/src/pantheon/delegation-finalize.ts +44 -11
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +45 -121
- package/src/pantheon/model-command.ts +858 -0
- package/src/pantheon/presets.d.mts +4 -3
- package/src/pantheon/presets.mjs +47 -54
- package/src/pantheon/zeus-delegate-with-retry.ts +9 -4
- package/src/plugin.ts +17 -29
- package/src/plugins/pantheon-hooks.ts +15 -158
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/dist/tui.tsx +13 -2
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +13 -2
- package/src/plugins/tui/tsdown.config.ts +11 -0
- package/src/routing.yml +71 -123
|
@@ -172,24 +172,48 @@ export const DELEGATION_DEFAULTS = {
|
|
|
172
172
|
|
|
173
173
|
// ─── Output pulling ────────────────────────────────────────────────────
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
interface PulledOutput {
|
|
176
|
+
text: string
|
|
177
|
+
/** True only when the child produced an assistant message or tool activity. */
|
|
178
|
+
hasWork: boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Pull visible text and structurally identify real child work. */
|
|
182
|
+
async function pullOutput(client: DelegationClient, childSessionID: string): Promise<PulledOutput> {
|
|
177
183
|
if (typeof client.session.messages !== 'function')
|
|
178
|
-
return '(child session messages API unavailable)'
|
|
184
|
+
return { text: '(child session messages API unavailable)', hasWork: false }
|
|
179
185
|
try {
|
|
180
186
|
const bundles = await client.session.messages({ path: { id: childSessionID } })
|
|
181
187
|
const lines: string[] = []
|
|
188
|
+
let hasWork = false
|
|
182
189
|
for (const bundle of bundles) {
|
|
190
|
+
const hasToolPart = (bundle.parts ?? []).some((part) => part.type === 'tool')
|
|
191
|
+
const hasTextPart = (bundle.parts ?? []).some(
|
|
192
|
+
(part) => part.type === 'text' && typeof part.text === 'string' && part.text.trim() !== '',
|
|
193
|
+
)
|
|
194
|
+
if (
|
|
195
|
+
bundle.info?.role === 'assistant' &&
|
|
196
|
+
bundle.info.error === undefined &&
|
|
197
|
+
(hasToolPart || hasTextPart)
|
|
198
|
+
) {
|
|
199
|
+
hasWork = true
|
|
200
|
+
}
|
|
183
201
|
for (const part of bundle.parts ?? []) {
|
|
202
|
+
if (
|
|
203
|
+
part.type === 'tool' &&
|
|
204
|
+
bundle.info?.role === 'assistant' &&
|
|
205
|
+
bundle.info.error === undefined
|
|
206
|
+
)
|
|
207
|
+
hasWork = true
|
|
184
208
|
if (part.type === 'text' && typeof part.text === 'string' && part.text.trim() !== '') {
|
|
185
209
|
lines.push(part.text)
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
}
|
|
189
|
-
return lines.join('\n\n')
|
|
213
|
+
return { text: lines.join('\n\n'), hasWork }
|
|
190
214
|
} catch (err: unknown) {
|
|
191
215
|
const reason = err instanceof Error ? err.message : String(err)
|
|
192
|
-
return `(failed to pull child session output: ${reason})
|
|
216
|
+
return { text: `(failed to pull child session output: ${reason})`, hasWork: false }
|
|
193
217
|
}
|
|
194
218
|
}
|
|
195
219
|
|
|
@@ -311,9 +335,18 @@ export async function finalizeDelegation(
|
|
|
311
335
|
const job = deps.board.get(childSessionID)
|
|
312
336
|
if (!job) return undefined
|
|
313
337
|
|
|
314
|
-
const
|
|
338
|
+
const pulled = await pullOutput(deps.client, childSessionID)
|
|
339
|
+
const output = pulled.text
|
|
340
|
+
const effectiveOpts: FinalizeInput =
|
|
341
|
+
opts.state === 'completed' && !pulled.hasWork
|
|
342
|
+
? {
|
|
343
|
+
state: 'error',
|
|
344
|
+
error: opts.error ?? 'Child session produced no assistant or tool output',
|
|
345
|
+
...(opts.timedOut !== undefined ? { timedOut: opts.timedOut } : {}),
|
|
346
|
+
}
|
|
347
|
+
: opts
|
|
315
348
|
const outputDir = deps.options.outputDir ?? DELEGATION_DEFAULTS.outputDir
|
|
316
|
-
await writeDelegationReport(outputDir, job, renderDelegationMarkdown(job, output,
|
|
349
|
+
await writeDelegationReport(outputDir, job, renderDelegationMarkdown(job, output, effectiveOpts))
|
|
317
350
|
|
|
318
351
|
const status: {
|
|
319
352
|
taskID: string
|
|
@@ -323,15 +356,15 @@ export async function finalizeDelegation(
|
|
|
323
356
|
resultSummary?: string
|
|
324
357
|
} = {
|
|
325
358
|
taskID: childSessionID,
|
|
326
|
-
state:
|
|
359
|
+
state: effectiveOpts.state,
|
|
327
360
|
resultSummary: summarizeOutput(output),
|
|
328
361
|
}
|
|
329
|
-
if (
|
|
330
|
-
if (
|
|
362
|
+
if (effectiveOpts.error !== undefined) status.error = effectiveOpts.error
|
|
363
|
+
if (effectiveOpts.timedOut !== undefined) status.timedOut = effectiveOpts.timedOut
|
|
331
364
|
|
|
332
365
|
const current = deps.board.get(childSessionID)
|
|
333
366
|
if (current !== undefined) {
|
|
334
|
-
if (current.state === 'running' || current.state ===
|
|
367
|
+
if (current.state === 'running' || current.state === effectiveOpts.state) {
|
|
335
368
|
await deps.board.updateStatus(status)
|
|
336
369
|
}
|
|
337
370
|
// Terminal in a DIFFERENT state (e.g. timeout already recorded) — the
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { BackgroundJobRecord } from './background-job-board.ts'
|
|
2
|
+
|
|
3
|
+
type ToastCategory = 'errors' | 'delegations' | 'council'
|
|
4
|
+
|
|
5
|
+
const toastMode = (process.env.PANTHEON_TOASTS ?? '').trim().toLowerCase()
|
|
6
|
+
const enabledToastCategories: ReadonlySet<ToastCategory> = (() => {
|
|
7
|
+
switch (toastMode) {
|
|
8
|
+
case 'off':
|
|
9
|
+
return new Set()
|
|
10
|
+
case 'errors':
|
|
11
|
+
return new Set(['errors'])
|
|
12
|
+
case 'council':
|
|
13
|
+
return new Set(['errors', 'council'])
|
|
14
|
+
case 'all':
|
|
15
|
+
return new Set(['errors', 'delegations', 'council'])
|
|
16
|
+
default:
|
|
17
|
+
return new Set(['errors', 'delegations', 'council'])
|
|
18
|
+
}
|
|
19
|
+
})()
|
|
20
|
+
const terminalToastsShown = new Set<string>()
|
|
21
|
+
const terminalStates = new Set(['completed', 'error', 'cancelled'])
|
|
22
|
+
|
|
23
|
+
/** Match the lifecycle toast gate used by pantheon-hooks. */
|
|
24
|
+
export function delegationToastEnabled(): boolean {
|
|
25
|
+
return enabledToastCategories.has('delegations')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ToastClient = {
|
|
29
|
+
tui?: {
|
|
30
|
+
showToast?: (input: {
|
|
31
|
+
body: {
|
|
32
|
+
title?: string
|
|
33
|
+
message: string
|
|
34
|
+
variant: 'info' | 'success' | 'warning' | 'error'
|
|
35
|
+
duration: number
|
|
36
|
+
}
|
|
37
|
+
}) => Promise<unknown>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Best-effort visual completion signal; never affects board finalization. */
|
|
42
|
+
export async function showDelegationTerminalToast(
|
|
43
|
+
client: ToastClient,
|
|
44
|
+
job: Pick<BackgroundJobRecord, 'taskID' | 'alias' | 'agent' | 'state' | 'timedOut'>,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
if (!delegationToastEnabled()) return
|
|
48
|
+
const isTerminal = job.timedOut || terminalStates.has(job.state)
|
|
49
|
+
if (isTerminal) {
|
|
50
|
+
if (terminalToastsShown.has(job.taskID)) return
|
|
51
|
+
terminalToastsShown.add(job.taskID)
|
|
52
|
+
}
|
|
53
|
+
const showToast = client.tui?.showToast
|
|
54
|
+
if (typeof showToast !== 'function') return
|
|
55
|
+
const state = job.timedOut ? 'timeout' : job.state
|
|
56
|
+
const variant =
|
|
57
|
+
state === 'completed'
|
|
58
|
+
? 'success'
|
|
59
|
+
: state === 'error' || state === 'timeout'
|
|
60
|
+
? 'error'
|
|
61
|
+
: 'warning'
|
|
62
|
+
await showToast({
|
|
63
|
+
body: {
|
|
64
|
+
title: 'Delegação concluída',
|
|
65
|
+
message: `${job.alias} (${job.agent}) — ${state}`,
|
|
66
|
+
variant,
|
|
67
|
+
duration: 4000,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
} catch {
|
|
71
|
+
// TUI is optional (and may disappear during shutdown); completion is durable.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -58,14 +58,9 @@ import {
|
|
|
58
58
|
finalizeDelegation as finalizeDelegationReport,
|
|
59
59
|
readDelegationReport,
|
|
60
60
|
} from './delegation-finalize.ts'
|
|
61
|
-
import {
|
|
62
|
-
type IdleChildScanDeps,
|
|
63
|
-
finalizeIdleChildrenWithoutMd,
|
|
64
|
-
startIdleChildScan,
|
|
65
|
-
} from './delegation-notify.ts'
|
|
61
|
+
import { finalizeIdleChildrenWithoutMd } from './delegation-notify.ts'
|
|
66
62
|
import { createPantheonLogger } from './logger.ts'
|
|
67
63
|
import { buildAgentListDescription } from './permission-globs.ts'
|
|
68
|
-
import { missingProviderKeyEnv, resolveActivePreset } from './presets.mjs'
|
|
69
64
|
import { buildStopInstruction, cappedSummary, DEFAULT_MAX_STEPS } from './step-cap.ts'
|
|
70
65
|
|
|
71
66
|
export type {
|
|
@@ -217,8 +212,10 @@ async function bootstrapChild(
|
|
|
217
212
|
const remainingMs = timeoutMs - (now() - startedAt)
|
|
218
213
|
if (hasMessagesAPI) {
|
|
219
214
|
try {
|
|
215
|
+
const messagesAPI = client.session.messages
|
|
216
|
+
if (messagesAPI === undefined) continue
|
|
220
217
|
const messages = await boundedProbe(
|
|
221
|
-
|
|
218
|
+
messagesAPI({ path: { id: childSessionID } }),
|
|
222
219
|
remainingMs,
|
|
223
220
|
)
|
|
224
221
|
if (messages !== undefined) {
|
|
@@ -231,8 +228,10 @@ async function bootstrapChild(
|
|
|
231
228
|
}
|
|
232
229
|
if (hasStatusAPI) {
|
|
233
230
|
try {
|
|
231
|
+
const statusAPI = client.session.get
|
|
232
|
+
if (statusAPI === undefined) continue
|
|
234
233
|
const status = await boundedProbe(
|
|
235
|
-
|
|
234
|
+
statusAPI({ path: { id: childSessionID } }),
|
|
236
235
|
timeoutMs - (now() - startedAt),
|
|
237
236
|
)
|
|
238
237
|
if (status !== undefined) {
|
|
@@ -591,93 +590,59 @@ export interface ChildSessionModelRef {
|
|
|
591
590
|
providerID: string
|
|
592
591
|
}
|
|
593
592
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
* opencode-go exception for the sandbox; an explicit caller-supplied
|
|
604
|
-
* `model` always wins over this fallback.
|
|
605
|
-
*/
|
|
606
|
-
const FALLBACK_MODEL = 'opencode-go/deepseek-v4-flash'
|
|
593
|
+
const MODEL_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._:+-]*$/
|
|
594
|
+
const MODEL_REF_HINT =
|
|
595
|
+
'Expected a non-empty "provider/model-id" reference with exactly one slash, no whitespace/control characters, backslashes, or path traversal. Remove the override or use that format.'
|
|
596
|
+
|
|
597
|
+
type ModelOverrideSource = 'explicit' | 'agentModels'
|
|
598
|
+
|
|
599
|
+
function invalidModelOverride(source: ModelOverrideSource): string {
|
|
600
|
+
return `pantheon_delegate rejected: invalid ${source} model override. ${MODEL_REF_HINT}`
|
|
601
|
+
}
|
|
607
602
|
|
|
608
603
|
/**
|
|
609
604
|
* Split a `provider/model` model ID (e.g. `opencode/deepseek-v4-flash-free`)
|
|
610
605
|
* into the `{ id, providerID }` ref the session.create body expects.
|
|
611
|
-
* Returns undefined for malformed
|
|
606
|
+
* Returns undefined for malformed or unsafe IDs.
|
|
612
607
|
*/
|
|
613
|
-
function splitModelRef(model:
|
|
608
|
+
function splitModelRef(model: unknown): ChildSessionModelRef | undefined {
|
|
609
|
+
if (typeof model !== 'string' || !MODEL_REF_PATTERN.test(model)) return undefined
|
|
614
610
|
const slash = model.indexOf('/')
|
|
615
|
-
if (slash <= 0 || slash === model.length - 1) return undefined
|
|
616
611
|
return { providerID: model.slice(0, slash), id: model.slice(slash + 1) }
|
|
617
612
|
}
|
|
618
613
|
|
|
619
614
|
/**
|
|
620
|
-
* Resolve the child session model
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
* 3. the active preset's agent entry via resolveActivePreset();
|
|
624
|
-
* 4. fallback: undefined — the child is created without a model (the
|
|
625
|
-
* caller warns) so opencode's default applies.
|
|
615
|
+
* Resolve the child session model from explicit or agent-specific overrides.
|
|
616
|
+
* When neither override is present, return undefined so OpenCode can apply
|
|
617
|
+
* native model inheritance.
|
|
626
618
|
*/
|
|
627
619
|
function resolveChildModel(
|
|
628
620
|
options: DelegationOptions,
|
|
629
621
|
agent: string,
|
|
630
622
|
explicitModel?: string,
|
|
631
|
-
): ChildSessionModelRef | undefined {
|
|
623
|
+
): { model: ChildSessionModelRef | undefined; error: string | undefined } {
|
|
632
624
|
// (a) explicit model option on the delegate tool call
|
|
633
|
-
if (explicitModel !== undefined
|
|
625
|
+
if (explicitModel !== undefined) {
|
|
634
626
|
const ref = splitModelRef(explicitModel)
|
|
635
|
-
if (ref !== undefined) return ref
|
|
627
|
+
if (ref !== undefined) return { model: ref, error: undefined }
|
|
628
|
+
return { model: undefined, error: invalidModelOverride('explicit') }
|
|
636
629
|
}
|
|
637
630
|
const key = agent.toLowerCase()
|
|
638
|
-
// (b) routing.yml agent entry
|
|
631
|
+
// (b) agent-specific model override (routing.yml agent entry via agentModels)
|
|
639
632
|
const mapped = options.agentModels?.[key]
|
|
640
|
-
if (mapped !== undefined
|
|
633
|
+
if (mapped !== undefined) {
|
|
641
634
|
const ref = splitModelRef(mapped)
|
|
642
|
-
if (ref !== undefined) return ref
|
|
643
|
-
|
|
644
|
-
// (c) active preset agent entry (resolveActivePreset reads routing.yml
|
|
645
|
-
// presets + .pantheon/active-preset.json). Guarded — a broken routing.yml
|
|
646
|
-
// must never kill the delegate.
|
|
647
|
-
try {
|
|
648
|
-
const presetModel = resolveActivePreset({
|
|
649
|
-
...(options.presetEnv !== undefined ? { env: options.presetEnv } : {}),
|
|
650
|
-
...(options.logger !== undefined ? { logger: options.logger } : {}),
|
|
651
|
-
})?.agents?.[key]?.model
|
|
652
|
-
if (presetModel !== undefined && presetModel !== '') {
|
|
653
|
-
const ref = splitModelRef(presetModel)
|
|
654
|
-
if (ref !== undefined) return ref
|
|
655
|
-
}
|
|
656
|
-
} catch (err: unknown) {
|
|
657
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
658
|
-
options.logger?.warn?.(`[pantheon-delegate] active preset resolution failed: ${reason}`)
|
|
635
|
+
if (ref !== undefined) return { model: ref, error: undefined }
|
|
636
|
+
return { model: undefined, error: invalidModelOverride('agentModels') }
|
|
659
637
|
}
|
|
660
|
-
return undefined
|
|
638
|
+
return { model: undefined, error: undefined }
|
|
661
639
|
}
|
|
662
640
|
|
|
663
641
|
/**
|
|
664
|
-
* Resolve a
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
* options.agentModels > active preset) — see resolveChildModel.
|
|
669
|
-
* 2. provider key gate: the resolved provider requires its apiKeyEnv env
|
|
670
|
-
* var (routing.yml preset defs — same source applyPreset enforces at
|
|
671
|
-
* startup). No gate (native providers) or key present → usable as-is.
|
|
672
|
-
* 3. key MISSING + EXPLICIT caller model → respected anyway (user intent),
|
|
673
|
-
* warned.
|
|
674
|
-
* 4. key MISSING + AUTO-resolved model → fall back to FALLBACK_MODEL
|
|
675
|
-
* (validated the same way). Fallback also unusable → returns an `error`
|
|
676
|
-
* TEXT (the caller returns it from the tool — never throws) and the
|
|
677
|
-
* caller registers NO job on the board.
|
|
678
|
-
*
|
|
679
|
-
* The no-model case (nothing resolved) keeps the pre-existing behavior:
|
|
680
|
-
* warn and let the child use opencode's default.
|
|
642
|
+
* Resolve a child model without selecting a provider or model on the caller's
|
|
643
|
+
* behalf. OpenCode owns native inheritance when this returns no model; if the
|
|
644
|
+
* host requires one, its session.create/promptAsync error is returned with
|
|
645
|
+
* the normal diagnostic rather than silently choosing a fallback.
|
|
681
646
|
*
|
|
682
647
|
* @returns `{ model, error }` — at most one of `model` / `error` is set;
|
|
683
648
|
* both may be unset (no model resolved, warn emitted).
|
|
@@ -687,50 +652,13 @@ function resolveUsableChildModel(
|
|
|
687
652
|
agent: string,
|
|
688
653
|
explicitModel: string | undefined,
|
|
689
654
|
): { model: ChildSessionModelRef | undefined; error: string | undefined } {
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
`[pantheon-delegate] no model resolved for agent "${agent}" — ` +
|
|
698
|
-
"child session will use opencode's default model (may require API keys)",
|
|
699
|
-
)
|
|
700
|
-
return { model: undefined, error: undefined }
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const missingVar = missingProviderKeyEnv(model.providerID, { env })
|
|
704
|
-
if (missingVar === undefined) return { model, error: undefined }
|
|
705
|
-
|
|
706
|
-
if (explicit) {
|
|
707
|
-
// Rule of precedence: the caller's explicit model is respected — warn only.
|
|
708
|
-
warn(
|
|
709
|
-
`[pantheon-delegate] model "${explicitModel}" provider "${model.providerID}" requires ` +
|
|
710
|
-
`API key ${missingVar} (unset) — respecting the explicit model anyway`,
|
|
711
|
-
)
|
|
712
|
-
return { model, error: undefined }
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Auto-resolved (agentModels / preset) → try the known-good fallback.
|
|
716
|
-
const fallback = splitModelRef(FALLBACK_MODEL)
|
|
717
|
-
if (
|
|
718
|
-
fallback !== undefined &&
|
|
719
|
-
(fallback.providerID === 'opencode-go' ||
|
|
720
|
-
missingProviderKeyEnv(fallback.providerID, { env }) === undefined)
|
|
721
|
-
) {
|
|
722
|
-
warn(
|
|
723
|
-
`[pantheon-delegate] provider "${model.providerID}" requires API key ${missingVar} ` +
|
|
724
|
-
`(unset) — falling back to ${FALLBACK_MODEL}`,
|
|
725
|
-
)
|
|
726
|
-
return { model: fallback, error: undefined }
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const message =
|
|
730
|
-
`pantheon_delegate: no usable model for agent "${agent}" — provider "${model.providerID}" ` +
|
|
731
|
-
`requires API key (set ${missingVar} or PANTHEON_MODEL_PRESET)`
|
|
732
|
-
warn(`[pantheon-delegate] ${message}`)
|
|
733
|
-
return { model: undefined, error: message }
|
|
655
|
+
const resolved = resolveChildModel(options, agent, explicitModel)
|
|
656
|
+
if (resolved.error !== undefined || resolved.model !== undefined) return resolved
|
|
657
|
+
options.logger?.warn?.(
|
|
658
|
+
`[pantheon-delegate] no model resolved for agent "${agent}" — ` +
|
|
659
|
+
'omitting model so OpenCode can inherit it from the parent session',
|
|
660
|
+
)
|
|
661
|
+
return resolved
|
|
734
662
|
}
|
|
735
663
|
|
|
736
664
|
// ─── Factory ───────────────────────────────────────────────────────────
|
|
@@ -900,13 +828,9 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
900
828
|
// session.create failure → return a clear error as TEXT (tools return
|
|
901
829
|
// errors as text, not thrown) and register NO job on the board — an
|
|
902
830
|
// unhandled rejection here would otherwise lose the failure entirely.
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
// caller model is always respected (warned); an auto-resolved model
|
|
907
|
-
// whose provider key is missing falls back to the known-good
|
|
908
|
-
// opencode-go/deepseek-v4-flash; if nothing usable remains the
|
|
909
|
-
// failure is returned as TEXT below — no session, no board job.
|
|
831
|
+
// Resolution order: explicit delegate model > active-preset agent model
|
|
832
|
+
// > omit (native inheritance). small_model is never used for delegated
|
|
833
|
+
// children.
|
|
910
834
|
const resolved = resolveUsableChildModel(options, args.agent, args.model)
|
|
911
835
|
if (resolved.error !== undefined) {
|
|
912
836
|
if (budgetReserved && budgetKey !== undefined)
|