@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
package/src/tools/agent-tools.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { type SkillActivation, skillActivationFromToolDetails } from "../core/sk
|
|
|
22
22
|
import type { ToolName } from "../core/tool-names.js";
|
|
23
23
|
import { ToolNames } from "../core/tool-names.js";
|
|
24
24
|
import type { ResolvedRuntimeTarget } from "../domains/providers/index.js";
|
|
25
|
+
import { type CallActionDescriptor, describeCallAction } from "../domains/safety/call-target.js";
|
|
25
26
|
import type { SafetyDecision } from "../domains/safety/contract.js";
|
|
26
27
|
import { formatModelRejection } from "../domains/safety/rejection-feedback.js";
|
|
27
28
|
import { validateEngineToolArguments } from "../engine/ai.js";
|
|
@@ -45,6 +46,12 @@ export type ToolOutcome = "ok" | "error" | "blocked";
|
|
|
45
46
|
|
|
46
47
|
export interface ToolStartEvent {
|
|
47
48
|
tool: string;
|
|
49
|
+
/**
|
|
50
|
+
* The engine's id for this call, when the producer has one. New producers
|
|
51
|
+
* put the same id on start and finish so concurrent calls of one tool remain
|
|
52
|
+
* independently attributable. Optional for direct callers and old streams.
|
|
53
|
+
*/
|
|
54
|
+
toolCallId?: string;
|
|
48
55
|
posture: "operating";
|
|
49
56
|
/**
|
|
50
57
|
* Wall-clock anchor for the instant the call began, for a human correlating
|
|
@@ -53,6 +60,14 @@ export interface ToolStartEvent {
|
|
|
53
60
|
* event is measured on that process's monotonic clock instead.
|
|
54
61
|
*/
|
|
55
62
|
startedAt: number;
|
|
63
|
+
/**
|
|
64
|
+
* What this call is doing, as a bounded redacted descriptor composed here
|
|
65
|
+
* where the arguments are trusted. This is the only account of a worker's
|
|
66
|
+
* arguments that may cross the NDJSON stdout seam; the arguments themselves
|
|
67
|
+
* never do, so an operator surface reads a verb and an object it can show,
|
|
68
|
+
* never an argument object it would have to sanitize itself.
|
|
69
|
+
*/
|
|
70
|
+
action?: CallActionDescriptor;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
export interface ToolFinishEvent {
|
|
@@ -130,11 +145,21 @@ async function runValidatedToolCall(input: RunValidatedToolCallInput): Promise<W
|
|
|
130
145
|
// spans the call, so `durationMs` survives a clock correction mid-tool.
|
|
131
146
|
const startedAt = Date.now();
|
|
132
147
|
const startedAtClock = performance.now();
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
// outcome to the engine's `tool_execution_end` for the same call.
|
|
148
|
+
// Stamped onto both lifecycle events so a concurrent finish resolves only
|
|
149
|
+
// the start for its own engine call. Direct callers may legitimately omit it.
|
|
136
150
|
const callId = input.invokeOptions?.toolCallId;
|
|
137
151
|
const withCallId = callId === undefined ? {} : { toolCallId: callId };
|
|
152
|
+
// The one seam that holds validated arguments and publishes telemetry, so it
|
|
153
|
+
// is where the redacted descriptor is composed. Everything downstream of here
|
|
154
|
+
// sees the descriptor and never the arguments.
|
|
155
|
+
const action = describeCallAction(spec.name, args);
|
|
156
|
+
telemetry?.onStart?.({
|
|
157
|
+
tool: spec.name,
|
|
158
|
+
...withCallId,
|
|
159
|
+
posture: "operating",
|
|
160
|
+
startedAt,
|
|
161
|
+
...(action !== null ? { action } : {}),
|
|
162
|
+
});
|
|
138
163
|
const invokeOpts: ToolInvokeOptions = {};
|
|
139
164
|
if (input.invokeOptions) Object.assign(invokeOpts, input.invokeOptions);
|
|
140
165
|
if (signal) invokeOpts.signal = signal;
|
package/src/tools/ask-user.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
import { ToolNames } from "../core/tool-names.js";
|
|
6
6
|
import { clioStateDir } from "../core/xdg.js";
|
|
7
7
|
import { type AutonomyExposure, DEFAULT_AUTONOMY_EXPOSURE } from "../domains/safety/autonomy.js";
|
|
8
|
+
import { classifyDecisionPresentation, decisionFactsForAnswer } from "../domains/safety/decision-presentation.js";
|
|
8
9
|
import { StringEnum } from "../engine/ai.js";
|
|
9
10
|
import type {
|
|
10
11
|
AskUserToolPolicy,
|
|
@@ -57,6 +58,7 @@ export interface AskUserResult {
|
|
|
57
58
|
|
|
58
59
|
export interface AskUserCall {
|
|
59
60
|
action: AskUserAction;
|
|
61
|
+
exposure?: AutonomyExposure;
|
|
60
62
|
mode?: AskUserMode;
|
|
61
63
|
questions?: AskUserQuestion[];
|
|
62
64
|
decisions?: AskUserDecision[];
|
|
@@ -284,6 +286,7 @@ export function normalizeAskUserCall(args: Record<string, unknown>): { call?: As
|
|
|
284
286
|
return {
|
|
285
287
|
call: {
|
|
286
288
|
action: "ask",
|
|
289
|
+
exposure: exposure.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
|
|
287
290
|
...(mode.mode ? { mode: mode.mode } : {}),
|
|
288
291
|
questions: questions.questions,
|
|
289
292
|
...(summary ? { summary } : {}),
|
|
@@ -319,6 +322,7 @@ function createStandalonePolicy(options?: ToolInvokeOptions): AskUserToolPolicy
|
|
|
319
322
|
updatedAt: now,
|
|
320
323
|
...(options?.sessionId ? { sessionId: options.sessionId } : {}),
|
|
321
324
|
...(options?.turnId ? { turnId: options.turnId } : {}),
|
|
325
|
+
exposure: DEFAULT_AUTONOMY_EXPOSURE,
|
|
322
326
|
rounds: [],
|
|
323
327
|
decisions: [],
|
|
324
328
|
inFlight: false,
|
|
@@ -335,6 +339,13 @@ function hydratePolicy(policy: AskUserToolPolicy, options?: ToolInvokeOptions):
|
|
|
335
339
|
if (options?.turnId && !policy.turnId) policy.turnId = options.turnId;
|
|
336
340
|
}
|
|
337
341
|
|
|
342
|
+
/** Once an interview is outward, a later local round cannot lower its replay tier. */
|
|
343
|
+
function mergePolicyExposure(policy: AskUserToolPolicy, exposure: AutonomyExposure): AutonomyExposure {
|
|
344
|
+
const merged = policy.exposure === "outward" || exposure === "outward" ? "outward" : "local";
|
|
345
|
+
policy.exposure = merged;
|
|
346
|
+
return merged;
|
|
347
|
+
}
|
|
348
|
+
|
|
338
349
|
function transcriptQuestions(questions: ReadonlyArray<AskUserQuestion>): AskUserTranscriptQuestion[] {
|
|
339
350
|
return questions.map((question) => ({
|
|
340
351
|
question: question.question,
|
|
@@ -455,6 +466,7 @@ async function persistAskUserTranscript(
|
|
|
455
466
|
...(policy.endedAt ? { endedAt: policy.endedAt } : {}),
|
|
456
467
|
...(policy.sessionId ? { sessionId: policy.sessionId } : {}),
|
|
457
468
|
...(policy.turnId ? { turnId: policy.turnId } : {}),
|
|
469
|
+
exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
|
|
458
470
|
...(policy.summary ? { summary: policy.summary } : {}),
|
|
459
471
|
decisions: policy.decisions,
|
|
460
472
|
rounds: policy.rounds,
|
|
@@ -478,6 +490,7 @@ function compactInterview(
|
|
|
478
490
|
rounds: policy.rounds.length,
|
|
479
491
|
max_rounds: policy.maxCalls,
|
|
480
492
|
transcript_path: policy.transcriptPath ?? null,
|
|
493
|
+
exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
|
|
481
494
|
latest_answers: latestAnswers,
|
|
482
495
|
decisions: policy.decisions,
|
|
483
496
|
...(policy.summary ? { summary: policy.summary } : {}),
|
|
@@ -591,6 +604,7 @@ export function createAskUserTool(deps: AskUserToolDeps = {}): ToolSpec {
|
|
|
591
604
|
return completeInterview(policy, "complete", options, call.summary, call.decisions ?? []);
|
|
592
605
|
}
|
|
593
606
|
const questions = call.questions ?? [];
|
|
607
|
+
const exposure = mergePolicyExposure(policy, call.exposure ?? DEFAULT_AUTONOMY_EXPOSURE);
|
|
594
608
|
if (call.max_rounds !== undefined) {
|
|
595
609
|
const nextLimit = policy.callCount === 0 ? call.max_rounds : Math.max(policy.maxCalls, call.max_rounds);
|
|
596
610
|
policy.maxCalls = Math.max(nextLimit, policy.callCount + 1);
|
|
@@ -632,7 +646,13 @@ export function createAskUserTool(deps: AskUserToolDeps = {}): ToolSpec {
|
|
|
632
646
|
if (call.summary) policy.summary = call.summary;
|
|
633
647
|
const handler = deps.askUser ?? defaultAskUserHandler;
|
|
634
648
|
try {
|
|
635
|
-
const result = normalizeAskUserResult(
|
|
649
|
+
const result = normalizeAskUserResult(
|
|
650
|
+
questions,
|
|
651
|
+
await handler(questions, {
|
|
652
|
+
...options,
|
|
653
|
+
decisionPresentation: classifyDecisionPresentation(decisionFactsForAnswer(exposure)),
|
|
654
|
+
}),
|
|
655
|
+
);
|
|
636
656
|
const answeredAt = new Date().toISOString();
|
|
637
657
|
policy.callCount += 1;
|
|
638
658
|
policy.askedQuestionKeys.add(key);
|
|
@@ -168,7 +168,7 @@ function policyIsRecipeBound(policy: { requests: ReadonlyArray<{ source: string
|
|
|
168
168
|
// load it (only the operator activates), so its move is suggest-and-wait.
|
|
169
169
|
const NO_PENDING_SKILL_DENIAL =
|
|
170
170
|
"context: no pending skill request is active this turn; only the operator can activate a skill, so do not retry this load. " +
|
|
171
|
-
`If a listed skill matches the task, open your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` and
|
|
171
|
+
`If a listed skill matches the task, open your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` and continue the task without it; only the operator can run it. Otherwise continue without skills.`;
|
|
172
172
|
|
|
173
173
|
function pendingSkillPolicyError(name: string, options: ToolInvokeOptions | undefined): string | null {
|
|
174
174
|
const policy = options?.pendingSkillPolicy;
|
|
@@ -287,7 +287,7 @@ function renderSkillsList(
|
|
|
287
287
|
// template where they skip conditional prose in the header.
|
|
288
288
|
lines.push(
|
|
289
289
|
"",
|
|
290
|
-
`If one skill above matches the current task, begin your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose)
|
|
290
|
+
`If one skill above matches the current task, begin your reply with the line \`${SKILL_SUGGESTION_ANCHOR}\` (a comma-separated sequence, in order, when several compose), then continue the task in the same turn without it; only the operator can run it. If none match, do not mention skills.`,
|
|
291
291
|
);
|
|
292
292
|
return lines.join("\n");
|
|
293
293
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AgentAutomationAuthority } from "../domains/agents/spec.js";
|
|
4
4
|
import { type AgentTaskType, classifyAgentTask } from "../domains/dispatch/agent-candidates.js";
|
|
5
|
+
import { cloneDispatchBudgetRequest } from "../domains/dispatch/budget-envelope.js";
|
|
5
6
|
import type { DispatchRequest } from "../domains/dispatch/contract.js";
|
|
6
7
|
import { type AgentRoleFactsResolver, requestExecutionRole } from "../domains/dispatch/execution-role.js";
|
|
7
8
|
import { parseRoutingIntent } from "../domains/dispatch/routing-intent.js";
|
|
@@ -171,6 +172,13 @@ function dispatchRequestFromArgs(
|
|
|
171
172
|
}
|
|
172
173
|
request.thinkingLevel = thinkingLevel as JobThinkingLevel;
|
|
173
174
|
}
|
|
175
|
+
if (args.budget !== undefined) {
|
|
176
|
+
try {
|
|
177
|
+
request.budget = cloneDispatchBudgetRequest(args.budget);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
174
182
|
return { ok: true, request };
|
|
175
183
|
}
|
|
176
184
|
|
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
import { responseModelIdObservationFromRecord } from "../core/response-model-id.js";
|
|
1
2
|
import { durableAssistantTextFromEvent } from "../domains/dispatch/event-pump.js";
|
|
3
|
+
import type { RunReceipt } from "../domains/dispatch/types.js";
|
|
2
4
|
|
|
3
5
|
/** Return the durable final assistant text carried by a worker event. */
|
|
4
6
|
export function assistantTextFromEvent(event: unknown): string {
|
|
5
7
|
return durableAssistantTextFromEvent(event).trim();
|
|
6
8
|
}
|
|
9
|
+
|
|
10
|
+
/** Human projection of the response model-id observations carried by a receipt. */
|
|
11
|
+
export function receiptResponseModelIdObservationLabel(receipt: Pick<RunReceipt, "upstreamResponses">): string | null {
|
|
12
|
+
const responses = receipt.upstreamResponses ?? [];
|
|
13
|
+
if (responses.length === 0) return null;
|
|
14
|
+
const labels = responses.map((response) => {
|
|
15
|
+
const observation = responseModelIdObservationFromRecord(
|
|
16
|
+
response as unknown as Record<string, unknown>,
|
|
17
|
+
"legacy-difference-only",
|
|
18
|
+
);
|
|
19
|
+
if (observation.state === "reported") return `reported:${observation.reportedModelId}`;
|
|
20
|
+
if (observation.state === "not-reported") return "not-reported";
|
|
21
|
+
if (observation.state === "not-observed") return "not-observed";
|
|
22
|
+
return `legacy-difference-only:${observation.differingModelId ?? "none"}`;
|
|
23
|
+
});
|
|
24
|
+
return `response_model_id_observation=${labels.join(",")}`;
|
|
25
|
+
}
|
package/src/tools/dispatch.ts
CHANGED
|
@@ -25,6 +25,27 @@ export type { DispatchToolDeps } from "./dispatch-types.js";
|
|
|
25
25
|
|
|
26
26
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
27
27
|
|
|
28
|
+
const DispatchBudgetPhaseSchema = Type.Object(
|
|
29
|
+
{
|
|
30
|
+
toolCalls: Type.Integer({ minimum: 1 }),
|
|
31
|
+
readReserve: Type.Integer({ minimum: 0 }),
|
|
32
|
+
},
|
|
33
|
+
{ additionalProperties: false },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const DispatchBudgetSchema = Type.Object(
|
|
37
|
+
{
|
|
38
|
+
toolCalls: Type.Integer({ minimum: 1, description: "Requested tool-call phase boundary." }),
|
|
39
|
+
readReserve: Type.Integer({ minimum: 0, description: "Requested tail reserve for canonical read calls." }),
|
|
40
|
+
retryRevision: Type.Optional(DispatchBudgetPhaseSchema),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
description:
|
|
45
|
+
"Invocation budget inside the recipe policy. retryRevision preauthorizes one ceiling for retry, result-contract revision, or review revision phases.",
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
28
49
|
/**
|
|
29
50
|
* Stable, lightweight dispatch surface. Admission remains synchronous so the
|
|
30
51
|
* policy decision and provisional reservation are bound to the exact argument
|
|
@@ -62,7 +83,7 @@ export function createDispatchTool(
|
|
|
62
83
|
return {
|
|
63
84
|
name: ToolNames.Dispatch,
|
|
64
85
|
description:
|
|
65
|
-
'Dispatch one bounded task with task, or a batch with tasks (never both). Singular example: {agent:"debugger", task:"Verify the receipt boundary", briefing:"Prior receipt evidence...", detach:true}. task is the worker assignment; briefing is separate bounded parent context/data and cannot replace task. Ordinary calls auto-wait; detach:true returns ids for monitoring/steering, and collect is the authoritative terminal batch operation before final synthesis. Batch modes are parallel (default), sequential, pipeline, or compete. Task objects may include persona and
|
|
86
|
+
'Dispatch one bounded task with task, or a batch with tasks (never both). Singular example: {agent:"debugger", task:"Verify the receipt boundary", briefing:"Prior receipt evidence...", detach:true}. task is the worker assignment; briefing is separate bounded parent context/data and cannot replace task. Ordinary calls auto-wait; detach:true returns ids for monitoring/steering, and collect is the authoritative terminal batch operation before final synthesis. Batch modes are parallel (default), sequential, pipeline, or compete. Task objects may include persona, tool_profile, and a recipe-admitted budget envelope. Sealed receipts are durable evidence; report receipt integrity, evidence verification, briefing provenance, and project-context provenance separately. Call with list:true to see agents. Do not repeat an identical successful dispatch in the same user turn. Prefer this tool over inline exploration whenever work is read-only fan-out, parallel investigation, or the operator asked for a worker by name; if you cannot dispatch, say so plainly and name the reason, and never narrate or summarize a worker you did not actually dispatch.',
|
|
66
87
|
parameters: Type.Object({
|
|
67
88
|
list: Type.Optional(Type.Boolean({ description: "List available agents instead of dispatching." })),
|
|
68
89
|
from_scout: Type.Optional(
|
|
@@ -105,6 +126,7 @@ export function createDispatchTool(
|
|
|
105
126
|
tool_profile: Type.Optional(
|
|
106
127
|
StringEnum(TOOL_PROFILE_NAMES, { description: "Narrow this worker's available tools." }),
|
|
107
128
|
),
|
|
129
|
+
budget: Type.Optional(DispatchBudgetSchema),
|
|
108
130
|
target: Type.Optional(Type.String()),
|
|
109
131
|
model: Type.Optional(Type.String()),
|
|
110
132
|
node: Type.Optional(Type.String({ description: "Fleet node pin: local or a fleet.nodes id." })),
|
|
@@ -190,6 +212,7 @@ export function createDispatchTool(
|
|
|
190
212
|
Type.String({ description: "Default ad-hoc specialist persona for dispatched tasks, max 8000 chars." }),
|
|
191
213
|
),
|
|
192
214
|
tool_profile: Type.Optional(StringEnum(TOOL_PROFILE_NAMES, { description: "Default worker tool profile." })),
|
|
215
|
+
budget: Type.Optional(DispatchBudgetSchema),
|
|
193
216
|
target: Type.Optional(Type.String({ description: "Default configured target id (omit for fleet default)." })),
|
|
194
217
|
model: Type.Optional(Type.String({ description: "Default model override." })),
|
|
195
218
|
node: Type.Optional(
|
package/src/tools/monitor.ts
CHANGED
|
@@ -2,6 +2,12 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { sleep } from "../core/timers.js";
|
|
3
3
|
import { renderAgentLedgerBoard } from "../domains/dispatch/agent-ledger-store.js";
|
|
4
4
|
import type { DurableAssignmentRecord } from "../domains/dispatch/assignment-store.js";
|
|
5
|
+
import {
|
|
6
|
+
formatBudgetPolicy,
|
|
7
|
+
formatBudgetReasons,
|
|
8
|
+
formatBudgetRequest,
|
|
9
|
+
formatEffectiveBudget,
|
|
10
|
+
} from "../domains/dispatch/budget-envelope.js";
|
|
5
11
|
import type { DispatchContract } from "../domains/dispatch/contract.js";
|
|
6
12
|
import { UNVERIFIABLE_RECEIPT_VERIFICATION } from "../domains/dispatch/receipt-findings.js";
|
|
7
13
|
import type { ReceiptIntegrityResult } from "../domains/dispatch/receipt-integrity.js";
|
|
@@ -117,6 +123,14 @@ function runStatus(deps: MonitorToolDeps, runId: string): ToolResult {
|
|
|
117
123
|
`started=${run.startedAt} ended=${run.endedAt ?? "n/a"} exit=${run.exitCode ?? "n/a"}`,
|
|
118
124
|
`tokens=${run.tokenCount} cost=${formatCostAggregate(costAggregateForAmount(run.costUsd, run.costProvenance)) ?? COST_NOT_MEASURED} receipt=${run.receiptPath ?? "n/a"}`,
|
|
119
125
|
];
|
|
126
|
+
if (run.budget !== undefined) {
|
|
127
|
+
lines.push(
|
|
128
|
+
`recipe policy: ${formatBudgetPolicy(run.budget)}`,
|
|
129
|
+
`requested envelope: ${formatBudgetRequest(run.budget)}`,
|
|
130
|
+
`effective envelope: ${formatEffectiveBudget(run.budget)}`,
|
|
131
|
+
`clamp or escalation reason: ${formatBudgetReasons(run.budget)}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
120
134
|
if (live) {
|
|
121
135
|
lines.push(
|
|
122
136
|
`live: phase=${live.outcomePhase} heartbeat=${live.heartbeat} elapsed=${Math.round(live.elapsedMs / 1000)}s tokens=${live.tokens.total}`,
|
|
@@ -143,6 +157,7 @@ function runStatus(deps: MonitorToolDeps, runId: string): ToolResult {
|
|
|
143
157
|
tokenCount: run.tokenCount,
|
|
144
158
|
costUsd: run.costUsd,
|
|
145
159
|
costProvenance: run.costProvenance ?? "unknown",
|
|
160
|
+
budget: run.budget ?? null,
|
|
146
161
|
receiptPath: run.receiptPath,
|
|
147
162
|
running: live !== null,
|
|
148
163
|
},
|
package/src/tools/registry.ts
CHANGED
|
@@ -22,13 +22,14 @@ import {
|
|
|
22
22
|
mapAutonomy,
|
|
23
23
|
} from "../domains/safety/autonomy.js";
|
|
24
24
|
import type { SafetyContract, SafetyDecision } from "../domains/safety/contract.js";
|
|
25
|
+
import type { DecisionPresentation } from "../domains/safety/decision-presentation.js";
|
|
25
26
|
import { hashToolCall } from "../domains/safety/loop-detector.js";
|
|
26
27
|
import { detectValidationCommand } from "../domains/safety/protected-artifacts.js";
|
|
27
28
|
import { askUserExposure } from "./ask-user.js";
|
|
28
29
|
import { type DispatchPlanView, describeDispatchPlan } from "./dispatch-plan.js";
|
|
29
30
|
import type { ToolPresentationPolicy } from "./presentation.js";
|
|
30
|
-
import type { ToolResultDisposition } from "./result-disposition.js";
|
|
31
|
-
import { DEFAULT_TOOL_RESULT_MAX_BYTES, shapeToolResult } from "./result-shaping.js";
|
|
31
|
+
import type { ToolResultDigest, ToolResultDisposition } from "./result-disposition.js";
|
|
32
|
+
import { DEFAULT_TOOL_RESULT_MAX_BYTES, shapeToolResult, toolResultDigestFor } from "./result-shaping.js";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Tool registry. Admission point for every tool call. Delegates classification
|
|
@@ -200,6 +201,8 @@ export interface ToolInvokeOptions {
|
|
|
200
201
|
correlationId?: string;
|
|
201
202
|
pendingSkillPolicy?: PendingSkillToolPolicy;
|
|
202
203
|
askUserPolicy?: AskUserToolPolicy;
|
|
204
|
+
/** Host-derived display copy for an ask_user round. It has no admission authority. */
|
|
205
|
+
decisionPresentation?: DecisionPresentation;
|
|
203
206
|
/** Registry-authenticated one-shot operator approval for this execution. */
|
|
204
207
|
approval?: { requestId: string; requestedBy: string; actionClass: ActionClass };
|
|
205
208
|
/**
|
|
@@ -259,6 +262,8 @@ export interface AskUserToolPolicy {
|
|
|
259
262
|
sessionId?: string;
|
|
260
263
|
turnId?: string;
|
|
261
264
|
transcriptPath?: string;
|
|
265
|
+
/** Monotonic exposure fact for live presentation and durable replay. */
|
|
266
|
+
exposure?: AutonomyExposure;
|
|
262
267
|
summary?: string;
|
|
263
268
|
rounds: AskUserTranscriptRound[];
|
|
264
269
|
decisions: AskUserTranscriptDecision[];
|
|
@@ -421,13 +426,15 @@ export function createRegistry(deps: RegistryDeps): ToolRegistry {
|
|
|
421
426
|
const preparedArgs = prepareToolArgs(spec, call.args ?? {});
|
|
422
427
|
resultDisposition = resolveToolResultDisposition(spec, preparedArgs);
|
|
423
428
|
const result = await spec.run(preparedArgs, options);
|
|
424
|
-
const
|
|
429
|
+
const digest = toolResultDigestFor(spec, result, resultDisposition);
|
|
430
|
+
const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
|
|
425
431
|
const finalResult = shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition);
|
|
426
432
|
return { kind: "ok", result: finalResult, decision };
|
|
427
433
|
} catch (err) {
|
|
428
434
|
const message = err instanceof Error ? err.message : String(err);
|
|
429
435
|
const result: ToolResult = { kind: "error", message };
|
|
430
|
-
const
|
|
436
|
+
const digest = toolResultDigestFor(spec, result, resultDisposition);
|
|
437
|
+
const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
|
|
431
438
|
return {
|
|
432
439
|
kind: "ok",
|
|
433
440
|
result: shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition),
|
|
@@ -446,9 +453,10 @@ export function createRegistry(deps: RegistryDeps): ToolRegistry {
|
|
|
446
453
|
decision: SafetyDecision,
|
|
447
454
|
options: ToolInvokeOptions | undefined,
|
|
448
455
|
result?: ToolResult,
|
|
456
|
+
resultDigest?: ToolResultDigest,
|
|
449
457
|
): ReadonlyArray<MiddlewareEffect> => {
|
|
450
458
|
if (!deps.middleware) return [];
|
|
451
|
-
const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result);
|
|
459
|
+
const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result, resultDigest);
|
|
452
460
|
const effects = deps.middleware.runHook(input).effects;
|
|
453
461
|
try {
|
|
454
462
|
deps.onMiddlewareEffects?.(effects, input);
|
|
@@ -1135,6 +1143,7 @@ function buildToolHookInput(
|
|
|
1135
1143
|
posture: string,
|
|
1136
1144
|
options: ToolInvokeOptions | undefined,
|
|
1137
1145
|
result: ToolResult | undefined,
|
|
1146
|
+
resultDigest: ToolResultDigest | undefined,
|
|
1138
1147
|
): MiddlewareHookInput {
|
|
1139
1148
|
const metadata: Record<string, MiddlewareMetadataValue> = {
|
|
1140
1149
|
posture,
|
|
@@ -1171,6 +1180,7 @@ function buildToolHookInput(
|
|
|
1171
1180
|
};
|
|
1172
1181
|
if (call.args !== undefined) input.toolArgs = call.args;
|
|
1173
1182
|
if (result?.details !== undefined) input.toolResultDetails = result.details;
|
|
1183
|
+
if (resultDigest !== undefined) input.toolResultDigest = resultDigest;
|
|
1174
1184
|
if (options?.runId !== undefined) input.runId = options.runId;
|
|
1175
1185
|
if (options?.sessionId !== undefined) input.sessionId = options.sessionId;
|
|
1176
1186
|
if (options?.turnId !== undefined) input.turnId = options.turnId;
|
|
@@ -52,6 +52,23 @@ export interface ToolResultSummaryProvenance {
|
|
|
52
52
|
redactions?: number;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export const TOOL_RESULT_DIGEST_MAX_BYTES = 240;
|
|
56
|
+
|
|
57
|
+
export interface ToolResultDigestProvenance {
|
|
58
|
+
producer: "code";
|
|
59
|
+
source: "canonical-result-disposition" | "legacy-fallback";
|
|
60
|
+
algorithm: "redacted-context-digest-v1" | "redacted-legacy-digest-v1";
|
|
61
|
+
contextMode?: AppliedToolResultContextMode;
|
|
62
|
+
summaryAlgorithm?: ToolResultSummaryProvenance["algorithm"];
|
|
63
|
+
redactions?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A bounded diagnostic that is safe to pass to task memory. */
|
|
67
|
+
export interface ToolResultDigest {
|
|
68
|
+
text: string;
|
|
69
|
+
provenance: ToolResultDigestProvenance;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
export interface ToolResultDispositionMetadata {
|
|
56
73
|
version: 1;
|
|
57
74
|
applications: 1;
|
|
@@ -223,6 +240,14 @@ function capUtf8(text: string, maxBytes: number): string {
|
|
|
223
240
|
return byteLength(text) <= maxBytes ? text : utf8Prefix(text, maxBytes);
|
|
224
241
|
}
|
|
225
242
|
|
|
243
|
+
function capDigestUtf8(text: string, maxBytes: number): string {
|
|
244
|
+
if (byteLength(text) <= maxBytes) return text;
|
|
245
|
+
const marker = "…";
|
|
246
|
+
const markerBytes = byteLength(marker);
|
|
247
|
+
if (markerBytes >= maxBytes) return utf8Prefix(text, maxBytes);
|
|
248
|
+
return `${utf8Prefix(text, maxBytes - markerBytes).trimEnd()}${marker}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
226
251
|
/** A deterministic, UTF-8-safe excerpt whose total bytes never exceed maxBytes. */
|
|
227
252
|
function boundedToolResultExcerpt(text: string, maxBytes: number, bias: ToolResultExcerptBias = "head-tail"): string {
|
|
228
253
|
if (maxBytes <= 0) return "";
|
|
@@ -504,6 +529,137 @@ export function projectToolResultContext(input: ProjectToolResultContextInput):
|
|
|
504
529
|
};
|
|
505
530
|
}
|
|
506
531
|
|
|
532
|
+
const DIGEST_FACT_KEYS = [
|
|
533
|
+
"outcome",
|
|
534
|
+
"exitCode",
|
|
535
|
+
"timedOut",
|
|
536
|
+
"aborted",
|
|
537
|
+
"outputCapped",
|
|
538
|
+
"signal",
|
|
539
|
+
"status",
|
|
540
|
+
"error",
|
|
541
|
+
"decision",
|
|
542
|
+
"next",
|
|
543
|
+
] as const;
|
|
544
|
+
|
|
545
|
+
function digestFactText(details: ToolResultDetails | undefined): string {
|
|
546
|
+
if (details === undefined) return "";
|
|
547
|
+
const facts: string[] = [];
|
|
548
|
+
for (const key of DIGEST_FACT_KEYS) {
|
|
549
|
+
if (details[key] !== undefined) facts.push(`${key}=${stableJson(details[key])}`);
|
|
550
|
+
}
|
|
551
|
+
return facts.join(" ");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function projectionBody(input: ProjectToolResultContextInput, projection: ToolResultContextProjection): string {
|
|
555
|
+
if (projection.appliedMode === "metadata-only") return "";
|
|
556
|
+
if (projection.appliedMode === "full") {
|
|
557
|
+
const trailer = fullTrailer(input);
|
|
558
|
+
return trailer.length > 0 && projection.text.endsWith(`\n\n${trailer}`)
|
|
559
|
+
? projection.text.slice(0, -(trailer.length + 2))
|
|
560
|
+
: projection.text;
|
|
561
|
+
}
|
|
562
|
+
const header = contextHeader(input, projection.appliedMode, projection.truncated, projection.summaryProvenance);
|
|
563
|
+
const prefix = `${header}\n`;
|
|
564
|
+
return projection.text.startsWith(prefix) ? projection.text.slice(prefix.length) : "";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function digestText(
|
|
568
|
+
status: string,
|
|
569
|
+
facts: string,
|
|
570
|
+
body: string,
|
|
571
|
+
redactions: number,
|
|
572
|
+
maxBytes: number,
|
|
573
|
+
): { text: string; redactions: number } {
|
|
574
|
+
const tally = createRedactionTally();
|
|
575
|
+
const safeStatus = redactSecretsText(status, tally);
|
|
576
|
+
const safeFacts = redactSecretsText(facts, tally);
|
|
577
|
+
const statusAndFacts = [safeStatus, safeFacts].filter((value) => value.length > 0).join(" ");
|
|
578
|
+
const bodyBudget = Math.max(0, maxBytes - byteLength(statusAndFacts) - 2);
|
|
579
|
+
const selected = deterministicDiagnosticSummary(body, bodyBudget, true);
|
|
580
|
+
const candidate = [statusAndFacts, selected.text.replace(/\s+/gu, " ").trim()]
|
|
581
|
+
.filter((value) => value.length > 0)
|
|
582
|
+
.join("; ");
|
|
583
|
+
return {
|
|
584
|
+
text: capDigestUtf8(candidate, maxBytes),
|
|
585
|
+
redactions: redactions + tally.count + selected.redactions,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Reuse the applied model-context projection as task memory's diagnostic
|
|
591
|
+
* source. Metadata-only never contributes captured content, and bounded or
|
|
592
|
+
* summarized modes contribute only the body admitted by that projection.
|
|
593
|
+
*/
|
|
594
|
+
export function deterministicToolResultDigest(
|
|
595
|
+
input: ProjectToolResultContextInput,
|
|
596
|
+
maxBytes = TOOL_RESULT_DIGEST_MAX_BYTES,
|
|
597
|
+
): ToolResultDigest {
|
|
598
|
+
const projection = projectToolResultContext(input);
|
|
599
|
+
const status = [
|
|
600
|
+
`kind=${input.kind}`,
|
|
601
|
+
`mode=${projection.appliedMode}`,
|
|
602
|
+
`truncated=${String(projection.truncated)}`,
|
|
603
|
+
...(projection.appliedMode === "metadata-only" || projection.truncated
|
|
604
|
+
? [`capturedBytes=${input.capturedBytes}`]
|
|
605
|
+
: []),
|
|
606
|
+
].join(" ");
|
|
607
|
+
const digest = digestText(
|
|
608
|
+
status,
|
|
609
|
+
digestFactText(input.details),
|
|
610
|
+
projectionBody(input, projection),
|
|
611
|
+
projection.summaryProvenance?.redactions ?? 0,
|
|
612
|
+
Math.max(1, Math.floor(maxBytes)),
|
|
613
|
+
);
|
|
614
|
+
return {
|
|
615
|
+
text: digest.text,
|
|
616
|
+
provenance: {
|
|
617
|
+
producer: "code",
|
|
618
|
+
source: "canonical-result-disposition",
|
|
619
|
+
algorithm: "redacted-context-digest-v1",
|
|
620
|
+
contextMode: projection.appliedMode,
|
|
621
|
+
...(projection.summaryProvenance === undefined ? {} : { summaryAlgorithm: projection.summaryProvenance.algorithm }),
|
|
622
|
+
...(digest.redactions > 0 ? { redactions: digest.redactions } : {}),
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Deterministic compatibility path for results without a canonical disposition. */
|
|
628
|
+
export function legacyToolResultDigest(
|
|
629
|
+
text: string,
|
|
630
|
+
options: { outcome: "ok" | "error"; maxBytes?: number },
|
|
631
|
+
): ToolResultDigest {
|
|
632
|
+
const maxBytes = Math.max(1, Math.floor(options.maxBytes ?? TOOL_RESULT_DIGEST_MAX_BYTES));
|
|
633
|
+
const fallback = options.outcome === "error" && text.trim().length === 0 ? "an unknown tool error" : text;
|
|
634
|
+
const digest = digestText("", "", fallback, 0, maxBytes);
|
|
635
|
+
return {
|
|
636
|
+
text: digest.text,
|
|
637
|
+
provenance: {
|
|
638
|
+
producer: "code",
|
|
639
|
+
source: "legacy-fallback",
|
|
640
|
+
algorithm: "redacted-legacy-digest-v1",
|
|
641
|
+
...(digest.redactions > 0 ? { redactions: digest.redactions } : {}),
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Reapply the memory boundary's redaction and byte cap to a supplied digest. */
|
|
647
|
+
export function sanitizeToolResultDigest(
|
|
648
|
+
digest: ToolResultDigest,
|
|
649
|
+
maxBytes = TOOL_RESULT_DIGEST_MAX_BYTES,
|
|
650
|
+
): ToolResultDigest {
|
|
651
|
+
const tally = createRedactionTally();
|
|
652
|
+
const safe = redactSecretsText(digest.text, tally).replace(/\s+/gu, " ").trim();
|
|
653
|
+
const redactions = (digest.provenance.redactions ?? 0) + tally.count;
|
|
654
|
+
return {
|
|
655
|
+
text: capDigestUtf8(safe, Math.max(1, Math.floor(maxBytes))),
|
|
656
|
+
provenance: {
|
|
657
|
+
...digest.provenance,
|
|
658
|
+
...(redactions > 0 ? { redactions } : {}),
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
507
663
|
/** The text inserted into the provider conversation for a registry result. */
|
|
508
664
|
export function toolResultContextText(result: ToolResult): string {
|
|
509
665
|
if (result.modelContext !== undefined) return result.modelContext;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readdirSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { clioStateDir } from "../core/xdg.js";
|
|
5
5
|
import type { ToolInvokeOptions, ToolResult, ToolResultDetails, ToolSpec } from "./registry.js";
|
|
6
6
|
import {
|
|
7
|
+
deterministicToolResultDigest,
|
|
8
|
+
legacyToolResultDigest,
|
|
7
9
|
normalizeToolResultDisposition,
|
|
8
10
|
projectToolResultContext,
|
|
11
|
+
type ToolResultDigest,
|
|
9
12
|
type ToolResultDisposition,
|
|
10
13
|
type ToolResultDispositionFallback,
|
|
11
14
|
type ToolResultDispositionMetadata,
|
|
@@ -21,6 +24,7 @@ export const DEFAULT_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES + 2 * 1024;
|
|
|
21
24
|
const RESULT_TRUNCATION_MARKER = "\n[tool result truncated]";
|
|
22
25
|
const RESULT_OFFLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
|
23
26
|
const TAIL_NOTICE_RESERVE_BYTES = 512;
|
|
27
|
+
export const TOOL_OFFLOAD_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
24
28
|
|
|
25
29
|
type ToolResultShapeContext = Pick<ToolInvokeOptions, "sessionId" | "toolCallId">;
|
|
26
30
|
|
|
@@ -49,6 +53,34 @@ function offloadMaxBytesFor(spec: ToolSpec): number {
|
|
|
49
53
|
: RESULT_OFFLOAD_MAX_BYTES;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Build the task-memory diagnostic from the same normalized disposition that
|
|
58
|
+
* will shape model context. Legacy tools use the explicit compatibility path.
|
|
59
|
+
*/
|
|
60
|
+
export function toolResultDigestFor(
|
|
61
|
+
spec: ToolSpec,
|
|
62
|
+
result: ToolResult,
|
|
63
|
+
requestedDisposition?: ToolResultDisposition,
|
|
64
|
+
): ToolResultDigest {
|
|
65
|
+
const text = resultText(result);
|
|
66
|
+
const disposition = normalizeToolResultDisposition(spec, maxBytesFor(spec), requestedDisposition);
|
|
67
|
+
if (disposition === null) {
|
|
68
|
+
const projected = shapeLegacyToolResult(spec, result);
|
|
69
|
+
return legacyToolResultDigest(resultText(projected), { outcome: result.kind });
|
|
70
|
+
}
|
|
71
|
+
const capturedBytes = capturedBytesFor(result, text);
|
|
72
|
+
return deterministicToolResultDigest({
|
|
73
|
+
text,
|
|
74
|
+
kind: result.kind,
|
|
75
|
+
details: result.details,
|
|
76
|
+
disposition,
|
|
77
|
+
capturedBytes,
|
|
78
|
+
displayedBytes: Math.min(capturedBytes, disposition.presentation.maxBytes),
|
|
79
|
+
offloadPath: existingOffloadPath(result.details),
|
|
80
|
+
followUpHint: followUpHint(spec),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
52
84
|
function detailsRecord(details: ToolResultDetails | undefined, key: string): Record<string, unknown> | null {
|
|
53
85
|
const candidate = details?.[key];
|
|
54
86
|
return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate)
|
|
@@ -91,6 +123,32 @@ function offloadBody(text: string, bytes: number, maxBytes: number): string {
|
|
|
91
123
|
return `${truncateUtf8(text, Math.max(0, prefixBudget), "")}${notice}`;
|
|
92
124
|
}
|
|
93
125
|
|
|
126
|
+
/** Remove regular offload files older than the boot retention cap. */
|
|
127
|
+
export function sweepExpiredToolOffloads(now = Date.now()): number {
|
|
128
|
+
const root = join(clioStateDir(), "scratch");
|
|
129
|
+
const cutoff = now - TOOL_OFFLOAD_MAX_AGE_MS;
|
|
130
|
+
let removed = 0;
|
|
131
|
+
try {
|
|
132
|
+
for (const session of readdirSync(root, { withFileTypes: true })) {
|
|
133
|
+
if (!session.isDirectory()) continue;
|
|
134
|
+
const sessionDir = join(root, session.name);
|
|
135
|
+
for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
|
|
136
|
+
if (!entry.isFile()) continue;
|
|
137
|
+
const path = join(sessionDir, entry.name);
|
|
138
|
+
try {
|
|
139
|
+
if (statSync(path).mtimeMs >= cutoff) continue;
|
|
140
|
+
rmSync(path, { force: true });
|
|
141
|
+
removed += 1;
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
if (readdirSync(sessionDir).length === 0) rmdirSync(sessionDir);
|
|
146
|
+
} catch {}
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
return removed;
|
|
150
|
+
}
|
|
151
|
+
|
|
94
152
|
/**
|
|
95
153
|
* Persist the full text of a tool result to a per-session scratch file and
|
|
96
154
|
* return its path (or null if the write fails). Self-shaping callers use this
|