@sema-agent/core 2.8.0 → 2.10.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 (44) hide show
  1. package/dist/agents/send-message-tool.js +37 -29
  2. package/dist/agents/subagent.js +91 -4
  3. package/dist/brain/circuit-breaker.js +18 -8
  4. package/dist/brain/retry.d.ts +1 -0
  5. package/dist/brain/retry.js +29 -7
  6. package/dist/brain/stream-engine.d.ts +1 -0
  7. package/dist/brain/stream-engine.js +74 -12
  8. package/dist/config/defaults.d.ts +1 -0
  9. package/dist/config/defaults.js +1 -0
  10. package/dist/core/auto-compaction.js +9 -1
  11. package/dist/core/background-agent-store.d.ts +2 -0
  12. package/dist/core/background-agent-store.js +20 -0
  13. package/dist/core/mcp.js +8 -5
  14. package/dist/core/runner/assemble-result.d.ts +1 -0
  15. package/dist/core/runner/assemble-result.js +1 -1
  16. package/dist/core/runner/prepare-task.d.ts +2 -1
  17. package/dist/core/runner/prepare-task.js +61 -20
  18. package/dist/core/runner/runtask.js +38 -12
  19. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  20. package/dist/core/runner/tool-disclosure.js +39 -10
  21. package/dist/core/skills-directory.d.ts +1 -1
  22. package/dist/core/skills-directory.js +257 -28
  23. package/dist/core/task-registry-agent.d.ts +2 -1
  24. package/dist/core/task-registry-agent.js +50 -54
  25. package/dist/core/task-registry.d.ts +1 -0
  26. package/dist/core/task-registry.js +1 -1
  27. package/dist/core/types.d.ts +9 -1
  28. package/dist/engine/compaction/compaction.js +71 -20
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/internal/harness-types.d.ts +1 -1
  32. package/dist/internal/harness.d.ts +1 -1
  33. package/dist/internal/harness.js +1 -1
  34. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  35. package/dist/orchestration/run-workflow-tool.js +4 -1
  36. package/dist/orchestration/workflow.d.ts +1 -1
  37. package/dist/orchestration/workflow.js +20 -12
  38. package/dist/tools/fs/bash-readonly-classifier.js +83 -17
  39. package/dist/tools/fs/fs-bash.js +17 -11
  40. package/dist/tools/fs/fs-shared.d.ts +1 -0
  41. package/dist/tools/fs/fs-shared.js +44 -2
  42. package/dist/tools/web.d.ts +15 -0
  43. package/dist/tools/web.js +42 -0
  44. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+ import { canonicalToolName } from "./tool-name-aliases.js";
3
4
  const SKILL_FILE = "SKILL.md";
4
5
  const RESOURCE_DIRS = ["assets", "references", "scripts"];
5
6
  const NAME_MAX_CHARS = 64;
@@ -22,26 +23,139 @@ function parseSkillFile(text) {
22
23
  }
23
24
  if (end === -1)
24
25
  return { fields, body: normalized, hadFrontmatter: false };
25
- for (let i = 1; i < end; i++) {
26
+ let i = 1;
27
+ while (i < end) {
26
28
  const line = lines[i];
27
29
  const trimmed = line.trim();
28
- if (trimmed === "" || trimmed.startsWith("#"))
30
+ if (trimmed === "" || trimmed.startsWith("#")) {
31
+ i++;
29
32
  continue;
30
- if (/^\s/.test(line))
33
+ }
34
+ if (/^\s/.test(line)) {
35
+ i++;
31
36
  continue;
37
+ }
32
38
  const kv = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(trimmed);
33
- if (!kv)
39
+ if (!kv) {
40
+ i++;
34
41
  continue;
42
+ }
35
43
  const [, key, raw] = kv;
44
+ const rawValue = raw.trim();
45
+ const block = parseBlockScalarHeader(rawValue);
46
+ if (block !== undefined) {
47
+ const read = readBlockScalar(lines, i + 1, end, block);
48
+ i = read.next;
49
+ if (!fields.has(key))
50
+ fields.set(key, read.value);
51
+ continue;
52
+ }
53
+ if (rawValue.startsWith("[") && !isFlowSequenceClosed(rawValue)) {
54
+ const joined = readFlowSequenceContinuation(lines, i + 1, end, rawValue);
55
+ i = joined.next;
56
+ if (!fields.has(key))
57
+ fields.set(key, joined.value);
58
+ continue;
59
+ }
60
+ i++;
36
61
  if (fields.has(key))
37
62
  continue;
38
- fields.set(key, unquote(raw.trim()));
63
+ fields.set(key, unquote(rawValue));
39
64
  }
40
65
  let body = lines.slice(end + 1).join("\n");
41
66
  if (body.startsWith("\n"))
42
67
  body = body.slice(1);
43
68
  return { fields, body, hadFrontmatter: true };
44
69
  }
70
+ function parseBlockScalarHeader(value) {
71
+ const m = /^([|>])([0-9+-]*)$/.exec(value);
72
+ if (!m)
73
+ return undefined;
74
+ const rest = m[2];
75
+ if (!/^(?:[1-9]?[+-]?|[+-][1-9]?)$/.test(rest))
76
+ return undefined;
77
+ const digit = /[1-9]/.exec(rest)?.[0];
78
+ return { style: m[1] === "|" ? "literal" : "folded", ...(digit !== undefined ? { indent: Number(digit) } : {}) };
79
+ }
80
+ function readBlockScalar(lines, start, end, header) {
81
+ const indentOf = (s) => /^[ \t]*/.exec(s)[0].length;
82
+ let indent = header.indent;
83
+ if (indent === undefined) {
84
+ for (let j = start; j < end; j++) {
85
+ if (lines[j].trim() === "")
86
+ continue;
87
+ indent = indentOf(lines[j]);
88
+ break;
89
+ }
90
+ }
91
+ if (indent === undefined || indent === 0)
92
+ return { value: "", next: start };
93
+ const taken = [];
94
+ let j = start;
95
+ for (; j < end; j++) {
96
+ const line = lines[j];
97
+ if (line.trim() === "") {
98
+ taken.push("");
99
+ continue;
100
+ }
101
+ if (indentOf(line) < indent)
102
+ break;
103
+ taken.push(line.slice(indent));
104
+ }
105
+ while (taken.length > 0 && taken[taken.length - 1] === "")
106
+ taken.pop();
107
+ if (header.style === "literal")
108
+ return { value: taken.join("\n"), next: j };
109
+ let folded = "";
110
+ let blanks = 0;
111
+ let started = false;
112
+ let prevMoreIndented = false;
113
+ for (const cur of taken) {
114
+ if (cur === "") {
115
+ blanks++;
116
+ continue;
117
+ }
118
+ const moreIndented = indentOf(cur) > 0;
119
+ if (!started) {
120
+ folded = cur;
121
+ started = true;
122
+ }
123
+ else if (blanks > 0) {
124
+ folded += "\n".repeat(blanks) + cur;
125
+ }
126
+ else if (moreIndented || prevMoreIndented) {
127
+ folded += `\n${cur}`;
128
+ }
129
+ else {
130
+ folded += ` ${cur}`;
131
+ }
132
+ blanks = 0;
133
+ prevMoreIndented = moreIndented;
134
+ }
135
+ return { value: folded, next: j };
136
+ }
137
+ function isFlowSequenceClosed(text) {
138
+ let depth = 0;
139
+ for (const ch of text) {
140
+ if (ch === "[")
141
+ depth++;
142
+ else if (ch === "]")
143
+ depth--;
144
+ }
145
+ return depth <= 0;
146
+ }
147
+ function readFlowSequenceContinuation(lines, start, end, head) {
148
+ let acc = head;
149
+ let j = start;
150
+ for (; j < end; j++) {
151
+ acc += ` ${lines[j].trim()}`;
152
+ if (isFlowSequenceClosed(acc)) {
153
+ j++;
154
+ break;
155
+ }
156
+ }
157
+ return { value: acc, next: j };
158
+ }
45
159
  function unquote(value) {
46
160
  if (value.length < 2)
47
161
  return value;
@@ -51,7 +165,13 @@ function unquote(value) {
51
165
  const inner = value.slice(1, -1);
52
166
  return q === '"' ? inner.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : inner;
53
167
  }
54
- function listRelativeFiles(base, dir, prefix, out) {
168
+ function isInsideRoot(root, p) {
169
+ if (p === root)
170
+ return true;
171
+ const rel = relative(root, p);
172
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
173
+ }
174
+ function listRelativeFiles(base, dir, prefix, out, walk) {
55
175
  let entries;
56
176
  try {
57
177
  entries = readdirSync(join(base, dir), { withFileTypes: true });
@@ -61,24 +181,68 @@ function listRelativeFiles(base, dir, prefix, out) {
61
181
  }
62
182
  for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
63
183
  const rel = `${prefix}${e.name}`;
64
- if (e.isDirectory())
65
- listRelativeFiles(base, join(dir, e.name), `${rel}/`, out);
66
- else if (e.isFile())
184
+ const abs = join(base, dir, e.name);
185
+ const resolved = resolveInsideSkill(abs, rel, walk);
186
+ if (resolved === undefined)
187
+ continue;
188
+ if (resolved.stats.isDirectory()) {
189
+ if (walk.visited.has(resolved.real))
190
+ continue;
191
+ walk.visited.add(resolved.real);
192
+ listRelativeFiles(base, join(dir, e.name), `${rel}/`, out, walk);
193
+ }
194
+ else if (resolved.stats.isFile()) {
67
195
  out.push(rel);
196
+ }
197
+ }
198
+ }
199
+ function resolveInsideSkill(abs, rel, walk) {
200
+ let real;
201
+ let stats;
202
+ try {
203
+ real = realpathSync(abs);
204
+ stats = statSync(abs);
205
+ }
206
+ catch (err) {
207
+ walk.warn({ code: "attachment_skipped", skill: walk.skill, detail: `${rel}: could not be resolved (${errText(err)})` });
208
+ return undefined;
209
+ }
210
+ if (!isInsideRoot(walk.realRoot, real)) {
211
+ walk.warn({
212
+ code: "attachment_skipped",
213
+ skill: walk.skill,
214
+ detail: `${rel}: resolves outside the skill directory — skipped (attachments are collected from the declared skill directory only)`,
215
+ });
216
+ return undefined;
68
217
  }
218
+ return { real, stats };
69
219
  }
70
220
  function readAttachments(skillDir, skillName, budgetBytes, warn) {
221
+ let realRoot;
222
+ try {
223
+ realRoot = realpathSync(skillDir);
224
+ }
225
+ catch (err) {
226
+ warn({ code: "read_failed", skill: skillName, detail: `the skill directory could not be resolved (${errText(err)}) — attachments skipped` });
227
+ return [];
228
+ }
229
+ const walk = { realRoot, skill: skillName, warn, visited: new Set([realRoot]) };
71
230
  const relPaths = [];
72
231
  for (const d of RESOURCE_DIRS) {
73
- let st;
232
+ let exists = true;
74
233
  try {
75
- st = statSync(join(skillDir, d));
234
+ statSync(join(skillDir, d));
76
235
  }
77
236
  catch {
78
- continue;
237
+ exists = false;
79
238
  }
80
- if (st.isDirectory())
81
- listRelativeFiles(skillDir, d, `${d}/`, relPaths);
239
+ if (!exists)
240
+ continue;
241
+ const resolved = resolveInsideSkill(join(skillDir, d), d, walk);
242
+ if (resolved === undefined || !resolved.stats.isDirectory())
243
+ continue;
244
+ walk.visited.add(resolved.real);
245
+ listRelativeFiles(skillDir, d, `${d}/`, relPaths, walk);
82
246
  }
83
247
  relPaths.sort();
84
248
  const files = [];
@@ -114,20 +278,60 @@ function isDecodableText(bytes) {
114
278
  function errText(err) {
115
279
  return err instanceof Error ? err.message : String(err);
116
280
  }
117
- function manifestFromAllowedTools(declared, skillName, deployedTools, warn) {
281
+ const ALL_TOOLS_WILDCARD = "*";
282
+ function splitToolNames(declared) {
283
+ let text = declared.trim();
284
+ if (text.startsWith("[") && text.endsWith("]"))
285
+ text = text.slice(1, -1);
118
286
  const names = [];
119
- for (const n of declared.split(/\s+/)) {
120
- if (n !== "" && !names.includes(n))
121
- names.push(n);
287
+ let current = "";
288
+ let depth = 0;
289
+ const flush = () => {
290
+ const name = unquote(current.trim());
291
+ if (name !== "")
292
+ names.push(name);
293
+ current = "";
294
+ };
295
+ for (const ch of text) {
296
+ if (ch === "(") {
297
+ depth++;
298
+ current += ch;
299
+ }
300
+ else if (ch === ")") {
301
+ if (depth > 0)
302
+ depth--;
303
+ current += ch;
304
+ }
305
+ else if (depth === 0 && (ch === "," || /\s/.test(ch))) {
306
+ flush();
307
+ }
308
+ else {
309
+ current += ch;
310
+ }
311
+ }
312
+ flush();
313
+ return names;
314
+ }
315
+ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools, warn) {
316
+ const names = [];
317
+ const seen = new Set();
318
+ for (const n of splitToolNames(declared)) {
319
+ const key = canonicalToolName(n);
320
+ if (seen.has(key))
321
+ continue;
322
+ seen.add(key);
323
+ names.push(n);
122
324
  }
123
325
  if (names.length === 0)
124
326
  return undefined;
327
+ if (names.some((n) => n === ALL_TOOLS_WILDCARD))
328
+ return undefined;
125
329
  let allowTools = names;
126
330
  if (deployedTools !== undefined) {
127
- const mounted = new Set(deployedTools);
128
- allowTools = names.filter((n) => mounted.has(n));
331
+ const mounted = new Set(deployedTools.map(canonicalToolName));
332
+ allowTools = names.filter((n) => mounted.has(canonicalToolName(n)));
129
333
  for (const n of names) {
130
- if (!mounted.has(n)) {
334
+ if (!mounted.has(canonicalToolName(n))) {
131
335
  warn({
132
336
  code: "allowed_tool_not_mounted",
133
337
  skill: skillName,
@@ -136,6 +340,10 @@ function manifestFromAllowedTools(declared, skillName, deployedTools, warn) {
136
340
  }
137
341
  }
138
342
  }
343
+ if (disallowed.length > 0) {
344
+ const denied = new Set(disallowed.map(canonicalToolName));
345
+ allowTools = allowTools.filter((n) => !denied.has(canonicalToolName(n)));
346
+ }
139
347
  return { allowTools, lineageId: `skill:${skillName}` };
140
348
  }
141
349
  export function createSkillsFromDirectory(dir, options = {}) {
@@ -153,10 +361,23 @@ export function createSkillsFromDirectory(dir, options = {}) {
153
361
  catch (err) {
154
362
  throw new Error(`skills directory could not be read: ${dir} (${errText(err)})`);
155
363
  }
156
- const dirNames = entries
157
- .filter((e) => e.isDirectory())
158
- .map((e) => e.name)
159
- .sort();
364
+ const dirNames = [];
365
+ for (const e of entries) {
366
+ if (e.isDirectory()) {
367
+ dirNames.push(e.name);
368
+ continue;
369
+ }
370
+ if (!e.isSymbolicLink())
371
+ continue;
372
+ try {
373
+ if (statSync(join(dir, e.name)).isDirectory())
374
+ dirNames.push(e.name);
375
+ }
376
+ catch (err) {
377
+ warn({ code: "read_failed", skill: e.name, detail: `entry is a symbolic link that could not be resolved (${errText(err)}) — skipped` });
378
+ }
379
+ }
380
+ dirNames.sort();
160
381
  const skills = [];
161
382
  for (const name of dirNames) {
162
383
  const skillDir = join(dir, name);
@@ -200,7 +421,15 @@ export function createSkillsFromDirectory(dir, options = {}) {
200
421
  continue;
201
422
  }
202
423
  const allowed = parsed.fields.get("allowed-tools");
203
- const manifest = allowed === undefined ? undefined : manifestFromAllowedTools(allowed, name, options.deployedTools, warn);
424
+ const disallowed = splitToolNames(parsed.fields.get("disallowed-tools") ?? "");
425
+ const manifest = allowed === undefined ? undefined : manifestFromAllowedTools(allowed, disallowed, name, options.deployedTools, warn);
426
+ if (disallowed.length > 0 && manifest === undefined) {
427
+ warn({
428
+ code: "disallowed_tools_unenforced",
429
+ skill: name,
430
+ detail: `disallowed-tools names ${disallowed.map((n) => `"${n}"`).join(", ")}, but this skill produced no allowlist to subtract from (allowed-tools is absent or claims every tool) — the deny declaration is NOT enforced (express it in the task's own tool policy)`,
431
+ });
432
+ }
204
433
  const files = readAttachments(skillDir, name, budget, warn);
205
434
  skills.push({
206
435
  name: declaredName,
@@ -2,7 +2,7 @@ import { type BackgroundAgentRecord, type BackgroundAgentStore } from "./backgro
2
2
  import { type StopSource, type TaskAccess, type UnifiedTaskResult, type BackgroundAgentTaskHandle, type DurableAgentCore, type ParkedClaimTicket, type RegisterBackgroundAgentInput } from "./task-registry-shared.js";
3
3
  import { type ToolResultStore } from "./tool-result-store.js";
4
4
  export declare function ensureDurableHeartbeatLane(core: DurableAgentCore): void;
5
- export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: (keyof BackgroundAgentRecord)[]): void;
5
+ export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: readonly (keyof BackgroundAgentRecord)[]): void;
6
6
  export declare function durableAgentArmedLane(core: DurableAgentCore, id: string): boolean;
7
7
  export declare function durableAgentRowProbeLane(core: DurableAgentCore, id: string): (() => Promise<boolean>) | undefined;
8
8
  export declare function beginDurableClaimLane(core: DurableAgentCore, id: string): boolean;
@@ -71,6 +71,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
71
71
  errorKind?: string;
72
72
  stoppedBy?: StopSource;
73
73
  seq?: number;
74
+ cycle?: number;
74
75
  }): "completed" | "failed" | "killed" | undefined;
75
76
  export declare function abortBackgroundAgentsForOwnerLane(core: DurableAgentCore, access: TaskAccess, opts?: {
76
77
  skipSessionScoped?: boolean;
@@ -1,6 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
- import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
3
+ import { canAccessAgentRecord, BackgroundAgentStoreError, REVIVED_ROW_CLEARED_FIELDS, } from "./background-agent-store.js";
4
4
  import { shutdownDebug } from "./shutdown-debug.js";
5
5
  import { delimitUntrusted } from "./untrusted-text.js";
6
6
  import { boundedRedactedSummary } from "./untrusted-egress.js";
@@ -652,6 +652,11 @@ export function settleBackgroundAgentLane(core, id, outcome) {
652
652
  const handle = core.handles.get(id);
653
653
  if (!handle || handle.type !== "background_agent")
654
654
  return undefined;
655
+ if (outcome.cycle === undefined && (handle.reviveCycle ?? 0) !== 0) {
656
+ throw new Error(`settleBackgroundAgent("${id}"): the row has been revived (cycle ${handle.reviveCycle}) — pass the cycle this settle speaks for, or use settleRevivedAgent`);
657
+ }
658
+ if ((outcome.cycle ?? 0) !== (handle.reviveCycle ?? 0))
659
+ return undefined;
655
660
  if (handle.status !== "running") {
656
661
  if (handle.status === "killed") {
657
662
  mintCompletionId(handle);
@@ -859,31 +864,11 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
859
864
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
860
865
  handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
861
866
  handle.updatedAt = Date.now();
862
- durableAgentWriteLane(handle, { status: "running" }, [
863
- "settledAt",
864
- "stoppedBy",
865
- "finalOutput",
866
- "finalOutputFull",
867
- "error",
868
- "errorCode",
869
- "errorRetryable",
870
- "errorKind",
871
- "resultIsPartial",
872
- "completionId",
873
- "summary",
874
- "recentSteps",
875
- "editedFiles",
876
- "usage",
877
- ]);
867
+ durableAgentWriteLane(handle, { status: "running" }, REVIVED_ROW_CLEARED_FIELDS);
878
868
  return { ok: true, cycle: handle.reviveCycle };
879
869
  }
880
870
  export function settleRevivedAgentLane(core, id, cycle, outcome) {
881
- const handle = core.handles.get(id);
882
- if (!handle || handle.type !== "background_agent")
883
- return undefined;
884
- if ((handle.reviveCycle ?? 0) !== cycle)
885
- return undefined;
886
- return settleBackgroundAgentLane(core, id, outcome);
871
+ return settleBackgroundAgentLane(core, id, { ...outcome, cycle });
887
872
  }
888
873
  export function unmarkRetainedContinuationLane(core, id) {
889
874
  const handle = core.handles.get(id);
@@ -1023,18 +1008,33 @@ export function notFoundRunningAgentsTail(footer) {
1023
1008
  return ((footer.named.length > 0 ? `. Running named agents: ${footer.named.join(", ")}` : "") +
1024
1009
  (footer.background.length > 0 ? `. Running background agents: ${footer.background.join(", ")}` : ""));
1025
1010
  }
1011
+ function buildAgentPollDetails(input) {
1012
+ const failed = input.status === "failed";
1013
+ return {
1014
+ task_id: input.taskId,
1015
+ type: "background_agent",
1016
+ status: input.status,
1017
+ retrieval_status: input.retrievalStatus,
1018
+ ...(input.seq !== undefined ? { seq: input.seq } : {}),
1019
+ ...(input.status === "killed" && input.stoppedBy !== undefined ? { stoppedBy: input.stoppedBy } : {}),
1020
+ ...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
1021
+ ...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
1022
+ ...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
1023
+ ...(input.resultIsPartial === true ? { partial_result: true } : {}),
1024
+ ...(input.completionId !== undefined ? { completionId: input.completionId } : {}),
1025
+ };
1026
+ }
1026
1027
  export function serveDurableAgentRowLane(row) {
1027
1028
  if (row.status === "parked") {
1028
1029
  return {
1029
1030
  content: delimitUntrusted(`TaskOutput ${row.handle}`, `status: parked
1030
1031
  The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
1031
- details: {
1032
- task_id: row.handle,
1033
- type: "background_agent",
1032
+ details: buildAgentPollDetails({
1033
+ taskId: row.handle,
1034
1034
  status: "parked",
1035
- retrieval_status: "success",
1035
+ retrievalStatus: "success",
1036
1036
  ...(row.seq !== undefined ? { seq: row.seq } : {}),
1037
- },
1037
+ }),
1038
1038
  };
1039
1039
  }
1040
1040
  const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
@@ -1046,18 +1046,18 @@ ${row.error ? `error: ${row.error}${kindClause}
1046
1046
  ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
1047
1047
  return {
1048
1048
  content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
1049
- details: {
1050
- task_id: row.handle,
1051
- type: "background_agent",
1049
+ details: buildAgentPollDetails({
1050
+ taskId: row.handle,
1052
1051
  status: row.status,
1053
- retrieval_status: "success",
1054
- ...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1052
+ retrievalStatus: "success",
1055
1053
  ...(row.seq !== undefined ? { seq: row.seq } : {}),
1056
- ...(row.resultIsPartial ? { partial_result: true } : {}),
1054
+ ...(row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1055
+ ...(row.error !== undefined ? { error: row.error } : {}),
1056
+ ...(row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1057
+ ...(row.errorRetryable !== undefined ? { errorRetryable: row.errorRetryable } : {}),
1058
+ ...(row.resultIsPartial === true ? { resultIsPartial: true } : {}),
1057
1059
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1058
- ...(row.status === "failed" && row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1059
- ...(row.status === "failed" && row.errorRetryable !== undefined ? { retryable: row.errorRetryable } : {}),
1060
- },
1060
+ }),
1061
1061
  ...(row.status === "failed" ? { isError: true } : {}),
1062
1062
  };
1063
1063
  }
@@ -1083,13 +1083,12 @@ export async function pollBackgroundAgentLane(handle, deadline, signal, oneShot,
1083
1083
  return {
1084
1084
  content: delimitUntrusted(`TaskOutput ${handle.id}`, `status: parked
1085
1085
  The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
1086
- details: {
1087
- task_id: handle.id,
1088
- type: "background_agent",
1086
+ details: buildAgentPollDetails({
1087
+ taskId: handle.id,
1089
1088
  status: "parked",
1090
- retrieval_status: "success",
1089
+ retrievalStatus: "success",
1091
1090
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1092
- },
1091
+ }),
1093
1092
  };
1094
1093
  }
1095
1094
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
@@ -1109,21 +1108,18 @@ ${handle.error ? `error: ${handle.error}${kindClause}
1109
1108
  ${resultText}` : "(no result text)"}`;
1110
1109
  return {
1111
1110
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
1112
- details: {
1113
- task_id: handle.id,
1114
- type: "background_agent",
1111
+ details: buildAgentPollDetails({
1112
+ taskId: handle.id,
1115
1113
  status: handle.status,
1116
- retrieval_status: retrieval,
1114
+ retrievalStatus: retrieval,
1117
1115
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1118
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1119
- ...(handle.status === "failed" && handle.error !== undefined
1120
- ? { error: delimitUntrusted("agent error", boundedRedactedSummary(handle.error, 300)) }
1121
- : {}),
1122
- ...(handle.status === "failed" && handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1123
- ...(handle.status === "failed" && handle.errorRetryable !== undefined ? { retryable: handle.errorRetryable } : {}),
1124
- ...(handle.resultIsPartial ? { partial_result: true } : {}),
1116
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1117
+ ...(handle.error !== undefined ? { error: handle.error } : {}),
1118
+ ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1119
+ ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
1120
+ ...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
1125
1121
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1126
- },
1122
+ }),
1127
1123
  ...(handle.status === "failed" ? { isError: true } : {}),
1128
1124
  };
1129
1125
  }
@@ -138,6 +138,7 @@ export declare class TaskRegistry {
138
138
  errorKind?: string;
139
139
  stoppedBy?: StopSource;
140
140
  seq?: number;
141
+ cycle?: number;
141
142
  }): "completed" | "failed" | "killed" | undefined;
142
143
  abortBackgroundAgentsForOwner(access: TaskAccess, opts?: {
143
144
  skipSessionScoped?: boolean;
@@ -518,7 +518,7 @@ export class TaskRegistry {
518
518
  this.markStopSource(handle.id, "system");
519
519
  const reapTerminalNote = handle.onReapTerminal;
520
520
  handle.abort.abort();
521
- this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released" });
521
+ this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released", cycle: handle.reviveCycle ?? 0 });
522
522
  if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
523
523
  handle.terminalNotified = true;
524
524
  try {
@@ -97,6 +97,7 @@ export interface ToolExecuteContext {
97
97
  clientContext?: TaskSpec["clientContext"];
98
98
  excludeTools?: readonly string[];
99
99
  deferTools?: readonly string[];
100
+ alwaysLoadTools?: readonly string[];
100
101
  promptProfile?: "simple" | "classic";
101
102
  additionalDirectories?: readonly string[];
102
103
  envFacts?: TaskSpec["envFacts"];
@@ -389,6 +390,10 @@ export interface TaskResult {
389
390
  atTurn: number;
390
391
  };
391
392
  structuredOutput?: unknown;
393
+ rewindNotes?: Array<{
394
+ code: "conversation_only" | "files_env_unsupported" | "snapshot_store_unconfigured";
395
+ message: string;
396
+ }>;
392
397
  stats: {
393
398
  turns: number;
394
399
  tokens: number;
@@ -465,11 +470,14 @@ export interface TaskEventIdentity {
465
470
  sourceTaskId?: string;
466
471
  bgAgentId?: string;
467
472
  }
468
- export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "circuit_open";
473
+ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "circuit_open" | "recovered" | "gave_up";
469
474
  export interface BrainStatus {
470
475
  phase: BrainStatusPhase;
471
476
  detail?: string;
472
477
  retryInSec?: number;
478
+ retryInMs?: number;
479
+ attempt?: number;
480
+ maxRetries?: number;
473
481
  }
474
482
  export interface ToolActivity {
475
483
  phase: "start" | "end";