pantheon-opencode 1.5.0-beta.8 → 1.5.0
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 +320 -0
- package/README.md +14 -27
- package/README.pt-BR.md +5 -6
- package/commands/pantheon.md +1 -35
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/scripts/doctor.mjs +32 -3
- package/scripts/install/plugin.mjs +9 -12
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/sync-tui.mjs +8 -11
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/zeus.md +9 -52
- 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/mcp_persistence_server.py +1 -1
- package/src/mcp/toon_codec.py +2 -4
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +61 -245
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +229 -77
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +509 -259
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +753 -279
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- 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/delegate-manager.ts +0 -766
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -406
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1337
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/native-probe.ts +0 -234
- 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/step-cap.ts +0 -110
- 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
- package/src/plugins/tui/dist/tui.tsx +0 -2777
|
@@ -1,766 +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 { createNativeProbe, type NativeProbeFn, type ProbeClient } from './native-probe.ts'
|
|
35
|
-
import { classifyNativeResult, type NativeTaskStatus } from './native-task-status.ts'
|
|
36
|
-
import {
|
|
37
|
-
buildStopInstruction,
|
|
38
|
-
cappedSummary,
|
|
39
|
-
DEFAULT_MAX_STEPS,
|
|
40
|
-
type StepCapTracker,
|
|
41
|
-
} from './step-cap.ts'
|
|
42
|
-
|
|
43
|
-
// ─── Types ─────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
/** Input shape for the native task() call (injected — fake in tests). */
|
|
46
|
-
export interface NativeTaskInput {
|
|
47
|
-
agent: string
|
|
48
|
-
prompt: string
|
|
49
|
-
model?: string
|
|
50
|
-
signal?: AbortSignal
|
|
51
|
-
/** Board taskID assigned by the manager (== child session id on V1 hosts). */
|
|
52
|
-
taskID: string
|
|
53
|
-
parentSessionID: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Result shape for the native task() call. */
|
|
57
|
-
export interface NativeTaskResult {
|
|
58
|
-
content?: string | null
|
|
59
|
-
tokensInput?: number
|
|
60
|
-
tokensOutput?: number
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** task()-shaped function: run one agent turn and return its result. */
|
|
64
|
-
export type NativeTaskFn = (input: NativeTaskInput) => Promise<NativeTaskResult>
|
|
65
|
-
|
|
66
|
-
/** Minimal launch receipt: id, agent, state, one short line. */
|
|
67
|
-
export interface ManagerReceipt {
|
|
68
|
-
id: string
|
|
69
|
-
agent: string
|
|
70
|
-
state: 'reconciled' | 'error'
|
|
71
|
-
status: NativeTaskStatus
|
|
72
|
-
line: string
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface LaunchArgs {
|
|
76
|
-
agent: string
|
|
77
|
-
prompt: string
|
|
78
|
-
description?: string
|
|
79
|
-
model?: string
|
|
80
|
-
/**
|
|
81
|
-
* Board taskID to register. The V1 session adapter supplies the child
|
|
82
|
-
* session id here so the idle hook and the manager share one record.
|
|
83
|
-
* Defaults to a generated `native-<ts>-<seq>` id.
|
|
84
|
-
*/
|
|
85
|
-
taskID?: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface DelegateManagerOptions {
|
|
89
|
-
board: BackgroundJobBoard
|
|
90
|
-
task: NativeTaskFn
|
|
91
|
-
parentSessionID: string
|
|
92
|
-
/** Env mapping (default: process.env). Kill-switch reads PANTHEON_DELEGATION. */
|
|
93
|
-
env?: Record<string, string | undefined>
|
|
94
|
-
/** Ordered model list for failover (default: single attempt, inherited model). */
|
|
95
|
-
models?: string[]
|
|
96
|
-
/** Per-attempt wall-clock budget in ms (default: 120_000). */
|
|
97
|
-
timeoutMs?: number
|
|
98
|
-
/** read() wait budget in ms (default: 60_000). */
|
|
99
|
-
readTimeoutMs?: number
|
|
100
|
-
/** Run inline without a board record when concurrency is full (default: true). */
|
|
101
|
-
foregroundFallback?: boolean
|
|
102
|
-
/** Hard cap on board entries; oldest terminal jobs evicted first (default: 50). */
|
|
103
|
-
maxEntries?: number
|
|
104
|
-
/** Aggressive prune: completed/reconciled jobs kept (default: 10). */
|
|
105
|
-
keepCompleted?: number
|
|
106
|
-
/** Per-agent step budgets (routing.yml max_steps). */
|
|
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
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export interface DelegateManager {
|
|
115
|
-
launch(args: LaunchArgs): Promise<ManagerReceipt>
|
|
116
|
-
ensureNativeAvailable(): Promise<NativeTaskStatus | undefined>
|
|
117
|
-
read(idOrAlias: string): Promise<string>
|
|
118
|
-
list(): Promise<string[]>
|
|
119
|
-
recover(): Promise<void>
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── Kill-switch ───────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
/** False only when `PANTHEON_DELEGATION=off` (case-insensitive). */
|
|
125
|
-
export function isDelegationEnabled(
|
|
126
|
-
env: Record<string, string | undefined> = process.env,
|
|
127
|
-
): boolean {
|
|
128
|
-
return (env.PANTHEON_DELEGATION ?? '').trim().toLowerCase() !== 'off'
|
|
129
|
-
}
|
|
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
|
-
|
|
138
|
-
// ─── Helpers ───────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
const LINE_MAX = 160
|
|
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
|
-
|
|
162
|
-
function firstLine(text: string, max = 80): string {
|
|
163
|
-
const line =
|
|
164
|
-
sanitizeReceiptText(text ?? '')
|
|
165
|
-
.split('\n')
|
|
166
|
-
.map((l) => l.trim())
|
|
167
|
-
.find((l) => l !== '') ?? ''
|
|
168
|
-
return line.length > max ? `${line.slice(0, max - 1)}…` : line
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function shortState(state: BackgroundJobRecord['state']): string {
|
|
172
|
-
switch (state) {
|
|
173
|
-
case 'completed':
|
|
174
|
-
return 'OK'
|
|
175
|
-
case 'error':
|
|
176
|
-
return 'ERR'
|
|
177
|
-
case 'cancelled':
|
|
178
|
-
return 'CAN'
|
|
179
|
-
case 'running':
|
|
180
|
-
return 'RUN'
|
|
181
|
-
default:
|
|
182
|
-
return 'REC'
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function boardLine(job: BackgroundJobRecord): string {
|
|
187
|
-
const desc = firstLine(job.description, 80)
|
|
188
|
-
return `[pantheon:${job.alias}] ${job.agent} — ${desc} — ${shortState(job.state)}`.slice(
|
|
189
|
-
0,
|
|
190
|
-
LINE_MAX,
|
|
191
|
-
)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
195
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
196
|
-
const guard = new Promise<never>((_, reject) => {
|
|
197
|
-
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
|
|
198
|
-
timer.unref?.()
|
|
199
|
-
})
|
|
200
|
-
return Promise.race([promise, guard]).finally(() => {
|
|
201
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ─── Factory ───────────────────────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
export function createDelegateManager(options: DelegateManagerOptions): DelegateManager {
|
|
208
|
-
const {
|
|
209
|
-
board,
|
|
210
|
-
task,
|
|
211
|
-
parentSessionID,
|
|
212
|
-
env = process.env,
|
|
213
|
-
models: modelsOpt = [],
|
|
214
|
-
timeoutMs = 120_000,
|
|
215
|
-
readTimeoutMs = 60_000,
|
|
216
|
-
foregroundFallback: fgFallbackOpt = true,
|
|
217
|
-
maxEntries = 50,
|
|
218
|
-
keepCompleted = 10,
|
|
219
|
-
stepCap,
|
|
220
|
-
nativeProbe,
|
|
221
|
-
probeClient,
|
|
222
|
-
} = options
|
|
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
|
-
|
|
230
|
-
// Synchronous in-flight reservations per agent: launch() must decide
|
|
231
|
-
// background vs foreground BEFORE its first await, otherwise concurrent
|
|
232
|
-
// launches without interleaved awaits would all see an empty board.
|
|
233
|
-
const pendingByAgent = new Map<string, number>()
|
|
234
|
-
let seq = 0
|
|
235
|
-
const foregroundRecent: { agent: string; line: string; at: number }[] = []
|
|
236
|
-
let probeResult: Awaited<ReturnType<NativeProbeFn>> | null = null
|
|
237
|
-
|
|
238
|
-
function pending(agent: string): number {
|
|
239
|
-
return pendingByAgent.get(agent.toLowerCase()) ?? 0
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function guardEnabled(): void {
|
|
243
|
-
if (!isDelegationEnabled(env)) {
|
|
244
|
-
throw new Error('pantheon delegation disabled (PANTHEON_DELEGATION=off)')
|
|
245
|
-
}
|
|
246
|
-
}
|
|
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
|
-
|
|
254
|
-
async function runTask(
|
|
255
|
-
agent: string,
|
|
256
|
-
prompt: string,
|
|
257
|
-
explicitModel: string | undefined,
|
|
258
|
-
taskID: string,
|
|
259
|
-
): Promise<{ ok: true; result: NativeTaskResult } | { ok: false; error: string }> {
|
|
260
|
-
const attempts: (string | undefined)[] =
|
|
261
|
-
explicitModel !== undefined ? [explicitModel] : models.length > 0 ? models : [undefined]
|
|
262
|
-
let lastError = 'no model attempts configured'
|
|
263
|
-
for (const model of attempts) {
|
|
264
|
-
try {
|
|
265
|
-
const controller = new AbortController()
|
|
266
|
-
const result = await withTimeout(
|
|
267
|
-
task({
|
|
268
|
-
agent,
|
|
269
|
-
prompt,
|
|
270
|
-
signal: controller.signal,
|
|
271
|
-
taskID,
|
|
272
|
-
parentSessionID,
|
|
273
|
-
...(model !== undefined ? { model } : {}),
|
|
274
|
-
}),
|
|
275
|
-
timeoutMs,
|
|
276
|
-
`task(${agent})`,
|
|
277
|
-
)
|
|
278
|
-
return { ok: true, result }
|
|
279
|
-
} catch (err) {
|
|
280
|
-
lastError = err instanceof Error ? err.message : String(err)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return { ok: false, error: lastError }
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function settle(
|
|
287
|
-
taskID: string,
|
|
288
|
-
alias: string,
|
|
289
|
-
verifyError?: string,
|
|
290
|
-
): Promise<ManagerReceipt> {
|
|
291
|
-
const job = board.get(taskID)
|
|
292
|
-
if (!job) throw new Error(`job ${taskID} vanished after terminal state`)
|
|
293
|
-
// The receipt reports the VERIFIED outcome, not the raw terminal state:
|
|
294
|
-
// a `completed` the manager could not verify stays `error` for the
|
|
295
|
-
// caller ("complete ONLY after verified result"). Reconcile is only the
|
|
296
|
-
// acknowledgment that the result was consumed.
|
|
297
|
-
const terminalState = job.state
|
|
298
|
-
const failed = verifyError !== undefined || terminalState === 'error'
|
|
299
|
-
const status = classifyNativeResult(job, verifyError)
|
|
300
|
-
// Auto-reconcile + delete the signal file on the spot.
|
|
301
|
-
if (job.state !== 'reconciled') await board.markReconciled(taskID)
|
|
302
|
-
await board.deleteSignal(alias)
|
|
303
|
-
// Teto de entradas + prune agressivo de completed.
|
|
304
|
-
await board.pruneCompleted(keepCompleted)
|
|
305
|
-
await board.enforceEntryCap(maxEntries)
|
|
306
|
-
const settled = board.get(taskID)
|
|
307
|
-
const line = failed
|
|
308
|
-
? `[pantheon:${alias}] ${job.agent} — ERROR: ${firstLine(verifyError ?? settled?.lastStatusError ?? job.lastStatusError ?? 'failed', 90)}`.slice(
|
|
309
|
-
0,
|
|
310
|
-
LINE_MAX,
|
|
311
|
-
)
|
|
312
|
-
: boardLine(settled ?? job)
|
|
313
|
-
return { id: taskID, agent: job.agent, state: failed ? 'error' : 'reconciled', status, line }
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
|
|
317
|
-
async function applyTerminal(
|
|
318
|
-
taskID: string,
|
|
319
|
-
state: 'completed' | 'error',
|
|
320
|
-
fields: { resultSummary?: string; error?: string },
|
|
321
|
-
): Promise<void> {
|
|
322
|
-
try {
|
|
323
|
-
await board.updateStatus({ taskID, state, ...fields })
|
|
324
|
-
} catch (err) {
|
|
325
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
326
|
-
if (!msg.startsWith('Invalid state transition')) throw err
|
|
327
|
-
// Lost the race to another terminal writer (e.g. the V1 idle hook) —
|
|
328
|
-
// the record stands; verification still governs the receipt.
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function launchBackground(args: LaunchArgs, prompt: string): Promise<ManagerReceipt> {
|
|
333
|
-
const key = args.agent.toLowerCase()
|
|
334
|
-
pendingByAgent.set(key, pending(args.agent) + 1)
|
|
335
|
-
seq += 1
|
|
336
|
-
const taskID = args.taskID ?? `native-${Date.now()}-${seq}`
|
|
337
|
-
try {
|
|
338
|
-
const record = await board.registerLaunch({
|
|
339
|
-
taskID,
|
|
340
|
-
parentSessionID,
|
|
341
|
-
agent: args.agent,
|
|
342
|
-
description: args.description ?? firstLine(args.prompt, 120),
|
|
343
|
-
})
|
|
344
|
-
const outcome = await runTask(args.agent, prompt, args.model, taskID)
|
|
345
|
-
if (!outcome.ok) {
|
|
346
|
-
await applyTerminal(taskID, 'error', { error: outcome.error })
|
|
347
|
-
return await settle(taskID, record.alias, outcome.error)
|
|
348
|
-
}
|
|
349
|
-
if (typeof outcome.result.content !== 'string' || outcome.result.content.trim() === '') {
|
|
350
|
-
const verifyError = 'task result empty — not verified, never completed'
|
|
351
|
-
await applyTerminal(taskID, 'error', { error: verifyError })
|
|
352
|
-
return await settle(taskID, record.alias, verifyError)
|
|
353
|
-
}
|
|
354
|
-
await applyTerminal(taskID, 'completed', {
|
|
355
|
-
resultSummary: firstLine(outcome.result.content, 500),
|
|
356
|
-
})
|
|
357
|
-
return await settle(taskID, record.alias)
|
|
358
|
-
} finally {
|
|
359
|
-
pendingByAgent.set(key, Math.max(0, pending(args.agent) - 1))
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async function launchForeground(args: LaunchArgs, prompt: string): Promise<ManagerReceipt> {
|
|
364
|
-
seq += 1
|
|
365
|
-
const id = args.taskID ?? `fg-${Date.now()}-${seq}`
|
|
366
|
-
const outcome = await runTask(args.agent, prompt, args.model, id)
|
|
367
|
-
const ok =
|
|
368
|
-
outcome.ok &&
|
|
369
|
-
typeof outcome.result.content === 'string' &&
|
|
370
|
-
outcome.result.content.trim() !== ''
|
|
371
|
-
const detail = ok
|
|
372
|
-
? firstLine((outcome as { ok: true; result: NativeTaskResult }).result.content ?? '', 90)
|
|
373
|
-
: `ERROR: ${(outcome as { ok: false; error: string }).error ?? 'empty result — not verified'}`
|
|
374
|
-
const line = `[native] ${args.agent} — ${detail} — ${ok ? 'OK' : 'ERR'} (foreground)`.slice(
|
|
375
|
-
0,
|
|
376
|
-
LINE_MAX,
|
|
377
|
-
)
|
|
378
|
-
foregroundRecent.push({ agent: args.agent, line, at: Date.now() })
|
|
379
|
-
if (foregroundRecent.length > 20) foregroundRecent.splice(0, foregroundRecent.length - 20)
|
|
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 }
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
async launch(args: LaunchArgs): Promise<ManagerReceipt> {
|
|
411
|
-
guardEnabled()
|
|
412
|
-
// R4 step budgets (routing.yml max_steps, every agent): capped agents
|
|
413
|
-
// skip dispatch with a stop summary — no session, no board job.
|
|
414
|
-
let prompt = args.prompt
|
|
415
|
-
if (stepCap !== undefined) {
|
|
416
|
-
const maxSteps = stepCap.maxStepsFor(args.agent)
|
|
417
|
-
if (stepCap.isCapped(args.agent)) {
|
|
418
|
-
const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
|
|
419
|
-
return {
|
|
420
|
-
id: `cap-${Date.now()}`,
|
|
421
|
-
agent: args.agent,
|
|
422
|
-
state: 'reconciled',
|
|
423
|
-
status: 'ESCALATE',
|
|
424
|
-
line,
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
const rec = stepCap.recordStep(args.agent)
|
|
428
|
-
if (rec.capped && rec.maxSteps !== undefined) {
|
|
429
|
-
prompt = `${args.prompt}${buildStopInstruction(args.agent, rec.maxSteps)}`
|
|
430
|
-
}
|
|
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
|
-
}
|
|
444
|
-
// Synchronous routing decision: background when a slot is free,
|
|
445
|
-
// foreground fallback when full, clean rejection otherwise.
|
|
446
|
-
const free =
|
|
447
|
-
board.canDispatch(args.agent) && pending(args.agent) < board.maxConcurrentPerAgent
|
|
448
|
-
if (free) return launchBackground(args, prompt)
|
|
449
|
-
if (foregroundFallback) return launchForeground(args, prompt)
|
|
450
|
-
throw new Error(
|
|
451
|
-
`pantheon_delegate rejected: concurrency limit reached for agent "${args.agent}"`,
|
|
452
|
-
)
|
|
453
|
-
},
|
|
454
|
-
ensureNativeAvailable,
|
|
455
|
-
|
|
456
|
-
async read(idOrAlias: string): Promise<string> {
|
|
457
|
-
guardEnabled()
|
|
458
|
-
const job = board.resolve(parentSessionID, idOrAlias) ?? board.get(idOrAlias)
|
|
459
|
-
if (!job) throw new Error(`delegation not found: ${idOrAlias}`)
|
|
460
|
-
const terminal = await withTimeout(
|
|
461
|
-
board.waitForTerminal(job.taskID, readTimeoutMs),
|
|
462
|
-
readTimeoutMs + 5_000,
|
|
463
|
-
`read(${idOrAlias})`,
|
|
464
|
-
)
|
|
465
|
-
if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
|
|
466
|
-
await board.deleteSignal(terminal.alias)
|
|
467
|
-
if (terminal.state === 'error' && !terminal.resultSummary) {
|
|
468
|
-
return `# Delegation Report — ${terminal.alias}\n\nERROR: ${sanitizeReceiptText(terminal.lastStatusError ?? 'failed')}\n`
|
|
469
|
-
}
|
|
470
|
-
return (
|
|
471
|
-
`# Delegation Report — ${terminal.alias}\n\n` +
|
|
472
|
-
`${sanitizeReceiptText(terminal.resultSummary ?? terminal.lastStatusError ?? terminal.state)}\n`
|
|
473
|
-
)
|
|
474
|
-
},
|
|
475
|
-
|
|
476
|
-
async list(): Promise<string[]> {
|
|
477
|
-
guardEnabled()
|
|
478
|
-
const lines = board.list(parentSessionID).map((job) => boardLine(job))
|
|
479
|
-
for (const fg of foregroundRecent) lines.push(fg.line)
|
|
480
|
-
return lines
|
|
481
|
-
},
|
|
482
|
-
|
|
483
|
-
async recover(): Promise<void> {
|
|
484
|
-
await board.recoverRunningJobs()
|
|
485
|
-
},
|
|
486
|
-
}
|
|
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
|
-
}
|