@iowarp/clio-coder 0.3.3 → 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 +74 -0
- package/CONTRIBUTING.md +7 -7
- package/README.md +3 -3
- package/dist/{acp-P2AQILE2.js → acp-2BEHC4DL.js} +9 -8
- package/dist/{agents-72W3BI7I.js → agents-LNNFTM53.js} +29 -24
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-5TWEIYDN.js → auth-KXXFI2VS.js} +14 -10
- package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
- 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-43AOLP7E.js +375 -0
- package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
- package/dist/{chunk-6SGHMWE3.js → chunk-5JGRAMKL.js} +5 -5
- package/dist/{chunk-V6RTAOC2.js → chunk-6US73PDB.js} +572 -51
- package/dist/{chunk-5UFT4SUX.js → chunk-6XXKFVSN.js} +3 -3
- package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
- package/dist/chunk-AOCYTWAV.js +449 -0
- package/dist/chunk-CFGTUFWB.js +67 -0
- package/dist/chunk-CJUB2JJ2.js +1478 -0
- package/dist/{chunk-FNTMWMX5.js → chunk-CKXWIANG.js} +14 -12
- package/dist/{chunk-PIWWS5BL.js → chunk-CYQKWTG3.js} +63 -78
- package/dist/{chunk-LZSJBIVT.js → chunk-DJVECN66.js} +271 -762
- package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
- package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
- package/dist/{chunk-STBPMHSX.js → chunk-EKY57CSP.js} +51 -84
- package/dist/{chunk-DUYJ5IO6.js → chunk-EYPA3EGJ.js} +12 -4
- package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
- package/dist/chunk-FYYLNIL5.js +313 -0
- package/dist/{chunk-OQ33BKR3.js → chunk-G7MUEIGA.js} +3 -60
- package/dist/chunk-GEYXPTRF.js +613 -0
- package/dist/chunk-GOXNB3AO.js +261 -0
- package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
- package/dist/chunk-HWUFFB6L.js +83 -0
- package/dist/{chunk-4XUGQOHA.js → chunk-K7T3E2SR.js} +15 -8
- package/dist/chunk-K7VKOLQQ.js +15 -0
- package/dist/{chunk-UFIIWP2H.js → chunk-KHSFENX2.js} +8 -8
- package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
- package/dist/chunk-LCGCVYZ4.js +57 -0
- package/dist/chunk-LL4KHSZI.js +22 -0
- package/dist/{chunk-PAJK6MAQ.js → chunk-LYF7OHWH.js} +42 -15
- package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
- package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
- package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
- package/dist/{chunk-TZK7PACC.js → chunk-NILBFAPG.js} +14 -8
- package/dist/chunk-ODFEOB4F.js +1082 -0
- package/dist/{chunk-AGYYIBLL.js → chunk-OH3TOQTB.js} +6 -2
- package/dist/chunk-OZNBF4L3.js +23 -0
- package/dist/{chunk-DSELYM6W.js → chunk-PBTHKCPN.js} +30 -10
- package/dist/{verify-375KUB3Y.js → chunk-PCZJO5TI.js} +127 -42
- package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
- package/dist/{chunk-SRDMMSEP.js → chunk-QM3F2GKX.js} +1063 -1645
- package/dist/{chunk-X6IAEBZR.js → chunk-QNQHSOLF.js} +7 -7
- package/dist/{chunk-OC7FIQPC.js → chunk-R46L2BIR.js} +10 -7
- package/dist/{chunk-2TLUCQVG.js → chunk-RD5U66HV.js} +3 -3
- package/dist/{chunk-6N5PTWMY.js → chunk-RY3LY4J5.js} +50 -13
- package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
- package/dist/{chunk-OOJYHWRB.js → chunk-TSHXZTOQ.js} +6 -5
- package/dist/chunk-TZSKNMZG.js +434 -0
- package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
- package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
- package/dist/{chunk-COU2UHX6.js → chunk-VEZEGCGW.js} +170 -2
- package/dist/chunk-W6GROXXM.js +69 -0
- package/dist/{chunk-OAO4GE4M.js → chunk-WHGPSPT5.js} +2 -2
- package/dist/chunk-WHJYKASB.js +677 -0
- package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
- package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
- package/dist/{chunk-TZTZS7QK.js → chunk-XE2VEJHX.js} +5 -3
- package/dist/{chunk-LM5TQCJZ.js → chunk-XF5N4U5A.js} +8 -7
- package/dist/{chunk-LWLEKMDQ.js → chunk-XXQNGV4M.js} +1073 -552
- package/dist/{chunk-KZWTDYJF.js → chunk-XYDYPRZI.js} +7 -7
- package/dist/chunk-ZGVHUX3M.js +66 -0
- package/dist/{chunk-LW6DSM3M.js → chunk-ZRGEBJ4T.js} +1192 -1119
- package/dist/{chunk-2DJ2KNFG.js → chunk-ZXF4XRKW.js} +202 -40
- package/dist/chunk-ZZMN5OM4.js +122 -0
- package/dist/cli/index.js +34 -30
- package/dist/{clio-JOU4FXVA.js → clio-M2KGYUFZ.js} +7 -6
- package/dist/{code-nav-7AX6FYE6.js → code-nav-GQNL7XA6.js} +8 -6
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
- package/dist/{config-XCDVKR23.js → config-XUUYQIWO.js} +47 -35
- package/dist/{configure-4GAP54ZW.js → configure-IHJ7YOMV.js} +18 -15
- package/dist/{context-77FM5DV5.js → context-74JLXAWD.js} +18 -10
- package/dist/{context-4UOGGLQ5.js → context-75MIWW3U.js} +41 -29
- package/dist/{context-5VKGUVJJ.js → context-ZQ7SIFJV.js} +85 -9
- package/dist/{context-clear-XXJRLCJJ.js → context-clear-GYKWNUML.js} +41 -29
- package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
- package/dist/context-working-set-UX5KEP4J.js +1553 -0
- package/dist/{dispatch-runner-QPRDDBDX.js → dispatch-runner-GIJBHNFL.js} +47 -32
- package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
- package/dist/{doctor-HR46URBJ.js → doctor-SVJ5BZCW.js} +12 -12
- package/dist/{eval-XSSNATB4.js → eval-CG6LLBLD.js} +54 -238
- package/dist/{evidence-6HG2PY2B.js → evidence-ZYFIEN42.js} +57 -28
- package/dist/{evolve-K7YU3NCY.js → evolve-QGEXEMDW.js} +36 -25
- package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
- package/dist/{fleet-VY3HHKN6.js → fleet-S5R4ZOQY.js} +73 -44
- package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-BHSNPBMH.js} +3 -3
- package/dist/{init-JYGXI3FK.js → init-5DRU55YR.js} +49 -37
- package/dist/memory-7YKKR6UC.js +467 -0
- package/dist/{models-I5QWSEOM.js → models-ZPOLRU2C.js} +24 -21
- package/dist/{monitor-GE4ID3IA.js → monitor-US5F5YGZ.js} +73 -46
- package/dist/{orchestrator-EM5MC3HM.js → orchestrator-E2AL4T5N.js} +1624 -1007
- package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
- package/dist/{reset-L2FQEE3E.js → reset-KZ652EK6.js} +6 -5
- package/dist/{run-ZU3QMZPZ.js → run-SRNBKDWD.js} +76 -54
- package/dist/{share-S5BZQC5I.js → share-CGZE33UP.js} +7 -6
- package/dist/{skills-X5VXCRNQ.js → skills-S2X4DLY5.js} +4 -4
- package/dist/{skills-eval-WKIHWTHR.js → skills-eval-W2GGIC4R.js} +40 -29
- package/dist/{targets-SNCPI2NR.js → targets-54SWINWB.js} +28 -23
- package/dist/{terminal-lease-BNAHVHBS.js → terminal-lease-SAIF2OGY.js} +6 -4
- package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
- package/dist/{upgrade-JQHHPQ4K.js → upgrade-JKAR27XC.js} +20 -19
- package/dist/{usage-OR4O5SMZ.js → usage-MSAWCLX4.js} +79 -36
- package/dist/verifiers-NCBTHHN2.js +1220 -0
- package/dist/verify-X5HDROLA.js +25 -0
- package/dist/{wiki-generate-UEXP2ARI.js → wiki-generate-GUSOQ6ZP.js} +50 -37
- package/dist/worker/entry.js +90 -70
- package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
- 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-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 +60 -26
- package/docs/config-knobs-audit.md +1 -2
- package/docs/configuration-and-targets.md +26 -1
- package/docs/context-engine.md +67 -13
- package/docs/context-working-set.md +194 -0
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +6 -6
- package/docs/documentation-guide.md +7 -6
- package/docs/environment-variables.md +2 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +4 -32
- package/docs/evidence-and-memory.md +139 -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 +49 -8
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +19 -2
- package/docs/model-catalog.md +7 -9
- package/docs/observability.md +4 -4
- package/docs/proactive-memory.md +26 -16
- package/docs/prompt-envelope-and-tools.md +7 -5
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +43 -40
- package/docs/safety-model.md +49 -8
- 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 +38 -4
- package/docs/worker-dispatch-mechanics.md +11 -1
- package/package.json +13 -13
- 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/agents.ts +2 -3
- package/src/cli/argv.ts +14 -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/fleet.ts +15 -0
- package/src/cli/index.ts +5 -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/cli/verifiers.ts +325 -0
- package/src/core/bash-exec.ts +39 -14
- package/src/core/bus-events.ts +22 -4
- package/src/core/config.ts +54 -0
- package/src/core/defaults.ts +50 -3
- package/src/core/response-model-id.ts +134 -0
- package/src/core/toml.ts +62 -0
- package/src/core/verification-scripts.ts +6 -0
- package/src/core/workspace-files.ts +0 -1
- package/src/domains/agents/builtins/architect.md +1 -1
- package/src/domains/agents/builtins/verifier.md +3 -0
- 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/config/classify.ts +1 -0
- 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/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 +20 -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/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/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/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 +38 -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 +208 -5
- 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/autonomy.ts +1 -1
- package/src/domains/safety/call-target.ts +211 -14
- package/src/domains/safety/decision-presentation.ts +268 -0
- 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/safety/redaction.ts +73 -0
- 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 +12 -1
- package/src/domains/session/decision-board.ts +4 -0
- package/src/domains/session/entries.ts +110 -1
- package/src/domains/session/history.ts +68 -19
- package/src/domains/session/manager.ts +9 -2
- package/src/domains/session/migrations/index.ts +22 -3
- package/src/domains/session/usage.ts +24 -7
- package/src/engine/acp/event-mapper.ts +7 -0
- package/src/engine/acp/server.ts +32 -2
- package/src/engine/agent.ts +18 -1
- 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/session.ts +9 -3
- package/src/engine/worker-events.ts +4 -3
- package/src/engine/worker-runtime.ts +59 -54
- package/src/entry/orchestrator.ts +34 -5
- package/src/interactive/chat-loop-messages.ts +40 -6
- package/src/interactive/chat-loop.ts +13 -0
- package/src/interactive/chat-panel.ts +17 -1
- package/src/interactive/chat-renderer.ts +49 -24
- package/src/interactive/clio-editor.ts +44 -7
- package/src/interactive/context-meter.ts +10 -0
- package/src/interactive/context-overlay.ts +120 -7
- package/src/interactive/context-recall-command.ts +110 -0
- 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/interactive-slash-runtime.ts +37 -1
- package/src/interactive/memory-overlay.ts +89 -4
- package/src/interactive/model-session-replay.ts +21 -0
- 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 +46 -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-session-lifecycle.ts +8 -4
- 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 +37 -51
- package/src/interactive/session-last-turn.ts +8 -1
- package/src/interactive/session-transcript.ts +2 -2
- package/src/interactive/session-usage-reseed.ts +36 -10
- package/src/interactive/slash-commands.ts +31 -4
- 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 +333 -110
- package/src/interactive/turn-middleware.ts +7 -6
- 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 +39 -7
- package/src/tools/ask-user.ts +21 -1
- package/src/tools/bash.ts +144 -82
- package/src/tools/builtin-tool-catalog.ts +11 -5
- package/src/tools/context/index.ts +107 -5
- package/src/tools/context/surface.ts +3 -2
- package/src/tools/core-bootstrap.ts +21 -0
- package/src/tools/dispatch-arguments.ts +8 -0
- package/src/tools/dispatch-event-text.ts +19 -0
- package/src/tools/dispatch-runner.ts +9 -7
- package/src/tools/dispatch.ts +24 -1
- package/src/tools/monitor.ts +43 -20
- package/src/tools/registry.ts +72 -10
- package/src/tools/result-disposition.ts +706 -0
- package/src/tools/result-shaping.ts +321 -20
- package/src/tools/safe-exec.ts +2 -0
- package/src/tools/verify/authoring.ts +1120 -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 +54 -12
- package/src/worker/spec-contract.ts +43 -3
- package/dist/chunk-J7CWMCQD.js +0 -255
- package/dist/chunk-T6YILFSB.js +0 -80
- package/dist/chunk-VAKQQHWR.js +0 -434
- package/dist/chunk-VPAYEGVX.js +0 -184
- package/dist/chunk-XBXAASKX.js +0 -18
- package/dist/memory-WFZMGYHX.js +0 -236
- package/src/domains/eval/metrics/chaos-stream.ts +0 -93
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line stub the projection renders in place of an evicted tool-result
|
|
3
|
+
* body.
|
|
4
|
+
*
|
|
5
|
+
* Byte-stable by construction: same input, same bytes, forever. No timestamp,
|
|
6
|
+
* no counter, no `Date.now()`. Two reasons. The marker is persisted inside the
|
|
7
|
+
* `contextEviction` entry and replayed by the projection on every request, so a
|
|
8
|
+
* marker that changed between renders would invalidate the prompt cache on a
|
|
9
|
+
* turn that evicted nothing new. And replay-lite reruns the same policy over
|
|
10
|
+
* recorded ledgers; a marker carrying wall-clock state would make two runs of
|
|
11
|
+
* the same trace disagree.
|
|
12
|
+
*
|
|
13
|
+
* The field order is fixed (ref, reason, by, tool, path, size, offload,
|
|
14
|
+
* recall, then the body tail) so a diff between two markers is readable and so
|
|
15
|
+
* a model reading many of them sees the same shape every time. Undefined
|
|
16
|
+
* fields are omitted rather than rendered empty. `recall` spells out the exact
|
|
17
|
+
* tool call that brings the body back, which is the only affordance the model
|
|
18
|
+
* has once the body is gone.
|
|
19
|
+
*
|
|
20
|
+
* The body tail is `preview` for every reason but one. A `failure_resolved`
|
|
21
|
+
* eviction renders `first_line` instead: failures are evidence, and the line
|
|
22
|
+
* that says what failed is the part worth a marker's tokens, where a preview
|
|
23
|
+
* of a stack trace is not.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { formatSize } from "../../../engine/truncate.js";
|
|
27
|
+
import type { EvictionReason, WorkingSetRef } from "./contract.js";
|
|
28
|
+
|
|
29
|
+
/** Characters of the original body the marker keeps as a preview. */
|
|
30
|
+
const PREVIEW_LIMIT = 120;
|
|
31
|
+
|
|
32
|
+
export interface MarkerInput {
|
|
33
|
+
ref: WorkingSetRef;
|
|
34
|
+
reason: EvictionReason;
|
|
35
|
+
/** Ref key of the entry that superseded or resolved this one, when the reason names one. */
|
|
36
|
+
by?: string | undefined;
|
|
37
|
+
toolName: string;
|
|
38
|
+
/** The body leaving the working set; drives size and preview. */
|
|
39
|
+
text: string;
|
|
40
|
+
/** Set when the original result was offloaded to scratch. Replaces the preview. */
|
|
41
|
+
offloadPath?: string | undefined;
|
|
42
|
+
/** Primary file the result was about, when the payload names exactly one. */
|
|
43
|
+
path?: string | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Lines as `split(/\r\n|\r|\n/)` would count them, without materializing them. */
|
|
47
|
+
function lineCount(text: string): number {
|
|
48
|
+
if (text.length === 0) return 0;
|
|
49
|
+
let count = 1;
|
|
50
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
51
|
+
const code = text.charCodeAt(index);
|
|
52
|
+
if (code === 10 || (code === 13 && text.charCodeAt(index + 1) !== 10)) count += 1;
|
|
53
|
+
}
|
|
54
|
+
return count;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* First `PREVIEW_LIMIT` characters with whitespace collapsed to single spaces
|
|
59
|
+
* and double quotes escaped, so the preview never breaks the quoted field or
|
|
60
|
+
* spills onto a second line. Bounded: the body is scanned only as far as the
|
|
61
|
+
* preview reaches, because the marker is priced once per candidate on every
|
|
62
|
+
* eviction and a body-length collapse was most of that price.
|
|
63
|
+
*/
|
|
64
|
+
function preview(text: string): string {
|
|
65
|
+
let out = "";
|
|
66
|
+
let pendingSpace = false;
|
|
67
|
+
for (const char of text) {
|
|
68
|
+
if (/\s/.test(char)) {
|
|
69
|
+
pendingSpace = out.length > 0;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (pendingSpace) out += " ";
|
|
73
|
+
pendingSpace = false;
|
|
74
|
+
out += char;
|
|
75
|
+
if (out.length >= PREVIEW_LIMIT) break;
|
|
76
|
+
}
|
|
77
|
+
return out.slice(0, PREVIEW_LIMIT).replace(/"/g, '\\"');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The first line that says anything, bounded and escaped like a preview. */
|
|
81
|
+
function firstLine(text: string): string {
|
|
82
|
+
const line = /[^\r\n]*\S[^\r\n]*/.exec(text)?.[0] ?? "";
|
|
83
|
+
return line.trim().slice(0, PREVIEW_LIMIT).replace(/"/g, '\\"');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function renderMarker(input: MarkerInput): string {
|
|
87
|
+
const ref = input.ref.entry;
|
|
88
|
+
const fields: string[] = [`ref=${ref}`, `reason=${input.reason}`];
|
|
89
|
+
if (input.by !== undefined) fields.push(`by=${input.by}`);
|
|
90
|
+
fields.push(`tool=${input.toolName}`);
|
|
91
|
+
if (input.path !== undefined) fields.push(`path=${input.path}`);
|
|
92
|
+
fields.push(`size=${lineCount(input.text)} lines/${formatSize(Buffer.byteLength(input.text, "utf8"))}`);
|
|
93
|
+
if (input.offloadPath !== undefined) fields.push(`offload=${input.offloadPath}`);
|
|
94
|
+
fields.push(`recall=context(scope="recall", ref="${ref}")`);
|
|
95
|
+
// An offloaded body is one `read` away at a stable path; a preview of it
|
|
96
|
+
// would spend tokens repeating what the pointer already promises.
|
|
97
|
+
if (input.offloadPath === undefined) {
|
|
98
|
+
const failed = input.reason === "failure_resolved";
|
|
99
|
+
const tail = failed ? firstLine(input.text) : preview(input.text);
|
|
100
|
+
if (tail.length > 0) fields.push(`${failed ? "first_line" : "preview"}="${tail}"`);
|
|
101
|
+
}
|
|
102
|
+
return `[evicted ${fields.join(" ")}]`;
|
|
103
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the session has observed about which files.
|
|
3
|
+
*
|
|
4
|
+
* One pass over the active-path entries produces one `PathObservation` per
|
|
5
|
+
* tool result (and per `fileEntry`) that names a path: which file, which line
|
|
6
|
+
* range, which paths a listing surfaced, whether the call failed, and where in
|
|
7
|
+
* the turn sequence it sits. The structural policy reads it to answer the four
|
|
8
|
+
* questions its rules ask ("was this file written after I read it", "did a
|
|
9
|
+
* later read cover this range", "did this failure get resolved", "has this
|
|
10
|
+
* listing been consumed"), and the replay reference graph reads the same index
|
|
11
|
+
* to label `file_reread`, `file_discovery`, and `file_rewrite` edges. One
|
|
12
|
+
* index, two consumers, so a rule and its measurement can never disagree about
|
|
13
|
+
* what the session did.
|
|
14
|
+
*
|
|
15
|
+
* Pure, deterministic, single pass. No filesystem access: paths are resolved
|
|
16
|
+
* lexically against the session cwd the caller passes (`options.cwd`), and only
|
|
17
|
+
* normalized when it does not. The cwd is never sniffed from the entries and
|
|
18
|
+
* never defaulted to `process.cwd()`: the live ledger readers strip the JSONL
|
|
19
|
+
* header, and a replay run and a live run must index the same ledger the same
|
|
20
|
+
* way.
|
|
21
|
+
*
|
|
22
|
+
* This is `extractFileOps` in `compaction/compact.ts` generalized: same
|
|
23
|
+
* `path | file_path | filePath` argument reading, same tool-call pairing as
|
|
24
|
+
* `chat-renderer.ts`, plus ranges, listings, failures, and turn positions.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { basename, isAbsolute, join, normalize, resolve } from "node:path";
|
|
28
|
+
import type { MessageEntry, SessionEntry } from "../../session/entries.js";
|
|
29
|
+
import type { WorkingSetRef } from "./contract.js";
|
|
30
|
+
import { isTurnStart } from "./horizon.js";
|
|
31
|
+
import { isRecord, toolResultText } from "./payload.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The observing verb. Clio's `git` and `verify` are command runners with an
|
|
35
|
+
* exit status, so they index as `bash`; `artifact` writes a file, so it indexes
|
|
36
|
+
* as `write`. Tools that observe no path at all (dispatch, web_fetch, tasks,
|
|
37
|
+
* ask_user, context, ...) produce no observation.
|
|
38
|
+
*/
|
|
39
|
+
export type PathOp = "read" | "grep" | "find" | "ls" | "code_nav" | "write" | "edit" | "bash";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lines of a file an observation covers. `offset` counts lines skipped from the
|
|
43
|
+
* top (0-based), so a whole-file read is `{ offset: 0, limit: null }` and
|
|
44
|
+
* `read(offset: 1)` normalizes to it. `limit: null` means "to EOF".
|
|
45
|
+
*/
|
|
46
|
+
export interface PathRange {
|
|
47
|
+
offset: number;
|
|
48
|
+
limit: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function rangeEnd(range: PathRange): number {
|
|
52
|
+
return range.limit === null ? Number.POSITIVE_INFINITY : range.offset + range.limit;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isFullRead(range: PathRange | null): boolean {
|
|
56
|
+
return range !== null && range.offset === 0 && range.limit === null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether a later read makes the earlier range redundant. A full read covers
|
|
61
|
+
* everything, including a tail read whose coverage is unknown. Any other read
|
|
62
|
+
* covers only an identical or containing range, and an unknown range covers
|
|
63
|
+
* nothing.
|
|
64
|
+
*/
|
|
65
|
+
export function covers(later: PathRange | null, earlier: PathRange | null): boolean {
|
|
66
|
+
if (isFullRead(later)) return true;
|
|
67
|
+
if (later === null || earlier === null) return false;
|
|
68
|
+
return later.offset <= earlier.offset && rangeEnd(later) >= rangeEnd(earlier);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PathObservation {
|
|
72
|
+
/** The evictable unit: the tool_result entry, or the fileEntry entry for write/edit evidence. */
|
|
73
|
+
ref: WorkingSetRef;
|
|
74
|
+
toolCallId: string | null;
|
|
75
|
+
toolName: string;
|
|
76
|
+
op: PathOp;
|
|
77
|
+
/**
|
|
78
|
+
* Canonical absolute path when the session cwd is known, else the path as
|
|
79
|
+
* the call wrote it. Empty when the call named no path (a bash command with
|
|
80
|
+
* no cwd argument), which keeps it out of `byPath` without dropping the
|
|
81
|
+
* observation the failure rules need.
|
|
82
|
+
*/
|
|
83
|
+
path: string;
|
|
84
|
+
/**
|
|
85
|
+
* Line coverage for `read`, null for everything else and for a `tail` read,
|
|
86
|
+
* whose coverage is unknowable without the file. Supersession treats a null
|
|
87
|
+
* range as unknown: it covers nothing and only a full read covers it.
|
|
88
|
+
*/
|
|
89
|
+
range: PathRange | null;
|
|
90
|
+
/** Listing ops only: concrete file paths the result surfaced, resolved like `path`. */
|
|
91
|
+
surfaced: ReadonlyArray<string>;
|
|
92
|
+
isError: boolean;
|
|
93
|
+
/** The safety rails refused the call: no observation happened, and it resolves nothing. */
|
|
94
|
+
isBlocked: boolean;
|
|
95
|
+
/** Turn starts (user message, bashExecution, branchSummary) strictly before this entry. */
|
|
96
|
+
turnIndex: number;
|
|
97
|
+
/** Index in the entries array this index was built from. */
|
|
98
|
+
entryIndex: number;
|
|
99
|
+
/** Tool-call arguments as deterministic JSON with sorted keys; empty when the call is unknown. */
|
|
100
|
+
argsKey: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface PathIndex {
|
|
104
|
+
/** Ledger order. */
|
|
105
|
+
observations: ReadonlyArray<PathObservation>;
|
|
106
|
+
byRef: ReadonlyMap<string, PathObservation>;
|
|
107
|
+
byPath: ReadonlyMap<string, ReadonlyArray<PathObservation>>;
|
|
108
|
+
/** Every entry's turn position, including entries that observe no path. */
|
|
109
|
+
turnIndexOf: ReadonlyMap<string, number>;
|
|
110
|
+
turnCount: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Commands whose stdout is a list of paths. Anything else surfaces nothing. */
|
|
114
|
+
const LISTING_COMMANDS = new Set(["ls", "find", "tree", "fd", "rg", "grep"]);
|
|
115
|
+
/** Commands whose stdout is `path:line:text` rather than one path per line. */
|
|
116
|
+
const MATCH_LINE_COMMANDS = new Set(["rg", "grep"]);
|
|
117
|
+
|
|
118
|
+
const TOOL_OPS: ReadonlyMap<string, PathOp> = new Map<string, PathOp>([
|
|
119
|
+
["read", "read"],
|
|
120
|
+
["grep", "grep"],
|
|
121
|
+
["find", "find"],
|
|
122
|
+
["ls", "ls"],
|
|
123
|
+
["code_nav", "code_nav"],
|
|
124
|
+
["write", "write"],
|
|
125
|
+
["edit", "edit"],
|
|
126
|
+
["artifact", "write"],
|
|
127
|
+
["bash", "bash"],
|
|
128
|
+
["git", "bash"],
|
|
129
|
+
["verify", "bash"],
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
/** code_nav modes whose `query` is a file path rather than a symbol or page name. */
|
|
133
|
+
const CODE_NAV_PATH_MODES = new Set(["path", "outline", "deps", "dependents"]);
|
|
134
|
+
|
|
135
|
+
/** Ops whose result is a list of other paths. */
|
|
136
|
+
const LISTING_OPS = new Set<PathOp>(["grep", "find", "ls", "bash"]);
|
|
137
|
+
|
|
138
|
+
export interface PathIndexOptions {
|
|
139
|
+
/** Session working directory; relative arguments resolve against it. Null leaves them relative (normalized). */
|
|
140
|
+
cwd?: string | null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Lexical canonicalization only: no realpath, no `process.cwd()`, no `~`
|
|
145
|
+
* expansion. With a cwd, `src/a.ts`, `./src/a.ts`, and `/cwd/src/a.ts` all key
|
|
146
|
+
* the same file; without one the first two still do.
|
|
147
|
+
*/
|
|
148
|
+
function canonicalize(value: string, cwd: string | null): string {
|
|
149
|
+
const trimmed = value.trim();
|
|
150
|
+
if (trimmed.length === 0) return "";
|
|
151
|
+
if (isAbsolute(trimmed)) return normalize(trimmed);
|
|
152
|
+
return cwd === null ? normalize(trimmed) : resolve(cwd, trimmed);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function usableCwd(options: PathIndexOptions | undefined): string | null {
|
|
156
|
+
const cwd = options?.cwd;
|
|
157
|
+
if (typeof cwd !== "string" || cwd.trim().length === 0 || !isAbsolute(cwd)) return null;
|
|
158
|
+
return normalize(cwd);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stableStringify(value: unknown): string {
|
|
162
|
+
if (value === null) return "null";
|
|
163
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
164
|
+
if (isRecord(value)) {
|
|
165
|
+
const keys = Object.keys(value).sort();
|
|
166
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
167
|
+
}
|
|
168
|
+
if (value === undefined) return "null";
|
|
169
|
+
return JSON.stringify(value) ?? "null";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface ToolCallFacts {
|
|
173
|
+
toolName: string;
|
|
174
|
+
args: unknown;
|
|
175
|
+
argsKey: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function callFacts(toolName: string, args: unknown): ToolCallFacts {
|
|
179
|
+
return { toolName, args, argsKey: args === undefined ? "" : stableStringify(args) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function stringField(record: Record<string, unknown> | null, ...keys: string[]): string | null {
|
|
183
|
+
if (record === null) return null;
|
|
184
|
+
for (const key of keys) {
|
|
185
|
+
const value = record[key];
|
|
186
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function payloadRecord(payload: unknown): Record<string, unknown> | null {
|
|
192
|
+
return isRecord(payload) ? payload : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Record every tool call the ledger holds, in both shapes it persists them. */
|
|
196
|
+
function collectToolCalls(entries: ReadonlyArray<SessionEntry>): Map<string, ToolCallFacts> {
|
|
197
|
+
const calls = new Map<string, ToolCallFacts>();
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (entry.kind !== "message") continue;
|
|
200
|
+
const obj = payloadRecord(entry.payload);
|
|
201
|
+
if (entry.role === "tool_call" && obj !== null) {
|
|
202
|
+
const id = stringField(obj, "toolCallId", "tool_call_id", "id") ?? entry.turnId;
|
|
203
|
+
const name = stringField(obj, "name", "toolName", "tool") ?? "tool";
|
|
204
|
+
calls.set(id, callFacts(name, obj.args ?? obj.arguments ?? obj.input));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (entry.role !== "assistant" || obj === null || !Array.isArray(obj.content)) continue;
|
|
208
|
+
for (const block of obj.content) {
|
|
209
|
+
if (!isRecord(block) || block.type !== "toolCall") continue;
|
|
210
|
+
const id = stringField(block, "id", "toolCallId") ?? entry.turnId;
|
|
211
|
+
const name = stringField(block, "name", "toolName") ?? "tool";
|
|
212
|
+
calls.set(id, callFacts(name, block.arguments ?? block.args ?? block.input));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return calls;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The one path argument of every tool call, as the model wrote it, keyed by
|
|
220
|
+
* toolCallId. Markers read this so a `read` that left the working set still
|
|
221
|
+
* says which file it was; the canonical, cwd-resolved form the rules key on
|
|
222
|
+
* would show the model an absolute path it never typed.
|
|
223
|
+
*/
|
|
224
|
+
export function callPathsByToolCallId(entries: ReadonlyArray<SessionEntry>): ReadonlyMap<string, string> {
|
|
225
|
+
const paths = new Map<string, string>();
|
|
226
|
+
for (const [id, call] of collectToolCalls(entries)) {
|
|
227
|
+
const named = stringField(isRecord(call.args) ? call.args : null, "path", "file_path", "filePath");
|
|
228
|
+
if (named !== null) paths.set(id, named);
|
|
229
|
+
}
|
|
230
|
+
return paths;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The path the call was about. Search tools default to the working directory,
|
|
235
|
+
* which is what they actually searched, so a `grep` with no `path` argument is
|
|
236
|
+
* an observation of the cwd rather than of nothing.
|
|
237
|
+
*/
|
|
238
|
+
function observedPath(op: PathOp, args: Record<string, unknown> | null, cwd: string | null): string {
|
|
239
|
+
if (op === "bash") {
|
|
240
|
+
const explicit = stringField(args, "cwd");
|
|
241
|
+
return explicit === null ? "" : canonicalize(explicit, cwd);
|
|
242
|
+
}
|
|
243
|
+
if (op === "code_nav") {
|
|
244
|
+
const mode = stringField(args, "mode");
|
|
245
|
+
if (mode === null || !CODE_NAV_PATH_MODES.has(mode)) return "";
|
|
246
|
+
const query = stringField(args, "query");
|
|
247
|
+
return query === null ? "" : canonicalize(query, cwd);
|
|
248
|
+
}
|
|
249
|
+
const named = stringField(args, "path", "file_path", "filePath");
|
|
250
|
+
if (named !== null) return canonicalize(named, cwd);
|
|
251
|
+
// grep, find, and ls all default to ".".
|
|
252
|
+
if (op === "grep" || op === "find" || op === "ls") return cwd ?? ".";
|
|
253
|
+
return "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readRange(args: Record<string, unknown> | null): PathRange | null {
|
|
257
|
+
if (args === null) return { offset: 0, limit: null };
|
|
258
|
+
const tail = args.tail;
|
|
259
|
+
// A tail read covers an unknown suffix; claiming a range would let it
|
|
260
|
+
// supersede reads it may not contain.
|
|
261
|
+
if (typeof tail === "number" && Number.isFinite(tail) && tail > 0) return null;
|
|
262
|
+
const rawOffset = args.offset;
|
|
263
|
+
const rawLimit = args.limit;
|
|
264
|
+
const offset =
|
|
265
|
+
typeof rawOffset === "number" && Number.isFinite(rawOffset) && rawOffset > 1 ? Math.floor(rawOffset) - 1 : 0;
|
|
266
|
+
const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : null;
|
|
267
|
+
return { offset, limit };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Is this line a concrete file path rather than prose, a directory, or a
|
|
272
|
+
* tool's own notice? Deliberately permissive about extensions (`Makefile` and
|
|
273
|
+
* `LICENSE` are files) and strict about whitespace, because every listing this
|
|
274
|
+
* parses prints one path per line. Over-counting only keeps a listing in the
|
|
275
|
+
* working set longer; under-counting would evict a listing whose paths the
|
|
276
|
+
* agent still needs.
|
|
277
|
+
*/
|
|
278
|
+
function looksLikeConcreteFilePath(line: string): boolean {
|
|
279
|
+
const text = line.trim();
|
|
280
|
+
if (text.length === 0) return false;
|
|
281
|
+
// The observation envelope's own trailer, e.g. `[grep: 261/261+ matches ...]`.
|
|
282
|
+
if (text.startsWith("[")) return false;
|
|
283
|
+
if (text.endsWith("/")) return false;
|
|
284
|
+
return !/\s/.test(text);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** `path:line: text` from grep content mode and from bash `rg`/`grep`. */
|
|
288
|
+
function pathFromMatchLine(line: string): string | null {
|
|
289
|
+
const match = /^([^\s:]+):\d+[:-]/.exec(line.trim());
|
|
290
|
+
return match?.[1] ?? null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function surfacedPaths(
|
|
294
|
+
op: PathOp,
|
|
295
|
+
args: Record<string, unknown> | null,
|
|
296
|
+
text: string,
|
|
297
|
+
root: string,
|
|
298
|
+
cwd: string | null,
|
|
299
|
+
): string[] {
|
|
300
|
+
const lines = text.split("\n");
|
|
301
|
+
const matchLines = op === "grep" || (op === "bash" && isMatchLineCommand(args));
|
|
302
|
+
const out: string[] = [];
|
|
303
|
+
const seen = new Set<string>();
|
|
304
|
+
for (const line of lines) {
|
|
305
|
+
const raw = matchLines ? (pathFromMatchLine(line) ?? candidateWholeLine(line)) : candidateWholeLine(line);
|
|
306
|
+
if (raw === null) continue;
|
|
307
|
+
const resolved = resolveSurfaced(raw, root, cwd);
|
|
308
|
+
if (resolved.length === 0 || seen.has(resolved)) continue;
|
|
309
|
+
seen.add(resolved);
|
|
310
|
+
out.push(resolved);
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function candidateWholeLine(line: string): string | null {
|
|
316
|
+
return looksLikeConcreteFilePath(line) ? line.trim() : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isMatchLineCommand(args: Record<string, unknown> | null): boolean {
|
|
320
|
+
const verb = commandVerb(args);
|
|
321
|
+
return verb !== null && MATCH_LINE_COMMANDS.has(verb);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function commandVerb(args: Record<string, unknown> | null): string | null {
|
|
325
|
+
const command = stringField(args, "command");
|
|
326
|
+
if (command === null) return null;
|
|
327
|
+
const first = command.trimStart().split(/\s+/)[0];
|
|
328
|
+
return first === undefined || first.length === 0 ? null : basename(first);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* A listing prints paths relative to what it searched, so they join onto the
|
|
333
|
+
* observation's own path before canonicalizing, whether that root is absolute
|
|
334
|
+
* or still relative. A single-file search root surfaces its own basename,
|
|
335
|
+
* which resolves back to the root rather than to a child of it.
|
|
336
|
+
*/
|
|
337
|
+
function resolveSurfaced(value: string, root: string, cwd: string | null): string {
|
|
338
|
+
if (isAbsolute(value)) return normalize(value);
|
|
339
|
+
if (root.length === 0) return canonicalize(value, cwd);
|
|
340
|
+
if (basename(root) === value) return canonicalize(root, cwd);
|
|
341
|
+
return canonicalize(join(root, value), cwd);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** A listing result only surfaces paths when the call was a listing in the first place. */
|
|
345
|
+
function shouldParseSurfaced(op: PathOp, args: Record<string, unknown> | null, isError: boolean): boolean {
|
|
346
|
+
if (isError || !LISTING_OPS.has(op)) return false;
|
|
347
|
+
if (op !== "bash") return true;
|
|
348
|
+
const verb = commandVerb(args);
|
|
349
|
+
return verb !== null && LISTING_COMMANDS.has(verb);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function fileEntryOp(operation: "read" | "write" | "edit" | "create" | "delete"): PathOp {
|
|
353
|
+
if (operation === "read") return "read";
|
|
354
|
+
if (operation === "edit") return "edit";
|
|
355
|
+
return "write";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function toolResultObservation(
|
|
359
|
+
entry: MessageEntry,
|
|
360
|
+
context: { entryIndex: number; turnIndex: number; cwd: string | null; calls: ReadonlyMap<string, ToolCallFacts> },
|
|
361
|
+
): PathObservation | null {
|
|
362
|
+
const obj = payloadRecord(entry.payload);
|
|
363
|
+
const toolCallId = stringField(obj, "toolCallId", "tool_call_id", "id");
|
|
364
|
+
const call = toolCallId === null ? undefined : context.calls.get(toolCallId);
|
|
365
|
+
const toolName = stringField(obj, "toolName", "name", "tool") ?? call?.toolName ?? "tool";
|
|
366
|
+
const op = TOOL_OPS.get(toolName);
|
|
367
|
+
if (op === undefined) return null;
|
|
368
|
+
const args = call !== undefined && isRecord(call.args) ? call.args : null;
|
|
369
|
+
const isError = obj?.isError === true || obj?.error === true;
|
|
370
|
+
const isBlocked = obj?.outcome === "blocked" || typeof obj?.blockReason === "string";
|
|
371
|
+
const path = observedPath(op, args, context.cwd);
|
|
372
|
+
const surfaced = shouldParseSurfaced(op, args, isError)
|
|
373
|
+
? surfacedPaths(op, args, toolResultText(obj?.result ?? entry.payload), path, context.cwd)
|
|
374
|
+
: [];
|
|
375
|
+
return {
|
|
376
|
+
ref: { entry: entry.turnId },
|
|
377
|
+
toolCallId,
|
|
378
|
+
toolName,
|
|
379
|
+
op,
|
|
380
|
+
path,
|
|
381
|
+
range: op === "read" ? readRange(args) : null,
|
|
382
|
+
surfaced,
|
|
383
|
+
isError,
|
|
384
|
+
isBlocked,
|
|
385
|
+
turnIndex: context.turnIndex,
|
|
386
|
+
entryIndex: context.entryIndex,
|
|
387
|
+
argsKey: call?.argsKey ?? "",
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function buildPathIndex(entries: ReadonlyArray<SessionEntry>, options?: PathIndexOptions): PathIndex {
|
|
392
|
+
const cwd = usableCwd(options);
|
|
393
|
+
const calls = collectToolCalls(entries);
|
|
394
|
+
const observations: PathObservation[] = [];
|
|
395
|
+
const byRef = new Map<string, PathObservation>();
|
|
396
|
+
const byPath = new Map<string, PathObservation[]>();
|
|
397
|
+
const turnIndexOf = new Map<string, number>();
|
|
398
|
+
let turnIndex = 0;
|
|
399
|
+
|
|
400
|
+
for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) {
|
|
401
|
+
const entry = entries[entryIndex];
|
|
402
|
+
if (entry === undefined) continue;
|
|
403
|
+
turnIndexOf.set(entry.turnId, turnIndex);
|
|
404
|
+
let observation: PathObservation | null = null;
|
|
405
|
+
if (entry.kind === "fileEntry") {
|
|
406
|
+
observation = {
|
|
407
|
+
ref: { entry: entry.turnId },
|
|
408
|
+
toolCallId: null,
|
|
409
|
+
toolName: "fileEntry",
|
|
410
|
+
op: fileEntryOp(entry.operation),
|
|
411
|
+
path: canonicalize(entry.path, cwd),
|
|
412
|
+
range: null,
|
|
413
|
+
surfaced: [],
|
|
414
|
+
isError: false,
|
|
415
|
+
isBlocked: false,
|
|
416
|
+
turnIndex,
|
|
417
|
+
entryIndex,
|
|
418
|
+
argsKey: "",
|
|
419
|
+
};
|
|
420
|
+
} else if (entry.kind === "message" && entry.role === "tool_result") {
|
|
421
|
+
observation = toolResultObservation(entry, { entryIndex, turnIndex, cwd, calls });
|
|
422
|
+
}
|
|
423
|
+
if (observation !== null) {
|
|
424
|
+
observations.push(observation);
|
|
425
|
+
byRef.set(observation.ref.entry, observation);
|
|
426
|
+
if (observation.path.length > 0) {
|
|
427
|
+
const bucket = byPath.get(observation.path);
|
|
428
|
+
if (bucket === undefined) byPath.set(observation.path, [observation]);
|
|
429
|
+
else bucket.push(observation);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (isTurnStart(entry)) turnIndex += 1;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { observations, byRef, byPath, turnIndexOf, turnCount: turnIndex };
|
|
436
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readers for the two message payload shapes the working set acts on:
|
|
3
|
+
* `tool_result` (whose body is replaced by a marker) and `assistant` (whose
|
|
4
|
+
* thinking blocks are dropped).
|
|
5
|
+
*
|
|
6
|
+
* These deliberately mirror the private helpers in
|
|
7
|
+
* `src/domains/session/compaction/mask-observations.ts`. That module is the
|
|
8
|
+
* destructive stage this layer replaces and it goes away once the legacy path
|
|
9
|
+
* is retired, so the working set carries its own copy rather than importing
|
|
10
|
+
* from a module scheduled for deletion. The shapes themselves are not this
|
|
11
|
+
* layer's invention: `turn-persistence.ts` writes
|
|
12
|
+
* `{ toolCallId, toolName, result, isError, resultSummary }`, and `result` is
|
|
13
|
+
* whatever the tool returned, usually `{ content: [...], details: {...} }`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToolResultPayload {
|
|
21
|
+
/** The payload object itself, or a synthetic wrapper when the payload is a bare value. */
|
|
22
|
+
obj: Record<string, unknown>;
|
|
23
|
+
/** The tool's own result value, wherever the payload put it. */
|
|
24
|
+
result: unknown;
|
|
25
|
+
toolName: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toolResultPayload(payload: unknown): ToolResultPayload {
|
|
29
|
+
const obj = isRecord(payload) ? payload : { result: payload };
|
|
30
|
+
const result = obj.result ?? obj.output ?? obj.out ?? obj.content ?? payload;
|
|
31
|
+
const toolName =
|
|
32
|
+
(typeof obj.toolName === "string" && obj.toolName) ||
|
|
33
|
+
(typeof obj.name === "string" && obj.name) ||
|
|
34
|
+
(typeof obj.tool === "string" && obj.tool) ||
|
|
35
|
+
"tool";
|
|
36
|
+
return { obj, result, toolName };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function textFromContent(content: unknown): string {
|
|
40
|
+
if (!Array.isArray(content)) return "";
|
|
41
|
+
const parts: string[] = [];
|
|
42
|
+
for (const block of content) {
|
|
43
|
+
if (!isRecord(block)) continue;
|
|
44
|
+
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
45
|
+
}
|
|
46
|
+
return parts.join("");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stringifyBody(value: unknown): string {
|
|
50
|
+
if (value === undefined || value === null) return "";
|
|
51
|
+
if (typeof value === "string") return value;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.stringify(value) ?? "";
|
|
54
|
+
} catch {
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The displayed body of a tool result: content text first, then the legacy single-field shapes. */
|
|
60
|
+
export function toolResultText(result: unknown): string {
|
|
61
|
+
if (typeof result === "string") return result;
|
|
62
|
+
if (!isRecord(result)) return stringifyBody(result);
|
|
63
|
+
const contentText = textFromContent(result.content);
|
|
64
|
+
if (contentText.length > 0) return contentText;
|
|
65
|
+
if (typeof result.text === "string") return result.text;
|
|
66
|
+
if (typeof result.output === "string") return result.output;
|
|
67
|
+
if (typeof result.message === "string") return result.message;
|
|
68
|
+
return stringifyBody(result);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Estimated tokens of the body the marker would replace: the text the model
|
|
73
|
+
* reads, not the payload JSON. `details`, `resultSummary`, and the observation
|
|
74
|
+
* envelope never reach the model, so a floor measured on the whole payload
|
|
75
|
+
* would let a two-byte result with a fat envelope through and buy a marker
|
|
76
|
+
* longer than the body it replaced.
|
|
77
|
+
*/
|
|
78
|
+
export function toolResultBodyTokens(payload: unknown): number {
|
|
79
|
+
return Math.ceil(toolResultText(toolResultPayload(payload).result).length / 4);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function nestedRecord(parent: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
|
|
83
|
+
if (parent === null) return null;
|
|
84
|
+
const value = parent[key];
|
|
85
|
+
return isRecord(value) ? value : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stringField(record: Record<string, unknown> | null, key: string): string | undefined {
|
|
89
|
+
if (record === null) return undefined;
|
|
90
|
+
const value = record[key];
|
|
91
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scratch path holding the complete output, when the tool spilled one. Checked
|
|
96
|
+
* in the same order `toolResultSummary` writes it: the OBSERVE envelope, then
|
|
97
|
+
* bash-style result shaping, then the persisted summary that mirrors both.
|
|
98
|
+
*/
|
|
99
|
+
export function offloadPathOf(payload: ToolResultPayload): string | undefined {
|
|
100
|
+
const details = nestedRecord(isRecord(payload.result) ? payload.result : null, "details");
|
|
101
|
+
return (
|
|
102
|
+
stringField(nestedRecord(details, "resultDisposition"), "offloadPath") ??
|
|
103
|
+
stringField(nestedRecord(details, "observation"), "offloadPath") ??
|
|
104
|
+
stringField(nestedRecord(details, "resultSize"), "offloadPath") ??
|
|
105
|
+
stringField(nestedRecord(payload.obj, "resultSummary"), "offloadPath")
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The one file this result was about, when the tool named exactly one.
|
|
111
|
+
* `edit`, `write`, and `artifact` record `details.paths`; a result naming
|
|
112
|
+
* several is left without a path rather than picking one arbitrarily. Read
|
|
113
|
+
* results carry no `paths`, so the marker falls back to the call's own
|
|
114
|
+
* argument (`callPathsByToolCallId` in path-index.ts).
|
|
115
|
+
*/
|
|
116
|
+
export function primaryPathOf(payload: ToolResultPayload): string | undefined {
|
|
117
|
+
const details = nestedRecord(isRecord(payload.result) ? payload.result : null, "details");
|
|
118
|
+
const paths = details?.paths;
|
|
119
|
+
if (!Array.isArray(paths) || paths.length !== 1) return undefined;
|
|
120
|
+
const first = paths[0];
|
|
121
|
+
return typeof first === "string" && first.length > 0 ? first : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isThinkingBlock(block: unknown): boolean {
|
|
125
|
+
return isRecord(block) && block.type === "thinking";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Both shapes the ledger holds reasoning in: `thinking` content blocks and the payload-level string. */
|
|
129
|
+
export function hasThinking(payload: unknown): boolean {
|
|
130
|
+
const obj = isRecord(payload) ? payload : null;
|
|
131
|
+
if (obj === null) return false;
|
|
132
|
+
if (Array.isArray(obj.content) && obj.content.some(isThinkingBlock)) return true;
|
|
133
|
+
return typeof obj.thinking === "string" && obj.thinking.length > 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function withoutThinkingBlocks(content: unknown): unknown[] | undefined {
|
|
137
|
+
if (!Array.isArray(content)) return undefined;
|
|
138
|
+
return content.filter((block) => !isThinkingBlock(block));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* True when an earlier destructive compaction run already replaced this body.
|
|
143
|
+
* Such a result has no body left to evict, and re-marking it would spend a
|
|
144
|
+
* marker on a marker. Mirrors `alreadyCompacted()` in mask-observations.ts.
|
|
145
|
+
*/
|
|
146
|
+
export function hasLegacyCompactionMarker(payload: unknown): boolean {
|
|
147
|
+
if (!isRecord(payload)) return false;
|
|
148
|
+
if (isRecord(payload.contextCompaction)) return true;
|
|
149
|
+
if (isRecord(nestedRecord(payload, "resultSummary")?.contextCompaction)) return true;
|
|
150
|
+
const result = payload.result ?? payload.output ?? payload.out;
|
|
151
|
+
return isRecord(nestedRecord(isRecord(result) ? result : null, "details")?.contextCompaction);
|
|
152
|
+
}
|