@sema-agent/core 5.47.0 → 5.48.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 (52) hide show
  1. package/CHANGELOG.md +52 -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/config/defaults.d.ts +20 -0
  9. package/dist/config/defaults.js +5 -0
  10. package/dist/core/background-agent-store.d.ts +1 -0
  11. package/dist/core/background-agent-store.js +13 -0
  12. package/dist/core/mcp.d.ts +6 -1
  13. package/dist/core/mcp.js +34 -7
  14. package/dist/core/reminder-disclosure.d.ts +90 -0
  15. package/dist/core/reminder-disclosure.js +64 -0
  16. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  17. package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
  18. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  19. package/dist/core/runner/prepare-hands-readface.js +1 -0
  20. package/dist/core/runner/prepare-task.d.ts +15 -0
  21. package/dist/core/runner/prepare-task.js +48 -30
  22. package/dist/core/runner/runtask.js +3 -1
  23. package/dist/core/session-store.d.ts +59 -1
  24. package/dist/core/session-store.js +82 -14
  25. package/dist/core/session.d.ts +83 -1
  26. package/dist/core/task-registry-agent.d.ts +28 -0
  27. package/dist/core/task-registry-agent.js +63 -2
  28. package/dist/core/task-registry.d.ts +21 -0
  29. package/dist/core/task-registry.js +4 -1
  30. package/dist/core/types.d.ts +60 -0
  31. package/dist/core/untrusted-text.d.ts +63 -0
  32. package/dist/core/untrusted-text.js +48 -0
  33. package/dist/core/wiring-manifest.d.ts +35 -0
  34. package/dist/core/wiring-manifest.js +21 -1
  35. package/dist/engine/harness/types.d.ts +36 -1
  36. package/dist/index.d.ts +5 -4
  37. package/dist/index.js +4 -3
  38. package/dist/internal/harness-types.d.ts +1 -0
  39. package/dist/stores/file/index.d.ts +19 -3
  40. package/dist/stores/file/index.js +24 -1
  41. package/dist/stores/file/session-store.d.ts +18 -4
  42. package/dist/stores/file/session-store.js +73 -12
  43. package/dist/tools/fs/fs-pdf.d.ts +12 -1
  44. package/dist/tools/fs/fs-pdf.js +17 -3
  45. package/dist/tools/fs/fs-read.d.ts +2 -1
  46. package/dist/tools/fs/fs-read.js +33 -5
  47. package/dist/tools/fs/fs-shared.d.ts +6 -2
  48. package/dist/tools/fs/index.d.ts +7 -0
  49. package/dist/tools/fs/index.js +1 -1
  50. package/dist/tools/web.js +21 -2
  51. package/package.json +3 -2
  52. package/test/export-surface.snapshot.json +15 -1
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, rmSync } from "node:fs";
1
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { BaseSessionStorage, StoredSession, SessionError, getEntriesToFork, uuidv7, validateEntriesForImport, } from "../../internal/harness.js";
4
4
  import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent } from "./fs-atomic.js";
@@ -83,18 +83,20 @@ export class FileSessionRepo {
83
83
  const lines = readJsonlRecords(path, (info) => this.disclose(info.path, info.reason));
84
84
  let createdAt = "";
85
85
  let forkedFrom;
86
+ let placement;
86
87
  const entries = [];
87
88
  for (const line of lines) {
88
89
  if (line.kind === "meta") {
89
90
  createdAt = line.createdAt;
90
91
  forkedFrom = line.forkedFrom;
92
+ placement = line.placement;
91
93
  }
92
94
  else
93
95
  entries.push(line.entry);
94
96
  }
95
- return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), entries };
97
+ return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}), entries };
96
98
  }
97
- storage(id, createdAt, entries, forkedFrom) {
99
+ storage(id, createdAt, entries, forkedFrom, placement) {
98
100
  const canonical = canonicalStoreKey(this.pathFor(id));
99
101
  const live = sharedSessionStorages.get(canonical)?.deref();
100
102
  if (live !== undefined) {
@@ -103,7 +105,7 @@ export class FileSessionRepo {
103
105
  return live;
104
106
  }
105
107
  const log = new AppendLog(this.pathFor(id));
106
- const created = new FileSessionStorage(log, { id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}) }, entries, canonical);
108
+ const created = new FileSessionStorage(log, { id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}) }, entries, canonical);
107
109
  sharedSessionStorages.set(canonical, new WeakRef(created));
108
110
  created.addHolder(this);
109
111
  this.joined.set(canonical, new WeakRef(created));
@@ -128,15 +130,16 @@ export class FileSessionRepo {
128
130
  const path = this.pathFor(id);
129
131
  if (!existsSync(path)) {
130
132
  evictSharedSessionStorage(canonicalStoreKey(path));
131
- atomicWriteFile(this.tmpDir, path, `${JSON.stringify({ kind: "meta", id, createdAt })}\n`);
132
- return new StoredSession(this.storage(id, createdAt, []));
133
+ const placement = options.placement !== undefined ? { ...options.placement, placedAt: options.placement.placedAt ?? Date.now() } : undefined;
134
+ atomicWriteFile(this.tmpDir, path, `${JSON.stringify({ kind: "meta", id, createdAt, ...(placement !== undefined ? { placement } : {}) })}\n`);
135
+ return new StoredSession(this.storage(id, createdAt, [], undefined, placement));
133
136
  }
134
- const { createdAt: existingCreatedAt, forkedFrom, entries } = this.read(id);
135
- return new StoredSession(this.storage(id, existingCreatedAt || createdAt, entries, forkedFrom));
137
+ const { createdAt: existingCreatedAt, forkedFrom, placement, entries } = this.read(id);
138
+ return new StoredSession(this.storage(id, existingCreatedAt || createdAt, entries, forkedFrom, placement));
136
139
  }
137
140
  async open(metadata) {
138
- const { createdAt, forkedFrom, entries } = this.read(metadata.id);
139
- return new StoredSession(this.storage(metadata.id, createdAt, entries, forkedFrom));
141
+ const { createdAt, forkedFrom, placement, entries } = this.read(metadata.id);
142
+ return new StoredSession(this.storage(metadata.id, createdAt, entries, forkedFrom, placement));
140
143
  }
141
144
  async list() {
142
145
  let names;
@@ -155,7 +158,9 @@ export class FileSessionRepo {
155
158
  continue;
156
159
  const id = name.slice(0, -SUFFIX.length);
157
160
  try {
158
- const { createdAt, forkedFrom } = this.read(id);
161
+ const { createdAt, forkedFrom, placement } = this.read(id);
162
+ if (placement !== undefined)
163
+ continue;
159
164
  out.push({ id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}) });
160
165
  }
161
166
  catch (err) {
@@ -166,12 +171,68 @@ export class FileSessionRepo {
166
171
  }
167
172
  return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
168
173
  }
174
+ async listPlaced(kind, opts) {
175
+ let names;
176
+ try {
177
+ names = readdirSync(this.dir);
178
+ }
179
+ catch (err) {
180
+ if (err.code !== "ENOENT") {
181
+ this.disclose(this.dir, `session directory unreadable (${err.code ?? "unknown"}) — placed listing degraded to empty`);
182
+ }
183
+ return [];
184
+ }
185
+ const now = Date.now();
186
+ const out = [];
187
+ for (const name of names) {
188
+ if (!name.endsWith(SUFFIX))
189
+ continue;
190
+ const id = name.slice(0, -SUFFIX.length);
191
+ let placement;
192
+ try {
193
+ placement = this.read(id).placement;
194
+ }
195
+ catch (err) {
196
+ if (!(err instanceof SessionError && err.code === "not_found")) {
197
+ this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the placed listing`);
198
+ }
199
+ continue;
200
+ }
201
+ if (placement === undefined || placement.kind !== kind)
202
+ continue;
203
+ if (opts?.scope !== undefined && placement.scope !== opts.scope)
204
+ continue;
205
+ if (opts?.olderThanMs !== undefined) {
206
+ let ageAnchor = placement.placedAt;
207
+ try {
208
+ ageAnchor = statSync(this.pathFor(id)).mtimeMs;
209
+ }
210
+ catch {
211
+ }
212
+ if (now - ageAnchor <= opts.olderThanMs)
213
+ continue;
214
+ }
215
+ out.push(placement.scope !== undefined && placement.handle !== undefined
216
+ ? { sessionId: id, placedAt: placement.placedAt, scope: placement.scope, handle: placement.handle }
217
+ : { sessionId: id, placedAt: placement.placedAt, tupleIncomplete: true });
218
+ }
219
+ return out;
220
+ }
221
+ async placementOf(sessionId) {
222
+ try {
223
+ return this.read(sessionId).placement;
224
+ }
225
+ catch {
226
+ return undefined;
227
+ }
228
+ }
169
229
  async delete(metadata) {
170
230
  evictSharedSessionStorage(canonicalStoreKey(this.pathFor(metadata.id)));
171
231
  try {
172
232
  rmSync(this.pathFor(metadata.id), { force: true });
173
233
  }
174
- catch {
234
+ catch (err) {
235
+ throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : undefined);
175
236
  }
176
237
  }
177
238
  async fork(sourceMetadata, options = {}) {
@@ -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.48.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
- "count": 1625,
3
+ "count": 1639,
4
4
  "exports": {
5
5
  "A2ATaskState": "type",
6
6
  "A2ATaskStateReversal": "type",
@@ -47,6 +47,7 @@
47
47
  "AdoptionSource": "type",
48
48
  "AdoptionStatus": "type",
49
49
  "AffectedDeploymentConfig": "interface",
50
+ "AgentContinuationReceipt": "interface",
50
51
  "AgentDefinition": "interface",
51
52
  "AgentDisplayStatus": "type",
52
53
  "AgentPollDetailsInput": "interface",
@@ -229,6 +230,8 @@
229
230
  "DEFAULT_TIER_ORDER": "variable",
230
231
  "DEFAULT_TOOL_RESULT_THRESHOLD_CHARS": "variable",
231
232
  "DEGRADED_DIAGNOSTIC_TYPE": "variable",
233
+ "DELEGATION_MAX_CONCURRENT_DEFAULT": "variable",
234
+ "DELEGATION_MAX_PER_SESSION_DEFAULT": "variable",
232
235
  "DESIGN_REVIEW_PROMPTS": "variable",
233
236
  "DISCUSSION_SCRIPT": "variable",
234
237
  "DISCUSSION_WORKFLOW_NAME": "variable",
@@ -547,6 +550,8 @@
547
550
  "ORG_ADMISSION_CHECKPOINT_VERSION": "variable",
548
551
  "ORG_RULE_DECISION_REASON": "variable",
549
552
  "ORG_UNAVAILABLE_DECISION_REASON": "variable",
553
+ "ORPHAN_ADOPT_MAX_DEFAULT": "variable",
554
+ "ORPHAN_ADOPT_WINDOW_MS_DEFAULT": "variable",
550
555
  "OUTPUT_EFFICIENCY": "variable",
551
556
  "OnAsk": "type",
552
557
  "OnElicit": "type",
@@ -636,6 +641,7 @@
636
641
  "PersistedRuleMatch": "type",
637
642
  "PersistedRuleTool": "type",
638
643
  "PersistedRuleUnreadable": "interface",
644
+ "PlacedSessionRow": "type",
639
645
  "PlatformLimitReason": "type",
640
646
  "PostCompactContext": "interface",
641
647
  "PostToolBatchCall": "interface",
@@ -748,6 +754,7 @@
748
754
  "ReportedFinding": "interface",
749
755
  "ResolveExpectation": "interface",
750
756
  "ResolvedAsk": "type",
757
+ "ResolvedDelegationEntryCaps": "interface",
751
758
  "ResolvedOutcome": "interface",
752
759
  "ResolvedReasoning": "interface",
753
760
  "ResolvedRole": "interface",
@@ -831,6 +838,7 @@
831
838
  "STUB_ARCHIVED_LINE": "variable",
832
839
  "SUBAGENT_PROMPT": "variable",
833
840
  "SUBAGENT_SYSTEM_NOTE": "variable",
841
+ "SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT": "variable",
834
842
  "SUMMARIZE_TOOL_RESULTS": "variable",
835
843
  "SUPERVISOR_PROMPT": "variable",
836
844
  "SafetyAxis": "interface",
@@ -869,6 +877,8 @@
869
877
  "SessionError": "class",
870
878
  "SessionMetadata": "interface",
871
879
  "SessionPermissionRules": "interface",
880
+ "SessionPlacement": "interface",
881
+ "SessionPlacementRecord": "interface",
872
882
  "SessionPolicyError": "class",
873
883
  "SessionPolicyStore": "interface",
874
884
  "SessionRepo": "interface",
@@ -930,6 +940,7 @@
930
940
  "SubagentSteerHandle": "interface",
931
941
  "SubagentStep": "interface",
932
942
  "SubagentToolOptions": "interface",
943
+ "SubagentTranscriptTier": "type",
933
944
  "SummarizableFinding": "interface",
934
945
  "SyncMemoryScopeOptions": "interface",
935
946
  "SyntheticContinuationReason": "type",
@@ -1206,6 +1217,7 @@
1206
1217
  "constraintChainEntryOf": "function",
1207
1218
  "cosineDistance": "function",
1208
1219
  "countElicitOptIns": "function",
1220
+ "createAgentContinuationVerb": "function",
1209
1221
  "createAgentTranscriptTool": "function",
1210
1222
  "createAllowDenyPolicy": "function",
1211
1223
  "createAnthropicBrain": "function",
@@ -1499,6 +1511,7 @@
1499
1511
  "resolveComplianceDenies": "function",
1500
1512
  "resolveDataRoot": "function",
1501
1513
  "resolveDeclaredDurability": "function",
1514
+ "resolveDelegationEntryCaps": "function",
1502
1515
  "resolveEffectiveConfig": "function",
1503
1516
  "resolveEffort": "function",
1504
1517
  "resolveFrozenPaths": "function",
@@ -1513,6 +1526,7 @@
1513
1526
  "resolveReadFace": "function",
1514
1527
  "resolveReasoning": "function",
1515
1528
  "resolveReasoningProfile": "function",
1529
+ "resolveSubagentTranscriptTier": "function",
1516
1530
  "resolveTaskLimits": "function",
1517
1531
  "resolveTaskModel": "function",
1518
1532
  "resolveUsageWindows": "function",