@iowarp/clio-coder 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -2
- package/CONTRIBUTING.md +6 -6
- package/README.md +2 -2
- package/dist/{acp-S5R4RR5B.js → acp-2BEHC4DL.js} +4 -4
- package/dist/{agents-P6DMMVZY.js → agents-LNNFTM53.js} +13 -11
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-2XCZLPKS.js → auth-KXXFI2VS.js} +6 -6
- package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
- package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
- package/dist/chunk-3BPUFZDL.js +37 -0
- package/dist/{chunk-WPQLXFOZ.js → chunk-43AOLP7E.js} +2 -2
- package/dist/{chunk-N4CZJQRK.js → chunk-5JGRAMKL.js} +4 -4
- package/dist/{chunk-BRXQQJFP.js → chunk-6US73PDB.js} +568 -47
- package/dist/{chunk-K6WL7QZT.js → chunk-6XXKFVSN.js} +2 -2
- package/dist/{chunk-QQK64KLB.js → chunk-CJUB2JJ2.js} +138 -20
- package/dist/{chunk-HV5X7OR2.js → chunk-CKXWIANG.js} +12 -12
- package/dist/{chunk-UZHIZC5S.js → chunk-CYQKWTG3.js} +61 -76
- package/dist/{chunk-QWU7ZBO7.js → chunk-DJVECN66.js} +204 -45
- package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
- package/dist/{chunk-7RXG6QRZ.js → chunk-EKY57CSP.js} +2 -75
- package/dist/{chunk-EDRHSCIE.js → chunk-EYPA3EGJ.js} +10 -2
- package/dist/{chunk-TTNYS3EA.js → chunk-G7MUEIGA.js} +1 -1
- package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
- package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
- package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
- package/dist/chunk-HWUFFB6L.js +83 -0
- package/dist/{chunk-35MKKU5R.js → chunk-K7T3E2SR.js} +15 -8
- package/dist/{chunk-VAWWTKDP.js → chunk-KHSFENX2.js} +2 -2
- package/dist/chunk-LCGCVYZ4.js +57 -0
- package/dist/{chunk-X6COSD2O.js → chunk-LYF7OHWH.js} +41 -14
- package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
- package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
- package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
- package/dist/{chunk-3JLKSKD7.js → chunk-OH3TOQTB.js} +5 -1
- package/dist/{chunk-MEQ45TQ4.js → chunk-PBTHKCPN.js} +18 -4
- package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
- package/dist/{chunk-QQL5RT5M.js → chunk-QM3F2GKX.js} +94 -36
- package/dist/{chunk-A2GZF7DC.js → chunk-QNQHSOLF.js} +4 -4
- package/dist/{chunk-KRPY7NTG.js → chunk-R46L2BIR.js} +3 -3
- package/dist/{chunk-BP4OYD6A.js → chunk-RY3LY4J5.js} +20 -2
- package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
- package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
- package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
- package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
- package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
- package/dist/{chunk-HXG4IURW.js → chunk-XE2VEJHX.js} +2 -2
- package/dist/{chunk-3HZ5RWN2.js → chunk-XF5N4U5A.js} +7 -6
- package/dist/{chunk-ZYKPLLNQ.js → chunk-XXQNGV4M.js} +590 -32
- package/dist/{chunk-4JUF2NNX.js → chunk-XYDYPRZI.js} +4 -4
- package/dist/{chunk-VMNQ6OZA.js → chunk-ZRGEBJ4T.js} +971 -794
- package/dist/{chunk-2LZI5CAG.js → chunk-ZXF4XRKW.js} +75 -33
- package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
- package/dist/cli/index.js +31 -31
- package/dist/{clio-J5JIOIDS.js → clio-M2KGYUFZ.js} +2 -2
- package/dist/{code-nav-AXCXSBHX.js → code-nav-GQNL7XA6.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
- package/dist/{config-OEBMIN2U.js → config-XUUYQIWO.js} +27 -25
- package/dist/{configure-PUQOSIXQ.js → configure-IHJ7YOMV.js} +7 -7
- package/dist/{context-URSXPBCK.js → context-74JLXAWD.js} +12 -12
- package/dist/{context-MGSE4Z2T.js → context-75MIWW3U.js} +24 -22
- package/dist/{context-EKDCKUUZ.js → context-ZQ7SIFJV.js} +8 -7
- package/dist/{context-clear-KDAJRNUK.js → context-clear-GYKWNUML.js} +24 -22
- package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
- package/dist/{context-working-set-SBKMPPI2.js → context-working-set-UX5KEP4J.js} +11 -10
- package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-GIJBHNFL.js} +21 -20
- package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
- package/dist/{doctor-7BSE27PJ.js → doctor-SVJ5BZCW.js} +4 -4
- package/dist/{eval-IZGDOO4H.js → eval-CG6LLBLD.js} +47 -232
- package/dist/{evidence-SR7WXB5B.js → evidence-ZYFIEN42.js} +19 -18
- package/dist/{evolve-K7VE2CBX.js → evolve-QGEXEMDW.js} +19 -18
- package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
- package/dist/{fleet-7XMJNQNF.js → fleet-S5R4ZOQY.js} +49 -30
- package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
- package/dist/{init-JGNPAYXT.js → init-5DRU55YR.js} +31 -29
- package/dist/memory-7YKKR6UC.js +467 -0
- package/dist/{models-ZMMLFJNN.js → models-ZPOLRU2C.js} +10 -10
- package/dist/{monitor-2F3T5KHP.js → monitor-US5F5YGZ.js} +33 -18
- package/dist/{orchestrator-ORHT43JB.js → orchestrator-E2AL4T5N.js} +1092 -659
- package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
- package/dist/{reset-NXGTYNUO.js → reset-KZ652EK6.js} +3 -3
- package/dist/{run-RF4WJGMT.js → run-SRNBKDWD.js} +52 -40
- package/dist/{share-UT3W6E4M.js → share-CGZE33UP.js} +3 -3
- package/dist/{skills-PSACKC5Q.js → skills-S2X4DLY5.js} +4 -4
- package/dist/{skills-eval-WJSI55RZ.js → skills-eval-W2GGIC4R.js} +19 -18
- package/dist/{targets-PIIRAOYS.js → targets-54SWINWB.js} +14 -12
- package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-SAIF2OGY.js} +5 -4
- package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
- package/dist/{upgrade-346TZ6AV.js → upgrade-JKAR27XC.js} +8 -8
- package/dist/{usage-6KKXR32N.js → usage-MSAWCLX4.js} +60 -27
- package/dist/{verifiers-4UUM6TEE.js → verifiers-NCBTHHN2.js} +60 -54
- package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-GUSOQ6ZP.js} +30 -28
- package/dist/worker/entry.js +69 -58
- package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
- package/docs/README.md +3 -3
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-placement.md +1 -2
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +9 -7
- package/docs/configuration-and-targets.md +12 -1
- package/docs/context-engine.md +4 -2
- package/docs/context-working-set.md +4 -4
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +2 -2
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +4 -45
- package/docs/evidence-and-memory.md +67 -7
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +1 -1
- package/docs/extensions-and-sharing.md +2 -2
- package/docs/fleet-dispatch.md +28 -2
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +19 -2
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +3 -3
- package/docs/proactive-memory.md +26 -16
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +38 -35
- package/docs/safety-model.md +29 -7
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +2 -2
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +38 -4
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +7 -4
- package/src/cli/agents.ts +2 -3
- package/src/cli/argv.ts +14 -1
- package/src/cli/fleet.ts +15 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/memory.ts +272 -10
- package/src/cli/modes/json-stream.ts +2 -2
- package/src/cli/modes/print.ts +12 -1
- package/src/cli/run.ts +22 -2
- package/src/cli/targets.ts +12 -3
- package/src/cli/usage.ts +55 -7
- package/src/core/bus-events.ts +3 -0
- package/src/core/response-model-id.ts +134 -0
- package/src/core/toml.ts +62 -0
- package/src/core/workspace-files.ts +0 -1
- package/src/domains/agents/builtins/architect.md +1 -1
- package/src/domains/agents/catalog.ts +5 -4
- package/src/domains/agents/recipe.ts +54 -14
- package/src/domains/agents/result-contract.ts +7 -4
- package/src/domains/context/bootstrap.ts +36 -27
- package/src/domains/context/project-metadata.ts +19 -63
- package/src/domains/context/prompt-context.ts +8 -0
- package/src/domains/context/working-set/policies/index.ts +3 -4
- package/src/domains/dispatch/budget-envelope.ts +396 -0
- package/src/domains/dispatch/contract.ts +2 -0
- package/src/domains/dispatch/extension.ts +81 -27
- package/src/domains/dispatch/orphan-recovery.ts +1 -0
- package/src/domains/dispatch/receipt-integrity.ts +4 -0
- package/src/domains/dispatch/state.ts +1 -0
- package/src/domains/dispatch/types.ts +10 -3
- package/src/domains/dispatch/validation.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +14 -3
- package/src/domains/eval/metrics/evidence.ts +0 -116
- package/src/domains/eval/metrics/invariants.ts +1 -1
- package/src/domains/eval/runners/clio-run.ts +1 -10
- package/src/domains/eval/runners/external-command.ts +2 -29
- package/src/domains/eval/schema/suite.ts +0 -7
- package/src/domains/eval/suites/run.ts +1 -7
- package/src/domains/memory/index.ts +22 -0
- package/src/domains/memory/operations.ts +58 -1
- package/src/domains/memory/promotion.ts +281 -0
- package/src/domains/memory/prompt-section.ts +25 -5
- package/src/domains/memory/proposal.ts +51 -7
- package/src/domains/memory/task-bank.ts +3 -2
- package/src/domains/memory/task-memory-handoff.ts +181 -24
- package/src/domains/memory/task-memory-policy.ts +3 -1
- package/src/domains/memory/types.ts +37 -0
- package/src/domains/memory/validate.ts +178 -0
- package/src/domains/middleware/memory-intervention.ts +35 -25
- package/src/domains/middleware/runtime.ts +6 -0
- package/src/domains/middleware/skills-reminder.ts +19 -4
- package/src/domains/middleware/stalled-turn.ts +43 -1
- package/src/domains/middleware/types.ts +10 -0
- package/src/domains/observability/contract.ts +6 -1
- package/src/domains/observability/cost.ts +20 -4
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/providers/index.ts +3 -0
- package/src/domains/providers/model-discovery.ts +9 -0
- package/src/domains/providers/runtime-resolution.ts +38 -1
- package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
- package/src/domains/providers/types/context-window-slots.ts +18 -0
- package/src/domains/providers/types/runtime-descriptor.ts +3 -1
- package/src/domains/safety/call-target.ts +211 -14
- package/src/domains/safety/decision-presentation.ts +268 -0
- package/src/domains/safety/redaction.ts +73 -0
- package/src/domains/session/context-ledger.ts +10 -1
- package/src/domains/session/decision-board.ts +4 -0
- package/src/domains/session/entries.ts +3 -0
- package/src/domains/session/history.ts +68 -19
- package/src/domains/session/usage.ts +24 -7
- package/src/engine/acp/event-mapper.ts +7 -0
- package/src/engine/acp/server.ts +29 -2
- package/src/engine/apis/lmstudio.ts +25 -4
- package/src/engine/apis/openai-completions.ts +147 -22
- package/src/engine/claude/sdk-runtime.ts +8 -2
- package/src/engine/claude/tool-safety.ts +13 -0
- package/src/engine/loop-guard.ts +27 -3
- package/src/engine/worker-events.ts +4 -3
- package/src/engine/worker-runtime.ts +59 -54
- package/src/entry/orchestrator.ts +18 -1
- package/src/interactive/chat-loop-messages.ts +22 -0
- package/src/interactive/chat-loop.ts +13 -0
- package/src/interactive/chat-renderer.ts +19 -3
- package/src/interactive/clio-editor.ts +44 -7
- package/src/interactive/context-overlay.ts +43 -5
- package/src/interactive/cost-overlay.ts +39 -8
- package/src/interactive/dispatch-board.ts +212 -35
- package/src/interactive/footer/widgets.ts +13 -0
- package/src/interactive/interactive-application.ts +6 -1
- package/src/interactive/interactive-input-runtime.ts +11 -1
- package/src/interactive/interactive-presentation.ts +11 -1
- package/src/interactive/memory-overlay.ts +89 -4
- package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
- package/src/interactive/overlay-frame.ts +5 -2
- package/src/interactive/overlay-general-openers.ts +40 -1
- package/src/interactive/overlay-key-routing.ts +41 -1
- package/src/interactive/overlay-lifecycle.ts +11 -4
- package/src/interactive/overlay-permission-lifecycle.ts +23 -8
- package/src/interactive/overlay-transitions.ts +11 -0
- package/src/interactive/overlays/ask-user.ts +74 -30
- package/src/interactive/overlays/decisions.ts +3 -1
- package/src/interactive/permission-hint.ts +35 -0
- package/src/interactive/permission-overlay.ts +95 -45
- package/src/interactive/renderers/tool-execution.ts +19 -49
- package/src/interactive/session-last-turn.ts +8 -1
- package/src/interactive/session-usage-reseed.ts +36 -10
- package/src/interactive/slash-commands.ts +2 -2
- package/src/interactive/status/summary.ts +5 -0
- package/src/interactive/status/types.ts +5 -0
- package/src/interactive/terminal-lease.ts +1 -0
- package/src/interactive/turn-context.ts +96 -23
- package/src/interactive/turn-middleware.ts +1 -0
- package/src/interactive/turn-runtime.ts +37 -8
- package/src/interactive/turn-state.ts +3 -0
- package/src/interactive/worker-progress.ts +440 -0
- package/src/interactive/worker-stream.ts +51 -110
- package/src/tools/agent-tools.ts +28 -3
- package/src/tools/ask-user.ts +21 -1
- package/src/tools/context/index.ts +2 -2
- package/src/tools/dispatch-arguments.ts +8 -0
- package/src/tools/dispatch-event-text.ts +19 -0
- package/src/tools/dispatch.ts +24 -1
- package/src/tools/monitor.ts +15 -0
- package/src/tools/registry.ts +15 -5
- package/src/tools/result-disposition.ts +156 -0
- package/src/tools/result-shaping.ts +59 -1
- package/src/tools/verify/authoring.ts +55 -54
- package/src/tools/worker-evidence.ts +19 -0
- package/src/worker/spec-contract.ts +43 -3
- package/dist/chunk-EFADSJET.js +0 -18
- package/dist/memory-4ALKDJ4Q.js +0 -246
- package/src/domains/eval/metrics/chaos-stream.ts +0 -93
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { ActionClass } from "./action-classifier.js";
|
|
2
|
+
import type { AutonomyExposure } from "./autonomy.js";
|
|
3
|
+
|
|
4
|
+
/** Closed consequence categories used only to explain an already-made decision request. */
|
|
5
|
+
export const DECISION_TIERS = ["conversation", "workspace", "outward", "safety-net", "system", "worker"] as const;
|
|
6
|
+
|
|
7
|
+
export type DecisionTier = (typeof DECISION_TIERS)[number];
|
|
8
|
+
export type DecisionSemanticToken = "accent" | "action" | "warning";
|
|
9
|
+
export type DecisionRequestKind = "question" | "permission";
|
|
10
|
+
export type DecisionAffectedScope = "conversation" | "workspace" | "outward" | "system";
|
|
11
|
+
export type DecisionReversibility = "reversible" | "limited" | "unknown";
|
|
12
|
+
export type DecisionAuthorityEffect = "records-answer" | "grants-once";
|
|
13
|
+
export type DecisionActionId = "record-answer" | "cancel" | "approve-once" | "deny" | "stop";
|
|
14
|
+
|
|
15
|
+
export type DecisionAxis =
|
|
16
|
+
| { kind: "answer" }
|
|
17
|
+
| { kind: "autonomy"; level: string }
|
|
18
|
+
| { kind: "safety-net"; ruleId: string };
|
|
19
|
+
|
|
20
|
+
export type DecisionOrigin = { kind: "main" } | { kind: "worker"; agentId: string; runId: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Facts the host has already authenticated or derived from enforced policy.
|
|
24
|
+
* No model-authored title, question, reason, summary, or option label belongs
|
|
25
|
+
* here, so prose has no path to the tier selection.
|
|
26
|
+
*/
|
|
27
|
+
export interface TrustedDecisionFacts {
|
|
28
|
+
requestKind: DecisionRequestKind;
|
|
29
|
+
axis: DecisionAxis;
|
|
30
|
+
exposure: AutonomyExposure;
|
|
31
|
+
affectedScope: DecisionAffectedScope;
|
|
32
|
+
reversibility: DecisionReversibility;
|
|
33
|
+
origin: DecisionOrigin;
|
|
34
|
+
authorityEffect: DecisionAuthorityEffect;
|
|
35
|
+
tool?: string;
|
|
36
|
+
actionClass?: ActionClass;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DecisionPresentationAction {
|
|
40
|
+
id: DecisionActionId;
|
|
41
|
+
label: string;
|
|
42
|
+
consequence: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* This projection contains display data only. Registry admission and interview
|
|
47
|
+
* settlement never consume it, and it contains no allow, deny, or autonomy
|
|
48
|
+
* disposition that an execution layer could mistake for authority.
|
|
49
|
+
*/
|
|
50
|
+
export interface DecisionPresentation {
|
|
51
|
+
tier: DecisionTier;
|
|
52
|
+
tierLabel: string;
|
|
53
|
+
title: string;
|
|
54
|
+
semanticToken: DecisionSemanticToken;
|
|
55
|
+
authorizationCopy: string;
|
|
56
|
+
consequenceCopy: string;
|
|
57
|
+
reversibilityCopy: string;
|
|
58
|
+
requestedByCopy: string;
|
|
59
|
+
requiredActions: ReadonlyArray<DecisionPresentationAction>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ACTION_CLASSES = new Set<ActionClass>([
|
|
63
|
+
"read",
|
|
64
|
+
"write",
|
|
65
|
+
"execute",
|
|
66
|
+
"dispatch",
|
|
67
|
+
"system_modify",
|
|
68
|
+
"git_destructive",
|
|
69
|
+
"unknown",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/** Unknown or malformed action classes classify conservatively as unknown. */
|
|
73
|
+
export function decisionActionClass(value: unknown): ActionClass {
|
|
74
|
+
return typeof value === "string" && ACTION_CLASSES.has(value as ActionClass) ? (value as ActionClass) : "unknown";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function actionConsequence(actionClass: ActionClass): {
|
|
78
|
+
affectedScope: DecisionAffectedScope;
|
|
79
|
+
reversibility: DecisionReversibility;
|
|
80
|
+
} {
|
|
81
|
+
switch (actionClass) {
|
|
82
|
+
case "read":
|
|
83
|
+
return { affectedScope: "workspace", reversibility: "reversible" };
|
|
84
|
+
case "write":
|
|
85
|
+
return { affectedScope: "workspace", reversibility: "reversible" };
|
|
86
|
+
case "execute":
|
|
87
|
+
case "dispatch":
|
|
88
|
+
return { affectedScope: "workspace", reversibility: "limited" };
|
|
89
|
+
case "system_modify":
|
|
90
|
+
case "git_destructive":
|
|
91
|
+
case "unknown":
|
|
92
|
+
return { affectedScope: "system", reversibility: "unknown" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function decisionFactsForPermission(input: {
|
|
97
|
+
tool: string;
|
|
98
|
+
actionClass: ActionClass;
|
|
99
|
+
axis: Exclude<DecisionAxis, { kind: "answer" }>;
|
|
100
|
+
origin: DecisionOrigin;
|
|
101
|
+
exposure?: AutonomyExposure;
|
|
102
|
+
}): TrustedDecisionFacts {
|
|
103
|
+
const exposure = input.exposure ?? "local";
|
|
104
|
+
const consequence = actionConsequence(input.actionClass);
|
|
105
|
+
return {
|
|
106
|
+
requestKind: "permission",
|
|
107
|
+
axis: input.axis,
|
|
108
|
+
exposure,
|
|
109
|
+
affectedScope: exposure === "outward" ? "outward" : consequence.affectedScope,
|
|
110
|
+
reversibility: exposure === "outward" ? "limited" : consequence.reversibility,
|
|
111
|
+
origin: input.origin,
|
|
112
|
+
authorityEffect: "grants-once",
|
|
113
|
+
tool: input.tool,
|
|
114
|
+
actionClass: input.actionClass,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function decisionFactsForAnswer(exposure: AutonomyExposure): TrustedDecisionFacts {
|
|
119
|
+
return {
|
|
120
|
+
requestKind: "question",
|
|
121
|
+
axis: { kind: "answer" },
|
|
122
|
+
exposure,
|
|
123
|
+
affectedScope: exposure === "outward" ? "outward" : "conversation",
|
|
124
|
+
reversibility: exposure === "outward" ? "limited" : "reversible",
|
|
125
|
+
origin: { kind: "main" },
|
|
126
|
+
authorityEffect: "records-answer",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function classifyTier(facts: TrustedDecisionFacts): DecisionTier {
|
|
131
|
+
if (facts.origin.kind === "worker") return "worker";
|
|
132
|
+
if (facts.affectedScope === "system") return "system";
|
|
133
|
+
if (facts.exposure === "outward" || facts.affectedScope === "outward") return "outward";
|
|
134
|
+
if (facts.axis.kind === "safety-net") return "safety-net";
|
|
135
|
+
if (facts.authorityEffect === "grants-once") return "workspace";
|
|
136
|
+
return "conversation";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function tierIdentity(tier: DecisionTier): {
|
|
140
|
+
tierLabel: string;
|
|
141
|
+
title: string;
|
|
142
|
+
semanticToken: DecisionSemanticToken;
|
|
143
|
+
} {
|
|
144
|
+
switch (tier) {
|
|
145
|
+
case "conversation":
|
|
146
|
+
return { tierLabel: "Conversational answer", title: "Answer a question", semanticToken: "accent" };
|
|
147
|
+
case "workspace":
|
|
148
|
+
return { tierLabel: "Workspace authority", title: "Approve workspace action", semanticToken: "action" };
|
|
149
|
+
case "outward":
|
|
150
|
+
return { tierLabel: "Outward consequence", title: "Confirm outward consequence", semanticToken: "warning" };
|
|
151
|
+
case "safety-net":
|
|
152
|
+
return { tierLabel: "Safety-net confirmation", title: "Safety-net confirmation", semanticToken: "warning" };
|
|
153
|
+
case "system":
|
|
154
|
+
return { tierLabel: "System change", title: "Approve system change", semanticToken: "warning" };
|
|
155
|
+
case "worker":
|
|
156
|
+
return { tierLabel: "Worker escalation", title: "Worker needs approval", semanticToken: "action" };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function requestedByCopy(facts: TrustedDecisionFacts): string {
|
|
161
|
+
const requester =
|
|
162
|
+
facts.origin.kind === "worker" ? `worker ${facts.origin.agentId} (run ${facts.origin.runId})` : "main agent";
|
|
163
|
+
if (facts.axis.kind === "answer") return requester;
|
|
164
|
+
if (facts.axis.kind === "safety-net") return `${requester} through safety-net rail ${facts.axis.ruleId}`;
|
|
165
|
+
return `${requester} through autonomy level (${facts.axis.level})`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function authorizationCopy(facts: TrustedDecisionFacts): string {
|
|
169
|
+
if (facts.authorityEffect === "records-answer") {
|
|
170
|
+
return facts.exposure === "outward"
|
|
171
|
+
? "Your response records an answer about an outward step. It does not publish or send anything by itself."
|
|
172
|
+
: "Your response records an answer for this interview. It grants no tool authority.";
|
|
173
|
+
}
|
|
174
|
+
const tool = facts.tool ?? "the presented tool";
|
|
175
|
+
const actionClass = facts.actionClass ?? "unknown";
|
|
176
|
+
if (tool === "ask_user" && facts.exposure === "outward") {
|
|
177
|
+
return "Approval opens this one outward-decision interview. It does not publish or send anything by itself.";
|
|
178
|
+
}
|
|
179
|
+
return `Approval authorizes one ${actionClass} call to ${tool}. It does not change the autonomy level.`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function consequenceCopy(facts: TrustedDecisionFacts, tier: DecisionTier): string {
|
|
183
|
+
switch (tier) {
|
|
184
|
+
case "conversation":
|
|
185
|
+
return "The answer stays in this session's decision record.";
|
|
186
|
+
case "workspace":
|
|
187
|
+
return facts.actionClass === "read"
|
|
188
|
+
? "The call reads within the workspace without mutating it."
|
|
189
|
+
: "The call can affect files or processes inside the workspace.";
|
|
190
|
+
case "outward":
|
|
191
|
+
return "The resulting step can reach people or systems outside the workspace.";
|
|
192
|
+
case "safety-net":
|
|
193
|
+
return "An always-on safety-net rail requires a one-shot operator decision before this call can run.";
|
|
194
|
+
case "system":
|
|
195
|
+
return "The call can change state outside the workspace or state the classifier cannot safely bound.";
|
|
196
|
+
case "worker":
|
|
197
|
+
return "A dispatched worker is parked. Your answer returns only to that run's exact request.";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function reversibilityCopy(facts: TrustedDecisionFacts): string {
|
|
202
|
+
if (facts.requestKind === "question") {
|
|
203
|
+
return facts.reversibility === "reversible"
|
|
204
|
+
? "Reversible: yes. The recorded answer can be superseded later."
|
|
205
|
+
: "Reversible: not guaranteed. An outward step that later uses this answer may have lasting effects.";
|
|
206
|
+
}
|
|
207
|
+
if (facts.actionClass === "read" && facts.reversibility === "reversible") {
|
|
208
|
+
return "Reversible: yes. This call does not mutate the workspace.";
|
|
209
|
+
}
|
|
210
|
+
switch (facts.reversibility) {
|
|
211
|
+
case "reversible":
|
|
212
|
+
return "Reversible: yes. Workspace changes can be reviewed and reverted.";
|
|
213
|
+
case "limited":
|
|
214
|
+
return "Reversible: not guaranteed. Effects that already ran or reached an outward system may remain.";
|
|
215
|
+
case "unknown":
|
|
216
|
+
return "Reversible: unknown. Treat the effect as not safely reversible.";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function requiredActions(facts: TrustedDecisionFacts, tier: DecisionTier): ReadonlyArray<DecisionPresentationAction> {
|
|
221
|
+
if (facts.authorityEffect === "records-answer") {
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
id: "record-answer",
|
|
225
|
+
label: facts.exposure === "outward" ? "Record outward answer" : "Record answer",
|
|
226
|
+
consequence: "Records the selected or typed answer without granting tool authority.",
|
|
227
|
+
},
|
|
228
|
+
{ id: "cancel", label: "Cancel", consequence: "Closes the interview without recording an answer for this round." },
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
const approveLabel =
|
|
232
|
+
tier === "system"
|
|
233
|
+
? "Approve system change once"
|
|
234
|
+
: tier === "safety-net"
|
|
235
|
+
? "Approve guarded action once"
|
|
236
|
+
: tier === "worker"
|
|
237
|
+
? "Approve worker request once"
|
|
238
|
+
: tier === "outward"
|
|
239
|
+
? "Approve outward decision once"
|
|
240
|
+
: "Approve workspace action once";
|
|
241
|
+
const stopConsequence =
|
|
242
|
+
facts.origin.kind === "worker"
|
|
243
|
+
? "Denies this worker request and ends the active main-agent turn. Other worker requests retain their timeout policy."
|
|
244
|
+
: "Denies every parked request from this turn and ends the run.";
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
id: "approve-once",
|
|
248
|
+
label: approveLabel,
|
|
249
|
+
consequence: "Runs only the presented request and does not change the autonomy level.",
|
|
250
|
+
},
|
|
251
|
+
{ id: "deny", label: "Deny this request", consequence: "Denies only the presented request and advances the queue." },
|
|
252
|
+
{ id: "stop", label: "Deny and stop", consequence: stopConsequence },
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Pure presentation classifier. It performs no I/O and has no execution authority. */
|
|
257
|
+
export function classifyDecisionPresentation(facts: TrustedDecisionFacts): DecisionPresentation {
|
|
258
|
+
const tier = classifyTier(facts);
|
|
259
|
+
return {
|
|
260
|
+
tier,
|
|
261
|
+
...tierIdentity(tier),
|
|
262
|
+
authorizationCopy: authorizationCopy(facts),
|
|
263
|
+
consequenceCopy: consequenceCopy(facts, tier),
|
|
264
|
+
reversibilityCopy: reversibilityCopy(facts),
|
|
265
|
+
requestedByCopy: requestedByCopy(facts),
|
|
266
|
+
requiredActions: requiredActions(facts, tier),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-time secret redaction for tool arguments.
|
|
3
|
+
*
|
|
4
|
+
* Two surfaces need the same rules and must not drift. The transcript's tool
|
|
5
|
+
* renderer projects a call's arguments for the operator's own screen, and the
|
|
6
|
+
* worker tool-admission seam composes a bounded action descriptor that crosses
|
|
7
|
+
* the NDJSON stdout boundary. A credential that one of them scrubs and the
|
|
8
|
+
* other prints is a leak, so the patterns live here and both import them.
|
|
9
|
+
*
|
|
10
|
+
* This is presentation only. Execution, receipts, and the safety verdict all
|
|
11
|
+
* keep the original argument values; nothing here changes what a tool runs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SECRET_KEY_RE =
|
|
19
|
+
/(?:api[-_]?key|access[-_]?key|auth(?:orization)?|credential|password|private[-_]?key|secret|token)/iu;
|
|
20
|
+
const ENV_KEY_RE = /^env(?:ironment)?$/iu;
|
|
21
|
+
const SECRET_STRING_RE = /(https?:\/\/[^\s/@]+:)[^\s/@]+@/giu;
|
|
22
|
+
const SECRET_URL_PARAM_RE =
|
|
23
|
+
/([?&](?:api[-_]?key|access[-_]?token|auth(?:orization)?|credential|password|secret|token)[^=\s]*=)[^&#\s]+/giu;
|
|
24
|
+
const SECRET_ASSIGNMENT_RE =
|
|
25
|
+
/(?<![A-Z0-9_])((?:[A-Z0-9_]*(?:ACCESS|API|AUTH|CREDENTIAL|KEY|PASS(?:WORD)?|PRIVATE|SECRET|TOKEN)[A-Z0-9_]*)=)[^\s;&|]+/giu;
|
|
26
|
+
const SECRET_FLAG_RE =
|
|
27
|
+
/(--?(?:[a-z0-9-]*(?:api[-_]?key|access[-_]?token|auth(?:orization)?|credential|password|secret|token)[a-z0-9-]*))(=|\s+)([^\s;&|]+)/giu;
|
|
28
|
+
|
|
29
|
+
/** Scrub credentials embedded in one string: URL userinfo, query params, `KEY=value`, and `--flag value`. */
|
|
30
|
+
export function redactSecretString(value: string): string {
|
|
31
|
+
return value
|
|
32
|
+
.replace(SECRET_STRING_RE, "$1[redacted]@")
|
|
33
|
+
.replace(SECRET_URL_PARAM_RE, "$1[redacted]")
|
|
34
|
+
.replace(SECRET_ASSIGNMENT_RE, "$1[redacted]")
|
|
35
|
+
.replace(SECRET_FLAG_RE, "$1$2[redacted]");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Whether an argument key names a secret outright, so its value never renders. */
|
|
39
|
+
export function isSecretArgKey(key: string): boolean {
|
|
40
|
+
return SECRET_KEY_RE.test(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether an argument key names an environment block, whose every string value is a candidate secret. */
|
|
44
|
+
function isEnvironmentArgKey(key: string): boolean {
|
|
45
|
+
return ENV_KEY_RE.test(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redactEnvironmentValue(value: unknown, depth: number): unknown {
|
|
49
|
+
if (depth > 8) return "[redacted nested values]";
|
|
50
|
+
if (typeof value === "string") return "[redacted]";
|
|
51
|
+
if (Array.isArray(value)) return value.map((item) => redactEnvironmentValue(item, depth + 1));
|
|
52
|
+
if (!isPlainObject(value)) return value;
|
|
53
|
+
const out: Record<string, unknown> = {};
|
|
54
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
55
|
+
out[childKey] = redactEnvironmentValue(childValue, depth + 1);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Project tool arguments for display only; execution and receipts retain the original values. */
|
|
61
|
+
export function redactToolArgs(value: unknown, key = "", depth = 0): unknown {
|
|
62
|
+
if (isSecretArgKey(key)) return "[redacted]";
|
|
63
|
+
if (isEnvironmentArgKey(key)) return redactEnvironmentValue(value, depth);
|
|
64
|
+
if (typeof value === "string") return redactSecretString(value);
|
|
65
|
+
if (depth > 8) return "[redacted nested values]";
|
|
66
|
+
if (Array.isArray(value)) return value.map((item) => redactToolArgs(item, key, depth + 1));
|
|
67
|
+
if (!isPlainObject(value)) return value;
|
|
68
|
+
const out: Record<string, unknown> = {};
|
|
69
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
70
|
+
out[childKey] = redactToolArgs(childValue, childKey, depth + 1);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* proportional reconciliation the footer bar already performs.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { ContextWindowSource } from "../providers/index.js";
|
|
19
|
+
import type { ContextWindowSlots, ContextWindowSource } from "../providers/index.js";
|
|
20
20
|
import { DEFAULT_COMPACTION_THRESHOLD } from "./compaction/auto.js";
|
|
21
21
|
|
|
22
22
|
/** Distinct buckets a context window is divided into for display. */
|
|
@@ -46,6 +46,8 @@ export interface BuildContextLedgerInput {
|
|
|
46
46
|
contextWindow: number | null;
|
|
47
47
|
/** Which layer answered `contextWindow`; omitted before a runtime resolves. */
|
|
48
48
|
contextWindowSource?: ContextWindowSource | null;
|
|
49
|
+
/** The server-side split the probed window is a share of, when there is one. */
|
|
50
|
+
contextWindowSlots?: ContextWindowSlots | null;
|
|
49
51
|
/**
|
|
50
52
|
* Per-segment token estimates from the prompt compiler's segment manifest.
|
|
51
53
|
* When empty, `systemPromptTokens` is used as a single opaque system bucket.
|
|
@@ -146,6 +148,12 @@ export interface ContextLedger {
|
|
|
146
148
|
* the difference decides whether autocompact fires in time.
|
|
147
149
|
*/
|
|
148
150
|
contextWindowSource: ContextWindowSource | null;
|
|
151
|
+
/**
|
|
152
|
+
* Present when `contextWindow` is one slot's share of the server's KV
|
|
153
|
+
* budget, so the overlay can show `196,608 (786,432 / 4 slots)` instead of
|
|
154
|
+
* a number that looks like the whole server.
|
|
155
|
+
*/
|
|
156
|
+
contextWindowSlots: ContextWindowSlots | null;
|
|
149
157
|
/** Sum of every content category (system..pending). */
|
|
150
158
|
usedTokens: number;
|
|
151
159
|
/** Autocompact headroom held in reserve above the conversation. */
|
|
@@ -346,6 +354,7 @@ export function buildContextLedger(input: BuildContextLedgerInput): ContextLedge
|
|
|
346
354
|
model: input.model ?? null,
|
|
347
355
|
contextWindow: window,
|
|
348
356
|
contextWindowSource: input.contextWindowSource ?? null,
|
|
357
|
+
contextWindowSlots: input.contextWindowSlots ?? null,
|
|
349
358
|
usedTokens,
|
|
350
359
|
reserveTokens,
|
|
351
360
|
freeTokens,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AskUserToolPolicy } from "../../tools/registry.js";
|
|
2
|
+
import type { AutonomyExposure } from "../safety/autonomy.js";
|
|
2
3
|
import type { DecisionLedgerEntry, DecisionRecord } from "./entries.js";
|
|
3
4
|
|
|
4
5
|
export interface DecisionLedgerEntryFields {
|
|
@@ -11,6 +12,7 @@ export interface DecisionLedgerEntryFields {
|
|
|
11
12
|
roundCount: number;
|
|
12
13
|
summary?: string;
|
|
13
14
|
transcriptPath?: string;
|
|
15
|
+
exposure?: AutonomyExposure;
|
|
14
16
|
decisions: DecisionRecord[];
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -83,6 +85,7 @@ export function finalizedInterviewEntryFields(policy: AskUserToolPolicy): Decisi
|
|
|
83
85
|
roundCount: policy.rounds.length,
|
|
84
86
|
...(policy.summary ? { summary: policy.summary } : {}),
|
|
85
87
|
...(policy.transcriptPath ? { transcriptPath: policy.transcriptPath } : {}),
|
|
88
|
+
exposure: policy.exposure ?? "local",
|
|
86
89
|
decisions: [...decisionsByKey.values()],
|
|
87
90
|
};
|
|
88
91
|
}
|
|
@@ -172,6 +175,7 @@ export function createDecisionBoardStore(deps: DecisionBoardStoreDeps = {}): Dec
|
|
|
172
175
|
roundCount: interview.roundCount,
|
|
173
176
|
...(interview.summary ? { summary: interview.summary } : {}),
|
|
174
177
|
...(interview.transcriptPath ? { transcriptPath: interview.transcriptPath } : {}),
|
|
178
|
+
...(interview.exposure ? { exposure: interview.exposure } : {}),
|
|
175
179
|
decisions: interview.decisions.map((decision) => {
|
|
176
180
|
if (decision.key !== selected.key) return { ...decision };
|
|
177
181
|
const next: DecisionRecord = { ...decision, status: "superseded", revisedAt };
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { isSkillActivation, type SkillActivation } from "../../core/skill-activation.js";
|
|
15
15
|
import type { ClioTurnRecord } from "../../engine/session.js";
|
|
16
|
+
import { AUTONOMY_EXPOSURES, type AutonomyExposure } from "../safety/autonomy.js";
|
|
16
17
|
|
|
17
18
|
export interface SessionHeader {
|
|
18
19
|
type: "session";
|
|
@@ -229,6 +230,7 @@ export interface DecisionLedgerEntry extends BaseSessionEntry {
|
|
|
229
230
|
roundCount: number;
|
|
230
231
|
summary?: string;
|
|
231
232
|
transcriptPath?: string;
|
|
233
|
+
exposure?: AutonomyExposure;
|
|
232
234
|
decisions: DecisionRecord[];
|
|
233
235
|
}
|
|
234
236
|
|
|
@@ -643,6 +645,7 @@ export function isSessionEntry(value: unknown): value is SessionEntry {
|
|
|
643
645
|
v.roundCount >= 0 &&
|
|
644
646
|
isOptionalString(v.summary) &&
|
|
645
647
|
isOptionalString(v.transcriptPath) &&
|
|
648
|
+
(v.exposure === undefined || isOneOf(v.exposure, AUTONOMY_EXPOSURES)) &&
|
|
646
649
|
Array.isArray(v.decisions) &&
|
|
647
650
|
v.decisions.every(isDecisionRecord)
|
|
648
651
|
);
|
|
@@ -48,21 +48,15 @@ function collapseWhitespace(text: string): string {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Pull the first non-empty piece of
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* Pull the first non-empty piece of text out of a message payload. Structured
|
|
52
|
+
* message payloads may contain a bare string, a text property, or pi-ai
|
|
53
|
+
* content blocks.
|
|
54
54
|
*/
|
|
55
|
-
function
|
|
56
|
-
if (typeof payload === "string")
|
|
57
|
-
const collapsed = collapseWhitespace(payload);
|
|
58
|
-
return collapsed.length > 0 ? collapsed : null;
|
|
59
|
-
}
|
|
55
|
+
function rawMessageText(payload: unknown): string | null {
|
|
56
|
+
if (typeof payload === "string") return payload.trim().length > 0 ? payload : null;
|
|
60
57
|
if (!payload || typeof payload !== "object") return null;
|
|
61
58
|
const obj = payload as Record<string, unknown>;
|
|
62
|
-
if (typeof obj.text === "string")
|
|
63
|
-
const collapsed = collapseWhitespace(obj.text);
|
|
64
|
-
if (collapsed.length > 0) return collapsed;
|
|
65
|
-
}
|
|
59
|
+
if (typeof obj.text === "string" && obj.text.trim().length > 0) return obj.text;
|
|
66
60
|
if (Array.isArray(obj.content)) {
|
|
67
61
|
const parts: string[] = [];
|
|
68
62
|
for (const part of obj.content) {
|
|
@@ -75,14 +69,65 @@ function extractUserText(payload: unknown): string | null {
|
|
|
75
69
|
parts.push((part as { text: string }).text);
|
|
76
70
|
}
|
|
77
71
|
}
|
|
78
|
-
const joined =
|
|
79
|
-
if (joined.length > 0) return joined;
|
|
72
|
+
const joined = parts.join("\n");
|
|
73
|
+
if (joined.trim().length > 0) return joined;
|
|
80
74
|
}
|
|
81
75
|
return null;
|
|
82
76
|
}
|
|
83
77
|
|
|
78
|
+
function extractMessageText(payload: unknown): string | null {
|
|
79
|
+
const raw = rawMessageText(payload);
|
|
80
|
+
if (raw === null) return null;
|
|
81
|
+
const collapsed = collapseWhitespace(raw);
|
|
82
|
+
return collapsed.length > 0 ? collapsed : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What the operator typed for a user turn, for the picker preview.
|
|
87
|
+
*
|
|
88
|
+
* The persisted text is the composed prompt: a `<system-reminder>` block and
|
|
89
|
+
* any `[Skill request]` preamble ride ahead of the operator's words as visible
|
|
90
|
+
* text the model receives. Reading that verbatim made every row of the
|
|
91
|
+
* `/resume` picker read `<system-reminder> [Skills] 9 installed…`, so sessions
|
|
92
|
+
* were indistinguishable and the type-to-filter matched them all (issue #188).
|
|
93
|
+
* Entries written since the operator text was persisted carry it as
|
|
94
|
+
* `operatorText`; older ones drop the leading scaffolding. Null when nothing
|
|
95
|
+
* operator-authored remains, so the caller moves on to the next turn.
|
|
96
|
+
*/
|
|
97
|
+
function operatorTextOfUserPayload(payload: unknown): string | null {
|
|
98
|
+
if (
|
|
99
|
+
payload &&
|
|
100
|
+
typeof payload === "object" &&
|
|
101
|
+
typeof (payload as { operatorText?: unknown }).operatorText === "string"
|
|
102
|
+
) {
|
|
103
|
+
const collapsed = collapseWhitespace((payload as { operatorText: string }).operatorText);
|
|
104
|
+
return collapsed.length > 0 ? collapsed : null;
|
|
105
|
+
}
|
|
106
|
+
// Strip before collapsing: the preamble's line structure is what the
|
|
107
|
+
// patterns anchor on.
|
|
108
|
+
const text = rawMessageText(payload);
|
|
109
|
+
if (text === null) return null;
|
|
110
|
+
const stripped = collapseWhitespace(stripInjectedPreamble(text));
|
|
111
|
+
return stripped.length > 0 ? stripped : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const LEADING_SYSTEM_REMINDER = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*/u;
|
|
115
|
+
const LEADING_SKILL_REQUEST =
|
|
116
|
+
/^\s*\[Skill request\][\s\S]*?Only these pending skill names are allowed this turn\.[^\n]*\s*/u;
|
|
117
|
+
|
|
118
|
+
function stripInjectedPreamble(text: string): string {
|
|
119
|
+
let current = text;
|
|
120
|
+
for (;;) {
|
|
121
|
+
const next = current.replace(LEADING_SYSTEM_REMINDER, "").replace(LEADING_SKILL_REQUEST, "");
|
|
122
|
+
if (next === current) return current;
|
|
123
|
+
current = next;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
84
127
|
interface ScanResult {
|
|
85
128
|
firstUserMessage: string | null;
|
|
129
|
+
/** The first assistant text, the preview when no turn carries operator words. */
|
|
130
|
+
firstAssistantMessage: string | null;
|
|
86
131
|
messageCount: number;
|
|
87
132
|
lastTimestamp: string | null;
|
|
88
133
|
name: string | null;
|
|
@@ -92,6 +137,7 @@ interface ScanResult {
|
|
|
92
137
|
function scanCurrentJsonl(currentPath: string): ScanResult {
|
|
93
138
|
const result: ScanResult = {
|
|
94
139
|
firstUserMessage: null,
|
|
140
|
+
firstAssistantMessage: null,
|
|
95
141
|
messageCount: 0,
|
|
96
142
|
lastTimestamp: null,
|
|
97
143
|
name: null,
|
|
@@ -106,10 +152,14 @@ function scanCurrentJsonl(currentPath: string): ScanResult {
|
|
|
106
152
|
if (parsed.kind === "message" && parsed.role === "user") {
|
|
107
153
|
result.messageCount += 1;
|
|
108
154
|
if (result.firstUserMessage === null) {
|
|
109
|
-
const text =
|
|
155
|
+
const text = operatorTextOfUserPayload(parsed.payload);
|
|
110
156
|
if (text !== null) result.firstUserMessage = text;
|
|
111
157
|
}
|
|
112
158
|
}
|
|
159
|
+
if (parsed.kind === "message" && parsed.role === "assistant" && result.firstAssistantMessage === null) {
|
|
160
|
+
const text = extractMessageText(parsed.payload);
|
|
161
|
+
if (text !== null) result.firstAssistantMessage = text;
|
|
162
|
+
}
|
|
113
163
|
if (parsed.kind === "sessionInfo") {
|
|
114
164
|
const info = parsed as SessionInfoEntry;
|
|
115
165
|
if (info.name !== undefined) {
|
|
@@ -139,11 +189,10 @@ function enrichMetaForListing(meta: SessionMeta, currentPath: string): void {
|
|
|
139
189
|
if (scan.name) meta.name = scan.name;
|
|
140
190
|
const labelValues = [...scan.labels.values()].map((entry) => entry.label.trim()).filter((label) => label.length > 0);
|
|
141
191
|
if (labelValues.length > 0) meta.labels = labelValues;
|
|
142
|
-
|
|
192
|
+
const preview = scan.firstUserMessage ?? scan.firstAssistantMessage;
|
|
193
|
+
if (preview) {
|
|
143
194
|
meta.firstMessagePreview =
|
|
144
|
-
|
|
145
|
-
? `${scan.firstUserMessage.slice(0, PREVIEW_MAX_CHARS - 1)}…`
|
|
146
|
-
: scan.firstUserMessage;
|
|
195
|
+
preview.length > PREVIEW_MAX_CHARS ? `${preview.slice(0, PREVIEW_MAX_CHARS - 1)}…` : preview;
|
|
147
196
|
}
|
|
148
197
|
if (scan.messageCount > 0) meta.messageCount = scan.messageCount;
|
|
149
198
|
const fallbackMtime = readMtimeIso(currentPath);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fold a session ledger's assistant turns into the per-call usage the rest of
|
|
3
3
|
* Clio reports: tokens in and out, cache reads and writes, reasoning tokens,
|
|
4
|
-
* and provider-reported cost, attributed to the target and
|
|
5
|
-
*
|
|
4
|
+
* and provider-reported cost, attributed to the target and observed model id
|
|
5
|
+
* for each call.
|
|
6
6
|
*
|
|
7
7
|
* This lives in the session domain because the ledger is the only durable
|
|
8
8
|
* record of what a session spent, and three surfaces read it: the `/cost`
|
|
@@ -19,12 +19,22 @@
|
|
|
19
19
|
* counting it would add an API call worth nothing.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import {
|
|
23
|
+
attributedModelId,
|
|
24
|
+
type ResponseModelIdObservation,
|
|
25
|
+
responseModelIdObservationFromRecord,
|
|
26
|
+
} from "../../core/response-model-id.js";
|
|
22
27
|
import type { SessionEntry } from "./entries.js";
|
|
23
28
|
|
|
24
29
|
/** One completed assistant API call, as the ledger recorded it. */
|
|
25
30
|
export interface LedgerUsageCall {
|
|
26
31
|
providerId: string;
|
|
27
|
-
|
|
32
|
+
/** Model id the accounting row uses, or `unknown` when an observed response omitted it. */
|
|
33
|
+
attributedModelId: string;
|
|
34
|
+
/** The id the session asked for (the configured model, or the message's own `model`). */
|
|
35
|
+
requestedModelId: string;
|
|
36
|
+
/** Direct presence observation, with the pre-#193 difference-only shape labeled separately. */
|
|
37
|
+
responseModelIdObservation: ResponseModelIdObservation;
|
|
28
38
|
input: number;
|
|
29
39
|
output: number;
|
|
30
40
|
cacheRead: number;
|
|
@@ -71,8 +81,8 @@ function stringAt(source: Record<string, unknown>, ...keys: string[]): string |
|
|
|
71
81
|
* payload instead split one endpoint into two blocks in `/cost`, so a single
|
|
72
82
|
* `node-a` target on `llamacpp` rendered as two providers whose turn counts
|
|
73
83
|
* diverged with every resume. `modelChange` rows are replayed in order so a
|
|
74
|
-
* session that switched targets mid-way attributes each call to the
|
|
75
|
-
*
|
|
84
|
+
* session that switched targets mid-way attributes each call to the active
|
|
85
|
+
* target.
|
|
76
86
|
*/
|
|
77
87
|
export function ledgerUsageCalls(
|
|
78
88
|
entries: ReadonlyArray<SessionEntry>,
|
|
@@ -92,7 +102,9 @@ export function ledgerUsageCalls(
|
|
|
92
102
|
if (!usage || usage.totalTokens <= 0) continue;
|
|
93
103
|
calls.push({
|
|
94
104
|
providerId: currentTarget ?? "unknown",
|
|
95
|
-
|
|
105
|
+
attributedModelId: currentModel ?? "unknown",
|
|
106
|
+
requestedModelId: currentModel ?? "unknown",
|
|
107
|
+
responseModelIdObservation: { state: "not-observed" },
|
|
96
108
|
input: usage.input,
|
|
97
109
|
output: usage.output,
|
|
98
110
|
cacheRead: usage.cacheRead,
|
|
@@ -132,9 +144,14 @@ export function ledgerUsageCalls(
|
|
|
132
144
|
if (totalTokens === 0) continue;
|
|
133
145
|
const cost = usage.cost;
|
|
134
146
|
const costUsd = cost && typeof cost === "object" ? numberAt(cost as Record<string, unknown>, "total") : 0;
|
|
147
|
+
const requestedModelId = currentModel ?? stringAt(record, "model") ?? "unknown";
|
|
148
|
+
const differingResponseModelId = stringAt(record, "responseModel");
|
|
149
|
+
const responseModelIdObservation = responseModelIdObservationFromRecord(record, "legacy-difference-only");
|
|
135
150
|
calls.push({
|
|
136
151
|
providerId: currentTarget ?? stringAt(record, "provider", "api") ?? "unknown",
|
|
137
|
-
|
|
152
|
+
attributedModelId: attributedModelId(responseModelIdObservation, requestedModelId, differingResponseModelId),
|
|
153
|
+
requestedModelId,
|
|
154
|
+
responseModelIdObservation,
|
|
138
155
|
input,
|
|
139
156
|
output,
|
|
140
157
|
cacheRead,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { describeCallAction } from "../../domains/safety/call-target.js";
|
|
2
3
|
import type { AgentEvent, AgentMessage } from "../types.js";
|
|
3
4
|
import type { ClioWorkerEvent } from "../worker-events.js";
|
|
4
5
|
import type { AcpPromptResponse, AcpSessionUpdateParams, AcpToolCallUpdate } from "./types.js";
|
|
@@ -111,12 +112,17 @@ export class AcpEventMapper {
|
|
|
111
112
|
// keeps the monotonic twin, which is what the duration below spans.
|
|
112
113
|
const startedAt = Date.now();
|
|
113
114
|
this.toolStarts.set(toolCallId, performance.now());
|
|
115
|
+
// A peer's `rawInput` is its own argument object and never reaches an
|
|
116
|
+
// operator surface. The descriptor composed from it here does.
|
|
117
|
+
const action = describeCallAction(update.kind ?? title, update.rawInput);
|
|
114
118
|
out.push({
|
|
115
119
|
type: "clio_tool_start",
|
|
116
120
|
payload: {
|
|
117
121
|
tool: update.kind ?? title,
|
|
122
|
+
toolCallId,
|
|
118
123
|
posture: "operating",
|
|
119
124
|
startedAt,
|
|
125
|
+
...(action !== null ? { action } : {}),
|
|
120
126
|
},
|
|
121
127
|
});
|
|
122
128
|
out.push({
|
|
@@ -142,6 +148,7 @@ export class AcpEventMapper {
|
|
|
142
148
|
type: "clio_tool_finish",
|
|
143
149
|
payload: {
|
|
144
150
|
tool: update.kind ?? title,
|
|
151
|
+
toolCallId,
|
|
145
152
|
posture: "operating",
|
|
146
153
|
durationMs: Math.round(performance.now() - startedAtClock),
|
|
147
154
|
outcome: isError ? "error" : "ok",
|