@inhuman.tools/turn 0.0.0-stage → 0.1.0
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/LICENSE.md +7 -0
- package/dist/agent-calls/acceptance.d.ts +12 -0
- package/dist/agent-calls/acceptance.d.ts.map +1 -0
- package/dist/agent-calls/acceptance.js +64 -0
- package/dist/agent-calls/acceptance.js.map +1 -0
- package/dist/agent-calls/callbacks.d.ts +14 -0
- package/dist/agent-calls/callbacks.d.ts.map +1 -0
- package/dist/agent-calls/callbacks.js +59 -0
- package/dist/agent-calls/callbacks.js.map +1 -0
- package/dist/agent-calls/delivery.d.ts +7 -0
- package/dist/agent-calls/delivery.d.ts.map +1 -0
- package/dist/agent-calls/delivery.js +16 -0
- package/dist/agent-calls/delivery.js.map +1 -0
- package/dist/agent-calls/index.d.ts +7 -0
- package/dist/agent-calls/index.d.ts.map +1 -0
- package/dist/agent-calls/index.js +6 -0
- package/dist/agent-calls/index.js.map +1 -0
- package/dist/agent-calls/operator.d.ts +21 -0
- package/dist/agent-calls/operator.d.ts.map +1 -0
- package/dist/agent-calls/operator.js +78 -0
- package/dist/agent-calls/operator.js.map +1 -0
- package/dist/agent-calls/runtime.d.ts +99 -0
- package/dist/agent-calls/runtime.d.ts.map +1 -0
- package/dist/agent-calls/runtime.js +245 -0
- package/dist/agent-calls/runtime.js.map +1 -0
- package/dist/agent-calls/types.d.ts +31 -0
- package/dist/agent-calls/types.d.ts.map +1 -0
- package/dist/agent-calls/types.js +2 -0
- package/dist/agent-calls/types.js.map +1 -0
- package/dist/context/compact-floors.d.ts +43 -0
- package/dist/context/compact-floors.d.ts.map +1 -0
- package/dist/context/compact-floors.js +52 -0
- package/dist/context/compact-floors.js.map +1 -0
- package/dist/context/compact.d.ts +42 -0
- package/dist/context/compact.d.ts.map +1 -0
- package/dist/context/compact.js +155 -0
- package/dist/context/compact.js.map +1 -0
- package/dist/context/delivery.d.ts +13 -0
- package/dist/context/delivery.d.ts.map +1 -0
- package/dist/context/delivery.js +6 -0
- package/dist/context/delivery.js.map +1 -0
- package/dist/context/history.d.ts +66 -0
- package/dist/context/history.d.ts.map +1 -0
- package/dist/context/history.js +573 -0
- package/dist/context/history.js.map +1 -0
- package/dist/context/index.d.ts +10 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +10 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/observations.d.ts +7 -0
- package/dist/context/observations.d.ts.map +1 -0
- package/dist/context/observations.js +11 -0
- package/dist/context/observations.js.map +1 -0
- package/dist/context/retention/defaults.d.ts +10 -0
- package/dist/context/retention/defaults.d.ts.map +1 -0
- package/dist/context/retention/defaults.js +47 -0
- package/dist/context/retention/defaults.js.map +1 -0
- package/dist/context/retention/index.d.ts +14 -0
- package/dist/context/retention/index.d.ts.map +1 -0
- package/dist/context/retention/index.js +21 -0
- package/dist/context/retention/index.js.map +1 -0
- package/dist/context/retention/narrow.d.ts +22 -0
- package/dist/context/retention/narrow.d.ts.map +1 -0
- package/dist/context/retention/narrow.js +140 -0
- package/dist/context/retention/narrow.js.map +1 -0
- package/dist/context/retention/render.d.ts +37 -0
- package/dist/context/retention/render.d.ts.map +1 -0
- package/dist/context/retention/render.js +149 -0
- package/dist/context/retention/render.js.map +1 -0
- package/dist/context/retention/resolve.d.ts +51 -0
- package/dist/context/retention/resolve.d.ts.map +1 -0
- package/dist/context/retention/resolve.js +254 -0
- package/dist/context/retention/resolve.js.map +1 -0
- package/dist/context/retention/schema.d.ts +345 -0
- package/dist/context/retention/schema.d.ts.map +1 -0
- package/dist/context/retention/schema.js +149 -0
- package/dist/context/retention/schema.js.map +1 -0
- package/dist/context/retention/stamp.d.ts +25 -0
- package/dist/context/retention/stamp.d.ts.map +1 -0
- package/dist/context/retention/stamp.js +103 -0
- package/dist/context/retention/stamp.js.map +1 -0
- package/dist/context/retention/step.d.ts +33 -0
- package/dist/context/retention/step.d.ts.map +1 -0
- package/dist/context/retention/step.js +74 -0
- package/dist/context/retention/step.js.map +1 -0
- package/dist/context/retention/view.d.ts +57 -0
- package/dist/context/retention/view.d.ts.map +1 -0
- package/dist/context/retention/view.js +121 -0
- package/dist/context/retention/view.js.map +1 -0
- package/dist/context/squeeze.d.ts +45 -0
- package/dist/context/squeeze.d.ts.map +1 -0
- package/dist/context/squeeze.js +182 -0
- package/dist/context/squeeze.js.map +1 -0
- package/dist/context/tools.d.ts +16 -0
- package/dist/context/tools.d.ts.map +1 -0
- package/dist/context/tools.js +73 -0
- package/dist/context/tools.js.map +1 -0
- package/dist/context/transcript.d.ts +151 -0
- package/dist/context/transcript.d.ts.map +1 -0
- package/dist/context/transcript.js +659 -0
- package/dist/context/transcript.js.map +1 -0
- package/dist/policy-processor/bypass.d.ts +95 -0
- package/dist/policy-processor/bypass.d.ts.map +1 -0
- package/dist/policy-processor/bypass.js +188 -0
- package/dist/policy-processor/bypass.js.map +1 -0
- package/dist/policy-processor/classifier.d.ts +47 -0
- package/dist/policy-processor/classifier.d.ts.map +1 -0
- package/dist/policy-processor/classifier.js +671 -0
- package/dist/policy-processor/classifier.js.map +1 -0
- package/dist/policy-processor/cluster.d.ts +7 -0
- package/dist/policy-processor/cluster.d.ts.map +1 -0
- package/dist/policy-processor/cluster.js +32 -0
- package/dist/policy-processor/cluster.js.map +1 -0
- package/dist/policy-processor/evidence.d.ts +26 -0
- package/dist/policy-processor/evidence.d.ts.map +1 -0
- package/dist/policy-processor/evidence.js +204 -0
- package/dist/policy-processor/evidence.js.map +1 -0
- package/dist/policy-processor/explain.d.ts +59 -0
- package/dist/policy-processor/explain.d.ts.map +1 -0
- package/dist/policy-processor/explain.js +224 -0
- package/dist/policy-processor/explain.js.map +1 -0
- package/dist/policy-processor/index.d.ts +6 -0
- package/dist/policy-processor/index.d.ts.map +1 -0
- package/dist/policy-processor/index.js +6 -0
- package/dist/policy-processor/index.js.map +1 -0
- package/dist/policy-processor/license.d.ts +23 -0
- package/dist/policy-processor/license.d.ts.map +1 -0
- package/dist/policy-processor/license.js +47 -0
- package/dist/policy-processor/license.js.map +1 -0
- package/dist/policy-processor/peel-cwd.d.ts +2 -0
- package/dist/policy-processor/peel-cwd.d.ts.map +1 -0
- package/dist/policy-processor/peel-cwd.js +2 -0
- package/dist/policy-processor/peel-cwd.js.map +1 -0
- package/dist/policy-processor/real-path.d.ts +7 -0
- package/dist/policy-processor/real-path.d.ts.map +1 -0
- package/dist/policy-processor/real-path.js +42 -0
- package/dist/policy-processor/real-path.js.map +1 -0
- package/dist/policy-processor/scope.d.ts +111 -0
- package/dist/policy-processor/scope.d.ts.map +1 -0
- package/dist/policy-processor/scope.js +629 -0
- package/dist/policy-processor/scope.js.map +1 -0
- package/dist/policy-processor/self-protect.d.ts +10 -0
- package/dist/policy-processor/self-protect.d.ts.map +1 -0
- package/dist/policy-processor/self-protect.js +48 -0
- package/dist/policy-processor/self-protect.js.map +1 -0
- package/dist/policy-processor/soft-deny.d.ts +75 -0
- package/dist/policy-processor/soft-deny.d.ts.map +1 -0
- package/dist/policy-processor/soft-deny.js +185 -0
- package/dist/policy-processor/soft-deny.js.map +1 -0
- package/dist/policy-processor/unattended-guard.d.ts +28 -0
- package/dist/policy-processor/unattended-guard.d.ts.map +1 -0
- package/dist/policy-processor/unattended-guard.js +145 -0
- package/dist/policy-processor/unattended-guard.js.map +1 -0
- package/dist/session/cancel.d.ts +14 -0
- package/dist/session/cancel.d.ts.map +1 -0
- package/dist/session/cancel.js +32 -0
- package/dist/session/cancel.js.map +1 -0
- package/dist/session/commands.d.ts +41 -0
- package/dist/session/commands.d.ts.map +1 -0
- package/dist/session/commands.js +241 -0
- package/dist/session/commands.js.map +1 -0
- package/dist/session/compact-actor.d.ts +23 -0
- package/dist/session/compact-actor.d.ts.map +1 -0
- package/dist/session/compact-actor.js +91 -0
- package/dist/session/compact-actor.js.map +1 -0
- package/dist/session/compact-env.d.ts +54 -0
- package/dist/session/compact-env.d.ts.map +1 -0
- package/dist/session/compact-env.js +345 -0
- package/dist/session/compact-env.js.map +1 -0
- package/dist/session/compact.d.ts +42 -0
- package/dist/session/compact.d.ts.map +1 -0
- package/dist/session/compact.js +68 -0
- package/dist/session/compact.js.map +1 -0
- package/dist/session/index.d.ts +9 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/park.d.ts +16 -0
- package/dist/session/park.d.ts.map +1 -0
- package/dist/session/park.js +73 -0
- package/dist/session/park.js.map +1 -0
- package/dist/session/side-channel-reminder.d.ts +41 -0
- package/dist/session/side-channel-reminder.d.ts.map +1 -0
- package/dist/session/side-channel-reminder.js +132 -0
- package/dist/session/side-channel-reminder.js.map +1 -0
- package/dist/session/types.d.ts +44 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/session/types.js +2 -0
- package/dist/session/types.js.map +1 -0
- package/dist/tool-catalog/catalog.d.ts +77 -0
- package/dist/tool-catalog/catalog.d.ts.map +1 -0
- package/dist/tool-catalog/catalog.js +380 -0
- package/dist/tool-catalog/catalog.js.map +1 -0
- package/dist/tool-catalog/index.d.ts +4 -0
- package/dist/tool-catalog/index.d.ts.map +1 -0
- package/dist/tool-catalog/index.js +3 -0
- package/dist/tool-catalog/index.js.map +1 -0
- package/dist/tool-catalog/recipes.d.ts +36 -0
- package/dist/tool-catalog/recipes.d.ts.map +1 -0
- package/dist/tool-catalog/recipes.js +121 -0
- package/dist/tool-catalog/recipes.js.map +1 -0
- package/dist/tool-catalog/types.d.ts +103 -0
- package/dist/tool-catalog/types.d.ts.map +1 -0
- package/dist/tool-catalog/types.js +5 -0
- package/dist/tool-catalog/types.js.map +1 -0
- package/dist/tool-runtime/approval/approved.d.ts +13 -0
- package/dist/tool-runtime/approval/approved.d.ts.map +1 -0
- package/dist/tool-runtime/approval/approved.js +20 -0
- package/dist/tool-runtime/approval/approved.js.map +1 -0
- package/dist/tool-runtime/approval/dist.d.ts +6 -0
- package/dist/tool-runtime/approval/dist.d.ts.map +1 -0
- package/dist/tool-runtime/approval/dist.js +28 -0
- package/dist/tool-runtime/approval/dist.js.map +1 -0
- package/dist/tool-runtime/approval/inbound.d.ts +7 -0
- package/dist/tool-runtime/approval/inbound.d.ts.map +1 -0
- package/dist/tool-runtime/approval/inbound.js +48 -0
- package/dist/tool-runtime/approval/inbound.js.map +1 -0
- package/dist/tool-runtime/approval/unattended.d.ts +8 -0
- package/dist/tool-runtime/approval/unattended.d.ts.map +1 -0
- package/dist/tool-runtime/approval/unattended.js +23 -0
- package/dist/tool-runtime/approval/unattended.js.map +1 -0
- package/dist/tool-runtime/blob-port.d.ts +46 -0
- package/dist/tool-runtime/blob-port.d.ts.map +1 -0
- package/dist/tool-runtime/blob-port.js +47 -0
- package/dist/tool-runtime/blob-port.js.map +1 -0
- package/dist/tool-runtime/extra-root.d.ts +3 -0
- package/dist/tool-runtime/extra-root.d.ts.map +1 -0
- package/dist/tool-runtime/extra-root.js +14 -0
- package/dist/tool-runtime/extra-root.js.map +1 -0
- package/dist/tool-runtime/handler.d.ts +29 -0
- package/dist/tool-runtime/handler.d.ts.map +1 -0
- package/dist/tool-runtime/handler.js +460 -0
- package/dist/tool-runtime/handler.js.map +1 -0
- package/dist/tool-runtime/index.d.ts +14 -0
- package/dist/tool-runtime/index.d.ts.map +1 -0
- package/dist/tool-runtime/index.js +14 -0
- package/dist/tool-runtime/index.js.map +1 -0
- package/dist/tool-runtime/peel.d.ts +19 -0
- package/dist/tool-runtime/peel.d.ts.map +1 -0
- package/dist/tool-runtime/peel.js +74 -0
- package/dist/tool-runtime/peel.js.map +1 -0
- package/dist/tool-runtime/plugin-context.d.ts +13 -0
- package/dist/tool-runtime/plugin-context.d.ts.map +1 -0
- package/dist/tool-runtime/plugin-context.js +55 -0
- package/dist/tool-runtime/plugin-context.js.map +1 -0
- package/dist/tool-runtime/recall.d.ts +18 -0
- package/dist/tool-runtime/recall.d.ts.map +1 -0
- package/dist/tool-runtime/recall.js +100 -0
- package/dist/tool-runtime/recall.js.map +1 -0
- package/dist/tool-runtime/run-system-tool.d.ts +13 -0
- package/dist/tool-runtime/run-system-tool.d.ts.map +1 -0
- package/dist/tool-runtime/run-system-tool.js +230 -0
- package/dist/tool-runtime/run-system-tool.js.map +1 -0
- package/dist/tool-runtime/tool-error.d.ts +6 -0
- package/dist/tool-runtime/tool-error.d.ts.map +1 -0
- package/dist/tool-runtime/tool-error.js +6 -0
- package/dist/tool-runtime/tool-error.js.map +1 -0
- package/dist/tool-runtime/worker-tools.d.ts +8 -0
- package/dist/tool-runtime/worker-tools.d.ts.map +1 -0
- package/dist/tool-runtime/worker-tools.js +65 -0
- package/dist/tool-runtime/worker-tools.js.map +1 -0
- package/dist/tool-runtime/write-admit.d.ts +21 -0
- package/dist/tool-runtime/write-admit.d.ts.map +1 -0
- package/dist/tool-runtime/write-admit.js +89 -0
- package/dist/tool-runtime/write-admit.js.map +1 -0
- package/package.json +114 -3
- package/policy/auto-mode-ask.scm +201 -0
- package/policy/bypass-detect.prompt +55 -0
- package/policy/classifier-bash-incomplete.prompt +46 -0
- package/policy/classifier-bash.prompt +45 -0
- package/policy/classifier-mcp.prompt +45 -0
- package/policy/classifier-read.prompt +44 -0
- package/policy/classifier-write-outside.prompt +46 -0
- package/policy/classifier-write-risky.prompt +45 -0
- package/policy/classifier-write-workspace.prompt +46 -0
- package/policy/classifier-write.prompt +45 -0
- package/policy/soft-deny-round1.prompt +51 -0
- package/policy/soft-deny-round2.prompt +60 -0
- package/src/agent-calls/acceptance.ts +71 -0
- package/src/agent-calls/callbacks.ts +75 -0
- package/src/agent-calls/delivery.ts +15 -0
- package/src/agent-calls/index.ts +16 -0
- package/src/agent-calls/operator.ts +99 -0
- package/src/agent-calls/runtime.ts +310 -0
- package/src/agent-calls/types.ts +33 -0
- package/src/context/compact-floors.ts +76 -0
- package/src/context/compact.ts +176 -0
- package/src/context/delivery.ts +15 -0
- package/src/context/history.ts +725 -0
- package/src/context/index.ts +35 -0
- package/src/context/observations.ts +13 -0
- package/src/context/retention/defaults.ts +58 -0
- package/src/context/retention/index.ts +45 -0
- package/src/context/retention/narrow.ts +123 -0
- package/src/context/retention/render.ts +177 -0
- package/src/context/retention/resolve.ts +306 -0
- package/src/context/retention/schema.ts +173 -0
- package/src/context/retention/stamp.ts +121 -0
- package/src/context/retention/step.ts +85 -0
- package/src/context/retention/view.ts +161 -0
- package/src/context/squeeze.ts +218 -0
- package/src/context/tools.ts +92 -0
- package/src/context/transcript.ts +705 -0
- package/src/policy-processor/README.md +43 -0
- package/src/policy-processor/bypass.ts +246 -0
- package/src/policy-processor/classifier.ts +711 -0
- package/src/policy-processor/cluster.ts +31 -0
- package/src/policy-processor/evidence.ts +214 -0
- package/src/policy-processor/explain.ts +310 -0
- package/src/policy-processor/index.ts +6 -0
- package/src/policy-processor/license.ts +45 -0
- package/src/policy-processor/peel-cwd.ts +1 -0
- package/src/policy-processor/real-path.ts +41 -0
- package/src/policy-processor/scope.ts +655 -0
- package/src/policy-processor/self-protect.ts +46 -0
- package/src/policy-processor/soft-deny.ts +225 -0
- package/src/policy-processor/unattended-guard.ts +158 -0
- package/src/session/cancel.ts +34 -0
- package/src/session/commands.ts +292 -0
- package/src/session/compact-actor.ts +132 -0
- package/src/session/compact-env.ts +539 -0
- package/src/session/compact.ts +95 -0
- package/src/session/index.ts +18 -0
- package/src/session/park.ts +73 -0
- package/src/session/side-channel-reminder.ts +148 -0
- package/src/session/types.ts +50 -0
- package/src/tool-catalog/catalog.ts +282 -0
- package/src/tool-catalog/index.ts +26 -0
- package/src/tool-catalog/recipes.ts +154 -0
- package/src/tool-catalog/types.ts +110 -0
- package/src/tool-runtime/approval/approved.ts +20 -0
- package/src/tool-runtime/approval/dist.ts +29 -0
- package/src/tool-runtime/approval/inbound.ts +50 -0
- package/src/tool-runtime/approval/unattended.ts +24 -0
- package/src/tool-runtime/blob-port.ts +64 -0
- package/src/tool-runtime/extra-root.ts +16 -0
- package/src/tool-runtime/handler.ts +576 -0
- package/src/tool-runtime/index.ts +21 -0
- package/src/tool-runtime/peel.ts +91 -0
- package/src/tool-runtime/plugin-context.ts +68 -0
- package/src/tool-runtime/recall.ts +104 -0
- package/src/tool-runtime/run-system-tool.ts +273 -0
- package/src/tool-runtime/tool-error.ts +6 -0
- package/src/tool-runtime/worker-tools.ts +76 -0
- package/src/tool-runtime/write-admit.ts +90 -0
- package/README.md +0 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layering (design §4.4) and per-item rule resolution.
|
|
3
|
+
*
|
|
4
|
+
* dist → managed → user trusted: may loosen, replace per key
|
|
5
|
+
* project, session, agent narrowing only, and project/agent never touch
|
|
6
|
+
* user-authored kinds
|
|
7
|
+
* tool self-declaration narrowing only, its own tool key only (at admit)
|
|
8
|
+
*
|
|
9
|
+
* Session kv is narrowing-only by owner decision: the interpreter can reach
|
|
10
|
+
* `kv/set`, so a model must not be able to lengthen what it later reads.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
import { DEFAULT_CONTEXT_RETENTION, LEGACY_CONTEXT_RETENTION } from "./defaults.js";
|
|
15
|
+
import { canonicalKeep, keepRank, narrowRule, narrows } from "./narrow.js";
|
|
16
|
+
import {
|
|
17
|
+
contextRetentionSchema,
|
|
18
|
+
horizonList,
|
|
19
|
+
horizonSchema,
|
|
20
|
+
NONE_FORBIDDEN_KINDS,
|
|
21
|
+
parseRetentionLayer,
|
|
22
|
+
RETENTION_KINDS,
|
|
23
|
+
USER_AUTHORED_KINDS,
|
|
24
|
+
type ConcreteRule,
|
|
25
|
+
type ContextRetention,
|
|
26
|
+
type Horizon,
|
|
27
|
+
type RetentionLayer,
|
|
28
|
+
type RuleInput,
|
|
29
|
+
} from "./schema.js";
|
|
30
|
+
|
|
31
|
+
export type NarrowSource = "project" | "session" | "agent" | "tool";
|
|
32
|
+
|
|
33
|
+
/** A resolved policy: the layered config plus the narrowing overlays that name tools. */
|
|
34
|
+
export type ResolvedPolicy = ContextRetention & {
|
|
35
|
+
/** Narrowing overlays keyed like `tools`; applied on top of the matched rule at admit. */
|
|
36
|
+
readonly narrowTools: Readonly<Record<string, RuleInput>>;
|
|
37
|
+
/** First 12 hex of the policy digest. Diagnostic only. */
|
|
38
|
+
readonly hash: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ResolveInput = {
|
|
42
|
+
/** Trusted layers: a partial layer or the raw `contextRetention` block (object or JSON text). */
|
|
43
|
+
readonly managed?: unknown;
|
|
44
|
+
readonly user?: unknown;
|
|
45
|
+
/** Narrowing layers. */
|
|
46
|
+
readonly project?: unknown;
|
|
47
|
+
readonly session?: RetentionLayer;
|
|
48
|
+
readonly agent?: unknown;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ResolveResult = { readonly policy: ResolvedPolicy; readonly ignored: readonly string[] };
|
|
52
|
+
|
|
53
|
+
function mergeRuleBag(
|
|
54
|
+
base: Readonly<Record<string, RuleInput>>,
|
|
55
|
+
layer: Readonly<Record<string, RuleInput>> | undefined,
|
|
56
|
+
): Record<string, RuleInput> {
|
|
57
|
+
const out: Record<string, RuleInput> = { ...base };
|
|
58
|
+
for (const [key, rule] of Object.entries(layer ?? {})) out[key] = { ...out[key], ...rule };
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyTrusted(base: ContextRetention, layer: RetentionLayer): ContextRetention {
|
|
63
|
+
return {
|
|
64
|
+
...base,
|
|
65
|
+
defaults: { ...base.defaults, ...layer.defaults },
|
|
66
|
+
step: { ...base.step, ...layer.step },
|
|
67
|
+
kinds: mergeRuleBag(base.kinds, layer.kinds),
|
|
68
|
+
tools: mergeRuleBag(base.tools, layer.tools),
|
|
69
|
+
dialects: mergeRuleBag(base.dialects, layer.dialects),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function digest(policy: Omit<ResolvedPolicy, "hash">): string {
|
|
74
|
+
return createHash("sha256").update(JSON.stringify(policy)).digest("hex").slice(0, 12);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function seal(policy: Omit<ResolvedPolicy, "hash">): ResolvedPolicy {
|
|
78
|
+
return { ...policy, hash: digest(policy) };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function applyNarrowing(
|
|
82
|
+
policy: Omit<ResolvedPolicy, "hash">,
|
|
83
|
+
layer: RetentionLayer,
|
|
84
|
+
source: NarrowSource,
|
|
85
|
+
ignored: string[],
|
|
86
|
+
): Omit<ResolvedPolicy, "hash"> {
|
|
87
|
+
let kinds: Record<string, RuleInput> = { ...policy.kinds };
|
|
88
|
+
const narrowTools: Record<string, RuleInput> = { ...policy.narrowTools };
|
|
89
|
+
let defaults = policy.defaults;
|
|
90
|
+
if (layer.step !== undefined) ignored.push(`${source}: step is not narrowable`);
|
|
91
|
+
if (layer.dialects !== undefined) ignored.push(`${source}: dialects are not narrowable`);
|
|
92
|
+
if (layer.defaults?.minChars !== undefined) {
|
|
93
|
+
defaults = { minChars: Math.min(defaults.minChars, layer.defaults.minChars) };
|
|
94
|
+
}
|
|
95
|
+
for (const [kind, rule] of Object.entries(layer.kinds ?? {})) {
|
|
96
|
+
if (source !== "session" && USER_AUTHORED_KINDS.has(kind)) {
|
|
97
|
+
ignored.push(`${source}: kind ${kind} is user-authored`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const base = kinds[kind] ?? {};
|
|
101
|
+
if (!narrows(base, rule, kind)) {
|
|
102
|
+
ignored.push(`${source}: kinds.${kind} does not shorten retention`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
kinds = { ...kinds, [kind]: narrowRule(base, rule, kind) };
|
|
106
|
+
}
|
|
107
|
+
for (const [key, rule] of Object.entries(layer.tools ?? {})) {
|
|
108
|
+
narrowTools[key] = narrowRule(narrowTools[key] ?? {}, rule, null);
|
|
109
|
+
}
|
|
110
|
+
return { ...policy, defaults, kinds, narrowTools };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveRetention(input: ResolveInput = {}): ResolveResult {
|
|
114
|
+
const ignored: string[] = [];
|
|
115
|
+
let base: ContextRetention = DEFAULT_CONTEXT_RETENTION;
|
|
116
|
+
if (input.managed !== undefined) base = applyTrusted(base, parseRetentionLayer(input.managed));
|
|
117
|
+
if (input.user !== undefined) base = applyTrusted(base, parseRetentionLayer(input.user));
|
|
118
|
+
let policy: Omit<ResolvedPolicy, "hash"> = { ...base, narrowTools: {} };
|
|
119
|
+
if (input.project !== undefined) {
|
|
120
|
+
policy = applyNarrowing(policy, parseRetentionLayer(input.project), "project", ignored);
|
|
121
|
+
}
|
|
122
|
+
if (input.session !== undefined) policy = applyNarrowing(policy, input.session, "session", ignored);
|
|
123
|
+
if (input.agent !== undefined) policy = applyNarrowing(policy, parseRetentionLayer(input.agent), "agent", ignored);
|
|
124
|
+
return { policy: seal(policy), ignored };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The pre-retention rule set: everything lives until compaction (plus the owner's picture and note rules). */
|
|
128
|
+
export const LEGACY_POLICY: ResolvedPolicy = seal({ ...LEGACY_CONTEXT_RETENTION, narrowTools: {} });
|
|
129
|
+
|
|
130
|
+
function sessionHorizon(raw: string): Horizon[] | null {
|
|
131
|
+
const text = raw.trim();
|
|
132
|
+
if (text === "") return null;
|
|
133
|
+
let value: unknown = text;
|
|
134
|
+
try {
|
|
135
|
+
value = JSON.parse(text);
|
|
136
|
+
} catch {
|
|
137
|
+
value = text;
|
|
138
|
+
}
|
|
139
|
+
const list = Array.isArray(value) ? value : [value];
|
|
140
|
+
const out: Horizon[] = [];
|
|
141
|
+
for (const entry of list) {
|
|
142
|
+
const parsed = horizonSchema.safeParse(entry);
|
|
143
|
+
if (!parsed.success) return null;
|
|
144
|
+
out.push(parsed.data);
|
|
145
|
+
}
|
|
146
|
+
return out.length === 0 ? null : out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Session kv → a narrowing layer. `retention.<kind>` and
|
|
151
|
+
* `retention.tool.<key>` hold a JSON horizon (`"round"`, `{"rounds":2}`, or an array).
|
|
152
|
+
*/
|
|
153
|
+
export function sessionRetentionLayer(kvs: Readonly<Record<string, string>>): RetentionLayer {
|
|
154
|
+
const kinds: Record<string, RuleInput> = {};
|
|
155
|
+
const tools: Record<string, RuleInput> = {};
|
|
156
|
+
for (const [key, raw] of Object.entries(kvs)) {
|
|
157
|
+
if (!key.startsWith("retention.")) continue;
|
|
158
|
+
const keep = sessionHorizon(raw);
|
|
159
|
+
if (keep === null) continue;
|
|
160
|
+
const rest = key.slice("retention.".length);
|
|
161
|
+
// A kind name wins (`retention.tool.read` is the kind); anything else under `tool.` is a tool key.
|
|
162
|
+
if ((RETENTION_KINDS as readonly string[]).includes(rest)) {
|
|
163
|
+
kinds[rest] = { keep };
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (rest.startsWith("tool.")) {
|
|
167
|
+
const toolKey = rest.slice("tool.".length);
|
|
168
|
+
if (toolKey !== "") tools[toolKey] = { keep };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...(Object.keys(kinds).length === 0 ? {} : { kinds }),
|
|
173
|
+
...(Object.keys(tools).length === 0 ? {} : { tools }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- stamp text -----------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
const policyCache = new Map<string, ResolvedPolicy | null>();
|
|
180
|
+
|
|
181
|
+
export function serializePolicy(policy: ResolvedPolicy): string {
|
|
182
|
+
return JSON.stringify(policy);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Parse an R1 stamp. Empty or malformed is `null` (the caller inherits or falls back to legacy). */
|
|
186
|
+
export function parsePolicy(json: string): ResolvedPolicy | null {
|
|
187
|
+
if (json === "") return null;
|
|
188
|
+
const hit = policyCache.get(json);
|
|
189
|
+
if (hit !== undefined) return hit;
|
|
190
|
+
let out: ResolvedPolicy | null = null;
|
|
191
|
+
try {
|
|
192
|
+
const raw = JSON.parse(json) as Record<string, unknown>;
|
|
193
|
+
const parsed = contextRetentionSchema.safeParse(raw);
|
|
194
|
+
if (parsed.success) {
|
|
195
|
+
const narrow = raw.narrowTools;
|
|
196
|
+
out = {
|
|
197
|
+
...parsed.data,
|
|
198
|
+
narrowTools:
|
|
199
|
+
narrow != null && typeof narrow === "object" ? (parseRetentionLayer({ tools: narrow }).tools ?? {}) : {},
|
|
200
|
+
hash: typeof raw.hash === "string" ? raw.hash : "",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
out = null;
|
|
205
|
+
}
|
|
206
|
+
if (policyCache.size > 64) policyCache.clear();
|
|
207
|
+
policyCache.set(json, out);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- per-item rule --------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function globToRegExp(glob: string): RegExp {
|
|
214
|
+
return new RegExp(
|
|
215
|
+
`^${glob
|
|
216
|
+
.split("*")
|
|
217
|
+
.map((part) => part.replaceAll(/[$()*+.?[\\\]^{|}]/g, String.raw`\$&`))
|
|
218
|
+
.join(".*")}$`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function lookup<T>(bag: Readonly<Record<string, T>>, names: readonly string[]): T | undefined {
|
|
223
|
+
for (const name of names) {
|
|
224
|
+
if (name !== "" && Object.hasOwn(bag, name)) return bag[name];
|
|
225
|
+
}
|
|
226
|
+
let best: { readonly glob: string; readonly value: T } | undefined;
|
|
227
|
+
for (const [glob, value] of Object.entries(bag)) {
|
|
228
|
+
if (!glob.includes("*")) continue;
|
|
229
|
+
const re = globToRegExp(glob);
|
|
230
|
+
if (names.some((name) => name !== "" && re.test(name)) && (best === undefined || glob.length > best.glob.length)) {
|
|
231
|
+
best = { glob, value };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return best?.value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ruleKeep(rule: RuleInput | undefined, fallback: readonly Horizon[]): readonly Horizon[] {
|
|
238
|
+
const keep = horizonList(rule?.keep);
|
|
239
|
+
return keep.length === 0 ? fallback : keep;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type RuleQuery = {
|
|
243
|
+
readonly kind: string;
|
|
244
|
+
/** Names to match `tools`, most specific first: `mcp:server/tool`, then the wire name. */
|
|
245
|
+
readonly names: readonly string[];
|
|
246
|
+
/** The tool's own narrowing hint (catalog `retention`). Applies to its own key only. */
|
|
247
|
+
readonly hint?: RuleInput;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/** The concrete rule for one item of `kind`. Pure over `policy`. */
|
|
251
|
+
export function ruleFor(policy: ResolvedPolicy, query: RuleQuery): ConcreteRule {
|
|
252
|
+
const baseRule = policy.kinds[query.kind];
|
|
253
|
+
const toolRule = lookup(policy.tools, query.names);
|
|
254
|
+
let merged: RuleInput = { ...baseRule, ...toolRule };
|
|
255
|
+
const overlay = lookup(policy.narrowTools, query.names);
|
|
256
|
+
if (overlay !== undefined) merged = narrowRule(merged, overlay, query.kind);
|
|
257
|
+
if (query.hint !== undefined) merged = narrowRule(merged, query.hint, query.kind);
|
|
258
|
+
const errorRule = policy.kinds.error;
|
|
259
|
+
const onErrorRaw =
|
|
260
|
+
merged.onError === undefined
|
|
261
|
+
? errorRule?.keep === undefined || errorRule.leave === undefined
|
|
262
|
+
? null
|
|
263
|
+
: { keep: errorRule.keep, leave: errorRule.leave }
|
|
264
|
+
: merged.onError;
|
|
265
|
+
let leave = merged.leave ?? "stub";
|
|
266
|
+
if (leave === "none" && NONE_FORBIDDEN_KINDS.has(query.kind)) leave = "stub";
|
|
267
|
+
return {
|
|
268
|
+
keep: canonicalKeep(ruleKeep(merged, ["compaction"])),
|
|
269
|
+
leave,
|
|
270
|
+
minChars: merged.minChars ?? policy.defaults.minChars,
|
|
271
|
+
onError:
|
|
272
|
+
onErrorRaw === null
|
|
273
|
+
? null
|
|
274
|
+
: {
|
|
275
|
+
keep: canonicalKeep(horizonList(onErrorRaw.keep)),
|
|
276
|
+
leave: onErrorRaw.leave === "none" ? "stub" : onErrorRaw.leave,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The rule of a non-tool item, judged by the policy of its own turn. */
|
|
282
|
+
export function kindRule(policy: ResolvedPolicy, kind: string, dialect?: string): ConcreteRule {
|
|
283
|
+
const rule = ruleFor(policy, { kind, names: [] });
|
|
284
|
+
if (kind !== "thinking" || dialect === undefined) return rule;
|
|
285
|
+
const override = Object.hasOwn(policy.dialects, dialect) ? policy.dialects[dialect] : undefined;
|
|
286
|
+
if (override === undefined) return rule;
|
|
287
|
+
return {
|
|
288
|
+
...rule,
|
|
289
|
+
keep: canonicalKeep(ruleKeep(override, rule.keep)),
|
|
290
|
+
leave: override.leave ?? rule.leave,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* A failed item outlives its kind: the longer of the kind's horizon and the
|
|
296
|
+
* error overlay's wins, and the remainder is the overlay's when it won.
|
|
297
|
+
*/
|
|
298
|
+
export function effectiveRule(rule: ConcreteRule, failed: boolean): Pick<ConcreteRule, "keep" | "leave" | "minChars"> {
|
|
299
|
+
if (!failed || rule.onError === null) return rule;
|
|
300
|
+
if (keepRank(rule.onError.keep) > keepRank(rule.keep)) {
|
|
301
|
+
return { keep: rule.onError.keep, leave: rule.onError.leave, minChars: 0 };
|
|
302
|
+
}
|
|
303
|
+
return rule;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export { narrowKeep } from "./narrow.js";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `contextRetention` — JSON-configurable tail retention. Nothing here deletes
|
|
3
|
+
* graph state: a rule decides what a projected item leaves behind once its
|
|
4
|
+
* horizon has passed. Design: `tail-retention-design.md` §3–§4.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export const RETENTION_KINDS = [
|
|
9
|
+
"image.user",
|
|
10
|
+
"image.tool",
|
|
11
|
+
"tool.read",
|
|
12
|
+
"tool.search",
|
|
13
|
+
"tool.shell",
|
|
14
|
+
"tool.write",
|
|
15
|
+
"tool.args.write",
|
|
16
|
+
"tool.web.search",
|
|
17
|
+
"tool.web.fetch",
|
|
18
|
+
"tool.mcp",
|
|
19
|
+
"tool.program",
|
|
20
|
+
"tool.agent",
|
|
21
|
+
"tool.comm",
|
|
22
|
+
"tool.other",
|
|
23
|
+
"thinking",
|
|
24
|
+
"steer",
|
|
25
|
+
"approval-note",
|
|
26
|
+
"notification",
|
|
27
|
+
"system-note",
|
|
28
|
+
"error",
|
|
29
|
+
] as const;
|
|
30
|
+
export type RetentionKind = (typeof RETENTION_KINDS)[number];
|
|
31
|
+
|
|
32
|
+
/** Authored by the user. Untrusted layers (project, agent, tool) cannot shorten these. */
|
|
33
|
+
export const USER_AUTHORED_KINDS: ReadonlySet<string> = new Set(["steer", "approval-note", "image.user"]);
|
|
34
|
+
|
|
35
|
+
/** Kinds whose item is a tool result row. `leave: "none"` is not allowed: every tool_call needs a tool row. */
|
|
36
|
+
export const NONE_FORBIDDEN_KINDS: ReadonlySet<string> = new Set([
|
|
37
|
+
"tool.read",
|
|
38
|
+
"tool.search",
|
|
39
|
+
"tool.shell",
|
|
40
|
+
"tool.write",
|
|
41
|
+
"tool.web.search",
|
|
42
|
+
"tool.web.fetch",
|
|
43
|
+
"tool.mcp",
|
|
44
|
+
"tool.program",
|
|
45
|
+
"tool.agent",
|
|
46
|
+
"tool.comm",
|
|
47
|
+
"tool.other",
|
|
48
|
+
"error",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export const horizonSchema = z.union([
|
|
52
|
+
z.enum(["round", "turn", "compaction", "forever"]),
|
|
53
|
+
z.object({ rounds: z.number().int().min(1) }).strict(),
|
|
54
|
+
z.object({ turns: z.number().int().min(1) }).strict(),
|
|
55
|
+
z.object({ newest: z.number().int().min(0) }).strict(),
|
|
56
|
+
]);
|
|
57
|
+
export type Horizon = z.output<typeof horizonSchema>;
|
|
58
|
+
|
|
59
|
+
export const leaveSchema = z.union([
|
|
60
|
+
z.enum(["none", "stub", "summary"]),
|
|
61
|
+
z.object({ head: z.number().int().min(0).max(5000) }).strict(),
|
|
62
|
+
]);
|
|
63
|
+
export type Leave = z.output<typeof leaveSchema>;
|
|
64
|
+
|
|
65
|
+
const keepSchema = z.union([horizonSchema, z.array(horizonSchema).min(1)]);
|
|
66
|
+
|
|
67
|
+
export const ruleSchema = z.object({
|
|
68
|
+
keep: keepSchema.optional(),
|
|
69
|
+
leave: leaveSchema.optional(),
|
|
70
|
+
/** Never elide below this many painted chars. */
|
|
71
|
+
minChars: z.number().int().min(0).optional(),
|
|
72
|
+
/** Error overlay for this rule. `null` disables it. */
|
|
73
|
+
onError: z.object({ keep: keepSchema, leave: leaveSchema }).nullable().optional(),
|
|
74
|
+
});
|
|
75
|
+
export type RuleInput = z.output<typeof ruleSchema>;
|
|
76
|
+
|
|
77
|
+
export const stepSchema = z.object({
|
|
78
|
+
turnBoundary: z.boolean().default(true),
|
|
79
|
+
everyRounds: z.number().int().min(1).nullable().default(4),
|
|
80
|
+
minFreedTokens: z.number().int().min(0).default(8000),
|
|
81
|
+
whenCacheCold: z.boolean().default(true),
|
|
82
|
+
atCompaction: z.literal(true).default(true),
|
|
83
|
+
});
|
|
84
|
+
export type StepConfig = z.output<typeof stepSchema>;
|
|
85
|
+
|
|
86
|
+
export const contextRetentionSchema = z.object({
|
|
87
|
+
version: z.literal(1).default(1),
|
|
88
|
+
defaults: z.object({ minChars: z.number().int().min(0).default(800) }).prefault({}),
|
|
89
|
+
step: stepSchema.prefault({}),
|
|
90
|
+
kinds: z.record(z.string(), ruleSchema).default({}),
|
|
91
|
+
/** Keys: wire name ("shell"), "mcp:<server>/<tool>", globs ("mcp:github/*", "mcp:*"). */
|
|
92
|
+
tools: z.record(z.string(), ruleSchema).default({}),
|
|
93
|
+
/** Per-dialect thinking override, e.g. {"anthropic": {"keep": "forever"}}. */
|
|
94
|
+
dialects: z.record(z.string(), ruleSchema).default({}),
|
|
95
|
+
});
|
|
96
|
+
export type ContextRetention = z.output<typeof contextRetentionSchema>;
|
|
97
|
+
|
|
98
|
+
/** A rule with every field concrete. This is what R2 stamps on a job. */
|
|
99
|
+
export type ConcreteRule = {
|
|
100
|
+
readonly keep: readonly Horizon[];
|
|
101
|
+
readonly leave: Leave;
|
|
102
|
+
readonly minChars: number;
|
|
103
|
+
readonly onError: { readonly keep: readonly Horizon[]; readonly leave: Leave } | null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** A partial policy as one source wrote it. */
|
|
107
|
+
export type RetentionLayer = {
|
|
108
|
+
readonly defaults?: { readonly minChars?: number };
|
|
109
|
+
readonly step?: Partial<StepConfig>;
|
|
110
|
+
readonly kinds?: Readonly<Record<string, RuleInput>>;
|
|
111
|
+
readonly tools?: Readonly<Record<string, RuleInput>>;
|
|
112
|
+
readonly dialects?: Readonly<Record<string, RuleInput>>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function horizonList(keep: RuleInput["keep"]): Horizon[] {
|
|
116
|
+
if (keep === undefined) return [];
|
|
117
|
+
return Array.isArray(keep) ? [...keep] : [keep];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function jsonOf(raw: unknown): unknown {
|
|
121
|
+
if (typeof raw !== "string") return raw;
|
|
122
|
+
if (raw.trim() === "") return undefined;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(raw);
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const STEP_FIELDS: ReadonlyArray<readonly [keyof StepConfig, z.ZodType]> = [
|
|
131
|
+
["turnBoundary", z.boolean()],
|
|
132
|
+
["everyRounds", z.number().int().min(1).nullable()],
|
|
133
|
+
["minFreedTokens", z.number().int().min(0)],
|
|
134
|
+
["whenCacheCold", z.boolean()],
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Tolerant parse of a `contextRetention` block (an object or its JSON text).
|
|
139
|
+
* An invalid field is dropped and never fatal, matching the rest of
|
|
140
|
+
* `config.json`. Only fields the author wrote survive: defaults are the dist
|
|
141
|
+
* layer's business, so the result is a partial layer.
|
|
142
|
+
*/
|
|
143
|
+
export function parseRetentionLayer(raw: unknown): RetentionLayer {
|
|
144
|
+
const value = jsonOf(raw);
|
|
145
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) return {};
|
|
146
|
+
const rec = value as Record<string, unknown>;
|
|
147
|
+
let layer: RetentionLayer = {};
|
|
148
|
+
const defaults = z.object({ minChars: z.number().int().min(0).optional() }).safeParse(rec.defaults);
|
|
149
|
+
if (defaults.success && defaults.data.minChars !== undefined) {
|
|
150
|
+
layer = { ...layer, defaults: { minChars: defaults.data.minChars } };
|
|
151
|
+
}
|
|
152
|
+
if (rec.step != null && typeof rec.step === "object" && !Array.isArray(rec.step)) {
|
|
153
|
+
const written = rec.step as Record<string, unknown>;
|
|
154
|
+
const step: Partial<Record<keyof StepConfig, unknown>> = {};
|
|
155
|
+
for (const [key, schema] of STEP_FIELDS) {
|
|
156
|
+
const parsed = schema.safeParse(written[key]);
|
|
157
|
+
if (parsed.success) step[key] = parsed.data;
|
|
158
|
+
}
|
|
159
|
+
if (Object.keys(step).length > 0) layer = { ...layer, step: step as Partial<StepConfig> };
|
|
160
|
+
}
|
|
161
|
+
for (const section of ["kinds", "tools", "dialects"] as const) {
|
|
162
|
+
const bag = rec[section];
|
|
163
|
+
if (bag == null || typeof bag !== "object" || Array.isArray(bag)) continue;
|
|
164
|
+
const out: Record<string, RuleInput> = {};
|
|
165
|
+
for (const [key, ruleRaw] of Object.entries(bag as Record<string, unknown>)) {
|
|
166
|
+
if (section === "kinds" && !(RETENTION_KINDS as readonly string[]).includes(key)) continue;
|
|
167
|
+
const parsed = ruleSchema.safeParse(ruleRaw);
|
|
168
|
+
if (parsed.success) out[key] = parsed.data;
|
|
169
|
+
}
|
|
170
|
+
if (Object.keys(out).length > 0) layer = { ...layer, [section]: out };
|
|
171
|
+
}
|
|
172
|
+
return layer;
|
|
173
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2: the resolved horizon of one tool job, stamped at admit. The kind comes
|
|
3
|
+
* from catalog facts read once, so later catalog churn cannot reclassify
|
|
4
|
+
* history; the projection reads the stamp and nothing else.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
AgentCallbackJob,
|
|
8
|
+
AgentCallJob,
|
|
9
|
+
AgentMessageJob,
|
|
10
|
+
AgentNotifyJob,
|
|
11
|
+
BotCallJob,
|
|
12
|
+
ContextNeedToolJob,
|
|
13
|
+
FileMutateToolJob,
|
|
14
|
+
MCPToolJob,
|
|
15
|
+
ProgrammaticJob,
|
|
16
|
+
ToolCallJob,
|
|
17
|
+
WebExtractToolJob,
|
|
18
|
+
WebSearchToolJob,
|
|
19
|
+
} from "@inhuman.tools/harness-model";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
import { ruleFor, type ResolvedPolicy } from "./resolve.js";
|
|
23
|
+
import { horizonSchema, leaveSchema, type ConcreteRule, type RetentionKind, type RuleInput } from "./schema.js";
|
|
24
|
+
|
|
25
|
+
const SEARCH_TOOLS: ReadonlySet<string> = new Set(["grep", "fff_search"]);
|
|
26
|
+
const READ_TOOLS: ReadonlySet<string> = new Set(["read_file", "list_dir", "glob"]);
|
|
27
|
+
const COMM_TOOLS: ReadonlySet<string> = new Set(["update_metadata"]);
|
|
28
|
+
export const WRITE_ARG_TOOLS: ReadonlySet<string> = new Set(["write_file", "edit_file"]);
|
|
29
|
+
|
|
30
|
+
export type RetainedJob = ToolCallJob | ProgrammaticJob;
|
|
31
|
+
|
|
32
|
+
export function kindOfJob(job: RetainedJob, entryClass?: string): RetentionKind {
|
|
33
|
+
if (job instanceof ProgrammaticJob) return "tool.program";
|
|
34
|
+
if (job instanceof MCPToolJob) return "tool.mcp";
|
|
35
|
+
if (job instanceof WebSearchToolJob) return "tool.web.search";
|
|
36
|
+
if (job instanceof WebExtractToolJob) return "tool.web.fetch";
|
|
37
|
+
if (job instanceof FileMutateToolJob) return "tool.write";
|
|
38
|
+
if (job instanceof AgentCallJob || job instanceof BotCallJob || job instanceof AgentMessageJob) return "tool.agent";
|
|
39
|
+
if (job instanceof AgentCallbackJob || job instanceof AgentNotifyJob || job instanceof ContextNeedToolJob) {
|
|
40
|
+
return "tool.comm";
|
|
41
|
+
}
|
|
42
|
+
if (job.name === "shell") return "tool.shell";
|
|
43
|
+
if (SEARCH_TOOLS.has(job.name)) return "tool.search";
|
|
44
|
+
if (READ_TOOLS.has(job.name) || entryClass === "read") return "tool.read";
|
|
45
|
+
if (COMM_TOOLS.has(job.name)) return "tool.comm";
|
|
46
|
+
return "tool.other";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `tools` keys that can name this job, most specific first. */
|
|
50
|
+
export function namesOfJob(job: RetainedJob): string[] {
|
|
51
|
+
if (job instanceof ProgrammaticJob) return [job.toolName];
|
|
52
|
+
if (job instanceof MCPToolJob) return [`mcp:${job.connectionKey}/${job.serverToolName}`, job.name];
|
|
53
|
+
return [job.name];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stampSchema = z.object({
|
|
57
|
+
kind: z.string(),
|
|
58
|
+
keep: z.array(horizonSchema),
|
|
59
|
+
leave: leaveSchema,
|
|
60
|
+
minChars: z.number().int().min(0),
|
|
61
|
+
onError: z.object({ keep: z.array(horizonSchema), leave: leaveSchema }).nullable(),
|
|
62
|
+
args: z
|
|
63
|
+
.object({
|
|
64
|
+
keep: z.array(horizonSchema),
|
|
65
|
+
leave: leaveSchema,
|
|
66
|
+
minChars: z.number().int().min(0),
|
|
67
|
+
})
|
|
68
|
+
.optional(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export type JobStamp = {
|
|
72
|
+
readonly kind: string;
|
|
73
|
+
readonly rule: ConcreteRule;
|
|
74
|
+
/** The rule for the call's own arguments (`write_file` / `edit_file` content). */
|
|
75
|
+
readonly args?: Pick<ConcreteRule, "keep" | "leave" | "minChars">;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function stampJobRetention(
|
|
79
|
+
job: RetainedJob,
|
|
80
|
+
policy: ResolvedPolicy,
|
|
81
|
+
opts: { readonly entryClass?: string; readonly hint?: RuleInput } = {},
|
|
82
|
+
): void {
|
|
83
|
+
const kind = kindOfJob(job, opts.entryClass);
|
|
84
|
+
const names = namesOfJob(job);
|
|
85
|
+
const rule = ruleFor(policy, { kind, names, ...(opts.hint === undefined ? {} : { hint: opts.hint }) });
|
|
86
|
+
const argsRule =
|
|
87
|
+
job instanceof ToolCallJob && WRITE_ARG_TOOLS.has(job.name)
|
|
88
|
+
? ruleFor(policy, { kind: "tool.args.write", names: [] })
|
|
89
|
+
: undefined;
|
|
90
|
+
job.retentionJson = JSON.stringify({
|
|
91
|
+
kind,
|
|
92
|
+
keep: rule.keep,
|
|
93
|
+
leave: rule.leave,
|
|
94
|
+
minChars: rule.minChars,
|
|
95
|
+
onError: rule.onError,
|
|
96
|
+
...(argsRule === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { args: { keep: argsRule.keep, leave: argsRule.leave, minChars: argsRule.minChars } }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const stamps = new Map<string, JobStamp | null>();
|
|
103
|
+
|
|
104
|
+
export function parseJobStamp(json: string): JobStamp | null {
|
|
105
|
+
if (json === "") return null;
|
|
106
|
+
const hit = stamps.get(json);
|
|
107
|
+
if (hit !== undefined) return hit;
|
|
108
|
+
let out: JobStamp | null = null;
|
|
109
|
+
try {
|
|
110
|
+
const parsed = stampSchema.safeParse(JSON.parse(json));
|
|
111
|
+
if (parsed.success) {
|
|
112
|
+
const { kind, args, ...rule } = parsed.data;
|
|
113
|
+
out = { kind, rule, ...(args === undefined ? {} : { args }) };
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
out = null;
|
|
117
|
+
}
|
|
118
|
+
if (stamps.size > 512) stamps.clear();
|
|
119
|
+
stamps.set(json, out);
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R3: the retention clock. Deep horizons expire against `at`, which only moves
|
|
3
|
+
* at a step, so between steps the projected prefix is byte-stable and only the
|
|
4
|
+
* tail grows. The live inputs (wall clock, prior `warmUntilMs`, estimated
|
|
5
|
+
* savings) are read once here and recorded; the projection reads only `at`.
|
|
6
|
+
*/
|
|
7
|
+
import { CompletionExpectation, latestCompactCutoff, owningTurn } from "@inhuman.tools/harness-model";
|
|
8
|
+
|
|
9
|
+
import { projectHistory } from "../history.js";
|
|
10
|
+
import type { StepConfig } from "./schema.js";
|
|
11
|
+
import { parseClock, policyOfTurn, type ClockReason, type RetentionClock } from "./view.js";
|
|
12
|
+
|
|
13
|
+
export type StepFacts = {
|
|
14
|
+
readonly self: string;
|
|
15
|
+
readonly step: StepConfig;
|
|
16
|
+
/** Prior completion on the spine: its recorded clock target, and when its cache lapses. */
|
|
17
|
+
readonly prior: { readonly at: string | null; readonly warmUntilMs: number | null } | null;
|
|
18
|
+
readonly turnFirstRound: boolean;
|
|
19
|
+
readonly afterCutoff: boolean;
|
|
20
|
+
/** Rounds between the prior clock target and this completion. */
|
|
21
|
+
readonly roundsSinceAt: number;
|
|
22
|
+
readonly now: number;
|
|
23
|
+
/** Estimated tokens a step would free. Lazy: walks the spine. */
|
|
24
|
+
readonly freedTokens: () => number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Pure: the order is compaction, turn, cold, every, freed, else hold. */
|
|
28
|
+
export function decideRetentionStep(f: StepFacts): RetentionClock {
|
|
29
|
+
const step = (reason: ClockReason, extra: Partial<RetentionClock> = {}): RetentionClock => ({
|
|
30
|
+
at: f.self,
|
|
31
|
+
reason,
|
|
32
|
+
...extra,
|
|
33
|
+
});
|
|
34
|
+
if (f.afterCutoff && f.step.atCompaction) return step("compaction");
|
|
35
|
+
const priorAt = f.prior?.at ?? null;
|
|
36
|
+
if (priorAt === null) return step("turn");
|
|
37
|
+
if (f.turnFirstRound && f.step.turnBoundary) return step("turn");
|
|
38
|
+
const warm = f.prior?.warmUntilMs ?? null;
|
|
39
|
+
if (f.step.whenCacheCold && warm !== null && f.now > warm) return step("cold", { coldByMs: f.now - warm });
|
|
40
|
+
if (f.step.everyRounds !== null && f.roundsSinceAt >= f.step.everyRounds) return step("every");
|
|
41
|
+
if (f.step.minFreedTokens > 0) {
|
|
42
|
+
const freed = f.freedTokens();
|
|
43
|
+
if (freed >= f.step.minFreedTokens) return step("freed", { freedTokens: freed });
|
|
44
|
+
}
|
|
45
|
+
return { at: priorAt, reason: "hold" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tokensOf(rows: Iterable<unknown>): number {
|
|
49
|
+
let chars = 0;
|
|
50
|
+
for (const row of rows)
|
|
51
|
+
chars += JSON.stringify(row, (_key, value) => (value instanceof Uint8Array ? value.length : value)).length;
|
|
52
|
+
return Math.ceil(chars / 4);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Stamp `tip.retentionClockJson` when its round opens. Call once the
|
|
57
|
+
* completion is on its turn's tape (it must be reachable from its turn).
|
|
58
|
+
*/
|
|
59
|
+
export function stampRetentionClock(tip: CompletionExpectation, now: number = Date.now()): RetentionClock {
|
|
60
|
+
const turn = owningTurn(tip);
|
|
61
|
+
const policy = policyOfTurn(turn);
|
|
62
|
+
const spine = tip.spine();
|
|
63
|
+
const index = spine.indexOf(tip);
|
|
64
|
+
const prior = index > 0 ? spine[index - 1]! : null;
|
|
65
|
+
const priorAt = prior === null ? null : (parseClock(prior.retentionClockJson)?.at ?? null);
|
|
66
|
+
const priorPos = priorAt === null ? -1 : spine.findIndex((c) => c.uuid === priorAt);
|
|
67
|
+
const clock = decideRetentionStep({
|
|
68
|
+
self: tip.uuid,
|
|
69
|
+
step: policy.step,
|
|
70
|
+
prior: prior === null ? null : { at: priorPos < 0 ? null : priorAt, warmUntilMs: prior.warmUntilMs },
|
|
71
|
+
turnFirstRound: prior === null || owningTurn(prior) !== turn,
|
|
72
|
+
afterCutoff: prior === null && turn !== null && latestCompactCutoff(turn) !== null,
|
|
73
|
+
roundsSinceAt: priorPos < 0 ? 0 : index - priorPos,
|
|
74
|
+
now,
|
|
75
|
+
freedTokens: () => {
|
|
76
|
+
const held = spine[priorPos];
|
|
77
|
+
if (held === undefined) return 0;
|
|
78
|
+
const before = tokensOf(projectHistory(tip, { clockAt: held }));
|
|
79
|
+
const after = tokensOf(projectHistory(tip, { clockAt: tip }));
|
|
80
|
+
return Math.max(0, before - after);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
tip.retentionClockJson = JSON.stringify(clock);
|
|
84
|
+
return clock;
|
|
85
|
+
}
|