pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.11
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/.pantheon/code-mode/manifest.json +21 -0
- package/CHANGELOG.md +1054 -0
- package/README.md +171 -16
- package/README.pt-BR.md +99 -13
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +23 -15
- package/plugin.json +1 -1
- package/pyproject.toml +65 -0
- package/scripts/code_mode_server.py +431 -55
- package/scripts/cost.mjs +30 -14
- package/scripts/dev-tui.sh +48 -0
- package/scripts/doctor.mjs +207 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +137 -35
- package/scripts/install/plugin.mjs +3 -33
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +537 -128
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-context-rehydrate.mjs +240 -0
- package/scripts/probe-pantheon-cost.mjs +313 -0
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +121 -53
- package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +431 -55
- package/src/mcp/mcp_persistence_server.py +537 -128
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +43 -1
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +345 -62
- package/src/pantheon/delegate-manager.ts +390 -8
- package/src/pantheon/delegation-finalize.ts +32 -0
- package/src/pantheon/delegation.ts +235 -18
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +146 -12
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/v2-events.ts +18 -1
- package/src/plugin-v2.ts +2 -14
- package/src/plugin.ts +28 -17
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +137 -27
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +459 -163
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +506 -103
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +506 -103
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
- package/src/pantheon/delegate-task-adapter.ts +0 -444
|
@@ -23,7 +23,16 @@
|
|
|
23
23
|
* @module delegate-manager
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { z } from 'zod'
|
|
26
27
|
import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
|
|
28
|
+
import type { DelegationToolset } from './delegation.ts'
|
|
29
|
+
import {
|
|
30
|
+
DELEGATION_DEFAULTS,
|
|
31
|
+
type DelegationClient,
|
|
32
|
+
readDelegationReport,
|
|
33
|
+
} from './delegation-finalize.ts'
|
|
34
|
+
import { createNativeProbe, type NativeProbeFn, type ProbeClient } from './native-probe.ts'
|
|
35
|
+
import { classifyNativeResult, type NativeTaskStatus } from './native-task-status.ts'
|
|
27
36
|
import {
|
|
28
37
|
buildStopInstruction,
|
|
29
38
|
cappedSummary,
|
|
@@ -59,6 +68,7 @@ export interface ManagerReceipt {
|
|
|
59
68
|
id: string
|
|
60
69
|
agent: string
|
|
61
70
|
state: 'reconciled' | 'error'
|
|
71
|
+
status: NativeTaskStatus
|
|
62
72
|
line: string
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -95,10 +105,15 @@ export interface DelegateManagerOptions {
|
|
|
95
105
|
keepCompleted?: number
|
|
96
106
|
/** Per-agent step budgets (routing.yml max_steps). */
|
|
97
107
|
stepCap?: StepCapTracker
|
|
108
|
+
/** Lazy capability probe, invoked once before the first native dispatch. */
|
|
109
|
+
nativeProbe?: NativeProbeFn
|
|
110
|
+
/** V1 client used by the lazy capability probe. */
|
|
111
|
+
probeClient?: ProbeClient
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
export interface DelegateManager {
|
|
101
115
|
launch(args: LaunchArgs): Promise<ManagerReceipt>
|
|
116
|
+
ensureNativeAvailable(): Promise<NativeTaskStatus | undefined>
|
|
102
117
|
read(idOrAlias: string): Promise<string>
|
|
103
118
|
list(): Promise<string[]>
|
|
104
119
|
recover(): Promise<void>
|
|
@@ -113,13 +128,40 @@ export function isDelegationEnabled(
|
|
|
113
128
|
return (env.PANTHEON_DELEGATION ?? '').trim().toLowerCase() !== 'off'
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
export type DelegateMode = 'native' | 'legacy'
|
|
132
|
+
export function resolveDelegateMode(
|
|
133
|
+
env: Record<string, string | undefined> = process.env,
|
|
134
|
+
): DelegateMode {
|
|
135
|
+
return (env.PANTHEON_DELEGATE_MODE ?? '').trim().toLowerCase() === 'native' ? 'native' : 'legacy'
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
// ─── Helpers ───────────────────────────────────────────────────────────
|
|
117
139
|
|
|
118
140
|
const LINE_MAX = 160
|
|
119
141
|
|
|
142
|
+
/** Remove terminal escapes and control characters from untrusted child output. */
|
|
143
|
+
export function sanitizeReceiptText(text: string): string {
|
|
144
|
+
let clean = ''
|
|
145
|
+
let skippingAnsi = false
|
|
146
|
+
for (const char of text) {
|
|
147
|
+
const code = char.charCodeAt(0)
|
|
148
|
+
if (skippingAnsi) {
|
|
149
|
+
if (code === 0x5b) continue
|
|
150
|
+
if (code >= 0x40 && code <= 0x7e) skippingAnsi = false
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (code === 0x1b) {
|
|
154
|
+
skippingAnsi = true
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (char === '\n' || char === '\t' || (code >= 0x20 && code <= 0x7e)) clean += char
|
|
158
|
+
}
|
|
159
|
+
return clean
|
|
160
|
+
}
|
|
161
|
+
|
|
120
162
|
function firstLine(text: string, max = 80): string {
|
|
121
163
|
const line =
|
|
122
|
-
(text ?? '')
|
|
164
|
+
sanitizeReceiptText(text ?? '')
|
|
123
165
|
.split('\n')
|
|
124
166
|
.map((l) => l.trim())
|
|
125
167
|
.find((l) => l !== '') ?? ''
|
|
@@ -168,21 +210,30 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
168
210
|
task,
|
|
169
211
|
parentSessionID,
|
|
170
212
|
env = process.env,
|
|
171
|
-
models = [],
|
|
213
|
+
models: modelsOpt = [],
|
|
172
214
|
timeoutMs = 120_000,
|
|
173
215
|
readTimeoutMs = 60_000,
|
|
174
|
-
foregroundFallback = true,
|
|
216
|
+
foregroundFallback: fgFallbackOpt = true,
|
|
175
217
|
maxEntries = 50,
|
|
176
218
|
keepCompleted = 10,
|
|
177
219
|
stepCap,
|
|
220
|
+
nativeProbe,
|
|
221
|
+
probeClient,
|
|
178
222
|
} = options
|
|
179
223
|
|
|
224
|
+
// Strict native mode: foregroundFallback and models are overridden.
|
|
225
|
+
// The user-facing options are ignored when PANTHEON_DELEGATE_MODE=native.
|
|
226
|
+
const isNativeMode = resolveDelegateMode(env) === 'native'
|
|
227
|
+
const foregroundFallback = isNativeMode ? false : fgFallbackOpt
|
|
228
|
+
const models = isNativeMode ? [] : modelsOpt
|
|
229
|
+
|
|
180
230
|
// Synchronous in-flight reservations per agent: launch() must decide
|
|
181
231
|
// background vs foreground BEFORE its first await, otherwise concurrent
|
|
182
232
|
// launches without interleaved awaits would all see an empty board.
|
|
183
233
|
const pendingByAgent = new Map<string, number>()
|
|
184
234
|
let seq = 0
|
|
185
235
|
const foregroundRecent: { agent: string; line: string; at: number }[] = []
|
|
236
|
+
let probeResult: Awaited<ReturnType<NativeProbeFn>> | null = null
|
|
186
237
|
|
|
187
238
|
function pending(agent: string): number {
|
|
188
239
|
return pendingByAgent.get(agent.toLowerCase()) ?? 0
|
|
@@ -194,6 +245,12 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
194
245
|
}
|
|
195
246
|
}
|
|
196
247
|
|
|
248
|
+
async function ensureNativeAvailable(): Promise<NativeTaskStatus | undefined> {
|
|
249
|
+
if (!isNativeMode || nativeProbe === undefined || probeClient === undefined) return undefined
|
|
250
|
+
probeResult ??= await nativeProbe({ client: probeClient, parentSessionID })
|
|
251
|
+
return probeResult.status === 'AVAILABLE' ? undefined : probeResult.status
|
|
252
|
+
}
|
|
253
|
+
|
|
197
254
|
async function runTask(
|
|
198
255
|
agent: string,
|
|
199
256
|
prompt: string,
|
|
@@ -239,6 +296,7 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
239
296
|
// acknowledgment that the result was consumed.
|
|
240
297
|
const terminalState = job.state
|
|
241
298
|
const failed = verifyError !== undefined || terminalState === 'error'
|
|
299
|
+
const status = classifyNativeResult(job, verifyError)
|
|
242
300
|
// Auto-reconcile + delete the signal file on the spot.
|
|
243
301
|
if (job.state !== 'reconciled') await board.markReconciled(taskID)
|
|
244
302
|
await board.deleteSignal(alias)
|
|
@@ -252,7 +310,7 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
252
310
|
LINE_MAX,
|
|
253
311
|
)
|
|
254
312
|
: boardLine(settled ?? job)
|
|
255
|
-
return { id: taskID, agent: job.agent, state: failed ? 'error' : 'reconciled', line }
|
|
313
|
+
return { id: taskID, agent: job.agent, state: failed ? 'error' : 'reconciled', status, line }
|
|
256
314
|
}
|
|
257
315
|
|
|
258
316
|
/** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
|
|
@@ -319,7 +377,33 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
319
377
|
)
|
|
320
378
|
foregroundRecent.push({ agent: args.agent, line, at: Date.now() })
|
|
321
379
|
if (foregroundRecent.length > 20) foregroundRecent.splice(0, foregroundRecent.length - 20)
|
|
322
|
-
|
|
380
|
+
const status = classifyNativeResult(
|
|
381
|
+
{
|
|
382
|
+
taskID: id,
|
|
383
|
+
parentSessionID,
|
|
384
|
+
agent: args.agent,
|
|
385
|
+
description: args.description ?? firstLine(args.prompt, 120),
|
|
386
|
+
state: ok ? 'completed' : 'error',
|
|
387
|
+
timedOut: false,
|
|
388
|
+
alias: id,
|
|
389
|
+
launchedAt: Date.now(),
|
|
390
|
+
updatedAt: Date.now(),
|
|
391
|
+
totalErrors: 0,
|
|
392
|
+
timeoutCount: 0,
|
|
393
|
+
terminalUnreconciled: false,
|
|
394
|
+
contextFiles: [],
|
|
395
|
+
...(ok
|
|
396
|
+
? {
|
|
397
|
+
resultSummary: firstLine(
|
|
398
|
+
(outcome as { ok: true; result: NativeTaskResult }).result.content ?? '',
|
|
399
|
+
500,
|
|
400
|
+
),
|
|
401
|
+
}
|
|
402
|
+
: {}),
|
|
403
|
+
},
|
|
404
|
+
ok ? undefined : (outcome as { ok: false; error?: string }).error,
|
|
405
|
+
)
|
|
406
|
+
return { id, agent: args.agent, state: ok ? 'reconciled' : 'error', status, line }
|
|
323
407
|
}
|
|
324
408
|
|
|
325
409
|
return {
|
|
@@ -332,13 +416,31 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
332
416
|
const maxSteps = stepCap.maxStepsFor(args.agent)
|
|
333
417
|
if (stepCap.isCapped(args.agent)) {
|
|
334
418
|
const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
|
|
335
|
-
return {
|
|
419
|
+
return {
|
|
420
|
+
id: `cap-${Date.now()}`,
|
|
421
|
+
agent: args.agent,
|
|
422
|
+
state: 'reconciled',
|
|
423
|
+
status: 'ESCALATE',
|
|
424
|
+
line,
|
|
425
|
+
}
|
|
336
426
|
}
|
|
337
427
|
const rec = stepCap.recordStep(args.agent)
|
|
338
428
|
if (rec.capped && rec.maxSteps !== undefined) {
|
|
339
429
|
prompt = `${args.prompt}${buildStopInstruction(args.agent, rec.maxSteps)}`
|
|
340
430
|
}
|
|
341
431
|
}
|
|
432
|
+
if (isNativeMode) {
|
|
433
|
+
const unavailableStatus = await ensureNativeAvailable()
|
|
434
|
+
if (unavailableStatus !== undefined) {
|
|
435
|
+
return {
|
|
436
|
+
id: `probe-${parentSessionID}`,
|
|
437
|
+
agent: args.agent,
|
|
438
|
+
state: 'error',
|
|
439
|
+
status: unavailableStatus,
|
|
440
|
+
line: `[native] ${args.agent} — native API ${unavailableStatus.toLowerCase()} — ERR`,
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
342
444
|
// Synchronous routing decision: background when a slot is free,
|
|
343
445
|
// foreground fallback when full, clean rejection otherwise.
|
|
344
446
|
const free =
|
|
@@ -349,6 +451,7 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
349
451
|
`pantheon_delegate rejected: concurrency limit reached for agent "${args.agent}"`,
|
|
350
452
|
)
|
|
351
453
|
},
|
|
454
|
+
ensureNativeAvailable,
|
|
352
455
|
|
|
353
456
|
async read(idOrAlias: string): Promise<string> {
|
|
354
457
|
guardEnabled()
|
|
@@ -362,11 +465,11 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
362
465
|
if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
|
|
363
466
|
await board.deleteSignal(terminal.alias)
|
|
364
467
|
if (terminal.state === 'error' && !terminal.resultSummary) {
|
|
365
|
-
return `# Delegation Report — ${terminal.alias}\n\nERROR: ${terminal.lastStatusError ?? 'failed'}\n`
|
|
468
|
+
return `# Delegation Report — ${terminal.alias}\n\nERROR: ${sanitizeReceiptText(terminal.lastStatusError ?? 'failed')}\n`
|
|
366
469
|
}
|
|
367
470
|
return (
|
|
368
471
|
`# Delegation Report — ${terminal.alias}\n\n` +
|
|
369
|
-
`${terminal.resultSummary ?? terminal.lastStatusError ?? terminal.state}\n`
|
|
472
|
+
`${sanitizeReceiptText(terminal.resultSummary ?? terminal.lastStatusError ?? terminal.state)}\n`
|
|
370
473
|
)
|
|
371
474
|
},
|
|
372
475
|
|
|
@@ -382,3 +485,282 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
382
485
|
},
|
|
383
486
|
}
|
|
384
487
|
}
|
|
488
|
+
|
|
489
|
+
// Native delegation tools live with the manager so the plugin mounts the
|
|
490
|
+
// manager directly; there is intentionally no adapter or kill-switch wrapper.
|
|
491
|
+
const delegateArgs = {
|
|
492
|
+
prompt: z.string().min(1),
|
|
493
|
+
agent: z.string().min(1),
|
|
494
|
+
description: z.string().optional(),
|
|
495
|
+
read_only: z.boolean().optional(),
|
|
496
|
+
model: z.string().optional(),
|
|
497
|
+
} satisfies z.ZodRawShape
|
|
498
|
+
const readArgs = { id: z.string().min(1) } satisfies z.ZodRawShape
|
|
499
|
+
const listArgs = {} satisfies z.ZodRawShape
|
|
500
|
+
export interface ToolContextLike {
|
|
501
|
+
sessionID: string
|
|
502
|
+
agent?: string
|
|
503
|
+
}
|
|
504
|
+
export interface SessionTaskOptions {
|
|
505
|
+
client: DelegationClient
|
|
506
|
+
board: BackgroundJobBoard
|
|
507
|
+
outputDir?: string
|
|
508
|
+
promptTimeoutMs?: number
|
|
509
|
+
settleTimeoutMs?: number
|
|
510
|
+
}
|
|
511
|
+
export function parseModelRef(model: string): { id: string; providerID: string } | undefined {
|
|
512
|
+
const idx = model.indexOf('/')
|
|
513
|
+
return idx > 0 && idx < model.length - 1
|
|
514
|
+
? { providerID: model.slice(0, idx), id: model.slice(idx + 1) }
|
|
515
|
+
: undefined
|
|
516
|
+
}
|
|
517
|
+
export function extractOutputSection(md: string | undefined): string {
|
|
518
|
+
if (md === undefined) return ''
|
|
519
|
+
const at = md.indexOf('## Output')
|
|
520
|
+
const body = at < 0 ? md : md.slice(at + 9)
|
|
521
|
+
const errAt = body.indexOf('\n**Error**:')
|
|
522
|
+
return (errAt < 0 ? body : body.slice(0, errAt)).trim()
|
|
523
|
+
}
|
|
524
|
+
async function promptAccept(
|
|
525
|
+
client: DelegationClient,
|
|
526
|
+
taskID: string,
|
|
527
|
+
agent: string,
|
|
528
|
+
prompt: string,
|
|
529
|
+
model: string | undefined,
|
|
530
|
+
signal: AbortSignal | undefined,
|
|
531
|
+
timeoutMs: number,
|
|
532
|
+
): Promise<void> {
|
|
533
|
+
const modelRef = model === undefined ? undefined : parseModelRef(model)
|
|
534
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
535
|
+
try {
|
|
536
|
+
await Promise.race([
|
|
537
|
+
client.session
|
|
538
|
+
.promptAsync({
|
|
539
|
+
path: { id: taskID },
|
|
540
|
+
body: {
|
|
541
|
+
agent,
|
|
542
|
+
...(modelRef === undefined ? {} : { model: modelRef }),
|
|
543
|
+
parts: [{ type: 'text', text: prompt }],
|
|
544
|
+
},
|
|
545
|
+
...(signal === undefined ? {} : { signal }),
|
|
546
|
+
})
|
|
547
|
+
.then(undefined, (error: unknown) => {
|
|
548
|
+
throw new Error(
|
|
549
|
+
`promptAsync rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
550
|
+
)
|
|
551
|
+
}),
|
|
552
|
+
new Promise<never>((_, reject) => {
|
|
553
|
+
timer = setTimeout(
|
|
554
|
+
() => reject(new Error(`promptAsync timed out after ${timeoutMs}ms`)),
|
|
555
|
+
timeoutMs,
|
|
556
|
+
)
|
|
557
|
+
timer.unref?.()
|
|
558
|
+
}),
|
|
559
|
+
])
|
|
560
|
+
} finally {
|
|
561
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
export function createSessionTaskFn(options: SessionTaskOptions): NativeTaskFn {
|
|
565
|
+
const {
|
|
566
|
+
client,
|
|
567
|
+
board,
|
|
568
|
+
outputDir = DELEGATION_DEFAULTS.outputDir,
|
|
569
|
+
promptTimeoutMs = 60_000,
|
|
570
|
+
settleTimeoutMs = DELEGATION_DEFAULTS.timeoutMs,
|
|
571
|
+
} = options
|
|
572
|
+
return async ({ agent, prompt, model, signal, taskID }) => {
|
|
573
|
+
await promptAccept(client, taskID, agent, prompt, model, signal, promptTimeoutMs)
|
|
574
|
+
const terminal = await board.waitForTerminal(taskID, settleTimeoutMs)
|
|
575
|
+
const md = await readDelegationReport(outputDir, terminal)
|
|
576
|
+
return { content: extractOutputSection(md) || terminal.resultSummary || '' }
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
export interface NativeDelegateWiring {
|
|
580
|
+
board: BackgroundJobBoard
|
|
581
|
+
client: DelegationClient
|
|
582
|
+
outputDir?: string
|
|
583
|
+
env?: Record<string, string | undefined>
|
|
584
|
+
isRootSession: (sessionID: string) => boolean
|
|
585
|
+
registerChildSession?: (sessionID: string, parentID: string) => void
|
|
586
|
+
registerReadOnlySession?: (
|
|
587
|
+
sessionID: string,
|
|
588
|
+
info: { agent: string; readOnlyFlag?: boolean },
|
|
589
|
+
) => void
|
|
590
|
+
isReadOnlyAgent?: (agent: string) => boolean
|
|
591
|
+
agentModel?: (agent: string) => string | undefined
|
|
592
|
+
managerOptions?: Partial<
|
|
593
|
+
Omit<DelegateManagerOptions, 'board' | 'task' | 'parentSessionID' | 'env'>
|
|
594
|
+
>
|
|
595
|
+
stepCap?: StepCapTracker
|
|
596
|
+
}
|
|
597
|
+
export interface NativeDelegateToolset {
|
|
598
|
+
pantheon_delegate: {
|
|
599
|
+
description: string
|
|
600
|
+
args: typeof delegateArgs
|
|
601
|
+
execute(args: z.infer<z.ZodObject<typeof delegateArgs>>, ctx: ToolContextLike): Promise<string>
|
|
602
|
+
}
|
|
603
|
+
pantheon_delegation_read: {
|
|
604
|
+
description: string
|
|
605
|
+
args: typeof readArgs
|
|
606
|
+
execute(args: z.infer<z.ZodObject<typeof readArgs>>, ctx: ToolContextLike): Promise<string>
|
|
607
|
+
}
|
|
608
|
+
pantheon_delegation_list: {
|
|
609
|
+
description: string
|
|
610
|
+
args: typeof listArgs
|
|
611
|
+
execute(args: z.infer<z.ZodObject<typeof listArgs>>, ctx: ToolContextLike): Promise<string>
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function shortFirstLine(text: string, max = 60): string {
|
|
615
|
+
const line =
|
|
616
|
+
sanitizeReceiptText(text)
|
|
617
|
+
.split('\n')
|
|
618
|
+
.map((item) => item.trim())
|
|
619
|
+
.find(Boolean) ?? ''
|
|
620
|
+
return line.length > max ? `${line.slice(0, max - 1)}…` : line
|
|
621
|
+
}
|
|
622
|
+
export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeDelegateToolset {
|
|
623
|
+
const {
|
|
624
|
+
board,
|
|
625
|
+
client,
|
|
626
|
+
env = process.env,
|
|
627
|
+
isRootSession,
|
|
628
|
+
registerChildSession,
|
|
629
|
+
registerReadOnlySession,
|
|
630
|
+
isReadOnlyAgent,
|
|
631
|
+
agentModel,
|
|
632
|
+
managerOptions = {},
|
|
633
|
+
stepCap,
|
|
634
|
+
} = wiring
|
|
635
|
+
const task = createSessionTaskFn({
|
|
636
|
+
client,
|
|
637
|
+
board,
|
|
638
|
+
...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
|
|
639
|
+
...(managerOptions.timeoutMs === undefined
|
|
640
|
+
? {}
|
|
641
|
+
: { promptTimeoutMs: managerOptions.timeoutMs }),
|
|
642
|
+
})
|
|
643
|
+
const managers = new Map<string, DelegateManager>()
|
|
644
|
+
const managerFor = (parentSessionID: string): DelegateManager => {
|
|
645
|
+
const existing = managers.get(parentSessionID)
|
|
646
|
+
if (existing !== undefined) return existing
|
|
647
|
+
const nativeProbe = createNativeProbe()
|
|
648
|
+
const manager = createDelegateManager({
|
|
649
|
+
...managerOptions,
|
|
650
|
+
board,
|
|
651
|
+
task,
|
|
652
|
+
parentSessionID,
|
|
653
|
+
env,
|
|
654
|
+
...(stepCap === undefined ? {} : { stepCap }),
|
|
655
|
+
nativeProbe,
|
|
656
|
+
probeClient: client,
|
|
657
|
+
})
|
|
658
|
+
managers.set(parentSessionID, manager)
|
|
659
|
+
return manager
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
pantheon_delegate: {
|
|
663
|
+
description: 'Dispatch a background agent as a child session and return a verified receipt.',
|
|
664
|
+
args: delegateArgs,
|
|
665
|
+
execute: async (args, ctx) => {
|
|
666
|
+
if (!isDelegationEnabled(env))
|
|
667
|
+
throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
|
|
668
|
+
if (!isRootSession(ctx.sessionID))
|
|
669
|
+
throw new Error(
|
|
670
|
+
`pantheon_delegate rejected: session ${ctx.sessionID} is a sub-session — only root sessions can delegate`,
|
|
671
|
+
)
|
|
672
|
+
const manager = managerFor(ctx.sessionID)
|
|
673
|
+
const unavailableStatus = await manager.ensureNativeAvailable()
|
|
674
|
+
if (unavailableStatus !== undefined) {
|
|
675
|
+
return `status: ${unavailableStatus}\n[native] ${args.agent} — native API ${unavailableStatus.toLowerCase()} — ERR`
|
|
676
|
+
}
|
|
677
|
+
const model = args.model ?? agentModel?.(args.agent)
|
|
678
|
+
let childID: string
|
|
679
|
+
try {
|
|
680
|
+
const modelRef = model === undefined ? undefined : parseModelRef(model)
|
|
681
|
+
const created = await client.session.create({
|
|
682
|
+
body: {
|
|
683
|
+
parentID: ctx.sessionID,
|
|
684
|
+
title: args.description ?? shortFirstLine(args.prompt),
|
|
685
|
+
...(modelRef === undefined ? {} : { model: modelRef }),
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
childID = created.id
|
|
689
|
+
} catch (error) {
|
|
690
|
+
return `pantheon_delegate failed: session.create rejected: ${sanitizeReceiptText(error instanceof Error ? error.message : String(error))}`
|
|
691
|
+
}
|
|
692
|
+
registerChildSession?.(childID, ctx.sessionID)
|
|
693
|
+
if (args.read_only === true || isReadOnlyAgent?.(args.agent) === true)
|
|
694
|
+
registerReadOnlySession?.(childID, {
|
|
695
|
+
agent: args.agent,
|
|
696
|
+
...(args.read_only === undefined ? {} : { readOnlyFlag: args.read_only }),
|
|
697
|
+
})
|
|
698
|
+
const receipt = await manager.launch({
|
|
699
|
+
agent: args.agent,
|
|
700
|
+
prompt: args.prompt,
|
|
701
|
+
...(args.description === undefined ? {} : { description: args.description }),
|
|
702
|
+
...(args.model === undefined ? {} : { model: args.model }),
|
|
703
|
+
taskID: childID,
|
|
704
|
+
})
|
|
705
|
+
return `status: ${receipt.status}\n${receipt.line}`
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
pantheon_delegation_read: {
|
|
709
|
+
description: 'Block until a delegation finishes, then return its verified report.',
|
|
710
|
+
args: readArgs,
|
|
711
|
+
execute: async (args, ctx) => {
|
|
712
|
+
if (!isDelegationEnabled(env))
|
|
713
|
+
throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
|
|
714
|
+
return managerFor(ctx.sessionID).read(args.id)
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
pantheon_delegation_list: {
|
|
718
|
+
description: 'List background delegations.',
|
|
719
|
+
args: listArgs,
|
|
720
|
+
execute: async (_args, ctx) => {
|
|
721
|
+
if (!isDelegationEnabled(env))
|
|
722
|
+
throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
|
|
723
|
+
const lines = await managerFor(ctx.sessionID).list()
|
|
724
|
+
return lines.length > 0 ? lines.join('\n') : 'No delegations.'
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
export interface PluginNativeDelegationWiring {
|
|
730
|
+
board: BackgroundJobBoard
|
|
731
|
+
client: DelegationClient
|
|
732
|
+
isRootSession: (sessionID: string) => boolean
|
|
733
|
+
registerChildSession: (sessionID: string, parentID: string) => void
|
|
734
|
+
registerReadOnlySession: (
|
|
735
|
+
sessionID: string,
|
|
736
|
+
info: { agent: string; readOnlyFlag?: boolean },
|
|
737
|
+
) => void
|
|
738
|
+
readOnlyAgents: ReadonlySet<string>
|
|
739
|
+
agentModels: Record<string, string | undefined>
|
|
740
|
+
stepCap: StepCapTracker
|
|
741
|
+
wallClockTimeoutMs?: number
|
|
742
|
+
finalizeDelegation: DelegationToolset['finalizeDelegation']
|
|
743
|
+
}
|
|
744
|
+
export type PluginNativeDelegation = NativeDelegateToolset & {
|
|
745
|
+
finalizeDelegation: DelegationToolset['finalizeDelegation']
|
|
746
|
+
}
|
|
747
|
+
export function buildPluginNativeDelegation(
|
|
748
|
+
wiring: PluginNativeDelegationWiring,
|
|
749
|
+
): PluginNativeDelegation {
|
|
750
|
+
return {
|
|
751
|
+
...createNativeDelegateTools({
|
|
752
|
+
board: wiring.board,
|
|
753
|
+
client: wiring.client,
|
|
754
|
+
isRootSession: wiring.isRootSession,
|
|
755
|
+
registerChildSession: wiring.registerChildSession,
|
|
756
|
+
registerReadOnlySession: wiring.registerReadOnlySession,
|
|
757
|
+
isReadOnlyAgent: (agent) => wiring.readOnlyAgents.has(agent.toLowerCase()),
|
|
758
|
+
agentModel: (agent) => wiring.agentModels[agent.toLowerCase()],
|
|
759
|
+
stepCap: wiring.stepCap,
|
|
760
|
+
...(wiring.wallClockTimeoutMs === undefined
|
|
761
|
+
? {}
|
|
762
|
+
: { managerOptions: { timeoutMs: wiring.wallClockTimeoutMs } }),
|
|
763
|
+
}),
|
|
764
|
+
finalizeDelegation: wiring.finalizeDelegation,
|
|
765
|
+
}
|
|
766
|
+
}
|
|
@@ -144,9 +144,25 @@ export interface DelegationOptions {
|
|
|
144
144
|
promptTimeoutMs?: number
|
|
145
145
|
/** Poll interval used by the bootstrap watchdog. */
|
|
146
146
|
bootstrapPollIntervalMs?: number
|
|
147
|
+
/**
|
|
148
|
+
* Max completed jobs to keep per agent after pruning (default: 10).
|
|
149
|
+
* Matches the native delegate-manager default.
|
|
150
|
+
*/
|
|
151
|
+
keepCompleted?: number
|
|
152
|
+
/**
|
|
153
|
+
* Hard cap on total board entries (default: 50).
|
|
154
|
+
* Matches the native delegate-manager default.
|
|
155
|
+
*/
|
|
156
|
+
maxEntries?: number
|
|
147
157
|
/** Injectable clock/sleeper for deterministic watchdog tests. */
|
|
148
158
|
now?: () => number
|
|
149
159
|
sleep?: (ms: number) => Promise<void>
|
|
160
|
+
/**
|
|
161
|
+
* Kill-switch: when false, all delegation tools (delegate/read/list) throw
|
|
162
|
+
* immediately. Mirrors `PANTHEON_DELEGATION=off` from the native manager.
|
|
163
|
+
* Defaults to true (delegation enabled) when not specified.
|
|
164
|
+
*/
|
|
165
|
+
delegationEnabled?: boolean
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
/** Dependencies threaded to the finalize path. */
|
|
@@ -370,5 +386,21 @@ export async function finalizeDelegation(
|
|
|
370
386
|
// Terminal in a DIFFERENT state (e.g. timeout already recorded) — the
|
|
371
387
|
// report above still captures this finalize; the board state stands.
|
|
372
388
|
}
|
|
389
|
+
|
|
390
|
+
// P1-1: Delete the auto-wake signal file to prevent leaks. The board
|
|
391
|
+
// writes .signal.json on every terminal transition, but the legacy path
|
|
392
|
+
// never called deleteSignal — only the native delegate-manager did.
|
|
393
|
+
// The consumer (auto-wake.ts consumeWakeSignals) is intentionally NOT
|
|
394
|
+
// wired in the event hook: finalizeDelegation already handles the
|
|
395
|
+
// terminal transition, so consuming signals would be redundant and risks
|
|
396
|
+
// double-processing. deleteSignal on finalize is sufficient.
|
|
397
|
+
await deps.board.deleteSignal(job.alias)
|
|
398
|
+
|
|
399
|
+
// P1-4: Prune old completed jobs and enforce the hard entry cap. The
|
|
400
|
+
// native delegate-manager called these in settle(), but the legacy
|
|
401
|
+
// finalize path omitted them, allowing unbounded board growth.
|
|
402
|
+
await deps.board.pruneCompleted(deps.options.keepCompleted ?? 10)
|
|
403
|
+
await deps.board.enforceEntryCap(deps.options.maxEntries ?? 50)
|
|
404
|
+
|
|
373
405
|
return deps.board.get(childSessionID) ?? job
|
|
374
406
|
}
|