@sema-agent/core 5.13.0 → 5.15.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 (108) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +4 -0
  4. package/dist/agents/subagent.js +133 -41
  5. package/dist/brain/anthropic.js +33 -10
  6. package/dist/brain/context-overflow.d.ts +20 -0
  7. package/dist/brain/context-overflow.js +58 -0
  8. package/dist/brain/errors.js +21 -1
  9. package/dist/brain/open-responses.js +24 -10
  10. package/dist/brain/openai.js +29 -11
  11. package/dist/brain/request-params.d.ts +2 -0
  12. package/dist/brain/request-params.js +16 -0
  13. package/dist/brain/retry.d.ts +5 -0
  14. package/dist/brain/retry.js +16 -4
  15. package/dist/brain/stream-engine.d.ts +9 -1
  16. package/dist/brain/stream-engine.js +261 -28
  17. package/dist/brain/timeout.d.ts +1 -0
  18. package/dist/brain/timeout.js +1 -0
  19. package/dist/core/a2a.d.ts +2 -2
  20. package/dist/core/a2a.js +3 -3
  21. package/dist/core/ask-question.d.ts +47 -2
  22. package/dist/core/ask-question.js +209 -28
  23. package/dist/core/background-agent-store.d.ts +2 -0
  24. package/dist/core/checkpoint-store.d.ts +41 -17
  25. package/dist/core/checkpoint-store.js +137 -6
  26. package/dist/core/hooks.d.ts +24 -2
  27. package/dist/core/hooks.js +97 -10
  28. package/dist/core/human-input-projection.d.ts +12 -0
  29. package/dist/core/human-input-projection.js +27 -0
  30. package/dist/core/mcp.d.ts +7 -2
  31. package/dist/core/mcp.js +7 -7
  32. package/dist/core/memory-admission.d.ts +4 -0
  33. package/dist/core/memory-admission.js +3 -0
  34. package/dist/core/memory-engine/engine.d.ts +3 -1
  35. package/dist/core/memory-engine/engine.js +4 -3
  36. package/dist/core/memory-recall.d.ts +1 -1
  37. package/dist/core/memory-recall.js +3 -2
  38. package/dist/core/runner/assemble-result.d.ts +1 -0
  39. package/dist/core/runner/assemble-result.js +1 -1
  40. package/dist/core/runner/prepare-memory.d.ts +1 -0
  41. package/dist/core/runner/prepare-memory.js +3 -3
  42. package/dist/core/runner/prepare-task.d.ts +19 -6
  43. package/dist/core/runner/prepare-task.js +343 -35
  44. package/dist/core/runner/runtask.d.ts +3 -6
  45. package/dist/core/runner/runtask.js +290 -64
  46. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  47. package/dist/core/runner/tool-disclosure.js +65 -16
  48. package/dist/core/runner/tool-output-projection.js +1 -0
  49. package/dist/core/runner/turn-attachments.d.ts +4 -0
  50. package/dist/core/runner/turn-attachments.js +15 -2
  51. package/dist/core/session-store.d.ts +3 -0
  52. package/dist/core/session-store.js +4 -0
  53. package/dist/core/session.d.ts +1 -0
  54. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  55. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  56. package/dist/core/task-notification.d.ts +2 -0
  57. package/dist/core/task-notification.js +5 -3
  58. package/dist/core/task-registry-agent.d.ts +1 -0
  59. package/dist/core/task-registry-agent.js +6 -0
  60. package/dist/core/task-registry.d.ts +1 -0
  61. package/dist/core/task-registry.js +4 -1
  62. package/dist/core/task-tool-shape.js +4 -3
  63. package/dist/core/tool-policy.d.ts +5 -0
  64. package/dist/core/tool-policy.js +2 -1
  65. package/dist/core/trace.d.ts +6 -0
  66. package/dist/core/types.d.ts +32 -1
  67. package/dist/core/wiring-manifest.d.ts +97 -0
  68. package/dist/core/wiring-manifest.js +186 -0
  69. package/dist/engine/compaction/compaction.js +2 -2
  70. package/dist/engine/harness/agent-harness.d.ts +2 -1
  71. package/dist/engine/harness/agent-harness.js +8 -1
  72. package/dist/engine/harness/types.d.ts +3 -1
  73. package/dist/engine/llm/types.d.ts +7 -0
  74. package/dist/engine/llm/types.js +8 -1
  75. package/dist/engine/session/import-validate.d.ts +6 -1
  76. package/dist/engine/session/import-validate.js +29 -6
  77. package/dist/engine/session/memory-repo.d.ts +3 -1
  78. package/dist/engine/session/memory-repo.js +2 -2
  79. package/dist/index.d.ts +10 -7
  80. package/dist/index.js +8 -5
  81. package/dist/internal/harness-types.d.ts +1 -1
  82. package/dist/internal/llm.d.ts +2 -2
  83. package/dist/internal/llm.js +1 -1
  84. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  85. package/dist/orchestration/run-workflow-tool.js +3 -0
  86. package/dist/orchestration/workflow-types.d.ts +8 -0
  87. package/dist/orchestration/workflow-types.js +14 -0
  88. package/dist/orchestration/workflow.d.ts +4 -0
  89. package/dist/orchestration/workflow.js +134 -5
  90. package/dist/prompts/default.js +1 -1
  91. package/dist/prompts/supervisor.d.ts +2 -2
  92. package/dist/prompts/supervisor.js +5 -4
  93. package/dist/stores/file/checkpoint-store.d.ts +3 -5
  94. package/dist/stores/file/checkpoint-store.js +31 -2
  95. package/dist/stores/file/index.js +1 -1
  96. package/dist/stores/file/session-store.d.ts +3 -1
  97. package/dist/stores/file/session-store.js +2 -2
  98. package/dist/stores/file/shared-ledger.js +8 -1
  99. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  100. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  101. package/dist/tools/fs/fs-bash.d.ts +1 -0
  102. package/dist/tools/fs/fs-bash.js +32 -13
  103. package/dist/tools/fs/gh-rate-limit.d.ts +1 -1
  104. package/dist/tools/fs/gh-rate-limit.js +4 -3
  105. package/dist/tools/fs/index.d.ts +1 -0
  106. package/dist/tools/fs/index.js +1 -0
  107. package/dist/tools/fs/safety.js +34 -10
  108. package/package.json +1 -1
@@ -5,6 +5,8 @@ import type { ToolSpec } from "../types.js";
5
5
  import type { ToolFingerprintInput } from "../cache-break-detector.js";
6
6
  export declare const TOOL_SEARCH_NAME = "ToolSearch";
7
7
  export declare const TOOL_SEARCH_DEFAULT_MAX_RESULTS = 5;
8
+ export declare const DEFERRED_NO_PROGRESS_LIMIT = 3;
9
+ export declare const DEFERRED_SCHEMA_INCOMPATIBLE_CODE = "tool.deferred_schema_incompatible";
8
10
  export interface DeferredToolInfo {
9
11
  name: string;
10
12
  hint: string;
@@ -28,12 +30,14 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
28
30
  export interface PlaceholderDirectCall {
29
31
  resolveReal: () => PlaceholderDirectTarget | undefined;
30
32
  executionMode?: ToolExecutionMode;
33
+ staticFace?: () => boolean;
31
34
  activate: () => Promise<string | undefined>;
32
35
  }
33
36
  export interface PlaceholderDirectTarget {
34
37
  parameters: TSchema;
35
38
  invoke: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<unknown>) => Promise<AgentToolResult<unknown>>;
36
39
  }
40
+ export declare function staticSchemaRenderable(schema: TSchema | undefined): boolean;
37
41
  export declare function createPlaceholderTool(info: DeferredToolInfo, direct?: PlaceholderDirectCall): AgentTool;
38
42
  export declare function scoreToolMatch(query: string, info: DeferredToolInfo): number;
39
43
  export interface ToolSearchArgs {
@@ -53,6 +57,7 @@ export declare function createToolSearchTool(opts: {
53
57
  listingRide?: (newlyActivated: readonly string[]) => string | undefined;
54
58
  mountedNames?: () => ReadonlySet<string>;
55
59
  directCallEnabled?: boolean;
60
+ isMounted?: (name: string) => boolean;
56
61
  serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
57
62
  staticSchemaFor?: (name: string) => TSchema | undefined;
58
63
  }): AgentTool;
@@ -7,6 +7,8 @@ const DEFER_AUTO_FRACTION = 0.1;
7
7
  const CHARS_PER_TOKEN = 4;
8
8
  export const TOOL_SEARCH_DEFAULT_MAX_RESULTS = 5;
9
9
  const MAX_QUERY_RESULTS = 25;
10
+ export const DEFERRED_NO_PROGRESS_LIMIT = 3;
11
+ export const DEFERRED_SCHEMA_INCOMPATIBLE_CODE = "tool.deferred_schema_incompatible";
10
12
  const EMPTY_PARAMS = Type.Object({});
11
13
  export function deferHint(description, max = 120) {
12
14
  const first = (description.split("\n").find((l) => l.trim() !== "") ?? "").trim();
@@ -70,7 +72,13 @@ function renderSchemaForModel(schema) {
70
72
  catch {
71
73
  return undefined;
72
74
  }
73
- return json === undefined ? undefined : truncateError(json);
75
+ if (json === undefined)
76
+ return undefined;
77
+ const text = truncateError(json);
78
+ return { text, complete: text === json };
79
+ }
80
+ export function staticSchemaRenderable(schema) {
81
+ return schema !== undefined && renderSchemaForModel(schema)?.complete === true;
74
82
  }
75
83
  export function createPlaceholderTool(info, direct) {
76
84
  const sn = safeName(info.name);
@@ -78,10 +86,36 @@ export function createPlaceholderTool(info, direct) {
78
86
  throw new Error(`Tool "${sn}" is not active yet. Call ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} ` +
79
87
  `(or a keyword \`query\`) to load its full schema, then call ${sn} with the proper arguments.`);
80
88
  };
81
- const invalidArgumentsRejection = (target, params, schemaJson, ride) => {
82
- const text = `Invalid arguments for \`${sn}\`: ${formatZodValidationError(target.parameters, params)} ` +
83
- `\`${sn}\` is now active its full parameter schema is below; use it for this and later calls. ` +
84
- `Call \`${sn}\` again with arguments matching it.\nParameter schema: ${schemaJson}`;
89
+ let lastFailureShape;
90
+ let repeats = 0;
91
+ const invalidArgumentsRejection = (target, params, schema, ride) => {
92
+ const completeness = schema.complete ? "its full parameter schema" : "an ABRIDGED copy of its parameter schema (too large to inline in full)";
93
+ const durability = !schema.complete
94
+ ? `${completeness} is below; the tools list carries the complete declaration. `
95
+ : direct?.staticFace?.() === true
96
+ ? `${completeness} is below and is carried by THIS result only — the tools list keeps the compact ` +
97
+ `placeholder entry, so re-run ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} if you need the schema again later. `
98
+ : `${completeness} is below, and the next request's tools list will advertise it too. `;
99
+ const shape = formatZodValidationError(target.parameters, params);
100
+ repeats = shape === lastFailureShape ? repeats + 1 : 1;
101
+ lastFailureShape = shape;
102
+ if (repeats >= DEFERRED_NO_PROGRESS_LIMIT) {
103
+ const stuck = `\`${sn}\` rejected the same arguments ${repeats} times in a row and its parameter schema has already ` +
104
+ `been delivered, so re-sending them will not start working. This usually means the caller cannot ` +
105
+ `produce arguments outside the compact placeholder entry advertised for \`${sn}\`. \`${sn}\` is ` +
106
+ `finished for this run — no further call to it will be answered differently. Other tools are ` +
107
+ `unaffected; if this was the only work left, the run stops here. ` +
108
+ `(${DEFERRED_SCHEMA_INCOMPATIBLE_CODE})\nLast rejection: ${shape}`;
109
+ return {
110
+ content: [{ type: "text", text: stuck }],
111
+ details: { invalidArguments: true, noProgress: DEFERRED_SCHEMA_INCOMPATIBLE_CODE, repeats },
112
+ isError: true,
113
+ terminate: true,
114
+ };
115
+ }
116
+ const text = `Invalid arguments for \`${sn}\`: ${shape} ` +
117
+ `\`${sn}\` is now active — ${durability}` +
118
+ `Call \`${sn}\` again with arguments matching it.\nParameter schema: ${schema.text}`;
85
119
  return {
86
120
  content: ride === undefined || ride === "" ? [{ type: "text", text }] : [{ type: "text", text }, { type: "text", text: ride }],
87
121
  details: { invalidArguments: true },
@@ -102,17 +136,19 @@ export function createPlaceholderTool(info, direct) {
102
136
  if (real === undefined)
103
137
  return teachingRejection();
104
138
  if (Value.Check(real.parameters, params)) {
139
+ lastFailureShape = undefined;
140
+ repeats = 0;
105
141
  const ride = await direct.activate();
106
142
  const result = await real.invoke(toolCallId, params, signal, onUpdate);
107
143
  if (ride === undefined || ride === "")
108
144
  return result;
109
145
  return { ...result, content: [...result.content, { type: "text", text: ride }] };
110
146
  }
111
- const schemaJson = renderSchemaForModel(real.parameters);
112
- if (schemaJson === undefined)
147
+ const schemaRender = renderSchemaForModel(real.parameters);
148
+ if (schemaRender === undefined)
113
149
  return teachingRejection();
114
150
  const ride = await direct.activate();
115
- return invalidArgumentsRejection(real, params, schemaJson, ride);
151
+ return invalidArgumentsRejection(real, params, schemaRender, ride);
116
152
  },
117
153
  };
118
154
  }
@@ -236,12 +272,13 @@ export function extractDiscoveredToolNames(messages, registry) {
236
272
  return [...names];
237
273
  }
238
274
  export function createToolSearchTool(opts) {
239
- const { registry, active, rematerialize, listingRide, mountedNames, staticSchemaFor } = opts;
275
+ const { registry, active, rematerialize, listingRide, mountedNames, staticSchemaFor, isMounted } = opts;
240
276
  const directCallEnabled = opts.directCallEnabled !== false;
241
277
  const activationPosture = directCallEnabled
242
278
  ? (staticSchemaFor !== undefined
243
279
  ? "Most tools start as name-only placeholders to keep requests small; activating one here returns its full " +
244
- "parameter schema in the result (the tools list keeps the compact placeholder entry). "
280
+ "parameter schema in the result (the tools list keeps the compact placeholder entry — except for a " +
281
+ "declaration too large to inline, which goes to the tools list instead). "
245
282
  : "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
246
283
  "parameter schema. ") +
247
284
  "Until you have that schema you cannot reliably form a call, so activate a tool rather " +
@@ -286,14 +323,21 @@ export function createToolSearchTool(opts) {
286
323
  `selection in \`query\` instead: {"query":"select:ToolA,ToolB"}. Nothing was activated.`);
287
324
  }
288
325
  const args = (raw ?? {});
289
- const { matched, missing } = resolveToolSearchDetailed(args, registry);
326
+ const resolved = resolveToolSearchDetailed(args, registry);
327
+ const missing = resolved.missing;
328
+ const withdrawn = isMounted === undefined ? [] : resolved.matched.filter((n) => !isMounted(n));
329
+ const matched = isMounted === undefined ? resolved.matched : resolved.matched.filter((n) => isMounted(n));
290
330
  const mounted = mountedNames?.() ?? new Set();
291
331
  const missCallable = missing.filter((n) => mounted.has(n));
292
332
  const missUnknown = missing.filter((n) => !mounted.has(n));
293
333
  const callableNote = missCallable.length > 0
294
334
  ? `\nAlready available: ${missCallable.map((n) => safeName(n)).join(", ")} — ${missCallable.length > 1 ? "these tools are" : "this tool is"} not deferred; call ${missCallable.length > 1 ? "them" : "it"} directly right now (no activation needed).`
295
335
  : "";
336
+ const withdrawnNote = withdrawn.length > 0
337
+ ? `\nNo longer available: ${withdrawn.map((n) => safeName(n)).join(", ")} — ${withdrawn.length > 1 ? "these tools were" : "this tool was"} withdrawn by ${withdrawn.length > 1 ? "their providers" : "its provider"} and cannot be activated or called.`
338
+ : "";
296
339
  const missingNote = callableNote +
340
+ withdrawnNote +
297
341
  (missUnknown.length > 0
298
342
  ? `\nNot found: ${missUnknown.map((n) => safeName(n)).join(", ")} — not in the deferred registry under this exact name (lookup is case-sensitive). ` +
299
343
  "(Already-active and non-deferred tools are callable directly and don't appear here.)"
@@ -311,7 +355,7 @@ export function createToolSearchTool(opts) {
311
355
  matches: [],
312
356
  query: typeof args.query === "string" ? args.query : "",
313
357
  total_deferred_tools: registry.size,
314
- ...(missing.length > 0 ? { missing: missing.map(safeName) } : {}),
358
+ ...(missing.length > 0 || withdrawn.length > 0 ? { missing: [...missing, ...withdrawn].map(safeName) } : {}),
315
359
  },
316
360
  };
317
361
  }
@@ -332,6 +376,7 @@ export function createToolSearchTool(opts) {
332
376
  return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
333
377
  });
334
378
  const { newly, ride } = await section;
379
+ let missingSchemaLines = false;
335
380
  const lines = matched.map((n) => {
336
381
  const info = registry.get(n);
337
382
  const tag = newly.includes(n) ? "activated" : "already active";
@@ -340,12 +385,16 @@ export function createToolSearchTool(opts) {
340
385
  return base;
341
386
  const schema = staticSchemaFor(n);
342
387
  const json = schema === undefined ? undefined : renderSchemaForModel(schema);
343
- return json === undefined ? base : `${base}\n parameters: ${json}`;
388
+ if (json === undefined || !json.complete) {
389
+ missingSchemaLines = true;
390
+ return `${base}\n parameters: not inlined — this tool's full declaration is in the tools list.`;
391
+ }
392
+ return `${base}\n parameters: ${json.text}`;
344
393
  });
345
394
  const head = staticSchemaFor !== undefined
346
395
  ? newly.length > 0
347
- ? `Activated ${newly.length} tool(s) — call them directly with arguments matching the parameter schemas below (the tools list keeps compact placeholder entries):`
348
- : "These tools are already active — call them directly; their parameter schemas are repeated below:"
396
+ ? `Activated ${newly.length} tool(s) — call them directly with arguments matching the parameter schemas below (the tools list keeps compact placeholder entries${missingSchemaLines ? ", except where a line says otherwise" : ""}):`
397
+ : `These tools are already active — call them directly; their parameter schemas are repeated below${missingSchemaLines ? " where they can be inlined" : ""}:`
349
398
  : newly.length > 0
350
399
  ? `Activated ${newly.length} tool(s); they are now available with full parameters — call them directly:`
351
400
  : "These tools are already active — call them directly:";
@@ -358,7 +407,7 @@ export function createToolSearchTool(opts) {
358
407
  matches: matched.map(safeName),
359
408
  query: typeof args.query === "string" ? args.query : "",
360
409
  total_deferred_tools: registry.size,
361
- ...(missing.length > 0 ? { missing: missing.map(safeName) } : {}),
410
+ ...(missing.length > 0 || withdrawn.length > 0 ? { missing: [...missing, ...withdrawn].map(safeName) } : {}),
362
411
  },
363
412
  };
364
413
  },
@@ -57,6 +57,7 @@ export const toolOutputFrom = (result) => {
57
57
  return { output: raw, truncated: true, totalChars };
58
58
  };
59
59
  const CC_DETAIL_TYPES = new Set([
60
+ "ask-question",
60
61
  "edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
62
  "agent", "task", "task-list", "task-output", "workflow-run",
62
63
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
@@ -101,6 +101,8 @@ export interface AttachmentInputs {
101
101
  backgroundTasks?: ReadonlyArray<BackgroundTaskSnapshot>;
102
102
  newTools?: readonly string[];
103
103
  newToolsStaticFace?: boolean;
104
+ newToolsSwappedUnderStatic?: readonly string[];
105
+ readdedToolsSwappedUnderStatic?: readonly string[];
104
106
  mcpToolsDelta?: McpToolsDeltaFacts;
105
107
  agentListing?: ReadonlyArray<AgentListingEntry>;
106
108
  agentToolName?: string;
@@ -150,6 +152,8 @@ export interface McpToolsDeltaFacts {
150
152
  export declare function renderToolsDelta(input: {
151
153
  added?: readonly string[];
152
154
  staticFace?: boolean;
155
+ swappedUnderStatic?: readonly string[];
156
+ readdedSwappedUnderStatic?: readonly string[];
153
157
  } & McpToolsDeltaFacts): string | undefined;
154
158
  export declare const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
155
159
  export declare const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
@@ -171,6 +171,12 @@ export function collectDueAttachments(state, inp) {
171
171
  const body = renderToolsDelta({
172
172
  ...(inp.newTools !== undefined ? { added: inp.newTools } : {}),
173
173
  ...(inp.newToolsStaticFace === true ? { staticFace: true } : {}),
174
+ ...(inp.newToolsSwappedUnderStatic !== undefined && inp.newToolsSwappedUnderStatic.length > 0
175
+ ? { swappedUnderStatic: inp.newToolsSwappedUnderStatic }
176
+ : {}),
177
+ ...(inp.readdedToolsSwappedUnderStatic !== undefined && inp.readdedToolsSwappedUnderStatic.length > 0
178
+ ? { readdedSwappedUnderStatic: inp.readdedToolsSwappedUnderStatic }
179
+ : {}),
174
180
  ...(inp.mcpToolsDelta ?? {}),
175
181
  });
176
182
  if (body !== undefined)
@@ -392,19 +398,26 @@ export function renderToolsDelta(input) {
392
398
  const blocks = [];
393
399
  const added = input.added ?? [];
394
400
  if (added.length > 0) {
401
+ const swapped = input.staticFace === true ? (input.swappedUnderStatic ?? []).filter((n) => added.includes(n)) : [];
395
402
  blocks.push((input.staticFace === true
396
403
  ? "The following deferred tools are now active — call them directly. Their parameter schemas were " +
397
404
  "provided in the ToolSearch result (the tools list itself keeps compact placeholder entries):\n"
398
405
  : "The following deferred tools are now available. Their full schemas are loaded — call them " +
399
406
  "directly like any other tool:\n") +
400
- added.map((n) => `- ${n}`).join("\n"));
407
+ added.map((n) => `- ${n}`).join("\n") +
408
+ (swapped.length > 0
409
+ ? `\nException: ${swapped.join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
410
+ : ""));
401
411
  }
402
412
  const readded = input.readded ?? [];
403
413
  if (readded.length > 0) {
404
414
  blocks.push(`${readded.length} deferred tool${readded.length === 1 ? " is" : "s are"} available again (MCP server reconnected — ` +
405
415
  `names announced earlier in this conversation): ${groupByMcpServer(readded)}. ` +
406
416
  (input.staticFace === true
407
- ? `The tools list keeps compact placeholder entries — re-run ToolSearch ("select:<name>") if you need their current parameter schemas.`
417
+ ? `The tools list keeps compact placeholder entries — re-run ToolSearch ("select:<name>") if you need their current parameter schemas.` +
418
+ ((input.readdedSwappedUnderStatic ?? []).filter((n) => readded.includes(n)).length > 0
419
+ ? ` Exception: ${(input.readdedSwappedUnderStatic ?? []).filter((n) => readded.includes(n)).join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
420
+ : "")
408
421
  : `Their schemas are loaded again — call them directly.`));
409
422
  }
410
423
  const removed = input.removed ?? [];
@@ -1,5 +1,6 @@
1
1
  import { type SessionRepo } from "../internal/harness.js";
2
2
  import { type AcquiredSession, type SessionStore, type SessionStoreSummary } from "./session.js";
3
+ import type { StoreDurability } from "./checkpoint-store.js";
3
4
  import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
4
5
  export { SESSION_DEFAULT_TTL_DAYS };
5
6
  export type EvictPolicy = "delete" | "forget";
@@ -8,9 +9,11 @@ export interface TtlSessionStoreOptions {
8
9
  sweepIntervalMs?: number;
9
10
  repo?: SessionRepo;
10
11
  evict?: EvictPolicy;
12
+ durability?: StoreDurability;
11
13
  }
12
14
  export declare class TtlSessionStore implements SessionStore {
13
15
  readonly retention: "none";
16
+ readonly durability?: StoreDurability;
14
17
  private repo;
15
18
  private entries;
16
19
  private owners;
@@ -5,6 +5,7 @@ import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
5
5
  export { SESSION_DEFAULT_TTL_DAYS };
6
6
  export class TtlSessionStore {
7
7
  retention = "none";
8
+ durability;
8
9
  repo;
9
10
  entries = new Map();
10
11
  owners = new Map();
@@ -15,6 +16,9 @@ export class TtlSessionStore {
15
16
  sweepTimer;
16
17
  constructor(opts = {}) {
17
18
  this.repo = opts.repo ?? new InMemorySessionRepo();
19
+ const durability = opts.durability ?? (opts.repo === undefined ? "process-local" : undefined);
20
+ if (durability !== undefined)
21
+ this.durability = durability;
18
22
  this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS;
19
23
  this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete");
20
24
  if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) {
@@ -20,6 +20,7 @@ export interface SessionStoreSummary {
20
20
  forkedFrom?: string;
21
21
  }
22
22
  export interface SessionStore {
23
+ readonly durability?: import("./checkpoint-store.js").StoreDurability;
23
24
  acquire(sessionId?: string, opts?: {
24
25
  requireExisting?: boolean;
25
26
  }): Promise<AcquiredSession>;
@@ -52,6 +52,25 @@ export async function backgroundAgentStoreContract(mk, runAssertion) {
52
52
  assert.equal(await store.get("a1", "tenant-C"), null, "an unrelated scope must not see the row");
53
53
  assert.equal(await store.get("a-missing", S), null);
54
54
  });
55
+ run("the org-admission fields round-trip, and ABSENT stays absent (a backend must not invent a verdict)", async (make) => {
56
+ const store = make();
57
+ await store.put(record({ handle: "a1", admittedOrgScopes: ["org:acme", "org:beta"], admittedOrgWriteScope: "org:acme" }));
58
+ const withVerdict = await held(store, "a1");
59
+ assert.deepEqual(withVerdict.admittedOrgScopes, ["org:acme", "org:beta"]);
60
+ assert.equal(withVerdict.admittedOrgWriteScope, "org:acme");
61
+ await store.put(record({ handle: "a2", admittedOrgScopes: [], admittedOrgWriteScope: null }));
62
+ const readOnly = await held(store, "a2");
63
+ assert.deepEqual(readOnly.admittedOrgScopes, []);
64
+ assert.equal(readOnly.admittedOrgWriteScope, null);
65
+ await store.put(record({ handle: "a3" }));
66
+ const untouched = await held(store, "a3");
67
+ assert.equal(untouched.admittedOrgScopes, undefined);
68
+ assert.equal(untouched.admittedOrgWriteScope, undefined);
69
+ await store.update("a3", S, { ...untouched, admittedOrgScopes: ["org:acme"], admittedOrgWriteScope: null }, { rev: untouched.rev });
70
+ const afterCas = await held(store, "a3");
71
+ assert.deepEqual(afterCas.admittedOrgScopes, ["org:acme"]);
72
+ assert.equal(afterCas.admittedOrgWriteScope, null);
73
+ });
55
74
  run("put refuses a scopeless row (a row with no tenant partition would be world-readable)", async (make) => {
56
75
  const store = make();
57
76
  await assert.rejects(store.put(record({ scope: "" })), "an empty scope must be refused loudly, not stored");
@@ -1,5 +1,5 @@
1
1
  import { strict as assert } from "node:assert";
2
- import { mintCheckpointToken, } from "../checkpoint-store.js";
2
+ import { MAX_PENDING_STEER_ENTRIES, mintCheckpointToken, readPendingSteerQueue, } from "../checkpoint-store.js";
3
3
  import { beginContract } from "./contract-harness.js";
4
4
  export function createCheckpointFixture(over = {}) {
5
5
  const token = over.token ?? mintCheckpointToken();
@@ -90,9 +90,68 @@ export async function checkpointStoreContract(make, runAssertion) {
90
90
  const cp = createCheckpointFixture();
91
91
  await store.put(cp.token, cp);
92
92
  await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "</SYSTEM-REMINDER>", trusted: true }), (e) => e.code === "steering.invalid_content");
93
- assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
93
+ assert.deepEqual(readPendingSteerQueue((await store.get(cp.token)).state), []);
94
94
  assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false }), true);
95
- assert.deepEqual((await store.get(cp.token)).state.pendingSteer, { text: "go", trusted: false });
95
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
96
+ assert.equal(q.length, 1);
97
+ assert.equal(q[0].text, "go");
98
+ assert.equal(q[0].trusted, false);
99
+ assert.equal(q[0].seq, 1);
100
+ assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
101
+ });
102
+ run("setPendingSteer APPENDS: two steers both survive in issue order, each keeping its frozen trust", async () => {
103
+ const store = make();
104
+ const cp = createCheckpointFixture();
105
+ await store.put(cp.token, cp);
106
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "first", trusted: false }), true);
107
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "second", trusted: true }), true);
108
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
109
+ assert.deepEqual(q.map((e) => e.text), ["first", "second"]);
110
+ assert.deepEqual(q.map((e) => e.trusted), [false, true]);
111
+ assert.deepEqual(q.map((e) => e.seq), [1, 2]);
112
+ });
113
+ run("setPendingSteer is idempotent on inputId (a retry with the caller's own message id adds nothing)", async () => {
114
+ const store = make();
115
+ const cp = createCheckpointFixture();
116
+ await store.put(cp.token, cp);
117
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "once", trusted: false, inputId: "m1" }), true);
118
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "once", trusted: false, inputId: "m1" }), true);
119
+ assert.equal(readPendingSteerQueue((await store.get(cp.token)).state).length, 1);
120
+ });
121
+ run("the queue bound is fail-loud steering.queue_full and evicts nothing", async () => {
122
+ const store = make();
123
+ const cp = createCheckpointFixture();
124
+ await store.put(cp.token, cp);
125
+ for (let i = 0; i < MAX_PENDING_STEER_ENTRIES; i++) {
126
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: `s${i}`, trusted: false }), true);
127
+ }
128
+ await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "overflow", trusted: false }), (e) => e.code === "steering.queue_full");
129
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
130
+ assert.equal(q.length, MAX_PENDING_STEER_ENTRIES);
131
+ assert.deepEqual(q.map((e) => e.text), Array.from({ length: MAX_PENDING_STEER_ENTRIES }, (_, i) => `s${i}`));
132
+ });
133
+ run("a queue entry round-trips every FROZEN field (actor/inputId/priority included)", async () => {
134
+ const store = make();
135
+ const cp = createCheckpointFixture();
136
+ await store.put(cp.token, cp);
137
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, {
138
+ text: "attributed",
139
+ trusted: true,
140
+ actor: { id: "slack:U1", hostAsserted: true, issuer: "slack-app:A1" },
141
+ inputId: "m-42",
142
+ priority: "now",
143
+ }), true);
144
+ const q = readPendingSteerQueue((await store.get(cp.token)).state);
145
+ assert.deepEqual(q, [
146
+ { text: "attributed", trusted: true, actor: { id: "slack:U1", hostAsserted: true, issuer: "slack-app:A1" }, inputId: "m-42", priority: "now", seq: 1 },
147
+ ]);
148
+ });
149
+ run("an unknown steering field is REFUSED (a producer must not have its new field silently dropped)", async () => {
150
+ const store = make();
151
+ const cp = createCheckpointFixture();
152
+ await store.put(cp.token, cp);
153
+ await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false, futureField: 1 }), (e) => e.code === "steering.invalid_content");
154
+ assert.deepEqual(readPendingSteerQueue((await store.get(cp.token)).state), []);
96
155
  });
97
156
  run("expire vs resolve on the same row → exactly one wins; reap expires past-deadline pending in scope", async () => {
98
157
  const store = make();
@@ -41,6 +41,8 @@ export interface SystemInjection<TPayload = unknown> {
41
41
  }
42
42
  export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
43
43
  export declare function taskNotificationLaneKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type">): string;
44
+ export declare function attrEscape(value: string): string;
45
+ export declare const EXTERNAL_SOURCE_MAX = 120;
44
46
  export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
45
47
  export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
46
48
  export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;
@@ -38,10 +38,10 @@ export function taskNotificationDedupKey(n) {
38
38
  export function taskNotificationLaneKey(n) {
39
39
  return n.task_type === "external" ? `external:${n.task_id}` : n.task_id;
40
40
  }
41
- function attrEscape(value) {
41
+ export function attrEscape(value) {
42
42
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
43
43
  }
44
- const EXTERNAL_SOURCE_MAX = 120;
44
+ export const EXTERNAL_SOURCE_MAX = 120;
45
45
  export function renderTaskNotificationXml(n) {
46
46
  const usage = n.usage === undefined
47
47
  ? undefined
@@ -54,7 +54,9 @@ export function renderTaskNotificationXml(n) {
54
54
  }
55
55
  })();
56
56
  const open = n.task_type === "external"
57
- ? `<task-notification type="external"${n.source !== undefined ? ` from="${attrEscape(inlineUntrusted(n.source, EXTERNAL_SOURCE_MAX))}"` : ""}>`
57
+ ? `<task-notification type="external"${n.source !== undefined
58
+ ? ` from="${attrEscape(inlineUntrusted(n.source, EXTERNAL_SOURCE_MAX))}" from-unverified="true"`
59
+ : ""}>`
58
60
  : "<task-notification>";
59
61
  return [
60
62
  open,
@@ -28,6 +28,7 @@ export declare function reapDurableAgentsLane(core: DurableAgentCore, scope: str
28
28
  }>;
29
29
  export declare function releaseDurableTranscriptAnchorLane(core: DurableAgentCore, id: string): void;
30
30
  export declare function bindBackgroundAgentSessionLane(core: DurableAgentCore, id: string, sessionId: string): void;
31
+ export declare function recordBackgroundAgentOrgAdmissionLane(core: DurableAgentCore, id: string, verdict: import("./memory-admission.js").OwnOrgAdmissionVerdict): void;
31
32
  export declare function registerBackgroundAgentLane(core: DurableAgentCore, input: RegisterBackgroundAgentInput): string;
32
33
  export declare function parkBackgroundAgentLane(core: DurableAgentCore, id: string, park: {
33
34
  checkpointToken: string;
@@ -247,6 +247,12 @@ export function bindBackgroundAgentSessionLane(core, id, sessionId) {
247
247
  handle.sessionId = sessionId;
248
248
  durableAgentWriteLane(handle, { sessionId });
249
249
  }
250
+ export function recordBackgroundAgentOrgAdmissionLane(core, id, verdict) {
251
+ const handle = core.handles.get(id);
252
+ if (!handle || handle.type !== "background_agent")
253
+ return;
254
+ durableAgentWriteLane(handle, { admittedOrgScopes: [...verdict.scopes], admittedOrgWriteScope: verdict.writeScope });
255
+ }
250
256
  export function registerBackgroundAgentLane(core, input) {
251
257
  assertOwnership(input, "registerBackgroundAgent");
252
258
  if (input.id !== undefined && !DURABLE_AGENT_HANDLE_RE.test(input.id)) {
@@ -101,6 +101,7 @@ export declare class TaskRegistry {
101
101
  releaseDurableTranscriptAnchor(id: string): void;
102
102
  bindBackgroundAgentSession(id: string, sessionId: string): void;
103
103
  registerBackgroundAgent(input: RegisterBackgroundAgentInput): string;
104
+ recordBackgroundAgentOrgAdmission(id: string, verdict: import("./memory-admission.js").OwnOrgAdmissionVerdict): void;
104
105
  parkBackgroundAgent(id: string, park: {
105
106
  checkpointToken: string;
106
107
  seq?: number;
@@ -10,7 +10,7 @@ import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task
10
10
  import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
11
11
  export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
12
12
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
13
- import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
13
+ import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
14
14
  export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
15
15
  const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
16
16
  const BLOCK_MAX_TIMEOUT_MS = 600_000;
@@ -146,6 +146,9 @@ export class TaskRegistry {
146
146
  registerBackgroundAgent(input) {
147
147
  return registerBackgroundAgentLane(this.core, input);
148
148
  }
149
+ recordBackgroundAgentOrgAdmission(id, verdict) {
150
+ return recordBackgroundAgentOrgAdmissionLane(this.core, id, verdict);
151
+ }
149
152
  parkBackgroundAgent(id, park) {
150
153
  return parkBackgroundAgentLane(this.core, id, park);
151
154
  }
@@ -39,8 +39,9 @@ export function composeTaskOutputDescription(caps) {
39
39
  }
40
40
  parts.push("Retrieve output or status for a background task by task_id. ");
41
41
  parts.push(caps.lanes
42
- ? "Supports background Bash tasks and workflow tasks returned by RunWorkflow. Returns only the caller's own task scope; unknown and out-of-scope ids are " +
43
- "reported the same way. "
42
+ ?
43
+ "Supports background Bash tasks and workflow tasks returned by Workflow. Returns only the caller's own task scope; unknown and out-of-scope ids are " +
44
+ "reported the same way. "
44
45
  : "Reads a background shell started with Bash(run_in_background): returns the run status (running / exited(code) / killed / failed) plus NEW stdout/stderr since your last call. ");
45
46
  if (!caps.notification) {
46
47
  parts.push('When using a background command as a wait condition, call this repeatedly until status is no longer "running" — completion ' +
@@ -64,7 +65,7 @@ export function composeTaskOutputParams(caps) {
64
65
  return Type.Object({
65
66
  task_id: Type.Optional(Type.String({
66
67
  description: caps.lanes
67
- ? "The task_id returned by Bash(run_in_background) or RunWorkflow."
68
+ ? "The task_id returned by Bash(run_in_background) or Workflow."
68
69
  : "The task_id returned by Bash(run_in_background).",
69
70
  })),
70
71
  ...(caps.blockWait
@@ -80,12 +80,17 @@ export interface AskRequest {
80
80
  toolCallId: string;
81
81
  args: unknown;
82
82
  readonly preview?: unknown;
83
+ readonly boundInputHash?: string;
83
84
  message: string;
84
85
  readonly principal?: string;
85
86
  readonly sourceTaskId?: string;
86
87
  readonly fromSubagent?: true;
87
88
  readonly sourceAgentName?: string;
88
89
  readonly requiresRealApproval?: boolean;
90
+ readonly riskAxes?: {
91
+ readonly irreversible?: boolean;
92
+ readonly egress?: boolean;
93
+ };
89
94
  readonly delegation?: AskDelegationProvenance;
90
95
  }
91
96
  export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal) => AskOutcome | Promise<AskOutcome>);
@@ -1,6 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
+ import { boundInputHashOf } from "./canonical-json.js";
4
5
  import { writeTargetPath } from "../tools/fs/safety.js";
5
6
  export function decisionText(d) {
6
7
  return d.message;
@@ -606,7 +607,7 @@ export async function resolveAsk(req, onAsk, signal) {
606
607
  decisionReason: "mode",
607
608
  };
608
609
  }
609
- ok = await onAsk({ ...req, args: approverView.value }, signal);
610
+ ok = await onAsk({ ...req, boundInputHash: boundInputHashOf(presented.value), args: approverView.value }, signal);
610
611
  }
611
612
  catch (err) {
612
613
  return {
@@ -9,6 +9,11 @@ export interface TraceMechanismsSummary {
9
9
  repetitionCuts?: number;
10
10
  repetitionSpared?: number;
11
11
  }
12
+ export interface ToolDisclosureManifest {
13
+ deferredTools: number;
14
+ strategy: "swap" | "static";
15
+ source: "spec" | "env" | "default" | "degraded_no_direct_lane";
16
+ }
12
17
  export type TraceEvent = {
13
18
  kind: "task.start";
14
19
  version: 1;
@@ -47,6 +52,7 @@ export type TraceEvent = {
47
52
  form: string;
48
53
  intentionalDivergences: readonly string[];
49
54
  };
55
+ toolDisclosure?: ToolDisclosureManifest;
50
56
  ts: number;
51
57
  } | {
52
58
  kind: "prompt.snapshot_changed";