@iowarp/clio-coder 0.3.4 → 0.3.6
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 +37 -2
- package/CONTRIBUTING.md +6 -6
- package/README.md +2 -2
- package/dist/{acp-S5R4RR5B.js → acp-2BEHC4DL.js} +4 -4
- package/dist/{agents-P6DMMVZY.js → agents-LNNFTM53.js} +13 -11
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-2XCZLPKS.js → auth-KXXFI2VS.js} +6 -6
- package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
- package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
- package/dist/chunk-3BPUFZDL.js +37 -0
- package/dist/{chunk-WPQLXFOZ.js → chunk-43AOLP7E.js} +2 -2
- package/dist/{chunk-N4CZJQRK.js → chunk-5JGRAMKL.js} +4 -4
- package/dist/{chunk-BRXQQJFP.js → chunk-6US73PDB.js} +568 -47
- package/dist/{chunk-K6WL7QZT.js → chunk-6XXKFVSN.js} +2 -2
- package/dist/{chunk-QQK64KLB.js → chunk-CJUB2JJ2.js} +138 -20
- package/dist/{chunk-HV5X7OR2.js → chunk-CKXWIANG.js} +12 -12
- package/dist/{chunk-UZHIZC5S.js → chunk-CYQKWTG3.js} +61 -76
- package/dist/{chunk-QWU7ZBO7.js → chunk-DJVECN66.js} +204 -45
- package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
- package/dist/{chunk-7RXG6QRZ.js → chunk-EKY57CSP.js} +2 -75
- package/dist/{chunk-EDRHSCIE.js → chunk-EYPA3EGJ.js} +10 -2
- package/dist/{chunk-TTNYS3EA.js → chunk-G7MUEIGA.js} +1 -1
- package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
- package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
- package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
- package/dist/chunk-HWUFFB6L.js +83 -0
- package/dist/{chunk-35MKKU5R.js → chunk-K7T3E2SR.js} +15 -8
- package/dist/{chunk-VAWWTKDP.js → chunk-KHSFENX2.js} +2 -2
- package/dist/chunk-LCGCVYZ4.js +57 -0
- package/dist/{chunk-X6COSD2O.js → chunk-LYF7OHWH.js} +41 -14
- package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
- package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
- package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
- package/dist/{chunk-3JLKSKD7.js → chunk-OH3TOQTB.js} +5 -1
- package/dist/{chunk-MEQ45TQ4.js → chunk-PBTHKCPN.js} +18 -4
- package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
- package/dist/{chunk-QQL5RT5M.js → chunk-QM3F2GKX.js} +94 -36
- package/dist/{chunk-A2GZF7DC.js → chunk-QNQHSOLF.js} +4 -4
- package/dist/{chunk-KRPY7NTG.js → chunk-R46L2BIR.js} +3 -3
- package/dist/{chunk-BP4OYD6A.js → chunk-RY3LY4J5.js} +20 -2
- package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
- package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
- package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
- package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
- package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
- package/dist/{chunk-HXG4IURW.js → chunk-XE2VEJHX.js} +2 -2
- package/dist/{chunk-3HZ5RWN2.js → chunk-XF5N4U5A.js} +7 -6
- package/dist/{chunk-ZYKPLLNQ.js → chunk-XXQNGV4M.js} +590 -32
- package/dist/{chunk-4JUF2NNX.js → chunk-XYDYPRZI.js} +4 -4
- package/dist/{chunk-VMNQ6OZA.js → chunk-ZRGEBJ4T.js} +971 -794
- package/dist/{chunk-2LZI5CAG.js → chunk-ZXF4XRKW.js} +75 -33
- package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
- package/dist/cli/index.js +31 -31
- package/dist/{clio-J5JIOIDS.js → clio-M2KGYUFZ.js} +2 -2
- package/dist/{code-nav-AXCXSBHX.js → code-nav-GQNL7XA6.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
- package/dist/{config-OEBMIN2U.js → config-XUUYQIWO.js} +27 -25
- package/dist/{configure-PUQOSIXQ.js → configure-IHJ7YOMV.js} +7 -7
- package/dist/{context-URSXPBCK.js → context-74JLXAWD.js} +12 -12
- package/dist/{context-MGSE4Z2T.js → context-75MIWW3U.js} +24 -22
- package/dist/{context-EKDCKUUZ.js → context-ZQ7SIFJV.js} +8 -7
- package/dist/{context-clear-KDAJRNUK.js → context-clear-GYKWNUML.js} +24 -22
- package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
- package/dist/{context-working-set-SBKMPPI2.js → context-working-set-UX5KEP4J.js} +11 -10
- package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-GIJBHNFL.js} +21 -20
- package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
- package/dist/{doctor-7BSE27PJ.js → doctor-SVJ5BZCW.js} +4 -4
- package/dist/{eval-IZGDOO4H.js → eval-CG6LLBLD.js} +47 -232
- package/dist/{evidence-SR7WXB5B.js → evidence-ZYFIEN42.js} +19 -18
- package/dist/{evolve-K7VE2CBX.js → evolve-QGEXEMDW.js} +19 -18
- package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
- package/dist/{fleet-7XMJNQNF.js → fleet-S5R4ZOQY.js} +49 -30
- package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
- package/dist/{init-JGNPAYXT.js → init-5DRU55YR.js} +31 -29
- package/dist/memory-7YKKR6UC.js +467 -0
- package/dist/{models-ZMMLFJNN.js → models-ZPOLRU2C.js} +10 -10
- package/dist/{monitor-2F3T5KHP.js → monitor-US5F5YGZ.js} +33 -18
- package/dist/{orchestrator-ORHT43JB.js → orchestrator-E2AL4T5N.js} +1092 -659
- package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
- package/dist/{reset-NXGTYNUO.js → reset-KZ652EK6.js} +3 -3
- package/dist/{run-RF4WJGMT.js → run-SRNBKDWD.js} +52 -40
- package/dist/{share-UT3W6E4M.js → share-CGZE33UP.js} +3 -3
- package/dist/{skills-PSACKC5Q.js → skills-S2X4DLY5.js} +4 -4
- package/dist/{skills-eval-WJSI55RZ.js → skills-eval-W2GGIC4R.js} +19 -18
- package/dist/{targets-PIIRAOYS.js → targets-54SWINWB.js} +14 -12
- package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-SAIF2OGY.js} +5 -4
- package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
- package/dist/{upgrade-346TZ6AV.js → upgrade-JKAR27XC.js} +8 -8
- package/dist/{usage-6KKXR32N.js → usage-MSAWCLX4.js} +60 -27
- package/dist/{verifiers-4UUM6TEE.js → verifiers-NCBTHHN2.js} +60 -54
- package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-GUSOQ6ZP.js} +30 -28
- package/dist/worker/entry.js +69 -58
- package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
- package/docs/README.md +3 -3
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-placement.md +1 -2
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +9 -7
- package/docs/configuration-and-targets.md +12 -1
- package/docs/context-engine.md +4 -2
- package/docs/context-working-set.md +4 -4
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +2 -2
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +4 -45
- package/docs/evidence-and-memory.md +67 -7
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +1 -1
- package/docs/extensions-and-sharing.md +2 -2
- package/docs/fleet-dispatch.md +28 -2
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +19 -2
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +3 -3
- package/docs/proactive-memory.md +26 -16
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +38 -35
- package/docs/safety-model.md +29 -7
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +2 -2
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +38 -4
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +7 -4
- package/src/cli/agents.ts +2 -3
- package/src/cli/argv.ts +14 -1
- package/src/cli/fleet.ts +15 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/memory.ts +272 -10
- package/src/cli/modes/json-stream.ts +2 -2
- package/src/cli/modes/print.ts +12 -1
- package/src/cli/run.ts +22 -2
- package/src/cli/targets.ts +12 -3
- package/src/cli/usage.ts +55 -7
- package/src/core/bus-events.ts +3 -0
- package/src/core/response-model-id.ts +134 -0
- package/src/core/toml.ts +62 -0
- package/src/core/workspace-files.ts +0 -1
- package/src/domains/agents/builtins/architect.md +1 -1
- package/src/domains/agents/catalog.ts +5 -4
- package/src/domains/agents/recipe.ts +54 -14
- package/src/domains/agents/result-contract.ts +7 -4
- package/src/domains/context/bootstrap.ts +36 -27
- package/src/domains/context/project-metadata.ts +19 -63
- package/src/domains/context/prompt-context.ts +8 -0
- package/src/domains/context/working-set/policies/index.ts +3 -4
- package/src/domains/dispatch/budget-envelope.ts +396 -0
- package/src/domains/dispatch/contract.ts +2 -0
- package/src/domains/dispatch/extension.ts +81 -27
- package/src/domains/dispatch/orphan-recovery.ts +1 -0
- package/src/domains/dispatch/receipt-integrity.ts +4 -0
- package/src/domains/dispatch/state.ts +1 -0
- package/src/domains/dispatch/types.ts +10 -3
- package/src/domains/dispatch/validation.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +14 -3
- package/src/domains/eval/metrics/evidence.ts +0 -116
- package/src/domains/eval/metrics/invariants.ts +1 -1
- package/src/domains/eval/runners/clio-run.ts +1 -10
- package/src/domains/eval/runners/external-command.ts +2 -29
- package/src/domains/eval/schema/suite.ts +0 -7
- package/src/domains/eval/suites/run.ts +1 -7
- package/src/domains/memory/index.ts +22 -0
- package/src/domains/memory/operations.ts +58 -1
- package/src/domains/memory/promotion.ts +281 -0
- package/src/domains/memory/prompt-section.ts +25 -5
- package/src/domains/memory/proposal.ts +51 -7
- package/src/domains/memory/task-bank.ts +3 -2
- package/src/domains/memory/task-memory-handoff.ts +181 -24
- package/src/domains/memory/task-memory-policy.ts +3 -1
- package/src/domains/memory/types.ts +37 -0
- package/src/domains/memory/validate.ts +178 -0
- package/src/domains/middleware/memory-intervention.ts +35 -25
- package/src/domains/middleware/runtime.ts +6 -0
- package/src/domains/middleware/skills-reminder.ts +19 -4
- package/src/domains/middleware/stalled-turn.ts +43 -1
- package/src/domains/middleware/types.ts +10 -0
- package/src/domains/observability/contract.ts +6 -1
- package/src/domains/observability/cost.ts +20 -4
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/providers/index.ts +3 -0
- package/src/domains/providers/model-discovery.ts +9 -0
- package/src/domains/providers/runtime-resolution.ts +38 -1
- package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
- package/src/domains/providers/types/context-window-slots.ts +18 -0
- package/src/domains/providers/types/runtime-descriptor.ts +3 -1
- package/src/domains/safety/call-target.ts +211 -14
- package/src/domains/safety/decision-presentation.ts +268 -0
- package/src/domains/safety/redaction.ts +73 -0
- package/src/domains/session/context-ledger.ts +10 -1
- package/src/domains/session/decision-board.ts +4 -0
- package/src/domains/session/entries.ts +3 -0
- package/src/domains/session/history.ts +68 -19
- package/src/domains/session/usage.ts +24 -7
- package/src/engine/acp/event-mapper.ts +7 -0
- package/src/engine/acp/server.ts +29 -2
- package/src/engine/apis/lmstudio.ts +25 -4
- package/src/engine/apis/openai-completions.ts +147 -22
- package/src/engine/claude/sdk-runtime.ts +8 -2
- package/src/engine/claude/tool-safety.ts +13 -0
- package/src/engine/loop-guard.ts +27 -3
- package/src/engine/worker-events.ts +4 -3
- package/src/engine/worker-runtime.ts +59 -54
- package/src/entry/orchestrator.ts +18 -1
- package/src/interactive/chat-loop-messages.ts +22 -0
- package/src/interactive/chat-loop.ts +13 -0
- package/src/interactive/chat-renderer.ts +19 -3
- package/src/interactive/clio-editor.ts +44 -7
- package/src/interactive/context-overlay.ts +43 -5
- package/src/interactive/cost-overlay.ts +39 -8
- package/src/interactive/dispatch-board.ts +212 -35
- package/src/interactive/footer/widgets.ts +13 -0
- package/src/interactive/interactive-application.ts +6 -1
- package/src/interactive/interactive-input-runtime.ts +11 -1
- package/src/interactive/interactive-presentation.ts +11 -1
- package/src/interactive/memory-overlay.ts +89 -4
- package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
- package/src/interactive/overlay-frame.ts +5 -2
- package/src/interactive/overlay-general-openers.ts +40 -1
- package/src/interactive/overlay-key-routing.ts +41 -1
- package/src/interactive/overlay-lifecycle.ts +11 -4
- package/src/interactive/overlay-permission-lifecycle.ts +23 -8
- package/src/interactive/overlay-transitions.ts +11 -0
- package/src/interactive/overlays/ask-user.ts +74 -30
- package/src/interactive/overlays/decisions.ts +3 -1
- package/src/interactive/permission-hint.ts +35 -0
- package/src/interactive/permission-overlay.ts +95 -45
- package/src/interactive/renderers/tool-execution.ts +19 -49
- package/src/interactive/session-last-turn.ts +8 -1
- package/src/interactive/session-usage-reseed.ts +36 -10
- package/src/interactive/slash-commands.ts +2 -2
- package/src/interactive/status/summary.ts +5 -0
- package/src/interactive/status/types.ts +5 -0
- package/src/interactive/terminal-lease.ts +1 -0
- package/src/interactive/turn-context.ts +96 -23
- package/src/interactive/turn-middleware.ts +1 -0
- package/src/interactive/turn-runtime.ts +37 -8
- package/src/interactive/turn-state.ts +3 -0
- package/src/interactive/worker-progress.ts +440 -0
- package/src/interactive/worker-stream.ts +51 -110
- package/src/tools/agent-tools.ts +28 -3
- package/src/tools/ask-user.ts +21 -1
- package/src/tools/context/index.ts +2 -2
- package/src/tools/dispatch-arguments.ts +8 -0
- package/src/tools/dispatch-event-text.ts +19 -0
- package/src/tools/dispatch.ts +24 -1
- package/src/tools/monitor.ts +15 -0
- package/src/tools/registry.ts +15 -5
- package/src/tools/result-disposition.ts +156 -0
- package/src/tools/result-shaping.ts +59 -1
- package/src/tools/verify/authoring.ts +55 -54
- package/src/tools/worker-evidence.ts +19 -0
- package/src/worker/spec-contract.ts +43 -3
- package/dist/chunk-EFADSJET.js +0 -18
- package/dist/memory-4ALKDJ4Q.js +0 -246
- package/src/domains/eval/metrics/chaos-stream.ts +0 -93
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { parse as parseYaml } from "yaml";
|
|
22
|
+
import { createTomlFileReader, type TomlFileReader, tomlTableAt } from "../../core/toml.js";
|
|
22
23
|
|
|
23
24
|
/** Longest description worth carrying into a one-sentence identity line. */
|
|
24
25
|
const MAX_DESCRIPTION_CHARS = 400;
|
|
@@ -39,7 +40,7 @@ interface MetadataFragment {
|
|
|
39
40
|
description?: string | null;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
type MetadataReader = (cwd: string) => { file: string; fragment: MetadataFragment } | null;
|
|
43
|
+
type MetadataReader = (cwd: string, tomlFiles: TomlFileReader) => { file: string; fragment: MetadataFragment } | null;
|
|
43
44
|
|
|
44
45
|
function readText(cwd: string, relative: string): string | null {
|
|
45
46
|
try {
|
|
@@ -83,54 +84,6 @@ function jsonRecord(cwd: string, relative: string): Record<string, unknown> | nu
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/**
|
|
87
|
-
* One key from one TOML table, without a TOML parser.
|
|
88
|
-
*
|
|
89
|
-
* Deliberately narrow: it reads `key = "value"` (single-line, single- or
|
|
90
|
-
* double-quoted) from the named top-level table and understands nothing else.
|
|
91
|
-
* That is the entire shape `description` and `name` take in `pyproject.toml`
|
|
92
|
-
* and `Cargo.toml`. A multi-line or otherwise exotic value reads as absent,
|
|
93
|
-
* which costs a description and never produces a wrong one.
|
|
94
|
-
*/
|
|
95
|
-
function tomlTableValue(raw: string, table: string, key: string): string | null {
|
|
96
|
-
const lines = raw.split(/\r?\n/u);
|
|
97
|
-
let inTable = false;
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
const trimmed = line.trim();
|
|
100
|
-
if (trimmed.startsWith("[")) {
|
|
101
|
-
inTable = trimmed === `[${table}]`;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (!inTable) continue;
|
|
105
|
-
const match = /^([A-Za-z0-9_-]+)\s*=\s*(.*)$/u.exec(trimmed);
|
|
106
|
-
if (!match || match[1] !== key) continue;
|
|
107
|
-
const value = (match[2] ?? "").trim();
|
|
108
|
-
const quoted = /^"((?:[^"\\]|\\.)*)"$/u.exec(value) ?? /^'([^']*)'$/u.exec(value);
|
|
109
|
-
if (!quoted) return null;
|
|
110
|
-
return (quoted[1] ?? "").replace(/\\"/gu, '"');
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Whether a TOML file declares the named top-level table, e.g. `tool.tox`.
|
|
117
|
-
* A trimmed line must start with `[table]` and carry nothing after it but an
|
|
118
|
-
* optional `# comment`, the only content TOML itself allows there. A raw
|
|
119
|
-
* `raw.includes("[table]")` check would also fire on a comment mentioning the
|
|
120
|
-
* table or a string value that happens to contain the same bracketed text;
|
|
121
|
-
* anchoring to the start of the line rules both out without a TOML parser,
|
|
122
|
-
* while still accepting the valid `[table] # comment` form that a plain
|
|
123
|
-
* equality check would have missed.
|
|
124
|
-
*/
|
|
125
|
-
export function hasTomlTable(raw: string, table: string): boolean {
|
|
126
|
-
const header = `[${table}]`;
|
|
127
|
-
return raw.split(/\r?\n/u).some((line) => {
|
|
128
|
-
const trimmed = line.trim();
|
|
129
|
-
if (!trimmed.startsWith(header)) return false;
|
|
130
|
-
return /^\s*(?:#.*)?$/u.test(trimmed.slice(header.length));
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
87
|
/** One `KEY = value` line from a Doxyfile or an INI-shaped config. */
|
|
135
88
|
function keyEqualsValue(raw: string, key: string): string | null {
|
|
136
89
|
const match = new RegExp(`^[ \\t]*${key}[ \\t]*(?:\\+?=)[ \\t]*(.+)$`, "mu").exec(raw);
|
|
@@ -182,27 +135,30 @@ const packageJson: MetadataReader = (cwd) => {
|
|
|
182
135
|
return { file: "package.json", fragment: { name: name(record.name), description: description(record.description) } };
|
|
183
136
|
};
|
|
184
137
|
|
|
185
|
-
const pyprojectToml: MetadataReader = (
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
138
|
+
const pyprojectToml: MetadataReader = (_cwd, tomlFiles) => {
|
|
139
|
+
const document = tomlFiles.read("pyproject.toml");
|
|
140
|
+
if (document === null) return null;
|
|
141
|
+
const project = tomlTableAt(document, ["project"]);
|
|
142
|
+
const poetry = tomlTableAt(document, ["tool", "poetry"]);
|
|
188
143
|
// PEP 621 `[project]` first, then Poetry's older table.
|
|
189
|
-
const projectName = tomlTableValue(raw, "project", "name") ?? tomlTableValue(raw, "tool.poetry", "name");
|
|
190
|
-
const projectDescription =
|
|
191
|
-
tomlTableValue(raw, "project", "description") ?? tomlTableValue(raw, "tool.poetry", "description");
|
|
192
144
|
return {
|
|
193
145
|
file: "pyproject.toml",
|
|
194
|
-
fragment: {
|
|
146
|
+
fragment: {
|
|
147
|
+
name: name(project?.name) ?? name(poetry?.name),
|
|
148
|
+
description: description(project?.description) ?? description(poetry?.description),
|
|
149
|
+
},
|
|
195
150
|
};
|
|
196
151
|
};
|
|
197
152
|
|
|
198
|
-
const cargoToml: MetadataReader = (
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
153
|
+
const cargoToml: MetadataReader = (_cwd, tomlFiles) => {
|
|
154
|
+
const document = tomlFiles.read("Cargo.toml");
|
|
155
|
+
if (document === null) return null;
|
|
156
|
+
const packageTable = tomlTableAt(document, ["package"]);
|
|
201
157
|
return {
|
|
202
158
|
file: "Cargo.toml",
|
|
203
159
|
fragment: {
|
|
204
|
-
name: name(
|
|
205
|
-
description: description(
|
|
160
|
+
name: name(packageTable?.name),
|
|
161
|
+
description: description(packageTable?.description),
|
|
206
162
|
},
|
|
207
163
|
};
|
|
208
164
|
};
|
|
@@ -488,13 +444,13 @@ function readmeTitle(content: string): string | null {
|
|
|
488
444
|
* a CMake project that names itself but describes itself only in its README
|
|
489
445
|
* should get both, not one or neither.
|
|
490
446
|
*/
|
|
491
|
-
export function readProjectMetadata(cwd: string): ProjectMetadata {
|
|
447
|
+
export function readProjectMetadata(cwd: string, tomlFiles = createTomlFileReader(cwd)): ProjectMetadata {
|
|
492
448
|
const out: ProjectMetadata = { name: null, description: null, nameSource: null, descriptionSource: null };
|
|
493
449
|
for (const reader of METADATA_READERS) {
|
|
494
450
|
if (out.name !== null && out.description !== null) break;
|
|
495
451
|
let read: ReturnType<MetadataReader>;
|
|
496
452
|
try {
|
|
497
|
-
read = reader(cwd);
|
|
453
|
+
read = reader(cwd, tomlFiles);
|
|
498
454
|
} catch {
|
|
499
455
|
continue;
|
|
500
456
|
}
|
|
@@ -20,6 +20,10 @@ import { wikiCompleteness, wikiStaleness } from "./wiki/staleness.js";
|
|
|
20
20
|
* context-init preload reporting, and `clio-coder config inspect`, so every surface
|
|
21
21
|
* measures the same text the session prompt would preload.
|
|
22
22
|
*/
|
|
23
|
+
/** The one line that stands in for a handbook this workspace does not have. */
|
|
24
|
+
export const HANDBOOK_ABSENT_FRAGMENT =
|
|
25
|
+
"<handbook>none: this workspace has no CLIO-CODER.md, so do not read one; learn the repository from its files, and the operator can run /context init to write a handbook</handbook>";
|
|
26
|
+
|
|
23
27
|
export function renderPromptContext(cwd: string): ProjectPromptContext {
|
|
24
28
|
const projectType = detectProjectType(cwd);
|
|
25
29
|
const pieces = [renderProjectTypeFragment(projectType)];
|
|
@@ -30,6 +34,10 @@ export function renderPromptContext(cwd: string): ProjectPromptContext {
|
|
|
30
34
|
for (const issue of loadedClioMd.errors) {
|
|
31
35
|
warnings.push(`clio: malformed ${issue.path} ignored: ${issue.error}`);
|
|
32
36
|
}
|
|
37
|
+
// Said where the handbook would have been: a model that sees no project
|
|
38
|
+
// context spends its first tool call reading CLIO-CODER.md and gets
|
|
39
|
+
// ENOENT, while the operator's header already says it is missing (#191).
|
|
40
|
+
if (loadedClioMd.files.length === 0 && loadedClioMd.errors.length === 0) pieces.push(HANDBOOK_ABSENT_FRAGMENT);
|
|
33
41
|
const codewiki = readCodewiki(cwd);
|
|
34
42
|
if (codewiki) {
|
|
35
43
|
const state = readClioState(cwd);
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* replay-lite runner resolve the same object from the same settings value and
|
|
4
4
|
* cannot drift into running different selections.
|
|
5
5
|
*
|
|
6
|
-
* `structural-v1` is the default;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* resolvable as the exact pre-layer selection.
|
|
6
|
+
* `structural-v1` is the default; internal replay studies put it ahead of
|
|
7
|
+
* `age-horizon` on retention and ahead of the random control on precision.
|
|
8
|
+
* `age-horizon` stays resolvable as the exact pre-layer selection.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import type { WorkingSetPolicy, WorkingSetPolicyId } from "../contract.js";
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import type { WorkerBudget } from "../../worker/spec-contract.js";
|
|
2
|
+
import type { AgentBudget, AgentBudgetPhase } from "../agents/recipe.js";
|
|
3
|
+
|
|
4
|
+
/** Invocation-level phase requested by dispatch. */
|
|
5
|
+
export interface DispatchBudgetPhase {
|
|
6
|
+
toolCalls: number;
|
|
7
|
+
readReserve: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Typed invocation request. The ceiling preauthorizes growth on a later phase. */
|
|
11
|
+
export interface DispatchBudgetRequest extends DispatchBudgetPhase {
|
|
12
|
+
retryRevision?: DispatchBudgetPhase;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type BudgetEnvelopeReasonCode =
|
|
16
|
+
| "global-cap-clamp"
|
|
17
|
+
| "read-reserve-unavailable-clamp"
|
|
18
|
+
| "read-reserve-cap-clamp"
|
|
19
|
+
| "retry-growth-authorized"
|
|
20
|
+
| "retry-growth-denied"
|
|
21
|
+
| "revision-growth-authorized"
|
|
22
|
+
| "revision-growth-denied";
|
|
23
|
+
|
|
24
|
+
export interface BudgetEnvelopeReason {
|
|
25
|
+
code: BudgetEnvelopeReasonCode;
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RunToolBudgetEnvelope {
|
|
30
|
+
version: 1;
|
|
31
|
+
policy: {
|
|
32
|
+
recipeId: string;
|
|
33
|
+
default: AgentBudgetPhase & { synthesis: boolean };
|
|
34
|
+
maximum: AgentBudgetPhase;
|
|
35
|
+
exact: boolean;
|
|
36
|
+
};
|
|
37
|
+
request: DispatchBudgetRequest | null;
|
|
38
|
+
effective: WorkerBudget;
|
|
39
|
+
reasons: ReadonlyArray<BudgetEnvelopeReason>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type BudgetAdmissionDenialCode =
|
|
43
|
+
| "exact-recipe-policy"
|
|
44
|
+
| "recipe-maximum"
|
|
45
|
+
| "global-cap"
|
|
46
|
+
| "ceiling-below-request";
|
|
47
|
+
|
|
48
|
+
export class BudgetAdmissionError extends Error {
|
|
49
|
+
readonly code: BudgetAdmissionDenialCode;
|
|
50
|
+
|
|
51
|
+
constructor(code: BudgetAdmissionDenialCode, message: string) {
|
|
52
|
+
super(`dispatch: budget admission denied (${code}): ${message}`);
|
|
53
|
+
this.name = "BudgetAdmissionError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clonePhase(value: unknown, prefix: string): DispatchBudgetPhase {
|
|
63
|
+
if (!isRecord(value)) throw new Error(`${prefix} must be an object`);
|
|
64
|
+
for (const key of Object.keys(value)) {
|
|
65
|
+
if (key !== "toolCalls" && key !== "readReserve") throw new Error(`${prefix}.${key} is unknown`);
|
|
66
|
+
}
|
|
67
|
+
for (const key of ["toolCalls", "readReserve"] as const) {
|
|
68
|
+
if (!Object.hasOwn(value, key)) throw new Error(`${prefix}.${key} is required`);
|
|
69
|
+
}
|
|
70
|
+
const toolCalls = value.toolCalls;
|
|
71
|
+
if (typeof toolCalls !== "number" || !Number.isSafeInteger(toolCalls) || toolCalls <= 0) {
|
|
72
|
+
throw new Error(`${prefix}.toolCalls must be a positive safe integer`);
|
|
73
|
+
}
|
|
74
|
+
const readReserve = value.readReserve;
|
|
75
|
+
if (typeof readReserve !== "number" || !Number.isSafeInteger(readReserve) || readReserve < 0) {
|
|
76
|
+
throw new Error(`${prefix}.readReserve must be a non-negative safe integer`);
|
|
77
|
+
}
|
|
78
|
+
if (readReserve >= toolCalls) throw new Error(`${prefix}.readReserve must be less than ${prefix}.toolCalls`);
|
|
79
|
+
return { toolCalls, readReserve };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Strict validation and cloning for model, CLI, and persisted request inputs. */
|
|
83
|
+
export function cloneDispatchBudgetRequest(value: unknown, prefix = "budget"): DispatchBudgetRequest {
|
|
84
|
+
if (!isRecord(value)) throw new Error(`${prefix} must be an object`);
|
|
85
|
+
for (const key of Object.keys(value)) {
|
|
86
|
+
if (key !== "toolCalls" && key !== "readReserve" && key !== "retryRevision") {
|
|
87
|
+
throw new Error(`${prefix}.${key} is unknown`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const phase = clonePhase({ toolCalls: value.toolCalls, readReserve: value.readReserve }, prefix);
|
|
91
|
+
const retryRevision =
|
|
92
|
+
value.retryRevision === undefined ? undefined : clonePhase(value.retryRevision, `${prefix}.retryRevision`);
|
|
93
|
+
return { ...phase, ...(retryRevision === undefined ? {} : { retryRevision }) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function freezeEnvelope(envelope: RunToolBudgetEnvelope): RunToolBudgetEnvelope {
|
|
97
|
+
Object.freeze(envelope.policy.default);
|
|
98
|
+
Object.freeze(envelope.policy.maximum);
|
|
99
|
+
Object.freeze(envelope.policy);
|
|
100
|
+
if (envelope.request !== null) {
|
|
101
|
+
if (envelope.request.retryRevision !== undefined) Object.freeze(envelope.request.retryRevision);
|
|
102
|
+
Object.freeze(envelope.request);
|
|
103
|
+
}
|
|
104
|
+
Object.freeze(envelope.effective);
|
|
105
|
+
for (const reason of envelope.reasons) Object.freeze(reason);
|
|
106
|
+
Object.freeze(envelope.reasons);
|
|
107
|
+
return Object.freeze(envelope);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function phaseWithin(candidate: DispatchBudgetPhase, maximum: AgentBudgetPhase): boolean {
|
|
111
|
+
return candidate.toolCalls <= maximum.toolCalls && candidate.readReserve <= maximum.readReserve;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function samePhase(left: DispatchBudgetPhase, right: AgentBudgetPhase): boolean {
|
|
115
|
+
return left.toolCalls === right.toolCalls && left.readReserve === right.readReserve;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertRequestAdmitted(input: { request: DispatchBudgetRequest; policy: AgentBudget; hardCap: number }): void {
|
|
119
|
+
const { request, policy, hardCap } = input;
|
|
120
|
+
const authoredMaximum = policy.maximum ?? policy;
|
|
121
|
+
if (policy.maximum === undefined && !samePhase(request, policy)) {
|
|
122
|
+
throw new BudgetAdmissionError(
|
|
123
|
+
"exact-recipe-policy",
|
|
124
|
+
`the recipe pins ${policy.toolCalls} tool calls with a read reserve of ${policy.readReserve}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (!phaseWithin(request, authoredMaximum)) {
|
|
128
|
+
throw new BudgetAdmissionError(
|
|
129
|
+
"recipe-maximum",
|
|
130
|
+
`the requested phase ${request.toolCalls}/${request.readReserve} exceeds recipe maximum ${authoredMaximum.toolCalls}/${authoredMaximum.readReserve}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (request.toolCalls > hardCap) {
|
|
134
|
+
throw new BudgetAdmissionError(
|
|
135
|
+
"global-cap",
|
|
136
|
+
`the requested ${request.toolCalls} tool calls exceed the operator cap of ${hardCap}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const ceiling = request.retryRevision;
|
|
140
|
+
if (ceiling === undefined) return;
|
|
141
|
+
if (ceiling.toolCalls < request.toolCalls || ceiling.readReserve < request.readReserve) {
|
|
142
|
+
throw new BudgetAdmissionError(
|
|
143
|
+
"ceiling-below-request",
|
|
144
|
+
`retryRevision ${ceiling.toolCalls}/${ceiling.readReserve} must not be smaller than the requested phase ${request.toolCalls}/${request.readReserve}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (policy.maximum === undefined && !samePhase(ceiling, policy)) {
|
|
148
|
+
throw new BudgetAdmissionError(
|
|
149
|
+
"exact-recipe-policy",
|
|
150
|
+
`the recipe pins ${policy.toolCalls} tool calls with a read reserve of ${policy.readReserve}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!phaseWithin(ceiling, authoredMaximum)) {
|
|
154
|
+
throw new BudgetAdmissionError(
|
|
155
|
+
"recipe-maximum",
|
|
156
|
+
`retryRevision ${ceiling.toolCalls}/${ceiling.readReserve} exceeds recipe maximum ${authoredMaximum.toolCalls}/${authoredMaximum.readReserve}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (ceiling.toolCalls > hardCap) {
|
|
160
|
+
throw new BudgetAdmissionError(
|
|
161
|
+
"global-cap",
|
|
162
|
+
`retryRevision ${ceiling.toolCalls} exceeds the operator cap of ${hardCap}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface ResolveToolBudgetEnvelopeInput {
|
|
168
|
+
recipeId: string;
|
|
169
|
+
policy: AgentBudget;
|
|
170
|
+
request?: DispatchBudgetRequest;
|
|
171
|
+
hardCap: number;
|
|
172
|
+
hasReadTool: boolean;
|
|
173
|
+
retry: boolean;
|
|
174
|
+
revision: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Resolve the one immutable envelope used by admission, enforcement, and evidence. */
|
|
178
|
+
export function resolveToolBudgetEnvelope(input: ResolveToolBudgetEnvelopeInput): RunToolBudgetEnvelope {
|
|
179
|
+
if (!Number.isSafeInteger(input.hardCap) || input.hardCap <= 0) {
|
|
180
|
+
throw new Error("dispatch: worker tool-call cap must be a positive safe integer");
|
|
181
|
+
}
|
|
182
|
+
if (input.request !== undefined)
|
|
183
|
+
assertRequestAdmitted({ request: input.request, policy: input.policy, hardCap: input.hardCap });
|
|
184
|
+
|
|
185
|
+
const request = input.request === undefined ? null : cloneDispatchBudgetRequest(input.request);
|
|
186
|
+
const base = request ?? input.policy;
|
|
187
|
+
const reasons: BudgetEnvelopeReason[] = [];
|
|
188
|
+
let selected: DispatchBudgetPhase = base;
|
|
189
|
+
const phaseKind = input.retry ? "retry" : input.revision ? "revision" : null;
|
|
190
|
+
if (phaseKind !== null) {
|
|
191
|
+
const ceiling = request?.retryRevision;
|
|
192
|
+
if (ceiling !== undefined && !samePhase(ceiling, base)) {
|
|
193
|
+
selected = ceiling;
|
|
194
|
+
reasons.push({
|
|
195
|
+
code: phaseKind === "retry" ? "retry-growth-authorized" : "revision-growth-authorized",
|
|
196
|
+
detail: `${phaseKind} phase grew from ${base.toolCalls}/${base.readReserve} to the preauthorized ceiling ${ceiling.toolCalls}/${ceiling.readReserve}`,
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
reasons.push({
|
|
200
|
+
code: phaseKind === "retry" ? "retry-growth-denied" : "revision-growth-denied",
|
|
201
|
+
detail:
|
|
202
|
+
ceiling === undefined
|
|
203
|
+
? `${phaseKind} phase retained ${base.toolCalls}/${base.readReserve} because the original request declared no ceiling`
|
|
204
|
+
: `${phaseKind} phase retained ${base.toolCalls}/${base.readReserve} because the declared ceiling did not authorize growth`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const revisionCeiling = phaseKind === null ? request?.retryRevision : undefined;
|
|
209
|
+
if (revisionCeiling !== undefined && !samePhase(revisionCeiling, selected)) {
|
|
210
|
+
reasons.push({
|
|
211
|
+
code: "revision-growth-authorized",
|
|
212
|
+
detail: `a result-contract revision may grow from ${selected.toolCalls}/${selected.readReserve} to the preauthorized ceiling ${revisionCeiling.toolCalls}/${revisionCeiling.readReserve}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let toolCalls = selected.toolCalls;
|
|
217
|
+
if (toolCalls > input.hardCap) {
|
|
218
|
+
reasons.push({
|
|
219
|
+
code: "global-cap-clamp",
|
|
220
|
+
detail: `tool calls were clamped from ${toolCalls} to the operator cap of ${input.hardCap}`,
|
|
221
|
+
});
|
|
222
|
+
toolCalls = input.hardCap;
|
|
223
|
+
}
|
|
224
|
+
let readReserve = selected.readReserve;
|
|
225
|
+
if (!input.hasReadTool && readReserve > 0) {
|
|
226
|
+
reasons.push({
|
|
227
|
+
code: "read-reserve-unavailable-clamp",
|
|
228
|
+
detail: `read reserve was clamped from ${readReserve} to 0 because the read tool is unavailable`,
|
|
229
|
+
});
|
|
230
|
+
readReserve = 0;
|
|
231
|
+
}
|
|
232
|
+
const maximumEffectiveReserve = Math.max(0, toolCalls - 1);
|
|
233
|
+
if (readReserve > maximumEffectiveReserve) {
|
|
234
|
+
reasons.push({
|
|
235
|
+
code: "read-reserve-cap-clamp",
|
|
236
|
+
detail: `read reserve was clamped from ${readReserve} to ${maximumEffectiveReserve} after the tool-call cap`,
|
|
237
|
+
});
|
|
238
|
+
readReserve = maximumEffectiveReserve;
|
|
239
|
+
}
|
|
240
|
+
let effectiveRevision: DispatchBudgetPhase | undefined;
|
|
241
|
+
if (revisionCeiling !== undefined && !samePhase(revisionCeiling, selected)) {
|
|
242
|
+
let revisionReadReserve = revisionCeiling.readReserve;
|
|
243
|
+
if (!input.hasReadTool && revisionReadReserve > 0) {
|
|
244
|
+
reasons.push({
|
|
245
|
+
code: "read-reserve-unavailable-clamp",
|
|
246
|
+
detail: `result-contract revision read reserve was clamped from ${revisionReadReserve} to 0 because the read tool is unavailable`,
|
|
247
|
+
});
|
|
248
|
+
revisionReadReserve = 0;
|
|
249
|
+
}
|
|
250
|
+
effectiveRevision = { toolCalls: revisionCeiling.toolCalls, readReserve: revisionReadReserve };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const authoredMaximum = input.policy.maximum ?? input.policy;
|
|
254
|
+
return freezeEnvelope({
|
|
255
|
+
version: 1,
|
|
256
|
+
policy: {
|
|
257
|
+
recipeId: input.recipeId,
|
|
258
|
+
default: {
|
|
259
|
+
toolCalls: input.policy.toolCalls,
|
|
260
|
+
readReserve: input.policy.readReserve,
|
|
261
|
+
synthesis: input.policy.synthesis,
|
|
262
|
+
},
|
|
263
|
+
maximum: { toolCalls: authoredMaximum.toolCalls, readReserve: authoredMaximum.readReserve },
|
|
264
|
+
exact: input.policy.maximum === undefined,
|
|
265
|
+
},
|
|
266
|
+
request,
|
|
267
|
+
effective: {
|
|
268
|
+
toolCalls,
|
|
269
|
+
readReserve,
|
|
270
|
+
synthesis: input.policy.synthesis,
|
|
271
|
+
hardCap: input.hardCap,
|
|
272
|
+
...(effectiveRevision === undefined ? {} : { revision: effectiveRevision }),
|
|
273
|
+
},
|
|
274
|
+
reasons,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatBudgetPolicy(envelope: RunToolBudgetEnvelope): string {
|
|
279
|
+
const policy = envelope.policy;
|
|
280
|
+
const range = policy.exact
|
|
281
|
+
? `exact ${policy.default.toolCalls}/${policy.default.readReserve}`
|
|
282
|
+
: `default ${policy.default.toolCalls}/${policy.default.readReserve}, max ${policy.maximum.toolCalls}/${policy.maximum.readReserve}`;
|
|
283
|
+
return `${policy.recipeId} ${range}, synthesis=${policy.default.synthesis ? "on" : "off"}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function formatBudgetRequest(envelope: RunToolBudgetEnvelope): string {
|
|
287
|
+
const request = envelope.request;
|
|
288
|
+
if (request === null) return "recipe default";
|
|
289
|
+
const ceiling = request.retryRevision;
|
|
290
|
+
return ceiling === undefined
|
|
291
|
+
? `${request.toolCalls}/${request.readReserve}, no retry/revision growth`
|
|
292
|
+
: `${request.toolCalls}/${request.readReserve}, retry/revision ceiling ${ceiling.toolCalls}/${ceiling.readReserve}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function formatEffectiveBudget(envelope: RunToolBudgetEnvelope): string {
|
|
296
|
+
const budget = envelope.effective;
|
|
297
|
+
const revision = budget.revision
|
|
298
|
+
? `, result revision ${budget.revision.toolCalls}/${budget.revision.readReserve}`
|
|
299
|
+
: "";
|
|
300
|
+
return `${budget.toolCalls}/${budget.readReserve}${revision}, lifetime cap ${budget.hardCap}, synthesis=${budget.synthesis ? "on" : "off"}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function formatBudgetReasons(envelope: RunToolBudgetEnvelope): string {
|
|
304
|
+
return envelope.reasons.length === 0 ? "none" : envelope.reasons.map((reason) => reason.code).join(",");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const BUDGET_REASON_CODES: ReadonlySet<string> = new Set<BudgetEnvelopeReasonCode>([
|
|
308
|
+
"global-cap-clamp",
|
|
309
|
+
"read-reserve-unavailable-clamp",
|
|
310
|
+
"read-reserve-cap-clamp",
|
|
311
|
+
"retry-growth-authorized",
|
|
312
|
+
"retry-growth-denied",
|
|
313
|
+
"revision-growth-authorized",
|
|
314
|
+
"revision-growth-denied",
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
/** Safely project an envelope that crossed a persistence or event boundary. */
|
|
318
|
+
export function cloneRunToolBudgetEnvelope(value: unknown): RunToolBudgetEnvelope | undefined {
|
|
319
|
+
try {
|
|
320
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.policy) || !isRecord(value.effective)) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const recipeId = value.policy.recipeId;
|
|
324
|
+
if (typeof recipeId !== "string" || recipeId.length === 0 || typeof value.policy.exact !== "boolean") {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
if (!isRecord(value.policy.default) || typeof value.policy.default.synthesis !== "boolean") return undefined;
|
|
328
|
+
const defaultPhase = clonePhase(
|
|
329
|
+
{ toolCalls: value.policy.default.toolCalls, readReserve: value.policy.default.readReserve },
|
|
330
|
+
"budget.policy.default",
|
|
331
|
+
);
|
|
332
|
+
const maximum = clonePhase(value.policy.maximum, "budget.policy.maximum");
|
|
333
|
+
const request = value.request === null ? null : cloneDispatchBudgetRequest(value.request);
|
|
334
|
+
const effectivePhase = clonePhase(
|
|
335
|
+
{ toolCalls: value.effective.toolCalls, readReserve: value.effective.readReserve },
|
|
336
|
+
"budget.effective",
|
|
337
|
+
);
|
|
338
|
+
const revision =
|
|
339
|
+
value.effective.revision === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: clonePhase(value.effective.revision, "budget.effective.revision");
|
|
342
|
+
if (
|
|
343
|
+
typeof value.effective.synthesis !== "boolean" ||
|
|
344
|
+
typeof value.effective.hardCap !== "number" ||
|
|
345
|
+
!Number.isSafeInteger(value.effective.hardCap) ||
|
|
346
|
+
value.effective.hardCap <= 0 ||
|
|
347
|
+
!Array.isArray(value.reasons)
|
|
348
|
+
) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
if (
|
|
352
|
+
maximum.toolCalls < defaultPhase.toolCalls ||
|
|
353
|
+
maximum.readReserve < defaultPhase.readReserve ||
|
|
354
|
+
(value.policy.exact && !samePhase(maximum, defaultPhase)) ||
|
|
355
|
+
effectivePhase.toolCalls > value.effective.hardCap ||
|
|
356
|
+
value.effective.synthesis !== value.policy.default.synthesis ||
|
|
357
|
+
(revision !== undefined &&
|
|
358
|
+
(revision.toolCalls < effectivePhase.toolCalls ||
|
|
359
|
+
revision.toolCalls > value.effective.hardCap ||
|
|
360
|
+
(revision.toolCalls === effectivePhase.toolCalls && revision.readReserve <= effectivePhase.readReserve)))
|
|
361
|
+
) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
const reasons: BudgetEnvelopeReason[] = [];
|
|
365
|
+
for (const reason of value.reasons) {
|
|
366
|
+
if (
|
|
367
|
+
!isRecord(reason) ||
|
|
368
|
+
typeof reason.code !== "string" ||
|
|
369
|
+
!BUDGET_REASON_CODES.has(reason.code) ||
|
|
370
|
+
typeof reason.detail !== "string"
|
|
371
|
+
) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
reasons.push({ code: reason.code as BudgetEnvelopeReasonCode, detail: reason.detail });
|
|
375
|
+
}
|
|
376
|
+
return freezeEnvelope({
|
|
377
|
+
version: 1,
|
|
378
|
+
policy: {
|
|
379
|
+
recipeId,
|
|
380
|
+
default: { ...defaultPhase, synthesis: value.policy.default.synthesis },
|
|
381
|
+
maximum,
|
|
382
|
+
exact: value.policy.exact,
|
|
383
|
+
},
|
|
384
|
+
request,
|
|
385
|
+
effective: {
|
|
386
|
+
...effectivePhase,
|
|
387
|
+
synthesis: value.effective.synthesis,
|
|
388
|
+
hardCap: value.effective.hardCap,
|
|
389
|
+
...(revision === undefined ? {} : { revision }),
|
|
390
|
+
},
|
|
391
|
+
reasons,
|
|
392
|
+
});
|
|
393
|
+
} catch {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { ProtectedArtifactState } from "../safety/protected-artifacts.js";
|
|
|
6
6
|
import type { AssignmentId, DispatchAssignment } from "./assignment.js";
|
|
7
7
|
import type { DurableAssignmentRecord } from "./assignment-store.js";
|
|
8
8
|
import type { DetachedBatchRecord, RegisterDetachedBatchInput } from "./batch-store.js";
|
|
9
|
+
import type { RunToolBudgetEnvelope } from "./budget-envelope.js";
|
|
9
10
|
import type { ExecutionRole } from "./execution-role.js";
|
|
10
11
|
import type { DispatchReservationRecord, ReservationTopology } from "./reservation-store.js";
|
|
11
12
|
import type { ApprovedAssignmentRoute } from "./route-approval.js";
|
|
@@ -111,6 +112,7 @@ export interface DispatchSnapshot {
|
|
|
111
112
|
outcomePhase: string;
|
|
112
113
|
heartbeat: "alive" | "stale" | "dead" | "n/a";
|
|
113
114
|
lineage: RunLineage;
|
|
115
|
+
budget?: RunToolBudgetEnvelope;
|
|
114
116
|
startedAt: string;
|
|
115
117
|
/** Execution-only elapsed time retained for runtime monitoring. */
|
|
116
118
|
elapsedMs: number;
|