pantheon-opencode 1.5.0-beta.13 → 1.5.0-beta.15
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/CHANGELOG.md +59 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/src/agents/zeus.md +4 -26
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/delegate-manager.ts +95 -10
- package/src/pantheon/delegation-classifier.ts +3 -4
- package/src/pantheon/delegation-finalize.ts +69 -7
- package/src/pantheon/delegation.ts +16 -4
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/toon-codec.ts +1 -1
- package/src/plugin-v2.ts +4 -6
- package/src/plugin.ts +6 -6
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +57 -40
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +145 -54
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +245 -83
- package/src/routing.yml +0 -16
- package/scripts/hash_verify.py +0 -192
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/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/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,65 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
18
18
|
|
|
19
19
|
## ✅ Closed Issues
|
|
20
20
|
|
|
21
|
+
## [v1.5.0-beta.15] - 2026-09-15
|
|
22
|
+
|
|
23
|
+
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
|
24
|
+
move this section to a versioned entry and reset the template below. -->
|
|
25
|
+
|
|
26
|
+
## 🆕 What's New
|
|
27
|
+
|
|
28
|
+
- **Delegation `read` devolve o relatório completo:** o retorno de `read` passa a
|
|
29
|
+
ler o relatório integral do disco (com cap configurável e marker explícito de
|
|
30
|
+
truncamento) em vez do recibo resumido — o conteúdo é o mesmo tanto no caminho
|
|
31
|
+
nativo quanto no legado, com paridade garantida entre eles.
|
|
32
|
+
- **`sanitizeReceiptText` UTF-8-safe:** a sanitização de recibos deixou de corromper
|
|
33
|
+
caracteres multibyte; acentuação e demais codepoints UTF-8 são preservados no
|
|
34
|
+
caminho nativo e no legado.
|
|
35
|
+
- **TUI — cor de status na row inteira:** cada linha do painel Delegations passa a
|
|
36
|
+
ser pintada pelo estado — vermelho = falha, verde = concluído, amarelo = em
|
|
37
|
+
andamento — com o glyph mantido como canal redundante para acessibilidade.
|
|
38
|
+
|
|
39
|
+
## 🐞 Fixed
|
|
40
|
+
|
|
41
|
+
- **TUI — painel Delegations só da sessão ativa:** o painel deixa de renderizar
|
|
42
|
+
rows cross-session/órfãs que causavam "Session not found" ao clicar; a lista
|
|
43
|
+
agora cobre apenas a sessão ativa, mantendo a retenção de concluídas (2 min)
|
|
44
|
+
e falhas (10 min).
|
|
45
|
+
- **Guards de estado e navegação órfã:** transições prematuras para `done` são
|
|
46
|
+
bloqueadas e a navegação defensiva ignora alvos órfãos, evitando seleção
|
|
47
|
+
inválida no painel.
|
|
48
|
+
- **Isolamento do board nos testes:** testes que importam o plugin real agora
|
|
49
|
+
isolam o BackgroundJobBoard, eliminando o fantasma "preserve this running
|
|
50
|
+
delegation"; o board real foi limpo.
|
|
51
|
+
- **Lean no core:** 17 módulos mortos de `src/pantheon/` removidos, ~38% dos
|
|
52
|
+
arquivos de teste removidos/consolidados e docs/scripts órfãos limpos, com
|
|
53
|
+
testes mínimos readicionados para os módulos vivos (tool-ceiling, native-probe).
|
|
54
|
+
- **Flake de TTL determinístico:** `test_ttl_expiry_real_time` deixou de competir
|
|
55
|
+
com a truncagem de microssegundos do SQLite — o teste agora espera o início de
|
|
56
|
+
um segundo limpo antes do store e usa margem sobre o TTL, eliminando o flake
|
|
57
|
+
dependente de carga (test-only).
|
|
58
|
+
|
|
59
|
+
## [v1.5.0-beta.14] - 2026-09-14
|
|
60
|
+
|
|
61
|
+
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
|
62
|
+
move this section to a versioned entry and reset the template below. -->
|
|
63
|
+
|
|
64
|
+
## 🆕 What's New
|
|
65
|
+
|
|
66
|
+
- **Ordenação active-first:** delegações ativas aparecem antes das concluídas,
|
|
67
|
+
falhas e canceladas no painel TUI.
|
|
68
|
+
- **Overflow active:** quando há mais delegações ativas que o teto visível, o
|
|
69
|
+
excedente ativo permanece representado na linha de overflow.
|
|
70
|
+
|
|
71
|
+
## 🐞 Fixed
|
|
72
|
+
|
|
73
|
+
- **Retenção visual:** concluídas permanecem visíveis por 2 min e falhas por
|
|
74
|
+
10 min antes de saírem do painel; canceladas seguem a retenção configurada.
|
|
75
|
+
|
|
76
|
+
## ⚠️ Known Issues
|
|
77
|
+
|
|
78
|
+
- **Stale running warning:** jobs em estado `running` além do limite esperado
|
|
79
|
+
recebem um aviso visual de stale no painel.
|
|
21
80
|
## [v1.5.0-beta.13] - 2026-09-14
|
|
22
81
|
|
|
23
82
|
<!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon",
|
|
3
3
|
"publisher": "ils15",
|
|
4
|
-
"version": "1.5.0-beta.
|
|
4
|
+
"version": "1.5.0-beta.15",
|
|
5
5
|
"displayName": "Pantheon",
|
|
6
6
|
"description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
|
|
7
7
|
"agents": "./src/agents",
|
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pantheon"
|
|
7
|
-
version = "1.5.0-beta.
|
|
7
|
+
version = "1.5.0-beta.15"
|
|
8
8
|
description = "Multi-agent orchestration framework"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = []
|
|
@@ -37,10 +37,8 @@ ignore = ["E501"]
|
|
|
37
37
|
"scripts/scrub-secrets.py" = ["N999"]
|
|
38
38
|
# These modules defer optional/heavy imports until the corresponding command is used.
|
|
39
39
|
"scripts/vector_memory/*.py" = ["PLC0415"]
|
|
40
|
-
"tests/test_code_mode_args.py" = ["PLC0415"]
|
|
41
40
|
"tests/test_code_mode_sandbox.py" = ["PLC0415", "N806"]
|
|
42
41
|
"tests/test_code_mode_server.py" = ["PLC0415"]
|
|
43
|
-
"tests/test_eval_store.py" = ["PLC0415", "PLR2004"]
|
|
44
42
|
"tests/test_memory_mcp_server.py" = ["PLC0415", "PLR2004"]
|
|
45
43
|
# Existing public APIs and test fixtures are intentionally broad/simple; refactoring
|
|
46
44
|
# them would be behavior-affecting rather than a mechanical lint correction.
|
|
@@ -54,8 +52,7 @@ ignore = ["E501"]
|
|
|
54
52
|
"scripts/vector_memory/query.py" = ["PLR0913"]
|
|
55
53
|
"src/mcp/eval_store.py" = ["PLR2004"]
|
|
56
54
|
"tests/test_code_mode_frontmatter.py" = ["PLR2004"]
|
|
57
|
-
"tests/
|
|
58
|
-
"tests/test_eval_static.py" = ["PLR2004"]
|
|
55
|
+
"tests/test_eval.py" = ["PLR2004"]
|
|
59
56
|
"tests/test_mcp_persistence_server.py" = ["PLR2004"]
|
|
60
57
|
|
|
61
58
|
[tool.mypy]
|
package/src/agents/zeus.md
CHANGED
|
@@ -301,32 +301,10 @@ vão a idle com todos incompletos. Guards (todos no `src/pantheon/todo-enforcer.
|
|
|
301
301
|
|
|
302
302
|
## Wave 4 (PR #46): Empty-Result Retry + /cost + Themis Tier
|
|
303
303
|
|
|
304
|
-
### Empty-Result Retry (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
import { createDispatchGuard } from '.../src/pantheon/dispatch-guard.ts' # via code-mode ou subagente
|
|
311
|
-
|
|
312
|
-
guard = createDispatchGuard({ retryOnEmpty: true, logger: { warn: console.warn } })
|
|
313
|
-
|
|
314
|
-
# classificar resultado de task_status/wait:
|
|
315
|
-
# 'content' → tem texto, pronto
|
|
316
|
-
# 'empty-mode1' → SEM texto E SEM tokens (nada voltou) → RETRY 1x
|
|
317
|
-
# 'empty-mode2' → SEM texto, MAS tokens (raciocinou, perdeu a parte de texto;
|
|
318
|
-
# assinatura da falha themis Wave-2) → RETRY 1x
|
|
319
|
-
|
|
320
|
-
out = await guard.maybeRetry(result, async () => { ...task() de novo... })
|
|
321
|
-
# out.retried=true se redisparou; CAP DURO de 1 retry — nunca 2x.
|
|
322
|
-
# Se o retry voltar vazio, `out.retried=false` — NAO tente de novo: escale.
|
|
323
|
-
```
|
|
324
|
-
Regra: retry 1x APENAS em `empty-mode1`/`empty-mode2`. Resultado com conteudo
|
|
325
|
-
nunca redispara. Apos 1 retry vazio → escalate (mesma regra do TODO Enforcer).
|
|
326
|
-
|
|
327
|
-
> **Waves DEVEM usar `zeusDelegateWithRetry`** (`src/pantheon/zeus-delegate-with-retry.ts`) — encapsula `delegate → waitForTerminal → classify → retry 1x → escalate` com cap duro 1; se ainda vazio throw `ZeusEscalationError` (escale: tente outro agente, simplifique, manual).
|
|
328
|
-
> Uso alto-nível: `import { zeusDelegateWithRetry } from './pantheon/zeus-delegate-with-retry.ts'` → `await zeusDelegateWithRetry({ board, client, sessionID, agent, prompt })`.
|
|
329
|
-
> Uso baixo-nível: `createZeusRetryHelper().executeWithRetry(first, () => secondRead())` — classifica empty e decide retry/escalate (mesmo cap 1).
|
|
304
|
+
### Empty-Result Retry (runtime — automatic)
|
|
305
|
+
Empty-response detection (`empty-mode1` / `empty-mode2`) and stuck-agent
|
|
306
|
+
classification are wired at runtime in `delegation-classifier.ts` (consumed by
|
|
307
|
+
`delegation.ts`). No manual retry helper is required.
|
|
330
308
|
|
|
331
309
|
### /cost — pantheon_cost tool (WIRED no plugin)
|
|
332
310
|
`pantheon_cost({ days?: number })` le o `opencode.db` READ-ONLY usando o backend
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Background Job Board — state machine for tracking background agent jobs.
|
|
3
3
|
*
|
|
4
4
|
* Tracks jobs through a state machine (running → completed/error/cancelled → reconciled),
|
|
5
|
-
* supports alias generation per session, persistence via adapter,
|
|
5
|
+
* supports alias generation per session, persistence via adapter, wake signal files,
|
|
6
6
|
* and terminal state listeners (persist-before-notify write-ahead-log pattern).
|
|
7
7
|
*
|
|
8
8
|
* Pure TypeScript — zero external dependencies beyond Node.js builtins (fs/path).
|
|
@@ -123,7 +123,7 @@ export interface BoardOptions {
|
|
|
123
123
|
/** Maximum completed jobs kept per agent for reuse (default: 3). */
|
|
124
124
|
maxReusablePerAgent?: number
|
|
125
125
|
/**
|
|
126
|
-
* Directory for
|
|
126
|
+
* Directory for wake signal files.
|
|
127
127
|
* When set, a `.signal.json` file is atomically written on every terminal transition.
|
|
128
128
|
* When null or undefined, no signal files are written.
|
|
129
129
|
*/
|
|
@@ -175,7 +175,7 @@ function getAgentPrefix(agent: string): string {
|
|
|
175
175
|
* - Strict state machine with validated transitions
|
|
176
176
|
* - Per-session, per-agent alias generation (apo-1, her-2, …)
|
|
177
177
|
* - Optional persistence via injected PersistenceAdapter
|
|
178
|
-
* - Optional
|
|
178
|
+
* - Optional wake signal files on terminal transitions
|
|
179
179
|
* - Terminal state listeners with persist-before-notify guarantee
|
|
180
180
|
* - expire/prune helpers for session lifecycle management
|
|
181
181
|
*/
|
|
@@ -652,7 +652,7 @@ export class BackgroundJobBoard {
|
|
|
652
652
|
}
|
|
653
653
|
|
|
654
654
|
/**
|
|
655
|
-
* Delete the
|
|
655
|
+
* Delete the wake signal file for an alias (`<alias>.signal.json`).
|
|
656
656
|
* No-op (false) when no signalDir is configured or the file is absent.
|
|
657
657
|
* The delegate manager calls this on the spot right after auto-reconcile.
|
|
658
658
|
*/
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
* @module delegate-manager
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { join } from 'node:path'
|
|
26
27
|
import { z } from 'zod'
|
|
27
28
|
import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
|
|
28
29
|
import type { DelegationToolset } from './delegation.ts'
|
|
29
30
|
import {
|
|
31
|
+
capReportOutput,
|
|
30
32
|
DELEGATION_DEFAULTS,
|
|
31
33
|
type DelegationClient,
|
|
32
34
|
readDelegationReport,
|
|
35
|
+
resolveReadMaxChars,
|
|
33
36
|
} from './delegation-finalize.ts'
|
|
34
37
|
import { createNativeProbe, type NativeProbeFn, type ProbeClient } from './native-probe.ts'
|
|
35
38
|
import { classifyNativeResult, type NativeTaskStatus } from './native-task-status.ts'
|
|
@@ -66,10 +69,14 @@ export type NativeTaskFn = (input: NativeTaskInput) => Promise<NativeTaskResult>
|
|
|
66
69
|
/** Minimal launch receipt: id, agent, state, one short line. */
|
|
67
70
|
export interface ManagerReceipt {
|
|
68
71
|
id: string
|
|
72
|
+
/** Board alias of the job (e.g. `apo-1`) — report files are keyed by it. */
|
|
73
|
+
alias: string
|
|
69
74
|
agent: string
|
|
70
75
|
state: 'reconciled' | 'error'
|
|
71
76
|
status: NativeTaskStatus
|
|
72
77
|
line: string
|
|
78
|
+
/** Verified child output (when the task completed with content). */
|
|
79
|
+
output?: string
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
export interface LaunchArgs {
|
|
@@ -109,6 +116,15 @@ export interface DelegateManagerOptions {
|
|
|
109
116
|
nativeProbe?: NativeProbeFn
|
|
110
117
|
/** V1 client used by the lazy capability probe. */
|
|
111
118
|
probeClient?: ProbeClient
|
|
119
|
+
/** Base directory for delegation reports (default `.pantheon/delegations`). */
|
|
120
|
+
outputDir?: string
|
|
121
|
+
/**
|
|
122
|
+
* Inline cap (chars) for report content returned to the caller. Oversized
|
|
123
|
+
* reports keep head+tail with an explicit `[TRUNCATED …]` marker pointing
|
|
124
|
+
* at the full report file. Default: PANTHEON_DELEGATION_READ_MAX_CHARS
|
|
125
|
+
* or 50_000.
|
|
126
|
+
*/
|
|
127
|
+
readMaxChars?: number
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
export interface DelegateManager {
|
|
@@ -139,7 +155,9 @@ export function resolveDelegateMode(
|
|
|
139
155
|
|
|
140
156
|
const LINE_MAX = 160
|
|
141
157
|
|
|
142
|
-
/** Remove terminal escapes and control characters from untrusted child output.
|
|
158
|
+
/** Remove terminal escapes and control characters from untrusted child output.
|
|
159
|
+
* UTF-8 text (accents, emoji) is PRESERVED — only ANSI sequences, C0/C1
|
|
160
|
+
* controls and DEL are stripped; receipt/report lines stay human-readable. */
|
|
143
161
|
export function sanitizeReceiptText(text: string): string {
|
|
144
162
|
let clean = ''
|
|
145
163
|
let skippingAnsi = false
|
|
@@ -154,7 +172,8 @@ export function sanitizeReceiptText(text: string): string {
|
|
|
154
172
|
skippingAnsi = true
|
|
155
173
|
continue
|
|
156
174
|
}
|
|
157
|
-
|
|
175
|
+
const isControl = code < 0x20 || (code >= 0x7f && code <= 0x9f)
|
|
176
|
+
if (!isControl || char === '\n' || char === '\t') clean += char
|
|
158
177
|
}
|
|
159
178
|
return clean
|
|
160
179
|
}
|
|
@@ -219,7 +238,13 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
219
238
|
stepCap,
|
|
220
239
|
nativeProbe,
|
|
221
240
|
probeClient,
|
|
241
|
+
outputDir = DELEGATION_DEFAULTS.outputDir,
|
|
242
|
+
readMaxChars,
|
|
222
243
|
} = options
|
|
244
|
+
const inlineMaxChars = readMaxChars ?? resolveReadMaxChars(env)
|
|
245
|
+
|
|
246
|
+
/** Report file path for a job alias (the durable full report). */
|
|
247
|
+
const reportPathFor = (alias: string): string => join(outputDir, parentSessionID, `${alias}.md`)
|
|
223
248
|
|
|
224
249
|
// Strict native mode: foregroundFallback and models are overridden.
|
|
225
250
|
// The user-facing options are ignored when PANTHEON_DELEGATE_MODE=native.
|
|
@@ -310,7 +335,14 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
310
335
|
LINE_MAX,
|
|
311
336
|
)
|
|
312
337
|
: boardLine(settled ?? job)
|
|
313
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
id: taskID,
|
|
340
|
+
alias,
|
|
341
|
+
agent: job.agent,
|
|
342
|
+
state: failed ? 'error' : 'reconciled',
|
|
343
|
+
status,
|
|
344
|
+
line,
|
|
345
|
+
}
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
/** Apply a terminal transition best-effort: a racing hook may have gotten there first. */
|
|
@@ -354,7 +386,11 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
354
386
|
await applyTerminal(taskID, 'completed', {
|
|
355
387
|
resultSummary: firstLine(outcome.result.content, 500),
|
|
356
388
|
})
|
|
357
|
-
|
|
389
|
+
const settled = await settle(taskID, record.alias)
|
|
390
|
+
// The task() call already blocked until completion — return the verified
|
|
391
|
+
// output in the receipt (the parent reads the full report via the alias
|
|
392
|
+
// when needed). The tool layer caps it inline.
|
|
393
|
+
return { ...settled, output: outcome.result.content }
|
|
358
394
|
} finally {
|
|
359
395
|
pendingByAgent.set(key, Math.max(0, pending(args.agent) - 1))
|
|
360
396
|
}
|
|
@@ -403,7 +439,17 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
403
439
|
},
|
|
404
440
|
ok ? undefined : (outcome as { ok: false; error?: string }).error,
|
|
405
441
|
)
|
|
406
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
id,
|
|
444
|
+
alias: id,
|
|
445
|
+
agent: args.agent,
|
|
446
|
+
state: ok ? 'reconciled' : 'error',
|
|
447
|
+
status,
|
|
448
|
+
line,
|
|
449
|
+
...(ok
|
|
450
|
+
? { output: (outcome as { ok: true; result: NativeTaskResult }).result.content ?? '' }
|
|
451
|
+
: {}),
|
|
452
|
+
}
|
|
407
453
|
}
|
|
408
454
|
|
|
409
455
|
return {
|
|
@@ -416,8 +462,10 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
416
462
|
const maxSteps = stepCap.maxStepsFor(args.agent)
|
|
417
463
|
if (stepCap.isCapped(args.agent)) {
|
|
418
464
|
const line = cappedSummary(args.agent, maxSteps ?? DEFAULT_MAX_STEPS).slice(0, LINE_MAX)
|
|
465
|
+
const cappedId = `cap-${Date.now()}`
|
|
419
466
|
return {
|
|
420
|
-
id:
|
|
467
|
+
id: cappedId,
|
|
468
|
+
alias: cappedId,
|
|
421
469
|
agent: args.agent,
|
|
422
470
|
state: 'reconciled',
|
|
423
471
|
status: 'ESCALATE',
|
|
@@ -432,8 +480,10 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
432
480
|
if (isNativeMode) {
|
|
433
481
|
const unavailableStatus = await ensureNativeAvailable()
|
|
434
482
|
if (unavailableStatus !== undefined) {
|
|
483
|
+
const probeId = `probe-${parentSessionID}`
|
|
435
484
|
return {
|
|
436
|
-
id:
|
|
485
|
+
id: probeId,
|
|
486
|
+
alias: probeId,
|
|
437
487
|
agent: args.agent,
|
|
438
488
|
state: 'error',
|
|
439
489
|
status: unavailableStatus,
|
|
@@ -464,6 +514,13 @@ export function createDelegateManager(options: DelegateManagerOptions): Delegate
|
|
|
464
514
|
)
|
|
465
515
|
if (terminal.state !== 'reconciled') await board.markReconciled(terminal.taskID)
|
|
466
516
|
await board.deleteSignal(terminal.alias)
|
|
517
|
+
// Full report first (same contract as the legacy engine): the md file
|
|
518
|
+
// written by finalizeDelegation carries the complete child output. The
|
|
519
|
+
// one-line resultSummary is only a fallback when no report was written.
|
|
520
|
+
const md = await readDelegationReport(outputDir, terminal)
|
|
521
|
+
if (md !== undefined) {
|
|
522
|
+
return capReportOutput(md, inlineMaxChars, reportPathFor(terminal.alias))
|
|
523
|
+
}
|
|
467
524
|
if (terminal.state === 'error' && !terminal.resultSummary) {
|
|
468
525
|
return `# Delegation Report — ${terminal.alias}\n\nERROR: ${sanitizeReceiptText(terminal.lastStatusError ?? 'failed')}\n`
|
|
469
526
|
}
|
|
@@ -640,6 +697,7 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
|
|
|
640
697
|
? {}
|
|
641
698
|
: { promptTimeoutMs: managerOptions.timeoutMs }),
|
|
642
699
|
})
|
|
700
|
+
const inlineMaxChars = managerOptions.readMaxChars ?? resolveReadMaxChars(env)
|
|
643
701
|
const managers = new Map<string, DelegateManager>()
|
|
644
702
|
const managerFor = (parentSessionID: string): DelegateManager => {
|
|
645
703
|
const existing = managers.get(parentSessionID)
|
|
@@ -652,6 +710,7 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
|
|
|
652
710
|
parentSessionID,
|
|
653
711
|
env,
|
|
654
712
|
...(stepCap === undefined ? {} : { stepCap }),
|
|
713
|
+
...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
|
|
655
714
|
nativeProbe,
|
|
656
715
|
probeClient: client,
|
|
657
716
|
})
|
|
@@ -702,7 +761,21 @@ export function createNativeDelegateTools(wiring: NativeDelegateWiring): NativeD
|
|
|
702
761
|
...(args.model === undefined ? {} : { model: args.model }),
|
|
703
762
|
taskID: childID,
|
|
704
763
|
})
|
|
705
|
-
return
|
|
764
|
+
// The launch blocked until the child finished — return the verified
|
|
765
|
+
// output inline (capped head+tail with a pointer to the full report),
|
|
766
|
+
// not just a status line. Foreground receipts carry no report file,
|
|
767
|
+
// so the marker points at pantheon_delegation_read instead.
|
|
768
|
+
const header = `status: ${receipt.status}\n${receipt.line}`
|
|
769
|
+
if (receipt.output === undefined || receipt.output.trim() === '') return header
|
|
770
|
+
const reportPath =
|
|
771
|
+
receipt.alias === receipt.id
|
|
772
|
+
? undefined
|
|
773
|
+
: join(
|
|
774
|
+
wiring.outputDir ?? DELEGATION_DEFAULTS.outputDir,
|
|
775
|
+
ctx.sessionID,
|
|
776
|
+
`${receipt.alias}.md`,
|
|
777
|
+
)
|
|
778
|
+
return `${header}\n\n${capReportOutput(receipt.output, inlineMaxChars, reportPath)}`
|
|
706
779
|
},
|
|
707
780
|
},
|
|
708
781
|
pantheon_delegation_read: {
|
|
@@ -739,6 +812,10 @@ export interface PluginNativeDelegationWiring {
|
|
|
739
812
|
agentModels: Record<string, string | undefined>
|
|
740
813
|
stepCap: StepCapTracker
|
|
741
814
|
wallClockTimeoutMs?: number
|
|
815
|
+
/** Base directory for delegation reports (default `.pantheon/delegations`). */
|
|
816
|
+
outputDir?: string
|
|
817
|
+
/** Inline cap (chars) for delegation report content (default env or 50_000). */
|
|
818
|
+
readMaxChars?: number
|
|
742
819
|
finalizeDelegation: DelegationToolset['finalizeDelegation']
|
|
743
820
|
}
|
|
744
821
|
export type PluginNativeDelegation = NativeDelegateToolset & {
|
|
@@ -751,15 +828,23 @@ export function buildPluginNativeDelegation(
|
|
|
751
828
|
...createNativeDelegateTools({
|
|
752
829
|
board: wiring.board,
|
|
753
830
|
client: wiring.client,
|
|
831
|
+
...(wiring.outputDir === undefined ? {} : { outputDir: wiring.outputDir }),
|
|
754
832
|
isRootSession: wiring.isRootSession,
|
|
755
833
|
registerChildSession: wiring.registerChildSession,
|
|
756
834
|
registerReadOnlySession: wiring.registerReadOnlySession,
|
|
757
835
|
isReadOnlyAgent: (agent) => wiring.readOnlyAgents.has(agent.toLowerCase()),
|
|
758
836
|
agentModel: (agent) => wiring.agentModels[agent.toLowerCase()],
|
|
759
837
|
stepCap: wiring.stepCap,
|
|
760
|
-
...(wiring.wallClockTimeoutMs === undefined
|
|
838
|
+
...(wiring.wallClockTimeoutMs === undefined && wiring.readMaxChars === undefined
|
|
761
839
|
? {}
|
|
762
|
-
: {
|
|
840
|
+
: {
|
|
841
|
+
managerOptions: {
|
|
842
|
+
...(wiring.wallClockTimeoutMs === undefined
|
|
843
|
+
? {}
|
|
844
|
+
: { timeoutMs: wiring.wallClockTimeoutMs }),
|
|
845
|
+
...(wiring.readMaxChars === undefined ? {} : { readMaxChars: wiring.readMaxChars }),
|
|
846
|
+
},
|
|
847
|
+
}),
|
|
763
848
|
}),
|
|
764
849
|
finalizeDelegation: wiring.finalizeDelegation,
|
|
765
850
|
}
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* - `timeout` (timeout-with-partial / timeout-empty / timeout-exhausted)
|
|
9
9
|
* - `error` — provider or infrastructure failure
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* empty-response detection and provides a structured `DelegationResult`
|
|
11
|
+
* Provides empty-response detection and a structured `DelegationResult`
|
|
13
12
|
* that replaces raw-text returns.
|
|
14
13
|
*
|
|
15
14
|
* @module delegation-classifier
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
|
|
18
17
|
// ─── Types ─────────────────────────────────────────────────────────────
|
|
19
18
|
|
|
20
|
-
/** Empty-response mode classification (
|
|
19
|
+
/** Empty-response mode classification (empty-mode1 / empty-mode2). */
|
|
21
20
|
export type EmptyMode = 'empty-mode1' | 'empty-mode2'
|
|
22
21
|
|
|
23
22
|
/** Timeout sub-type based on partial output and retry history. */
|
|
@@ -82,7 +81,7 @@ const NO_OUTPUT_RE = /_No output captured\._/i
|
|
|
82
81
|
|
|
83
82
|
/**
|
|
84
83
|
* Classify whether a response is empty, empty-with-tokens, or has content.
|
|
85
|
-
*
|
|
84
|
+
* Classifies empty-mode1 (no text/tokens) vs empty-mode2 (tokens, no text).
|
|
86
85
|
*
|
|
87
86
|
* @param text The response text (may be null/undefined).
|
|
88
87
|
* @param tokens Optional token counts for mode2 detection.
|
|
@@ -157,6 +157,13 @@ export interface DelegationOptions {
|
|
|
157
157
|
/** Injectable clock/sleeper for deterministic watchdog tests. */
|
|
158
158
|
now?: () => number
|
|
159
159
|
sleep?: (ms: number) => Promise<void>
|
|
160
|
+
/**
|
|
161
|
+
* Inline cap (chars) for report content returned by pantheon_delegation_read.
|
|
162
|
+
* Oversized reports keep head+tail with an explicit `[TRUNCATED …]` marker
|
|
163
|
+
* pointing at the full report file. Default: PANTHEON_DELEGATION_READ_MAX_CHARS
|
|
164
|
+
* or 50_000.
|
|
165
|
+
*/
|
|
166
|
+
readMaxChars?: number
|
|
160
167
|
/**
|
|
161
168
|
* Kill-switch: when false, all delegation tools (delegate/read/list) throw
|
|
162
169
|
* immediately. Mirrors `PANTHEON_DELEGATION=off` from the native manager.
|
|
@@ -276,6 +283,62 @@ function summarizeOutput(output: string): string {
|
|
|
276
283
|
return firstLine.length > 120 ? `${firstLine.slice(0, 120)}…` : firstLine
|
|
277
284
|
}
|
|
278
285
|
|
|
286
|
+
// ─── Report inline cap ─────────────────────────────────────────────────
|
|
287
|
+
//
|
|
288
|
+
// Delegation reports are returned INLINE to the orchestrator (tool result).
|
|
289
|
+
// A very large child output must be capped BEFORE it floods the parent
|
|
290
|
+
// context — but a hard mid-content cut loses the report header AND the
|
|
291
|
+
// conclusion. The cap keeps head + tail with an explicit marker pointing at
|
|
292
|
+
// the full report (the durable `.pantheon/delegations/<parent>/<alias>.md`
|
|
293
|
+
// file or the read tool), mirroring how comparable agent hosts surface
|
|
294
|
+
// oversized tool output.
|
|
295
|
+
|
|
296
|
+
/** Default inline cap for delegation report content (chars). */
|
|
297
|
+
export const DELEGATION_READ_MAX_CHARS = 50_000
|
|
298
|
+
/** Lower bound for PANTHEON_DELEGATION_READ_MAX_CHARS. */
|
|
299
|
+
export const DELEGATION_READ_MAX_CHARS_MIN = 1_000
|
|
300
|
+
|
|
301
|
+
/** Resolve the inline report cap from the environment (default 50k, min 1k). */
|
|
302
|
+
export function resolveReadMaxChars(env: Record<string, string | undefined> = process.env): number {
|
|
303
|
+
const raw = env.PANTHEON_DELEGATION_READ_MAX_CHARS?.trim()
|
|
304
|
+
if (raw === undefined || raw === '') return DELEGATION_READ_MAX_CHARS
|
|
305
|
+
const value = Number(raw)
|
|
306
|
+
return Number.isSafeInteger(value) && value >= DELEGATION_READ_MAX_CHARS_MIN
|
|
307
|
+
? value
|
|
308
|
+
: DELEGATION_READ_MAX_CHARS
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Cap report markdown for inline return: ≤ `maxChars` passes through
|
|
313
|
+
* untouched; anything larger keeps the head (60% of the budget) + tail (25%)
|
|
314
|
+
* with an explicit `[TRUNCATED …]` marker between them. `reportPath` names
|
|
315
|
+
* the durable full-report file in the marker; without it the marker points
|
|
316
|
+
* at `pantheon_delegation_read`. Pure — no I/O.
|
|
317
|
+
*/
|
|
318
|
+
export function capReportOutput(md: string, maxChars: number, reportPath?: string): string {
|
|
319
|
+
if (md.length <= maxChars) return md
|
|
320
|
+
if (maxChars < 200) {
|
|
321
|
+
// Budget too small for a head+tail split — keep what fits after the marker.
|
|
322
|
+
const marker = capReportMarker(md.length, 0, reportPath)
|
|
323
|
+
return `${md.slice(0, Math.max(0, maxChars - marker.length - 1))}\n${marker}`
|
|
324
|
+
}
|
|
325
|
+
const headLen = Math.floor(maxChars * 0.6)
|
|
326
|
+
const tailLen = Math.floor(maxChars * 0.25)
|
|
327
|
+
return `${md.slice(0, headLen)}\n${capReportMarker(md.length, headLen + tailLen, reportPath)}\n${md.slice(md.length - tailLen)}`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function capReportMarker(
|
|
331
|
+
totalChars: number,
|
|
332
|
+
keptChars: number,
|
|
333
|
+
reportPath: string | undefined,
|
|
334
|
+
): string {
|
|
335
|
+
const where =
|
|
336
|
+
reportPath !== undefined
|
|
337
|
+
? `full report: ${reportPath}`
|
|
338
|
+
: 'read the full report with pantheon_delegation_read'
|
|
339
|
+
return `[TRUNCATED: ${totalChars - keptChars} of ${totalChars} chars hidden — ${where}]`
|
|
340
|
+
}
|
|
341
|
+
|
|
279
342
|
// ─── Report persistence ────────────────────────────────────────────────
|
|
280
343
|
|
|
281
344
|
/**
|
|
@@ -387,13 +450,12 @@ export async function finalizeDelegation(
|
|
|
387
450
|
// report above still captures this finalize; the board state stands.
|
|
388
451
|
}
|
|
389
452
|
|
|
390
|
-
// P1-1: Delete the
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// double-processing. deleteSignal on finalize is sufficient.
|
|
453
|
+
// P1-1: Delete the wake signal file to prevent leaks. The board writes
|
|
454
|
+
// .signal.json on every terminal transition, but the legacy path never
|
|
455
|
+
// called deleteSignal — only the native delegate-manager did.
|
|
456
|
+
// finalizeDelegation already handles the terminal transition, so
|
|
457
|
+
// consuming signals would be redundant; deleteSignal on finalize is
|
|
458
|
+
// sufficient.
|
|
397
459
|
await deps.board.deleteSignal(job.alias)
|
|
398
460
|
|
|
399
461
|
// P1-4: Prune old completed jobs and enforce the hard entry cap. The
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* @module delegation
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
import { join } from 'node:path'
|
|
38
39
|
import { z } from 'zod'
|
|
39
40
|
|
|
40
41
|
import type { BackgroundJobBoard, BackgroundJobRecord } from './background-job-board.ts'
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
readOnlyRegistry,
|
|
50
51
|
} from './delegation-enforce.ts'
|
|
51
52
|
import {
|
|
53
|
+
capReportOutput,
|
|
52
54
|
DELEGATION_DEFAULTS,
|
|
53
55
|
type DelegationClient,
|
|
54
56
|
type DelegationDeps,
|
|
@@ -57,6 +59,7 @@ import {
|
|
|
57
59
|
type FinalizeInput,
|
|
58
60
|
finalizeDelegation as finalizeDelegationReport,
|
|
59
61
|
readDelegationReport,
|
|
62
|
+
resolveReadMaxChars,
|
|
60
63
|
} from './delegation-finalize.ts'
|
|
61
64
|
import { finalizeIdleChildrenWithoutMd } from './delegation-notify.ts'
|
|
62
65
|
import { createPantheonLogger } from './logger.ts'
|
|
@@ -814,6 +817,10 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
814
817
|
|
|
815
818
|
const knownChildren = new Set<string>()
|
|
816
819
|
const timers = new Map<string, NodeJS.Timeout>()
|
|
820
|
+
// Inline cap for report content returned by pantheon_delegation_read —
|
|
821
|
+
// oversized reports keep head+tail with an explicit marker pointing at the
|
|
822
|
+
// full report file (never a silent mid-content cut).
|
|
823
|
+
const inlineMaxChars = options.readMaxChars ?? resolveReadMaxChars()
|
|
817
824
|
// Fase B3: per-parent-session delegation budget tracking, scoped to this
|
|
818
825
|
// factory/process instance. Keyed by `${parentSessionID}:${parentAgent}->${targetAgent}`;
|
|
819
826
|
// it is not restart-persistent because the factory has no reliable store.
|
|
@@ -1276,6 +1283,11 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
1276
1283
|
if (md === undefined) {
|
|
1277
1284
|
return `Delegation [${terminal.alias}] reached state ${terminal.state} but no report file was found.`
|
|
1278
1285
|
}
|
|
1286
|
+
// Classify on the FULL report (markers may sit at either end), but
|
|
1287
|
+
// return the inline-capped markdown so a huge child output cannot
|
|
1288
|
+
// flood the parent context — the marker points at the full file.
|
|
1289
|
+
const reportPath = join(outputDir, terminal.parentSessionID, `${terminal.alias}.md`)
|
|
1290
|
+
const cappedMd = capReportOutput(md, inlineMaxChars, reportPath)
|
|
1279
1291
|
|
|
1280
1292
|
// Classify the result for stuck-agent detection
|
|
1281
1293
|
const classification = classifyStuckAgent(md)
|
|
@@ -1284,15 +1296,15 @@ export function createDelegationTools(input: CreateDelegationToolsInput): Delega
|
|
|
1284
1296
|
|
|
1285
1297
|
if (classification.status === 'success') {
|
|
1286
1298
|
await board.markReconciled(job.taskID)
|
|
1287
|
-
// Backward compatible: success reports returned as-is
|
|
1288
|
-
if (!hasActivity) return
|
|
1289
|
-
return `${
|
|
1299
|
+
// Backward compatible: success reports returned as-is (capped)
|
|
1300
|
+
if (!hasActivity) return cappedMd
|
|
1301
|
+
return `${cappedMd.replace(/\n+$/, '')}${activitySuffix}`
|
|
1290
1302
|
}
|
|
1291
1303
|
|
|
1292
1304
|
// Non-success: build structured DelegationResult and format
|
|
1293
1305
|
const delegationResult: DelegationResult = {
|
|
1294
1306
|
status: classification.status === 'empty' ? 'empty' : classification.status,
|
|
1295
|
-
content:
|
|
1307
|
+
content: cappedMd,
|
|
1296
1308
|
retryCount: 0,
|
|
1297
1309
|
partialResult: classification.partialResult,
|
|
1298
1310
|
recommendation: classification.recommendation,
|
|
@@ -104,26 +104,6 @@ export interface RoutingRetryPolicyOptions {
|
|
|
104
104
|
logger?: { warn?: (msg: string) => void }
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* R1: per-error-type retry policy from routing.yml `retry_policy`
|
|
109
|
-
* (auth/rate_limit/timeout/other → max retries). Fail-open → null.
|
|
110
|
-
*/
|
|
111
|
-
export declare function loadRoutingRetryPolicy(
|
|
112
|
-
options?: RoutingRetryPolicyOptions,
|
|
113
|
-
): Record<string, number> | null
|
|
114
|
-
|
|
115
|
-
export interface RoutingCooldownResult {
|
|
116
|
-
allowed_fails: number
|
|
117
|
-
cooldown_time_seconds: number
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* R1: provider cooldown config from routing.yml `cooldown`. Fail-open → null.
|
|
122
|
-
*/
|
|
123
|
-
export declare function loadRoutingCooldown(
|
|
124
|
-
options?: RoutingRetryPolicyOptions,
|
|
125
|
-
): RoutingCooldownResult | null
|
|
126
|
-
|
|
127
107
|
/**
|
|
128
108
|
* R4: per-agent step caps from routing.yml `agents.<name>.max_steps`.
|
|
129
109
|
* Fail-open → {} (no agent capped).
|