@sema-agent/core 5.47.0 → 5.49.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +4 -0
  3. package/dist/agents/agent-transcript-tool.js +10 -3
  4. package/dist/agents/send-message-tool.d.ts +43 -1
  5. package/dist/agents/send-message-tool.js +50 -11
  6. package/dist/agents/subagent.d.ts +18 -0
  7. package/dist/agents/subagent.js +102 -2
  8. package/dist/agents/teacher.d.ts +25 -1
  9. package/dist/agents/teacher.js +85 -12
  10. package/dist/config/defaults.d.ts +20 -0
  11. package/dist/config/defaults.js +5 -0
  12. package/dist/core/background-agent-store.d.ts +1 -0
  13. package/dist/core/background-agent-store.js +14 -0
  14. package/dist/core/mcp.d.ts +6 -1
  15. package/dist/core/mcp.js +34 -7
  16. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  17. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  18. package/dist/core/memory-engine/dual-root.js +11 -0
  19. package/dist/core/memory-engine/engine.d.ts +6 -1
  20. package/dist/core/memory-engine/engine.js +136 -21
  21. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  22. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  23. package/dist/core/memory-engine/origin-clearance.js +10 -0
  24. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  25. package/dist/core/memory-engine/provenance-wording.js +1 -0
  26. package/dist/core/memory-engine/tools.js +6 -4
  27. package/dist/core/reminder-disclosure.d.ts +90 -0
  28. package/dist/core/reminder-disclosure.js +64 -0
  29. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  30. package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
  31. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  32. package/dist/core/runner/prepare-hands-readface.js +1 -0
  33. package/dist/core/runner/prepare-task.d.ts +15 -0
  34. package/dist/core/runner/prepare-task.js +51 -33
  35. package/dist/core/runner/runtask.d.ts +26 -1
  36. package/dist/core/runner/runtask.js +21 -3
  37. package/dist/core/session-store.d.ts +59 -1
  38. package/dist/core/session-store.js +82 -14
  39. package/dist/core/session.d.ts +83 -1
  40. package/dist/core/strategy-store.d.ts +180 -3
  41. package/dist/core/strategy-store.js +172 -23
  42. package/dist/core/task-registry-agent.d.ts +28 -0
  43. package/dist/core/task-registry-agent.js +63 -2
  44. package/dist/core/task-registry.d.ts +21 -0
  45. package/dist/core/task-registry.js +4 -1
  46. package/dist/core/types.d.ts +66 -0
  47. package/dist/core/untrusted-text.d.ts +63 -0
  48. package/dist/core/untrusted-text.js +48 -0
  49. package/dist/core/wiring-manifest.d.ts +35 -0
  50. package/dist/core/wiring-manifest.js +21 -1
  51. package/dist/engine/harness/types.d.ts +36 -1
  52. package/dist/index.d.ts +7 -6
  53. package/dist/index.js +6 -5
  54. package/dist/internal/harness-types.d.ts +1 -0
  55. package/dist/stores/file/file-snapshot-store.js +7 -1
  56. package/dist/stores/file/index.d.ts +27 -3
  57. package/dist/stores/file/index.js +36 -1
  58. package/dist/stores/file/session-policy-store.d.ts +0 -13
  59. package/dist/stores/file/session-policy-store.js +7 -1
  60. package/dist/stores/file/session-store.d.ts +22 -5
  61. package/dist/stores/file/session-store.js +80 -13
  62. package/dist/stores/file/strategy-store.d.ts +97 -0
  63. package/dist/stores/file/strategy-store.js +340 -0
  64. package/dist/tools/fs/fs-pdf.d.ts +12 -1
  65. package/dist/tools/fs/fs-pdf.js +17 -3
  66. package/dist/tools/fs/fs-read.d.ts +2 -1
  67. package/dist/tools/fs/fs-read.js +33 -5
  68. package/dist/tools/fs/fs-shared.d.ts +6 -2
  69. package/dist/tools/fs/index.d.ts +7 -0
  70. package/dist/tools/fs/index.js +1 -1
  71. package/dist/tools/web.js +21 -2
  72. package/package.json +3 -2
  73. package/test/export-surface.snapshot.json +22 -1
@@ -0,0 +1,340 @@
1
+ import { accessSync, constants as FS, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { atomicWriteFile } from "./fs-atomic.js";
5
+ import { createSafeNotifier, observeThenableRejection } from "../../core/safe-notify.js";
6
+ import { MAX_STRATEGY_INJECTION_TOTAL_BYTES, MAX_STRATEGY_TEXT_BYTES, compileStrategyQuery, resolveCapacityCap, resolveFindLimit, scoreStoredStrategy, storedStrategyShapeIssue, strategyDedupKey, validateStrategyForWrite, } from "../../core/strategy-store.js";
7
+ function configRefusal(message, code) {
8
+ const e = new Error(message);
9
+ e.code = code;
10
+ return e;
11
+ }
12
+ const SCOPE_SLUG_MAX = 40;
13
+ const READ_FILE_CAP_FACTOR = 4;
14
+ const STALE_TEMP_MS = 60 * 60 * 1000;
15
+ const MAX_ENTRY_FILE_BYTES = 65536;
16
+ const FILE_RE = /^([A-Za-z0-9_-]{1,64})\.json$/;
17
+ export class FileStrategyStore {
18
+ root;
19
+ maxPerScope;
20
+ onIncident;
21
+ warnedOps = new Set();
22
+ sinkNotifier = createSafeNotifier();
23
+ sweptTemps = new Set();
24
+ constructor(opts) {
25
+ this.maxPerScope = resolveCapacityCap("maxPerScope", opts.maxPerScope ?? 100);
26
+ this.onIncident = opts.onIncident;
27
+ const root = opts.root;
28
+ if (typeof root !== "string" || root.length === 0) {
29
+ throw configRefusal("FileStrategyStore: root must be a non-empty path", "config.strategy_root_invalid");
30
+ }
31
+ try {
32
+ mkdirSync(root, { recursive: true, mode: 0o700 });
33
+ }
34
+ catch (err) {
35
+ throw configRefusal(`FileStrategyStore: cannot create root ${root}: ${String(err.message ?? err)}`, "config.strategy_root_invalid");
36
+ }
37
+ let st;
38
+ try {
39
+ st = statSync(root);
40
+ }
41
+ catch (err) {
42
+ throw configRefusal(`FileStrategyStore: cannot stat root ${root}: ${String(err.message ?? err)}`, "config.strategy_root_invalid");
43
+ }
44
+ if (!st.isDirectory()) {
45
+ throw configRefusal(`FileStrategyStore: root ${root} exists and is not a directory`, "config.strategy_root_invalid");
46
+ }
47
+ try {
48
+ accessSync(root, FS.W_OK);
49
+ }
50
+ catch {
51
+ throw configRefusal(`FileStrategyStore: root ${root} is not writable`, "config.strategy_root_invalid");
52
+ }
53
+ this.root = root;
54
+ }
55
+ scopeDirName(scope) {
56
+ const slug = scope
57
+ .toLowerCase()
58
+ .replace(/[^a-z0-9-]+/g, "-")
59
+ .replace(/^-+|-+$/g, "")
60
+ .slice(0, SCOPE_SLUG_MAX);
61
+ const h = createHash("sha256").update(scope, "utf8").digest("hex").slice(0, 24);
62
+ return `${slug || "s"}-${h}`;
63
+ }
64
+ scopeDirPath(scope) {
65
+ return join(this.root, this.scopeDirName(scope));
66
+ }
67
+ assertScopeDirSafe(dir) {
68
+ let st;
69
+ try {
70
+ st = lstatSync(dir);
71
+ }
72
+ catch (err) {
73
+ if (err.code === "ENOENT")
74
+ return;
75
+ throw err;
76
+ }
77
+ if (st.isSymbolicLink() || !st.isDirectory()) {
78
+ const e = new Error(`FileStrategyStore: scope directory ${dir} is not a plain directory (symlink or non-dir refused)`);
79
+ e.code = "strategy.scope_dir_invalid";
80
+ throw e;
81
+ }
82
+ }
83
+ incident(i) {
84
+ const sink = this.onIncident;
85
+ if (sink !== undefined) {
86
+ const site = `FileStrategyStore.onIncident.${i.op}`;
87
+ this.sinkNotifier.notify(() => observeThenableRejection(sink(i), this.sinkNotifier, site), site);
88
+ return;
89
+ }
90
+ if (!this.warnedOps.has(i.op)) {
91
+ this.warnedOps.add(i.op);
92
+ console.warn(`FileStrategyStore ${i.op} incident: ${i.error}${i.path !== undefined ? ` (${i.path})` : ""}`);
93
+ }
94
+ }
95
+ sweepStaleTemps(dir) {
96
+ if (this.sweptTemps.has(dir))
97
+ return;
98
+ this.sweptTemps.add(dir);
99
+ let names;
100
+ try {
101
+ names = readdirSync(dir);
102
+ }
103
+ catch {
104
+ return;
105
+ }
106
+ const cutoff = Date.now() - STALE_TEMP_MS;
107
+ for (const name of names) {
108
+ if (!name.endsWith(".tmp"))
109
+ continue;
110
+ const p = join(dir, name);
111
+ try {
112
+ if (statSync(p).mtimeMs < cutoff)
113
+ unlinkSync(p);
114
+ }
115
+ catch {
116
+ }
117
+ }
118
+ }
119
+ loadScope(scope, dir, readCap) {
120
+ let names;
121
+ try {
122
+ names = readdirSync(dir);
123
+ }
124
+ catch (err) {
125
+ if (err.code === "ENOENT")
126
+ return [];
127
+ throw err;
128
+ }
129
+ let candidates = names.filter((n) => FILE_RE.test(n)).sort();
130
+ if (Number.isFinite(readCap) && candidates.length > readCap) {
131
+ this.incident({
132
+ op: "find",
133
+ error: `scope directory holds ${candidates.length} entry files, over the ${readCap} read cap — parsing the first ${readCap} only (an external writer likely inflated this directory)`,
134
+ path: dir,
135
+ });
136
+ candidates = candidates.slice(0, readCap);
137
+ }
138
+ const out = [];
139
+ for (const name of candidates) {
140
+ const p = join(dir, name);
141
+ let fst;
142
+ try {
143
+ fst = lstatSync(p);
144
+ }
145
+ catch (err) {
146
+ if (err.code === "ENOENT")
147
+ continue;
148
+ this.incident({ op: "find", error: `unstat-able entry skipped: ${String(err.message ?? err)}`, path: p });
149
+ continue;
150
+ }
151
+ if (!fst.isFile()) {
152
+ this.incident({ op: "find", error: "non-regular entry skipped (symlink or special file refused)", path: p });
153
+ continue;
154
+ }
155
+ if (fst.size > MAX_ENTRY_FILE_BYTES) {
156
+ this.quarantine(p, `entry file is ${fst.size} bytes, over the ${MAX_ENTRY_FILE_BYTES} read bound`);
157
+ continue;
158
+ }
159
+ let raw;
160
+ try {
161
+ raw = readFileSync(p, "utf8");
162
+ }
163
+ catch (err) {
164
+ if (err.code === "ENOENT")
165
+ continue;
166
+ this.incident({ op: "find", error: `unreadable entry skipped: ${String(err.message ?? err)}`, path: p });
167
+ continue;
168
+ }
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(raw);
172
+ }
173
+ catch {
174
+ this.quarantine(p, "unparseable JSON");
175
+ continue;
176
+ }
177
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
178
+ this.quarantine(p, "top-level JSON value is not an object");
179
+ continue;
180
+ }
181
+ const rec = parsed;
182
+ if (rec.v !== 1) {
183
+ this.incident({ op: "parse", error: `unknown schema version ${String(rec.v)} — entry skipped, file left in place`, path: p });
184
+ continue;
185
+ }
186
+ const { v: _v, ...candidate } = rec;
187
+ const issue = storedStrategyShapeIssue(candidate);
188
+ if (issue !== null) {
189
+ this.quarantine(p, issue);
190
+ continue;
191
+ }
192
+ const entry = candidate;
193
+ const fileId = FILE_RE.exec(name)[1];
194
+ if (entry.scope !== scope || entry.id !== fileId) {
195
+ this.incident({
196
+ op: "find",
197
+ error: `entry skipped: scope/id mismatch (file claims scope ${JSON.stringify(entry.scope)}, id ${JSON.stringify(entry.id)})`,
198
+ id: fileId,
199
+ path: p,
200
+ });
201
+ continue;
202
+ }
203
+ out.push({ entry, path: p });
204
+ }
205
+ const byKey = new Map();
206
+ for (const le of out) {
207
+ const key = strategyDedupKey(le.entry);
208
+ const prev = byKey.get(key);
209
+ if (prev === undefined || scoreStoredStrategy(le.entry) > scoreStoredStrategy(prev.entry))
210
+ byKey.set(key, le);
211
+ }
212
+ return [...byKey.values()];
213
+ }
214
+ quarantine(p, reason) {
215
+ try {
216
+ renameSync(p, `${p}.bad`);
217
+ this.incident({ op: "parse", error: `corrupt entry quarantined (${reason})`, path: p });
218
+ }
219
+ catch (err) {
220
+ this.incident({ op: "parse", error: `corrupt entry skipped (${reason}); quarantine rename failed: ${String(err.message ?? err)}`, path: p });
221
+ }
222
+ }
223
+ writeEntry(dir, path, entry) {
224
+ const record = {
225
+ v: 1,
226
+ id: entry.id,
227
+ problem: entry.problem,
228
+ strategy: entry.strategy,
229
+ confidence: entry.confidence,
230
+ scope: entry.scope,
231
+ ts: entry.ts,
232
+ ...(entry.teacherModel !== undefined ? { teacherModel: entry.teacherModel } : {}),
233
+ ...(entry.signature !== undefined ? { signature: entry.signature } : {}),
234
+ ...(entry.origin !== undefined ? { origin: entry.origin } : {}),
235
+ };
236
+ const bytes = JSON.stringify(record);
237
+ if (Buffer.byteLength(bytes, "utf8") > MAX_ENTRY_FILE_BYTES) {
238
+ const e = new Error(`FileStrategyStore: entry serializes over the ${MAX_ENTRY_FILE_BYTES}-byte read bound (JSON escaping expanded it past the field caps) — refusing a write the read side would quarantine`);
239
+ e.code = "strategy.entry_invalid";
240
+ throw e;
241
+ }
242
+ atomicWriteFile(dir, path, bytes);
243
+ }
244
+ save(s) {
245
+ validateStrategyForWrite(s);
246
+ const dir = this.scopeDirPath(s.scope);
247
+ this.assertScopeDirSafe(dir);
248
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
249
+ this.sweepStaleTemps(dir);
250
+ const existing = this.loadScope(s.scope, dir, this.retrievalReadCap());
251
+ const key = strategyDedupKey(s);
252
+ const dup = existing.find((le) => strategyDedupKey(le.entry) === key);
253
+ let all;
254
+ if (dup !== undefined) {
255
+ const refreshed = {
256
+ ...dup.entry,
257
+ ...(s.confidence >= dup.entry.confidence ? { confidence: s.confidence, ts: s.ts } : {}),
258
+ ...(s.teacherModel !== undefined ? { teacherModel: s.teacherModel } : {}),
259
+ };
260
+ this.writeEntry(dir, dup.path, refreshed);
261
+ dup.entry = refreshed;
262
+ all = existing;
263
+ }
264
+ else {
265
+ const p = join(dir, `${s.id}.json`);
266
+ const byId = existing.find((le) => le.entry.id === s.id);
267
+ this.writeEntry(dir, p, s);
268
+ if (byId !== undefined) {
269
+ byId.entry = s;
270
+ all = existing;
271
+ }
272
+ else {
273
+ all = [...existing, { entry: s, path: p }];
274
+ }
275
+ }
276
+ if (all.length > this.maxPerScope) {
277
+ this.evict(all, this.maxPerScope);
278
+ }
279
+ }
280
+ evict(all, keep) {
281
+ const sorted = [...all].sort((a, b) => scoreStoredStrategy(b.entry) - scoreStoredStrategy(a.entry));
282
+ for (const le of sorted.slice(keep)) {
283
+ try {
284
+ unlinkSync(le.path);
285
+ }
286
+ catch (err) {
287
+ if (err.code === "ENOENT")
288
+ continue;
289
+ this.incident({ op: "evict", error: String(err.message ?? err), id: le.entry.id, path: le.path });
290
+ }
291
+ }
292
+ }
293
+ find(scope, query, limit) {
294
+ const cap = resolveFindLimit(limit);
295
+ const dir = this.scopeDirPath(scope);
296
+ this.assertScopeDirSafe(dir);
297
+ const matches = compileStrategyQuery(query);
298
+ if (matches === null)
299
+ return [];
300
+ const ranked = this.loadScope(scope, dir, this.retrievalReadCap())
301
+ .map((le) => le.entry)
302
+ .filter((e) => matches(e.problem))
303
+ .sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a))
304
+ .slice(0, cap);
305
+ let bytes = 0;
306
+ for (let i = 0; i < ranked.length; i++) {
307
+ bytes += Buffer.byteLength(ranked[i].strategy, "utf8");
308
+ if (bytes > MAX_STRATEGY_INJECTION_TOTAL_BYTES) {
309
+ this.incident({
310
+ op: "find",
311
+ error: `result truncated at ${i} of ${ranked.length} entries — combined strategy text exceeded ${MAX_STRATEGY_INJECTION_TOTAL_BYTES} bytes (per-entry cap ${MAX_STRATEGY_TEXT_BYTES})`,
312
+ });
313
+ return ranked.slice(0, i);
314
+ }
315
+ }
316
+ return ranked;
317
+ }
318
+ retrievalReadCap() {
319
+ return this.maxPerScope * READ_FILE_CAP_FACTOR;
320
+ }
321
+ prune(scope, maxSize) {
322
+ const n = resolveCapacityCap("maxSize", maxSize);
323
+ const dir = this.scopeDirPath(scope);
324
+ this.assertScopeDirSafe(dir);
325
+ const all = this.loadScope(scope, dir, Number.POSITIVE_INFINITY);
326
+ if (all.length > n)
327
+ this.evict(all, n);
328
+ }
329
+ scopeUsage(scope) {
330
+ const dir = this.scopeDirPath(scope);
331
+ this.assertScopeDirSafe(dir);
332
+ return { used: this.loadScope(scope, dir, Number.POSITIVE_INFINITY).length, capacity: this.maxPerScope };
333
+ }
334
+ hasStrategy(scope, entry) {
335
+ const dir = this.scopeDirPath(scope);
336
+ this.assertScopeDirSafe(dir);
337
+ const key = strategyDedupKey(entry);
338
+ return this.loadScope(scope, dir, this.retrievalReadCap()).some((le) => strategyDedupKey(le.entry) === key);
339
+ }
340
+ }
@@ -3,6 +3,17 @@ import type { DocumentContent, ImageContent, TextContent } from "../../internal/
3
3
  import type { ExecutionEnv } from "../../internal/harness-types.js";
4
4
  import { type PdfModelCapabilities } from "./pdf.js";
5
5
  import { type ReadImageDownsamplerOption } from "./fs-shared.js";
6
+ import { type ReminderDisclosureCounts } from "../../core/reminder-disclosure.js";
7
+ /** design/319 (B ticket) — the Read tool's disclosure state, threaded into the PDF TEXT legs (the
8
+ * two `pdftotext` extraction arms — the only PDF returns with a model-facing text projection of
9
+ * the document; the native document block and rendered page images have no text to scan). Shares
10
+ * the Read closure's throttle windows so a PDF and its text read dedup on the same file key. */
11
+ export interface PdfReminderDisclosure {
12
+ mark: string | undefined;
13
+ windows: Map<string, number>;
14
+ key: string;
15
+ counts?: ReminderDisclosureCounts;
16
+ }
6
17
  export { pdfModelCapabilitiesOf, type PdfModelCapabilities } from "./pdf.js";
7
18
  /** What the Read tool's PDF pipeline returns (document block, or rendered page images, or an error string). */
8
19
  type ReadPdfReturn = string | {
@@ -33,7 +44,7 @@ type ReadPdfReturn = string | {
33
44
  * Every degraded return carries `details.fallback = { level, reason }` (telemetry on the structured frame).
34
45
  * `caps` absent ⇒ fully capable (byte-compat: native document block; the brain placeholder still guards).
35
46
  */
36
- export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities, readDeny?: import("./read-deny.js").ReadDenyMatcher): Promise<ReadPdfReturn>;
47
+ export declare function readPdfFile(env: ExecutionEnv, path: string, key: string, pages: string | undefined, signal: AbortSignal | undefined, downsamplerOpt: ReadImageDownsamplerOption, cwd: string, preRead?: Uint8Array, caps?: PdfModelCapabilities, readDeny?: import("./read-deny.js").ReadDenyMatcher, disclosure?: PdfReminderDisclosure): Promise<ReadPdfReturn>;
37
48
  /** E1: readPdfFile's own return type stays `ReadPdfReturn` (its INTERNAL string-means-error dispatch
38
49
  * contract, shared with pdfPagesToImageBlocks) — the isError flag is applied once, here, at the tool's
39
50
  * actual execute() boundary, not inside the helper. */
@@ -3,6 +3,20 @@ import { delimitUntrusted } from "../../core/untrusted-text.js";
3
3
  import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
4
4
  import { PDF_FALLBACK_RENDER_PAGES, PDF_INLINE_PAGE_THRESHOLD, PDF_MAX_EXTRACT_SIZE, PDF_MAX_PAGES_PER_READ, PDF_TARGET_RAW_SIZE, extractPdfPagesAsImages, extractPdfTextLayer, getPdfPageCount, parsePdfPageRange, pdfMagicMatches, } from "./pdf.js";
5
5
  import { resolveAutoDownsampler, enoentMessage, MAX_READ_BYTES } from "./fs-shared.js";
6
+ import { discloseReminderShaped } from "../../core/reminder-disclosure.js";
7
+ function pdfDisclosureTrailer(body, d) {
8
+ if (d === undefined)
9
+ return "";
10
+ const out = discloseReminderShaped({
11
+ segments: [body],
12
+ mark: d.mark,
13
+ outlet: "pdf",
14
+ defuseExactMark: false,
15
+ throttle: { key: d.key, windows: d.windows },
16
+ counts: d.counts,
17
+ });
18
+ return out.trailer !== undefined ? `\n${out.trailer}` : "";
19
+ }
6
20
  export { pdfModelCapabilitiesOf } from "./pdf.js";
7
21
  async function pdfPagesToImageBlocks(pages, path, downsamplerOpt) {
8
22
  const downsampler = downsamplerOpt === false ? undefined : (downsamplerOpt ?? (await resolveAutoDownsampler()));
@@ -30,7 +44,7 @@ function boundPdfExtractedText(text) {
30
44
  return { body: text, truncated: false };
31
45
  return { body: text.slice(0, MAX_READ_BYTES), truncated: true };
32
46
  }
33
- export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, preRead, caps, readDeny) {
47
+ export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, preRead, caps, readDeny, disclosure) {
34
48
  const cap = caps ?? { document: true, vision: true };
35
49
  const meta = await env.fileInfo(key, signal);
36
50
  if (!meta.ok) {
@@ -83,7 +97,7 @@ export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt,
83
97
  {
84
98
  type: "text",
85
99
  text: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} extracted as text via pdftotext (the serving model does not support native PDF input)${timeoutNote}]` +
86
- `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; narrow the pages range]` : ""}`,
100
+ `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; narrow the pages range]` : ""}${pdfDisclosureTrailer(body, disclosure)}`,
87
101
  },
88
102
  ],
89
103
  details: {
@@ -166,7 +180,7 @@ export async function readPdfFile(env, path, key, pages, signal, downsamplerOpt,
166
180
  {
167
181
  type: "text",
168
182
  text: `[PDF: ${path} — extracted text layer via pdftotext${pageCount !== undefined ? `; ${pageCount} page${pageCount === 1 ? "" : "s"}` : ""} (the serving model does not support native PDF input)${timeoutNote}]` +
169
- `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; use the pages parameter to read specific ranges]` : ""}`,
183
+ `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; use the pages parameter to read specific ranges]` : ""}${pdfDisclosureTrailer(body, disclosure)}`,
170
184
  },
171
185
  ],
172
186
  details: {
@@ -2,7 +2,8 @@ import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import { type ReadFileState } from "./safety.js";
3
3
  import { type PdfModelCapabilities } from "./pdf.js";
4
4
  import { type ReadImageDownsamplerOption, type CwdRef } from "./fs-shared.js";
5
+ import { type ReminderDisclosureCounts } from "../../core/reminder-disclosure.js";
5
6
  export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
6
7
  taskId?: string;
7
8
  principal?: string;
8
- }) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./read-face.js").ReadFace, reminderMark?: string): AgentTool;
9
+ }) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./read-face.js").ReadFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
@@ -8,8 +8,16 @@ import { PDF_MAX_PAGES_PER_READ, pdfMagicMatches } from "./pdf.js";
8
8
  import { MAX_READ_BYTES, SLICED_READ_MAX_BYTES, MAX_IMAGE_READ_BYTES, MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES, NO_DOWNSAMPLER_IMAGE_CAP_HINT, resolveAutoDownsampler, MAX_READ_OUTPUT_CHARS, readCyberReminderText, FILE_PATH_PARAMS, countLines, seededFileUnchangedReminder, enoentMessage, } from "./fs-shared.js";
9
9
  import { readPdfFile, pdfResultToToolReturn } from "./fs-pdf.js";
10
10
  import { openSystemReminder } from "../../core/reminder-mint.js";
11
- export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readCyberReminder, readDeny, readFace, reminderMark) {
11
+ import { discloseReminderShaped } from "../../core/reminder-disclosure.js";
12
+ export function createReadFileTool(env, state, rootCanonical, cwdRef, additionalRoots, imageDownsampler, pdfCapabilities, bgOutputReadExemption, readCyberReminder, readDeny, readFace, reminderMark, reminderDisclosureCounts) {
12
13
  const cyberReminder = readCyberReminder === false ? "" : readCyberReminderText(reminderMark);
14
+ const reminderDisclosureWindows = new Map();
15
+ const pdfReminderDisclosure = (key) => ({
16
+ mark: reminderMark,
17
+ windows: reminderDisclosureWindows,
18
+ key,
19
+ counts: reminderDisclosureCounts,
20
+ });
13
21
  const pathBoundLine = readFace === "open"
14
22
  ? "- `file_path` may be relative (resolved against the tracked working directory) or absolute. Reads are not confined to the workspace roots; a small sensitive-path deny list applies.\n"
15
23
  : "- `file_path` may be relative (resolved against the tracked working directory) or absolute (within the configured roots).\n";
@@ -125,7 +133,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
125
133
  };
126
134
  }
127
135
  if (r.key.toLowerCase().endsWith(".pdf")) {
128
- return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, undefined, pdfCapabilities, readDeny));
136
+ return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, undefined, pdfCapabilities, readDeny, pdfReminderDisclosure(r.key)));
129
137
  }
130
138
  const binaryExt = hasBinaryExtension(r.key);
131
139
  const binaryExtRefusal = () => errorResult(`Error (Read): "${path}" appears to be a binary file (by extension); this tool reads UTF-8 text only.`);
@@ -170,7 +178,7 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
170
178
  return errorResult(`Error (Read): "${path}" is too large to read in full (${readSize} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`);
171
179
  }
172
180
  if (pdfMagicMatches(readBin.value)) {
173
- return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities, readDeny));
181
+ return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities, readDeny, pdfReminderDisclosure(r.key)));
174
182
  }
175
183
  const magicFormat = binaryMagicFormat(readBin.value);
176
184
  if (magicFormat !== undefined) {
@@ -225,8 +233,20 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
225
233
  }
226
234
  state.set(r.key, { hash, totalLines: countLines(content), truncated: false, view: { start: 1, end: total }, lastReadAt: Date.now() });
227
235
  const bodyBlocks = rendered.blocks.length > 0 ? rendered.blocks : [{ type: "text", text: "[notebook has 0 cells]" }];
236
+ const nbDisclosure = discloseReminderShaped({
237
+ segments: rendered.blocks.filter((b) => b.type === "text").map((b) => b.text),
238
+ mark: reminderMark,
239
+ outlet: "notebook",
240
+ defuseExactMark: false,
241
+ throttle: { key: r.key, windows: reminderDisclosureWindows },
242
+ counts: reminderDisclosureCounts,
243
+ });
228
244
  return {
229
- content: cyberReminder ? [...bodyBlocks, { type: "text", text: cyberReminder }] : bodyBlocks,
245
+ content: [
246
+ ...bodyBlocks,
247
+ ...(cyberReminder ? [{ type: "text", text: cyberReminder }] : []),
248
+ ...(nbDisclosure.trailer !== undefined ? [{ type: "text", text: nbDisclosure.trailer }] : []),
249
+ ],
230
250
  details: { type: "notebook", file: { filePath: path, cells: parsed.cells.map(stripNotebookImageData) } },
231
251
  };
232
252
  }
@@ -291,8 +311,16 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
291
311
  if (total === 0)
292
312
  return `${openSystemReminder(reminderMark)}Warning: the file exists but the contents are empty.</system-reminder>`;
293
313
  const header = pageMarker ?? (truncated ? `[${path}: lines ${start}-${end} of ${total}${end < total ? " — use offset to see more" : ""}]\n` : "");
314
+ const disclosure = discloseReminderShaped({
315
+ segments: [body],
316
+ mark: reminderMark,
317
+ outlet: "read",
318
+ defuseExactMark: false,
319
+ throttle: { key: r.key, windows: reminderDisclosureWindows },
320
+ counts: reminderDisclosureCounts,
321
+ });
294
322
  return {
295
- content: `${nbFallbackPrefix}${header}${body}${cyberReminder}`,
323
+ content: `${nbFallbackPrefix}${header}${body}${cyberReminder}${disclosure.trailer !== undefined ? `\n${disclosure.trailer}` : ""}`,
296
324
  details: {
297
325
  type: "text",
298
326
  file: {
@@ -140,8 +140,12 @@ export declare const MAX_READ_OUTPUT_CHARS = 100000;
140
140
  * COVERAGE, stated because the switch is easy to over-read: the reminder rides the plain-text read and
141
141
  * the notebook projection. PDF text extraction returns through its own result builder and has never
142
142
  * carried it, so extracted PDF text reaches the model without this mitigation whatever the switch says.
143
- * That gap predates the switch and is left as an open item rather than closed silently — widening the
144
- * reminder to a third surface is a change to what every PDF read costs, not a wiring fix.
143
+ * That gap predates the switch; its IMPERSONATION half is now closed design/319 (B ticket) gave the
144
+ * PDF text legs the same detect-and-disclose trailer as this lane (reminder-shaped bytes in extracted
145
+ * PDF text are disclosed at the outlet, fs-pdf.ts), and their bodies were already fenced. The CYBER
146
+ * half (this malware-alertness sentence) still does not ride PDF text and stays an open item rather
147
+ * than closed silently — widening it to a third surface is a change to what every PDF read costs,
148
+ * not a wiring fix.
145
149
  *
146
150
  * design/319 (A ticket): a FUNCTION over the session mark (formerly the `READ_CYBER_REMINDER`
147
151
  * constant) — the open tag is rendered by the mint home so it carries the run's provenance mark;
@@ -161,6 +161,13 @@ export interface HandsToolkitOptions {
161
161
  * the honest form). The tag is rendered by the mint home (core/reminder-mint.ts) — the content
162
162
  * BODY next to it is byte-untouched. */
163
163
  reminderMark?: string;
164
+ /** design/319 (B ticket) — the per-run trailer/defuse trigger counters (observation seat, G9②):
165
+ * the Read text/notebook/PDF-text disclosure trailers bump `read.*` / `notebook.*` / `pdf.*`
166
+ * keys here (appended / marked / bare_throttled). Threaded by prepare-task, which folds the
167
+ * non-zero result into `TaskResult.stats.mechanisms.reminderDisclosures`; a library-direct
168
+ * mount may pass its own object or omit it (counting off — and the whole disclosure pipeline
169
+ * is off anyway when `reminderMark` is absent). */
170
+ reminderDisclosureCounts?: import("../../core/reminder-disclosure.js").ReminderDisclosureCounts;
164
171
  }
165
172
  /**
166
173
  * Build the per-task hand tool band over an injected env + fresh per-task read state (design/44 §11 A).
@@ -46,7 +46,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
46
46
  onDeploymentClamp: () => deliverEngineNotice(opts.onNotice, deploymentReadFaceClampNotice()),
47
47
  });
48
48
  const tools = [
49
- createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark),
49
+ createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts),
50
50
  ];
51
51
  if (!readOnly) {
52
52
  tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite));
package/dist/tools/web.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
4
+ import { discloseReminderShaped } from "../core/reminder-disclosure.js";
4
5
  import { redactSecrets } from "../core/untrusted-egress.js";
5
6
  import { isPrivateHost } from "../core/runner/image.js";
6
7
  import { binaryMagicFormat } from "./fs/safety.js";
@@ -638,7 +639,17 @@ export function webFetchToolSpec(config = {}) {
638
639
  const partialNote = bodyCut
639
640
  ? `\n\n[WebFetch: ${cutPhrase} — the content above is PARTIAL: ${bodyBytes?.length ?? 0} bytes were received before the cutoff and the tail is missing. Treat absent information as unfetched, not absent from the source.]`
640
641
  : "";
641
- const modelText = withNote + truncationNote + partialNote + sourceEcho;
642
+ let modelText = withNote + truncationNote + partialNote + sourceEcho;
643
+ {
644
+ const d = discloseReminderShaped({
645
+ segments: [modelText],
646
+ mark: ctx.reminderMark,
647
+ outlet: "webFetch",
648
+ defuseExactMark: true,
649
+ ...(ctx.reminderDisclosureCounts !== undefined ? { counts: ctx.reminderDisclosureCounts } : {}),
650
+ });
651
+ modelText = d.trailer !== undefined ? `${d.segments[0]}\n\n${d.trailer}` : d.segments[0];
652
+ }
642
653
  const RESULT_PREVIEW_CHARS = 8_000;
643
654
  return {
644
655
  content: modelText,
@@ -947,7 +958,15 @@ export function createWebSearchTool(config) {
947
958
  const tailReminder = shown.length > 0
948
959
  ? "REMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks."
949
960
  : "REMINDER: this search returned ZERO usable sources — there is nothing above to cite. Do not fabricate sources, URLs or citations; tell the user the search returned no usable results (the notes above say why, when there is a why) and answer from what you already know, or try a different query.";
950
- const modelText = `${fenced}${dropNote}${domainNote}${maxResultsNote}${schemeNote}${clipNote}\n\n${tailReminder}`;
961
+ const searchDisclosure = discloseReminderShaped({
962
+ segments: [fenced],
963
+ mark: ctx.reminderMark,
964
+ outlet: "webSearch",
965
+ defuseExactMark: true,
966
+ ...(ctx.reminderDisclosureCounts !== undefined ? { counts: ctx.reminderDisclosureCounts } : {}),
967
+ });
968
+ const searchDisclosureNote = searchDisclosure.trailer !== undefined ? `\n\n${searchDisclosure.trailer}` : "";
969
+ const modelText = `${searchDisclosure.segments[0]}${dropNote}${domainNote}${maxResultsNote}${schemeNote}${clipNote}${searchDisclosureNote}\n\n${tailReminder}`;
951
970
  return {
952
971
  content: modelText,
953
972
  details: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.47.0",
3
+ "version": "5.49.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -66,7 +66,8 @@
66
66
  "gate:nuia": "node scripts/verify-nuia-baseline.mjs",
67
67
  "gate:field-liveness": "node scripts/verify-field-liveness.mjs",
68
68
  "gate:error-surface": "node scripts/verify-error-surface.mjs",
69
- "gate:criteria": "node scripts/criteria-lint.mjs"
69
+ "gate:criteria": "node scripts/criteria-lint.mjs",
70
+ "gate:reminder-literal": "node scripts/verify-reminder-literal.mjs"
70
71
  },
71
72
  "dependencies": {
72
73
  "@modelcontextprotocol/sdk": "1.30.0",