@sema-agent/core 2.4.0 → 2.6.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 (38) hide show
  1. package/dist/agents/subagent.js +17 -14
  2. package/dist/core/auto-compaction.d.ts +2 -0
  3. package/dist/core/auto-compaction.js +2 -1
  4. package/dist/core/mcp.d.ts +9 -0
  5. package/dist/core/mcp.js +60 -6
  6. package/dist/core/runner/prepare-task.d.ts +5 -0
  7. package/dist/core/runner/prepare-task.js +40 -2
  8. package/dist/core/runner/runtask.js +42 -9
  9. package/dist/core/runner/tool-disclosure.d.ts +9 -2
  10. package/dist/core/runner/tool-disclosure.js +39 -11
  11. package/dist/core/session-reconcile.d.ts +1 -0
  12. package/dist/core/session-reconcile.js +40 -20
  13. package/dist/core/skills-directory.d.ts +13 -0
  14. package/dist/core/skills-directory.js +214 -0
  15. package/dist/core/task-registry-agent.d.ts +2 -0
  16. package/dist/core/task-registry-agent.js +11 -1
  17. package/dist/core/task-registry-shared.d.ts +1 -0
  18. package/dist/core/task-registry.d.ts +2 -0
  19. package/dist/core/task-registry.js +11 -0
  20. package/dist/core/trace.d.ts +3 -0
  21. package/dist/core/types.d.ts +11 -3
  22. package/dist/engine/compaction/compaction.d.ts +5 -0
  23. package/dist/engine/compaction/compaction.js +68 -2
  24. package/dist/engine/compaction/utils.d.ts +6 -0
  25. package/dist/engine/compaction/utils.js +53 -3
  26. package/dist/engine/harness/messages.d.ts +1 -1
  27. package/dist/engine/harness/messages.js +11 -3
  28. package/dist/engine/loop/types.d.ts +2 -0
  29. package/dist/engine/session/import-validate.js +30 -1
  30. package/dist/engine/session/session.js +7 -5
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +8 -0
  33. package/dist/internal/harness.d.ts +1 -1
  34. package/dist/internal/harness.js +1 -1
  35. package/dist/orchestration/workflow-types.d.ts +1 -0
  36. package/dist/orchestration/workflow.js +10 -1
  37. package/dist/tools/fs/fs-bash.js +11 -5
  38. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { Value } from "typebox/value";
2
3
  import { defineTool } from "../tools.js";
3
4
  export const TOOL_SEARCH_NAME = "ToolSearch";
4
5
  const DEFER_AUTO_FRACTION = 0.1;
@@ -24,17 +25,24 @@ function inlinedChars(t) {
24
25
  return t.description.length + schema.length;
25
26
  }
26
27
  export function classifyDeferred(opts) {
28
+ const pinned = new Set(opts.alwaysLoadNames ?? []);
29
+ for (const s of opts.specs) {
30
+ if (s.alwaysLoad === true)
31
+ pinned.add(s.name);
32
+ }
27
33
  const deferred = new Set();
28
34
  for (const s of opts.specs) {
29
- if (s.defer === true)
35
+ if (s.defer === true && !pinned.has(s.name))
30
36
  deferred.add(s.name);
31
37
  }
32
38
  for (const name of opts.mcpToolNames)
33
- deferred.add(name);
39
+ if (!pinned.has(name))
40
+ deferred.add(name);
34
41
  for (const name of opts.deferNames ?? [])
35
- deferred.add(name);
42
+ if (!pinned.has(name))
43
+ deferred.add(name);
36
44
  if (opts.deferMode === "auto") {
37
- const candidates = opts.fullTools.filter((t) => !deferred.has(t.name));
45
+ const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name));
38
46
  const total = candidates.reduce((n, t) => n + inlinedChars(t), 0);
39
47
  const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
40
48
  if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
@@ -52,17 +60,33 @@ export function buildDeferredRegistry(deferred, tools) {
52
60
  }
53
61
  return reg;
54
62
  }
55
- export function createPlaceholderTool(info) {
63
+ export function createPlaceholderTool(info, direct) {
56
64
  const sn = safeName(info.name);
65
+ const teachingRejection = () => {
66
+ throw new Error(`Tool "${sn}" is not active yet. Call ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} ` +
67
+ `(or a keyword \`query\`) to load its full schema, then call ${sn} with the proper arguments.`);
68
+ };
69
+ if (direct !== undefined) {
70
+ return {
71
+ name: info.name,
72
+ label: info.name,
73
+ description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
74
+ parameters: EMPTY_PARAMS,
75
+ execute: async (toolCallId, params, signal) => {
76
+ if (Value.Check(direct.parameters, params)) {
77
+ await direct.activate();
78
+ return direct.invoke(toolCallId, params, signal);
79
+ }
80
+ return teachingRejection();
81
+ },
82
+ };
83
+ }
57
84
  return defineTool({
58
85
  name: info.name,
59
86
  description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
60
87
  parameters: EMPTY_PARAMS,
61
88
  effect: "read",
62
- execute: () => {
63
- throw new Error(`Tool "${sn}" is not active yet. Call ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} ` +
64
- `(or a keyword \`query\`) to load its full schema, then call ${sn} with the proper arguments.`);
65
- },
89
+ execute: () => teachingRejection(),
66
90
  });
67
91
  }
68
92
  export function scoreToolMatch(query, info) {
@@ -159,10 +183,14 @@ export function createToolSearchTool(opts) {
159
183
  name: TOOL_SEARCH_NAME,
160
184
  contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
161
185
  description: "Discover and activate deferred tools. Most tools start as name-only placeholders to keep requests " +
162
- "small; to USE one you must activate it here first. Query forms: " +
186
+ "small; to USE one you must activate it here first. When any instruction, reminder, or another " +
187
+ 'tool\'s description names a deferred tool, activate it with query "select:<name>" before calling it. ' +
188
+ "Query forms: " +
163
189
  '"select:ToolA,ToolB" — activate these exact tools by name; ' +
164
190
  '"notebook jupyter" — keyword search, up to max_results best matches; ' +
165
191
  "a bare tool name — activates that tool directly. " +
192
+ "Activate every tool you expect to need in one call (select accepts a comma-separated list) " +
193
+ "rather than one at a time. " +
166
194
  "Activated tools become callable with their full parameters on your next turn.",
167
195
  parameters: Type.Object({
168
196
  query: Type.Optional(Type.String({
@@ -182,7 +210,7 @@ export function createToolSearchTool(opts) {
182
210
  : "";
183
211
  const missingNote = callableNote +
184
212
  (missUnknown.length > 0
185
- ? `\nNot found: ${missUnknown.map((n) => safeName(n)).join(", ")} — no deferred tool has this exact name. ` +
213
+ ? `\nNot found: ${missUnknown.map((n) => safeName(n)).join(", ")} — not in the deferred registry under this exact name (lookup is case-sensitive). ` +
186
214
  "(Already-active and non-deferred tools are callable directly and don't appear here.)"
187
215
  : "");
188
216
  if (matched.length === 0) {
@@ -4,6 +4,7 @@ import type { ToolEffect } from "./types.js";
4
4
  export interface OrphanToolCall {
5
5
  toolCallId: string;
6
6
  toolName: string;
7
+ kind?: "result";
7
8
  }
8
9
  export interface ReconcileReport {
9
10
  recovered: Array<OrphanToolCall & {
@@ -13,6 +13,7 @@ const INTERRUPTED_NEVER_STARTED = "[INTERRUPTED] The run was aborted before this
13
13
  "no side effects — it is safe to re-issue this call if you still need it.";
14
14
  export function findOrphanToolCalls(messages, suspendedBatch) {
15
15
  const resolvedAt = new Map();
16
+ const callSitesAt = new Map();
16
17
  messages.forEach((m, i) => {
17
18
  if (m.role === "toolResult") {
18
19
  const at = resolvedAt.get(m.toolCallId);
@@ -21,35 +22,54 @@ export function findOrphanToolCalls(messages, suspendedBatch) {
21
22
  else
22
23
  at.push(i);
23
24
  }
24
- });
25
- const nextReuseAfter = (id, from) => {
26
- for (let i = from + 1; i < messages.length; i++) {
27
- const m = messages[i];
28
- if (m?.role !== "assistant")
29
- continue;
30
- if (m.content.some((p) => p.type === "toolCall" && p.id === id))
31
- return i;
25
+ else if (m.role === "assistant") {
26
+ for (const part of m.content) {
27
+ if (part.type !== "toolCall")
28
+ continue;
29
+ const at = callSitesAt.get(part.id);
30
+ if (at === undefined)
31
+ callSitesAt.set(part.id, [i]);
32
+ else
33
+ at.push(i);
34
+ }
32
35
  }
33
- return Number.POSITIVE_INFINITY;
34
- };
36
+ });
37
+ const nextReuseAfter = (id, from) => callSitesAt.get(id)?.find((j) => j > from) ?? Number.POSITIVE_INFINITY;
35
38
  const orphans = [];
36
39
  messages.forEach((m, i) => {
37
- if (m.role !== "assistant")
38
- return;
39
- for (const part of m.content) {
40
- if (part.type !== "toolCall" || suspendedBatch?.has(part.id))
41
- continue;
42
- const bound = nextReuseAfter(part.id, i);
43
- const closed = (resolvedAt.get(part.id) ?? []).some((at) => at > i && at < bound);
44
- if (!closed)
45
- orphans.push({ toolCallId: part.id, toolName: part.name });
40
+ if (m.role === "assistant") {
41
+ for (const part of m.content) {
42
+ if (part.type !== "toolCall" || suspendedBatch?.has(part.id))
43
+ continue;
44
+ const bound = nextReuseAfter(part.id, i);
45
+ const closed = (resolvedAt.get(part.id) ?? []).some((at) => at > i && at < bound);
46
+ if (!closed)
47
+ orphans.push({ toolCallId: part.id, toolName: part.name });
48
+ }
49
+ }
50
+ else if (m.role === "toolResult") {
51
+ if (suspendedBatch?.has(m.toolCallId))
52
+ return;
53
+ const sites = callSitesAt.get(m.toolCallId) ?? [];
54
+ const results = resolvedAt.get(m.toolCallId) ?? [];
55
+ let paired = false;
56
+ for (const j of sites) {
57
+ if (j >= i)
58
+ break;
59
+ if (!results.some((at) => at > j && at < i)) {
60
+ paired = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!paired)
65
+ orphans.push({ toolCallId: m.toolCallId, toolName: m.toolName, kind: "result" });
46
66
  }
47
67
  });
48
68
  return orphans;
49
69
  }
50
70
  export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
51
71
  const { messages } = await session.buildContext();
52
- const orphans = findOrphanToolCalls(messages, suspendedBatch);
72
+ const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
53
73
  const recovered = [];
54
74
  for (const orphan of orphans) {
55
75
  const effect = toolEffects?.get(canonicalToolName(orphan.toolName)) ?? "write";
@@ -0,0 +1,13 @@
1
+ import type { SkillSpec } from "./types.js";
2
+ export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "attachment_skipped" | "read_failed";
3
+ export interface SkillsDirectoryWarning {
4
+ code: SkillsDirectoryWarningCode;
5
+ skill: string;
6
+ detail: string;
7
+ }
8
+ export interface SkillsDirectoryOptions {
9
+ deployedTools?: readonly string[];
10
+ onWarning?: (warning: SkillsDirectoryWarning) => void;
11
+ maxAttachmentBytes?: number;
12
+ }
13
+ export declare function createSkillsFromDirectory(dir: string, options?: SkillsDirectoryOptions): SkillSpec[];
@@ -0,0 +1,214 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const SKILL_FILE = "SKILL.md";
4
+ const RESOURCE_DIRS = ["assets", "references", "scripts"];
5
+ const NAME_MAX_CHARS = 64;
6
+ const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
7
+ const DESCRIPTION_MAX_CHARS = 1024;
8
+ const DEFAULT_MAX_ATTACHMENT_BYTES = 256 * 1024;
9
+ const FENCE = "---";
10
+ function parseSkillFile(text) {
11
+ const fields = new Map();
12
+ const normalized = text.replace(/\r\n/g, "\n");
13
+ const lines = normalized.split("\n");
14
+ if (lines[0]?.trim() !== FENCE)
15
+ return { fields, body: normalized, hadFrontmatter: false };
16
+ let end = -1;
17
+ for (let i = 1; i < lines.length; i++) {
18
+ if (lines[i].trim() === FENCE) {
19
+ end = i;
20
+ break;
21
+ }
22
+ }
23
+ if (end === -1)
24
+ return { fields, body: normalized, hadFrontmatter: false };
25
+ for (let i = 1; i < end; i++) {
26
+ const line = lines[i];
27
+ const trimmed = line.trim();
28
+ if (trimmed === "" || trimmed.startsWith("#"))
29
+ continue;
30
+ if (/^\s/.test(line))
31
+ continue;
32
+ const kv = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(trimmed);
33
+ if (!kv)
34
+ continue;
35
+ const [, key, raw] = kv;
36
+ if (fields.has(key))
37
+ continue;
38
+ fields.set(key, unquote(raw.trim()));
39
+ }
40
+ let body = lines.slice(end + 1).join("\n");
41
+ if (body.startsWith("\n"))
42
+ body = body.slice(1);
43
+ return { fields, body, hadFrontmatter: true };
44
+ }
45
+ function unquote(value) {
46
+ if (value.length < 2)
47
+ return value;
48
+ const q = value[0];
49
+ if ((q !== '"' && q !== "'") || value[value.length - 1] !== q)
50
+ return value;
51
+ const inner = value.slice(1, -1);
52
+ return q === '"' ? inner.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : inner;
53
+ }
54
+ function listRelativeFiles(base, dir, prefix, out) {
55
+ let entries;
56
+ try {
57
+ entries = readdirSync(join(base, dir), { withFileTypes: true });
58
+ }
59
+ catch {
60
+ return;
61
+ }
62
+ for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
63
+ const rel = `${prefix}${e.name}`;
64
+ if (e.isDirectory())
65
+ listRelativeFiles(base, join(dir, e.name), `${rel}/`, out);
66
+ else if (e.isFile())
67
+ out.push(rel);
68
+ }
69
+ }
70
+ function readAttachments(skillDir, skillName, budgetBytes, warn) {
71
+ const relPaths = [];
72
+ for (const d of RESOURCE_DIRS) {
73
+ let st;
74
+ try {
75
+ st = statSync(join(skillDir, d));
76
+ }
77
+ catch {
78
+ continue;
79
+ }
80
+ if (st.isDirectory())
81
+ listRelativeFiles(skillDir, d, `${d}/`, relPaths);
82
+ }
83
+ relPaths.sort();
84
+ const files = [];
85
+ let spent = 0;
86
+ for (const rel of relPaths) {
87
+ let bytes;
88
+ try {
89
+ bytes = readFileSync(join(skillDir, rel));
90
+ }
91
+ catch (err) {
92
+ warn({ code: "attachment_skipped", skill: skillName, detail: `${rel}: unreadable (${errText(err)})` });
93
+ continue;
94
+ }
95
+ if (!isDecodableText(bytes)) {
96
+ warn({ code: "attachment_skipped", skill: skillName, detail: `${rel}: not text (attachment content is a string; binary is not carried)` });
97
+ continue;
98
+ }
99
+ if (spent + bytes.byteLength > budgetBytes) {
100
+ warn({ code: "attachment_skipped", skill: skillName, detail: `${rel}: over the ${budgetBytes}-byte attachment budget for this skill` });
101
+ continue;
102
+ }
103
+ spent += bytes.byteLength;
104
+ files.push({ path: rel, content: bytes.toString("utf8") });
105
+ }
106
+ return files;
107
+ }
108
+ function isDecodableText(bytes) {
109
+ if (bytes.includes(0))
110
+ return false;
111
+ const decoded = bytes.toString("utf8");
112
+ return Buffer.byteLength(decoded, "utf8") === bytes.byteLength;
113
+ }
114
+ function errText(err) {
115
+ return err instanceof Error ? err.message : String(err);
116
+ }
117
+ function manifestFromAllowedTools(declared, skillName, deployedTools, warn) {
118
+ const names = [];
119
+ for (const n of declared.split(/\s+/)) {
120
+ if (n !== "" && !names.includes(n))
121
+ names.push(n);
122
+ }
123
+ if (names.length === 0)
124
+ return undefined;
125
+ let allowTools = names;
126
+ if (deployedTools !== undefined) {
127
+ const mounted = new Set(deployedTools);
128
+ allowTools = names.filter((n) => mounted.has(n));
129
+ for (const n of names) {
130
+ if (!mounted.has(n)) {
131
+ warn({
132
+ code: "allowed_tool_not_mounted",
133
+ skill: skillName,
134
+ detail: `allowed-tools names "${n}", which this deployment does not mount — dropped (a skill declaration may only narrow capability, never add a tool)`,
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return { allowTools, lineageId: `skill:${skillName}` };
140
+ }
141
+ export function createSkillsFromDirectory(dir, options = {}) {
142
+ const warn = (w) => {
143
+ options.onWarning?.(w);
144
+ };
145
+ const budget = options.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES;
146
+ let entries;
147
+ try {
148
+ const st = statSync(dir);
149
+ if (!st.isDirectory())
150
+ throw new Error("not a directory");
151
+ entries = readdirSync(dir, { withFileTypes: true });
152
+ }
153
+ catch (err) {
154
+ throw new Error(`skills directory could not be read: ${dir} (${errText(err)})`);
155
+ }
156
+ const dirNames = entries
157
+ .filter((e) => e.isDirectory())
158
+ .map((e) => e.name)
159
+ .sort();
160
+ const skills = [];
161
+ for (const name of dirNames) {
162
+ const skillDir = join(dir, name);
163
+ let text;
164
+ try {
165
+ text = readFileSync(join(skillDir, SKILL_FILE), "utf8");
166
+ }
167
+ catch {
168
+ warn({ code: "no_skill_file", skill: name, detail: `no ${SKILL_FILE} in this directory — skipped` });
169
+ continue;
170
+ }
171
+ const parsed = parseSkillFile(text);
172
+ if (!parsed.hadFrontmatter) {
173
+ warn({ code: "no_frontmatter", skill: name, detail: `${SKILL_FILE} has no fenced --- frontmatter block — skipped` });
174
+ continue;
175
+ }
176
+ const declaredName = parsed.fields.get("name");
177
+ if (declaredName === undefined || declaredName === "") {
178
+ warn({ code: "missing_name", skill: name, detail: "frontmatter has no `name` (required) — skipped" });
179
+ continue;
180
+ }
181
+ if (declaredName.length > NAME_MAX_CHARS || !NAME_RE.test(declaredName)) {
182
+ warn({
183
+ code: "invalid_name",
184
+ skill: name,
185
+ detail: `\`name\` must be ≤${NAME_MAX_CHARS} chars of lowercase alphanumerics in hyphen-separated runs — skipped`,
186
+ });
187
+ continue;
188
+ }
189
+ if (declaredName !== name) {
190
+ warn({ code: "name_mismatch", skill: name, detail: `frontmatter \`name\` is "${declaredName}" but the directory is "${name}" — skipped` });
191
+ continue;
192
+ }
193
+ const description = parsed.fields.get("description");
194
+ if (description === undefined || description === "") {
195
+ warn({ code: "missing_description", skill: name, detail: "frontmatter has no `description` (required) — skipped" });
196
+ continue;
197
+ }
198
+ if (description.length > DESCRIPTION_MAX_CHARS) {
199
+ warn({ code: "description_too_long", skill: name, detail: `\`description\` is ${description.length} chars, over the ${DESCRIPTION_MAX_CHARS} cap — skipped` });
200
+ continue;
201
+ }
202
+ const allowed = parsed.fields.get("allowed-tools");
203
+ const manifest = allowed === undefined ? undefined : manifestFromAllowedTools(allowed, name, options.deployedTools, warn);
204
+ const files = readAttachments(skillDir, name, budget, warn);
205
+ skills.push({
206
+ name: declaredName,
207
+ description,
208
+ content: parsed.body,
209
+ ...(manifest !== undefined ? { manifest } : {}),
210
+ ...(files.length > 0 ? { files } : {}),
211
+ });
212
+ }
213
+ return skills;
214
+ }
@@ -68,6 +68,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
68
68
  error?: string;
69
69
  errorCode?: string;
70
70
  retryable?: boolean;
71
+ errorKind?: string;
71
72
  stoppedBy?: StopSource;
72
73
  seq?: number;
73
74
  }): "completed" | "failed" | "killed" | undefined;
@@ -102,6 +103,7 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
102
103
  error?: string;
103
104
  errorCode?: string;
104
105
  retryable?: boolean;
106
+ errorKind?: string;
105
107
  }): "completed" | "failed" | "killed" | undefined;
106
108
  export declare function unmarkRetainedContinuationLane(core: DurableAgentCore, id: string): void;
107
109
  export declare function attachAgentNotifyLane(core: DurableAgentCore, id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
@@ -706,6 +706,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
706
706
  handle.errorCode = outcome.errorCode;
707
707
  if (outcome.retryable !== undefined)
708
708
  handle.errorRetryable = outcome.retryable;
709
+ if (outcome.errorKind !== undefined)
710
+ handle.errorKind = outcome.errorKind;
709
711
  }
710
712
  handle.updatedAt = Date.now();
711
713
  if (outcome.seq !== undefined)
@@ -844,6 +846,9 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
844
846
  handle.resultFull = undefined;
845
847
  handle.spillRef = undefined;
846
848
  handle.error = undefined;
849
+ handle.errorCode = undefined;
850
+ handle.errorRetryable = undefined;
851
+ handle.errorKind = undefined;
847
852
  handle.resultIsPartial = undefined;
848
853
  handle.stopSource = undefined;
849
854
  handle.completionId = undefined;
@@ -1030,6 +1035,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1030
1035
  ...(row.resultIsPartial ? { partial_result: true } : {}),
1031
1036
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1032
1037
  },
1038
+ ...(row.status === "failed" ? { isError: true } : {}),
1033
1039
  };
1034
1040
  }
1035
1041
  export async function spillClippedAgentResult(handle, full, clipped, store, sessionId) {
@@ -1065,6 +1071,9 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1065
1071
  }
1066
1072
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
1067
1073
  const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
1074
+ const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
1075
+ ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
1076
+ : "";
1068
1077
  const body = running
1069
1078
  ? oneShot === true
1070
1079
  ? `status: running
@@ -1072,7 +1081,7 @@ This is a ONE-SHOT submission — there is no later turn for a background notifi
1072
1081
  : `status: running
1073
1082
  The agent is still working — you will be notified when it completes.`
1074
1083
  : `status: ${handle.status}
1075
- ${handle.error ? `error: ${handle.error}
1084
+ ${handle.error ? `error: ${handle.error}${kindClause}
1076
1085
  ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1077
1086
  ${resultText}` : "(no result text)"}`;
1078
1087
  return {
@@ -1092,6 +1101,7 @@ ${resultText}` : "(no result text)"}`;
1092
1101
  ...(handle.resultIsPartial ? { partial_result: true } : {}),
1093
1102
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1094
1103
  },
1104
+ ...(handle.status === "failed" ? { isError: true } : {}),
1095
1105
  };
1096
1106
  }
1097
1107
  export async function stopBackgroundAgentLane(core, handle) {
@@ -139,6 +139,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
139
139
  error?: string;
140
140
  errorCode?: string;
141
141
  errorRetryable?: boolean;
142
+ errorKind?: string;
142
143
  resultIsPartial?: boolean;
143
144
  stopSource?: StopSource;
144
145
  stoppedBy?: StopSource;
@@ -135,6 +135,7 @@ export declare class TaskRegistry {
135
135
  error?: string;
136
136
  errorCode?: string;
137
137
  retryable?: boolean;
138
+ errorKind?: string;
138
139
  stoppedBy?: StopSource;
139
140
  seq?: number;
140
141
  }): "completed" | "failed" | "killed" | undefined;
@@ -170,6 +171,7 @@ export declare class TaskRegistry {
170
171
  error?: string;
171
172
  errorCode?: string;
172
173
  retryable?: boolean;
174
+ errorKind?: string;
173
175
  }): "completed" | "failed" | "killed" | undefined;
174
176
  unmarkRetainedContinuation(id: string): void;
175
177
  attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
@@ -368,6 +368,17 @@ export class TaskRegistry {
368
368
  async settleKilledForOwner(access, opts) {
369
369
  const source = opts?.source ?? "parent";
370
370
  const clause = (by) => by === "user" ? "stopped by user" : by === "parent" ? "its parent run ended" : `stopped by ${by}`;
371
+ if (source === "user") {
372
+ for (const handle of this.handles.values()) {
373
+ if (handle.type !== "background_agent" || handle.status !== "running")
374
+ continue;
375
+ if (!canAccess(handle, { owner: access.owner, scope: access.scope }))
376
+ continue;
377
+ if (opts?.skipSessionScoped && handle.sessionScoped)
378
+ continue;
379
+ this.markStopSource(handle.id, source);
380
+ }
381
+ }
371
382
  let settled = 0;
372
383
  for (const handle of this.handles.values()) {
373
384
  if (handle.type !== "background_bash" && handle.type !== "monitor")
@@ -138,6 +138,7 @@ export type TraceEvent = {
138
138
  taskId: string;
139
139
  model: string;
140
140
  provider?: string;
141
+ turn?: number;
141
142
  promptTokens: number;
142
143
  completionTokens: number;
143
144
  cacheRead: number;
@@ -155,6 +156,8 @@ export type TraceEvent = {
155
156
  version: 1;
156
157
  taskId: string;
157
158
  name: string;
159
+ toolCallId?: string;
160
+ turn?: number;
158
161
  durationMs: number;
159
162
  ok: boolean;
160
163
  effect?: ToolEffect;
@@ -47,6 +47,7 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
47
47
  offload?: boolean;
48
48
  offloadThresholdChars?: number;
49
49
  defer?: boolean;
50
+ alwaysLoad?: boolean;
50
51
  contract?: {
51
52
  contractId: string;
52
53
  implementationRevision: string;
@@ -256,6 +257,8 @@ export interface TaskSpec {
256
257
  tools?: ToolSpec[];
257
258
  excludeTools?: string[];
258
259
  deferTools?: string[];
260
+ alwaysLoadTools?: string[];
261
+ deferSelfResolve?: boolean;
259
262
  promptProfile?: "simple" | "classic";
260
263
  agents?: AgentDefinition[];
261
264
  toolPolicy?: import("./tool-policy.js").ToolPolicy;
@@ -514,9 +517,9 @@ export type TaskEvent = ({
514
517
  };
515
518
  usageMissing?: true;
516
519
  stopReason?: string;
517
- } & TaskEventIdentity) | {
520
+ } & TaskEventIdentity) | ({
518
521
  type: "compacted";
519
- trigger: "auto" | "manual";
522
+ trigger: "auto" | "manual" | "forced";
520
523
  tokensBefore: number;
521
524
  tokensAfter?: number;
522
525
  triggerTokensBefore?: number;
@@ -534,7 +537,12 @@ export type TaskEvent = ({
534
537
  clampedRatio?: number;
535
538
  clampReason?: "budget" | "walltime" | "tolerance";
536
539
  phaseDurations?: import("./auto-compaction.js").CompactionPhaseDurations;
537
- } | ({
540
+ } & TaskEventIdentity) | ({
541
+ type: "compaction_outcome";
542
+ outcome: Exclude<CompactOutcome, "compacted"> | "suppressed";
543
+ trigger: "auto" | "manual" | "forced";
544
+ reason?: string;
545
+ } & TaskEventIdentity) | ({
538
546
  type: "steering_injected";
539
547
  source: "deadline_nudge" | "finalize" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
540
548
  preview: string;
@@ -8,6 +8,9 @@ export interface CompactionDetails {
8
8
  modifiedFiles: string[];
9
9
  modifiedFilesByRecency?: string[];
10
10
  invokedSkills?: InvokedSkillRetention[];
11
+ elidedMessages?: number;
12
+ persistedOutputRefs?: string[];
13
+ activeTools?: string[];
11
14
  }
12
15
  export interface CompactionResult<T = unknown> {
13
16
  summary: string;
@@ -68,6 +71,8 @@ export interface CompactionPreparation {
68
71
  previousSummary?: string;
69
72
  fileOps: FileOperations;
70
73
  invokedSkills: InvokedSkillRetention[];
74
+ persistedOutputRefs?: string[];
75
+ elidedMessages?: number;
71
76
  settings: CompactionSettings;
72
77
  }
73
78
  export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number): Result<CompactionPreparation | undefined, CompactionError>;