@sema-agent/core 5.55.0 → 5.57.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/CHANGELOG.md +140 -0
- package/dist/agents/send-message-tool.d.ts +11 -0
- package/dist/agents/send-message-tool.js +81 -13
- package/dist/agents/subagent.js +250 -89
- package/dist/agents/team.d.ts +10 -1
- package/dist/agents/team.js +1 -0
- package/dist/brain/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/mcp.js +4 -4
- package/dist/core/memory-engine/engine.d.ts +15 -5
- package/dist/core/memory-engine/engine.js +3 -1
- package/dist/core/permission-rule-consent.d.ts +45 -0
- package/dist/core/permission-rule-consent.js +40 -11
- package/dist/core/permission-rule-model.d.ts +110 -75
- package/dist/core/permission-rule-model.js +61 -28
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +66 -26
- package/dist/core/runner/runtask.d.ts +4 -1
- package/dist/core/runner/runtask.js +206 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/scheduler.d.ts +5 -0
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/side-query.d.ts +12 -5
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +69 -11
- package/dist/core/untrusted-text.js +8 -0
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +114 -13
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +16 -6
- package/dist/engine/llm/types.d.ts +65 -0
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +12 -6
- package/dist/prompts/default.js +2 -0
- package/dist/scenarios/scenario-registry.d.ts +5 -1
- package/dist/scenarios/scenario-registry.js +4 -2
- package/dist/tools/fs/index.js +8 -1
- package/dist/tools/scheduler-tools.js +28 -6
- package/dist/tools/web.d.ts +15 -0
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export const ROUTE_ADJUDICATION_CONFORMANCE_CORPUS = [
|
|
2
|
+
{
|
|
3
|
+
name: "per-model-on-root",
|
|
4
|
+
intent: "a per-model credential is paired by construction, on the deployment root",
|
|
5
|
+
model: { id: "m-main", baseUrl: "https://gw.example.com" },
|
|
6
|
+
credential: { source: "per-model", apiKey: "k-main" },
|
|
7
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
8
|
+
expect: { ok: true, posture: "per-model" },
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "per-model-off-root",
|
|
12
|
+
intent: "a per-model credential is paired by construction even off the deployment root (the resolver was asked about THIS entry)",
|
|
13
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net/v1" },
|
|
14
|
+
credential: { source: "per-model", apiKey: "k-third" },
|
|
15
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
16
|
+
expect: { ok: true, posture: "per-model" },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "per-model-headers-borne",
|
|
20
|
+
intent: "an entry-declared auth header is a per-model credential (headers-borne)",
|
|
21
|
+
model: { id: "m-hdr", baseUrl: "https://third-party.example.net" },
|
|
22
|
+
credential: { source: "per-model", headersBorne: true, carrierBag: "model" },
|
|
23
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
24
|
+
expect: { ok: true, posture: "per-model" },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "per-model-empty-string-key",
|
|
28
|
+
intent: "an empty-string per-model key is a PRESENT credential (fails closed at emit, never falls to the deployment key)",
|
|
29
|
+
model: { id: "m-degraded", baseUrl: "https://third-party.example.net" },
|
|
30
|
+
credential: { source: "per-model", apiKey: "" },
|
|
31
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
32
|
+
expect: { ok: true, posture: "per-model" },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "deployment-paired",
|
|
36
|
+
intent: "the main-gateway pass: entry URL === declared config root, deployment credential allowed",
|
|
37
|
+
model: { id: "m-main", baseUrl: "https://gw.example.com" },
|
|
38
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
39
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
40
|
+
expect: { ok: true, posture: "paired" },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "deployment-paired-empty-entry",
|
|
44
|
+
intent: "an entry with an empty baseUrl targets the config root — deployment credential paired",
|
|
45
|
+
model: { id: "m-implicit", baseUrl: "" },
|
|
46
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
47
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
48
|
+
expect: { ok: true, posture: "paired" },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "deployment-paired-slash-normalized",
|
|
52
|
+
intent: "trailing slashes do not break the pairing (both halves normalize before compare)",
|
|
53
|
+
model: { id: "m-main", baseUrl: "https://gw.example.com/" },
|
|
54
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
55
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
56
|
+
expect: { ok: true, posture: "paired" },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "deployment-mismatch",
|
|
60
|
+
intent: "THE torn pair: deployment credential + entry URL off the declared root = refusal, not a silently-followed key",
|
|
61
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net/v1" },
|
|
62
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
63
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
64
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
65
|
+
leg: "named",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "deployment-mismatch-path-differs",
|
|
69
|
+
intent: "a differing path is a differing root (no prefix leniency — the credential goes to the exact declared root only)",
|
|
70
|
+
model: { id: "m-path", baseUrl: "https://gw.example.com/other" },
|
|
71
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
72
|
+
config: { baseUrl: "https://gw.example.com/v1" },
|
|
73
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
74
|
+
leg: "named",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "deployment-mismatch-derived-leg",
|
|
78
|
+
intent: "the same torn pair on a DERIVED model: same verdict; the engine disposes of it as fallback-to-primary + notice",
|
|
79
|
+
model: { id: "m-summarize", baseUrl: "https://third-party.example.net" },
|
|
80
|
+
credential: { source: "deployment-config", apiKey: "k-gw" },
|
|
81
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
82
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
83
|
+
leg: "derived",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "deployment-headers-borne-mismatch",
|
|
87
|
+
intent: "a deployment auth HEADER is the same fallback arm: off the declared root it is refused, not sent",
|
|
88
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net" },
|
|
89
|
+
credential: { source: "deployment-config", headersBorne: true, carrierBag: "config" },
|
|
90
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
91
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
92
|
+
leg: "named",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "deployment-unpinned",
|
|
96
|
+
intent: "the quick-start posture: config key with NO declared config root stays allowed (single-endpoint form: key on config, URL on the model) — surfaced as unpinned, not refused",
|
|
97
|
+
model: { id: "m-quickstart", baseUrl: "https://api.example.com/v1" },
|
|
98
|
+
credential: { source: "deployment-config", apiKey: "k-quickstart" },
|
|
99
|
+
config: {},
|
|
100
|
+
expect: { ok: true, posture: "unpinned" },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "absent-keyless-on-root",
|
|
104
|
+
intent: "a keyless deployment on its own root has nothing to protect — allowed",
|
|
105
|
+
model: { id: "m-local", baseUrl: "http://127.0.0.1:8000/v1" },
|
|
106
|
+
credential: { source: "absent" },
|
|
107
|
+
config: { baseUrl: "http://127.0.0.1:8000/v1" },
|
|
108
|
+
expect: { ok: true, posture: "keyless" },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "absent-keyless-undeclared",
|
|
112
|
+
intent: "keyless with no declared config root (URL only on the model) — allowed",
|
|
113
|
+
model: { id: "m-local", baseUrl: "http://127.0.0.1:8000/v1" },
|
|
114
|
+
credential: { source: "absent" },
|
|
115
|
+
config: {},
|
|
116
|
+
expect: { ok: true, posture: "keyless" },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "absent-missing-off-root",
|
|
120
|
+
intent: "an entry declaring its OWN URL away from a DECLARED deployment root with no credential anywhere is a configuration hole — loud refusal beats an unauthenticated request to a third-party host",
|
|
121
|
+
model: { id: "m-third", baseUrl: "https://third-party.example.net" },
|
|
122
|
+
credential: { source: "absent" },
|
|
123
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
124
|
+
expect: { ok: false, code: "route.credential_missing" },
|
|
125
|
+
leg: "named",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "absent-missing-derived-leg",
|
|
129
|
+
intent: "the same hole on a DERIVED model: same verdict; engine disposition is fallback-to-primary + notice",
|
|
130
|
+
model: { id: "m-summarize", baseUrl: "https://third-party.example.net" },
|
|
131
|
+
credential: { source: "absent" },
|
|
132
|
+
config: { baseUrl: "https://gw.example.com" },
|
|
133
|
+
expect: { ok: false, code: "route.credential_missing" },
|
|
134
|
+
leg: "derived",
|
|
135
|
+
},
|
|
136
|
+
];
|
package/dist/brain/routing.js
CHANGED
|
@@ -15,13 +15,17 @@ function errorMessage(model, text) {
|
|
|
15
15
|
}
|
|
16
16
|
export function createRoutingBrain(routes, opts = {}) {
|
|
17
17
|
const by = opts.by ?? "provider";
|
|
18
|
-
const
|
|
18
|
+
const resolveTarget = (model) => {
|
|
19
19
|
const key = by === "api" ? model.api : model.provider;
|
|
20
20
|
const routed = Object.hasOwn(routes, key) ? routes[key] : undefined;
|
|
21
21
|
const matched = routed !== null && typeof routed === "object" && typeof routed.stream === "function"
|
|
22
22
|
? routed
|
|
23
23
|
: undefined;
|
|
24
|
-
|
|
24
|
+
return matched ?? opts.fallback;
|
|
25
|
+
};
|
|
26
|
+
const stream = (model, context, options) => {
|
|
27
|
+
const key = by === "api" ? model.api : model.provider;
|
|
28
|
+
const target = resolveTarget(model);
|
|
25
29
|
if (!target) {
|
|
26
30
|
const out = createAssistantMessageEventStream();
|
|
27
31
|
out.push({
|
|
@@ -33,5 +37,6 @@ export function createRoutingBrain(routes, opts = {}) {
|
|
|
33
37
|
}
|
|
34
38
|
return target.stream(model, context, options);
|
|
35
39
|
};
|
|
36
|
-
|
|
40
|
+
const adjudicateRoute = (model, perModelAuth) => resolveTarget(model)?.adjudicateRoute?.(model, perModelAuth);
|
|
41
|
+
return { stream, adjudicateRoute };
|
|
37
42
|
}
|
|
@@ -383,7 +383,12 @@ export interface MaybeCompactOptions {
|
|
|
383
383
|
* compaction is FORCED through the real `generateSummary` LLM path even if the provider would hit — this
|
|
384
384
|
* bounds summary drift from indefinite reuse. The caller owns the counter via {@link onCompaction}
|
|
385
385
|
* (`reused`) and feeds it back via this option; core treats `summaryProvider` as if absent for that one
|
|
386
|
-
* boundary when `consecutiveProviderReuse >= maxConsecutiveProviderReuse`. Default `3
|
|
386
|
+
* boundary when `consecutiveProviderReuse >= maxConsecutiveProviderReuse`. Default `3`, a
|
|
387
|
+
* SEMA choice — the earlier "(CC parity)" label was unsupported and is withdrawn (anchoring
|
|
388
|
+
* re-check 2026-08-23: CC 2.1.223 has no external summary provider and no reuse concept at all;
|
|
389
|
+
* its three 3s are the PTL retry cap `b$d`, the consecutive-compaction-failure breaker `C$d` and
|
|
390
|
+
* the rapid-refill trip `Q3u`, none of them this). 3 is picked for the same reason those are: a
|
|
391
|
+
* small bound on how long a degraded path may keep answering before the real one is forced.
|
|
387
392
|
* `0`/undefined with no `consecutiveProviderReuse` = provider always consulted (no forced refresh).
|
|
388
393
|
*/
|
|
389
394
|
maxConsecutiveProviderReuse?: number;
|
|
@@ -618,9 +623,17 @@ export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{
|
|
|
618
623
|
* (clamped ≥0, so ≤0 means "freed nothing") — orthogonal to rule 2/3's threshold test, which
|
|
619
624
|
* misses this shape because the bloated post (44609) can still sit UNDER the threshold (44800),
|
|
620
625
|
* so rule 3 alone would re-enable the force and repeat the negative-yield pass every boundary.
|
|
621
|
-
* SEMA-ONLY DEFENSE, not a CC port
|
|
622
|
-
*
|
|
623
|
-
*
|
|
626
|
+
* SEMA-ONLY DEFENSE, not a CC port — but the ORIGINAL justification for that label was wrong
|
|
627
|
+
* on both of its premises, and the corrected pair still supports it. (a) "CC's full compact
|
|
628
|
+
* keeps tail 0 and monotonically shrinks" describes the `GNo` branch, which a LOCAL threshold
|
|
629
|
+
* compaction never reaches (CC 2.1.223 `y9s` :432704 routes local passes to the reactive
|
|
630
|
+
* pipeline, which preserves the last group verbatim — `YMo` :399971, `s = 1` :399982); CC's
|
|
631
|
+
* default posture therefore has a keep-tail, exactly as ours does. (b) "CC has no
|
|
632
|
+
* request-layer trim" holds only for the MAIN conversation request: CC does own a
|
|
633
|
+
* keep-the-newest-groups-that-fit primitive (`UPb` :645223), whose only caller is the
|
|
634
|
+
* prompt-hook evaluator's own transcript window, not a main-lane request. What actually makes
|
|
635
|
+
* this regime ours is the third factor — the trim seam feeding a deceived usage anchor, and
|
|
636
|
+
* attachment mass re-entering after the fold — not the absence of a keep-tail. Scoped to trim-forced
|
|
624
637
|
* passes only (`trimForced`), so Seam C reused-summary landings (freed≈0 by design) on the
|
|
625
638
|
* natural/manual paths never trip it. Release path unchanged: backoff gates only the FORCE, a
|
|
626
639
|
* later natural landing that posts under threshold (rule 3, non-trim-forced) turns it back off.
|
|
@@ -196,6 +196,9 @@ export async function maybeCompact(opts) {
|
|
|
196
196
|
if (prep.value.elidedMessages !== undefined && prep.value.elidedMessages > 0) {
|
|
197
197
|
details.elidedMessages = prep.value.elidedMessages;
|
|
198
198
|
}
|
|
199
|
+
if (prep.value.carriedUnsummarizedMessages !== undefined && prep.value.carriedUnsummarizedMessages > 0) {
|
|
200
|
+
details.unsummarizedMessages = prep.value.carriedUnsummarizedMessages;
|
|
201
|
+
}
|
|
199
202
|
}
|
|
200
203
|
else {
|
|
201
204
|
let summaryModel = opts.compactionModel ?? opts.model;
|
|
@@ -14,11 +14,18 @@ export declare const EDIT_FRACTION = 0.7;
|
|
|
14
14
|
* effectiveWindow = autocompactWindow − min(maxOutputTokens, 20000) (CC `Nye`, cap `uMd`)
|
|
15
15
|
* trigger = effectiveWindow − 13000 (CC `dSo`, buffer `rMd`)
|
|
16
16
|
* i.e. trigger = W − 33000 for every model whose max output is ≥ 20k: a 200k window triggers at
|
|
17
|
-
* 167000 (83.5% of nominal), a 1M window at 967000.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* 167000 (83.5% of nominal), a 1M window at 967000.
|
|
18
|
+
*
|
|
19
|
+
* 1M DIVERGENCE, stated as fact rather than as an equivalence (anchoring re-check 2026-08-23): CC
|
|
20
|
+
* carries a MODEL-DEFAULT autocompact-window table (2.1.223 `j3u` :242918 — `claude-sonnet-5`
|
|
21
|
+
* default 967000, and since 223 a surface dimension: `remote_cowork` / `local-agent` 500000), so a
|
|
22
|
+
* 1M sonnet-5 gets 967000 as its window and 934000 as its trigger WITHOUT anyone configuring
|
|
23
|
+
* anything. We ship no such table: a model declaring only `contextWindow: 1e6` triggers at 967000
|
|
24
|
+
* here, 33000 LATER than CC. `model.autoCompactTokens = 967000` reproduces CC's number — but that
|
|
25
|
+
* is an EMBEDDER action against CC's product default, not the same posture, and the choice not to
|
|
26
|
+
* follow 934000 is deliberate (design/146 §1.3 argues against living on that thin a cushion). The
|
|
27
|
+
* knob itself is real and unchanged: `autoCompactTokens` lowers only this trigger-side window while
|
|
28
|
+
* the guard and physical request budgeting stay on the 1M window — CC's dual-window shape. We take the
|
|
22
29
|
* 20000 cap branch unconditionally (flat 33000), i.e. the `min(maxOutputTokens, 20000)` branch is
|
|
23
30
|
* deliberately NOT ported: the deduction is exact parity only for models with max output ≥ 20k. A
|
|
24
31
|
* model with a smaller max output would deduct less in CC (trigger LATER); we deduct the full
|
|
@@ -51,11 +58,53 @@ export declare function contextEditFrontier(window: number): number;
|
|
|
51
58
|
* PowerShell is kept for CC fidelity even though sema does not mount it. Overridable per call via
|
|
52
59
|
* {@link ContextEditOptions.compactableTools} for custom-tool-heavy embedders.
|
|
53
60
|
*/
|
|
61
|
+
/**
|
|
62
|
+
* How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs.
|
|
63
|
+
*
|
|
64
|
+
* 3, UNCHANGED — and the fact that CC's corresponding number is 5 (2.1.223 `uAp`, and the same value
|
|
65
|
+
* on CC 88's `timeBasedMCConfig`, so it is stable across both corpus generations) is deliberately NOT
|
|
66
|
+
* adopted here. Two reasons, in order of weight:
|
|
67
|
+
*
|
|
68
|
+
* 1. MEASURED: widening the window removes this pass's only lever in the shape where a terminal
|
|
69
|
+
* parallel batch holds exactly as many large results as the window keeps. Probed on a 200k model
|
|
70
|
+
* with a 140k usage anchor and five 36k-char Bash results: at 3 the pass clears two and the
|
|
71
|
+
* request lands at ~167k, UNDER the 177k request guard; at 5 nothing is clearable, the request
|
|
72
|
+
* stays at 185k, and the guard cannot recover it either — `trimToBudget` must turn-align back to
|
|
73
|
+
* the emitting assistant, so the whole batch is retained. The window is count-based on both
|
|
74
|
+
* sides (CC's `slice(-keepRecent)` too), so this ceiling exists at every value; 5 simply widens
|
|
75
|
+
* the band that reaches it.
|
|
76
|
+
* 2. SEAT: the two numbers do not govern the same machine. CC's keep-recent clear runs ONLY on the
|
|
77
|
+
* `context_hint` rejection leg — gated off by default — and behind a hard "saves ≥ 20000 tokens
|
|
78
|
+
* or don't bother" test, with request REFUSAL as the real backstop. Ours is on by default, is
|
|
79
|
+
* the only reduction between the frontier and the guard, and its backstop drops messages instead
|
|
80
|
+
* of refusing. Copying a constant across that difference is the "same name, different question"
|
|
81
|
+
* mistake, not parity.
|
|
82
|
+
*
|
|
83
|
+
* So the VALUE is an open adjudication (recorded with the probe above), while the two things CC
|
|
84
|
+
* unambiguously answers — the floor and the candidate-scoped window — are followed exactly.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
|
|
87
|
+
/** Minimum kept results — CC `EUs` :397710 `Math.max(1, keepRecent)`. See
|
|
88
|
+
* {@link ContextEditOptions.keepRecentToolResults} for why both degenerate ends are unusable. */
|
|
89
|
+
export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
|
|
54
90
|
export declare const COMPACTABLE_TOOLS: ReadonlySet<string>;
|
|
55
91
|
export interface ContextEditOptions {
|
|
56
92
|
/** Start clearing once estimated context tokens exceed this. */
|
|
57
93
|
budgetTokens: number;
|
|
58
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Always keep the content of this many most-recent CANDIDATE tool results (candidates = results
|
|
96
|
+
* from {@link compactableTools}). Default {@link DEFAULT_KEEP_RECENT_TOOL_RESULTS} = 3 — see that
|
|
97
|
+
* constant for why CC's 5 is not adopted here.
|
|
98
|
+
*
|
|
99
|
+
* Floored at 1, which IS CC's rule (`EUs` `Math.max(1, keepRecent)`), whose own source note gives
|
|
100
|
+
* the reason: 0 leaves the model with zero working tool context, and in CC's `slice(-0)` spelling
|
|
101
|
+
* it degenerates the other way into keeping everything — two unusable extremes for one value.
|
|
102
|
+
* Ours degenerated identically (a 0 or negative `keep` cleared EVERY candidate). Values that are
|
|
103
|
+
* not a non-negative safe integer are REFUSED rather than floored: a NaN used to make this whole
|
|
104
|
+
* defense silently inert (`slice(0, NaN)` = clear nothing), which is the one outcome a context
|
|
105
|
+
* defense must never reach quietly, and there is no config door upstream to announce at — this
|
|
106
|
+
* option only ever arrives as a direct argument from an embedder.
|
|
107
|
+
*/
|
|
59
108
|
keepRecentToolResults?: number;
|
|
60
109
|
/**
|
|
61
110
|
* roadmap #6② (CC contentReplacementState parity): when set, a result's FULL TEXT is offloaded to
|
|
@@ -27,6 +27,17 @@ export const COMPACTION_TRIGGER_BUFFER_TOKENS = 13000;
|
|
|
27
27
|
export function contextEditFrontier(window) {
|
|
28
28
|
return Math.max(window - (CONTEXT_OUTPUT_RESERVE_TOKENS + COMPACTION_TRIGGER_BUFFER_TOKENS), Math.floor(window * EDIT_FRACTION));
|
|
29
29
|
}
|
|
30
|
+
export const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
|
|
31
|
+
export const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
|
|
32
|
+
function resolveKeepRecentToolResults(value) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return DEFAULT_KEEP_RECENT_TOOL_RESULTS;
|
|
35
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
36
|
+
throw new TypeError(`keepRecentToolResults must be a non-negative safe integer (got ${String(value)}); ` +
|
|
37
|
+
`omit it for the default ${DEFAULT_KEEP_RECENT_TOOL_RESULTS}`);
|
|
38
|
+
}
|
|
39
|
+
return Math.max(MIN_KEEP_RECENT_TOOL_RESULTS, value);
|
|
40
|
+
}
|
|
30
41
|
export const COMPACTABLE_TOOLS = new Set([
|
|
31
42
|
"Read",
|
|
32
43
|
"Bash",
|
|
@@ -74,7 +85,7 @@ export function clearStaleToolResults(messages, opts) {
|
|
|
74
85
|
if (total <= opts.budgetTokens) {
|
|
75
86
|
return messages;
|
|
76
87
|
}
|
|
77
|
-
const keep = opts.keepRecentToolResults
|
|
88
|
+
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults);
|
|
78
89
|
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
79
90
|
const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
|
|
80
91
|
const clearable = toolResultCandidates.slice(0, Math.max(0, toolResultCandidates.length - keep));
|