@sema-agent/core 2.5.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/core/mcp.d.ts +9 -0
- package/dist/core/mcp.js +56 -5
- package/dist/core/runner/prepare-task.js +19 -1
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/runner/tool-disclosure.d.ts +8 -2
- package/dist/core/runner/tool-disclosure.js +23 -6
- package/dist/core/skills-directory.d.ts +13 -0
- package/dist/core/skills-directory.js +214 -0
- package/dist/core/trace.d.ts +3 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/dist/core/mcp.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare function mcpToolTotalTimeoutMs(perCallMs: number): number;
|
|
|
68
68
|
export declare const MCP_IDLE_TIMEOUT_STDIO_DEFAULT_MS: number;
|
|
69
69
|
export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
|
|
70
70
|
export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
|
|
71
|
+
export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
|
|
71
72
|
export declare function normalizeMcpName(name: string): string;
|
|
72
73
|
export declare function clampNameSegment(seg: string, max?: number): string;
|
|
73
74
|
export * from "./image-downsample.js";
|
|
@@ -106,3 +107,11 @@ export type McpSchemaNormalizeResult = {
|
|
|
106
107
|
export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormalizeResult;
|
|
107
108
|
export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
|
|
108
109
|
export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer): Promise<MaterializedMcp>;
|
|
110
|
+
export declare function classifyDirReadInvalidParams(message: string): "not_found" | "not_directory";
|
|
111
|
+
export declare function parseCallToolResultLenient(data: unknown): {
|
|
112
|
+
success: true;
|
|
113
|
+
data: unknown;
|
|
114
|
+
} | {
|
|
115
|
+
success: false;
|
|
116
|
+
error: unknown;
|
|
117
|
+
};
|
package/dist/core/mcp.js
CHANGED
|
@@ -5,7 +5,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
5
5
|
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import { ElicitRequestSchema, ErrorCode, ListResourcesResultSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { CallToolResultSchema, ElicitRequestSchema, ErrorCode, ListResourcesResultSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { MCP_IMAGE_MAX_BASE64, sharpImageResizer } from "./image-downsample.js";
|
|
10
10
|
import { truncateError } from "./tool-errors.js";
|
|
11
11
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
@@ -137,6 +137,14 @@ function armMcpIdleWatchdog(health, idleMs, outerSignal) {
|
|
|
137
137
|
function mcpStartupTimeoutMs() {
|
|
138
138
|
return parseEnvMs("MCP_TIMEOUT");
|
|
139
139
|
}
|
|
140
|
+
const MCP_SPEC_ERROR_CODE_NAMES = new Map([
|
|
141
|
+
[-32020, "header mismatch (the server saw an HTTP header that disagreed with the tool parameter mapped onto it)"],
|
|
142
|
+
[-32021, "missing required client capability (the server requires a capability this client does not declare)"],
|
|
143
|
+
[-32022, "unsupported protocol version (the server implements no protocol revision this client offers)"],
|
|
144
|
+
]);
|
|
145
|
+
export function describeMcpSpecErrorCode(code) {
|
|
146
|
+
return typeof code === "number" ? MCP_SPEC_ERROR_CODE_NAMES.get(code) : undefined;
|
|
147
|
+
}
|
|
140
148
|
function isTransportLost(err) {
|
|
141
149
|
if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed)
|
|
142
150
|
return true;
|
|
@@ -180,6 +188,15 @@ function rethrowHonestMcpError(err, ctx) {
|
|
|
180
188
|
e.details = { transportLost: true, server: ctx.server };
|
|
181
189
|
throw e;
|
|
182
190
|
}
|
|
191
|
+
if (err instanceof McpError) {
|
|
192
|
+
const condition = describeMcpSpecErrorCode(err.code);
|
|
193
|
+
if (condition !== undefined) {
|
|
194
|
+
const e = new Error(`${ctx.what} on MCP server "${serverLabel}" was rejected with MCP protocol error ${err.code} — ${condition}. The server's error text follows as external/untrusted data:\n${delimitUntrusted(`${ctx.server} error`, truncateMcpErrorText(err.message))}`, { cause: err });
|
|
195
|
+
e.errorKind = "protocol_error";
|
|
196
|
+
e.details = { server: ctx.server, specErrorCode: err.code };
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
183
200
|
if (ctx.attributeServer) {
|
|
184
201
|
const msg = err instanceof Error ? err.message : String(err);
|
|
185
202
|
throw new Error(`${ctx.what} on MCP server "${serverLabel}" failed. The server's error text follows as external/untrusted data:\n${delimitUntrusted(`${ctx.server} error`, truncateMcpErrorText(msg))}`, { cause: err });
|
|
@@ -653,6 +670,15 @@ const LEGACY_READ_MCP_RESOURCE = "ReadMcpResource";
|
|
|
653
670
|
const LEGACY_READ_MCP_RESOURCE_DIR = "ReadMcpResourceDir";
|
|
654
671
|
const MCP_SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
|
|
655
672
|
const MAX_DIR_READ_PAGES = 20;
|
|
673
|
+
const DIR_READ_NOT_A_DIRECTORY_RE = /not a directory|isn'?t a directory|not a folder/i;
|
|
674
|
+
const DIR_READ_NOT_FOUND_RE = /not found|no such|does not exist|doesn'?t exist|unknown (?:resource|uri)/i;
|
|
675
|
+
export function classifyDirReadInvalidParams(message) {
|
|
676
|
+
if (DIR_READ_NOT_A_DIRECTORY_RE.test(message))
|
|
677
|
+
return "not_directory";
|
|
678
|
+
if (DIR_READ_NOT_FOUND_RE.test(message))
|
|
679
|
+
return "not_found";
|
|
680
|
+
return "not_directory";
|
|
681
|
+
}
|
|
656
682
|
function resourceLine(r) {
|
|
657
683
|
return `${r.uri}${r.name ? ` — ${r.name}` : ""}${r.mimeType ? ` (${r.mimeType})` : ""}${r.description ? `: ${r.description}` : ""}`;
|
|
658
684
|
}
|
|
@@ -672,7 +698,7 @@ async function readDirViaExtension(rs, uri, signal, timeoutMs, watchdog) {
|
|
|
672
698
|
if (err instanceof McpError && err.code === ErrorCode.InvalidParams) {
|
|
673
699
|
if (pages > 0)
|
|
674
700
|
return { kind: "ok", resources, cursorInvalid: true };
|
|
675
|
-
return { kind:
|
|
701
|
+
return { kind: classifyDirReadInvalidParams(err.message), detail: err.message };
|
|
676
702
|
}
|
|
677
703
|
if (err instanceof McpError && err.code === ErrorCode.MethodNotFound)
|
|
678
704
|
return { kind: "unsupported" };
|
|
@@ -866,9 +892,22 @@ function buildResourceTools(resourceServers) {
|
|
|
866
892
|
const ext = rs.client.getServerCapabilities()?.extensions?.[MCP_SKILLS_EXTENSION];
|
|
867
893
|
if (ext?.directoryRead === true) {
|
|
868
894
|
const r = await readDirViaExtension(rs, uri, signal, timeoutMs, watchdog).catch((err) => rethrowHonestMcpError(err, { server, what, timeoutMs, writeEffect: false, signal, attributeServer: true, idle: { signal: watchdog.idleSignal, idleMs } }));
|
|
895
|
+
const serverSaid = (detail) => `\nThe server's error text follows as external/untrusted data:\n${delimitUntrusted(`${server} error`, truncateMcpErrorText(detail))}`;
|
|
896
|
+
if (r.kind === "not_found") {
|
|
897
|
+
return {
|
|
898
|
+
content: [
|
|
899
|
+
{
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `Resource not found: ${inlineUntrusted(uri)} — the server reports no such resource, so re-reading it will not help either. Use ${LIST_MCP_RESOURCES} to see what this server exposes.${serverSaid(r.detail)}`,
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
details: { resources: [], notFound: true },
|
|
905
|
+
terminate: false,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
869
908
|
if (r.kind === "not_directory") {
|
|
870
909
|
return {
|
|
871
|
-
content: [{ type: "text", text: `Not a directory resource: ${inlineUntrusted(uri)}. If it is a file resource, use ${READ_MCP_RESOURCE} instead
|
|
910
|
+
content: [{ type: "text", text: `Not a directory resource: ${inlineUntrusted(uri)}. If it is a file resource, use ${READ_MCP_RESOURCE} instead.${serverSaid(r.detail)}` }],
|
|
872
911
|
details: { resources: [] },
|
|
873
912
|
terminate: false,
|
|
874
913
|
};
|
|
@@ -929,6 +968,17 @@ const LenientListToolsResultSchema = {
|
|
|
929
968
|
return { success: true, data: { tools, ...(typeof nextCursor === "string" ? { nextCursor } : {}) } };
|
|
930
969
|
},
|
|
931
970
|
};
|
|
971
|
+
export function parseCallToolResultLenient(data) {
|
|
972
|
+
if (typeof data !== "object" || data === null || Array.isArray(data) || !("structuredContent" in data)) {
|
|
973
|
+
return CallToolResultSchema.safeParse(data);
|
|
974
|
+
}
|
|
975
|
+
const { structuredContent, ...rest } = data;
|
|
976
|
+
const parsed = CallToolResultSchema.safeParse(rest);
|
|
977
|
+
if (!parsed.success)
|
|
978
|
+
return parsed;
|
|
979
|
+
return { success: true, data: { ...parsed.data, structuredContent } };
|
|
980
|
+
}
|
|
981
|
+
const LENIENT_CALL_TOOL_RESULT_SCHEMA = { safeParse: parseCallToolResultLenient };
|
|
932
982
|
async function listToolsLenient(client, options) {
|
|
933
983
|
return client.request({ method: "tools/list", params: {} }, LenientListToolsResultSchema, options);
|
|
934
984
|
}
|
|
@@ -1059,7 +1109,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
|
|
|
1059
1109
|
let res;
|
|
1060
1110
|
try {
|
|
1061
1111
|
res = await client
|
|
1062
|
-
.callTool({ name: remoteName, arguments: (params ?? {}) },
|
|
1112
|
+
.callTool({ name: remoteName, arguments: (params ?? {}) }, LENIENT_CALL_TOOL_RESULT_SCHEMA, {
|
|
1063
1113
|
signal: watchdog.combinedSignal,
|
|
1064
1114
|
timeout: timeoutMs,
|
|
1065
1115
|
resetTimeoutOnProgress: true,
|
|
@@ -1106,7 +1156,8 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
|
|
|
1106
1156
|
}
|
|
1107
1157
|
function asServerWarning(spec, err) {
|
|
1108
1158
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1109
|
-
const
|
|
1159
|
+
const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
|
|
1160
|
+
const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${condition !== undefined ? `${condition}: ` : ""}${detail})`, { cause: err });
|
|
1110
1161
|
warning.code = "mcp.server_unavailable";
|
|
1111
1162
|
return warning;
|
|
1112
1163
|
}
|
|
@@ -1826,7 +1826,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1826
1826
|
throw e;
|
|
1827
1827
|
}
|
|
1828
1828
|
const registry = buildDeferredRegistry(deferred, tools);
|
|
1829
|
-
const
|
|
1829
|
+
const realByName = new Map(tools.map((t) => [t.name, t]));
|
|
1830
|
+
const directCallFor = (name) => {
|
|
1831
|
+
if (spec.deferSelfResolve === false)
|
|
1832
|
+
return undefined;
|
|
1833
|
+
const real = realByName.get(name);
|
|
1834
|
+
if (real === undefined)
|
|
1835
|
+
return undefined;
|
|
1836
|
+
return {
|
|
1837
|
+
parameters: real.parameters,
|
|
1838
|
+
invoke: (toolCallId, params, signal) => real.execute(toolCallId, params, signal),
|
|
1839
|
+
activate: async () => {
|
|
1840
|
+
if (activeTools.has(name))
|
|
1841
|
+
return;
|
|
1842
|
+
activeTools.add(name);
|
|
1843
|
+
await rematerialize(activeTools);
|
|
1844
|
+
},
|
|
1845
|
+
};
|
|
1846
|
+
};
|
|
1847
|
+
const placeholders = new Map([...registry.values()].map((i) => [i.name, createPlaceholderTool(i, directCallFor(i.name))]));
|
|
1830
1848
|
const { messages } = await session.buildContext();
|
|
1831
1849
|
for (const n of extractDiscoveredToolNames(messages, registry)) {
|
|
1832
1850
|
if (deferred.has(n))
|
|
@@ -844,6 +844,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
844
844
|
taskId: rs.telemetry.taskId,
|
|
845
845
|
model: m.model,
|
|
846
846
|
provider: m.provider,
|
|
847
|
+
turn: stats.turns + 1,
|
|
847
848
|
promptTokens: turnInput,
|
|
848
849
|
completionTokens: u.output || 0,
|
|
849
850
|
cacheRead: u.cacheRead || 0,
|
|
@@ -1011,6 +1012,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1011
1012
|
version: 1,
|
|
1012
1013
|
taskId: rs.telemetry.taskId,
|
|
1013
1014
|
name: event.toolName,
|
|
1015
|
+
toolCallId: event.toolCallId,
|
|
1016
|
+
turn: stats.turns + 1,
|
|
1014
1017
|
durationMs: toolStarted !== undefined ? toolNow - toolStarted : 0,
|
|
1015
1018
|
ok: !event.isError,
|
|
1016
1019
|
ts: toolNow,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type TSchema } from "typebox";
|
|
2
|
+
import type { AgentMessage, AgentTool, AgentToolResult } from "../../internal/harness-types.js";
|
|
2
3
|
import type { Model } from "../../internal/llm.js";
|
|
3
4
|
import type { ToolSpec } from "../types.js";
|
|
4
5
|
export declare const TOOL_SEARCH_NAME = "ToolSearch";
|
|
@@ -27,7 +28,12 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
|
|
|
27
28
|
name: string;
|
|
28
29
|
description: string;
|
|
29
30
|
}>): Map<string, DeferredToolInfo>;
|
|
30
|
-
export
|
|
31
|
+
export interface PlaceholderDirectCall {
|
|
32
|
+
parameters: TSchema;
|
|
33
|
+
invoke: (toolCallId: string, params: unknown, signal?: AbortSignal) => Promise<AgentToolResult<unknown>>;
|
|
34
|
+
activate: () => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare function createPlaceholderTool(info: DeferredToolInfo, direct?: PlaceholderDirectCall): AgentTool;
|
|
31
37
|
export declare function scoreToolMatch(query: string, info: DeferredToolInfo): number;
|
|
32
38
|
export interface ToolSearchArgs {
|
|
33
39
|
query?: string;
|
|
@@ -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;
|
|
@@ -59,17 +60,33 @@ export function buildDeferredRegistry(deferred, tools) {
|
|
|
59
60
|
}
|
|
60
61
|
return reg;
|
|
61
62
|
}
|
|
62
|
-
export function createPlaceholderTool(info) {
|
|
63
|
+
export function createPlaceholderTool(info, direct) {
|
|
63
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
|
+
}
|
|
64
84
|
return defineTool({
|
|
65
85
|
name: info.name,
|
|
66
86
|
description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
|
|
67
87
|
parameters: EMPTY_PARAMS,
|
|
68
88
|
effect: "read",
|
|
69
|
-
execute: () =>
|
|
70
|
-
throw new Error(`Tool "${sn}" is not active yet. Call ${TOOL_SEARCH_NAME} with {"query":"select:${sn}"} ` +
|
|
71
|
-
`(or a keyword \`query\`) to load its full schema, then call ${sn} with the proper arguments.`);
|
|
72
|
-
},
|
|
89
|
+
execute: () => teachingRejection(),
|
|
73
90
|
});
|
|
74
91
|
}
|
|
75
92
|
export function scoreToolMatch(query, info) {
|
|
@@ -193,7 +210,7 @@ export function createToolSearchTool(opts) {
|
|
|
193
210
|
: "";
|
|
194
211
|
const missingNote = callableNote +
|
|
195
212
|
(missUnknown.length > 0
|
|
196
|
-
? `\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). ` +
|
|
197
214
|
"(Already-active and non-deferred tools are callable directly and don't appear here.)"
|
|
198
215
|
: "");
|
|
199
216
|
if (matched.length === 0) {
|
|
@@ -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
|
+
}
|
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
|
@@ -258,6 +258,7 @@ export interface TaskSpec {
|
|
|
258
258
|
excludeTools?: string[];
|
|
259
259
|
deferTools?: string[];
|
|
260
260
|
alwaysLoadTools?: string[];
|
|
261
|
+
deferSelfResolve?: boolean;
|
|
261
262
|
promptProfile?: "simple" | "classic";
|
|
262
263
|
agents?: AgentDefinition[];
|
|
263
264
|
toolPolicy?: import("./tool-policy.js").ToolPolicy;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { SESSION_LOG_DIGEST_SCHEME, sessionEntryDigest, sessionLogDigest, sessio
|
|
|
3
3
|
export type { ResumeTaskConfig } from "./core/runner/runtask.js";
|
|
4
4
|
export { defineTool } from "./core/tools.js";
|
|
5
5
|
export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
|
|
6
|
+
export { createSkillsFromDirectory, type SkillsDirectoryOptions, type SkillsDirectoryWarning, type SkillsDirectoryWarningCode, } from "./core/skills-directory.js";
|
|
6
7
|
export { REPORT_FINDINGS_TOOL_NAME, type ReportedFinding } from "./core/runner/synthetic-tools.js";
|
|
7
8
|
export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
|
|
8
9
|
export type { WorkerErrorClass } from "./core/tool-errors.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { Runner, runTask, DEFAULT_MAX_TURNS } from "./core/runner/runtask.js";
|
|
|
2
2
|
export { SESSION_LOG_DIGEST_SCHEME, sessionEntryDigest, sessionLogDigest, sessionLogDigestsComparable, } from "./engine/session/log-digest.js";
|
|
3
3
|
export { defineTool } from "./core/tools.js";
|
|
4
4
|
export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
|
|
5
|
+
export { createSkillsFromDirectory, } from "./core/skills-directory.js";
|
|
5
6
|
export { REPORT_FINDINGS_TOOL_NAME } from "./core/runner/synthetic-tools.js";
|
|
6
7
|
export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
|
|
7
8
|
export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
|