@iowarp/clio-coder 0.3.2 → 0.3.4
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 +269 -458
- package/CONTRIBUTING.md +1 -1
- package/README.md +3 -3
- package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
- package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
- package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
- package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
- package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
- package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
- package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
- package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
- package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
- package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
- package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
- package/dist/chunk-5M54SPOL.js +926 -0
- package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
- package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
- package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
- package/dist/chunk-AOCYTWAV.js +449 -0
- package/dist/chunk-BEY543CS.js +258 -0
- package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
- package/dist/chunk-BPGS2WCQ.js +612 -0
- package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
- package/dist/chunk-CFGTUFWB.js +67 -0
- package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
- package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
- package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
- package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
- package/dist/chunk-FYYLNIL5.js +313 -0
- package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
- package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
- package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
- package/dist/chunk-K7VKOLQQ.js +15 -0
- package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
- package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
- package/dist/chunk-LL4KHSZI.js +22 -0
- package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
- package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
- package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
- package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
- package/dist/chunk-OZNBF4L3.js +23 -0
- package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
- package/dist/chunk-QQK64KLB.js +1360 -0
- package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
- package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
- package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
- package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
- package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
- package/dist/chunk-TW3WDMVS.js +677 -0
- package/dist/chunk-TZSKNMZG.js +434 -0
- package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
- package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
- package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
- package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
- package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
- package/dist/chunk-VSNATDE6.js +122 -0
- package/dist/chunk-W6GROXXM.js +69 -0
- package/dist/chunk-WPQLXFOZ.js +375 -0
- package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
- package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
- package/dist/chunk-ZGVHUX3M.js +66 -0
- package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
- package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
- package/dist/cli/index.js +27 -23
- package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
- package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
- package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
- package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
- package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
- package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
- package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
- package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
- package/dist/context-working-set-SBKMPPI2.js +1552 -0
- package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
- package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
- package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
- package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
- package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
- package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
- package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
- package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
- package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
- package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
- package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
- package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
- package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
- package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
- package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
- package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
- package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
- package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
- package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
- package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
- package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
- package/dist/verifiers-4UUM6TEE.js +1214 -0
- package/dist/verify-X5HDROLA.js +25 -0
- package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
- package/dist/worker/entry.js +33 -24
- package/docs/README.md +8 -7
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -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 +53 -21
- package/docs/config-knobs-audit.md +1 -2
- package/docs/configuration-and-targets.md +15 -1
- package/docs/context-engine.md +64 -12
- package/docs/context-working-set.md +194 -0
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +5 -5
- package/docs/documentation-guide.md +6 -5
- package/docs/environment-variables.md +2 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +14 -1
- package/docs/evidence-and-memory.md +74 -2
- 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 +22 -7
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +6 -6
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +7 -9
- package/docs/observability.md +4 -4
- package/docs/performance-methodology.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -4
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +35 -35
- package/docs/safety-model.md +23 -4
- package/docs/scientific-validation.md +21 -3
- package/docs/session-lifecycle.md +3 -3
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +79 -12
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +2 -2
- package/docs/worker-dispatch-mechanics.md +11 -1
- package/package.json +8 -11
- package/skills/meta/clio-test/SKILL.md +20 -17
- package/skills/meta/clio-test/evals.md +3 -3
- package/skills/meta/clio-test/references/harness.md +35 -6
- package/skills/meta/clio-test/references/test-map.md +20 -10
- package/skills/registry.yaml +2 -2
- package/skills/skill-marketplace.json +1 -1
- package/src/cli/context-working-set.ts +513 -0
- package/src/cli/context.ts +8 -0
- package/src/cli/evidence.ts +20 -2
- package/src/cli/index.ts +4 -0
- package/src/cli/verifiers.ts +325 -0
- package/src/core/bash-exec.ts +39 -14
- package/src/core/bus-events.ts +19 -4
- package/src/core/config.ts +54 -0
- package/src/core/defaults.ts +50 -3
- package/src/core/git-commit-attribution.ts +46 -21
- package/src/core/verification-scripts.ts +6 -0
- package/src/domains/agents/builtins/verifier.md +3 -0
- package/src/domains/config/classify.ts +1 -0
- package/src/domains/config/keybindings.ts +3 -3
- package/src/domains/context/working-set/contract.ts +161 -0
- package/src/domains/context/working-set/defaults.ts +28 -0
- package/src/domains/context/working-set/engine.ts +203 -0
- package/src/domains/context/working-set/fold.ts +62 -0
- package/src/domains/context/working-set/horizon.ts +38 -0
- package/src/domains/context/working-set/marker.ts +103 -0
- package/src/domains/context/working-set/path-index.ts +436 -0
- package/src/domains/context/working-set/payload.ts +152 -0
- package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
- package/src/domains/context/working-set/policies/index.ts +21 -0
- package/src/domains/context/working-set/policies/structural.ts +160 -0
- package/src/domains/context/working-set/project.ts +132 -0
- package/src/domains/context/working-set/protect.ts +109 -0
- package/src/domains/context/working-set/recall.ts +177 -0
- package/src/domains/context/working-set/replay/controls.ts +112 -0
- package/src/domains/context/working-set/replay/load-clio.ts +199 -0
- package/src/domains/context/working-set/replay/metrics.ts +185 -0
- package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
- package/src/domains/context/working-set/replay/report.ts +139 -0
- package/src/domains/context/working-set/replay/runner.ts +325 -0
- package/src/domains/context/working-set/replay/synthetic.ts +422 -0
- package/src/domains/context/working-set/replay/trace.ts +21 -0
- package/src/domains/context/working-set/visible.ts +54 -0
- package/src/domains/evidence/build.ts +112 -45
- package/src/domains/evidence/eval.ts +24 -7
- package/src/domains/evidence/index.ts +53 -0
- package/src/domains/evidence/ordering.ts +12 -0
- package/src/domains/evidence/run-trust.ts +221 -0
- package/src/domains/evidence/store.ts +46 -6
- package/src/domains/evidence/trust-status.ts +854 -0
- package/src/domains/evidence/types.ts +26 -0
- package/src/domains/middleware/memory-intervention.ts +3 -0
- package/src/domains/middleware/stalled-turn.ts +165 -4
- package/src/domains/safety/autonomy.ts +1 -1
- package/src/domains/safety/default-path-policy.ts +8 -0
- package/src/domains/safety/finish-contract.ts +4 -3
- package/src/domains/safety/policy-engine.ts +48 -6
- package/src/domains/session/compaction/compact.ts +23 -1
- package/src/domains/session/compaction/cut-point.ts +2 -0
- package/src/domains/session/compaction/tokens.ts +16 -1
- package/src/domains/session/context-ledger.ts +2 -0
- package/src/domains/session/entries.ts +107 -1
- package/src/domains/session/manager.ts +9 -2
- package/src/domains/session/migrations/index.ts +22 -3
- package/src/engine/acp/server.ts +3 -0
- package/src/engine/agent.ts +18 -1
- package/src/engine/session.ts +9 -3
- package/src/entry/orchestrator.ts +16 -4
- package/src/interactive/chat-loop-messages.ts +18 -6
- package/src/interactive/chat-panel.ts +571 -244
- package/src/interactive/chat-renderer.ts +79 -39
- package/src/interactive/context-meter.ts +10 -0
- package/src/interactive/context-overlay.ts +81 -6
- package/src/interactive/context-recall-command.ts +110 -0
- package/src/interactive/editor-submit.ts +26 -1
- package/src/interactive/footer/widgets.ts +22 -20
- package/src/interactive/footer-panel.ts +6 -1
- package/src/interactive/interactive-application.ts +2 -0
- package/src/interactive/interactive-event-projection.ts +12 -0
- package/src/interactive/interactive-slash-runtime.ts +49 -8
- package/src/interactive/model-session-replay.ts +21 -0
- package/src/interactive/overlay-general-openers.ts +6 -0
- package/src/interactive/overlay-session-lifecycle.ts +8 -4
- package/src/interactive/overlays/ask-user.ts +146 -24
- package/src/interactive/renderers/tool-execution.ts +167 -56
- package/src/interactive/session-transcript.ts +2 -2
- package/src/interactive/slash-commands.ts +29 -2
- package/src/interactive/status/index.ts +12 -1
- package/src/interactive/status/reasoning.ts +87 -0
- package/src/interactive/status/summary.ts +13 -2
- package/src/interactive/transcript-detail.ts +120 -0
- package/src/interactive/turn-context.ts +238 -88
- package/src/interactive/turn-middleware.ts +6 -6
- package/src/tools/agent-tools.ts +11 -4
- package/src/tools/bash.ts +144 -82
- package/src/tools/builtin-tool-catalog.ts +18 -6
- package/src/tools/context/index.ts +105 -3
- package/src/tools/context/surface.ts +3 -2
- package/src/tools/core-bootstrap.ts +21 -0
- package/src/tools/dispatch-runner.ts +9 -7
- package/src/tools/monitor.ts +28 -20
- package/src/tools/presentation.ts +107 -0
- package/src/tools/registry.ts +65 -7
- package/src/tools/result-disposition.ts +550 -0
- package/src/tools/result-shaping.ts +262 -19
- package/src/tools/safe-exec.ts +2 -0
- package/src/tools/verify/authoring.ts +1119 -0
- package/src/tools/verify/catalog.ts +346 -0
- package/src/tools/verify/index.ts +13 -3
- package/src/tools/verify/scripts.ts +135 -37
- package/src/tools/verify/surface.ts +9 -5
- package/src/tools/worker-evidence.ts +35 -12
- package/dist/chunk-MNA4JGU4.js +0 -255
- package/dist/chunk-SRF2PJNW.js +0 -184
- package/dist/chunk-T6YILFSB.js +0 -80
- package/dist/chunk-VAKQQHWR.js +0 -434
|
@@ -15,11 +15,15 @@
|
|
|
15
15
|
import { sanitizeCallTargetText } from "../../domains/safety/call-target.js";
|
|
16
16
|
import { formatSize } from "../../engine/truncate.js";
|
|
17
17
|
import { visibleWidth, wrapTextWithAnsi } from "../../engine/tui.js";
|
|
18
|
+
import { classifyResourceRead, toolPresentationPolicy } from "../../tools/presentation.js";
|
|
19
|
+
import { toolResultPresentationPolicy, toolResultPresentationText } from "../../tools/result-disposition.js";
|
|
18
20
|
import type { ApprovalRequestView } from "../permission-overlay.js";
|
|
19
21
|
import { clioTheme, formatCompactMs, GLYPH } from "../theme/index.js";
|
|
20
22
|
import { renderDiffLines } from "./diff.js";
|
|
21
23
|
import { tryRenderJson, tryRenderXml } from "./structured.js";
|
|
22
24
|
|
|
25
|
+
export { classifyResourceRead };
|
|
26
|
+
|
|
23
27
|
const theme = clioTheme();
|
|
24
28
|
const dim = (text: string): string => theme.fg("dim", text);
|
|
25
29
|
const red = (text: string): string => theme.fg("error", text);
|
|
@@ -40,6 +44,8 @@ const FULL_RESULT_PREVIEW_LIMIT = 60_000;
|
|
|
40
44
|
const FULL_RESULT_ROW_LIMIT = 120;
|
|
41
45
|
const STREAMING_RESULT_ROW_LIMIT = 20;
|
|
42
46
|
const ARGS_BODY_LINE_LIMIT = 24;
|
|
47
|
+
/** Rows of mutation diff a folded edit/write row keeps visible before it defers to the body. */
|
|
48
|
+
const FOLDED_DIFF_ROW_LIMIT = 24;
|
|
43
49
|
const STATUS_OK_GLYPH = GLYPH.ok;
|
|
44
50
|
const STATUS_ERROR_GLYPH = GLYPH.error;
|
|
45
51
|
|
|
@@ -76,6 +82,13 @@ export interface ToolExecutionFinished {
|
|
|
76
82
|
* model reading the same transcript, to guess at the rule.
|
|
77
83
|
*/
|
|
78
84
|
blockReason?: string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Working-set eviction reason, when the projection has replaced this
|
|
87
|
+
* result's body for the model. The transcript still renders the full body:
|
|
88
|
+
* the ledger is what the operator scrolls, the projection is only what the
|
|
89
|
+
* next request carries. The tag says the two now differ here.
|
|
90
|
+
*/
|
|
91
|
+
evictedReason?: string | undefined;
|
|
79
92
|
/** Structured exit status when the caller has one; text parsing is legacy fallback only. */
|
|
80
93
|
exitCode?: number | string | null | undefined;
|
|
81
94
|
/** Local `!!` bash output is visible to the operator but excluded from model context. */
|
|
@@ -286,24 +299,6 @@ function summarizeArgs(toolName: string, args: unknown): string {
|
|
|
286
299
|
return truncate(jsonStringifySafe(safeArgs), ARG_PREVIEW_LIMIT);
|
|
287
300
|
}
|
|
288
301
|
|
|
289
|
-
/**
|
|
290
|
-
* Compact resource-read classification. Reads of skill/handbook/agent
|
|
291
|
-
* instruction files and docs pages collapse to one labeled line and never
|
|
292
|
-
* auto-expand; their bodies are reference material, not task output.
|
|
293
|
-
*/
|
|
294
|
-
export function classifyResourceRead(toolName: string, args: unknown): string | null {
|
|
295
|
-
if (toolName !== "read") return null;
|
|
296
|
-
const path = readStringField(args, "path");
|
|
297
|
-
if (path === null) return null;
|
|
298
|
-
const normalized = path.replace(/\\/g, "/");
|
|
299
|
-
const base = normalized.split("/").pop() ?? "";
|
|
300
|
-
if (base === "SKILL.md") return "skill";
|
|
301
|
-
if (base === "CLIO-CODER.md") return "handbook";
|
|
302
|
-
if (base === "AGENTS.md") return "agents";
|
|
303
|
-
if (/(^|\/)docs\//.test(normalized)) return "docs";
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
302
|
function detailsOf(result: unknown): Record<string, unknown> | null {
|
|
308
303
|
if (!isPlainObject(result)) return null;
|
|
309
304
|
return isPlainObject(result.details) ? result.details : null;
|
|
@@ -448,15 +443,61 @@ function ledgerTail(finished: ToolExecutionFinished): { facts: string; offload:
|
|
|
448
443
|
const parts: string[] = [];
|
|
449
444
|
const outcome = outcomeSummary(finished);
|
|
450
445
|
if (outcome !== null) parts.push(outcome);
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
446
|
+
const executed = !isNonExecutedOutcome(finished.outcome);
|
|
447
|
+
// A folded bash row is the only view of the call the operator gets by
|
|
448
|
+
// default, so its settlement facts have to be here rather than only in the
|
|
449
|
+
// expanded body. A failed row already carries `(exit N)` on the status glyph.
|
|
450
|
+
if (executed && finished.toolName === "bash" && !finished.isError) {
|
|
451
|
+
parts.push(`exit ${structuredExitCode(finished) ?? "0"}`);
|
|
452
|
+
}
|
|
453
|
+
const bytes = executed ? shownBytesOf(finished) : null;
|
|
454
|
+
if (bytes !== null && bytes > 0) {
|
|
455
|
+
const total = totalBytesOf(finished);
|
|
456
|
+
parts.push(
|
|
457
|
+
total !== null && total > bytes ? `${formatSize(bytes)} shown / ${formatSize(total)} total` : formatSize(bytes),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (executed) {
|
|
461
|
+
if (isTruncatedResult(finished)) parts.push("truncated");
|
|
462
|
+
const details = detailsOf(finished.result);
|
|
463
|
+
if (details?.timedOut === true) parts.push("timed out");
|
|
464
|
+
if (details?.outputCapped === true) parts.push("output capped");
|
|
465
|
+
}
|
|
466
|
+
if (finished.excludeFromContext === true) parts.push("excluded from context");
|
|
467
|
+
if (finished.evictedReason !== undefined) parts.push("evicted", finished.evictedReason);
|
|
468
|
+
const offloadPath = executed ? offloadPathOf(finished) : null;
|
|
454
469
|
return {
|
|
455
470
|
facts: parts.length > 0 ? dim(` · ${parts.join(" · ")}`) : "",
|
|
456
471
|
offload: offloadPath !== null ? dim(` · full: ${offloadPath}`) : "",
|
|
457
472
|
};
|
|
458
473
|
}
|
|
459
474
|
|
|
475
|
+
/** Longest diagnostic excerpt a folded failure row may carry. */
|
|
476
|
+
const FAILURE_EXCERPT_LIMIT = 80;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Last non-empty output line of a failed call, bounded so the folded row stays
|
|
480
|
+
* diagnostically useful without opening the body. Sanitized to one line: the
|
|
481
|
+
* source is raw tool output. The tool's presentation policy decides whether
|
|
482
|
+
* its failures carry one; the renderer never names a tool here.
|
|
483
|
+
*/
|
|
484
|
+
function failureExcerpt(finished: ToolExecutionFinished, width: number): string {
|
|
485
|
+
const presentation =
|
|
486
|
+
toolResultPresentationPolicy(finished.result) ?? toolPresentationPolicy(finished.toolName, finished.args);
|
|
487
|
+
if (!finished.isError || !presentation.failureExcerpt) return "";
|
|
488
|
+
if (isNonExecutedOutcome(finished.outcome)) return "";
|
|
489
|
+
const text = unwrapResultEnvelope(finished.result);
|
|
490
|
+
if (typeof text !== "string") return "";
|
|
491
|
+
let excerpt: string | undefined;
|
|
492
|
+
for (const raw of text.split("\n")) {
|
|
493
|
+
const line = sanitizeCallTargetText(raw).trim();
|
|
494
|
+
if (line.length > 0) excerpt = line;
|
|
495
|
+
}
|
|
496
|
+
if (excerpt === undefined) return "";
|
|
497
|
+
const limit = Math.min(FAILURE_EXCERPT_LIMIT, Math.max(20, width - 20));
|
|
498
|
+
return dim(` · ${truncate(excerpt, limit)}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
460
501
|
/**
|
|
461
502
|
* Marker for a call parked at admission awaiting operator approval. Rendered
|
|
462
503
|
* in place of the elapsed counter so a parked call never reads as executing
|
|
@@ -531,8 +572,8 @@ function statusGlyph(status: HeaderStatus, meta: StatusMeta = {}): string {
|
|
|
531
572
|
}
|
|
532
573
|
|
|
533
574
|
function headerLine(toolName: string, args: unknown, status: HeaderStatus, meta: StatusMeta = {}): string {
|
|
534
|
-
const
|
|
535
|
-
const head = `${dim(HEADER_PREFIX_PLAIN)}${
|
|
575
|
+
const body = styleSublineBody(buildSublineBody(toolName, args, status, undefined, meta.outcome));
|
|
576
|
+
const head = `${dim(HEADER_PREFIX_PLAIN)}${body}`;
|
|
536
577
|
return `${head}${statusGlyph(status, meta)}`;
|
|
537
578
|
}
|
|
538
579
|
|
|
@@ -546,8 +587,9 @@ function styleSublineBody(body: string): string {
|
|
|
546
587
|
return sublineLead(match.groups.lead, match.groups.rest);
|
|
547
588
|
}
|
|
548
589
|
|
|
549
|
-
function
|
|
550
|
-
|
|
590
|
+
function buildGenericToolBody(toolName: string, args: unknown): string {
|
|
591
|
+
const summary = summarizeArgs(toolName, args);
|
|
592
|
+
return summary.length > 0 ? `tool action ${summary}` : "tool action";
|
|
551
593
|
}
|
|
552
594
|
|
|
553
595
|
function buildFieldSublineBody(
|
|
@@ -604,6 +646,7 @@ const SUBLINE_BODY_BUILDERS: Readonly<Record<string, (args: unknown) => string |
|
|
|
604
646
|
bash: (args) => buildFieldSublineBody(args, "command", "running ", { wrapInBackticks: true }),
|
|
605
647
|
grep: (args) => buildFieldSublineBody(args, "pattern", "searching for ", { wrapInBackticks: true }),
|
|
606
648
|
find: (args) => buildFieldSublineBody(args, "pattern", "finding ", { wrapInBackticks: true }),
|
|
649
|
+
web_fetch: (args) => buildFieldSublineBody(args, "url", "fetching "),
|
|
607
650
|
git: (args) => buildFieldSublineBody(args, "op", "git "),
|
|
608
651
|
verify: (args) => buildFieldSublineBody(args, "check", "verifying "),
|
|
609
652
|
code_nav: (args) => {
|
|
@@ -641,7 +684,7 @@ const SUBLINE_BODY_BUILDERS: Readonly<Record<string, (args: unknown) => string |
|
|
|
641
684
|
/**
|
|
642
685
|
* Per-tool subline templates. Maps a tool name to a function that builds the
|
|
643
686
|
* verb-led subline body without the leading glyph and without the trailing
|
|
644
|
-
* status glyph. Unknown tools fall back to
|
|
687
|
+
* status glyph. Unknown tools fall back to a tool-neutral action summary.
|
|
645
688
|
*/
|
|
646
689
|
function webFetchMeta(result: unknown): string | null {
|
|
647
690
|
if (!isPlainObject(result) || !isPlainObject(result.details)) return null;
|
|
@@ -658,11 +701,10 @@ function webFetchMeta(result: unknown): string | null {
|
|
|
658
701
|
|
|
659
702
|
/**
|
|
660
703
|
* A call the permission gate blocked never executed, so the collapsed row must
|
|
661
|
-
* not claim it did.
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
* output.
|
|
704
|
+
* not claim it did. Blocked calls keep the ordinary operator-facing action
|
|
705
|
+
* description but use a neutral noun for commands; the status tail supplies the
|
|
706
|
+
* blocked outcome. The ledger byte count is suppressed elsewhere because those
|
|
707
|
+
* bytes are the denial text, not output.
|
|
666
708
|
*/
|
|
667
709
|
function isNonExecutedOutcome(outcome: ToolExecutionFinished["outcome"]): boolean {
|
|
668
710
|
return outcome === "blocked";
|
|
@@ -675,21 +717,29 @@ function buildSublineBody(
|
|
|
675
717
|
result?: unknown,
|
|
676
718
|
outcome?: ToolExecutionFinished["outcome"],
|
|
677
719
|
): string {
|
|
678
|
-
if (isNonExecutedOutcome(outcome))
|
|
679
|
-
|
|
720
|
+
if (isNonExecutedOutcome(outcome)) {
|
|
721
|
+
if (toolName === "bash") {
|
|
722
|
+
return (
|
|
723
|
+
buildFieldSublineBody(args, "command", "command ", { wrapInBackticks: true }) ??
|
|
724
|
+
buildGenericToolBody(toolName, args)
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
return SUBLINE_BODY_BUILDERS[toolName]?.(args) ?? buildGenericToolBody(toolName, args);
|
|
728
|
+
}
|
|
680
729
|
if (toolName === "web_fetch") {
|
|
681
730
|
const meta = status === undefined ? null : webFetchMeta(result);
|
|
682
|
-
|
|
731
|
+
const body = SUBLINE_BODY_BUILDERS.web_fetch?.(args) ?? buildGenericToolBody(toolName, args);
|
|
732
|
+
return `${body}${meta ? dim(` · ${meta}`) : ""}`;
|
|
683
733
|
}
|
|
684
734
|
if (toolName === "bash") {
|
|
685
|
-
const lead = status === "
|
|
735
|
+
const lead = status === "ok" || status === "error" ? "ran " : "running ";
|
|
686
736
|
return (
|
|
687
|
-
buildFieldSublineBody(args, "command", lead, { wrapInBackticks: true }) ??
|
|
737
|
+
buildFieldSublineBody(args, "command", lead, { wrapInBackticks: true }) ?? buildGenericToolBody(toolName, args)
|
|
688
738
|
);
|
|
689
739
|
}
|
|
690
740
|
const body = SUBLINE_BODY_BUILDERS[toolName]?.(args);
|
|
691
741
|
if (body !== null && body !== undefined) return body;
|
|
692
|
-
return
|
|
742
|
+
return buildGenericToolBody(toolName, args);
|
|
693
743
|
}
|
|
694
744
|
|
|
695
745
|
interface SublineParts {
|
|
@@ -832,6 +882,8 @@ function renderArgsBody(toolName: string, args: unknown, width: number, isError:
|
|
|
832
882
|
*/
|
|
833
883
|
function unwrapResultEnvelope(result: unknown): unknown {
|
|
834
884
|
if (typeof result === "string" || result === null || result === undefined) return result;
|
|
885
|
+
const presentationText = toolResultPresentationText(result);
|
|
886
|
+
if (presentationText !== null) return presentationText;
|
|
835
887
|
const blocks = Array.isArray(result)
|
|
836
888
|
? result
|
|
837
889
|
: isPlainObject(result) && Array.isArray(result.content)
|
|
@@ -1002,6 +1054,7 @@ function outputFacts(finished: ToolExecutionFinished): string[] {
|
|
|
1002
1054
|
}
|
|
1003
1055
|
if (isPlainObject(finished.result) && finished.result.terminate === true) parts.push("terminal result");
|
|
1004
1056
|
if (finished.excludeFromContext === true) parts.push("excluded from context");
|
|
1057
|
+
if (finished.evictedReason !== undefined) parts.push("evicted", finished.evictedReason);
|
|
1005
1058
|
return parts;
|
|
1006
1059
|
}
|
|
1007
1060
|
|
|
@@ -1116,10 +1169,42 @@ function sublineStatus(call: ToolExecutionStart | ToolExecutionFinished): Header
|
|
|
1116
1169
|
* imports the keybindings manager directly; the caller resolves the key string
|
|
1117
1170
|
* and passes it in to keep this module pure.
|
|
1118
1171
|
*/
|
|
1172
|
+
export interface ToolSublineRenderOptions {
|
|
1173
|
+
/** Live rows color the folded mutation diff; replay and export request plain rows. */
|
|
1174
|
+
diffStyle?: "color" | "plain";
|
|
1175
|
+
/**
|
|
1176
|
+
* Whether the folded row carries the extras its tool's presentation asks
|
|
1177
|
+
* for (today the bounded mutation diff). The bare subline level drops them;
|
|
1178
|
+
* the failure excerpt is not one of them and always rides the row.
|
|
1179
|
+
*/
|
|
1180
|
+
foldedExtras?: "per-tool" | "none";
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* The bounded diff a folded mutation row keeps under itself, when the tool's
|
|
1185
|
+
* presentation asks for one and the result carries one. A folded `edit` that
|
|
1186
|
+
* hid its change told the operator nothing they could act on.
|
|
1187
|
+
*/
|
|
1188
|
+
function foldedDiffRows(finished: ToolExecutionFinished, width: number, opts: ToolSublineRenderOptions): string[] {
|
|
1189
|
+
if (opts.foldedExtras === "none") return [];
|
|
1190
|
+
if (finished.isError || isNonExecutedOutcome(finished.outcome)) return [];
|
|
1191
|
+
const presentation =
|
|
1192
|
+
toolResultPresentationPolicy(finished.result) ?? toolPresentationPolicy(finished.toolName, finished.args);
|
|
1193
|
+
if (!presentation.showDiffWhenFolded) return [];
|
|
1194
|
+
const diff = resultDiff(finished.result);
|
|
1195
|
+
if (diff === null) return [];
|
|
1196
|
+
return truncateRowsMiddle(
|
|
1197
|
+
renderMutationDiffBlock(diff, width, opts.diffStyle !== "plain"),
|
|
1198
|
+
FOLDED_DIFF_ROW_LIMIT,
|
|
1199
|
+
false,
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1119
1203
|
export function renderToolSubline(
|
|
1120
1204
|
call: ToolExecutionStart | ToolExecutionFinished,
|
|
1121
1205
|
width: number,
|
|
1122
1206
|
expandKey?: string,
|
|
1207
|
+
opts: ToolSublineRenderOptions = {},
|
|
1123
1208
|
): string[] {
|
|
1124
1209
|
const status = sublineStatus(call);
|
|
1125
1210
|
const meta: StatusMeta =
|
|
@@ -1132,9 +1217,12 @@ export function renderToolSubline(
|
|
|
1132
1217
|
}
|
|
1133
1218
|
: { elapsedMs: call.elapsedMs };
|
|
1134
1219
|
const parts = sublineParts(call, status, meta);
|
|
1220
|
+
const excerpt = "result" in call ? failureExcerpt(call, width) : "";
|
|
1135
1221
|
const showHint = "result" in call && expandKey !== undefined && expandKey.length > 0;
|
|
1136
1222
|
const tail = showHint ? `${parts.tail}${dim(` (${expandKey})`)}` : parts.tail;
|
|
1137
|
-
|
|
1223
|
+
const lines = wrapSublineWithTail(`${parts.lead}${excerpt}`, tail, width);
|
|
1224
|
+
if ("result" in call) lines.push(...foldedDiffRows(call, width, opts));
|
|
1225
|
+
return lines;
|
|
1138
1226
|
}
|
|
1139
1227
|
|
|
1140
1228
|
/**
|
|
@@ -1294,6 +1382,13 @@ export interface BashTranscriptExecution {
|
|
|
1294
1382
|
fullOutputPath?: string | undefined;
|
|
1295
1383
|
excludeFromContext?: boolean | undefined;
|
|
1296
1384
|
error?: string | undefined;
|
|
1385
|
+
/**
|
|
1386
|
+
* Whether the block draws its one-line row instead of the full body. The
|
|
1387
|
+
* caller resolves this from the transcript detail policy and the operator's
|
|
1388
|
+
* override, the same way the panel resolves a model bash call. Omitted means
|
|
1389
|
+
* folded.
|
|
1390
|
+
*/
|
|
1391
|
+
folded?: boolean | undefined;
|
|
1297
1392
|
}
|
|
1298
1393
|
|
|
1299
1394
|
/**
|
|
@@ -1301,7 +1396,12 @@ export interface BashTranscriptExecution {
|
|
|
1301
1396
|
* as model-initiated bash. This is a view projection only; callers keep Clio's
|
|
1302
1397
|
* existing immutable `bashExecution` ledger entry.
|
|
1303
1398
|
*/
|
|
1304
|
-
export function renderBashTranscriptExecution(
|
|
1399
|
+
export function renderBashTranscriptExecution(
|
|
1400
|
+
execution: BashTranscriptExecution,
|
|
1401
|
+
width: number,
|
|
1402
|
+
expandKey?: string,
|
|
1403
|
+
bodyOptions: ToolBodyRenderOptions = {},
|
|
1404
|
+
): string[] {
|
|
1305
1405
|
const shownBytes = Buffer.byteLength(execution.output, "utf8");
|
|
1306
1406
|
const totalBytes = execution.totalBytes ?? shownBytes;
|
|
1307
1407
|
const args: Record<string, unknown> = { command: execution.command };
|
|
@@ -1319,7 +1419,20 @@ export function renderBashTranscriptExecution(execution: BashTranscriptExecution
|
|
|
1319
1419
|
content: [{ type: "text", text: execution.output }],
|
|
1320
1420
|
details,
|
|
1321
1421
|
};
|
|
1422
|
+
const folded = execution.folded !== false;
|
|
1322
1423
|
if (execution.running) {
|
|
1424
|
+
if (folded) {
|
|
1425
|
+
return renderToolSubline(
|
|
1426
|
+
{
|
|
1427
|
+
toolCallId: "local-bash",
|
|
1428
|
+
toolName: "bash",
|
|
1429
|
+
args,
|
|
1430
|
+
elapsedMs: execution.elapsedMs,
|
|
1431
|
+
phase: "running",
|
|
1432
|
+
},
|
|
1433
|
+
width,
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1323
1436
|
return renderToolStreamingExecution(
|
|
1324
1437
|
{
|
|
1325
1438
|
toolCallId: "local-bash",
|
|
@@ -1336,22 +1449,20 @@ export function renderBashTranscriptExecution(execution: BashTranscriptExecution
|
|
|
1336
1449
|
const output = message
|
|
1337
1450
|
? `${execution.output}${execution.output.length > 0 ? "\n\n" : ""}${message}`
|
|
1338
1451
|
: execution.output;
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
...(execution.fullOutputPath !== undefined ? { offloadPath: execution.fullOutputPath } : {}),
|
|
1353
|
-
},
|
|
1452
|
+
const finished: ToolExecutionFinished = {
|
|
1453
|
+
toolCallId: "local-bash",
|
|
1454
|
+
toolName: "bash",
|
|
1455
|
+
args,
|
|
1456
|
+
result: { ...result, content: [{ type: "text", text: output }] },
|
|
1457
|
+
isError: execution.cancelled === true || execution.error !== undefined || (execution.exitCode ?? 0) !== 0,
|
|
1458
|
+
exitCode: execution.exitCode,
|
|
1459
|
+
...(execution.cancelled === true ? { outcome: "aborted" as const } : {}),
|
|
1460
|
+
excludeFromContext: execution.excludeFromContext,
|
|
1461
|
+
resultSummary: {
|
|
1462
|
+
bytes: shownBytes,
|
|
1463
|
+
truncated: execution.truncated === true,
|
|
1464
|
+
...(execution.fullOutputPath !== undefined ? { offloadPath: execution.fullOutputPath } : {}),
|
|
1354
1465
|
},
|
|
1355
|
-
|
|
1356
|
-
);
|
|
1466
|
+
};
|
|
1467
|
+
return folded ? renderToolSubline(finished, width, expandKey) : renderToolExecution(finished, width, bodyOptions);
|
|
1357
1468
|
}
|
|
@@ -3,7 +3,7 @@ import { collectSessionEntries } from "../domains/session/compaction/session-ent
|
|
|
3
3
|
import type { SessionContract, SessionEntry } from "../domains/session/index.js";
|
|
4
4
|
import { openSession, sessionPaths } from "../engine/session.js";
|
|
5
5
|
import type { ChatLoop } from "./chat-loop.js";
|
|
6
|
-
import {
|
|
6
|
+
import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
|
|
7
7
|
|
|
8
8
|
type SessionOwner = Pick<SessionContract, "create" | "current">;
|
|
9
9
|
type SessionChat = Pick<ChatLoop, "resetForSession">;
|
|
@@ -87,7 +87,7 @@ export function createSessionTranscript(deps: SessionTranscriptDeps): SessionTra
|
|
|
87
87
|
const turns = deps.readSessionEntries();
|
|
88
88
|
deps.chat.resetForSession(
|
|
89
89
|
leafTurnId,
|
|
90
|
-
|
|
90
|
+
buildModelReplayAgentMessagesFromTurns(turns, {
|
|
91
91
|
...(leafTurnId ? { activeLeafTurnId: leafTurnId } : {}),
|
|
92
92
|
}),
|
|
93
93
|
);
|
|
@@ -44,6 +44,8 @@ type SlashCommandVariant =
|
|
|
44
44
|
| { kind: "init"; options: InitCommandOptions }
|
|
45
45
|
| { kind: "context-clear"; options: ContextClearCommandOptions }
|
|
46
46
|
| { kind: "context-refresh" }
|
|
47
|
+
/** `ref` is the turnId an `[evicted ...]` marker names. */
|
|
48
|
+
| { kind: "context-recall"; ref: string }
|
|
47
49
|
| { kind: "skill-selector" }
|
|
48
50
|
| { kind: "skill-invocation"; text: string }
|
|
49
51
|
| { kind: "prompts" }
|
|
@@ -343,6 +345,13 @@ export interface SlashCommandContext {
|
|
|
343
345
|
* Optional until the host wires onContextRefresh.
|
|
344
346
|
*/
|
|
345
347
|
runContextRefresh?: () => void;
|
|
348
|
+
/**
|
|
349
|
+
* Read an evicted tool-result body back into the transcript by ref, and
|
|
350
|
+
* record the `contextRecall` entry. Transcript-only: an operator recall
|
|
351
|
+
* answers the person, so the body never becomes model context. Optional
|
|
352
|
+
* until the host wires a session.
|
|
353
|
+
*/
|
|
354
|
+
runContextRecall?: (ref: string) => void;
|
|
346
355
|
openSkillsHub?: () => void;
|
|
347
356
|
listPrompts: () => ResourceList<PromptTemplate>;
|
|
348
357
|
/**
|
|
@@ -481,6 +490,13 @@ const COMPACT_POSITIONALS: ReadonlyArray<CommandPositionalSpec> = [
|
|
|
481
490
|
{ name: "instructions", required: false, rest: true },
|
|
482
491
|
];
|
|
483
492
|
|
|
493
|
+
/**
|
|
494
|
+
* `/context recall <ref>` takes exactly one ref, not a rest tail: a ref is a
|
|
495
|
+
* single turnId, and a second token is a mistake worth reporting rather than
|
|
496
|
+
* text to fold into the first.
|
|
497
|
+
*/
|
|
498
|
+
const RECALL_POSITIONALS: ReadonlyArray<CommandPositionalSpec> = [{ name: "ref", required: true }];
|
|
499
|
+
|
|
484
500
|
/**
|
|
485
501
|
* A registered command whose arguments do not parse is a mistake to report,
|
|
486
502
|
* not a chat message to send. Falling through to `unknown` submitted the raw
|
|
@@ -859,11 +875,12 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
|
859
875
|
},
|
|
860
876
|
{
|
|
861
877
|
name: "context",
|
|
862
|
-
description: "Context hub: window overlay plus compact, init, refresh, and reset",
|
|
878
|
+
description: "Context hub: window overlay plus compact, recall, init, refresh, and reset",
|
|
863
879
|
group: "Inspect",
|
|
864
|
-
kinds: ["context-view", "compact", "init", "context-clear", "context-refresh"],
|
|
880
|
+
kinds: ["context-view", "compact", "context-recall", "init", "context-clear", "context-refresh"],
|
|
865
881
|
subcommandDescriptions: {
|
|
866
882
|
compact: "Compact session context",
|
|
883
|
+
recall: "Recall an evicted result",
|
|
867
884
|
init: "Initialize project context",
|
|
868
885
|
refresh: "Refresh project context",
|
|
869
886
|
reset: "Reset project context",
|
|
@@ -871,6 +888,7 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
|
871
888
|
args: {
|
|
872
889
|
subcommands: {
|
|
873
890
|
compact: { positionals: [...COMPACT_POSITIONALS] },
|
|
891
|
+
recall: { positionals: [...RECALL_POSITIONALS] },
|
|
874
892
|
init: {},
|
|
875
893
|
refresh: {},
|
|
876
894
|
reset: {},
|
|
@@ -881,6 +899,8 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
|
881
899
|
switch (parsed.subcommand) {
|
|
882
900
|
case "compact":
|
|
883
901
|
return { kind: "compact", instructions: parsed.rest };
|
|
902
|
+
case "recall":
|
|
903
|
+
return { kind: "context-recall", ref: parsed.positionals[0] ?? "" };
|
|
884
904
|
case "init":
|
|
885
905
|
return { kind: "init", options: {} };
|
|
886
906
|
case "refresh":
|
|
@@ -899,6 +919,13 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
|
|
|
899
919
|
case "compact":
|
|
900
920
|
ctx.runCompact(command.instructions);
|
|
901
921
|
return;
|
|
922
|
+
case "context-recall":
|
|
923
|
+
if (ctx.runContextRecall) {
|
|
924
|
+
ctx.runContextRecall(command.ref);
|
|
925
|
+
} else {
|
|
926
|
+
ctx.notice("error", "context recall is not wired; no session is bound to this process");
|
|
927
|
+
}
|
|
928
|
+
return;
|
|
902
929
|
case "init":
|
|
903
930
|
ctx.runInit(command.options);
|
|
904
931
|
return;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
export type { StatusController, StatusControllerDeps } from "./controller.js";
|
|
2
2
|
export { createStatusController } from "./controller.js";
|
|
3
|
+
export {
|
|
4
|
+
compactReasoningTokens,
|
|
5
|
+
formatReasoningChip,
|
|
6
|
+
formatReasoningLabel,
|
|
7
|
+
type ReasoningProvenance,
|
|
8
|
+
type ReasoningUsageView,
|
|
9
|
+
reasoningFromSummary,
|
|
10
|
+
reasoningFromTally,
|
|
11
|
+
UNMEASURED_REASONING,
|
|
12
|
+
} from "./reasoning.js";
|
|
3
13
|
export { type ReduceContext, reduceStatus, type StatusInputEvent } from "./state-machine.js";
|
|
4
|
-
export { type BuildSummaryInput, buildSummary } from "./summary.js";
|
|
14
|
+
export { type BuildSummaryInput, buildSummary, emptyRunTally, foldMessageIntoRunTally } from "./summary.js";
|
|
5
15
|
export {
|
|
6
16
|
type AgentStatus,
|
|
7
17
|
type AgentStatusChangedPayload,
|
|
8
18
|
type AgentStatusEvent,
|
|
9
19
|
INITIAL_STATUS,
|
|
10
20
|
type ReasoningTokenProvenance,
|
|
21
|
+
type RunTally,
|
|
11
22
|
type StatusPhase,
|
|
12
23
|
type TurnStopReason,
|
|
13
24
|
type TurnSummary,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReasoningTokenProvenance, RunTally, TurnSummary } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One projection of a turn's reasoning spend for every surface that shows it.
|
|
5
|
+
*
|
|
6
|
+
* Reasoning counts used to be derived in three places (the status tally, the
|
|
7
|
+
* chat panel's own `message_end` re-derivation, and a chars/4 estimate over the
|
|
8
|
+
* visible thinking text), so the transcript, the turn receipt, and the footer
|
|
9
|
+
* could each report a different number for the same turn. Everything now reads
|
|
10
|
+
* `RunTally`/`TurnSummary` through this module, so a surface can differ in
|
|
11
|
+
* layout but never in the number or its provenance.
|
|
12
|
+
*
|
|
13
|
+
* `unmeasured` is a view-only state: nothing has been folded yet, so the count
|
|
14
|
+
* is unknown rather than zero. It never reaches `TurnSummary`, which keeps its
|
|
15
|
+
* persisted `provider | estimated | mixed` vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
export type ReasoningProvenance = ReasoningTokenProvenance | "unmeasured";
|
|
18
|
+
|
|
19
|
+
export interface ReasoningUsageView {
|
|
20
|
+
tokens: number;
|
|
21
|
+
provenance: ReasoningProvenance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const UNMEASURED_REASONING: ReasoningUsageView = { tokens: 0, provenance: "unmeasured" };
|
|
25
|
+
|
|
26
|
+
function provenanceOf(hadProvider: boolean, hadEstimated: boolean): ReasoningProvenance {
|
|
27
|
+
if (hadProvider && hadEstimated) return "mixed";
|
|
28
|
+
if (hadProvider) return "provider";
|
|
29
|
+
if (hadEstimated) return "estimated";
|
|
30
|
+
return "unmeasured";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function finiteTokens(value: unknown): number {
|
|
34
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Live view of the in-flight turn, folded message by message as the run settles. */
|
|
38
|
+
export function reasoningFromTally(tally: RunTally | undefined): ReasoningUsageView {
|
|
39
|
+
if (!tally) return UNMEASURED_REASONING;
|
|
40
|
+
const provenance = provenanceOf(tally.hadProviderReasoning === true, tally.hadEstimatedReasoning === true);
|
|
41
|
+
if (provenance === "unmeasured") return UNMEASURED_REASONING;
|
|
42
|
+
return { tokens: finiteTokens(tally.reasoningTokens), provenance };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Settled view of a turn, including one replayed from a persisted summary. */
|
|
46
|
+
export function reasoningFromSummary(summary: TurnSummary | undefined): ReasoningUsageView {
|
|
47
|
+
if (!summary || typeof summary.reasoningTokens !== "number") return UNMEASURED_REASONING;
|
|
48
|
+
// A summary persisted before provenance was recorded carries a count and no
|
|
49
|
+
// label. `summaryFromRunTally` writes both together, so this only covers
|
|
50
|
+
// replayed history, where the count came from provider usage.
|
|
51
|
+
return { tokens: finiteTokens(summary.reasoningTokens), provenance: summary.reasoningTokenProvenance ?? "provider" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compact token text for the surfaces that have no formatter of their own. The
|
|
56
|
+
* footer passes its own `formatFooterTokens` so its chips keep the width
|
|
57
|
+
* budget the rest of the footer is measured against.
|
|
58
|
+
*/
|
|
59
|
+
export function compactReasoningTokens(value: number): string {
|
|
60
|
+
if (value < 1000) return String(Math.round(value));
|
|
61
|
+
if (value < 1_000_000) {
|
|
62
|
+
const scaled = (value / 1000).toFixed(1);
|
|
63
|
+
return `${scaled.endsWith(".0") ? scaled.slice(0, -2) : scaled}k`;
|
|
64
|
+
}
|
|
65
|
+
const scaled = (value / 1_000_000).toFixed(1);
|
|
66
|
+
return `${scaled.endsWith(".0") ? scaled.slice(0, -2) : scaled}M`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `r123` for a provider-attested count, `r≈123` for anything Clio inferred.
|
|
71
|
+
* Null when there is nothing to state: an unmeasured turn, or one that spent no
|
|
72
|
+
* reasoning tokens at all (a chip reading `r0` names the provenance of zero).
|
|
73
|
+
*/
|
|
74
|
+
export function formatReasoningChip(
|
|
75
|
+
view: ReasoningUsageView,
|
|
76
|
+
format: (value: number) => string = compactReasoningTokens,
|
|
77
|
+
): string | null {
|
|
78
|
+
if (view.provenance === "unmeasured" || view.tokens <= 0) return null;
|
|
79
|
+
return `r${view.provenance === "provider" ? "" : "≈"}${format(view.tokens)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatReasoningLabel(view: ReasoningUsageView): string {
|
|
83
|
+
if (view.provenance === "provider") return "provider-reported";
|
|
84
|
+
if (view.provenance === "estimated") return "estimated";
|
|
85
|
+
if (view.provenance === "mixed") return "mixed";
|
|
86
|
+
return "unmeasured";
|
|
87
|
+
}
|
|
@@ -18,6 +18,7 @@ interface UsageLike {
|
|
|
18
18
|
output?: number;
|
|
19
19
|
cacheRead?: number;
|
|
20
20
|
cacheWrite?: number;
|
|
21
|
+
estimated?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function assistantThinkingText(message: AgentMessage): string {
|
|
@@ -91,7 +92,10 @@ export function foldMessageIntoRunTally(tally: RunTally, message: AgentMessage):
|
|
|
91
92
|
cacheReadTokens: tally.cacheReadTokens + (usage ? finite(usage.cacheRead) : 0),
|
|
92
93
|
cacheWriteTokens: tally.cacheWriteTokens + (usage ? finite(usage.cacheWrite) : 0),
|
|
93
94
|
};
|
|
94
|
-
|
|
95
|
+
// Interrupted turns carry Clio's own estimated usage object. It retains the
|
|
96
|
+
// completed-record shape, including `reasoning: 0`, but that zero is not a
|
|
97
|
+
// provider attestation and must not suppress the thinking-text fallback.
|
|
98
|
+
const reasoning = usage && usage.estimated !== true ? extractReasoningTokens(usage) : null;
|
|
95
99
|
if (reasoning !== null) {
|
|
96
100
|
next.reasoningTokens += reasoning;
|
|
97
101
|
next.hadProviderReasoning = true;
|
|
@@ -102,7 +106,14 @@ export function foldMessageIntoRunTally(tally: RunTally, message: AgentMessage):
|
|
|
102
106
|
// reported as mixed rather than silently dropping the unreported block.
|
|
103
107
|
const estimated = estimateReasoningTextTokens(assistantThinkingText(message));
|
|
104
108
|
if (estimated !== null) {
|
|
105
|
-
|
|
109
|
+
// A chars/4 estimate over displayed thinking text can outrun what the
|
|
110
|
+
// provider says the call generated (summarized reasoning, a rail that
|
|
111
|
+
// re-renders the same block). Reported output is the ceiling for anything
|
|
112
|
+
// inferred: reasoning is part of that output, never more than it. No
|
|
113
|
+
// clamp exists upstream in the adapters, so it lives here, once.
|
|
114
|
+
const reportedOutput =
|
|
115
|
+
usage && typeof usage.output === "number" && Number.isFinite(usage.output) ? Math.max(0, usage.output) : null;
|
|
116
|
+
next.reasoningTokens += reportedOutput === null ? estimated : Math.min(estimated, reportedOutput);
|
|
106
117
|
next.hadEstimatedReasoning = true;
|
|
107
118
|
}
|
|
108
119
|
return next;
|