pantheon-opencode 1.5.0-beta.14 → 1.5.0-beta.16
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/AGENTS.md +14 -63
- package/CHANGELOG.md +65 -0
- package/README.md +1 -2
- package/README.pt-BR.md +1 -1
- package/commands/pantheon.md +1 -35
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/src/agents/zeus.md +4 -26
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/toon_codec.py +2 -4
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/delegate-manager.ts +95 -10
- package/src/pantheon/delegation-classifier.ts +3 -4
- package/src/pantheon/delegation-finalize.ts +69 -7
- package/src/pantheon/delegation.ts +16 -4
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/plugin-v2.ts +4 -6
- package/src/plugin.ts +6 -6
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +43 -36
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +102 -38
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +168 -67
- package/src/routing.yml +0 -16
- package/scripts/dev-tui.sh +0 -48
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
* @module delegate-manager
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { join } from 'node:path'
|
|
26
27
|
import { z } from 'zod'
|
|
27
28
|
import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
|
|
28
29
|
import type { DelegationToolset } from './delegation.ts'
|
|
29
30
|
import {
|
|
31
|
+
capReportOutput,
|
|
30
32
|
DELEGATION_DEFAULTS,
|
|
31
33
|
type DelegationClient,
|
|
32
34
|
readDelegationReport,
|
|
35
|
+
resolveReadMaxChars,
|
|
33
36
|
} from './delegation-finalize.ts'
|
|
34
37
|
import { createNativeProbe, type NativeProbeFn, type ProbeClient } from './native-probe.ts'
|
|
35
38
|
import { classifyNativeResult, type NativeTaskStatus } from './native-task-status.ts'
|
|
@@ -66,10 +69,14 @@ export type NativeTaskFn = (input: NativeTaskInput) => Promise<NativeTaskResult>
|
|
|
66
69
|
/** Minimal launch receipt: id, agent, state, one short line. */
|
|
67
70
|
export interface ManagerReceipt {
|
|
68
71
|
id: string
|
|
72
|
+
/** Board alias of the job (e.g. `apo-1`) — report files are keyed by it. */
|
|
73
|
+
alias: string
|
|
69
74
|
agent: string
|
|
70
75
|
state: 'reconciled' | 'error'
|
|
71
76
|
status: NativeTaskStatus
|
|
72
77
|
line: string
|
|
78
|
+
/** Verified child output (when the task completed with content). */
|
|
79
|
+
output?: string
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
export interface LaunchArgs {
|
|
@@ -109,6 +116,15 @@ export interface DelegateManagerOptions {
|
|
|
109
116
|
nativeProbe?: NativeProbeFn
|
|
110
117
|
/** V1 client used by the lazy capability probe. */
|
|
111
118
|
probeClient?: ProbeClient
|
|
119
|
+
/** Base directory for delegation reports (default `.pantheon/delegations`). */
|
|
120
|
+
outputDir?: string
|
|
121
|
+
/**
|
|
122
|
+
* Inline cap (chars) for report content returned to the caller. Oversized
|
|
123
|
+
* reports keep head+tail with an explicit `[TRUNCATED …]` marker pointing
|
|
124
|
+
* at the full report file. Default: PANTHEON_DELEGATION_READ_MAX_CHARS
|
|
125
|
+
* or 50_000.
|
|
126
|
+
*/
|
|
127
|
+
readMaxChars?: number
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
export interface DelegateManager {
|
|
@@ -139,7 +155,9 @@ export function resolveDelegateMode(
|
|
|
139
155
|
|
|
140
156
|
const LINE_MAX = 160
|
|
141
157
|
|
|
142
|
-
/** Remove terminal escapes and control characters from untrusted child output.
|
|
158
|
+
/** Remove terminal escapes and control characters from untrusted child output.
|
|
159
|
+
* UTF-8 text (accents, emoji) is PRESERVED — only ANSI sequences, C0/C1
|
|
160
|
+
* controls and DEL are stripped; receipt/report lines stay human-readable. */
|
|
143
161
|
export function sanitizeReceiptText(text: string): string {
|
|
144
162
|
let clean = ''
|
|
145
163
|
let skippingAnsi = false
|
|
@@ -154,7 +172,8 @@ export function sanitizeReceiptText(text: string): string {
|
|
|
154
172
|
skippingAnsi = true
|
|
155
173
|
continue
|
|
156
174
|
}
|
|
157
|
-
|
|
175
|
+
const isControl = code < 0x20 || (code >= 0x7f && code <= 0x9f)
|
|
176
|
+
if (!isControl || char === '\n' || char === '\t') clean += char
|
|
158
177
|
}
|
|
159
178
|
return clean
|
|
160
179
|
}
|
|
@@ -219,7 +238,13 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
219
238
|
stepCap,
|
|
220
239
|
nativeProbe,
|
|
221
240
|
probeClient,
|
|
241
|
+
outputDir = DELEGATION_DEFAULTS.outputDir,
|
|
242
|
+
readMaxChars,
|
|
222
243
|
} = options
|
|
244
|
+
const inlineMaxChars = readMaxChars ?? resolveReadMaxChars(env)
|
|
245
|
+
|
|
246
|
+
/** Report file path for a job alias (the durable full report). */
|
|
247
|
+
const reportPathFor = (alias: string): string => join(outputDir, parentSessionID, `${alias}.md`)
|
|
223
248
|
|
|
224
249
|
// Strict native mode: foregroundFallback and models are overridden.
|
|
225
250
|
// The user-facing options are ignored when PANTHEON_DELEGATE_MODE=native.
|
|
@@ -310,7 +335,14 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
310
335
|
LINE_MAX,
|
|
311
336
|
)
|
|
312
337
|
: boardLine(settled ?? job)
|
|
313
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
id: taskID,
|
|
340
|
+
alias,
|
|
341
|
+
agent: job.agent,
|
|
342
|
+
state: failed ? 'error' : 'reconciled',
|
|
343
|
+
status,
|
|
344
|
+
line,
|
|
345
|
+
}
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
/** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
|
|
@@ -354,7 +386,11 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
354
386
|
await applyTerminal(taskID, 'completed', {
|
|
355
387
|
resultSummary: firstLine(outcome.result.content, 500),
|
|
356
388
|
})
|
|
357
|
-
|
|
389
|
+
const settled = await settle(taskID, record.alias)
|
|
390
|
+
// The task() call already blocked until completion — return the verified
|
|
391
|
+
// output in the receipt (the parent reads the full report via the alias
|
|
392
|
+
// when needed). The tool layer caps it inline.
|
|
393
|
+
return { ...settled, output: outcome.result.content }
|
|
358
394
|
} finally {
|
|
359
395
|
pendingByAgent.set(key, Math.max(0, pending(args.agent) - 1))
|
|
360
396
|
}
|
|
@@ -403,7 +439,17 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
403
439
|
},
|
|
404
440
|
ok ? undefined : (outcome as { ok: false; error?: string }).error,
|
|
405
441
|
)
|
|
406
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
id,
|
|
444
|
+
alias: id,
|
|
445
|
+
agent: args.agent,
|
|
446
|
+
state: ok ? 'reconciled' : 'error',
|
|
447
|
+
status,
|
|
448
|
+
line,
|
|
449
|
+
...(ok
|
|
450
|
+
? { output: (outcome as { ok: true; result: NativeTaskResult }).result.content ?? '' }
|
|
451
|
+
: {}),
|
|
452
|
+
}
|
|
407
453
|
}
|
|
408
454
|
|
|
409
455
|
return {
|
|
@@ -416,8 +462,10 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
416
462
|
const maxSteps = stepCap.maxStepsFor(args.agent)
|
|
417
463
|
if (stepCap.isCapped(args.agent)) {
|
|
418
464
|
const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
|
|
465
|
+
const cappedId = `cap-${Date.now()}`
|
|
419
466
|
return {
|
|
420
|
-
id:
|
|
467
|
+
id: cappedId,
|
|
468
|
+
alias: cappedId,
|
|
421
469
|
agent: args.agent,
|
|
422
470
|
state: 'reconciled',
|
|
423
471
|
status: 'ESCALATE',
|
|
@@ -432,8 +480,10 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
432
480
|
if (isNativeMode) {
|
|
433
481
|
const unavailableStatus = await ensureNativeAvailable()
|
|
434
482
|
if (unavailableStatus !== undefined) {
|
|
483
|
+
const probeId = `probe-${parentSessionID}`
|
|
435
484
|
return {
|
|
436
|
-
id:
|
|
485
|
+
id: probeId,
|
|
486
|
+
alias: probeId,
|
|
437
487
|
agent: args.agent,
|
|
438
488
|
state: 'error',
|
|
439
489
|
status: unavailableStatus,
|
|
@@ -464,6 +514,13 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
464
514
|
)
|
|
465
515
|
if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
|
|
466
516
|
await board.deleteSignal(terminal.alias)
|
|
517
|
+
// Full report first (same contract as the legacy engine): the md file
|
|
518
|
+
// written by finalizeDelegation carries the complete child output. The
|
|
519
|
+
// one-line resultSummary is only a fallback when no report was written.
|
|
520
|
+
const md = await readDelegationReport(outputDir, terminal)
|
|
521
|
+
if (md !== undefined) {
|
|
522
|
+
return capReportOutput(md, inlineMaxChars, reportPathFor(terminal.alias))
|
|
523
|
+
}
|
|
467
524
|
if (terminal.state === 'error' && !terminal.resultSummary) {
|
|
468
525
|
return `# Delegation Report — ${terminal.alias}\n\nERROR: ${sanitizeReceiptText(terminal.lastStatusError ?? 'failed')}\n`
|
|
469
526
|
}
|
|
@@ -640,6 +697,7 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
|
|
|
640
697
|
? {}
|
|
641
698
|
: { promptTimeoutMs: managerOptions.timeoutMs }),
|
|
642
699
|
})
|
|
700
|
+
const inlineMaxChars = managerOptions.readMaxChars ?? resolveReadMaxChars(env)
|
|
643
701
|
const managers = new Map<string, DelegateManager>()
|
|
644
702
|
const managerFor = (parentSessionID: string): DelegateManager => {
|
|
645
703
|
const existing = managers.get(parentSessionID)
|
|
@@ -652,6 +710,7 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
|
|
|
652
710
|
parentSessionID,
|
|
653
711
|
env,
|
|
654
712
|
...(stepCap === undefined ? {} : { stepCap }),
|
|
713
|
+
...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
|
|
655
714
|
nativeProbe,
|
|
656
715
|
probeClient: client,
|
|
657
716
|
})
|
|
@@ -702,7 +761,21 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
|
|
|
702
761
|
...(args.model === undefined ? {} : { model: args.model }),
|
|
703
762
|
taskID: childID,
|
|
704
763
|
})
|
|
705
|
-
return
|
|
764
|
+
// The launch blocked until the child finished — return the verified
|
|
765
|
+
// output inline (capped head+tail with a pointer to the full report),
|
|
766
|
+
// not just a status line. Foreground receipts carry no report file,
|
|
767
|
+
// so the marker points at pantheon_delegation_read instead.
|
|
768
|
+
const header = `status: ${receipt.status}\n${receipt.line}`
|
|
769
|
+
if (receipt.output === undefined || receipt.output.trim() === '') return header
|
|
770
|
+
const reportPath =
|
|
771
|
+
receipt.alias === receipt.id
|
|
772
|
+
? undefined
|
|
773
|
+
: join(
|
|
774
|
+
wiring.outputDir ?? DELEGATION_DEFAULTS.outputDir,
|
|
775
|
+
ctx.sessionID,
|
|
776
|
+
`${receipt.alias}.md`,
|
|
777
|
+
)
|
|
778
|
+
return `${header}\n\n${capReportOutput(receipt.output, inlineMaxChars, reportPath)}`
|
|
706
779
|
},
|
|
707
780
|
},
|
|
708
781
|
pantheon_delegation_read: {
|
|
@@ -739,6 +812,10 @@ export interface PluginNativeDelegationWiring {
|
|
|
739
812
|
agentModels: Record<string, string | undefined>
|
|
740
813
|
stepCap: StepCapTracker
|
|
741
814
|
wallClockTimeoutMs?: number
|
|
815
|
+
/** Base directory for delegation reports (default `.pantheon/delegations`). */
|
|
816
|
+
outputDir?: string
|
|
817
|
+
/** Inline cap (chars) for delegation report content (default env or 50_000). */
|
|
818
|
+
readMaxChars?: number
|
|
742
819
|
finalizeDelegation: DelegationToolset['finalizeDelegation']
|
|
743
820
|
}
|
|
744
821
|
export type PluginNativeDelegation = NativeDelegateToolset & {
|
|
@@ -751,15 +828,23 @@ export function buildPluginNativeDelegation(
|
|
|
751
828
|
...createNativeDelegateTools({
|
|
752
829
|
board: wiring.board,
|
|
753
830
|
client: wiring.client,
|
|
831
|
+
...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
|
|
754
832
|
isRootSession: wiring.isRootSession,
|
|
755
833
|
registerChildSession: wiring.registerChildSession,
|
|
756
834
|
registerReadOnlySession: wiring.registerReadOnlySession,
|
|
757
835
|
isReadOnlyAgent: (agent) => wiring.readOnlyAgents.has(agent.toLowerCase()),
|
|
758
836
|
agentModel: (agent) => wiring.agentModels[agent.toLowerCase()],
|
|
759
837
|
stepCap: wiring.stepCap,
|
|
760
|
-
...(wiring.wallClockTimeoutMs === undefined
|
|
838
|
+
...(wiring.wallClockTimeoutMs === undefined && wiring.readMaxChars === undefined
|
|
761
839
|
? {}
|
|
762
|
-
: {
|
|
840
|
+
: {
|
|
841
|
+
managerOptions: {
|
|
842
|
+
...(wiring.wallClockTimeoutMs === undefined
|
|
843
|
+
? {}
|
|
844
|
+
: { timeoutMs: wiring.wallClockTimeoutMs }),
|
|
845
|
+
...(wiring.readMaxChars === undefined ? {} : { readMaxChars: wiring.readMaxChars }),
|
|
846
|
+
},
|
|
847
|
+
}),
|
|
763
848
|
}),
|
|
764
849
|
finalizeDelegation: wiring.finalizeDelegation,
|
|
765
850
|
}
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* - `timeout` (timeout-with-partial / timeout-empty / timeout-exhausted)
|
|
9
9
|
* - `error` — provider or infrastructure failure
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* empty-response detection and provides a structured `DelegationResult`
|
|
11
|
+
* Provides empty-response detection and a structured `DelegationResult`
|
|
13
12
|
* that replaces raw-text returns.
|
|
14
13
|
*
|
|
15
14
|
* @module delegation-classifier
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
|
|
18
17
|
// ─── Types ─────────────────────────────────────────────────────────────
|
|
19
18
|
|
|
20
|
-
/** Empty-response mode classification (
|
|
19
|
+
/** Empty-response mode classification (empty-mode1 / empty-mode2). */
|
|
21
20
|
export type EmptyMode = 'empty-mode1' | 'empty-mode2'
|
|
22
21
|
|
|
23
22
|
/** Timeout sub-type based on partial output and retry history. */
|
|
@@ -82,7 +81,7 @@ const NO_OUTPUT_RE = /_No output captured\._/i
|
|
|
82
81
|
|
|
83
82
|
/**
|
|
84
83
|
* Classify whether a response is empty, empty-with-tokens, or has content.
|
|
85
|
-
*
|
|
84
|
+
* Classifies empty-mode1 (no text/tokens) vs empty-mode2 (tokens, no text).
|
|
86
85
|
*
|
|
87
86
|
* @param text The response text (may be null/undefined).
|
|
88
87
|
* @param tokens Optional token counts for mode2 detection.
|
|
@@ -157,6 +157,13 @@ export interface DelegationOptions {
|
|
|
157
157
|
/** Injectable clock/sleeper for deterministic watchdog tests. */
|
|
158
158
|
now?: () => number
|
|
159
159
|
sleep?: (ms: number) => Promise<void>
|
|
160
|
+
/**
|
|
161
|
+
* Inline cap (chars) for report content returned by pantheon_delegation_read.
|
|
162
|
+
* Oversized reports keep head+tail with an explicit `[TRUNCATED …]` marker
|
|
163
|
+
* pointing at the full report file. Default: PANTHEON_DELEGATION_READ_MAX_CHARS
|
|
164
|
+
* or 50_000.
|
|
165
|
+
*/
|
|
166
|
+
readMaxChars?: number
|
|
160
167
|
/**
|
|
161
168
|
* Kill-switch: when false, all delegation tools (delegate/read/list) throw
|
|
162
169
|
* immediately. Mirrors `PANTHEON_DELEGATION=off` from the native manager.
|
|
@@ -276,6 +283,62 @@ function summarizeOutput(output: string): string {
|
|
|
276
283
|
return firstLine.length > 120 ? `${firstLine.slice(0, 120)}…` : firstLine
|
|
277
284
|
}
|
|
278
285
|
|
|
286
|
+
// ─── Report inline cap ─────────────────────────────────────────────────
|
|
287
|
+
//
|
|
288
|
+
// Delegation reports are returned INLINE to the orchestrator (tool result).
|
|
289
|
+
// A very large child output must be capped BEFORE it floods the parent
|
|
290
|
+
// context — but a hard mid-content cut loses the report header AND the
|
|
291
|
+
// conclusion. The cap keeps head + tail with an explicit marker pointing at
|
|
292
|
+
// the full report (the durable `.pantheon/delegations/<parent>/<alias>.md`
|
|
293
|
+
// file or the read tool), mirroring how comparable agent hosts surface
|
|
294
|
+
// oversized tool output.
|
|
295
|
+
|
|
296
|
+
/** Default inline cap for delegation report content (chars). */
|
|
297
|
+
export const DELEGATION_READ_MAX_CHARS = 50_000
|
|
298
|
+
/** Lower bound for PANTHEON_DELEGATION_READ_MAX_CHARS. */
|
|
299
|
+
export const DELEGATION_READ_MAX_CHARS_MIN = 1_000
|
|
300
|
+
|
|
301
|
+
/** Resolve the inline report cap from the environment (default 50k, min 1k). */
|
|
302
|
+
export function resolveReadMaxChars(env: Record<string, string | undefined> = process.env): number {
|
|
303
|
+
const raw = env.PANTHEON_DELEGATION_READ_MAX_CHARS?.trim()
|
|
304
|
+
if (raw === undefined || raw === '') return DELEGATION_READ_MAX_CHARS
|
|
305
|
+
const value = Number(raw)
|
|
306
|
+
return Number.isSafeInteger(value) && value >= DELEGATION_READ_MAX_CHARS_MIN
|
|
307
|
+
? value
|
|
308
|
+
: DELEGATION_READ_MAX_CHARS
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Cap report markdown for inline return: ≤ `maxChars` passes through
|
|
313
|
+
* untouched; anything larger keeps the head (60% of the budget) + tail (25%)
|
|
314
|
+
* with an explicit `[TRUNCATED …]` marker between them. `reportPath` names
|
|
315
|
+
* the durable full-report file in the marker; without it the marker points
|
|
316
|
+
* at `pantheon_delegation_read`. Pure — no I/O.
|
|
317
|
+
*/
|
|
318
|
+
export function capReportOutput(md: string, maxChars: number, reportPath?: string): string {
|
|
319
|
+
if (md.length <= maxChars) return md
|
|
320
|
+
if (maxChars < 200) {
|
|
321
|
+
// Budget too small for a head+tail split — keep what fits after the marker.
|
|
322
|
+
const marker = capReportMarker(md.length, 0, reportPath)
|
|
323
|
+
return `${md.slice(0, Math.max(0, maxChars - marker.length - 1))}\n${marker}`
|
|
324
|
+
}
|
|
325
|
+
const headLen = Math.floor(maxChars * 0.6)
|
|
326
|
+
const tailLen = Math.floor(maxChars * 0.25)
|
|
327
|
+
return `${md.slice(0, headLen)}\n${capReportMarker(md.length, headLen + tailLen, reportPath)}\n${md.slice(md.length - tailLen)}`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function capReportMarker(
|
|
331
|
+
totalChars: number,
|
|
332
|
+
keptChars: number,
|
|
333
|
+
reportPath: string | undefined,
|
|
334
|
+
): string {
|
|
335
|
+
const where =
|
|
336
|
+
reportPath !== undefined
|
|
337
|
+
? `full report: ${reportPath}`
|
|
338
|
+
: 'read the full report with pantheon_delegation_read'
|
|
339
|
+
return `[TRUNCATED: ${totalChars - keptChars} of ${totalChars} chars hidden — ${where}]`
|
|
340
|
+
}
|
|
341
|
+
|
|
279
342
|
// ─── Report persistence ────────────────────────────────────────────────
|
|
280
343
|
|
|
281
344
|
/**
|
|
@@ -387,13 +450,12 @@ export async function finalizeDelegation(
|
|
|
387
450
|
// report above still captures this finalize; the board state stands.
|
|
388
451
|
}
|
|
389
452
|
|
|
390
|
-
// P1-1: Delete the
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// double-processing. deleteSignal on finalize is sufficient.
|
|
453
|
+
// P1-1: Delete the wake signal file to prevent leaks. The board writes
|
|
454
|
+
// .signal.json on every terminal transition, but the legacy path never
|
|
455
|
+
// called deleteSignal — only the native delegate-manager did.
|
|
456
|
+
// finalizeDelegation already handles the terminal transition, so
|
|
457
|
+
// consuming signals would be redundant; deleteSignal on finalize is
|
|
458
|
+
// sufficient.
|
|
397
459
|
await deps.board.deleteSignal(job.alias)
|
|
398
460
|
|
|
399
461
|
// P1-4: Prune old completed jobs and enforce the hard entry cap. The
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* @module delegation
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
import { join } from 'node:path'
|
|
38
39
|
import { z } from 'zod'
|
|
39
40
|
|
|
40
41
|
import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
readOnlyRegistry,
|
|
50
51
|
} from './delegation-enforce.ts'
|
|
51
52
|
import {
|
|
53
|
+
capReportOutput,
|
|
52
54
|
DELEGATION_DEFAULTS,
|
|
53
55
|
type DelegationClient,
|
|
54
56
|
type DelegationDeps,
|
|
@@ -57,6 +59,7 @@ import {
|
|
|
57
59
|
type FinalizeInput,
|
|
58
60
|
finalizeDelegation as finalizeDelegationReport,
|
|
59
61
|
readDelegationReport,
|
|
62
|
+
resolveReadMaxChars,
|
|
60
63
|
} from './delegation-finalize.ts'
|
|
61
64
|
import { finalizeIdleChildrenWithoutMd } from './delegation-notify.ts'
|
|
62
65
|
import { createPantheonLogger } from './logger.ts'
|
|
@@ -814,6 +817,10 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
814
817
|
|
|
815
818
|
const knownChildren = new Set<string>()
|
|
816
819
|
const timers = new Map<string, NodeJS.Timeout>()
|
|
820
|
+
// Inline cap for report content returned by pantheon_delegation_read —
|
|
821
|
+
// oversized reports keep head+tail with an explicit marker pointing at the
|
|
822
|
+
// full report file (never a silent mid-content cut).
|
|
823
|
+
const inlineMaxChars = options.readMaxChars ?? resolveReadMaxChars()
|
|
817
824
|
// Fase B3: per-parent-session delegation budget tracking, scoped to this
|
|
818
825
|
// factory/process instance. Keyed by `${parentSessionID}:${parentAgent}->${targetAgent}`;
|
|
819
826
|
// it is not restart-persistent because the factory has no reliable store.
|
|
@@ -1276,6 +1283,11 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
1276
1283
|
if (md === undefined) {
|
|
1277
1284
|
return `Delegation [${terminal.alias}] reached state ${terminal.state} but no report file was found.`
|
|
1278
1285
|
}
|
|
1286
|
+
// Classify on the FULL report (markers may sit at either end), but
|
|
1287
|
+
// return the inline-capped markdown so a huge child output cannot
|
|
1288
|
+
// flood the parent context — the marker points at the full file.
|
|
1289
|
+
const reportPath = join(outputDir, terminal.parentSessionID, `${terminal.alias}.md`)
|
|
1290
|
+
const cappedMd = capReportOutput(md, inlineMaxChars, reportPath)
|
|
1279
1291
|
|
|
1280
1292
|
// Classify the result for stuck-agent detection
|
|
1281
1293
|
const classification = classifyStuckAgent(md)
|
|
@@ -1284,15 +1296,15 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
1284
1296
|
|
|
1285
1297
|
if (classification.status === 'success') {
|
|
1286
1298
|
await board.markReconciled(job.taskID)
|
|
1287
|
-
// Backward compatible: success reports returned as-is
|
|
1288
|
-
if (!hasActivity) return
|
|
1289
|
-
return `${
|
|
1299
|
+
// Backward compatible: success reports returned as-is (capped)
|
|
1300
|
+
if (!hasActivity) return cappedMd
|
|
1301
|
+
return `${cappedMd.replace(/\n+$/, '')}${activitySuffix}`
|
|
1290
1302
|
}
|
|
1291
1303
|
|
|
1292
1304
|
// Non-success: build structured DelegationResult and format
|
|
1293
1305
|
const delegationResult: DelegationResult = {
|
|
1294
1306
|
status: classification.status === 'empty' ? 'empty' : classification.status,
|
|
1295
|
-
content:
|
|
1307
|
+
content: cappedMd,
|
|
1296
1308
|
retryCount: 0,
|
|
1297
1309
|
partialResult: classification.partialResult,
|
|
1298
1310
|
recommendation: classification.recommendation,
|
|
@@ -104,26 +104,6 @@ export interface RoutingRetryPolicyOptions {
|
|
|
104
104
|
logger?: { warn?: (msg: string) => void }
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* R1: per-error-type retry policy from routing.yml `retry_policy`
|
|
109
|
-
* (auth/rate_limit/timeout/other → max retries). Fail-open → null.
|
|
110
|
-
*/
|
|
111
|
-
export declare function loadRoutingRetryPolicy(
|
|
112
|
-
options?: RoutingRetryPolicyOptions,
|
|
113
|
-
): Record<string, number> | null
|
|
114
|
-
|
|
115
|
-
export interface RoutingCooldownResult {
|
|
116
|
-
allowed_fails: number
|
|
117
|
-
cooldown_time_seconds: number
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* R1: provider cooldown config from routing.yml `cooldown`. Fail-open → null.
|
|
122
|
-
*/
|
|
123
|
-
export declare function loadRoutingCooldown(
|
|
124
|
-
options?: RoutingRetryPolicyOptions,
|
|
125
|
-
): RoutingCooldownResult | null
|
|
126
|
-
|
|
127
107
|
/**
|
|
128
108
|
* R4: per-agent step caps from routing.yml `agents.<name>.max_steps`.
|
|
129
109
|
* Fail-open → {} (no agent capped).
|
package/src/pantheon/presets.mjs
CHANGED
|
@@ -213,70 +213,6 @@ function loadRoutingYaml(routingPath) {
|
|
|
213
213
|
return routing && typeof routing === 'object' ? routing : null
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
const RETRY_ERROR_TYPES = new Set(['auth', 'rate_limit', 'timeout', 'other'])
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Load the R1 per-error-type retry policy from routing.yml (`retry_policy:`).
|
|
220
|
-
* Fail-open: missing/unparseable config yields null (the caller falls back
|
|
221
|
-
* to DEFAULT_RETRY_POLICY) and warns via the optional logger.
|
|
222
|
-
*
|
|
223
|
-
* @param {object} [opts]
|
|
224
|
-
* @param {string} [opts.routingPath]
|
|
225
|
-
* @param {{warn?: Function}} [opts.logger]
|
|
226
|
-
* @returns {Record<string, number>|null} error-type → max retries
|
|
227
|
-
*/
|
|
228
|
-
export function loadRoutingRetryPolicy({ routingPath, logger = console } = {}) {
|
|
229
|
-
try {
|
|
230
|
-
const routing = loadRoutingYaml(routingPath)
|
|
231
|
-
const policy = routing?.retry_policy
|
|
232
|
-
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) return null
|
|
233
|
-
const out = {}
|
|
234
|
-
for (const [type, max] of Object.entries(policy)) {
|
|
235
|
-
if (RETRY_ERROR_TYPES.has(type) && Number.isSafeInteger(max) && max >= 0) {
|
|
236
|
-
out[type] = max
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return Object.keys(out).length > 0 ? out : null
|
|
240
|
-
} catch (err) {
|
|
241
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
242
|
-
logger.warn?.(`presets: routing.yml retry_policy unavailable (${reason})`)
|
|
243
|
-
return null
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Load the R1 provider cooldown config from routing.yml (`cooldown:`).
|
|
249
|
-
* Fail-open: missing/unparseable config yields null (caller falls back to
|
|
250
|
-
* DEFAULT_COOLDOWN).
|
|
251
|
-
*
|
|
252
|
-
* @param {object} [opts]
|
|
253
|
-
* @param {string} [opts.routingPath]
|
|
254
|
-
* @param {{warn?: Function}} [opts.logger]
|
|
255
|
-
* @returns {{allowed_fails: number, cooldown_time_seconds: number}|null}
|
|
256
|
-
*/
|
|
257
|
-
export function loadRoutingCooldown({ routingPath, logger = console } = {}) {
|
|
258
|
-
try {
|
|
259
|
-
const routing = loadRoutingYaml(routingPath)
|
|
260
|
-
const cooldown = routing?.cooldown
|
|
261
|
-
if (!cooldown || typeof cooldown !== 'object' || Array.isArray(cooldown)) return null
|
|
262
|
-
const allowedFails = cooldown.allowed_fails
|
|
263
|
-
const cooldownTimeSeconds = cooldown.cooldown_time_seconds
|
|
264
|
-
if (
|
|
265
|
-
!Number.isSafeInteger(allowedFails) ||
|
|
266
|
-
allowedFails < 1 ||
|
|
267
|
-
!Number.isSafeInteger(cooldownTimeSeconds) ||
|
|
268
|
-
cooldownTimeSeconds < 1
|
|
269
|
-
) {
|
|
270
|
-
return null
|
|
271
|
-
}
|
|
272
|
-
return { allowed_fails: allowedFails, cooldown_time_seconds: cooldownTimeSeconds }
|
|
273
|
-
} catch (err) {
|
|
274
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
275
|
-
logger.warn?.(`presets: routing.yml cooldown unavailable (${reason})`)
|
|
276
|
-
return null
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
216
|
/**
|
|
281
217
|
* Load the R4 per-agent step caps from routing.yml
|
|
282
218
|
* (`agents.<name>.max_steps`). Fail-open: missing config yields {}.
|
package/src/plugin-v2.ts
CHANGED
|
@@ -681,12 +681,10 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
|
|
|
681
681
|
name,
|
|
682
682
|
description: toolDescriptions[name] ?? `Pantheon tool: ${name}`,
|
|
683
683
|
input: { type: 'object' as const, properties: {} },
|
|
684
|
-
// FOLLOW-UP:
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
// are likewise unwired. Until then, placeholders resolve to the object
|
|
689
|
-
// shape so the beta host never sees a bare string.
|
|
684
|
+
// FOLLOW-UP: wire the real tool wrappers (costCommand/goalTools) once V1
|
|
685
|
+
// infrastructure is resolvable in the V2 standalone context. Until then,
|
|
686
|
+
// placeholders resolve to the object shape so the beta host never sees a
|
|
687
|
+
// bare string.
|
|
690
688
|
execute: async (_input: Record<string, unknown>, _context: unknown): Promise<V2ToolResult> => {
|
|
691
689
|
return { output: `${name}: ${v2UnavailableMessage}` }
|
|
692
690
|
},
|
package/src/plugin.ts
CHANGED
|
@@ -66,10 +66,10 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
|
|
|
66
66
|
const log = createPantheonLogger({ module: 'pantheon-plugin' })
|
|
67
67
|
|
|
68
68
|
// R4: per-agent step caps from routing.yml `agents.<name>.max_steps`
|
|
69
|
-
// (fail-open → {} = no agent capped). R1 retry_policy/cooldown
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
69
|
+
// (fail-open → {} = no agent capped). The R1 retry_policy/cooldown block and
|
|
70
|
+
// its presets.mjs loaders were removed together with their only consumer
|
|
71
|
+
// (zeus-delegate-with-retry.ts); the plugin has no retry path (opencode cannot
|
|
72
|
+
// intercept task completion).
|
|
73
73
|
const stepCapTracker = new StepCapTracker(loadRoutingMaxSteps({ logger: log }))
|
|
74
74
|
|
|
75
75
|
// O5: permission.task glob rules from routing.yml `permission.task`
|
|
@@ -517,8 +517,8 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
517
517
|
|
|
518
518
|
// Wave 4 (PR #46): /cost — delegation cost + token visibility. Reads
|
|
519
519
|
// opencode.db read-only via the single node:sqlite backend.
|
|
520
|
-
// Fully wired (
|
|
521
|
-
//
|
|
520
|
+
// Fully wired. (opencode 1.18.x cannot intercept task completion via hooks,
|
|
521
|
+
// so empty-result handling lives in delegation-classifier.ts.)
|
|
522
522
|
const costCommand = createCostCommand()
|
|
523
523
|
const modelCommand = createModelCommand()
|
|
524
524
|
|
|
@@ -783,7 +783,7 @@ function delegationAgent(tool: string, args?: unknown): string {
|
|
|
783
783
|
* panel tracks the child end-to-end. Fire-and-forget and silent: a mirror
|
|
784
784
|
* failure must never surface into the tool call or the transcript.
|
|
785
785
|
* registerLaunchIfAbsent keeps it idempotent against any other dispatcher
|
|
786
|
-
* (
|
|
786
|
+
* (plugin double-load, repeated after-hook firings).
|
|
787
787
|
*/
|
|
788
788
|
function mirrorNativeBackgroundDispatch(
|
|
789
789
|
input: { tool: string; sessionID: string },
|