@sema-agent/core 5.25.0 → 5.26.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 +66 -0
- package/dist/agents/agent-definition.js +5 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/core/hooks.js +3 -2
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +45 -1
- package/dist/core/memory-engine/engine.js +23 -5
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +85 -17
- package/dist/core/runner/prepare-task.js +31 -7
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +68 -0
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +5 -4
- package/package.json +1 -1
|
@@ -28,14 +28,89 @@ export interface ToolResultStore {
|
|
|
28
28
|
* durable store still owns key hygiene for its storage layer). RB-266: the floor of that hygiene is
|
|
29
29
|
* NOT per-backend — every implementation rejects the unsafe ref set via {@link assertSafeToolResultRef}
|
|
30
30
|
* (a volatile backend silently accepting what a durable one rejects is the drift this closes).
|
|
31
|
+
*
|
|
32
|
+
* `provenance` (backlog #119, 2026-08-10) states WHO the stored bytes belong to, so a host-side read
|
|
33
|
+
* face can authorize a fetch without parsing the ref. Its rules, uniform on every backend:
|
|
34
|
+
* - **the write-once election decides the owner too** — whoever wins the race to store the content
|
|
35
|
+
* is the owner, and no later writer can change that. A backend that publishes content and owner
|
|
36
|
+
* as two objects (the file one does) may, if it is interrupted between them, leave an entry
|
|
37
|
+
* content-complete and UNOWNED; that is the fail-closed direction (a read face refuses it) and it
|
|
38
|
+
* is never mis-attribution, but it does mean "unowned" is a reachable state, not only a
|
|
39
|
+
* pre-provenance one. A single-row backend (memory/SQL) has no such window;
|
|
40
|
+
* - a later `put` on the same ref carrying the **same** provenance (or none) is the usual idempotent
|
|
41
|
+
* no-op — the stored owner is neither re-written nor erased;
|
|
42
|
+
* - a later `put` carrying a **different** provenance is a typed refusal
|
|
43
|
+
* ({@link ToolResultRefConflictError}, `code: "tool_result.ref_conflict"`), never a silent no-op:
|
|
44
|
+
* `ref` is the storage PRIMARY KEY, and with a host-supplied `sessionId` two distinct owners can
|
|
45
|
+
* land on one ref — under a silent write-once the second owner would read back the first owner's
|
|
46
|
+
* bytes, which is an identity fault, not a caching detail;
|
|
47
|
+
* - provenance must never be FINER-GRAINED than the ref's own key. A ref keyed on
|
|
48
|
+
* (session, tool call) is legitimately written by two different tasks of one session — the
|
|
49
|
+
* Runner shares one store across its tasks on purpose, and a BYOM brain may mint the same
|
|
50
|
+
* tool-call id in both — so stamping a `taskId` there would turn a designed sharing case into a
|
|
51
|
+
* refusal. Write sites whose ref carries the task (the background-agent and monitor spills key
|
|
52
|
+
* on the handle id) state it; the offload/budget/projection sites state the session only;
|
|
53
|
+
* - an entry stored **without** provenance (written by an older engine, or by a call site that has
|
|
54
|
+
* none) stays UNOWNED: `put` never back-fills an owner onto it (that would be adoption by a party
|
|
55
|
+
* that cannot prove it wrote it) and {@link ownerOf} keeps answering `undefined`, which a read face
|
|
56
|
+
* reads fail-closed. Back-filling such rows is an explicit migration, never a side effect of a write.
|
|
31
57
|
*/
|
|
32
|
-
put(ref: string, content: string): Promise<void> | void;
|
|
58
|
+
put(ref: string, content: string, provenance?: ToolResultProvenance): Promise<void> | void;
|
|
33
59
|
/** Read a slice of the stored content. Unknown `ref` → undefined. */
|
|
34
60
|
get(ref: string, opts?: {
|
|
35
61
|
offset?: number;
|
|
36
62
|
limit?: number;
|
|
37
63
|
}): Promise<ToolResultSlice | undefined> | ToolResultSlice | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Backlog #119 — the READ-side half of provenance: who owns `ref`. `undefined` for an unknown ref AND
|
|
66
|
+
* for a stored-but-unowned one (a row whose write stated no owner), which a read face treats identically:
|
|
67
|
+
* fail-closed, nobody is authorized. Without this operation a `put`-only provenance parameter would be
|
|
68
|
+
* write-only bookkeeping — the read face would still have nothing to decide on.
|
|
69
|
+
*
|
|
70
|
+
* Typed OPTIONAL so an implementation compiled against an older engine still satisfies the interface;
|
|
71
|
+
* the published contract kit (`toolResultStoreContract`) nevertheless REQUIRES it, because a backend
|
|
72
|
+
* that cannot answer this cannot back a host read face at all.
|
|
73
|
+
*/
|
|
74
|
+
ownerOf?(ref: string): Promise<ToolResultProvenance | undefined> | ToolResultProvenance | undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Backlog #119 — WHO a stored tool result belongs to. Structured (not a single opaque string) because
|
|
78
|
+
* the read face authorizes by COMPARING fields it already knows; `taskId` is present when the write site
|
|
79
|
+
* has one (it is a narrowing of the session, never a substitute for it).
|
|
80
|
+
*
|
|
81
|
+
* Deliberately NOT part of it: `toolCallId`. It is a retention/addressing coordinate, not an ownership
|
|
82
|
+
* one — two different tool calls in one session are the same owner.
|
|
83
|
+
*/
|
|
84
|
+
export interface ToolResultProvenance {
|
|
85
|
+
readonly sessionId: string;
|
|
86
|
+
readonly taskId?: string;
|
|
38
87
|
}
|
|
88
|
+
/** Backlog #119 — the `code` on {@link ToolResultRefConflictError}, so callers branch on a constant. */
|
|
89
|
+
export declare const TOOL_RESULT_REF_CONFLICT_CODE = "tool_result.ref_conflict";
|
|
90
|
+
/**
|
|
91
|
+
* Backlog #119 — a `put` whose provenance disagrees with the stored one. Typed (a `code`, like
|
|
92
|
+
* `CheckpointError`/`WorkflowRunStoreError`) rather than a silent no-op: this is the one write-once
|
|
93
|
+
* outcome where "keep what is there" would hand the second caller a reference to somebody else's bytes.
|
|
94
|
+
*/
|
|
95
|
+
export declare class ToolResultRefConflictError extends Error {
|
|
96
|
+
readonly ref: string;
|
|
97
|
+
readonly stored: ToolResultProvenance;
|
|
98
|
+
readonly incoming: ToolResultProvenance;
|
|
99
|
+
readonly code = "tool_result.ref_conflict";
|
|
100
|
+
constructor(ref: string, stored: ToolResultProvenance, incoming: ToolResultProvenance);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Backlog #119 — the ONE comparison every backend uses, so "same owner" cannot mean two things in two
|
|
104
|
+
* stores. A missing PROVENANCE on either side ⇒ no verdict (an unowned entry is never adopted, and a
|
|
105
|
+
* put with no provenance never disturbs a stored owner). Both present ⇒ every field must agree,
|
|
106
|
+
* including `taskId`: a missing task id is a DIFFERENT owner from a stated one, not a wildcard that
|
|
107
|
+
* matches it — two writers that disagree about how narrowly they own a key have not proven they are
|
|
108
|
+
* the same writer. Unequal ⇒ {@link ToolResultRefConflictError}.
|
|
109
|
+
*/
|
|
110
|
+
export declare function assertToolResultProvenanceMatch(ref: string, stored: ToolResultProvenance | undefined, incoming: ToolResultProvenance | undefined): void;
|
|
111
|
+
/** Normalize to the stored shape: an ABSENT `taskId` and an explicit `undefined` one are one value, so
|
|
112
|
+
* round-tripping through a durable backend (NULL column, missing JSON key) compares equal. */
|
|
113
|
+
export declare function normalizeToolResultProvenance(p: ToolResultProvenance): ToolResultProvenance;
|
|
39
114
|
/**
|
|
40
115
|
* RB-266 — the ref-safety half of the {@link ToolResultStore} contract, shared by every backend.
|
|
41
116
|
*
|
|
@@ -64,7 +139,7 @@ export interface ToolResultStore {
|
|
|
64
139
|
export declare function assertSafeToolResultRef(ref: string): void;
|
|
65
140
|
/**
|
|
66
141
|
* RB-273 — the SINGLE mint point for a tool-result `ref`. Three call sites compose the same
|
|
67
|
-
* `tr_<sessionId
|
|
142
|
+
* `tr_<sessionId>~<toolCallId>` string (`withToolResultOffload` here, the aggregate budget's preview
|
|
68
143
|
* pass, and the runner's clear-with-offload persist); this is that string's one definition.
|
|
69
144
|
*
|
|
70
145
|
* Why it must sanitize: `sessionId` is engine-minted, but **`toolCallId` is PROVIDER-minted** — it is
|
|
@@ -87,8 +162,76 @@ export declare function assertSafeToolResultRef(ref: string): void;
|
|
|
87
162
|
* and an unsafe one only ever lived in a memory/pg deployment, where the ref the model reads back comes
|
|
88
163
|
* from the `<persisted-output ref="…">` text already in its context — the read path does not re-mint,
|
|
89
164
|
* and `get` is keyed on that literal string.
|
|
165
|
+
*
|
|
166
|
+
* ── Backlog #119 (2026-08-10): the composition is now INJECTIVE ───────────────────────────────────
|
|
167
|
+
* It was not. With `_` between the segments — a character legal INSIDE both — `tr_team_blue_x`
|
|
168
|
+
* decomposed as ("team","blue_x") and as ("team_blue","x"), i.e. two distinct (session, tool call)
|
|
169
|
+
* pairs minted one ref. `ref` is the store's PRIMARY KEY under a write-once contract, so the second
|
|
170
|
+
* pair did not get an error, it got the FIRST pair's bytes back — and `sessionId` is host-supplied in a
|
|
171
|
+
* BYOM deployment, so the two pairs need not be the same tenant. (Measured on the published artifact:
|
|
172
|
+
* ("s","1_a_b") and ("s_1","a_b") produced the same ref.)
|
|
173
|
+
*
|
|
174
|
+
* The separator is now {@link REF_SEGMENT_SEPARATOR} — a character OUTSIDE {@link NATIVE_REF_CHARSET}.
|
|
175
|
+
* `refSegment` emits only characters from that charset (identity branch by test; fold branch by
|
|
176
|
+
* construction: a readable base with every out-of-charset char replaced, plus hex), so the separator
|
|
177
|
+
* can never occur inside a segment and `tr_<seg>~<seg>` decomposes exactly one way. Injectivity of the
|
|
178
|
+
* whole map then follows from `refSegment`'s own injectivity per segment.
|
|
179
|
+
*
|
|
180
|
+
* Why this is NOT a breaking change, spelled out because it looks like one:
|
|
181
|
+
* - refs are OPAQUE HANDLES. A ref reaches the model inside the preview text and comes back verbatim
|
|
182
|
+
* to `ReadToolResult`; `get` is keyed on that literal string and no read path re-mints;
|
|
183
|
+
* - so an entry written under the old form stays readable forever: an old ref in an old context reads
|
|
184
|
+
* back byte-for-byte, on every backend (the file backend's filename encoding is likewise keyed on
|
|
185
|
+
* the literal ref, and is identity for the old form);
|
|
186
|
+
* - the mint is deterministic per process, so a ref MINTED here is also RESOLVED here — there is no
|
|
187
|
+
* window in which one process writes `~` and another expects `_` for the same pair. The upgrade
|
|
188
|
+
* boundary is a process restart, and it needs no double-read window;
|
|
189
|
+
* - the only observable change is the ref STRING for newly offloaded results, which no wire contract
|
|
190
|
+
* pins (it is generated text, and the length ceiling downstream validators apply is unchanged).
|
|
191
|
+
* The store-key charset is unchanged as far as the shared rule is concerned ({@link assertSafeToolResultRef}
|
|
192
|
+
* accepts `~`: it is not empty/dot/separator/control). A backend whose NATIVE key space excludes `~`
|
|
193
|
+
* (the file one's filename charset) already owns an injective encoding into it — the RB-273 rule — so
|
|
194
|
+
* for that backend this simply moves engine refs from the identity branch to the digest branch.
|
|
195
|
+
*
|
|
196
|
+
* `extraSegments` (also #119) is how a call site that addresses something FINER than one tool call —
|
|
197
|
+
* a `details` member's JSON path, a stale-projection content digest — keeps that coordinate in its own
|
|
198
|
+
* segment instead of splicing it into the `toolCallId` one. Splicing was the ticket's other residue:
|
|
199
|
+
* a ref built from the compound `"<callId> <path>"` is exactly the ref another call would mint if its
|
|
200
|
+
* provider-assigned toolCallId happened to BE that whole string. With the coordinate in its own
|
|
201
|
+
* segment the two can no longer coincide — the separator cannot occur inside a segment, so refs with
|
|
202
|
+
* different segment COUNTS are always distinct, and same-count refs are distinct unless every segment
|
|
203
|
+
* matches.
|
|
204
|
+
*/
|
|
205
|
+
export declare function buildToolResultRef(sessionId: string, toolCallId: string, ...extraSegments: string[]): string;
|
|
206
|
+
/**
|
|
207
|
+
* Backlog #119 — the ceiling on a ref this module can mint, so a durable backend can size its key
|
|
208
|
+
* column from a stated invariant instead of a guess (TiDB's was `VARCHAR(191)`, which a long
|
|
209
|
+
* three-segment ref overruns — a truncating key aliases distinct refs).
|
|
210
|
+
*
|
|
211
|
+
* Derivation: `"tr_"` (3) + four segments at their own ceiling + three separators. A segment is either
|
|
212
|
+
* an identity one (≤ {@link MAX_REF_SEGMENT_CHARS}) or a folded one (marker + ≤32 base + `-` + 64 hex
|
|
213
|
+
* = 102), so {@link MAX_REF_SEGMENT_CHARS} is the larger. Four segments is the engine's own widest
|
|
214
|
+
* shape (session + tool call + a `details` path + the content coordinate); `buildToolResultRef` is
|
|
215
|
+
* variadic, so a caller that passes more owns the wider bound.
|
|
216
|
+
*/
|
|
217
|
+
export declare const MAX_MINTED_TOOL_RESULT_REF_CHARS: number;
|
|
218
|
+
/**
|
|
219
|
+
* Backlog #119 (adversarial review, round 2) — the CONTENT coordinate of an offload ref.
|
|
220
|
+
*
|
|
221
|
+
* A tool-call id carries no cross-turn uniqueness contract: a BYOM brain legitimately mints recurring
|
|
222
|
+
* ids like `call-1`, and the Runner shares one store across a session's tasks on purpose. Keyed on
|
|
223
|
+
* (session, tool call) alone, the second result under a recurring id hits a write-once no-op — a
|
|
224
|
+
* SUCCESSFUL put that stored nothing — while its own preview goes out naming that ref, so paging it
|
|
225
|
+
* back returns the FIRST result's bytes. Provenance cannot see this (both writers are the same
|
|
226
|
+
* session, and they legitimately are), so the ref itself has to carry content identity.
|
|
227
|
+
*
|
|
228
|
+
* 128 bits, the same width and the same reasoning as the stale-result projection that closed this in
|
|
229
|
+
* its own lane earlier: tool results are untrusted text (web content rides them), and a 48-bit
|
|
230
|
+
* truncation is birthday-collidable at ~2^24 work — a deliberate collision plus a recurring id would
|
|
231
|
+
* re-open exactly the cross-serving this closes. Deterministic, so a re-derived ref for identical
|
|
232
|
+
* content is byte-identical and the preview stays prompt-cache stable.
|
|
90
233
|
*/
|
|
91
|
-
export declare function
|
|
234
|
+
export declare function toolResultContentSegment(text: string): string;
|
|
92
235
|
export interface ToolResultSlice {
|
|
93
236
|
content: string;
|
|
94
237
|
/** Byte/char offset this slice starts at. */
|
|
@@ -108,12 +251,17 @@ export declare class InMemoryToolResultStore implements ToolResultStore {
|
|
|
108
251
|
private readonly opts?;
|
|
109
252
|
/** put/get only — no scheduled deletion contract: honestly `"none"` (retention.ts). */
|
|
110
253
|
readonly retention: "none";
|
|
254
|
+
/** #119: the entry is content AND owner as ONE value — the write-once election that decides the
|
|
255
|
+
* content decides the owner in the same step (no window where an entry exists ownerless). */
|
|
111
256
|
private readonly map;
|
|
112
257
|
private totalChars;
|
|
113
258
|
constructor(opts?: {
|
|
114
259
|
maxTotalChars?: number;
|
|
115
260
|
} | undefined);
|
|
116
|
-
put(ref: string, content: string): void;
|
|
261
|
+
put(ref: string, content: string, provenance?: ToolResultProvenance): void;
|
|
262
|
+
/** #119 — the owner recorded at the winning write; `undefined` for unknown AND for unowned entries,
|
|
263
|
+
* which a read face treats identically (fail-closed). */
|
|
264
|
+
ownerOf(ref: string): ToolResultProvenance | undefined;
|
|
117
265
|
/** design/80 D-2: true when NOTHING has been offloaded — a durable suspend can then proceed safely even on
|
|
118
266
|
* this in-memory store, because a cross-replica resume has no offloaded result to deref to null. */
|
|
119
267
|
isEmpty(): boolean;
|
|
@@ -147,15 +295,18 @@ export declare class ScopedToolResultStore implements ToolResultStore {
|
|
|
147
295
|
readonly volatileBacking: boolean;
|
|
148
296
|
constructor(inner: ToolResultStore, scope: string);
|
|
149
297
|
/** Length-prefixed namespace — unambiguous for any scope string (no delimiter-injection ambiguity).
|
|
150
|
-
* RB-266: the scope segment is
|
|
151
|
-
*
|
|
152
|
-
*
|
|
298
|
+
* RB-266: the scope segment is ENCODED (injective, and its output charset carries no path separator
|
|
299
|
+
* or control character), so a deployment whose trust scope contains one cannot compose a key the
|
|
300
|
+
* inner store must reject — the ref-safety rule then applies to the caller's ref alone. */
|
|
153
301
|
private key;
|
|
154
|
-
put(ref: string, content: string): Promise<void> | void;
|
|
302
|
+
put(ref: string, content: string, provenance?: ToolResultProvenance): Promise<void> | void;
|
|
155
303
|
get(ref: string, opts?: {
|
|
156
304
|
offset?: number;
|
|
157
305
|
limit?: number;
|
|
158
306
|
}): Promise<ToolResultSlice | undefined> | ToolResultSlice | undefined;
|
|
307
|
+
/** #119 — same namespacing as {@link get}. `undefined` when the inner store predates the operation
|
|
308
|
+
* (an unowned answer, which the read face reads fail-closed — never a fabricated owner). */
|
|
309
|
+
ownerOf(ref: string): Promise<ToolResultProvenance | undefined> | ToolResultProvenance | undefined;
|
|
159
310
|
/** THIS task's offload count only — the D-2 gate's per-task semantics (see class doc). */
|
|
160
311
|
isEmpty(): boolean;
|
|
161
312
|
}
|
|
@@ -171,6 +322,10 @@ export declare const OFFLOAD_TOOL_NAME = "ReadToolResult";
|
|
|
171
322
|
* "expired" are different answers). If the marker write fails too, the generic miss remains.
|
|
172
323
|
*/
|
|
173
324
|
export declare function createOffloadPersist(store: ToolResultStore, sessionId: string): (toolCallId: string, fullText: string) => string;
|
|
325
|
+
/** Backlog #119 — build the provenance a write site records, from the two coordinates every write site
|
|
326
|
+
* already has in hand. `taskId` is omitted (not `undefined`-valued) when the run declares none, so the
|
|
327
|
+
* stored shape compares equal across a durable round-trip. */
|
|
328
|
+
export declare function toolResultProvenanceOf(sessionId: string, taskId?: string): ToolResultProvenance;
|
|
174
329
|
/**
|
|
175
330
|
* RB-374① — the ONE source of the "page the offloaded text back" suggestion sentence, shared by
|
|
176
331
|
* {@link buildPreview} (offload preview tail) and context-edit's cleared-marker refNote. The two
|
|
@@ -228,7 +383,7 @@ export declare function buildPreview(full: string, ref: string, sizes?: {
|
|
|
228
383
|
}, reachableTools?: ReadonlySet<string>): string;
|
|
229
384
|
/**
|
|
230
385
|
* Wrap an {@link AgentTool} so that an oversized text result is offloaded to `store` and replaced with a
|
|
231
|
-
* preview + `ref` before it ever reaches the session/model. `ref = tr_<toolCallId>` is stable and the
|
|
386
|
+
* preview + `ref` before it ever reaches the session/model. `ref = tr_<sessionId>~<toolCallId>` is stable and the
|
|
232
387
|
* store is write-once: a **same-process** replay keeps an identical (cacheable) preview; a **cross-process**
|
|
233
388
|
* wake needs a durable store (the default in-memory store loses the full text — the preview still stands).
|
|
234
389
|
* Image blocks are left untouched; only text is offloaded. Forwards the `onUpdate` progress callback so a
|
|
@@ -2,6 +2,34 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { defineTool, errorResult } from "./tools.js";
|
|
4
4
|
import { TOOL_SEARCH_NAME } from "./runner/tool-disclosure.js";
|
|
5
|
+
export const TOOL_RESULT_REF_CONFLICT_CODE = "tool_result.ref_conflict";
|
|
6
|
+
export class ToolResultRefConflictError extends Error {
|
|
7
|
+
ref;
|
|
8
|
+
stored;
|
|
9
|
+
incoming;
|
|
10
|
+
code = TOOL_RESULT_REF_CONFLICT_CODE;
|
|
11
|
+
constructor(ref, stored, incoming) {
|
|
12
|
+
super(`tool-result store: ref ${JSON.stringify(ref)} already belongs to a different origin ` +
|
|
13
|
+
`(stored ${describeProvenance(stored)}, offered ${describeProvenance(incoming)}) — refusing to store under it`);
|
|
14
|
+
this.ref = ref;
|
|
15
|
+
this.stored = stored;
|
|
16
|
+
this.incoming = incoming;
|
|
17
|
+
this.name = "ToolResultRefConflictError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function describeProvenance(p) {
|
|
21
|
+
return p.taskId === undefined ? `session=${p.sessionId}` : `session=${p.sessionId} task=${p.taskId}`;
|
|
22
|
+
}
|
|
23
|
+
export function assertToolResultProvenanceMatch(ref, stored, incoming) {
|
|
24
|
+
if (stored === undefined || incoming === undefined)
|
|
25
|
+
return;
|
|
26
|
+
if (stored.sessionId === incoming.sessionId && stored.taskId === incoming.taskId)
|
|
27
|
+
return;
|
|
28
|
+
throw new ToolResultRefConflictError(ref, stored, incoming);
|
|
29
|
+
}
|
|
30
|
+
export function normalizeToolResultProvenance(p) {
|
|
31
|
+
return p.taskId === undefined ? { sessionId: p.sessionId } : { sessionId: p.sessionId, taskId: p.taskId };
|
|
32
|
+
}
|
|
5
33
|
export function assertSafeToolResultRef(ref) {
|
|
6
34
|
const bad = ref === "" ||
|
|
7
35
|
ref === "." ||
|
|
@@ -14,14 +42,23 @@ export function assertSafeToolResultRef(ref) {
|
|
|
14
42
|
}
|
|
15
43
|
const NATIVE_REF_CHARSET = /^[A-Za-z0-9_.-]+$/;
|
|
16
44
|
const MAX_REF_SEGMENT_CHARS = 128;
|
|
17
|
-
|
|
18
|
-
|
|
45
|
+
const REF_SEGMENT_SEPARATOR = "~";
|
|
46
|
+
const FOLDED_SEGMENT_PREFIX = "fold.";
|
|
47
|
+
export function buildToolResultRef(sessionId, toolCallId, ...extraSegments) {
|
|
48
|
+
return [`tr_${refSegment(sessionId)}`, refSegment(toolCallId), ...extraSegments.map(refSegment)].join(REF_SEGMENT_SEPARATOR);
|
|
19
49
|
}
|
|
20
50
|
function refSegment(raw) {
|
|
21
|
-
if (raw.length <= MAX_REF_SEGMENT_CHARS && NATIVE_REF_CHARSET.test(raw))
|
|
51
|
+
if (raw.length <= MAX_REF_SEGMENT_CHARS && NATIVE_REF_CHARSET.test(raw) && !raw.startsWith(FOLDED_SEGMENT_PREFIX))
|
|
22
52
|
return raw;
|
|
23
53
|
const base = raw.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 32);
|
|
24
|
-
return `${base || "x"}-${
|
|
54
|
+
return `${FOLDED_SEGMENT_PREFIX}${base || "x"}-${digestOfString(raw)}`;
|
|
55
|
+
}
|
|
56
|
+
export const MAX_MINTED_TOOL_RESULT_REF_CHARS = 3 + 4 * MAX_REF_SEGMENT_CHARS + 3;
|
|
57
|
+
export function toolResultContentSegment(text) {
|
|
58
|
+
return `s${digestOfString(text).slice(0, 32)}`;
|
|
59
|
+
}
|
|
60
|
+
function digestOfString(s) {
|
|
61
|
+
return createHash("sha256").update(Buffer.from(s, "utf16le")).digest("hex");
|
|
25
62
|
}
|
|
26
63
|
export class InMemoryToolResultStore {
|
|
27
64
|
opts;
|
|
@@ -35,27 +72,33 @@ export class InMemoryToolResultStore {
|
|
|
35
72
|
throw new Error("InMemoryToolResultStore: maxTotalChars must be a number (got NaN) — omit it for an unbounded store");
|
|
36
73
|
}
|
|
37
74
|
}
|
|
38
|
-
put(ref, content) {
|
|
75
|
+
put(ref, content, provenance) {
|
|
39
76
|
assertSafeToolResultRef(ref);
|
|
40
|
-
|
|
77
|
+
const existing = this.map.get(ref);
|
|
78
|
+
if (existing !== undefined) {
|
|
79
|
+
assertToolResultProvenanceMatch(ref, existing.provenance, provenance);
|
|
41
80
|
return;
|
|
42
|
-
|
|
81
|
+
}
|
|
82
|
+
this.map.set(ref, provenance === undefined ? { content } : { content, provenance: normalizeToolResultProvenance(provenance) });
|
|
43
83
|
this.totalChars += content.length;
|
|
44
84
|
const cap = this.opts?.maxTotalChars;
|
|
45
85
|
if (cap !== undefined && Number.isFinite(cap)) {
|
|
46
|
-
for (const [oldRef,
|
|
86
|
+
for (const [oldRef, oldEntry] of this.map) {
|
|
47
87
|
if (this.totalChars <= cap || oldRef === ref)
|
|
48
88
|
break;
|
|
49
89
|
this.map.delete(oldRef);
|
|
50
|
-
this.totalChars -=
|
|
90
|
+
this.totalChars -= oldEntry.content.length;
|
|
51
91
|
}
|
|
52
92
|
}
|
|
53
93
|
}
|
|
94
|
+
ownerOf(ref) {
|
|
95
|
+
return this.map.get(ref)?.provenance;
|
|
96
|
+
}
|
|
54
97
|
isEmpty() {
|
|
55
98
|
return this.map.size === 0;
|
|
56
99
|
}
|
|
57
100
|
get(ref, opts) {
|
|
58
|
-
const full = this.map.get(ref);
|
|
101
|
+
const full = this.map.get(ref)?.content;
|
|
59
102
|
if (full === undefined)
|
|
60
103
|
return undefined;
|
|
61
104
|
const offset = Math.min(full.length, Math.max(0, intOr(opts?.offset, 0)));
|
|
@@ -77,21 +120,32 @@ export class ScopedToolResultStore {
|
|
|
77
120
|
this.volatileBacking = inner instanceof InMemoryToolResultStore;
|
|
78
121
|
}
|
|
79
122
|
key(ref) {
|
|
80
|
-
const scope =
|
|
123
|
+
const scope = encodeScopeSegment(this.scope);
|
|
81
124
|
return `${scope.length}:${scope}:${ref}`;
|
|
82
125
|
}
|
|
83
|
-
put(ref, content) {
|
|
126
|
+
put(ref, content, provenance) {
|
|
84
127
|
assertSafeToolResultRef(ref);
|
|
85
128
|
this.localPuts++;
|
|
86
|
-
return this.inner.put(this.key(ref), content);
|
|
129
|
+
return this.inner.put(this.key(ref), content, provenance);
|
|
87
130
|
}
|
|
88
131
|
get(ref, opts) {
|
|
89
132
|
return this.inner.get(this.key(ref), opts);
|
|
90
133
|
}
|
|
134
|
+
ownerOf(ref) {
|
|
135
|
+
return this.inner.ownerOf?.(this.key(ref));
|
|
136
|
+
}
|
|
91
137
|
isEmpty() {
|
|
92
138
|
return this.localPuts === 0;
|
|
93
139
|
}
|
|
94
140
|
}
|
|
141
|
+
function encodeScopeSegment(scope) {
|
|
142
|
+
let out = "";
|
|
143
|
+
for (let i = 0; i < scope.length; i++) {
|
|
144
|
+
const ch = scope[i];
|
|
145
|
+
out += /[A-Za-z0-9]/.test(ch) ? ch : `%${ch.charCodeAt(0).toString(16).padStart(4, "0")}`;
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
95
149
|
export function isVolatileOffloadStore(store) {
|
|
96
150
|
if (store instanceof ScopedToolResultStore)
|
|
97
151
|
return store.volatileBacking;
|
|
@@ -99,15 +153,19 @@ export function isVolatileOffloadStore(store) {
|
|
|
99
153
|
}
|
|
100
154
|
export const OFFLOAD_TOOL_NAME = "ReadToolResult";
|
|
101
155
|
export function createOffloadPersist(store, sessionId) {
|
|
156
|
+
const provenance = toolResultProvenanceOf(sessionId);
|
|
102
157
|
return (toolCallId, fullText) => {
|
|
103
|
-
const ref = buildToolResultRef(sessionId, toolCallId);
|
|
104
|
-
void Promise.resolve(store.put(ref, fullText)).catch((err) => {
|
|
158
|
+
const ref = buildToolResultRef(sessionId, toolCallId, toolResultContentSegment(fullText));
|
|
159
|
+
void Promise.resolve(store.put(ref, fullText, provenance)).catch((err) => {
|
|
105
160
|
const cause = err instanceof Error ? err.message : String(err);
|
|
106
|
-
void Promise.resolve(store.put(ref, `[offload LOST at write time: ${cause} — the inline preview is all that survived]
|
|
161
|
+
void Promise.resolve(store.put(ref, `[offload LOST at write time: ${cause} — the inline preview is all that survived]`, provenance)).catch(() => undefined);
|
|
107
162
|
});
|
|
108
163
|
return ref;
|
|
109
164
|
};
|
|
110
165
|
}
|
|
166
|
+
export function toolResultProvenanceOf(sessionId, taskId) {
|
|
167
|
+
return taskId === undefined ? { sessionId } : { sessionId, taskId };
|
|
168
|
+
}
|
|
111
169
|
export function offloadPagebackHint(ref, form, reachableTools) {
|
|
112
170
|
const activate = reachableTools !== undefined && !reachableTools.has(OFFLOAD_TOOL_NAME)
|
|
113
171
|
? `${OFFLOAD_TOOL_NAME} is not active yet — call ${TOOL_SEARCH_NAME} with {"query":"select:${OFFLOAD_TOOL_NAME}"} to activate it, then `
|
|
@@ -164,9 +222,10 @@ export function buildPreview(full, ref, sizes, reachableTools) {
|
|
|
164
222
|
export function withToolResultOffload(tool, store, thresholdChars, sessionId, reachableTools) {
|
|
165
223
|
if (!(Number.isFinite(thresholdChars) && thresholdChars > 0))
|
|
166
224
|
return tool;
|
|
225
|
+
const provenance = toolResultProvenanceOf(sessionId);
|
|
167
226
|
const wrappedExecute = async (toolCallId, params, signal, onUpdate) => {
|
|
168
227
|
const res = await tool.execute(toolCallId, params, signal, onUpdate);
|
|
169
|
-
const offloadedDetails = res.details === undefined ? undefined : await offloadOversizedDetailStrings(res.details, store, thresholdChars, sessionId, toolCallId);
|
|
228
|
+
const offloadedDetails = res.details === undefined ? undefined : await offloadOversizedDetailStrings(res.details, store, thresholdChars, sessionId, toolCallId, provenance);
|
|
170
229
|
const withDetails = (r) => offloadedDetails === undefined || offloadedDetails.value === res.details ? r : { ...r, details: offloadedDetails.value };
|
|
171
230
|
if (totalTextChars(res.content) <= thresholdChars)
|
|
172
231
|
return withDetails(res);
|
|
@@ -176,20 +235,20 @@ export function withToolResultOffload(tool, store, thresholdChars, sessionId, re
|
|
|
176
235
|
.join("\n");
|
|
177
236
|
if (full.length <= PREVIEW_HEAD_CHARS + PREVIEW_TAIL_CHARS)
|
|
178
237
|
return withDetails(res);
|
|
179
|
-
const ref = buildToolResultRef(sessionId, toolCallId);
|
|
180
|
-
await store.put(ref, full);
|
|
238
|
+
const ref = buildToolResultRef(sessionId, toolCallId, toolResultContentSegment(full));
|
|
239
|
+
await store.put(ref, full, provenance);
|
|
181
240
|
const images = res.content.filter((b) => b.type !== "text");
|
|
182
241
|
return withDetails({ ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] });
|
|
183
242
|
};
|
|
184
243
|
return { ...tool, execute: wrappedExecute };
|
|
185
244
|
}
|
|
186
245
|
const OFFLOADED_DETAIL_HEAD_CHARS = 2_000;
|
|
187
|
-
const OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS =
|
|
246
|
+
const OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS = MAX_MINTED_TOOL_RESULT_REF_CHARS + 200;
|
|
188
247
|
export const OFFLOADED_DETAIL_NOTICE_PREFIX = "…[offloaded — ";
|
|
189
248
|
export function isOffloadedDetailReplacement(s) {
|
|
190
249
|
return s.includes(`\n${OFFLOADED_DETAIL_NOTICE_PREFIX}`);
|
|
191
250
|
}
|
|
192
|
-
async function offloadOversizedDetailStrings(details, store, thresholdChars, sessionId, toolCallId) {
|
|
251
|
+
async function offloadOversizedDetailStrings(details, store, thresholdChars, sessionId, toolCallId, provenance) {
|
|
193
252
|
const puts = [];
|
|
194
253
|
const onStack = new Set();
|
|
195
254
|
const isPlainObject = (v) => {
|
|
@@ -199,8 +258,8 @@ async function offloadOversizedDetailStrings(details, store, thresholdChars, ses
|
|
|
199
258
|
return p === Object.prototype || p === null;
|
|
200
259
|
};
|
|
201
260
|
const replace = (full, path) => {
|
|
202
|
-
const detailRef = buildToolResultRef(sessionId, toolCallId
|
|
203
|
-
puts.push(Promise.resolve(store.put(detailRef, full)));
|
|
261
|
+
const detailRef = buildToolResultRef(sessionId, toolCallId, path, toolResultContentSegment(full));
|
|
262
|
+
puts.push(Promise.resolve(store.put(detailRef, full, provenance)));
|
|
204
263
|
return (`${full.slice(0, OFFLOADED_DETAIL_HEAD_CHARS)}\n` +
|
|
205
264
|
`${OFFLOADED_DETAIL_NOTICE_PREFIX}${full.length} chars total; ref "${detailRef}"; the remainder is retained in this run's tool-result store and reads back through the deployment's tool-results face]`);
|
|
206
265
|
};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -320,6 +320,12 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
|
320
320
|
* text under `"static"`, where the placeholder is never swapped. Use for large/MCP tool sets where
|
|
321
321
|
* inlining hundreds of schemas blows up turn-1 tokens and risks prefix-cache breakage. Default: not
|
|
322
322
|
* deferred (full schema inlined). See {@link RunnerDeps.deferMode}.
|
|
323
|
+
*
|
|
324
|
+
* SCOPE: this flag reaches the classifier through CALLER specs (`TaskSpec.tools`) only — a
|
|
325
|
+
* built-in/injected tool's spec never enters that list, so setting `defer` on a spec that shadows a
|
|
326
|
+
* built-in name does nothing. To defer an already-mounted tool (built-ins included), list its wire
|
|
327
|
+
* name in {@link TaskSpec.deferTools}. Also inert when the tool ends up excluded (exclusion wins)
|
|
328
|
+
* or pinned ({@link alwaysLoad} / {@link TaskSpec.alwaysLoadTools}).
|
|
323
329
|
*/
|
|
324
330
|
defer?: boolean;
|
|
325
331
|
/**
|
|
@@ -705,6 +711,17 @@ export interface ToolExecuteContext {
|
|
|
705
711
|
* is a DEPLOYMENT property, not a per-task one — a child in the same sandbox needs the same facts
|
|
706
712
|
* (its env block renders the scratchpad section; its root fence admits the scratchpad dir). */
|
|
707
713
|
envFacts?: TaskSpec["envFacts"];
|
|
714
|
+
/**
|
|
715
|
+
* The parent task's DECLARED {@link TaskSpec.memoryPersistenceCapable} — present ONLY when the spec
|
|
716
|
+
* set it (an inferred run's ctx gains no key), Runner-filled on the same trusted seat as
|
|
717
|
+
* {@link principal}: never a model/tool argument. A delegation tool (`createSubagentTool`) forwards
|
|
718
|
+
* it into every child spec it builds so the deployment's persistence statement survives delegation:
|
|
719
|
+
* `false` is a floor a chosen agent definition cannot loosen (the read-only-memory disclosure must
|
|
720
|
+
* hold tree-wide when the deployment says nothing durable is reachable), `true` is a default a
|
|
721
|
+
* definition may narrow back to `false`. Absent ⇒ nothing is forwarded and each child's own roster
|
|
722
|
+
* inference decides, exactly as before the seat existed.
|
|
723
|
+
*/
|
|
724
|
+
memoryPersistenceCapable?: boolean;
|
|
708
725
|
/**
|
|
709
726
|
* [893]④a — the parent task's per-model auth hook ({@link TaskSpec.getApiKeyAndHeaders}), inherited
|
|
710
727
|
* verbatim down the delegation tree like `principal`/`clientContext` (Runner-filled, read-only, NEVER
|
|
@@ -1058,6 +1075,19 @@ export interface AgentDefinition {
|
|
|
1058
1075
|
maxTurns?: number;
|
|
1059
1076
|
/** Long-term memory scope for this agent (same shape as {@link TaskSpec.memory}). */
|
|
1060
1077
|
memory?: TaskSpec["memory"];
|
|
1078
|
+
/**
|
|
1079
|
+
* This agent's own {@link TaskSpec.memoryPersistenceCapable} declaration — the same three-state
|
|
1080
|
+
* statement, made per definition: an agent whose closure tools persist memory declares `true`
|
|
1081
|
+
* (suppressing the read-only-memory disclosure no roster inference can clear), one whose roster
|
|
1082
|
+
* looks write-capable but reaches no durable store declares `false` (forcing the disclosure).
|
|
1083
|
+
* Arbitration with the spawning run's declaration: a parent's explicit `false` is a FLOOR and wins
|
|
1084
|
+
* over a definition `true` (the disclosure is about the deployment's storage, which choosing this
|
|
1085
|
+
* agent does not change); otherwise a declared definition value wins over the parent's; absent
|
|
1086
|
+
* both, the child spec carries no value and the child's own roster inference decides.
|
|
1087
|
+
* On the workflow lane (`agent(…, {agentType})`) the definition value fills an ABSENT governed-spec
|
|
1088
|
+
* value only — a base/spec declaration of either polarity wins (the lane's spec-pinned-fields rule).
|
|
1089
|
+
*/
|
|
1090
|
+
memoryPersistenceCapable?: boolean;
|
|
1061
1091
|
/**
|
|
1062
1092
|
* Observer agents (CC 2.1.206 parity — 逐字锚 docs/CC206-OBSERVER-ANCHORS-2026-07-11.md, schema
|
|
1063
1093
|
* 面 C @28385708): "Agent type auto-spawned as a background observer whenever this agent runs.
|
|
@@ -2016,6 +2046,39 @@ export interface TaskSpec {
|
|
|
2016
2046
|
* modify the project. No effect unless an `executionEnv` is injected. Default false.
|
|
2017
2047
|
*/
|
|
2018
2048
|
handsReadOnly?: boolean;
|
|
2049
|
+
/**
|
|
2050
|
+
* Whether this run can persist a user's "remember X" somewhere durable — the deployment's own
|
|
2051
|
+
* statement, overriding the runner's inference. The runner mounts a read-only-memory disclosure
|
|
2052
|
+
* (the model must decline to "remember" instead of receipting a save that never happens) when the
|
|
2053
|
+
* session provably cannot persist; provability is inferred from KNOWN store paths only (a mounted
|
|
2054
|
+
* file-write tool or a write-capable shell). A caller tool that persists through its own closure
|
|
2055
|
+
* (a custom memory writer) is invisible to that inference and would be contradicted by the
|
|
2056
|
+
* disclosure — set `true` to declare the channel and suppress it. Set `false` to force the
|
|
2057
|
+
* disclosure even when write-capable tools mount (e.g. they cannot reach any durable store);
|
|
2058
|
+
* a forced `false` also replaces the `# Memory` write instruction itself and drops the
|
|
2059
|
+
* preference-writing discipline and index seed that serve it.
|
|
2060
|
+
* Default: inferred. The inference also treats a REMOTE execution env's hand band as unable to
|
|
2061
|
+
* reach the host-side memory root (sandbox filesystem); a deployment whose remote env shares a
|
|
2062
|
+
* mount with the memory root declares `true` — on such a remote, `true` also restores the
|
|
2063
|
+
* `# Memory` write instruction when a Write tool mounts (a declared-capable session must be told
|
|
2064
|
+
* the path, not left silent). If your persistence channel is NOT the file face, do not mount a
|
|
2065
|
+
* tool named `Write` alongside the declaration, or keep the default and teach your own channel.
|
|
2066
|
+
* Delegation: a DECLARED value crosses the delegation boundary (sync / background / fork spawns and
|
|
2067
|
+
* the retained-resume snapshot alike). An explicit `false` is a FLOOR — a chosen
|
|
2068
|
+
* {@link AgentDefinition.memoryPersistenceCapable} `true` cannot loosen it (the disclosure is
|
|
2069
|
+
* about the deployment's storage, which no agent selection changes); an explicit `true` is a
|
|
2070
|
+
* DEFAULT the chosen definition may narrow back to `false`; absent = absent downstream too — each
|
|
2071
|
+
* child runs its own inference over its own roster. A retained child woken by a DIFFERENT run
|
|
2072
|
+
* additionally folds the waker's declared `false` on top of the spawn snapshot (tighten-only,
|
|
2073
|
+
* like the other resume clamps).
|
|
2074
|
+
* KNOWN GAPS (registered, whole-clamp-family shapes — `handsReadOnly`/`interactiveTools` share
|
|
2075
|
+
* them): a TIER-3 durable revival rebuilds the child from the REVIVER's context (the durable row
|
|
2076
|
+
* records lookup keys, never a serialized spec), so the spawn-time declaration does not survive
|
|
2077
|
+
* that lane — the reviver's own declaration governs; and the workflow HOST lane does not forward
|
|
2078
|
+
* the host TaskSpec's declaration into `agent()` children (an {@link AgentDefinition} on the
|
|
2079
|
+
* workflow agent type does carry).
|
|
2080
|
+
*/
|
|
2081
|
+
memoryPersistenceCapable?: boolean;
|
|
2019
2082
|
/**
|
|
2020
2083
|
* design/119 (CC --add-dir parity): extra directories the FILE tools may access in addition to the
|
|
2021
2084
|
* containment root — each is canonicalized into the containment allowlist and listed in the
|
|
@@ -2090,6 +2153,11 @@ export interface TaskSpec {
|
|
|
2090
2153
|
* (writes, egress, pipes, unknown commands) tightens to ask. An egress command (e.g. `curl`) suspends under
|
|
2091
2154
|
* the IRREVERSIBLE axis (the shell mark is irreversibility, not egress) — the kind is non-budgetable either
|
|
2092
2155
|
* way, so the budget resolver still never auto-approves it; the axis label is informational.
|
|
2156
|
+
* NOT unconditional: a POSITIVE per-tool mark on the shell tool keeps its seat — an explicit
|
|
2157
|
+
* `irreversibility:"always"` is never downgraded (the tier combine is tighten-only) and an explicit
|
|
2158
|
+
* `"maybe"` keeps its own probe (a probe-less explicit `"maybe"` stays fail-closed ask; the doctrine's
|
|
2159
|
+
* generic probe installs only on doctrine-owned seats). An explicit `"never"` is NOT a mark — the
|
|
2160
|
+
* doctrine governs that seat as if unmarked.
|
|
2093
2161
|
* No effect under `handsReadOnly` (that mounts `bash_readonly`, already allowlisted) or with no execution env.
|
|
2094
2162
|
* **Re-supply on resume:** like `toolPolicy`/`tools`/`durableApproval`, `shellGate` is part of the resume
|
|
2095
2163
|
* task config — a resume that omits it leaves the resumed run's SUBSEQUENT `bash` calls ungated (the approved
|
|
@@ -170,8 +170,12 @@ export declare function defuseControlChars(text: string): string;
|
|
|
170
170
|
* Make untrusted text safe to interpolate INLINE on a trusted prompt line (not inside a {@link delimitUntrusted}
|
|
171
171
|
* fence) — e.g. design/80 D-F echoes a chosen option label into the "The user answered:" block. Folds EVERY
|
|
172
172
|
* line/space separator to a single space so the text cannot forge a new line — `\s` (covers CR/LF/TAB/VT/FF and
|
|
173
|
-
* U+2028/U+2029 + the Unicode spaces) PLUS
|
|
174
|
-
*
|
|
173
|
+
* U+2028/U+2029 + the Unicode spaces) PLUS the whole C0 + DEL + C1 block (U+0000–U+001F, U+007F–U+009F), which
|
|
174
|
+
* `\s` does NOT fully match: the C1 half carries the 8-bit CSI/OSC/ST forms (U+009B/U+009D/U+009C) a
|
|
175
|
+
* C1-honoring terminal treats like their ESC-prefixed spellings, so leaving them through would let a
|
|
176
|
+
* 'sanitized' value repaint the trusted line it is interpolated onto (adversarial round finding) — caps the
|
|
177
|
+
* length, then applies the same tag-neutralization (`</system-reminder>`) + fence-sentinel (`<<<`/`>>>`)
|
|
178
|
+
* defusing the fenced body gets.
|
|
175
179
|
* Defense-in-depth, NOT a guarantee (same posture as the rest of this module).
|
|
176
180
|
*/
|
|
177
181
|
export declare function inlineUntrusted(text: string, maxLen?: number): string;
|
|
@@ -99,7 +99,7 @@ export function defuseControlChars(text) {
|
|
|
99
99
|
}
|
|
100
100
|
const LABEL_MAX = 160;
|
|
101
101
|
export function inlineUntrusted(text, maxLen = LABEL_MAX) {
|
|
102
|
-
const oneLine = text.replace(/[\s\u0000-\u001f\u007f
|
|
102
|
+
const oneLine = text.replace(/[\s\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
|
|
103
103
|
const cps = [...oneLine];
|
|
104
104
|
const capped = cps.length > maxLen ? cps.slice(0, maxLen).join("") + "…" : oneLine;
|
|
105
105
|
const out = defuseFenceMarkers(sanitizeUntrustedText(capped));
|
|
@@ -3,12 +3,13 @@ import { leafIdAfterEntry } from "./storage-base.js";
|
|
|
3
3
|
import { parseSessionTimestampMs } from "./timestamps.js";
|
|
4
4
|
import { flattenableUserText, normalizeEngineSegments } from "../../core/untrusted-text.js";
|
|
5
5
|
import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
|
|
6
|
+
import { MAX_MINTED_TOOL_RESULT_REF_CHARS } from "../../core/tool-result-store.js";
|
|
6
7
|
import { PERSISTED_OUTPUT_REFS_MAX_ENTRIES, SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, } from "../compaction/utils.js";
|
|
7
8
|
const PATH_LIST_MAX_CHARS = 4096;
|
|
8
9
|
const PATH_LIST_MAX_ENTRIES = 1000;
|
|
9
10
|
const INVOKED_SKILLS_MAX_ENTRIES = 1000;
|
|
10
11
|
const INVOKED_SKILL_NAME_MAX_CHARS = 1024;
|
|
11
|
-
const PERSISTED_REF_MAX_CHARS =
|
|
12
|
+
const PERSISTED_REF_MAX_CHARS = MAX_MINTED_TOOL_RESULT_REF_CHARS;
|
|
12
13
|
const ACTIVE_TOOL_NAME_MAX_CHARS = 1024;
|
|
13
14
|
const ACTIVE_TOOLS_MAX_ENTRIES = 1000;
|
|
14
15
|
export class StreamingImportValidator {
|