@sema-agent/core 5.12.0 → 5.14.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 (101) hide show
  1. package/CHANGELOG.md +320 -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 +137 -43
  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/open-responses.js +24 -10
  9. package/dist/brain/openai.js +29 -11
  10. package/dist/brain/request-params.d.ts +2 -0
  11. package/dist/brain/request-params.js +16 -0
  12. package/dist/brain/stream-engine.d.ts +9 -1
  13. package/dist/brain/stream-engine.js +256 -27
  14. package/dist/brain/timeout.d.ts +1 -0
  15. package/dist/brain/timeout.js +1 -0
  16. package/dist/core/a2a.d.ts +2 -2
  17. package/dist/core/a2a.js +3 -3
  18. package/dist/core/ask-question.d.ts +47 -2
  19. package/dist/core/ask-question.js +209 -28
  20. package/dist/core/background-agent-store.d.ts +2 -0
  21. package/dist/core/checkpoint-store.d.ts +49 -18
  22. package/dist/core/checkpoint-store.js +117 -4
  23. package/dist/core/compliance.d.ts +11 -0
  24. package/dist/core/compliance.js +34 -0
  25. package/dist/core/governance-codes.d.ts +12 -0
  26. package/dist/core/governance-codes.js +24 -0
  27. package/dist/core/hooks.d.ts +24 -2
  28. package/dist/core/hooks.js +97 -10
  29. package/dist/core/human-input-projection.d.ts +12 -0
  30. package/dist/core/human-input-projection.js +27 -0
  31. package/dist/core/locked-config.d.ts +27 -0
  32. package/dist/core/locked-config.js +42 -0
  33. package/dist/core/mcp.d.ts +7 -2
  34. package/dist/core/mcp.js +7 -7
  35. package/dist/core/memory-admission.d.ts +51 -0
  36. package/dist/core/memory-admission.js +159 -0
  37. package/dist/core/memory.d.ts +2 -0
  38. package/dist/core/memory.js +3 -2
  39. package/dist/core/retention.d.ts +36 -0
  40. package/dist/core/retention.js +31 -0
  41. package/dist/core/runner/assemble-result.d.ts +1 -0
  42. package/dist/core/runner/assemble-result.js +2 -2
  43. package/dist/core/runner/prepare-memory.d.ts +9 -0
  44. package/dist/core/runner/prepare-memory.js +28 -2
  45. package/dist/core/runner/prepare-task.d.ts +18 -6
  46. package/dist/core/runner/prepare-task.js +428 -38
  47. package/dist/core/runner/runtask.d.ts +3 -6
  48. package/dist/core/runner/runtask.js +258 -77
  49. package/dist/core/runner/tool-output-projection.js +1 -0
  50. package/dist/core/session-store.d.ts +4 -0
  51. package/dist/core/session-store.js +5 -0
  52. package/dist/core/session.d.ts +1 -0
  53. package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
  54. package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
  55. package/dist/core/task-notification.d.ts +2 -0
  56. package/dist/core/task-notification.js +5 -3
  57. package/dist/core/task-registry-agent.d.ts +1 -0
  58. package/dist/core/task-registry-agent.js +6 -0
  59. package/dist/core/task-registry.d.ts +1 -0
  60. package/dist/core/task-registry.js +4 -1
  61. package/dist/core/tool-policy.d.ts +5 -0
  62. package/dist/core/tool-policy.js +2 -1
  63. package/dist/core/tool-result-store.d.ts +2 -0
  64. package/dist/core/tool-result-store.js +1 -0
  65. package/dist/core/types.d.ts +38 -2
  66. package/dist/core/wiring-manifest.d.ts +97 -0
  67. package/dist/core/wiring-manifest.js +186 -0
  68. package/dist/engine/compaction/compaction.js +2 -2
  69. package/dist/engine/harness/agent-harness.d.ts +2 -1
  70. package/dist/engine/harness/agent-harness.js +19 -2
  71. package/dist/engine/harness/types.d.ts +3 -1
  72. package/dist/engine/llm/types.d.ts +7 -0
  73. package/dist/engine/llm/types.js +8 -1
  74. package/dist/engine/llm/validation.js +11 -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 +12 -4
  80. package/dist/index.js +12 -4
  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/stores/file/checkpoint-store.d.ts +3 -5
  92. package/dist/stores/file/checkpoint-store.js +31 -2
  93. package/dist/stores/file/index.js +1 -1
  94. package/dist/stores/file/session-store.d.ts +3 -1
  95. package/dist/stores/file/session-store.js +2 -2
  96. package/dist/stores/file/shared-ledger.js +8 -1
  97. package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
  98. package/dist/tools/fs/bash-readonly-classifier.js +94 -0
  99. package/dist/tools/fs/fs-bash.js +31 -12
  100. package/dist/tools/fs/safety.js +34 -10
  101. package/package.json +1 -1
@@ -5,7 +5,7 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
- import { RESPONSES_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
8
+ import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
9
9
  import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
10
10
  import { runStreamingBrain } from "./stream-engine.js";
11
11
  const DEGENERATE_POLL_CHARS = 64;
@@ -222,6 +222,8 @@ export function createOpenResponsesBrain(config = {}) {
222
222
  let sentMaxTokens;
223
223
  let sentMaxTokensLane = "unset";
224
224
  let sentEffort;
225
+ let builtMaxTokens;
226
+ let builtMaxTokensLane = "unset";
225
227
  return runStreamingBrain({
226
228
  model,
227
229
  doFetch,
@@ -229,7 +231,7 @@ export function createOpenResponsesBrain(config = {}) {
229
231
  config,
230
232
  httpLabel: "responses",
231
233
  stallTimeouts: options?.stallTimeouts,
232
- buildRequest: () => {
234
+ buildRequest: (overrides) => {
233
235
  const apiKey = options?.apiKey ?? config.apiKey;
234
236
  const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
235
237
  if (!root) {
@@ -239,9 +241,8 @@ export function createOpenResponsesBrain(config = {}) {
239
241
  throw new Error("createOpenResponsesBrain: options.stop is not expressible in the Open Responses request (the wire form has no stop-sequence field); " +
240
242
  "remove it, or serve this model through the chat-completions lane, which does support it");
241
243
  }
242
- const requestedMaxTokens = options?.maxTokens ?? model.maxTokens;
244
+ const requestedMaxTokens = overrides?.maxOutputTokens ?? options?.maxTokens ?? model.maxTokens;
243
245
  const effort = resolveWireEffort(model, options?.reasoning);
244
- sentEffort = effort;
245
246
  const body = {
246
247
  model: model.id,
247
248
  input: toResponsesInput(context, model, config.replayReasoning !== false),
@@ -257,12 +258,13 @@ export function createOpenResponsesBrain(config = {}) {
257
258
  if (requestedMaxTokens !== undefined) {
258
259
  const capped = Math.max(1, requestedMaxTokens);
259
260
  body.max_output_tokens = capped;
260
- sentMaxTokens = capped;
261
- sentMaxTokensLane = options?.maxTokens !== undefined ? "options" : "model";
261
+ builtMaxTokens = capped;
262
+ builtMaxTokensLane =
263
+ overrides?.maxOutputTokens !== undefined ? "engine" : options?.maxTokens !== undefined ? "options" : "model";
262
264
  }
263
265
  else {
264
- sentMaxTokens = undefined;
265
- sentMaxTokensLane = "unset";
266
+ builtMaxTokens = undefined;
267
+ builtMaxTokensLane = "unset";
266
268
  }
267
269
  if (effort !== undefined)
268
270
  body.reasoning = { effort };
@@ -276,10 +278,18 @@ export function createOpenResponsesBrain(config = {}) {
276
278
  headers["content-type"] = "application/json";
277
279
  if (apiKey)
278
280
  headers["authorization"] = `Bearer ${apiKey}`;
281
+ const wire = applyExtraBody(body, model.extraBody, RESPONSES_RESERVED);
279
282
  return {
280
283
  url: `${root}/responses`,
281
284
  headers,
282
- body: JSON.stringify(applyExtraBody(body, model.extraBody, RESPONSES_RESERVED)),
285
+ body: JSON.stringify(wire),
286
+ outputCapTokens: effectiveOutputCap(wire, OUTPUT_CAP_KEYS.responses),
287
+ capDerivedPaths: OUTPUT_CAP_KEYS.responses,
288
+ onCommitted: () => {
289
+ sentMaxTokens = builtMaxTokens;
290
+ sentMaxTokensLane = builtMaxTokensLane;
291
+ sentEffort = effort;
292
+ },
283
293
  };
284
294
  },
285
295
  makeParser: (ctrl) => {
@@ -671,7 +681,11 @@ export function createOpenResponsesBrain(config = {}) {
671
681
  const refused = accumRefusal.trim().length > 0;
672
682
  const emptyNoFinish = noUsableContent && terminalStatus === undefined && !degenerate;
673
683
  const maxTokensNote = sentMaxTokens !== undefined
674
- ? `effective max_output_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : "model.maxTokens"})`
684
+ ? `effective max_output_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options"
685
+ ? "options.maxTokens"
686
+ : sentMaxTokensLane === "engine"
687
+ ? "the engine's context-overflow recovery"
688
+ : "model.maxTokens"})`
675
689
  : "max_output_tokens not set (neither options.maxTokens nor model.maxTokens — the endpoint's default cap applied)";
676
690
  const errorMessage = degenerate
677
691
  ? DEGENERATE_MESSAGE
@@ -5,7 +5,7 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
- import { OPENAI_RESERVED, applyExtraBody, stripAuthHeaders } from "./request-params.js";
8
+ import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
9
9
  import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
10
10
  import { runStreamingBrain } from "./stream-engine.js";
11
11
  function closeToolCallAccum(acc) {
@@ -268,6 +268,8 @@ export function createOpenAIBrain(config = {}) {
268
268
  const stream = (model, context, options) => {
269
269
  let sentMaxTokens;
270
270
  let sentMaxTokensLane = "unset";
271
+ let builtMaxTokens;
272
+ let builtMaxTokensLane = "unset";
271
273
  return runStreamingBrain({
272
274
  model,
273
275
  doFetch,
@@ -275,7 +277,7 @@ export function createOpenAIBrain(config = {}) {
275
277
  config,
276
278
  httpLabel: "gateway",
277
279
  stallTimeouts: options?.stallTimeouts,
278
- buildRequest: () => {
280
+ buildRequest: (overrides) => {
279
281
  const apiKey = options?.apiKey ?? config.apiKey;
280
282
  const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
281
283
  if (!root) {
@@ -292,18 +294,19 @@ export function createOpenAIBrain(config = {}) {
292
294
  body.tools = tools;
293
295
  if (options?.temperature !== undefined)
294
296
  body.temperature = options.temperature;
295
- const requestedMaxTokens = options?.maxTokens ?? model.maxTokens;
297
+ const requestedMaxTokens = overrides?.maxOutputTokens ?? options?.maxTokens ?? model.maxTokens;
298
+ const declared = (model.compat ?? {}).maxTokensField;
299
+ const mtField = declared ?? inferMaxTokensField(model.id);
296
300
  if (requestedMaxTokens !== undefined) {
297
- const declared = (model.compat ?? {}).maxTokensField;
298
- const mtField = declared ?? inferMaxTokensField(model.id);
299
301
  const capped = Math.max(1, requestedMaxTokens);
300
302
  body[mtField] = capped;
301
- sentMaxTokens = capped;
302
- sentMaxTokensLane = options?.maxTokens !== undefined ? "options" : "model";
303
+ builtMaxTokens = capped;
304
+ builtMaxTokensLane =
305
+ overrides?.maxOutputTokens !== undefined ? "engine" : options?.maxTokens !== undefined ? "options" : "model";
303
306
  }
304
307
  else {
305
- sentMaxTokens = undefined;
306
- sentMaxTokensLane = "unset";
308
+ builtMaxTokens = undefined;
309
+ builtMaxTokensLane = "unset";
307
310
  }
308
311
  if (options?.stop)
309
312
  body.stop = options.stop;
@@ -318,7 +321,18 @@ export function createOpenAIBrain(config = {}) {
318
321
  headers["content-type"] = "application/json";
319
322
  if (apiKey)
320
323
  headers["authorization"] = `Bearer ${apiKey}`;
321
- return { url: `${root}/chat/completions`, headers, body: JSON.stringify(applyExtraBody(body, model.extraBody, OPENAI_RESERVED)) };
324
+ const wire = applyExtraBody(body, model.extraBody, OPENAI_RESERVED);
325
+ return {
326
+ url: `${root}/chat/completions`,
327
+ headers,
328
+ body: JSON.stringify(wire),
329
+ outputCapTokens: effectiveOutputCap(wire, OUTPUT_CAP_KEYS.openai),
330
+ capDerivedPaths: [mtField],
331
+ onCommitted: () => {
332
+ sentMaxTokens = builtMaxTokens;
333
+ sentMaxTokensLane = builtMaxTokensLane;
334
+ },
335
+ };
322
336
  },
323
337
  makeParser: (ctrl) => {
324
338
  const { out, partial } = ctrl;
@@ -557,7 +571,11 @@ export function createOpenAIBrain(config = {}) {
557
571
  const refused = accumRefusal.trim().length > 0;
558
572
  const emptyNoFinish = noUsableContent && finishReason == null && !degenerate;
559
573
  const maxTokensNote = sentMaxTokens !== undefined
560
- ? `effective max_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options" ? "options.maxTokens" : "model.maxTokens"})`
574
+ ? `effective max_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options"
575
+ ? "options.maxTokens"
576
+ : sentMaxTokensLane === "engine"
577
+ ? "the engine's context-overflow recovery"
578
+ : "model.maxTokens"})`
561
579
  : "max_tokens not set (neither options.maxTokens nor model.maxTokens — the provider's default cap applied)";
562
580
  const errorMessage = degenerate
563
581
  ? DEGENERATE_MESSAGE
@@ -4,4 +4,6 @@ export declare const RESPONSES_RESERVED: ReadonlySet<string>;
4
4
  export declare function reservedFor(api: string): ReadonlySet<string>;
5
5
  export declare function applyExtraBody(body: Record<string, unknown>, extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): Record<string, unknown>;
6
6
  export declare function stripAuthHeaders(headers: Record<string, string>): void;
7
+ export declare const OUTPUT_CAP_KEYS: Readonly<Record<"openai" | "anthropic" | "responses", readonly string[]>>;
8
+ export declare function effectiveOutputCap(body: Record<string, unknown>, keys: readonly string[]): number | undefined;
7
9
  export declare function reservedCollisions(extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): string[];
@@ -61,6 +61,22 @@ export function stripAuthHeaders(headers) {
61
61
  delete headers[k];
62
62
  }
63
63
  }
64
+ export const OUTPUT_CAP_KEYS = {
65
+ openai: ["max_tokens", "max_completion_tokens"],
66
+ anthropic: ["max_tokens"],
67
+ responses: ["max_output_tokens"],
68
+ };
69
+ export function effectiveOutputCap(body, keys) {
70
+ let smallest;
71
+ for (const key of keys) {
72
+ const raw = body[key];
73
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
74
+ continue;
75
+ if (smallest === undefined || raw < smallest)
76
+ smallest = raw;
77
+ }
78
+ return smallest;
79
+ }
64
80
  export function reservedCollisions(extraBody, reserved) {
65
81
  return extraBody ? Object.keys(extraBody).filter((k) => reserved.has(k)) : [];
66
82
  }
@@ -5,10 +5,18 @@ export interface StreamEngineConfig extends BrainTimeoutConfig {
5
5
  retryDelayMs?: number;
6
6
  }
7
7
  export declare function resolveMaxRetries(configured: number | undefined): number;
8
+ export declare function sameRequestModuloCap(original: SSERequest, rebuilt: SSERequest, imposedCap: number): boolean;
9
+ export declare function shouldRetryHeaderVerdict(res: Response | undefined): boolean | undefined;
8
10
  export interface SSERequest {
9
11
  url: string;
10
12
  headers: Record<string, string>;
11
13
  body: string;
14
+ outputCapTokens?: number;
15
+ capDerivedPaths?: readonly string[];
16
+ onCommitted?: () => void;
17
+ }
18
+ export interface BuildRequestOverrides {
19
+ maxOutputTokens?: number;
12
20
  }
13
21
  export interface StreamControls {
14
22
  out: ReturnType<typeof createAssistantMessageEventStream>;
@@ -32,7 +40,7 @@ export declare function runStreamingBrain(args: {
32
40
  signal?: AbortSignal;
33
41
  config: StreamEngineConfig;
34
42
  httpLabel: string;
35
- buildRequest: () => SSERequest;
43
+ buildRequest: (overrides?: BuildRequestOverrides) => SSERequest;
36
44
  makeParser: (ctrl: StreamControls) => StreamParser;
37
45
  stallTimeouts?: {
38
46
  connectMs?: number;
@@ -1,4 +1,5 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
+ import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
2
3
  import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
3
4
  import { retryBackoffMs } from "./retry.js";
4
5
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
@@ -17,6 +18,169 @@ export function resolveMaxRetries(configured) {
17
18
  }
18
19
  return DEFAULT_MAX_RETRIES;
19
20
  }
21
+ const SHOULD_RETRY_HEADER = "x-should-retry";
22
+ function sameHeaders(a, b) {
23
+ const aKeys = Object.keys(a);
24
+ if (aKeys.length !== Object.keys(b).length)
25
+ return false;
26
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]);
27
+ }
28
+ function canonicalJson(value) {
29
+ if (Array.isArray(value))
30
+ return `[${value.map(canonicalJson).join(",")}]`;
31
+ if (typeof value === "object" && value !== null) {
32
+ const rec = value;
33
+ return `{${Object.keys(rec)
34
+ .sort()
35
+ .map((k) => `${JSON.stringify(k)}:${canonicalJson(rec[k])}`)
36
+ .join(",")}}`;
37
+ }
38
+ return JSON.stringify(value) ?? "null";
39
+ }
40
+ function leafHolder(root, path) {
41
+ const parts = path.split(".");
42
+ const leaf = parts.pop();
43
+ if (leaf === undefined)
44
+ return undefined;
45
+ let cur = root;
46
+ for (const part of parts) {
47
+ if (typeof cur !== "object" || cur === null || Array.isArray(cur))
48
+ return undefined;
49
+ cur = cur[part];
50
+ }
51
+ if (typeof cur !== "object" || cur === null || Array.isArray(cur))
52
+ return undefined;
53
+ return { holder: cur, leaf };
54
+ }
55
+ export function sameRequestModuloCap(original, rebuilt, imposedCap) {
56
+ if (rebuilt.url !== original.url || !sameHeaders(rebuilt.headers, original.headers))
57
+ return false;
58
+ if (rebuilt.outputCapTokens !== imposedCap)
59
+ return false;
60
+ if (rebuilt.body === original.body)
61
+ return true;
62
+ const parse = (raw) => {
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(raw);
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
71
+ return undefined;
72
+ return parsed;
73
+ };
74
+ const a = parse(original.body);
75
+ const b = parse(rebuilt.body);
76
+ if (a === undefined || b === undefined)
77
+ return false;
78
+ for (const path of new Set([...(original.capDerivedPaths ?? []), ...(rebuilt.capDerivedPaths ?? [])])) {
79
+ const from = leafHolder(a, path);
80
+ const to = leafHolder(b, path);
81
+ const before = from === undefined ? undefined : from.holder[from.leaf];
82
+ const after = to === undefined ? undefined : to.holder[to.leaf];
83
+ if (before !== undefined) {
84
+ if (typeof before !== "number" || typeof after !== "number")
85
+ return false;
86
+ if (after > before)
87
+ return false;
88
+ }
89
+ if (from !== undefined)
90
+ delete from.holder[from.leaf];
91
+ if (to !== undefined)
92
+ delete to.holder[to.leaf];
93
+ }
94
+ return canonicalJson(a) === canonicalJson(b);
95
+ }
96
+ const ERROR_BODY_BYTE_CAP = 64 * 1024;
97
+ const ERROR_BODY_READ_TIMEOUT_MS = 5_000;
98
+ async function readErrorBody(res, signal) {
99
+ const body = res.body;
100
+ if (body === null)
101
+ return { text: "", endedNormally: true };
102
+ let reader;
103
+ try {
104
+ reader = body.getReader();
105
+ }
106
+ catch {
107
+ return { text: "", endedNormally: false };
108
+ }
109
+ const decoder = new TextDecoder();
110
+ let stopped = false;
111
+ const stop = () => {
112
+ stopped = true;
113
+ void reader.cancel().catch(() => undefined);
114
+ };
115
+ let timer;
116
+ let out = "";
117
+ let endedNormally = false;
118
+ try {
119
+ timer = setTimeout(stop, ERROR_BODY_READ_TIMEOUT_MS);
120
+ signal?.addEventListener("abort", stop, { once: true });
121
+ if (signal?.aborted === true)
122
+ stop();
123
+ let bytes = 0;
124
+ for (;;) {
125
+ const chunk = await reader.read();
126
+ if (chunk.done) {
127
+ endedNormally = !stopped;
128
+ break;
129
+ }
130
+ const value = chunk.value;
131
+ if (value === undefined)
132
+ continue;
133
+ const room = ERROR_BODY_BYTE_CAP - bytes;
134
+ const usable = value.byteLength <= room ? value : value.subarray(0, room);
135
+ bytes += usable.byteLength;
136
+ out += decoder.decode(usable, { stream: true });
137
+ if (bytes >= ERROR_BODY_BYTE_CAP) {
138
+ stop();
139
+ break;
140
+ }
141
+ }
142
+ out += decoder.decode();
143
+ }
144
+ catch {
145
+ endedNormally = false;
146
+ }
147
+ finally {
148
+ if (timer !== undefined)
149
+ clearTimeout(timer);
150
+ signal?.removeEventListener("abort", stop);
151
+ stop();
152
+ }
153
+ return { text: out, endedNormally };
154
+ }
155
+ function discardResponseBody(res) {
156
+ const body = res?.body;
157
+ if (body === undefined || body === null || body.locked)
158
+ return;
159
+ try {
160
+ void body.cancel().catch(() => undefined);
161
+ }
162
+ catch {
163
+ }
164
+ }
165
+ export function shouldRetryHeaderVerdict(res) {
166
+ if (res === undefined)
167
+ return undefined;
168
+ let raw = null;
169
+ try {
170
+ const headers = res.headers;
171
+ if (headers === undefined || headers === null || typeof headers.get !== "function")
172
+ return undefined;
173
+ raw = res.headers.get(SHOULD_RETRY_HEADER);
174
+ }
175
+ catch {
176
+ return undefined;
177
+ }
178
+ if (raw === "true")
179
+ return true;
180
+ if (raw === "false")
181
+ return false;
182
+ return undefined;
183
+ }
20
184
  const RETRY_STATUS_SLICE_MS = 30_000;
21
185
  const THINKING_RETRY_BUDGET = 2;
22
186
  const EMPTY_USAGE = {
@@ -103,7 +267,9 @@ export function runStreamingBrain(args) {
103
267
  });
104
268
  return out;
105
269
  async function run() {
106
- const req = buildRequest();
270
+ let req = buildRequest();
271
+ req.onCommitted?.();
272
+ let maxOutputTokensOverride;
107
273
  const maxRetries = resolveMaxRetries(config.maxRetries);
108
274
  const baseDelay = config.retryDelayMs ?? 500;
109
275
  const firstTokenTimeoutMs = resolveStallTimeoutMs(config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs, "firstTokenTimeoutMs");
@@ -146,33 +312,96 @@ export function runStreamingBrain(args) {
146
312
  res = r;
147
313
  break;
148
314
  }
149
- cc.dispose();
150
- const retryable = netErr !== undefined ||
151
- (r !== undefined && (r.status >= 500 || r.status === 429 || r.status === 408 || r.status === 409));
152
- if (retryable && attempt < maxRetries) {
153
- const delayMs = retryBackoffMs(baseDelay, attempt, r);
154
- const statusPhase = r?.status === 429 ? "rate_limited" : netErr !== undefined ? "reconnecting" : "retrying";
155
- emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect" });
156
- announcedRetry = true;
157
- await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
158
- phase: statusPhase,
159
- detail: statusPhase === "rate_limited"
160
- ? "rate limited, backing off"
161
- : statusPhase === "reconnecting"
162
- ? "connection lost, reconnecting"
163
- : "transient error, retrying",
164
- retryInSec: Math.ceil(remainingMs / 1000),
165
- retryInMs: remainingMs,
166
- attempt: attempt + 1,
167
- maxRetries,
168
- }));
169
- continue;
315
+ cc.clearTimer();
316
+ try {
317
+ let overflowBody;
318
+ let overflowDeclineNote = "";
319
+ if (r !== undefined && r.status === 400) {
320
+ const read = await readErrorBody(r, signal);
321
+ overflowBody = read.text;
322
+ const report = read.endedNormally ? parseContextOverflow(r.status, read.text) : undefined;
323
+ if (report !== undefined) {
324
+ const plan = planOutputCapAdjustment(report, [maxOutputTokensOverride, req.outputCapTokens]);
325
+ if (plan.outcome === "adjust" && attempt < maxRetries) {
326
+ let rebuilt;
327
+ try {
328
+ rebuilt = buildRequest({ maxOutputTokens: plan.maxOutputTokens });
329
+ }
330
+ catch {
331
+ rebuilt = undefined;
332
+ }
333
+ let acceptable = false;
334
+ try {
335
+ acceptable = rebuilt !== undefined && sameRequestModuloCap(req, rebuilt, plan.maxOutputTokens);
336
+ }
337
+ catch {
338
+ acceptable = false;
339
+ }
340
+ if (acceptable && rebuilt !== undefined) {
341
+ const previousCap = plan.current;
342
+ maxOutputTokensOverride = plan.maxOutputTokens;
343
+ req = rebuilt;
344
+ req.onCommitted?.();
345
+ emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect" });
346
+ announcedRetry = true;
347
+ emitBrainStatus({
348
+ phase: "retrying",
349
+ detail: `output limit lowered from ${previousCap} to ${plan.maxOutputTokens} tokens, retrying`,
350
+ attempt: attempt + 1,
351
+ maxRetries,
352
+ });
353
+ continue;
354
+ }
355
+ overflowDeclineNote =
356
+ rebuilt === undefined
357
+ ? ` — output-cap recovery declined: the request could not be rebuilt with a smaller cap`
358
+ : ` — output-cap recovery declined: rebuilding the request changed more than the output cap (its destination, headers, or some other body field moved), so this would no longer be the same request with a smaller cap`;
359
+ }
360
+ else {
361
+ overflowDeclineNote =
362
+ plan.outcome === "below_floor"
363
+ ? ` — output-cap recovery declined: only ${plan.available} tokens are available under the context limit, below the ${FLOOR_OUTPUT_TOKENS}-token floor; the input itself must shrink`
364
+ : plan.outcome === "no_progress"
365
+ ? ` — output-cap recovery declined: the recomputed cap (${plan.available}) is not below the ${plan.current} already in effect, so re-sending cannot help`
366
+ : ` — output-cap recovery declined: the retry budget (${maxRetries}) is exhausted`;
367
+ }
368
+ }
369
+ }
370
+ const retryableByStatus = netErr !== undefined ||
371
+ (r !== undefined && (r.status >= 500 || r.status === 429 || r.status === 408 || r.status === 409));
372
+ const retryable = shouldRetryHeaderVerdict(r) ?? retryableByStatus;
373
+ if (retryable && attempt < maxRetries) {
374
+ const delayMs = retryBackoffMs(baseDelay, attempt, r);
375
+ const statusPhase = r?.status === 429 ? "rate_limited" : netErr !== undefined ? "reconnecting" : "retrying";
376
+ emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect" });
377
+ announcedRetry = true;
378
+ discardResponseBody(r);
379
+ cc.abort();
380
+ cc.dispose();
381
+ await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
382
+ phase: statusPhase,
383
+ detail: statusPhase === "rate_limited"
384
+ ? "rate limited, backing off"
385
+ : statusPhase === "reconnecting"
386
+ ? "connection lost, reconnecting"
387
+ : "transient error, retrying",
388
+ retryInSec: Math.ceil(remainingMs / 1000),
389
+ retryInMs: remainingMs,
390
+ attempt: attempt + 1,
391
+ maxRetries,
392
+ }));
393
+ continue;
394
+ }
395
+ if (netErr)
396
+ throw new BrainError("network", describeNetworkError(netErr));
397
+ const detail = overflowBody ?? (r ? (await readErrorBody(r, signal)).text : "");
398
+ const status = r?.status ?? 0;
399
+ throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}${overflowDeclineNote}`, status);
400
+ }
401
+ finally {
402
+ cc.abort();
403
+ cc.dispose();
170
404
  }
171
- if (netErr)
172
- throw new BrainError("network", describeNetworkError(netErr));
173
- const detail = r ? await r.text().catch(() => "") : "";
174
- const status = r?.status ?? 0;
175
- throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}`, status);
176
405
  }
177
406
  const partial = emptyAssistant(model);
178
407
  if (!startEmitted) {
@@ -12,6 +12,7 @@ export declare function createConnectController(connectTimeoutMs: number | undef
12
12
  signal: AbortSignal;
13
13
  timedOut: () => boolean;
14
14
  clearTimer: () => void;
15
+ abort: () => void;
15
16
  dispose: () => void;
16
17
  };
17
18
  export declare const BRAIN_CALL_GUARDRAIL_DEFAULT_MS = 3600000;
@@ -37,6 +37,7 @@ export function createConnectController(connectTimeoutMs, outerSignal) {
37
37
  signal: ac.signal,
38
38
  timedOut: () => timedOut,
39
39
  clearTimer,
40
+ abort: () => ac.abort(),
40
41
  dispose: () => {
41
42
  clearTimer();
42
43
  if (outerSignal)
@@ -3,8 +3,8 @@ import type { A2aServerSpec, ToolEffect } from "./types.js";
3
3
  export declare const A2A_SKILL_DESCRIPTION_MAX_CHARS = 2048;
4
4
  export interface A2aToolAxis {
5
5
  name: string;
6
- irreversibility?: "always";
7
- egress?: true;
6
+ irreversibility?: "always" | "never";
7
+ egress?: boolean;
8
8
  effect?: ToolEffect;
9
9
  }
10
10
  export interface A2aPeerStatus {
package/dist/core/a2a.js CHANGED
@@ -325,9 +325,9 @@ function a2aAxisFor(name, override) {
325
325
  if (override.effect !== undefined)
326
326
  axis.effect = override.effect;
327
327
  if (override.egress === false)
328
- delete axis.egress;
329
- if (override.irreversibility === "always")
330
- axis.irreversibility = "always";
328
+ axis.egress = false;
329
+ if (override.irreversibility !== undefined)
330
+ axis.irreversibility = override.irreversibility;
331
331
  return axis;
332
332
  }
333
333
  function renderParts(parts) {
@@ -17,6 +17,8 @@ export interface AskQuestionRequest {
17
17
  questions: AskQuestion[];
18
18
  readonly principal?: string;
19
19
  readonly sourceTaskId?: string;
20
+ readonly boundInputHash?: string;
21
+ readonly deliveryId?: string;
20
22
  }
21
23
  export interface QuestionAnswerItem {
22
24
  header: string;
@@ -26,10 +28,53 @@ export interface QuestionAnswerItem {
26
28
  export interface QuestionAnswer {
27
29
  answers: QuestionAnswerItem[];
28
30
  }
29
- export type OnQuestion = (req: AskQuestionRequest, signal?: AbortSignal) => Promise<QuestionAnswer>;
31
+ export interface QuestionUnavailable {
32
+ readonly kind: "unavailable";
33
+ }
34
+ export type OnQuestionOutcome = QuestionAnswer | QuestionUnavailable;
35
+ export type QuestionOutcomeReading = {
36
+ readonly shape: "answered";
37
+ readonly answer: QuestionAnswer;
38
+ } | {
39
+ readonly shape: "unavailable";
40
+ } | {
41
+ readonly shape: "contradictory";
42
+ };
43
+ export declare function classifyQuestionOutcome(outcome: OnQuestionOutcome): QuestionOutcomeReading;
44
+ export declare function canonicalizeCapturedPlainData(node: unknown, seen?: WeakSet<object>): boolean;
45
+ export declare function isQuestionUnavailable(outcome: OnQuestionOutcome): outcome is QuestionUnavailable;
46
+ export type OnQuestion = (req: AskQuestionRequest, signal?: AbortSignal) => Promise<OnQuestionOutcome>;
47
+ export type SyntheticContinuationReason = "seam_absent" | "callback_failed" | "declined_unavailable";
48
+ export type AskAnswerContinuationSource = "human_response" | "synthetic_self_answer_instruction";
49
+ export type AskQuestionCardDetails = {
50
+ type: "ask-question";
51
+ questionId: string;
52
+ continuationSource: AskAnswerContinuationSource;
53
+ runContinues: true;
54
+ reason?: SyntheticContinuationReason;
55
+ } | {
56
+ type: "ask-question";
57
+ questionId: string;
58
+ code: "question.human_channel_failed" | "question.human_unavailable";
59
+ reason: "callback_failed" | "declined_unavailable";
60
+ };
61
+ export declare function askQuestionContinuationCard(questionId: string, continuationSource: AskAnswerContinuationSource, reason?: SyntheticContinuationReason): AskQuestionCardDetails;
62
+ export declare function validateAskQuestions(questions: unknown): string | undefined;
63
+ export interface AskUserQuestionToolOptions {
64
+ posture?: "interactive" | "headless";
65
+ interactiveFallback?: boolean;
66
+ onSyntheticContinuation?: (info: {
67
+ questionId: string;
68
+ reason: SyntheticContinuationReason;
69
+ }) => void;
70
+ redeemedApprovalCallId?: string;
71
+ redeemedApprovalQuestionsHash?: string;
72
+ }
30
73
  export declare function createDurableQuestionPolicy(): ToolPolicy;
31
74
  export declare const QUESTION_AWAITS_RESUME: OnQuestion;
75
+ export declare function markBoundOnlyQuestionFace<F extends OnQuestion>(face: F): F;
76
+ export declare function isLiveQuestionFace(face: unknown): face is OnQuestion;
32
77
  export declare function createAskUserQuestionTool(onQuestion?: OnQuestion, source?: {
33
78
  principal?: string;
34
79
  sourceTaskId?: string;
35
- }): AgentTool;
80
+ }, opts?: AskUserQuestionToolOptions): AgentTool;