@sema-agent/core 2.8.0 → 2.9.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/send-message-tool.js +37 -29
- package/dist/agents/subagent.js +15 -3
- package/dist/brain/circuit-breaker.js +18 -8
- package/dist/brain/retry.d.ts +1 -0
- package/dist/brain/retry.js +29 -7
- package/dist/brain/stream-engine.d.ts +1 -0
- package/dist/brain/stream-engine.js +74 -12
- package/dist/core/auto-compaction.js +9 -1
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +20 -0
- package/dist/core/mcp.js +8 -5
- package/dist/core/runner/prepare-task.js +24 -8
- package/dist/core/runner/runtask.js +20 -7
- package/dist/core/runner/tool-disclosure.d.ts +8 -3
- package/dist/core/runner/tool-disclosure.js +22 -8
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +257 -28
- package/dist/core/task-registry-agent.d.ts +2 -1
- package/dist/core/task-registry-agent.js +47 -54
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +1 -1
- package/dist/core/types.d.ts +5 -1
- package/dist/engine/compaction/compaction.js +71 -20
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +4 -1
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +2 -2
- package/dist/tools/fs/bash-readonly-classifier.js +83 -17
- package/dist/tools/fs/fs-bash.js +17 -11
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +44 -2
- package/package.json +1 -1
|
@@ -688,13 +688,6 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
688
688
|
queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
|
|
689
689
|
}
|
|
690
690
|
}
|
|
691
|
-
if (comp.compacted && !forceManual) {
|
|
692
|
-
if (recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
|
|
693
|
-
compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
694
|
-
runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
|
|
695
|
-
queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
691
|
runnerHooks.recordCompactionReuse(prepared, comp);
|
|
699
692
|
if (comp.compacted) {
|
|
700
693
|
queue.push({
|
|
@@ -713,6 +706,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
713
706
|
...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
|
|
714
707
|
...ident(),
|
|
715
708
|
});
|
|
709
|
+
if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
|
|
710
|
+
compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
711
|
+
queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
|
|
712
|
+
runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
|
|
713
|
+
}
|
|
716
714
|
if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
|
|
717
715
|
const pd = comp.phaseDurations;
|
|
718
716
|
const pdDur = comp.durationMs;
|
|
@@ -2074,6 +2072,9 @@ export class Runner {
|
|
|
2074
2072
|
phase: s.phase,
|
|
2075
2073
|
...(s.detail !== undefined ? { detail: s.detail } : {}),
|
|
2076
2074
|
...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
|
|
2075
|
+
...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
|
|
2076
|
+
...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
|
|
2077
|
+
...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
|
|
2077
2078
|
...ident(),
|
|
2078
2079
|
});
|
|
2079
2080
|
};
|
|
@@ -2675,6 +2676,7 @@ export class Runner {
|
|
|
2675
2676
|
minTokens: rs.counters.compactionFloor,
|
|
2676
2677
|
brain: compactionBrain,
|
|
2677
2678
|
windowSafety: windowSafetyOptions(prepared.model),
|
|
2679
|
+
onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
|
|
2678
2680
|
});
|
|
2679
2681
|
try {
|
|
2680
2682
|
if (comp?.compacted) {
|
|
@@ -3516,6 +3518,17 @@ export class Runner {
|
|
|
3516
3518
|
comp = finishComp;
|
|
3517
3519
|
}
|
|
3518
3520
|
catch (err) {
|
|
3521
|
+
const failMsg = String(err instanceof Error ? err.message : err);
|
|
3522
|
+
const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
|
|
3523
|
+
emitTrace(spec.tracer ?? this.deps.tracer, () => ({
|
|
3524
|
+
kind: "compaction.failed",
|
|
3525
|
+
version: 1,
|
|
3526
|
+
taskId: spec.taskId ?? prepared.sessionId,
|
|
3527
|
+
trigger: "auto",
|
|
3528
|
+
reason: failReason,
|
|
3529
|
+
ts: Date.now(),
|
|
3530
|
+
}));
|
|
3531
|
+
opts?.onCompactionFailed?.(failReason);
|
|
3519
3532
|
this.deps.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
|
|
3520
3533
|
}
|
|
3521
3534
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TSchema } from "typebox";
|
|
2
|
-
import type { AgentMessage, AgentTool, AgentToolResult } from "../../internal/harness-types.js";
|
|
2
|
+
import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
|
|
3
3
|
import type { Model } from "../../internal/llm.js";
|
|
4
4
|
import type { ToolSpec } from "../types.js";
|
|
5
5
|
export declare const TOOL_SEARCH_NAME = "ToolSearch";
|
|
@@ -29,10 +29,14 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
|
|
|
29
29
|
description: string;
|
|
30
30
|
}>): Map<string, DeferredToolInfo>;
|
|
31
31
|
export interface PlaceholderDirectCall {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
resolveReal: () => PlaceholderDirectTarget | undefined;
|
|
33
|
+
executionMode?: ToolExecutionMode;
|
|
34
34
|
activate: () => Promise<void>;
|
|
35
35
|
}
|
|
36
|
+
export interface PlaceholderDirectTarget {
|
|
37
|
+
parameters: TSchema;
|
|
38
|
+
invoke: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<unknown>) => Promise<AgentToolResult<unknown>>;
|
|
39
|
+
}
|
|
36
40
|
export declare function createPlaceholderTool(info: DeferredToolInfo, direct?: PlaceholderDirectCall): AgentTool;
|
|
37
41
|
export declare function scoreToolMatch(query: string, info: DeferredToolInfo): number;
|
|
38
42
|
export interface ToolSearchArgs {
|
|
@@ -52,4 +56,5 @@ export declare function createToolSearchTool(opts: {
|
|
|
52
56
|
rematerialize: (active: ReadonlySet<string>) => Promise<void>;
|
|
53
57
|
listingRide?: (newlyActivated: readonly string[]) => string | undefined;
|
|
54
58
|
mountedNames?: () => ReadonlySet<string>;
|
|
59
|
+
directCallEnabled?: boolean;
|
|
55
60
|
}): AgentTool;
|
|
@@ -43,7 +43,8 @@ export function classifyDeferred(opts) {
|
|
|
43
43
|
deferred.add(name);
|
|
44
44
|
if (opts.deferMode === "auto") {
|
|
45
45
|
const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name));
|
|
46
|
-
const
|
|
46
|
+
const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
|
|
47
|
+
const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
|
|
47
48
|
const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
|
|
48
49
|
if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
|
|
49
50
|
for (const t of candidates)
|
|
@@ -70,12 +71,16 @@ export function createPlaceholderTool(info, direct) {
|
|
|
70
71
|
return {
|
|
71
72
|
name: info.name,
|
|
72
73
|
label: info.name,
|
|
73
|
-
description: `${info.hint} — deferred:
|
|
74
|
+
description: `${info.hint} — deferred: its parameters are not listed here. Call ` +
|
|
75
|
+
`${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load them; a call that already matches this ` +
|
|
76
|
+
`tool's real parameters runs directly and activates it.`,
|
|
74
77
|
parameters: EMPTY_PARAMS,
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
...(direct.executionMode !== undefined ? { executionMode: direct.executionMode } : {}),
|
|
79
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
80
|
+
const real = direct.resolveReal();
|
|
81
|
+
if (real !== undefined && Value.Check(real.parameters, params)) {
|
|
77
82
|
await direct.activate();
|
|
78
|
-
return
|
|
83
|
+
return real.invoke(toolCallId, params, signal, onUpdate);
|
|
79
84
|
}
|
|
80
85
|
return teachingRejection();
|
|
81
86
|
},
|
|
@@ -178,13 +183,22 @@ export function extractDiscoveredToolNames(messages, registry) {
|
|
|
178
183
|
}
|
|
179
184
|
export function createToolSearchTool(opts) {
|
|
180
185
|
const { registry, active, rematerialize, listingRide, mountedNames } = opts;
|
|
186
|
+
const directCallEnabled = opts.directCallEnabled !== false;
|
|
187
|
+
const activationPosture = directCallEnabled
|
|
188
|
+
? "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
|
|
189
|
+
"parameter schema. Until you have that schema you cannot reliably form a call, so activate a tool rather " +
|
|
190
|
+
"than guessing its arguments — a call that does match the real schema executes and activates the tool. " +
|
|
191
|
+
"When any instruction, reminder, or another tool's description names a deferred tool, activate it here " +
|
|
192
|
+
'with query "select:<name>". '
|
|
193
|
+
: "Most tools start as name-only placeholders to keep requests small; to USE one you must activate it here " +
|
|
194
|
+
"first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
|
|
195
|
+
'with query "select:<name>" before calling it. ';
|
|
181
196
|
let activationChain = Promise.resolve();
|
|
182
197
|
return defineTool({
|
|
183
198
|
name: TOOL_SEARCH_NAME,
|
|
184
199
|
contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
|
|
185
|
-
description: "Discover and activate deferred tools.
|
|
186
|
-
|
|
187
|
-
'tool\'s description names a deferred tool, activate it with query "select:<name>" before calling it. ' +
|
|
200
|
+
description: "Discover and activate deferred tools. " +
|
|
201
|
+
activationPosture +
|
|
188
202
|
"Query forms: " +
|
|
189
203
|
'"select:ToolA,ToolB" — activate these exact tools by name; ' +
|
|
190
204
|
'"notebook jupyter" — keyword search, up to max_results best matches; ' +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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";
|
|
2
|
+
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
|
|
3
3
|
export interface SkillsDirectoryWarning {
|
|
4
4
|
code: SkillsDirectoryWarningCode;
|
|
5
5
|
skill: string;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
232
|
+
let exists = true;
|
|
74
233
|
try {
|
|
75
|
-
|
|
234
|
+
statSync(join(skillDir, d));
|
|
76
235
|
}
|
|
77
236
|
catch {
|
|
78
|
-
|
|
237
|
+
exists = false;
|
|
79
238
|
}
|
|
80
|
-
if (
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 =
|
|
157
|
-
|
|
158
|
-
.
|
|
159
|
-
|
|
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
|
|
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;
|