@webskill/sdk 0.2.7 → 0.3.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/README.md +42 -0
- package/dist/browser.d.ts +13 -4
- package/dist/browser.js +19 -10
- package/dist/{catalogComponents-C_V39rbF-B94i0fW7.js → catalogComponents-C_V39rbF-BOHveMWa.js} +254 -9090
- package/dist/client-BCM6Z3yq-qUQNkKrK.js +7787 -0
- package/dist/{dist-BQzncxXg.js → dist-8oQRa8Xz.js} +212 -13
- package/dist/{dist-CtBLBbEz.js → dist-ZKaM8j06.js} +2 -2
- package/dist/{dist-BdOW8N4V.js → dist-rorEJsNi.js} +596 -59
- package/dist/governance.d.ts +30 -197
- package/dist/governance.js +38 -199
- package/dist/{index-BpIK7tJM.d.ts → index-8d-oEDww.d.ts} +215 -2
- package/dist/{index-BNQNSDKg.d.ts → index-wiV5X8Rz.d.ts} +21 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/{jsonRenderRegistry-9GrWP_hE-CMkryt1H.js → jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js} +2 -2
- package/dist/mcp.d.ts +5 -3
- package/dist/mcp.js +10 -5
- package/dist/node.d.ts +338 -4
- package/dist/node.js +2338 -4
- package/dist/{openUiLibrary-B8-Cvou9-Cu1QZqoT.js → openUiLibrary-B8-Cvou9-BbpNTXS3.js} +2 -2
- package/dist/sandboxWorkerEntry.js +12 -3
- package/dist/skillVersionStore-DOEI9ptb-BxbYL70B.d.ts +127 -0
- package/dist/stdio-CFMoANJJ-BxrTeXh7.js +31 -0
- package/dist/{testing-BUoXvm1u.js → testing-CsrG3XLz.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-7fnqDVrf-BnRQjVU3.d.ts → types-AmKCKJn_-VGabeXK4.d.ts} +140 -3
- package/dist/types-WovEf4ED-CZSDiiBU.js +6215 -0
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui-react.js +6 -6
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/{webskillLitCatalog-CNaUpasU-CfSRvqCZ.js → webskillLitCatalog-CNaUpasU-BslMcxRZ.js} +1 -1
- package/package.json +4 -7
- package/dist/dist-CcMIUXeZ.js +0 -1896
- package/dist/index-BHL5FWGw.d.ts +0 -240
|
@@ -1,7 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as parseSkillMarkdown, L as resolveInsideRoot, O as messageOf, P as renderAvailableSkillsXml, V as validateSkills, f as SkillDiscovery, g as assertSafePathSegment, m as WebSkillError, p as SkillReader, v as buildCatalog } from "./dist-8oQRa8Xz.js";
|
|
2
2
|
import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
|
|
3
3
|
|
|
4
4
|
//#region ../runtime/dist/index.js
|
|
5
|
+
function createSseFrameReader() {
|
|
6
|
+
let buffer = "";
|
|
7
|
+
let data = [];
|
|
8
|
+
const dispatch = (out) => {
|
|
9
|
+
if (data.length === 0) return;
|
|
10
|
+
out.push(data.join("\n"));
|
|
11
|
+
data = [];
|
|
12
|
+
};
|
|
13
|
+
const consumeLine = (line, out) => {
|
|
14
|
+
if (line === "") {
|
|
15
|
+
dispatch(out);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (line.startsWith(":")) return;
|
|
19
|
+
const colon = line.indexOf(":");
|
|
20
|
+
if (colon < 0) return;
|
|
21
|
+
if (line.slice(0, colon) !== "data") return;
|
|
22
|
+
const value = line.slice(colon + 1);
|
|
23
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
push(chunk) {
|
|
27
|
+
const out = [];
|
|
28
|
+
buffer += chunk;
|
|
29
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
30
|
+
let newline;
|
|
31
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
32
|
+
const line = buffer.slice(0, newline);
|
|
33
|
+
buffer = buffer.slice(newline + 1);
|
|
34
|
+
consumeLine(line, out);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
},
|
|
38
|
+
flush() {
|
|
39
|
+
const out = [];
|
|
40
|
+
if (buffer !== "") {
|
|
41
|
+
const line = buffer;
|
|
42
|
+
buffer = "";
|
|
43
|
+
consumeLine(line, out);
|
|
44
|
+
}
|
|
45
|
+
dispatch(out);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
5
50
|
const toOpenAiMessage = (msg) => {
|
|
6
51
|
if (msg.role === "tool") return {
|
|
7
52
|
role: "tool",
|
|
@@ -68,7 +113,7 @@ var OpenAiCompatibleClient = class {
|
|
|
68
113
|
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
69
114
|
const decoder = new TextDecoder();
|
|
70
115
|
const reader = res.body.getReader();
|
|
71
|
-
|
|
116
|
+
const frames = createSseFrameReader();
|
|
72
117
|
let done = false;
|
|
73
118
|
const handleFrame = function* (data) {
|
|
74
119
|
if (data === "[DONE]") {
|
|
@@ -104,19 +149,12 @@ var OpenAiCompatibleClient = class {
|
|
|
104
149
|
try {
|
|
105
150
|
for (;;) {
|
|
106
151
|
const { value, done: readerDone } = await reader.read();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let newline;
|
|
111
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
112
|
-
const line = buffer.slice(0, newline).trim();
|
|
113
|
-
buffer = buffer.slice(newline + 1);
|
|
114
|
-
if (line === "" || line.startsWith(":")) continue;
|
|
115
|
-
if (!line.startsWith("data:")) continue;
|
|
116
|
-
yield* handleFrame(line.slice(5).trim());
|
|
152
|
+
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
153
|
+
for (const payload of payloads) {
|
|
154
|
+
yield* handleFrame(payload);
|
|
117
155
|
if (done) break;
|
|
118
156
|
}
|
|
119
|
-
if (done) break;
|
|
157
|
+
if (done || readerDone) break;
|
|
120
158
|
}
|
|
121
159
|
} finally {
|
|
122
160
|
reader.releaseLock();
|
|
@@ -125,13 +163,17 @@ var OpenAiCompatibleClient = class {
|
|
|
125
163
|
type: "tool-calls",
|
|
126
164
|
toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
|
|
127
165
|
let args = {};
|
|
166
|
+
let parseError;
|
|
128
167
|
try {
|
|
129
168
|
args = JSON.parse(acc.arguments || "{}");
|
|
130
|
-
} catch {
|
|
169
|
+
} catch (e) {
|
|
170
|
+
parseError = e instanceof Error ? e.message : String(e);
|
|
171
|
+
}
|
|
131
172
|
return {
|
|
132
173
|
id: acc.id || `call-${index}`,
|
|
133
174
|
name: acc.name,
|
|
134
|
-
arguments: args
|
|
175
|
+
arguments: args,
|
|
176
|
+
...parseError ? { argumentsParseError: parseError } : {}
|
|
135
177
|
};
|
|
136
178
|
})
|
|
137
179
|
};
|
|
@@ -292,7 +334,7 @@ var AnthropicClient = class {
|
|
|
292
334
|
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
293
335
|
const decoder = new TextDecoder();
|
|
294
336
|
const reader = res.body.getReader();
|
|
295
|
-
|
|
337
|
+
const frames = createSseFrameReader();
|
|
296
338
|
const handleFrame = function* (data) {
|
|
297
339
|
let chunk;
|
|
298
340
|
try {
|
|
@@ -339,17 +381,9 @@ var AnthropicClient = class {
|
|
|
339
381
|
try {
|
|
340
382
|
for (;;) {
|
|
341
383
|
const { value, done: readerDone } = await reader.read();
|
|
384
|
+
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
385
|
+
for (const payload of payloads) yield* handleFrame(payload);
|
|
342
386
|
if (readerDone) break;
|
|
343
|
-
buffer += decoder.decode(value, { stream: true });
|
|
344
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
345
|
-
let newline;
|
|
346
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
347
|
-
const line = buffer.slice(0, newline).trim();
|
|
348
|
-
buffer = buffer.slice(newline + 1);
|
|
349
|
-
if (line === "" || line.startsWith(":") || line.startsWith("event:")) continue;
|
|
350
|
-
if (!line.startsWith("data:")) continue;
|
|
351
|
-
yield* handleFrame(line.slice(5).trim());
|
|
352
|
-
}
|
|
353
387
|
}
|
|
354
388
|
} finally {
|
|
355
389
|
reader.releaseLock();
|
|
@@ -358,13 +392,17 @@ var AnthropicClient = class {
|
|
|
358
392
|
type: "tool-calls",
|
|
359
393
|
toolCalls: [...toolCallsByIndex.entries()].sort(([a], [b]) => a - b).map(([index, acc]) => {
|
|
360
394
|
let args = {};
|
|
395
|
+
let parseError;
|
|
361
396
|
try {
|
|
362
397
|
args = JSON.parse(acc.arguments || "{}");
|
|
363
|
-
} catch {
|
|
398
|
+
} catch (e) {
|
|
399
|
+
parseError = e instanceof Error ? e.message : String(e);
|
|
400
|
+
}
|
|
364
401
|
return {
|
|
365
402
|
id: acc.id || `call-${index}`,
|
|
366
403
|
name: acc.name,
|
|
367
|
-
arguments: args
|
|
404
|
+
arguments: args,
|
|
405
|
+
...parseError ? { argumentsParseError: parseError } : {}
|
|
368
406
|
};
|
|
369
407
|
})
|
|
370
408
|
};
|
|
@@ -520,7 +558,7 @@ var GoogleGenAiClient = class {
|
|
|
520
558
|
const toolCalls = [];
|
|
521
559
|
const decoder = new TextDecoder();
|
|
522
560
|
const reader = res.body.getReader();
|
|
523
|
-
|
|
561
|
+
const frames = createSseFrameReader();
|
|
524
562
|
const handleFrame = function* (data) {
|
|
525
563
|
let chunk;
|
|
526
564
|
try {
|
|
@@ -548,17 +586,9 @@ var GoogleGenAiClient = class {
|
|
|
548
586
|
try {
|
|
549
587
|
for (;;) {
|
|
550
588
|
const { value, done: readerDone } = await reader.read();
|
|
589
|
+
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
590
|
+
for (const payload of payloads) yield* handleFrame(payload);
|
|
551
591
|
if (readerDone) break;
|
|
552
|
-
buffer += decoder.decode(value, { stream: true });
|
|
553
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
554
|
-
let newline;
|
|
555
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
556
|
-
const line = buffer.slice(0, newline).trim();
|
|
557
|
-
buffer = buffer.slice(newline + 1);
|
|
558
|
-
if (line === "" || line.startsWith(":") || line.startsWith("event:")) continue;
|
|
559
|
-
if (!line.startsWith("data:")) continue;
|
|
560
|
-
yield* handleFrame(line.slice(5).trim());
|
|
561
|
-
}
|
|
562
592
|
}
|
|
563
593
|
} finally {
|
|
564
594
|
reader.releaseLock();
|
|
@@ -1279,6 +1309,51 @@ var TraceRecorder = class {
|
|
|
1279
1309
|
return [...this.#events];
|
|
1280
1310
|
}
|
|
1281
1311
|
};
|
|
1312
|
+
/**
|
|
1313
|
+
* LLM 可见名 → 作者在 SKILL.md 里写的形态。
|
|
1314
|
+
* `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
|
|
1315
|
+
* `mcp__x` → `mcp#x`,其余 `a__b` → `endpoint:a/b`。
|
|
1316
|
+
*/
|
|
1317
|
+
function canonicalToolName(llmToolName, activated) {
|
|
1318
|
+
const sep = llmToolName.indexOf("__");
|
|
1319
|
+
if (sep > 0 && activated.has(llmToolName.slice(0, sep))) return llmToolName;
|
|
1320
|
+
if (llmToolName.startsWith("mcp__")) return `mcp#${llmToolName.slice(5)}`;
|
|
1321
|
+
if (sep > 0) return `endpoint:${llmToolName.slice(0, sep)}/${llmToolName.slice(sep + 2)}`;
|
|
1322
|
+
return llmToolName;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* 只支持整段匹配与尾部 `/*`。通配符越自由,作者越容易写出一个自以为很窄、实际很宽的规则;
|
|
1326
|
+
* 裸 `endpoint:github`(不带 `/*`)判为未匹配——含糊写法应当报警而不是被善意解释。
|
|
1327
|
+
*/
|
|
1328
|
+
function matchesPattern(pattern, declaringSkill, llmToolName, canonical) {
|
|
1329
|
+
if (!pattern.includes(":") && !pattern.includes("#")) return llmToolName === `${declaringSkill}__${pattern}`;
|
|
1330
|
+
if (pattern === canonical) return true;
|
|
1331
|
+
if (pattern.endsWith("/*")) {
|
|
1332
|
+
const prefix = pattern.slice(0, -1);
|
|
1333
|
+
return canonical.startsWith(prefix) && canonical.length > prefix.length;
|
|
1334
|
+
}
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
function denialReason(state, canonical) {
|
|
1338
|
+
const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
|
|
1339
|
+
const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
|
|
1340
|
+
const slash = canonical.lastIndexOf("/");
|
|
1341
|
+
return `${subject} allowed-tools, but tool "${canonical}" is not listed. It is still available in 0.3.0 but WILL BE REJECTED in 0.4.0. Add "${canonical}"${slash > 0 ? ` or "${canonical.slice(0, slash)}/*"` : ""} to allowed-tools.`;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
|
|
1345
|
+
* 或存在一个**未声明**清单的激活技能(否则就是把 A 的声明施加到 B 头上)。
|
|
1346
|
+
*/
|
|
1347
|
+
function evaluateToolAccess(state, llmToolName) {
|
|
1348
|
+
if (state.skillAllowedTools.size === 0) return { allowed: true };
|
|
1349
|
+
for (const skill of state.activated) if (!state.skillAllowedTools.has(skill)) return { allowed: true };
|
|
1350
|
+
const canonical = canonicalToolName(llmToolName, state.activated);
|
|
1351
|
+
for (const [skill, patterns] of state.skillAllowedTools) for (const pattern of patterns) if (matchesPattern(pattern, skill, llmToolName, canonical)) return { allowed: true };
|
|
1352
|
+
return {
|
|
1353
|
+
allowed: false,
|
|
1354
|
+
reason: denialReason(state, canonical)
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1282
1357
|
const MAX_SURFACE_PATCHES_PER_SECOND = 240;
|
|
1283
1358
|
/** 交互终态(取消/超时):从工具执行深处直接终止 run */
|
|
1284
1359
|
var RunTerminated = class extends Error {
|
|
@@ -1373,6 +1448,8 @@ var AgentLoop = class {
|
|
|
1373
1448
|
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
1374
1449
|
activated: /* @__PURE__ */ new Set(),
|
|
1375
1450
|
activatedTools: /* @__PURE__ */ new Map(),
|
|
1451
|
+
skillAllowedTools: /* @__PURE__ */ new Map(),
|
|
1452
|
+
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1376
1453
|
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
1377
1454
|
now,
|
|
1378
1455
|
interactionSeq: 0,
|
|
@@ -1384,6 +1461,7 @@ var AgentLoop = class {
|
|
|
1384
1461
|
surfacePatchWindowStartedAt: Date.now(),
|
|
1385
1462
|
surfacePatchCount: 0,
|
|
1386
1463
|
processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
|
|
1464
|
+
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
1387
1465
|
startMs,
|
|
1388
1466
|
pausedMs: 0,
|
|
1389
1467
|
maxTurns: this.#config.maxTurns,
|
|
@@ -1447,6 +1525,19 @@ var AgentLoop = class {
|
|
|
1447
1525
|
state.timer = void 0;
|
|
1448
1526
|
}
|
|
1449
1527
|
}
|
|
1528
|
+
/**
|
|
1529
|
+
* D10 判定的唯一出口(暴露点 + 分发点共用)。
|
|
1530
|
+
* 0.3.0 只记警告并返回 false,调用点照常放行;0.4.0 收紧时改的是调用点对返回值的处理。
|
|
1531
|
+
*/
|
|
1532
|
+
#checkToolAccess(state, toolName) {
|
|
1533
|
+
const verdict = evaluateToolAccess(state, toolName);
|
|
1534
|
+
if (verdict.allowed) return true;
|
|
1535
|
+
if (!state.warnedDeniedTools.has(toolName)) {
|
|
1536
|
+
state.warnedDeniedTools.add(toolName);
|
|
1537
|
+
state.trace.record("run.warning", { message: verdict.reason ?? `Tool "${toolName}" is not allowed` });
|
|
1538
|
+
}
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1450
1541
|
/** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
|
|
1451
1542
|
async #turnLoop(state, startTurn, externalSpecs) {
|
|
1452
1543
|
const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
|
|
@@ -1458,11 +1549,12 @@ var AgentLoop = class {
|
|
|
1458
1549
|
state.turn = turn;
|
|
1459
1550
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1460
1551
|
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1552
|
+
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs];
|
|
1553
|
+
for (const spec of skillToolSpecs) this.#checkToolAccess(state, spec.name);
|
|
1461
1554
|
const toolSpecs = [
|
|
1462
1555
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1463
1556
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
1464
|
-
...
|
|
1465
|
-
...externalSpecs
|
|
1557
|
+
...skillToolSpecs
|
|
1466
1558
|
];
|
|
1467
1559
|
trace.record("llm.request", { data: {
|
|
1468
1560
|
turn,
|
|
@@ -1592,6 +1684,7 @@ var AgentLoop = class {
|
|
|
1592
1684
|
turn: state.turn,
|
|
1593
1685
|
activeSkillNames: [...state.activated].sort(),
|
|
1594
1686
|
activatedTools: [...state.activatedTools.values()],
|
|
1687
|
+
...state.skillAllowedTools.size > 0 ? { skillAllowedTools: Object.fromEntries([...state.skillAllowedTools].map(([k, v]) => [k, [...v]])) } : {},
|
|
1595
1688
|
...pending.interaction ? { pendingInteraction: pending.interaction } : {},
|
|
1596
1689
|
...pending.surfaceAction ? { pendingSurfaceAction: pending.surfaceAction } : {},
|
|
1597
1690
|
interactionExpiresAt: state.run.interruptExpiresAt,
|
|
@@ -1641,6 +1734,8 @@ var AgentLoop = class {
|
|
|
1641
1734
|
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
1642
1735
|
activated: new Set(snapshot.activeSkillNames),
|
|
1643
1736
|
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
1737
|
+
skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
|
|
1738
|
+
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1644
1739
|
toolTimeoutMs: snapshot.config.toolTimeoutMs,
|
|
1645
1740
|
now,
|
|
1646
1741
|
interactionSeq: snapshot.interactionSeq ?? 0,
|
|
@@ -1652,6 +1747,7 @@ var AgentLoop = class {
|
|
|
1652
1747
|
surfacePatchWindowStartedAt: Date.now(),
|
|
1653
1748
|
surfacePatchCount: 0,
|
|
1654
1749
|
processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
|
|
1750
|
+
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
1655
1751
|
startMs,
|
|
1656
1752
|
pausedMs: snapshot.pausedMs ?? 0,
|
|
1657
1753
|
maxTurns: snapshot.config.maxTurns,
|
|
@@ -1896,9 +1992,11 @@ var AgentLoop = class {
|
|
|
1896
1992
|
} });
|
|
1897
1993
|
this.#emitTool(state, "started", call);
|
|
1898
1994
|
let result;
|
|
1899
|
-
if (call.
|
|
1995
|
+
if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
|
|
1996
|
+
else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
|
|
1900
1997
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
1901
1998
|
else {
|
|
1999
|
+
this.#checkToolAccess(state, call.name);
|
|
1902
2000
|
const resolution = resolveToolName(call.name, state.activated);
|
|
1903
2001
|
if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
1904
2002
|
else {
|
|
@@ -2099,8 +2197,18 @@ var AgentLoop = class {
|
|
|
2099
2197
|
* 逐工具实时事件(execute 相位,data.type='tool'):chatbot 思维链工具行等的 live 状态源;
|
|
2100
2198
|
* 与既有 execute 相位事件({turn})并存,监听方按 data.type 区分。
|
|
2101
2199
|
* data.args 为参数摘要(JSON 截断 100 字符,展开详情用)。
|
|
2200
|
+
*
|
|
2201
|
+
* 幂等:同一 run 内每个 `(callId, status)` 至多投递一次。
|
|
2202
|
+
* 模型在不同轮次重发同一个 tool call id 是真实发生的,消费侧本来各自
|
|
2203
|
+
* 建去重表兑付;幂等是事件流自己的语义,不应该要求每个订阅者重建一次。
|
|
2204
|
+
*
|
|
2205
|
+
* 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
|
|
2206
|
+
* 消费者永远收不到它本来就没见过的事件。
|
|
2102
2207
|
*/
|
|
2103
2208
|
#emitTool(state, status, call) {
|
|
2209
|
+
const key = `${call.id}:${status}`;
|
|
2210
|
+
if (state.emittedToolEvents.has(key)) return;
|
|
2211
|
+
state.emittedToolEvents.add(key);
|
|
2104
2212
|
this.#deps.eventBus?.emit({
|
|
2105
2213
|
phase: "execute",
|
|
2106
2214
|
runId: state.runId,
|
|
@@ -2253,8 +2361,10 @@ var AgentLoop = class {
|
|
|
2253
2361
|
const rawDeps = metadata["dependencies"];
|
|
2254
2362
|
if (Array.isArray(rawDeps)) dependencies = rawDeps.filter((d) => typeof d === "string");
|
|
2255
2363
|
const rawAllowed = metadata["allowed-tools"];
|
|
2256
|
-
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed))
|
|
2257
|
-
|
|
2364
|
+
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) {
|
|
2365
|
+
allowedTools = rawAllowed.filter((e) => typeof e === "string");
|
|
2366
|
+
state.skillAllowedTools.set(skillName, allowedTools);
|
|
2367
|
+
} else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
2258
2368
|
} catch (e) {
|
|
2259
2369
|
state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf(e)}` });
|
|
2260
2370
|
}
|
|
@@ -2264,13 +2374,17 @@ var AgentLoop = class {
|
|
|
2264
2374
|
let scriptFiles;
|
|
2265
2375
|
try {
|
|
2266
2376
|
scriptFiles = (await this.#deps.fs.list(`${root}/scripts`)).filter((s) => s.type === "file").map((s) => baseName(s.path));
|
|
2267
|
-
} catch {
|
|
2377
|
+
} catch (e) {
|
|
2268
2378
|
scriptFiles = [];
|
|
2379
|
+
if (!(e instanceof WebSkillError && e.code === "FS_NOT_FOUND")) state.trace.record("run.warning", { message: `Failed to list scripts of skill "${skillName}": ${messageOf(e)}` });
|
|
2269
2380
|
}
|
|
2270
2381
|
for (const file of scriptFiles) {
|
|
2271
2382
|
const match = /^(.*)\.(ts|js)$/.exec(file);
|
|
2272
2383
|
if (!match?.[1]) continue;
|
|
2273
|
-
if (allowedTools && !allowedTools.includes(match[1]))
|
|
2384
|
+
if (allowedTools && !allowedTools.includes(match[1])) {
|
|
2385
|
+
state.trace.record("run.warning", { message: `Skill "${skillName}" declares allowed-tools, so its script "${match[1]}" is not registered as a tool. Add "${match[1]}" to allowed-tools if that was not intended.` });
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2274
2388
|
try {
|
|
2275
2389
|
const def = await executor.loadDefinition(root, match[1]);
|
|
2276
2390
|
await this.#enrichDefinition(root, match[1], def, state);
|
|
@@ -2402,10 +2516,11 @@ var AgentLoop = class {
|
|
|
2402
2516
|
#nextInteractionId(state) {
|
|
2403
2517
|
return `int-${++state.interactionSeq}`;
|
|
2404
2518
|
}
|
|
2405
|
-
async #memoryGet(scope, key) {
|
|
2519
|
+
async #memoryGet(scope, key, state) {
|
|
2406
2520
|
try {
|
|
2407
2521
|
return await this.#deps.memory?.get(scope, key);
|
|
2408
|
-
} catch {
|
|
2522
|
+
} catch (e) {
|
|
2523
|
+
state.trace.record("run.warning", { message: `Memory read failed: ${messageOf(e)}` });
|
|
2409
2524
|
return;
|
|
2410
2525
|
}
|
|
2411
2526
|
}
|
|
@@ -2430,7 +2545,7 @@ var AgentLoop = class {
|
|
|
2430
2545
|
}
|
|
2431
2546
|
return;
|
|
2432
2547
|
}
|
|
2433
|
-
await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key)), state);
|
|
2548
|
+
await this.#memorySet(scope, key, mutate(await this.#memoryGet(scope, key, state)), state);
|
|
2434
2549
|
}
|
|
2435
2550
|
async #writeActivationMemory(skillName, state) {
|
|
2436
2551
|
if (!this.#deps.memory) return;
|
|
@@ -2962,9 +3077,26 @@ const encode = (s) => encodeURIComponent(s);
|
|
|
2962
3077
|
var FsMemoryStore = class {
|
|
2963
3078
|
#root;
|
|
2964
3079
|
#fs;
|
|
3080
|
+
#onWarning;
|
|
2965
3081
|
constructor(deps) {
|
|
2966
3082
|
this.#root = deps.root.replace(/\/+$/, "");
|
|
2967
3083
|
this.#fs = deps.fs;
|
|
3084
|
+
this.#onWarning = deps.onWarning ?? ((message) => console.warn(message));
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* 0.2.8 G2:损坏条目此前直接 remove——静默销毁用户数据,且对调用方伪装成「没有这条记忆」。
|
|
3088
|
+
* 改为隔离到 .corrupt 并告警:run 照常继续,但数据留存、故障可见。
|
|
3089
|
+
*/
|
|
3090
|
+
async #quarantine(path, error) {
|
|
3091
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
3092
|
+
const target = `${path}.corrupt`;
|
|
3093
|
+
try {
|
|
3094
|
+
await this.#fs.rename(path, target);
|
|
3095
|
+
this.#onWarning(`[webskill] Corrupted memory entry quarantined to ${target}: ${reason}`);
|
|
3096
|
+
} catch (renameError) {
|
|
3097
|
+
const detail = renameError instanceof Error ? renameError.message : String(renameError);
|
|
3098
|
+
this.#onWarning(`[webskill] Corrupted memory entry at ${path} could not be quarantined (${detail}): ${reason}`);
|
|
3099
|
+
}
|
|
2968
3100
|
}
|
|
2969
3101
|
#scopeDir(scope) {
|
|
2970
3102
|
return `${this.#root}/${encode(scope)}`;
|
|
@@ -2977,8 +3109,8 @@ var FsMemoryStore = class {
|
|
|
2977
3109
|
if (!await this.#fs.exists(path)) return void 0;
|
|
2978
3110
|
try {
|
|
2979
3111
|
return JSON.parse(await this.#fs.readText(path));
|
|
2980
|
-
} catch {
|
|
2981
|
-
await this.#
|
|
3112
|
+
} catch (e) {
|
|
3113
|
+
await this.#quarantine(path, e);
|
|
2982
3114
|
return;
|
|
2983
3115
|
}
|
|
2984
3116
|
}
|
|
@@ -3002,8 +3134,8 @@ var FsMemoryStore = class {
|
|
|
3002
3134
|
key,
|
|
3003
3135
|
value: JSON.parse(await this.#fs.readText(entry.path))
|
|
3004
3136
|
});
|
|
3005
|
-
} catch {
|
|
3006
|
-
await this.#
|
|
3137
|
+
} catch (e) {
|
|
3138
|
+
await this.#quarantine(entry.path, e);
|
|
3007
3139
|
}
|
|
3008
3140
|
}
|
|
3009
3141
|
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
@@ -3018,7 +3150,7 @@ var FsMemoryStore = class {
|
|
|
3018
3150
|
for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
|
|
3019
3151
|
}
|
|
3020
3152
|
};
|
|
3021
|
-
const INDEX_FILE = "index.json";
|
|
3153
|
+
const INDEX_FILE$1 = "index.json";
|
|
3022
3154
|
/**
|
|
3023
3155
|
* 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
|
|
3024
3156
|
* 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
|
|
@@ -3027,9 +3159,11 @@ const INDEX_FILE = "index.json";
|
|
|
3027
3159
|
var FsArtifactStore = class {
|
|
3028
3160
|
#root;
|
|
3029
3161
|
#fs;
|
|
3162
|
+
#onWarning;
|
|
3030
3163
|
constructor(deps) {
|
|
3031
3164
|
this.#root = deps.root.replace(/\/+$/, "");
|
|
3032
3165
|
this.#fs = deps.fs;
|
|
3166
|
+
this.#onWarning = deps.onWarning ?? ((message) => console.warn(message));
|
|
3033
3167
|
}
|
|
3034
3168
|
async createTextArtifact(input) {
|
|
3035
3169
|
const size = new TextEncoder().encode(input.content).length;
|
|
@@ -3050,13 +3184,20 @@ var FsArtifactStore = class {
|
|
|
3050
3184
|
}
|
|
3051
3185
|
async listArtifacts(runId) {
|
|
3052
3186
|
assertSafePathSegment(runId, "runId");
|
|
3053
|
-
const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
|
|
3187
|
+
const indexPath = `${this.#root}/${runId}/${INDEX_FILE$1}`;
|
|
3054
3188
|
if (!await this.#fs.exists(indexPath)) return [];
|
|
3055
3189
|
const raw = await this.#fs.readText(indexPath);
|
|
3056
3190
|
try {
|
|
3057
3191
|
return JSON.parse(raw).artifacts ?? [];
|
|
3058
|
-
} catch {
|
|
3059
|
-
|
|
3192
|
+
} catch (e) {
|
|
3193
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
3194
|
+
try {
|
|
3195
|
+
await this.#fs.rename(indexPath, `${indexPath}.corrupt`);
|
|
3196
|
+
this.#onWarning(`[webskill] Corrupted artifact index for run ${runId} quarantined to ${indexPath}.corrupt: ${reason}`);
|
|
3197
|
+
} catch (renameError) {
|
|
3198
|
+
const detail = renameError instanceof Error ? renameError.message : String(renameError);
|
|
3199
|
+
this.#onWarning(`[webskill] Corrupted artifact index for run ${runId} could not be quarantined (${detail}): ${reason}`);
|
|
3200
|
+
}
|
|
3060
3201
|
return [];
|
|
3061
3202
|
}
|
|
3062
3203
|
}
|
|
@@ -3076,7 +3217,7 @@ var FsArtifactStore = class {
|
|
|
3076
3217
|
metadata: input.metadata
|
|
3077
3218
|
};
|
|
3078
3219
|
const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
|
|
3079
|
-
await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
|
|
3220
|
+
await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE$1}`, JSON.stringify({ artifacts: next }, null, 2));
|
|
3080
3221
|
return artifact;
|
|
3081
3222
|
}
|
|
3082
3223
|
};
|
|
@@ -3170,6 +3311,20 @@ function networkUrlHost(url) {
|
|
|
3170
3311
|
return "(unparseable-url)";
|
|
3171
3312
|
}
|
|
3172
3313
|
}
|
|
3314
|
+
/**
|
|
3315
|
+
* 网络策略判定逻辑的可注入源码(单一来源)。
|
|
3316
|
+
*
|
|
3317
|
+
* 0.2.8 C4:此前各注入点直接拼 `isNetworkAllowed.toString()`,依赖**函数名在产物里保持不变**。
|
|
3318
|
+
* SDK 自身不压缩,但消费方一旦跑生产构建,打包器会把导出函数改名(`function Ke(...)`),
|
|
3319
|
+
* 注入后的沙箱里 `isNetworkAllowed` 就是 undefined —— 沙箱内任何 fetch 直接
|
|
3320
|
+
* TOOL_EXECUTION_FAILED,网络白名单形同虚设。dev server 不压缩,所以只在真实产物上暴露。
|
|
3321
|
+
*
|
|
3322
|
+
* 因此改为把函数源码绑定到**固定的变量名**上:`var isNetworkAllowed = function Ke(...) {…};`
|
|
3323
|
+
* ——名字随便压缩,绑定名恒定。两个函数都自包含(不引用模块内其它符号),故可独立绑定。
|
|
3324
|
+
*/
|
|
3325
|
+
function networkPolicyLibSource() {
|
|
3326
|
+
return `var isNetworkAllowed = ${isNetworkAllowed.toString()};\nvar networkUrlHost = ${networkUrlHost.toString()};`;
|
|
3327
|
+
}
|
|
3173
3328
|
/** WebSkillErrorCode 全量白名单(错误码归一用;与 core errors.ts 保持同步) */
|
|
3174
3329
|
const WHITELIST = /* @__PURE__ */ new Set([
|
|
3175
3330
|
"FS_NOT_FOUND",
|
|
@@ -3281,6 +3436,388 @@ var CapabilityApproval = class CapabilityApproval {
|
|
|
3281
3436
|
return "allowed";
|
|
3282
3437
|
}
|
|
3283
3438
|
};
|
|
3439
|
+
const RUN_TRACE_SCHEMA_VERSION = 1;
|
|
3440
|
+
/** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
|
|
3441
|
+
function extractEndReason(events) {
|
|
3442
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
3443
|
+
const event = events[i];
|
|
3444
|
+
if (event.type === "run.completed" || event.type === "run.cancelled" || event.type === "run.failed") {
|
|
3445
|
+
const reason = event.data?.["reason"];
|
|
3446
|
+
return typeof reason === "string" ? reason : void 0;
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
function summarize(trace) {
|
|
3451
|
+
const endReason = extractEndReason(trace.events);
|
|
3452
|
+
const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
|
|
3453
|
+
return {
|
|
3454
|
+
runId: trace.runId,
|
|
3455
|
+
startedAt: trace.startedAt,
|
|
3456
|
+
status: trace.status,
|
|
3457
|
+
activeSkills: trace.activeSkills,
|
|
3458
|
+
eventCount: trace.events.length,
|
|
3459
|
+
turnCount: trace.events.filter((e) => e.type === "llm.request").length,
|
|
3460
|
+
...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
|
|
3461
|
+
...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
|
|
3462
|
+
...endReason !== void 0 ? { endReason } : {},
|
|
3463
|
+
...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function parseTraceFile(raw, path) {
|
|
3467
|
+
let data;
|
|
3468
|
+
try {
|
|
3469
|
+
data = JSON.parse(raw);
|
|
3470
|
+
} catch {
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
3474
|
+
if (typeof data.runId !== "string" || typeof data.startedAt !== "string") return void 0;
|
|
3475
|
+
const schemaVersion = typeof data.schemaVersion === "number" ? data.schemaVersion : 0;
|
|
3476
|
+
if (schemaVersion > 1) throw new WebSkillError("RUN_TRACE_INCOMPATIBLE", `Run trace "${path}" declares schemaVersion ${schemaVersion}, which is newer than the supported 1`);
|
|
3477
|
+
return {
|
|
3478
|
+
schemaVersion,
|
|
3479
|
+
runId: data.runId,
|
|
3480
|
+
...typeof data.sessionId === "string" ? { sessionId: data.sessionId } : {},
|
|
3481
|
+
startedAt: data.startedAt,
|
|
3482
|
+
...typeof data.endedAt === "string" ? { endedAt: data.endedAt } : {},
|
|
3483
|
+
status: typeof data.status === "string" ? data.status : "unknown",
|
|
3484
|
+
activeSkills: Array.isArray(data.activeSkills) ? data.activeSkills.filter((s) => typeof s === "string") : [],
|
|
3485
|
+
events: Array.isArray(data.events) ? data.events : []
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
const INDEX_FILE = "index.jsonl";
|
|
3489
|
+
/**
|
|
3490
|
+
* `FileSystemProvider` 后端:`<root>/<runId>.json` 存全量 trace,
|
|
3491
|
+
* `<root>/index.jsonl` 是 append-only 的摘要旁路索引。
|
|
3492
|
+
*
|
|
3493
|
+
* 索引存在的唯一理由是**读次数**:0.0.1 的实现为了拼一份列表要整读每个 trace
|
|
3494
|
+
* 文件,点一次指标再来一遍。有了索引,`list()` 与 `metrics()` 各只读一个文件。
|
|
3495
|
+
*
|
|
3496
|
+
* 索引与 trace 目录发散(写完 trace 崩在追加前)的检测不引入新的读放大:
|
|
3497
|
+
* 目录列举本来就要做,比较 `*.json` 个数与索引行数即可;不等则整体重建。
|
|
3498
|
+
* @stable
|
|
3499
|
+
*/
|
|
3500
|
+
var FsRunTraceStore = class {
|
|
3501
|
+
#root;
|
|
3502
|
+
#fs;
|
|
3503
|
+
#onError;
|
|
3504
|
+
constructor(deps) {
|
|
3505
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
3506
|
+
this.#fs = deps.fs;
|
|
3507
|
+
this.#onError = deps.onError ?? ((error, run) => {
|
|
3508
|
+
console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
#path(runId) {
|
|
3512
|
+
return resolveInsideRoot(this.#root, `${runId}.json`);
|
|
3513
|
+
}
|
|
3514
|
+
async put(run) {
|
|
3515
|
+
const trace = {
|
|
3516
|
+
schemaVersion: 1,
|
|
3517
|
+
runId: run.id,
|
|
3518
|
+
sessionId: run.sessionId,
|
|
3519
|
+
startedAt: run.startedAt,
|
|
3520
|
+
...run.endedAt !== void 0 ? { endedAt: run.endedAt } : {},
|
|
3521
|
+
status: run.status === "completed" || run.status === "cancelled" ? run.status : "failed",
|
|
3522
|
+
activeSkills: run.activeSkillNames,
|
|
3523
|
+
events: run.trace
|
|
3524
|
+
};
|
|
3525
|
+
try {
|
|
3526
|
+
await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
|
|
3527
|
+
await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
|
|
3528
|
+
} catch (e) {
|
|
3529
|
+
this.#onError(e, run);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
async get(runId) {
|
|
3533
|
+
assertSafePathSegment(runId, "run id");
|
|
3534
|
+
const path = this.#path(runId);
|
|
3535
|
+
let raw;
|
|
3536
|
+
try {
|
|
3537
|
+
raw = await this.#fs.readText(path);
|
|
3538
|
+
} catch (e) {
|
|
3539
|
+
if (e instanceof WebSkillError) throw e;
|
|
3540
|
+
throw new WebSkillError("FS_NOT_FOUND", `Trace not found for run ${JSON.stringify(runId)}: ${path}`);
|
|
3541
|
+
}
|
|
3542
|
+
const trace = parseTraceFile(raw, path);
|
|
3543
|
+
if (!trace) throw new WebSkillError("VALIDATION_FAILED", `Trace file is corrupted or malformed: ${path}`);
|
|
3544
|
+
return trace;
|
|
3545
|
+
}
|
|
3546
|
+
async list(filter = {}) {
|
|
3547
|
+
const { summaries } = await this.#readIndex();
|
|
3548
|
+
const matched = applyFilter(summaries, filter);
|
|
3549
|
+
const offset = Math.max(0, filter.offset ?? 0);
|
|
3550
|
+
const sliced = matched.slice(offset);
|
|
3551
|
+
return filter.limit !== void 0 ? sliced.slice(0, Math.max(0, filter.limit)) : sliced;
|
|
3552
|
+
}
|
|
3553
|
+
async metrics(filter = {}) {
|
|
3554
|
+
const { summaries, skipped } = await this.#readIndex();
|
|
3555
|
+
const runs = applyFilter(summaries, filter);
|
|
3556
|
+
const succeeded = runs.filter((r) => r.status === "completed").length;
|
|
3557
|
+
const failedRuns = runs.filter((r) => r.status === "failed");
|
|
3558
|
+
const durations = runs.map((r) => r.durationMs).filter((ms) => ms !== void 0);
|
|
3559
|
+
return {
|
|
3560
|
+
totalRuns: runs.length,
|
|
3561
|
+
succeeded,
|
|
3562
|
+
failed: failedRuns.length,
|
|
3563
|
+
successRate: runs.length === 0 ? 0 : succeeded / runs.length,
|
|
3564
|
+
avgTurns: runs.length === 0 ? 0 : runs.reduce((sum, r) => sum + r.turnCount, 0) / runs.length,
|
|
3565
|
+
skippedRuns: skipped,
|
|
3566
|
+
avgDurationMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
3567
|
+
recentFailures: failedRuns.slice(0, 5)
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
/** 健康路径恒为「一次目录列举 + 一次索引读」,与 run 数无关。 */
|
|
3571
|
+
async #readIndex() {
|
|
3572
|
+
if (!await this.#fs.exists(this.#root)) return {
|
|
3573
|
+
summaries: [],
|
|
3574
|
+
skipped: 0
|
|
3575
|
+
};
|
|
3576
|
+
const traceCount = (await this.#fs.list(this.#root)).filter((f) => f.type === "file" && f.path.endsWith(".json")).length;
|
|
3577
|
+
const indexPath = `${this.#root}/${INDEX_FILE}`;
|
|
3578
|
+
let lines = [];
|
|
3579
|
+
if (await this.#fs.exists(indexPath)) lines = (await this.#fs.readText(indexPath)).split("\n").filter((line) => line.trim() !== "");
|
|
3580
|
+
if (lines.length !== traceCount) return {
|
|
3581
|
+
summaries: await this.#rebuildIndex(),
|
|
3582
|
+
skipped: 0
|
|
3583
|
+
};
|
|
3584
|
+
const summaries = [];
|
|
3585
|
+
let skipped = 0;
|
|
3586
|
+
for (const line of lines) try {
|
|
3587
|
+
summaries.push(JSON.parse(line));
|
|
3588
|
+
} catch {
|
|
3589
|
+
skipped += 1;
|
|
3590
|
+
}
|
|
3591
|
+
if (skipped > 0) return {
|
|
3592
|
+
summaries: await this.#rebuildIndex(),
|
|
3593
|
+
skipped
|
|
3594
|
+
};
|
|
3595
|
+
return {
|
|
3596
|
+
summaries: sortByStartedAtDesc(summaries),
|
|
3597
|
+
skipped: 0
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
/** 异常路径,允许 O(N):整读全部 trace 文件后用 temp + rename 原子替换索引 */
|
|
3601
|
+
async #rebuildIndex() {
|
|
3602
|
+
const summaries = [];
|
|
3603
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
3604
|
+
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
3605
|
+
let trace;
|
|
3606
|
+
try {
|
|
3607
|
+
trace = parseTraceFile(await this.#fs.readText(entry.path), entry.path);
|
|
3608
|
+
} catch {
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
if (!trace) continue;
|
|
3612
|
+
summaries.push(summarize(trace));
|
|
3613
|
+
}
|
|
3614
|
+
const sorted = sortByStartedAtDesc(summaries);
|
|
3615
|
+
const indexPath = `${this.#root}/${INDEX_FILE}`;
|
|
3616
|
+
const tempPath = `${this.#root}/${INDEX_FILE}.rebuilding`;
|
|
3617
|
+
try {
|
|
3618
|
+
await this.#fs.writeText(tempPath, sorted.map((s) => `${JSON.stringify(s)}\n`).join(""));
|
|
3619
|
+
await this.#fs.rename(tempPath, indexPath);
|
|
3620
|
+
} catch {
|
|
3621
|
+
await this.#fs.remove(tempPath).catch(() => void 0);
|
|
3622
|
+
}
|
|
3623
|
+
return sorted;
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
/** ISO 时间戳可按字典序比较 */
|
|
3627
|
+
function sortByStartedAtDesc(summaries) {
|
|
3628
|
+
return [...summaries].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
3629
|
+
}
|
|
3630
|
+
function applyFilter(summaries, filter) {
|
|
3631
|
+
let runs = summaries;
|
|
3632
|
+
if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
|
|
3633
|
+
const q = filter.search?.trim().toLowerCase();
|
|
3634
|
+
if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
|
|
3635
|
+
return runs;
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* 从 run 的 trace 推导终态工具调用列表。
|
|
3639
|
+
*
|
|
3640
|
+
* 消费者过去要靠自己遍历 trace 才能拿到这份列表,而遍历 trace 的同一段代码
|
|
3641
|
+
* 又顺手承担了「补发漏掉的 live 事件」的职责——两件事绑在一起,谁也删不掉。
|
|
3642
|
+
* 幂等归 runtime(见 `AgentLoop` 的 per-run 已发集合)之后,
|
|
3643
|
+
* 推导终态列表就是纯函数,独立导出。
|
|
3644
|
+
* @stable
|
|
3645
|
+
*/
|
|
3646
|
+
function summarizeToolCalls(run) {
|
|
3647
|
+
const calls = [];
|
|
3648
|
+
for (const event of run.trace) {
|
|
3649
|
+
if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
|
|
3650
|
+
const name = event.data?.["name"];
|
|
3651
|
+
const callId = event.data?.["callId"];
|
|
3652
|
+
if (typeof name !== "string" || typeof callId !== "string") continue;
|
|
3653
|
+
const args = event.data?.["args"];
|
|
3654
|
+
const durationMs = event.data?.["durationMs"];
|
|
3655
|
+
calls.push({
|
|
3656
|
+
callId,
|
|
3657
|
+
name,
|
|
3658
|
+
status: event.type === "tool.completed" ? "completed" : "failed",
|
|
3659
|
+
...typeof args === "string" ? { args } : {},
|
|
3660
|
+
...typeof durationMs === "number" ? { durationMs } : {}
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3663
|
+
return calls;
|
|
3664
|
+
}
|
|
3665
|
+
const SESSION_SCHEMA_VERSION = 1;
|
|
3666
|
+
const toMeta = (record) => ({
|
|
3667
|
+
id: record.id,
|
|
3668
|
+
createdAt: record.createdAt,
|
|
3669
|
+
...record.title !== void 0 ? { title: record.title } : {},
|
|
3670
|
+
...record.titleLocked === true ? { titleLocked: true } : {},
|
|
3671
|
+
...record.archived === true ? { archived: true } : {},
|
|
3672
|
+
messageCount: record.messages.length
|
|
3673
|
+
});
|
|
3674
|
+
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3675
|
+
/**
|
|
3676
|
+
* 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
|
|
3677
|
+
* 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
|
|
3678
|
+
*/
|
|
3679
|
+
function parseSessionFile(raw, path) {
|
|
3680
|
+
let parsed;
|
|
3681
|
+
try {
|
|
3682
|
+
parsed = JSON.parse(raw);
|
|
3683
|
+
} catch (e) {
|
|
3684
|
+
throw new WebSkillError("VALIDATION_FAILED", `Session file is not valid JSON: ${path}`, e);
|
|
3685
|
+
}
|
|
3686
|
+
if (typeof parsed !== "object" || parsed === null) throw new WebSkillError("VALIDATION_FAILED", `Session file is not an object: ${path}`);
|
|
3687
|
+
const file = parsed;
|
|
3688
|
+
const schemaVersion = typeof file.schemaVersion === "number" ? file.schemaVersion : 0;
|
|
3689
|
+
if (schemaVersion > 1) throw new WebSkillError("SESSION_INCOMPATIBLE", `Session file at ${path} has schemaVersion ${schemaVersion}, but this build understands at most 1`);
|
|
3690
|
+
if (typeof file.id !== "string" || typeof file.createdAt !== "string") throw new WebSkillError("VALIDATION_FAILED", `Session file is missing "id" or "createdAt": ${path}`);
|
|
3691
|
+
return {
|
|
3692
|
+
schemaVersion,
|
|
3693
|
+
id: file.id,
|
|
3694
|
+
createdAt: file.createdAt,
|
|
3695
|
+
...typeof file.title === "string" ? { title: file.title } : {},
|
|
3696
|
+
...file.titleLocked === true ? { titleLocked: true } : {},
|
|
3697
|
+
...file.archived === true ? { archived: true } : {},
|
|
3698
|
+
messages: Array.isArray(file.messages) ? file.messages : [],
|
|
3699
|
+
messageCount: Array.isArray(file.messages) ? file.messages.length : 0
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
/**
|
|
3703
|
+
* `FileSystemProvider` 后端的会话存储:`<root>/<id>.json`。
|
|
3704
|
+
*
|
|
3705
|
+
* 所有变更操作按 id 串到一条 promise 链上:会话文件是整读整写的,
|
|
3706
|
+
* 两个并发 `appendMessages` 若都先读后写,后写的会覆盖先写的那条消息。
|
|
3707
|
+
*/
|
|
3708
|
+
var FsSessionStore = class {
|
|
3709
|
+
#root;
|
|
3710
|
+
#fs;
|
|
3711
|
+
/** id → 该 id 上最后一次变更的完成时点,用于串行化读改写 */
|
|
3712
|
+
#writes = /* @__PURE__ */ new Map();
|
|
3713
|
+
constructor(deps) {
|
|
3714
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
3715
|
+
this.#fs = deps.fs;
|
|
3716
|
+
}
|
|
3717
|
+
#path(id) {
|
|
3718
|
+
assertSafePathSegment(id, "session id");
|
|
3719
|
+
return resolveInsideRoot(this.#root, `${id}.json`);
|
|
3720
|
+
}
|
|
3721
|
+
/** 把 mutation 排到该 id 的队尾;前一个失败不阻塞后一个 */
|
|
3722
|
+
async #serialize(id, work) {
|
|
3723
|
+
const next = (this.#writes.get(id) ?? Promise.resolve()).catch(() => void 0).then(work);
|
|
3724
|
+
this.#writes.set(id, next);
|
|
3725
|
+
try {
|
|
3726
|
+
return await next;
|
|
3727
|
+
} finally {
|
|
3728
|
+
if (this.#writes.get(id) === next) this.#writes.delete(id);
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
async #write(record) {
|
|
3732
|
+
const file = {
|
|
3733
|
+
schemaVersion: 1,
|
|
3734
|
+
id: record.id,
|
|
3735
|
+
createdAt: record.createdAt,
|
|
3736
|
+
...record.title !== void 0 ? { title: record.title } : {},
|
|
3737
|
+
...record.titleLocked === true ? { titleLocked: true } : {},
|
|
3738
|
+
...record.archived === true ? { archived: true } : {},
|
|
3739
|
+
messages: record.messages
|
|
3740
|
+
};
|
|
3741
|
+
await this.#fs.writeText(this.#path(record.id), JSON.stringify(file, null, 2));
|
|
3742
|
+
}
|
|
3743
|
+
async #require(id) {
|
|
3744
|
+
const record = await this.get(id);
|
|
3745
|
+
if (!record) throw new WebSkillError("FS_NOT_FOUND", `Session "${id}" not found under ${this.#root}`);
|
|
3746
|
+
return record;
|
|
3747
|
+
}
|
|
3748
|
+
async list(options = {}) {
|
|
3749
|
+
if (!await this.#fs.exists(this.#root)) return [];
|
|
3750
|
+
const metas = [];
|
|
3751
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
3752
|
+
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
3753
|
+
let record;
|
|
3754
|
+
try {
|
|
3755
|
+
record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
|
|
3756
|
+
} catch (e) {
|
|
3757
|
+
console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
if (record.archived === true && options.includeArchived !== true) continue;
|
|
3761
|
+
metas.push(toMeta(record));
|
|
3762
|
+
}
|
|
3763
|
+
return metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3764
|
+
}
|
|
3765
|
+
async get(id) {
|
|
3766
|
+
const path = this.#path(id);
|
|
3767
|
+
if (!await this.#fs.exists(path)) return void 0;
|
|
3768
|
+
return parseSessionFile(await this.#fs.readText(path), path);
|
|
3769
|
+
}
|
|
3770
|
+
async create(init = {}) {
|
|
3771
|
+
const record = {
|
|
3772
|
+
schemaVersion: 1,
|
|
3773
|
+
id: init.id ?? newSessionId(),
|
|
3774
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3775
|
+
...init.title !== void 0 ? { title: init.title } : {},
|
|
3776
|
+
messages: [],
|
|
3777
|
+
messageCount: 0
|
|
3778
|
+
};
|
|
3779
|
+
return this.#serialize(record.id, async () => {
|
|
3780
|
+
await this.#write(record);
|
|
3781
|
+
return toMeta(record);
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
async appendMessages(id, messages) {
|
|
3785
|
+
if (messages.length === 0) return;
|
|
3786
|
+
await this.#serialize(id, async () => {
|
|
3787
|
+
const record = await this.#require(id);
|
|
3788
|
+
record.messages.push(...messages);
|
|
3789
|
+
await this.#write(record);
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
async replaceMessages(id, messages) {
|
|
3793
|
+
await this.#serialize(id, async () => {
|
|
3794
|
+
const record = await this.#require(id);
|
|
3795
|
+
record.messages = [...messages];
|
|
3796
|
+
await this.#write(record);
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
async setTitle(id, title, options = {}) {
|
|
3800
|
+
await this.#serialize(id, async () => {
|
|
3801
|
+
const record = await this.#require(id);
|
|
3802
|
+
record.title = title;
|
|
3803
|
+
if (options.lock === true) record.titleLocked = true;
|
|
3804
|
+
await this.#write(record);
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
async setArchived(id, archived) {
|
|
3808
|
+
await this.#serialize(id, async () => {
|
|
3809
|
+
const record = await this.#require(id);
|
|
3810
|
+
record.archived = archived;
|
|
3811
|
+
await this.#write(record);
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
async delete(id) {
|
|
3815
|
+
await this.#serialize(id, async () => {
|
|
3816
|
+
const path = this.#path(id);
|
|
3817
|
+
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
};
|
|
3284
3821
|
|
|
3285
3822
|
//#endregion
|
|
3286
|
-
export {
|
|
3823
|
+
export { createWebSkillApi as A, normalizeToolContent as B, SESSION_SCHEMA_VERSION as C, bridgeError as D, WebSkillRuntime as E, isNetworkAllowed as F, summarizeToolCalls as G, parseBridgeRequest as H, mergeCatalogEntries as I, validateUiSurface as J, toLlmToolSpec as K, networkPolicyLibSource as L, extractUiSurfaceEvents as M, fromVercelResult as N, buildRenderResult as O, fromVercelStreamPart as P, networkUrlHost as R, RUN_TRACE_SCHEMA_VERSION as S, TraceRecorder as T, resolveToolName as U, normalizeToolError as V, schemaToForm as W, validateUiSurfaceEvent as Y, ProgressiveRouter as _, AnthropicClient as a, READ_SKILL_FILE_TOOL_NAME as b, FsArtifactStore as c, FsRunTraceStore as d, FsSessionStore as f, OpenAiCompatibleClient as g, HookRunner as h, AgentLoop as i, extractChartSpec as j, createScriptContext as k, FsMemoryStore as l, GoogleGenAiClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, FullDisclosureRouter as p, toVercelToolSpecs as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_INPUT_SCHEMA as v, SerializingMemoryStore as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, normalizeErrorCode as z };
|