@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.
- package/dist/agents/subagent.js +17 -14
- package/dist/core/auto-compaction.d.ts +2 -0
- package/dist/core/auto-compaction.js +2 -1
- package/dist/core/mcp.d.ts +9 -0
- package/dist/core/mcp.js +60 -6
- package/dist/core/runner/prepare-task.d.ts +5 -0
- package/dist/core/runner/prepare-task.js +40 -2
- package/dist/core/runner/runtask.js +42 -9
- package/dist/core/runner/tool-disclosure.d.ts +9 -2
- package/dist/core/runner/tool-disclosure.js +39 -11
- package/dist/core/session-reconcile.d.ts +1 -0
- package/dist/core/session-reconcile.js +40 -20
- package/dist/core/skills-directory.d.ts +13 -0
- package/dist/core/skills-directory.js +214 -0
- package/dist/core/task-registry-agent.d.ts +2 -0
- package/dist/core/task-registry-agent.js +11 -1
- package/dist/core/task-registry-shared.d.ts +1 -0
- package/dist/core/task-registry.d.ts +2 -0
- package/dist/core/task-registry.js +11 -0
- package/dist/core/trace.d.ts +3 -0
- package/dist/core/types.d.ts +11 -3
- package/dist/engine/compaction/compaction.d.ts +5 -0
- package/dist/engine/compaction/compaction.js +68 -2
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +53 -3
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/messages.js +11 -3
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/engine/session/import-validate.js +30 -1
- package/dist/engine/session/session.js +7 -5
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.js +10 -1
- package/dist/tools/fs/fs-bash.js +11 -5
- 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
|
-
|
|
39
|
+
if (!pinned.has(name))
|
|
40
|
+
deferred.add(name);
|
|
34
41
|
for (const name of opts.deferNames ?? [])
|
|
35
|
-
|
|
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.
|
|
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(", ")} —
|
|
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) {
|
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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) {
|
|
@@ -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")
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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>;
|