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.
Files changed (82) hide show
  1. package/AGENTS.md +14 -63
  2. package/CHANGELOG.md +320 -0
  3. package/README.md +14 -27
  4. package/README.pt-BR.md +5 -6
  5. package/commands/pantheon.md +1 -35
  6. package/package.json +1 -2
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +2 -5
  9. package/scripts/doctor.mjs +32 -3
  10. package/scripts/install/plugin.mjs +9 -12
  11. package/scripts/mcp_persistence_server.py +1 -1
  12. package/scripts/sync-tui.mjs +8 -11
  13. package/src/agents/apollo.md +1 -0
  14. package/src/agents/gaia.md +1 -0
  15. package/src/agents/zeus.md +9 -52
  16. package/src/instructions/memory-protocol.instructions.md +6 -3
  17. package/src/instructions/zeus-anti-stall.instructions.md +2 -4
  18. package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
  19. package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
  20. package/src/mcp/mcp_persistence_server.py +1 -1
  21. package/src/mcp/toon_codec.py +2 -4
  22. package/src/pantheon/background-job-board.ts +4 -4
  23. package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
  24. package/src/pantheon/cost-command.ts +1 -2
  25. package/src/pantheon/delegation-enforce.ts +59 -14
  26. package/src/pantheon/goal-loop.ts +1 -1
  27. package/src/pantheon/goal-store.ts +1 -1
  28. package/src/pantheon/idle-continuation.ts +4 -6
  29. package/src/pantheon/model-command.ts +1 -2
  30. package/src/pantheon/native-task-status.ts +2 -2
  31. package/src/pantheon/presets.d.mts +0 -20
  32. package/src/pantheon/presets.mjs +0 -64
  33. package/src/pantheon/session-guard.ts +2 -2
  34. package/src/pantheon/session-id.ts +30 -0
  35. package/src/pantheon/task-result-guard.ts +2 -3
  36. package/src/pantheon/todo-enforcer.ts +3 -3
  37. package/src/pantheon/tool-context.ts +23 -0
  38. package/src/pantheon/v2-bridge.ts +1 -4
  39. package/src/plugin-v2.ts +8 -21
  40. package/src/plugin.ts +61 -245
  41. package/src/plugins/pantheon-hooks.ts +1 -1
  42. package/src/plugins/tui/dist/tui.d.ts +229 -77
  43. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  44. package/src/plugins/tui/dist/tui.js +509 -259
  45. package/src/plugins/tui/package-lock.json +2 -2
  46. package/src/plugins/tui/package.json +2 -2
  47. package/src/plugins/tui/src/index.tsx +753 -279
  48. package/src/routing.yml +0 -16
  49. package/src/skills/context-compression/SKILL.md +1 -1
  50. package/scripts/generate-preset-docs.mjs +0 -300
  51. package/scripts/generate-prompts.sh +0 -222
  52. package/scripts/hash_verify.py +0 -192
  53. package/scripts/install/component.mjs +0 -24
  54. package/scripts/install/migrate-config.mjs +0 -302
  55. package/scripts/test-context-compression.sh +0 -169
  56. package/src/pantheon/auto-wake.ts +0 -85
  57. package/src/pantheon/chat-reminders.ts +0 -9
  58. package/src/pantheon/comment-checker.ts +0 -48
  59. package/src/pantheon/compaction-assert.ts +0 -160
  60. package/src/pantheon/compaction-handoff.ts +0 -304
  61. package/src/pantheon/cost-tracker.ts +0 -108
  62. package/src/pantheon/delegate-manager.ts +0 -766
  63. package/src/pantheon/delegation-classifier.ts +0 -307
  64. package/src/pantheon/delegation-finalize.ts +0 -406
  65. package/src/pantheon/delegation-notify.ts +0 -212
  66. package/src/pantheon/delegation-toast.ts +0 -73
  67. package/src/pantheon/delegation.ts +0 -1337
  68. package/src/pantheon/dispatch-guard.ts +0 -109
  69. package/src/pantheon/native-probe.ts +0 -234
  70. package/src/pantheon/permission-eval.ts +0 -284
  71. package/src/pantheon/preemptive-compact.ts +0 -76
  72. package/src/pantheon/provider-capabilities.ts +0 -221
  73. package/src/pantheon/provider-stream-tracker.ts +0 -182
  74. package/src/pantheon/retry-policy.ts +0 -212
  75. package/src/pantheon/step-cap.ts +0 -110
  76. package/src/pantheon/token-opt.ts +0 -689
  77. package/src/pantheon/toon-codec.ts +0 -481
  78. package/src/pantheon/v2-events.ts +0 -148
  79. package/src/pantheon/v2-hooks.ts +0 -287
  80. package/src/pantheon/v2-tool-definitions.ts +0 -331
  81. package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
  82. package/src/plugins/tui/dist/tui.tsx +0 -2777
@@ -1,689 +0,0 @@
1
- /**
2
- * WS3 Token-Opt (PR #94) — measurable net token reduction, quality floor.
3
- *
4
- * Five levers, each with before/after metering and a NET delta (gross
5
- * saving minus retrieval/measurement overhead). A lever with net <= 0 is
6
- * DISABLED by flag (never removed) via `applyGate()`.
7
- *
8
- * 1. C9 pre-retrieval Mode-1 — top-k (default 3) per category BEFORE
9
- * task()/delegate, relevance cutoff (default 0.3) per category; only
10
- * auditable chunks (id + score + text) are injected; injection cost
11
- * is debited in `injectedTokens`.
12
- * 2. 4 meta-tools ceiling + lazy — `memory_search`, `kv_search`,
13
- * `code_query` (codemap) and `pantheon://resources` are always on
14
- * (real repo names verified); everything else resolves on demand via
15
- * pantheon-code-mode `execute_code_script` (search-first discovery).
16
- * 3. Response filtering + truncation — keep only used MCP fields;
17
- * per-turn cap preserving summary (first) + tail (last).
18
- * 4. TOON encoding — see `toon-codec.ts` (board signals, checkpoints,
19
- * KV payloads; per-class savings ~11% content-dominated board records
20
- * up to ~49% large tabular checkpoints — see
21
- * `docs/ws3-token-opt-measurements.md` — JSON fallback).
22
- * 5. Schema compression — slim MCP tool descriptions for familiar
23
- * tools, full detail on demand.
24
- *
25
- * Constraints (PLAN GATE 1): tokens-only accounting (NO monetary layer —
26
- * this extends WS0 `pantheon_cost`/cost-tracker WITHOUT touching the
27
- * legacy USD ledger); deterministic metering (NO LLM calls, NO external
28
- * gateway, NO generative embedding). `estimateTokens` is a fixed
29
- * chars/4 ceiling so before/after numbers are directly comparable.
30
- *
31
- * Kill-switch: `PANTHEON_TOKEN_OPT=off` (see `isTokenOptEnabled`).
32
- *
33
- * @module token-opt
34
- */
35
-
36
- import type { ToonValue } from './toon-codec.ts'
37
-
38
- export {
39
- type HostToolCeilingSource,
40
- probeToolCeiling,
41
- type ToolCeilingResult,
42
- type ToolCeilingSnapshot,
43
- } from './tool-ceiling.ts'
44
-
45
- // ─── Deterministic token estimator (WS0 extension, tokens-only) ──────────
46
-
47
- /** Nominal chars-per-token ratio (fixed so measurements are comparable). */
48
- export const CHARS_PER_TOKEN = 4
49
-
50
- function charsToTokens(chars: number): number {
51
- if (chars <= 0) return 0
52
- return Math.ceil(chars / CHARS_PER_TOKEN)
53
- }
54
-
55
- /**
56
- * Deterministic token estimate for a text (no LLM, no I/O).
57
- * Pure function of length: `ceil(len / 4)`, `0` for empty.
58
- */
59
- export function estimateTokens(text: string): number {
60
- return charsToTokens(text.length)
61
- }
62
-
63
- // ─── Lever names + feature flags ─────────────────────────────────────────
64
-
65
- /** The five WS3 levers. */
66
- export type LeverName = 'c9' | 'tools-ceiling' | 'filter-truncate' | 'toon' | 'schema'
67
-
68
- /** Per-lever measurement with NET delta (gross minus overhead). */
69
- export interface LeverMeasurement {
70
- name: LeverName
71
- before: number
72
- after: number
73
- overhead: number
74
- gross: number
75
- net: number
76
- enabled: boolean
77
- /** Itemized overhead (present when measured via `measureLeverFull`). */
78
- overheadBreakdown?: OverheadBreakdown
79
- }
80
-
81
- /** Default flags: every lever enabled until measurement gates it. */
82
- export const DEFAULT_FLAGS: Record<LeverName, boolean> = {
83
- c9: true,
84
- 'tools-ceiling': true,
85
- 'filter-truncate': true,
86
- toon: true,
87
- schema: true,
88
- }
89
-
90
- /** Measure one lever in tokens from char counts (deterministic).
91
- * `enabled` is `net > 0` — the caller persists the verdict via flags.
92
- * `overheadChars` here is the TOTAL overhead (retrieval + discovery +
93
- * detail-on-demand + markers); use `measureLeverFull` with an explicit
94
- * `OverheadBreakdown` so no component is silently omitted.
95
- */
96
- export function measureLever(
97
- name: LeverName,
98
- beforeChars: number,
99
- afterChars: number,
100
- overheadChars: number,
101
- ): LeverMeasurement {
102
- return measureLeverFull(name, beforeChars, afterChars, {
103
- ...EMPTY_OVERHEAD,
104
- retrievalChars: overheadChars,
105
- })
106
- }
107
-
108
- /** Overhead components debited against a lever's gross saving (chars). */
109
- export interface OverheadBreakdown {
110
- /** Pre-retrieval injection cost (C9 context chars actually injected). */
111
- retrievalChars: number
112
- /** Search-first discovery probes for lazy tools (`discoveryOverheadChars`). */
113
- discoveryChars: number
114
- /** Full detail fetched on demand (`fullDetailChars`). */
115
- detailChars: number
116
- /** Truncation markers kept in the output (`TruncateResult.markerChars`). */
117
- markerChars: number
118
- }
119
-
120
- /** Zero overhead (a lever that replaces bytes in place, e.g. TOON). */
121
- export const EMPTY_OVERHEAD: OverheadBreakdown = {
122
- retrievalChars: 0,
123
- discoveryChars: 0,
124
- detailChars: 0,
125
- markerChars: 0,
126
- }
127
-
128
- /** Sum of all overhead components (chars). */
129
- export function totalOverheadChars(breakdown: OverheadBreakdown): number {
130
- return (
131
- breakdown.retrievalChars +
132
- breakdown.discoveryChars +
133
- breakdown.detailChars +
134
- breakdown.markerChars
135
- )
136
- }
137
-
138
- /**
139
- * Measure one lever with an explicit TOTAL overhead breakdown.
140
- * Net = gross − (retrieval + discovery + detail + markers); net <= 0
141
- * disables the lever via `applyGate` (flag kept, never removed).
142
- */
143
- export function measureLeverFull(
144
- name: LeverName,
145
- beforeChars: number,
146
- afterChars: number,
147
- breakdown: OverheadBreakdown,
148
- ): LeverMeasurement {
149
- const before = charsToTokens(beforeChars)
150
- const after = charsToTokens(afterChars)
151
- const overheadChars = totalOverheadChars(breakdown)
152
- const overhead = charsToTokens(overheadChars)
153
- const gross = before - after
154
- const net = gross - overhead
155
- return {
156
- name,
157
- before,
158
- after,
159
- overhead,
160
- gross,
161
- net,
162
- enabled: net > 0,
163
- overheadBreakdown: breakdown,
164
- }
165
- }
166
-
167
- /**
168
- * Gate flags by measurement: a lever with net <= 0 is DISABLED (set to
169
- * false) but KEPT (never removed) so it can be re-enabled later.
170
- */
171
- export function applyGate(
172
- flags: Record<LeverName, boolean>,
173
- measurements: LeverMeasurement[],
174
- ): Record<LeverName, boolean> {
175
- const gated: Record<LeverName, boolean> = { ...flags }
176
- for (const m of measurements) {
177
- if (m.net <= 0) gated[m.name] = false
178
- }
179
- return gated
180
- }
181
-
182
- /** Kill-switch: `PANTHEON_TOKEN_OPT=off` disables the whole WS3 layer. */
183
- export function isTokenOptEnabled(env: Readonly<Record<string, string | undefined>> = {}): boolean {
184
- return env.PANTHEON_TOKEN_OPT !== 'off'
185
- }
186
-
187
- // ─── Lever 1: C9 pre-retrieval Mode-1 ────────────────────────────────────
188
-
189
- /** Default top-k per category (before task()/delegate). */
190
- export const C9_DEFAULT_TOP_K = 3
191
-
192
- /** Default relevance cutoff per category (chunks below are dropped). */
193
- export const C9_DEFAULT_CUTOFF = 0.3
194
-
195
- /** One scored retrieval candidate (auditable: id + category + score). */
196
- export interface C9Chunk {
197
- id: string
198
- category: string
199
- score: number
200
- text: string
201
- }
202
-
203
- /** Immutable input batch for the safe C9 context-preparation contract. */
204
- export interface C9CandidateBatch {
205
- query: string
206
- candidates: readonly C9Chunk[]
207
- }
208
-
209
- /** Per-category override of top-k / cutoff. */
210
- export interface C9CategoryConfig {
211
- topK?: number
212
- cutoff?: number
213
- }
214
-
215
- /** Optional query used only to activate the explicit low-score fallback. */
216
- export interface C9FilterOptions {
217
- perCategory?: Readonly<Record<string, C9CategoryConfig>>
218
- query?: string
219
- }
220
-
221
- /** Result of C9 filtering with debited injection cost. */
222
- export interface C9FilterResult {
223
- selected: C9Chunk[]
224
- dropped: C9Chunk[]
225
- injectedChars: number
226
- injectedTokens: number
227
- /**
228
- * Quality-floor signal (never silent on empty):
229
- * - `ok` — at least one chunk selected;
230
- * - `empty-input` — no chunks retrieved at all;
231
- * - `all-dropped` — chunks existed but none survived cutoff/top-k.
232
- */
233
- signal: 'ok' | 'empty-input' | 'all-dropped'
234
- /** Human-readable alert for the non-`ok` signals (null when `ok`). */
235
- notice: string | null
236
- /**
237
- * Recall over cutoff-eligible chunks: selected-eligible / eligible.
238
- * Top-k overflow lowers recall honestly (e.g. 3 kept of 5 eligible = 0.6).
239
- * 1 when nothing was eligible (vacuous — read with `signal`).
240
- * Precision is N/A by construction: the cutoff IS the relevance proxy,
241
- * so every selected chunk is eligible by definition.
242
- */
243
- recall: number
244
- }
245
-
246
- /**
247
- * Filter scored chunks per category: drop below cutoff, keep top-k by
248
- * score desc. Scores must come from retrieval; text is never used as a proxy.
249
- * The injected context cost is debited (`injectedTokens`).
250
- */
251
- export function c9Filter(
252
- chunks: readonly C9Chunk[],
253
- perCategoryOrOptions?: Readonly<Record<string, C9CategoryConfig>> | C9FilterOptions | string,
254
- _query?: string,
255
- ): C9FilterResult {
256
- const isOptions = isC9FilterOptions(perCategoryOrOptions)
257
- const perCategory =
258
- typeof perCategoryOrOptions === 'string'
259
- ? undefined
260
- : isOptions
261
- ? perCategoryOrOptions.perCategory
262
- : perCategoryOrOptions
263
- const byCategory = new Map<string, C9Chunk[]>()
264
- for (const chunk of chunks) {
265
- const group = byCategory.get(chunk.category)
266
- if (group !== undefined) group.push(chunk)
267
- else byCategory.set(chunk.category, [chunk])
268
- }
269
- const selected: C9Chunk[] = []
270
- const dropped: C9Chunk[] = []
271
- let eligibleTotal = 0
272
- let eligibleSelected = 0
273
- for (const [category, group] of byCategory) {
274
- const config = perCategory?.[category]
275
- const topK = config?.topK ?? C9_DEFAULT_TOP_K
276
- const cutoff = config?.cutoff ?? C9_DEFAULT_CUTOFF
277
- const eligible = group.filter((c) => c.score >= cutoff).sort((a, b) => b.score - a.score)
278
- const kept = eligible.slice(0, topK)
279
- eligibleTotal += eligible.length
280
- eligibleSelected += kept.length
281
- selected.push(...kept)
282
- dropped.push(...group.filter((c) => c.score < cutoff))
283
- dropped.push(...eligible.slice(topK))
284
- }
285
- const signal: C9FilterResult['signal'] =
286
- chunks.length === 0 ? 'empty-input' : selected.length === 0 ? 'all-dropped' : 'ok'
287
- const notice =
288
- signal === 'empty-input'
289
- ? 'C9: empty input — no chunks retrieved; proceed without injected context'
290
- : signal === 'all-dropped'
291
- ? 'C9: all chunks dropped by cutoff/top-k; no context was injected'
292
- : null
293
-
294
- const context = buildC9Context(selected)
295
- return {
296
- selected,
297
- dropped,
298
- injectedChars: context.length,
299
- injectedTokens: estimateTokens(context),
300
- signal,
301
- notice,
302
- recall: eligibleTotal === 0 ? 1 : eligibleSelected / eligibleTotal,
303
- }
304
- }
305
-
306
- /** Configuration for the pure C9 context-preparation boundary. */
307
- export interface C9ContextOptions {
308
- env?: Readonly<Record<string, string | undefined>>
309
- perCategory?: Readonly<Record<string, C9CategoryConfig>>
310
- }
311
-
312
- /** Metrics emitted by C9; deliberately contains no candidate or context text. */
313
- export interface C9Telemetry {
314
- signal: C9FilterResult['signal'] | 'disabled' | 'empty-query'
315
- counts: {
316
- candidates: number
317
- selected: number
318
- dropped: number
319
- }
320
- recall: number
321
- injectedChars: number
322
- injectedTokens: number
323
- }
324
-
325
- /** C9 result with the kill-switch state available as a type discriminator. */
326
- export type C9ContextResult =
327
- | {
328
- enabled: true
329
- context: string
330
- telemetry: C9Telemetry
331
- }
332
- | {
333
- enabled: false
334
- context: ''
335
- telemetry: C9Telemetry
336
- }
337
-
338
- function telemetry(
339
- signal: C9Telemetry['signal'],
340
- candidates: number,
341
- selected: number,
342
- dropped: number,
343
- recall: number,
344
- context = '',
345
- ): C9Telemetry {
346
- return {
347
- signal,
348
- counts: { candidates, selected, dropped },
349
- recall,
350
- injectedChars: context.length,
351
- injectedTokens: estimateTokens(context),
352
- }
353
- }
354
-
355
- /**
356
- * Prepare auditable context without performing retrieval or side effects.
357
- *
358
- * The caller supplies the query and candidates already retrieved. The
359
- * kill-switch is read only from the supplied environment object, so this
360
- * boundary remains deterministic and testable. C9 always receives the batch
361
- * query explicitly; no option can replace it with an implicit query.
362
- */
363
- export function prepareC9Context(
364
- batch: C9CandidateBatch,
365
- options: C9ContextOptions = {},
366
- ): C9ContextResult {
367
- const candidateCount = batch.candidates.length
368
- if (!isTokenOptEnabled(options.env)) {
369
- return {
370
- enabled: false,
371
- context: '',
372
- telemetry: telemetry('disabled', candidateCount, 0, 0, 0),
373
- }
374
- }
375
-
376
- const filtered = c9Filter(batch.candidates, options.perCategory, batch.query)
377
- if (batch.query.trim().length === 0) {
378
- return {
379
- enabled: true,
380
- context: '',
381
- telemetry: telemetry(
382
- 'empty-query',
383
- candidateCount,
384
- 0,
385
- candidateCount,
386
- candidateCount === 0 ? filtered.recall : 0,
387
- ),
388
- }
389
- }
390
-
391
- const context = buildC9Context(filtered.selected)
392
- const signal = filtered.signal
393
- const selectedCount = context.length === 0 ? 0 : filtered.selected.length
394
- const droppedCount = context.length === 0 ? candidateCount : filtered.dropped.length
395
- const recall = context.length === 0 && filtered.signal === 'ok' ? 0 : filtered.recall
396
- return {
397
- enabled: true,
398
- context,
399
- telemetry: telemetry(signal, candidateCount, selectedCount, droppedCount, recall, context),
400
- }
401
- }
402
-
403
- function isC9FilterOptions(
404
- value: Readonly<Record<string, C9CategoryConfig>> | C9FilterOptions | string | undefined,
405
- ): value is C9FilterOptions {
406
- return (
407
- value !== undefined &&
408
- typeof value !== 'string' &&
409
- ('perCategory' in value || ('query' in value && typeof value.query === 'string'))
410
- )
411
- }
412
-
413
- /** Build the injected context: only auditable chunks (id + score + text). */
414
- export function buildC9Context(selected: readonly C9Chunk[]): string {
415
- return selected.map((c) => `[${c.category}:${c.id}|${c.score}] ${c.text}`).join('\n')
416
- }
417
-
418
- // ─── Lever 2: 4 meta-tools ceiling + lazy ────────────────────────────────
419
-
420
- /**
421
- * Always-on meta-tools (real repo names, verified):
422
- * - `memory_search` (memory server: hybrid FTS5 + vector),
423
- * - `kv_search` (persistence server: FTS5),
424
- * - `code_query` (codemap: FTS5 code entities),
425
- * - `pantheon://resources` (resources server URI scheme).
426
- */
427
- export const ALWAYS_ON_TOOLS: readonly string[] = [
428
- 'memory_search',
429
- 'kv_search',
430
- 'code_query',
431
- 'pantheon://resources',
432
- ]
433
-
434
- /** Lazy route for non-ceiling tools (on demand, search-first). */
435
- export interface LazyRoute {
436
- via: 'execute_code_script'
437
- tool: string
438
- }
439
-
440
- /** Classify a tool: ceiling (always on) or lazy (via pantheon-code-mode). */
441
- export function classifyTool(name: string): 'always-on' | 'lazy' {
442
- return (ALWAYS_ON_TOOLS as readonly string[]).includes(name) ? 'always-on' : 'lazy'
443
- }
444
-
445
- /**
446
- * Resolve a non-ceiling tool to its on-demand code-mode route.
447
- * Verifies the tool EXISTS first (known description or ceiling entry) —
448
- * unknown names throw instead of being silently misrouted.
449
- */
450
- export function lazyRoute(name: string): LazyRoute {
451
- if (!isKnownTool(name)) {
452
- throw new Error(
453
- `lazyRoute: unknown tool ${JSON.stringify(name)} — register it in descriptions/ALWAYS_ON_TOOLS before routing`,
454
- )
455
- }
456
- return { via: 'execute_code_script', tool: name }
457
- }
458
-
459
- /** Whether a tool name is known (has a description or is always-on). */
460
- export function isKnownTool(name: string): boolean {
461
- return name in FULL_DESCRIPTIONS || (ALWAYS_ON_TOOLS as readonly string[]).includes(name)
462
- }
463
-
464
- // ─── Lever 3: response filtering + truncation ────────────────────────────
465
-
466
- /**
467
- * Keep only the MCP fields the caller actually uses (drops embeddings,
468
- * result_info and other unused payload weight).
469
- * `required` names fields that MUST be present — missing ones throw
470
- * instead of being silently dropped (quality floor).
471
- */
472
- export function filterFields(
473
- raw: Record<string, unknown>,
474
- allowed: readonly string[],
475
- required: readonly string[] = [],
476
- ): Record<string, unknown> {
477
- const missing = required.filter((key) => !(key in raw))
478
- if (missing.length > 0) {
479
- throw new Error(`filterFields: missing required fields: ${missing.join(', ')}`)
480
- }
481
- const out: Record<string, unknown> = {}
482
- for (const key of allowed) {
483
- if (key in raw) out[key] = raw[key]
484
- }
485
- return out
486
- }
487
-
488
- /** Result of per-turn truncation. */
489
- export interface TruncateResult {
490
- blocks: string[]
491
- truncated: boolean
492
- /** Chars of the truncation marker kept in the output (0 when untruncated — debit via `OverheadBreakdown.markerChars`). */
493
- markerChars: number
494
- }
495
-
496
- /** Fit text into a char budget, slicing with `…` when over (hard cap). */
497
- function fitBudget(text: string, budget: number): string {
498
- if (text.length <= budget) return text
499
- if (budget <= 0) return ''
500
- if (budget === 1) return '…'
501
- return `${text.slice(0, budget - 1)}…`
502
- }
503
-
504
- /**
505
- * Cap a turn's blocks to `maxChars` preserving summary (first) + tail
506
- * (last); middle blocks are replaced by a bounded marker.
507
- *
508
- * HARD CAP (margin 0): the joined output NEVER exceeds `maxChars` — head
509
- * and tail are sliced with `…` when over budget. The marker itself counts
510
- * toward the cap and its length is reported in `markerChars` for overhead
511
- * debiting. When `maxChars` is smaller than the marker, the marker alone
512
- * (sliced) is returned.
513
- */
514
- export function truncateTurn(blocks: readonly string[], maxChars: number): TruncateResult {
515
- const joined = blocks.join('\n')
516
- if (joined.length <= maxChars) return { blocks: [...blocks], truncated: false, markerChars: 0 }
517
- const head = blocks[0] ?? ''
518
- const tail = blocks.length > 1 ? (blocks[blocks.length - 1] as string) : ''
519
- if (blocks.length === 1) {
520
- return { blocks: [fitBudget(head, maxChars)], truncated: true, markerChars: 0 }
521
- }
522
- if (blocks.length === 2) {
523
- const tailOut = fitBudget(tail, maxChars)
524
- const headOut = fitBudget(head, Math.max(0, maxChars - tailOut.length - 1))
525
- return { blocks: [headOut, tailOut], truncated: true, markerChars: 0 }
526
- }
527
- const marker = `…[+${blocks.length - 2} truncated]…`
528
- const budget = maxChars - marker.length - 2
529
- if (budget < 0) {
530
- return { blocks: [fitBudget(marker, maxChars)], truncated: true, markerChars: marker.length }
531
- }
532
- const headOut = fitBudget(head, budget)
533
- const tailOut = fitBudget(tail, budget - headOut.length)
534
- return { blocks: [headOut, marker, tailOut], truncated: true, markerChars: marker.length }
535
- }
536
-
537
- // ─── Lever 5: schema compression ─────────────────────────────────────────
538
-
539
- /** Full descriptions for familiar tools (detail on demand). */
540
- const FULL_DESCRIPTIONS: Record<string, string> = {
541
- memory_search:
542
- 'memory_search: hybrid semantic search across memories. Combines vector cosine ' +
543
- 'similarity and FTS5 BM25 keyword search via Reciprocal Rank Fusion (RRF). ' +
544
- 'Optional decay_days applies a freshness half-life so recent entries rank higher.',
545
- kv_search:
546
- 'kv_search: full-text search across keys and values using FTS5. ' +
547
- 'Optionally filter by namespace. Returns matching entries with rank order.',
548
- code_query:
549
- 'code_query: search code entities via FTS5 (with LIKE fallback) and optional ' +
550
- 'type filter. Returns entity id, file path, signature and docstring.',
551
- 'pantheon://resources':
552
- 'pantheon://resources: MCP resource URI scheme (agents, skills, routing, ' +
553
- 'deepwork plans, memory-bank files, code-mode scripts). Read-only discovery.',
554
- kv_get: 'kv_get: retrieve a value by namespace and key. Returns None if not found or expired.',
555
- kv_store:
556
- 'kv_store: store a key-value pair in a namespace with optional TTL (seconds). ' +
557
- 'INSERT OR REPLACE on duplicate (namespace, key).',
558
- context_save:
559
- 'context_save: save a context checkpoint for a session/phase. Stores structured ' +
560
- 'JSON in persistence KV with auto-TTL of 4h. Also updates the "latest" pointer.',
561
- context_get:
562
- 'context_get: retrieve a context checkpoint by session slug and key. ' +
563
- 'Returns the raw content string or null if expired/not found.',
564
- memory_recall:
565
- 'memory_recall: recall a specific memory entry by its key within a namespace. ' +
566
- 'Returns the full entry including parsed metadata.',
567
- memory_list:
568
- 'memory_list: list memory entries chronologically with optional namespace ' +
569
- 'and key-prefix filters.',
570
- code_neighbors:
571
- 'code_neighbors: get neighbors of a code entity via relations graph (BFS depth 1-3).',
572
- execute_code_script:
573
- 'execute_code_script: run a .sh/.py script from .pantheon/code-mode/ with ' +
574
- 'optional args. On-demand route for non-ceiling tools.',
575
- }
576
-
577
- /** Slim descriptions for familiar tools (default; saves >= 40% chars). */
578
- const SLIM_DESCRIPTIONS: Record<string, string> = {
579
- memory_search: 'memory_search: hybrid memory search (vector + FTS5).',
580
- kv_search: 'kv_search: text search over KV keys/values.',
581
- code_query: 'code_query: FTS5 code-entity search.',
582
- 'pantheon://resources': 'pantheon://resources: read-only discovery URIs.',
583
- kv_get: 'kv_get: read one KV entry.',
584
- kv_store: 'kv_store: write one KV entry (upsert).',
585
- context_save: 'context_save: save a session checkpoint.',
586
- context_get: 'context_get: read a session checkpoint.',
587
- memory_recall: 'memory_recall: recall one memory by key.',
588
- memory_list: 'memory_list: list memories.',
589
- code_neighbors: 'code_neighbors: code graph neighbors.',
590
- execute_code_script: 'execute_code_script: on-demand lazy-tool route.',
591
- }
592
-
593
- /**
594
- * Describe a tool: slim by default, full detail on demand.
595
- * Unknown tools fall back to their own name (never throws).
596
- */
597
- export function describeTool(name: string, detail = false): string {
598
- if (detail) return FULL_DESCRIPTIONS[name] ?? SLIM_DESCRIPTIONS[name] ?? name
599
- return SLIM_DESCRIPTIONS[name] ?? FULL_DESCRIPTIONS[name] ?? name
600
- }
601
-
602
- /**
603
- * Detail-on-demand overhead for a tool (chars): fetching the full
604
- * description costs `fullDetailChars(name)` — debit via
605
- * `OverheadBreakdown.detailChars`. 0 for unknown tools (nothing to fetch).
606
- */
607
- export function fullDetailChars(name: string): number {
608
- return FULL_DESCRIPTIONS[name]?.length ?? 0
609
- }
610
-
611
- /**
612
- * Nominal search-first discovery probe for one lazy tool
613
- * (`code_query <tool>` + resource read). `discoveryOverheadChars` is its
614
- * char length — debit once per lazily-resolved tool via
615
- * `OverheadBreakdown.discoveryChars`.
616
- */
617
- export function discoveryProbe(tool: string): string {
618
- return `code_query ${tool} + read pantheon://resources`
619
- }
620
-
621
- /** Discovery overhead for one lazily-resolved tool (chars). */
622
- export function discoveryOverheadChars(tool: string): number {
623
- return discoveryProbe(tool).length
624
- }
625
-
626
- // ─── Tokens-only metering (WS0 extension, NO monetary layer) ─────────────
627
-
628
- /** Token totals for one phase (input/output only — never USD). */
629
- export interface PhaseTokens {
630
- input: number
631
- output: number
632
- }
633
-
634
- /**
635
- * Deterministic per-phase/per-agent token ledger (no LLM, no I/O).
636
- * Extends WS0 `pantheon_cost`/cost-tracker with phase accounting while
637
- * keeping the tokens-only mandate (no `costUsd` field anywhere).
638
- */
639
- export class TokenMeter {
640
- private readonly chars = new Map<string, { input: number; output: number }>()
641
-
642
- /** Record one delegation's char counts under a phase + agent. */
643
- record(phase: string, agent: string, inputChars: number, outputChars: number): void {
644
- const key = `${phase}\0${agent}`
645
- const entry = this.chars.get(key) ?? { input: 0, output: 0 }
646
- entry.input += Math.max(0, inputChars)
647
- entry.output += Math.max(0, outputChars)
648
- this.chars.set(key, entry)
649
- }
650
-
651
- /** Totals for one phase across agents (tokens, converted once). */
652
- phaseTotals(phase: string): PhaseTokens {
653
- const chars = this.phaseChars(phase)
654
- return { input: charsToTokens(chars.inputChars), output: charsToTokens(chars.outputChars) }
655
- }
656
-
657
- /** Totals across all phases. */
658
- totals(): PhaseTokens {
659
- const chars = this.charsTotals()
660
- return { input: charsToTokens(chars.inputChars), output: charsToTokens(chars.outputChars) }
661
- }
662
-
663
- /** Raw char counts for one phase (honest basis behind the token totals). */
664
- phaseChars(phase: string): { inputChars: number; outputChars: number } {
665
- let inputChars = 0
666
- let outputChars = 0
667
- for (const [key, entry] of this.chars) {
668
- if (key.startsWith(`${phase}\0`)) {
669
- inputChars += entry.input
670
- outputChars += entry.output
671
- }
672
- }
673
- return { inputChars, outputChars }
674
- }
675
-
676
- /** Raw char counts across all phases. */
677
- charsTotals(): { inputChars: number; outputChars: number } {
678
- let inputChars = 0
679
- let outputChars = 0
680
- for (const entry of this.chars.values()) {
681
- inputChars += entry.input
682
- outputChars += entry.output
683
- }
684
- return { inputChars, outputChars }
685
- }
686
- }
687
-
688
- /** Re-export for callers that thread TOON payloads through metering. */
689
- export type { ToonValue }