pantheon-opencode 1.5.0-beta.2 → 1.5.0-beta.20

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.
Files changed (107) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/AGENTS.md +14 -63
  3. package/CHANGELOG.md +1260 -0
  4. package/README.md +162 -23
  5. package/README.pt-BR.md +104 -19
  6. package/bin/pantheon-init.mjs +178 -21
  7. package/commands/pantheon.md +1 -35
  8. package/package.json +23 -15
  9. package/plugin.json +1 -1
  10. package/pyproject.toml +62 -0
  11. package/scripts/code_mode_server.py +431 -55
  12. package/scripts/doctor.mjs +207 -9
  13. package/scripts/install/cli-ui.mjs +17 -11
  14. package/scripts/install/config-migration.mjs +4 -6
  15. package/scripts/install/opencode.mjs +137 -35
  16. package/scripts/install/plugin.mjs +12 -45
  17. package/scripts/install/shared.mjs +43 -2
  18. package/scripts/install/strings.mjs +129 -0
  19. package/scripts/install-mcp.mjs +1 -0
  20. package/scripts/manifest-inventory.mjs +410 -0
  21. package/scripts/mcp_persistence_server.py +536 -164
  22. package/scripts/package-evidence.mjs +242 -0
  23. package/scripts/probe-pantheon-cost.mjs +10 -1
  24. package/scripts/release-beta-version.mjs +56 -60
  25. package/scripts/sync-artifacts.mjs +169 -0
  26. package/scripts/sync-tui.mjs +129 -64
  27. package/scripts/test-opencode-v1-v2-sandbox.sh +79 -161
  28. package/scripts/validation-policy.mjs +7 -7
  29. package/scripts/version-check.mjs +113 -108
  30. package/scripts/versioning.mjs +230 -86
  31. package/src/agents/apollo.md +1 -0
  32. package/src/agents/gaia.md +1 -0
  33. package/src/agents/mnemosyne.md +1 -1
  34. package/src/agents/zeus.md +11 -54
  35. package/src/instructions/memory-protocol.instructions.md +6 -3
  36. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  37. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  38. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  39. package/src/mcp/code_mode_server.py +431 -55
  40. package/src/mcp/mcp_persistence_server.py +536 -164
  41. package/src/mcp/toon_codec.py +2 -4
  42. package/src/pantheon/background-job-board.ts +47 -5
  43. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  44. package/src/pantheon/cost-command.ts +309 -44
  45. package/src/pantheon/delegation-enforce.ts +59 -14
  46. package/src/pantheon/file-persistence-salvage.ts +64 -0
  47. package/src/pantheon/file-persistence.ts +194 -45
  48. package/src/pantheon/goal-loop.ts +1 -1
  49. package/src/pantheon/goal-store.ts +1 -1
  50. package/src/pantheon/idle-continuation.ts +4 -6
  51. package/src/pantheon/model-command.ts +1 -2
  52. package/src/pantheon/native-task-status.ts +139 -0
  53. package/src/pantheon/presets.d.mts +0 -20
  54. package/src/pantheon/presets.mjs +0 -64
  55. package/src/pantheon/session-guard.ts +2 -2
  56. package/src/pantheon/session-id.ts +30 -0
  57. package/src/pantheon/shared-board.ts +37 -0
  58. package/src/pantheon/task-result-guard.ts +106 -0
  59. package/src/pantheon/todo-enforcer.ts +3 -3
  60. package/src/pantheon/tool-ceiling.ts +159 -0
  61. package/src/pantheon/tool-context.ts +23 -0
  62. package/src/pantheon/v2-bridge.ts +1 -4
  63. package/src/plugin-v2.ts +8 -21
  64. package/src/plugin.ts +83 -251
  65. package/src/plugins/pantheon-hooks.ts +51 -0
  66. package/src/plugins/tui/dist/server.d.ts +1 -1
  67. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  68. package/src/plugins/tui/dist/server.js +1 -1
  69. package/src/plugins/tui/dist/server.js.map +1 -1
  70. package/src/plugins/tui/dist/tui.d.ts +261 -77
  71. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  72. package/src/plugins/tui/dist/tui.js +640 -234
  73. package/src/plugins/tui/dist/tui.js.map +1 -1
  74. package/src/plugins/tui/package-lock.json +2 -2
  75. package/src/plugins/tui/package.json +2 -2
  76. package/src/plugins/tui/src/index.tsx +872 -226
  77. package/src/routing.yml +0 -16
  78. package/src/skills/context-compression/SKILL.md +1 -1
  79. package/scripts/generate-preset-docs.mjs +0 -300
  80. package/scripts/generate-prompts.sh +0 -222
  81. package/scripts/hash_verify.py +0 -192
  82. package/scripts/install/component.mjs +0 -24
  83. package/scripts/install/migrate-config.mjs +0 -302
  84. package/scripts/test-context-compression.sh +0 -169
  85. package/src/pantheon/auto-wake.ts +0 -89
  86. package/src/pantheon/chat-reminders.ts +0 -9
  87. package/src/pantheon/comment-checker.ts +0 -48
  88. package/src/pantheon/compaction-assert.ts +0 -160
  89. package/src/pantheon/cost-tracker.ts +0 -108
  90. package/src/pantheon/delegate-manager.ts +0 -669
  91. package/src/pantheon/delegation-classifier.ts +0 -307
  92. package/src/pantheon/delegation-finalize.ts +0 -374
  93. package/src/pantheon/delegation-notify.ts +0 -212
  94. package/src/pantheon/delegation-toast.ts +0 -73
  95. package/src/pantheon/delegation.ts +0 -1120
  96. package/src/pantheon/dispatch-guard.ts +0 -109
  97. package/src/pantheon/permission-eval.ts +0 -284
  98. package/src/pantheon/preemptive-compact.ts +0 -76
  99. package/src/pantheon/retry-policy.ts +0 -212
  100. package/src/pantheon/step-cap.ts +0 -110
  101. package/src/pantheon/token-opt.ts +0 -781
  102. package/src/pantheon/toon-codec.ts +0 -481
  103. package/src/pantheon/v2-events.ts +0 -131
  104. package/src/pantheon/v2-hooks.ts +0 -287
  105. package/src/pantheon/v2-tool-definitions.ts +0 -331
  106. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  107. package/src/plugins/tui/dist/tui.tsx +0 -2605
@@ -1,669 +0,0 @@
1
- /**
2
- * Delegate Manager (WS1, PR #94) — thin manager over the native task() call.
3
- *
4
- * Relaunch of the 1.4.2 delegation as a THIN manager: launch / monitor /
5
- * reconcile over an injected `task()`-shaped function. No monolithic
6
- * scheduler: no timers of its own beyond wait/timeout bounds, no idle-event
7
- * plumbing, no hidden queues. Completion is recorded ONLY after the result
8
- * is verified (non-empty content); anything else becomes `error`.
9
- *
10
- * - Minimal records: every launch returns `{ id, agent, state, line }`
11
- * (id, agent, state, one short line). Full reports only via `read()`.
12
- * - Auto-reconcile + signal delete on the spot after every terminal state.
13
- * - Entry cap + aggressive prune of completed/reconciled jobs.
14
- * - Concurrency limit (read live from the board) + foreground fallback.
15
- * - Model failover across an ordered model list.
16
- * - Step budgets reuse StepCapTracker (routing.yml max_steps, already
17
- * extended beyond zeus) + summarize-and-stop cut.
18
- * - Kill-switch: `PANTHEON_DELEGATION=off` disables launch/read/list.
19
- *
20
- * The V1 fire-and-forget path (delegation.ts) is untouched; the plugin wires
21
- * this manager in front of it behind `PANTHEON_DELEGATE_MODE`.
22
- *
23
- * @module delegate-manager
24
- */
25
-
26
- import { z } from 'zod'
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 {
35
- buildStopInstruction,
36
- cappedSummary,
37
- DEFAULT_MAX_STEPS,
38
- type StepCapTracker,
39
- } from './step-cap.ts'
40
-
41
- // ─── Types ─────────────────────────────────────────────────────────────
42
-
43
- /** Input shape for the native task() call (injected — fake in tests). */
44
- export interface NativeTaskInput {
45
- agent: string
46
- prompt: string
47
- model?: string
48
- signal?: AbortSignal
49
- /** Board taskID assigned by the manager (== child session id on V1 hosts). */
50
- taskID: string
51
- parentSessionID: string
52
- }
53
-
54
- /** Result shape for the native task() call. */
55
- export interface NativeTaskResult {
56
- content?: string | null
57
- tokensInput?: number
58
- tokensOutput?: number
59
- }
60
-
61
- /** task()-shaped function: run one agent turn and return its result. */
62
- export type NativeTaskFn = (input: NativeTaskInput) => Promise<NativeTaskResult>
63
-
64
- /** Minimal launch receipt: id, agent, state, one short line. */
65
- export interface ManagerReceipt {
66
- id: string
67
- agent: string
68
- state: 'reconciled' | 'error'
69
- line: string
70
- }
71
-
72
- export interface LaunchArgs {
73
- agent: string
74
- prompt: string
75
- description?: string
76
- model?: string
77
- /**
78
- * Board taskID to register. The V1 session adapter supplies the child
79
- * session id here so the idle hook and the manager share one record.
80
- * Defaults to a generated `native-<ts>-<seq>` id.
81
- */
82
- taskID?: string
83
- }
84
-
85
- export interface DelegateManagerOptions {
86
- board: BackgroundJobBoard
87
- task: NativeTaskFn
88
- parentSessionID: string
89
- /** Env mapping (default: process.env). Kill-switch reads PANTHEON_DELEGATION. */
90
- env?: Record<string, string | undefined>
91
- /** Ordered model list for failover (default: single attempt, inherited model). */
92
- models?: string[]
93
- /** Per-attempt wall-clock budget in ms (default: 120_000). */
94
- timeoutMs?: number
95
- /** read() wait budget in ms (default: 60_000). */
96
- readTimeoutMs?: number
97
- /** Run inline without a board record when concurrency is full (default: true). */
98
- foregroundFallback?: boolean
99
- /** Hard cap on board entries; oldest terminal jobs evicted first (default: 50). */
100
- maxEntries?: number
101
- /** Aggressive prune: completed/reconciled jobs kept (default: 10). */
102
- keepCompleted?: number
103
- /** Per-agent step budgets (routing.yml max_steps). */
104
- stepCap?: StepCapTracker
105
- }
106
-
107
- export interface DelegateManager {
108
- launch(args: LaunchArgs): Promise<ManagerReceipt>
109
- read(idOrAlias: string): Promise<string>
110
- list(): Promise<string[]>
111
- recover(): Promise<void>
112
- }
113
-
114
- // ─── Kill-switch ───────────────────────────────────────────────────────
115
-
116
- /** False only when `PANTHEON_DELEGATION=off` (case-insensitive). */
117
- export function isDelegationEnabled(
118
- env: Record<string, string | undefined> = process.env,
119
- ): boolean {
120
- return (env.PANTHEON_DELEGATION ?? '').trim().toLowerCase() !== 'off'
121
- }
122
-
123
- export type DelegateMode = 'native' | 'legacy'
124
- export function resolveDelegateMode(
125
- env: Record<string, string | undefined> = process.env,
126
- ): DelegateMode {
127
- return (env.PANTHEON_DELEGATE_MODE ?? '').trim().toLowerCase() === 'native' ? 'native' : 'legacy'
128
- }
129
-
130
- // ─── Helpers ───────────────────────────────────────────────────────────
131
-
132
- const LINE_MAX = 160
133
-
134
- function firstLine(text: string, max = 80): string {
135
- const line =
136
- (text ?? '')
137
- .split('\n')
138
- .map((l) => l.trim())
139
- .find((l) => l !== '') ?? ''
140
- return line.length > max ? `${line.slice(0, max - 1)}…` : line
141
- }
142
-
143
- function shortState(state: BackgroundJobRecord['state']): string {
144
- switch (state) {
145
- case 'completed':
146
- return 'OK'
147
- case 'error':
148
- return 'ERR'
149
- case 'cancelled':
150
- return 'CAN'
151
- case 'running':
152
- return 'RUN'
153
- default:
154
- return 'REC'
155
- }
156
- }
157
-
158
- function boardLine(job: BackgroundJobRecord): string {
159
- const desc = firstLine(job.description, 80)
160
- return `[pantheon:${job.alias}] ${job.agent} — ${desc} — ${shortState(job.state)}`.slice(
161
- 0,
162
- LINE_MAX,
163
- )
164
- }
165
-
166
- function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
167
- let timer: ReturnType<typeof setTimeout> | undefined
168
- const guard = new Promise<never>((_, reject) => {
169
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
170
- timer.unref?.()
171
- })
172
- return Promise.race([promise, guard]).finally(() => {
173
- if (timer !== undefined) clearTimeout(timer)
174
- })
175
- }
176
-
177
- // ─── Factory ───────────────────────────────────────────────────────────
178
-
179
- export function createDelegateManager(options: DelegateManagerOptions): DelegateManager {
180
- const {
181
- board,
182
- task,
183
- parentSessionID,
184
- env = process.env,
185
- models = [],
186
- timeoutMs = 120_000,
187
- readTimeoutMs = 60_000,
188
- foregroundFallback = true,
189
- maxEntries = 50,
190
- keepCompleted = 10,
191
- stepCap,
192
- } = options
193
-
194
- // Synchronous in-flight reservations per agent: launch() must decide
195
- // background vs foreground BEFORE its first await, otherwise concurrent
196
- // launches without interleaved awaits would all see an empty board.
197
- const pendingByAgent = new Map<string, number>()
198
- let seq = 0
199
- const foregroundRecent: { agent: string; line: string; at: number }[] = []
200
-
201
- function pending(agent: string): number {
202
- return pendingByAgent.get(agent.toLowerCase()) ?? 0
203
- }
204
-
205
- function guardEnabled(): void {
206
- if (!isDelegationEnabled(env)) {
207
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
208
- }
209
- }
210
-
211
- async function runTask(
212
- agent: string,
213
- prompt: string,
214
- explicitModel: string | undefined,
215
- taskID: string,
216
- ): Promise<{ ok: true; result: NativeTaskResult } | { ok: false; error: string }> {
217
- const attempts: (string | undefined)[] =
218
- explicitModel !== undefined ? [explicitModel] : models.length > 0 ? models : [undefined]
219
- let lastError = 'no model attempts configured'
220
- for (const model of attempts) {
221
- try {
222
- const controller = new AbortController()
223
- const result = await withTimeout(
224
- task({
225
- agent,
226
- prompt,
227
- signal: controller.signal,
228
- taskID,
229
- parentSessionID,
230
- ...(model !== undefined ? { model } : {}),
231
- }),
232
- timeoutMs,
233
- `task(${agent})`,
234
- )
235
- return { ok: true, result }
236
- } catch (err) {
237
- lastError = err instanceof Error ? err.message : String(err)
238
- }
239
- }
240
- return { ok: false, error: lastError }
241
- }
242
-
243
- async function settle(
244
- taskID: string,
245
- alias: string,
246
- verifyError?: string,
247
- ): Promise<ManagerReceipt> {
248
- const job = board.get(taskID)
249
- if (!job) throw new Error(`job ${taskID} vanished after terminal state`)
250
- // The receipt reports the VERIFIED outcome, not the raw terminal state:
251
- // a `completed` the manager could not verify stays `error` for the
252
- // caller ("complete ONLY after verified result"). Reconcile is only the
253
- // acknowledgment that the result was consumed.
254
- const terminalState = job.state
255
- const failed = verifyError !== undefined || terminalState === 'error'
256
- // Auto-reconcile + delete the signal file on the spot.
257
- if (job.state !== 'reconciled') await board.markReconciled(taskID)
258
- await board.deleteSignal(alias)
259
- // Teto de entradas + prune agressivo de completed.
260
- await board.pruneCompleted(keepCompleted)
261
- await board.enforceEntryCap(maxEntries)
262
- const settled = board.get(taskID)
263
- const line = failed
264
- ? `[pantheon:${alias}] ${job.agent} — ERROR: ${firstLine(verifyError ?? settled?.lastStatusError ?? job.lastStatusError ?? 'failed', 90)}`.slice(
265
- 0,
266
- LINE_MAX,
267
- )
268
- : boardLine(settled ?? job)
269
- return { id: taskID, agent: job.agent, state: failed ? 'error' : 'reconciled', line }
270
- }
271
-
272
- /** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
273
- async function applyTerminal(
274
- taskID: string,
275
- state: 'completed' | 'error',
276
- fields: { resultSummary?: string; error?: string },
277
- ): Promise<void> {
278
- try {
279
- await board.updateStatus({ taskID, state, ...fields })
280
- } catch (err) {
281
- const msg = err instanceof Error ? err.message : String(err)
282
- if (!msg.startsWith('Invalid state transition')) throw err
283
- // Lost the race to another terminal writer (e.g. the V1 idle hook) —
284
- // the record stands; verification still governs the receipt.
285
- }
286
- }
287
-
288
- async function launchBackground(args: LaunchArgs, prompt: string): Promise<ManagerReceipt> {
289
- const key = args.agent.toLowerCase()
290
- pendingByAgent.set(key, pending(args.agent) + 1)
291
- seq += 1
292
- const taskID = args.taskID ?? `native-${Date.now()}-${seq}`
293
- try {
294
- const record = await board.registerLaunch({
295
- taskID,
296
- parentSessionID,
297
- agent: args.agent,
298
- description: args.description ?? firstLine(args.prompt, 120),
299
- })
300
- const outcome = await runTask(args.agent, prompt, args.model, taskID)
301
- if (!outcome.ok) {
302
- await applyTerminal(taskID, 'error', { error: outcome.error })
303
- return await settle(taskID, record.alias, outcome.error)
304
- }
305
- if (typeof outcome.result.content !== 'string' || outcome.result.content.trim() === '') {
306
- const verifyError = 'task result empty — not verified, never completed'
307
- await applyTerminal(taskID, 'error', { error: verifyError })
308
- return await settle(taskID, record.alias, verifyError)
309
- }
310
- await applyTerminal(taskID, 'completed', {
311
- resultSummary: firstLine(outcome.result.content, 500),
312
- })
313
- return await settle(taskID, record.alias)
314
- } finally {
315
- pendingByAgent.set(key, Math.max(0, pending(args.agent) - 1))
316
- }
317
- }
318
-
319
- async function launchForeground(args: LaunchArgs, prompt: string): Promise<ManagerReceipt> {
320
- seq += 1
321
- const id = args.taskID ?? `fg-${Date.now()}-${seq}`
322
- const outcome = await runTask(args.agent, prompt, args.model, id)
323
- const ok =
324
- outcome.ok &&
325
- typeof outcome.result.content === 'string' &&
326
- outcome.result.content.trim() !== ''
327
- const detail = ok
328
- ? firstLine((outcome as { ok: true; result: NativeTaskResult }).result.content ?? '', 90)
329
- : `ERROR: ${(outcome as { ok: false; error: string }).error ?? 'empty result — not verified'}`
330
- const line = `[native] ${args.agent} — ${detail} — ${ok ? 'OK' : 'ERR'} (foreground)`.slice(
331
- 0,
332
- LINE_MAX,
333
- )
334
- foregroundRecent.push({ agent: args.agent, line, at: Date.now() })
335
- if (foregroundRecent.length > 20) foregroundRecent.splice(0, foregroundRecent.length - 20)
336
- return { id, agent: args.agent, state: ok ? 'reconciled' : 'error', line }
337
- }
338
-
339
- return {
340
- async launch(args: LaunchArgs): Promise<ManagerReceipt> {
341
- guardEnabled()
342
- // R4 step budgets (routing.yml max_steps, every agent): capped agents
343
- // skip dispatch with a stop summary — no session, no board job.
344
- let prompt = args.prompt
345
- if (stepCap !== undefined) {
346
- const maxSteps = stepCap.maxStepsFor(args.agent)
347
- if (stepCap.isCapped(args.agent)) {
348
- const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
349
- return { id: `cap-${Date.now()}`, agent: args.agent, state: 'reconciled', line }
350
- }
351
- const rec = stepCap.recordStep(args.agent)
352
- if (rec.capped && rec.maxSteps !== undefined) {
353
- prompt = `${args.prompt}${buildStopInstruction(args.agent, rec.maxSteps)}`
354
- }
355
- }
356
- // Synchronous routing decision: background when a slot is free,
357
- // foreground fallback when full, clean rejection otherwise.
358
- const free =
359
- board.canDispatch(args.agent) && pending(args.agent) < board.maxConcurrentPerAgent
360
- if (free) return launchBackground(args, prompt)
361
- if (foregroundFallback) return launchForeground(args, prompt)
362
- throw new Error(
363
- `pantheon_delegate rejected: concurrency limit reached for agent "${args.agent}"`,
364
- )
365
- },
366
-
367
- async read(idOrAlias: string): Promise<string> {
368
- guardEnabled()
369
- const job = board.resolve(parentSessionID, idOrAlias) ?? board.get(idOrAlias)
370
- if (!job) throw new Error(`delegation not found: ${idOrAlias}`)
371
- const terminal = await withTimeout(
372
- board.waitForTerminal(job.taskID, readTimeoutMs),
373
- readTimeoutMs + 5_000,
374
- `read(${idOrAlias})`,
375
- )
376
- if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
377
- await board.deleteSignal(terminal.alias)
378
- if (terminal.state === 'error' && !terminal.resultSummary) {
379
- return `# Delegation Report — ${terminal.alias}\n\nERROR: ${terminal.lastStatusError ?? 'failed'}\n`
380
- }
381
- return (
382
- `# Delegation Report — ${terminal.alias}\n\n` +
383
- `${terminal.resultSummary ?? terminal.lastStatusError ?? terminal.state}\n`
384
- )
385
- },
386
-
387
- async list(): Promise<string[]> {
388
- guardEnabled()
389
- const lines = board.list(parentSessionID).map((job) => boardLine(job))
390
- for (const fg of foregroundRecent) lines.push(fg.line)
391
- return lines
392
- },
393
-
394
- async recover(): Promise<void> {
395
- await board.recoverRunningJobs()
396
- },
397
- }
398
- }
399
-
400
- // Native delegation tools live with the manager so the plugin mounts the
401
- // manager directly; there is intentionally no adapter or kill-switch wrapper.
402
- const delegateArgs = {
403
- prompt: z.string().min(1),
404
- agent: z.string().min(1),
405
- description: z.string().optional(),
406
- read_only: z.boolean().optional(),
407
- model: z.string().optional(),
408
- } satisfies z.ZodRawShape
409
- const readArgs = { id: z.string().min(1) } satisfies z.ZodRawShape
410
- const listArgs = {} satisfies z.ZodRawShape
411
- export interface ToolContextLike {
412
- sessionID: string
413
- agent?: string
414
- }
415
- export interface SessionTaskOptions {
416
- client: DelegationClient
417
- board: BackgroundJobBoard
418
- outputDir?: string
419
- promptTimeoutMs?: number
420
- settleTimeoutMs?: number
421
- }
422
- export function parseModelRef(model: string): { id: string; providerID: string } | undefined {
423
- const idx = model.indexOf('/')
424
- return idx > 0 && idx < model.length - 1
425
- ? { providerID: model.slice(0, idx), id: model.slice(idx + 1) }
426
- : undefined
427
- }
428
- export function extractOutputSection(md: string | undefined): string {
429
- if (md === undefined) return ''
430
- const at = md.indexOf('## Output')
431
- const body = at < 0 ? md : md.slice(at + 9)
432
- const errAt = body.indexOf('\n**Error**:')
433
- return (errAt < 0 ? body : body.slice(0, errAt)).trim()
434
- }
435
- async function promptAccept(
436
- client: DelegationClient,
437
- taskID: string,
438
- agent: string,
439
- prompt: string,
440
- model: string | undefined,
441
- signal: AbortSignal | undefined,
442
- timeoutMs: number,
443
- ): Promise<void> {
444
- const modelRef = model === undefined ? undefined : parseModelRef(model)
445
- let timer: ReturnType<typeof setTimeout> | undefined
446
- try {
447
- await Promise.race([
448
- client.session
449
- .promptAsync({
450
- path: { id: taskID },
451
- body: {
452
- agent,
453
- ...(modelRef === undefined ? {} : { model: modelRef }),
454
- parts: [{ type: 'text', text: prompt }],
455
- },
456
- ...(signal === undefined ? {} : { signal }),
457
- })
458
- .then(undefined, (error: unknown) => {
459
- throw new Error(
460
- `promptAsync rejected: ${error instanceof Error ? error.message : String(error)}`,
461
- )
462
- }),
463
- new Promise<never>((_, reject) => {
464
- timer = setTimeout(
465
- () => reject(new Error(`promptAsync timed out after ${timeoutMs}ms`)),
466
- timeoutMs,
467
- )
468
- timer.unref?.()
469
- }),
470
- ])
471
- } finally {
472
- if (timer !== undefined) clearTimeout(timer)
473
- }
474
- }
475
- export function createSessionTaskFn(options: SessionTaskOptions): NativeTaskFn {
476
- const {
477
- client,
478
- board,
479
- outputDir = DELEGATION_DEFAULTS.outputDir,
480
- promptTimeoutMs = 60_000,
481
- settleTimeoutMs = DELEGATION_DEFAULTS.timeoutMs,
482
- } = options
483
- return async ({ agent, prompt, model, signal, taskID }) => {
484
- await promptAccept(client, taskID, agent, prompt, model, signal, promptTimeoutMs)
485
- const terminal = await board.waitForTerminal(taskID, settleTimeoutMs)
486
- const md = await readDelegationReport(outputDir, terminal)
487
- return { content: extractOutputSection(md) || terminal.resultSummary || '' }
488
- }
489
- }
490
- export interface NativeDelegateWiring {
491
- board: BackgroundJobBoard
492
- client: DelegationClient
493
- outputDir?: string
494
- env?: Record<string, string | undefined>
495
- isRootSession: (sessionID: string) => boolean
496
- registerChildSession?: (sessionID: string, parentID: string) => void
497
- registerReadOnlySession?: (
498
- sessionID: string,
499
- info: { agent: string; readOnlyFlag?: boolean },
500
- ) => void
501
- isReadOnlyAgent?: (agent: string) => boolean
502
- agentModel?: (agent: string) => string | undefined
503
- managerOptions?: Partial<
504
- Omit<DelegateManagerOptions, 'board' | 'task' | 'parentSessionID' | 'env'>
505
- >
506
- stepCap?: StepCapTracker
507
- }
508
- export interface NativeDelegateToolset {
509
- pantheon_delegate: {
510
- description: string
511
- args: typeof delegateArgs
512
- execute(args: z.infer<z.ZodObject<typeof delegateArgs>>, ctx: ToolContextLike): Promise<string>
513
- }
514
- pantheon_delegation_read: {
515
- description: string
516
- args: typeof readArgs
517
- execute(args: z.infer<z.ZodObject<typeof readArgs>>, ctx: ToolContextLike): Promise<string>
518
- }
519
- pantheon_delegation_list: {
520
- description: string
521
- args: typeof listArgs
522
- execute(args: z.infer<z.ZodObject<typeof listArgs>>, ctx: ToolContextLike): Promise<string>
523
- }
524
- }
525
- function shortFirstLine(text: string, max = 60): string {
526
- const line =
527
- text
528
- .split('\n')
529
- .map((item) => item.trim())
530
- .find(Boolean) ?? ''
531
- return line.length > max ? `${line.slice(0, max - 1)}…` : line
532
- }
533
- export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeDelegateToolset {
534
- const {
535
- board,
536
- client,
537
- env = process.env,
538
- isRootSession,
539
- registerChildSession,
540
- registerReadOnlySession,
541
- isReadOnlyAgent,
542
- agentModel,
543
- managerOptions = {},
544
- stepCap,
545
- } = wiring
546
- const task = createSessionTaskFn({
547
- client,
548
- board,
549
- ...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
550
- ...(managerOptions.timeoutMs === undefined
551
- ? {}
552
- : { promptTimeoutMs: managerOptions.timeoutMs }),
553
- })
554
- const managers = new Map<string, DelegateManager>()
555
- const managerFor = (parentSessionID: string): DelegateManager => {
556
- const existing = managers.get(parentSessionID)
557
- if (existing !== undefined) return existing
558
- const manager = createDelegateManager({
559
- ...managerOptions,
560
- board,
561
- task,
562
- parentSessionID,
563
- env,
564
- ...(stepCap === undefined ? {} : { stepCap }),
565
- })
566
- managers.set(parentSessionID, manager)
567
- return manager
568
- }
569
- return {
570
- pantheon_delegate: {
571
- description: 'Dispatch a background agent as a child session and return a verified receipt.',
572
- args: delegateArgs,
573
- execute: async (args, ctx) => {
574
- if (!isDelegationEnabled(env))
575
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
576
- if (!isRootSession(ctx.sessionID))
577
- throw new Error(
578
- `pantheon_delegate rejected: session ${ctx.sessionID} is a sub-session — only root sessions can delegate`,
579
- )
580
- const model = args.model ?? agentModel?.(args.agent)
581
- let childID: string
582
- try {
583
- const modelRef = model === undefined ? undefined : parseModelRef(model)
584
- const created = await client.session.create({
585
- body: {
586
- parentID: ctx.sessionID,
587
- title: args.description ?? shortFirstLine(args.prompt),
588
- ...(modelRef === undefined ? {} : { model: modelRef }),
589
- },
590
- })
591
- childID = created.id
592
- } catch (error) {
593
- return `pantheon_delegate failed: session.create rejected: ${error instanceof Error ? error.message : String(error)}`
594
- }
595
- registerChildSession?.(childID, ctx.sessionID)
596
- if (args.read_only === true || isReadOnlyAgent?.(args.agent) === true)
597
- registerReadOnlySession?.(childID, {
598
- agent: args.agent,
599
- ...(args.read_only === undefined ? {} : { readOnlyFlag: args.read_only }),
600
- })
601
- const receipt = await managerFor(ctx.sessionID).launch({
602
- agent: args.agent,
603
- prompt: args.prompt,
604
- ...(args.description === undefined ? {} : { description: args.description }),
605
- ...(args.model === undefined ? {} : { model: args.model }),
606
- taskID: childID,
607
- })
608
- return receipt.line
609
- },
610
- },
611
- pantheon_delegation_read: {
612
- description: 'Block until a delegation finishes, then return its verified report.',
613
- args: readArgs,
614
- execute: async (args, ctx) => {
615
- if (!isDelegationEnabled(env))
616
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
617
- return managerFor(ctx.sessionID).read(args.id)
618
- },
619
- },
620
- pantheon_delegation_list: {
621
- description: 'List background delegations.',
622
- args: listArgs,
623
- execute: async (_args, ctx) => {
624
- if (!isDelegationEnabled(env))
625
- throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
626
- const lines = await managerFor(ctx.sessionID).list()
627
- return lines.length > 0 ? lines.join('\n') : 'No delegations.'
628
- },
629
- },
630
- }
631
- }
632
- export interface PluginNativeDelegationWiring {
633
- board: BackgroundJobBoard
634
- client: DelegationClient
635
- isRootSession: (sessionID: string) => boolean
636
- registerChildSession: (sessionID: string, parentID: string) => void
637
- registerReadOnlySession: (
638
- sessionID: string,
639
- info: { agent: string; readOnlyFlag?: boolean },
640
- ) => void
641
- readOnlyAgents: ReadonlySet<string>
642
- agentModels: Record<string, string | undefined>
643
- stepCap: StepCapTracker
644
- wallClockTimeoutMs?: number
645
- finalizeDelegation: DelegationToolset['finalizeDelegation']
646
- }
647
- export type PluginNativeDelegation = NativeDelegateToolset & {
648
- finalizeDelegation: DelegationToolset['finalizeDelegation']
649
- }
650
- export function buildPluginNativeDelegation(
651
- wiring: PluginNativeDelegationWiring,
652
- ): PluginNativeDelegation {
653
- return {
654
- ...createNativeDelegateTools({
655
- board: wiring.board,
656
- client: wiring.client,
657
- isRootSession: wiring.isRootSession,
658
- registerChildSession: wiring.registerChildSession,
659
- registerReadOnlySession: wiring.registerReadOnlySession,
660
- isReadOnlyAgent: (agent) => wiring.readOnlyAgents.has(agent.toLowerCase()),
661
- agentModel: (agent) => wiring.agentModels[agent.toLowerCase()],
662
- stepCap: wiring.stepCap,
663
- ...(wiring.wallClockTimeoutMs === undefined
664
- ? {}
665
- : { managerOptions: { timeoutMs: wiring.wallClockTimeoutMs } }),
666
- }),
667
- finalizeDelegation: wiring.finalizeDelegation,
668
- }
669
- }