@webskill/sdk 0.2.8 → 0.4.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 +2 -2
- package/dist/browser.js +4 -9
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-KsujmL4b-Clx1kCnU.js} +278 -122
- package/dist/client-BCM6Z3yq-qUQNkKrK.js +7787 -0
- package/dist/{dist-BQzncxXg.js → dist-8oQRa8Xz.js} +212 -13
- package/dist/{dist-B9VLwOME.js → dist-C-Sh0MDU.js} +1019 -317
- package/dist/{dist-CtBLBbEz.js → dist-D9Lcn5Pp.js} +528 -838
- package/dist/governance.d.ts +46 -11
- package/dist/governance.js +152 -25
- package/dist/{index-7DVaZJU7.d.ts → index-CHXxDccV.d.ts} +62 -144
- package/dist/{index-QrHtAudz.d.ts → index-DLfR2Y6I.d.ts} +412 -21
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
- package/dist/node.d.ts +297 -4
- package/dist/node.js +281 -7
- package/dist/{openUiLibrary-B8-Cvou9-D3RsU2EB.js → openUiLibrary-YLS-cxyT-C96jWDQq.js} +6 -5
- package/dist/skillVersionStore-uyefLPR1-DXOzbksv.d.ts +158 -0
- package/dist/stdio-CFMoANJJ-BxrTeXh7.js +31 -0
- package/dist/{testing-BUoXvm1u.js → testing-DDCJWvgA.js} +8 -6
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-AmKCKJn_-BogJPQHU.d.ts → types-7Wcg--Vh-1YlQ4jF9.d.ts} +219 -70
- package/dist/types-WovEf4ED-CZSDiiBU.js +6215 -0
- package/dist/ui-react.d.ts +329 -18
- package/dist/ui-react.js +3715 -3464
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CNaUpasU-CfSRvqCZ.js → webskillLitCatalog-CSTbhBe_-CYIs5BX8.js} +312 -122
- package/package.json +4 -7
- package/dist/jsonRenderRegistry-9GrWP_hE-CQY8bT9w.js +0 -2468
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
- package/dist/skillVersionStore-B7rGjtMi-BgnQho9v.d.ts +0 -365
|
@@ -1,28 +1,111 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as MemoryArtifactStore } from "./memoryArtifactStore-
|
|
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
|
+
import { a as validateLlmMessages, i as textParts, n as partsToText, r as rejectUnsupportedPart, t as MemoryArtifactStore } from "./memoryArtifactStore-BtOeB_hm-tj3fC5ip.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
|
+
}
|
|
50
|
+
const dataUrl = (part) => `data:${part.mimeType};base64,${part.data}`;
|
|
51
|
+
/** parts → OpenAI content;纯文本折叠成字符串(兼容端点对数组形态支持不一) */
|
|
52
|
+
const toOpenAiContent = (parts, where) => {
|
|
53
|
+
if (parts.every((p) => p.type === "text")) return partsToText(parts);
|
|
54
|
+
return parts.map((part) => {
|
|
55
|
+
switch (part.type) {
|
|
56
|
+
case "text": return {
|
|
57
|
+
type: "text",
|
|
58
|
+
text: part.text
|
|
59
|
+
};
|
|
60
|
+
case "image": return {
|
|
61
|
+
type: "image_url",
|
|
62
|
+
image_url: { url: dataUrl(part) }
|
|
63
|
+
};
|
|
64
|
+
case "file": return {
|
|
65
|
+
type: "file",
|
|
66
|
+
file: {
|
|
67
|
+
...part.name !== void 0 ? { filename: part.name } : {},
|
|
68
|
+
file_data: dataUrl(part)
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
default: return rejectUnsupportedPart(part, "OpenAI", where);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
/** system / tool 消息只接受文本:这两类在 OpenAI 协议里没有多模态形态 */
|
|
76
|
+
const toOpenAiText = (parts, where) => {
|
|
77
|
+
const nonText = parts.find((p) => p.type !== "text");
|
|
78
|
+
if (nonText) rejectUnsupportedPart(nonText, "OpenAI", where);
|
|
79
|
+
return partsToText(parts);
|
|
80
|
+
};
|
|
5
81
|
const toOpenAiMessage = (msg) => {
|
|
6
82
|
if (msg.role === "tool") return {
|
|
7
83
|
role: "tool",
|
|
8
84
|
tool_call_id: msg.toolCallId,
|
|
9
|
-
content: msg.content
|
|
85
|
+
content: toOpenAiText(msg.content, "tool")
|
|
10
86
|
};
|
|
11
|
-
if (msg.role === "
|
|
12
|
-
role: "
|
|
13
|
-
content: msg.content
|
|
14
|
-
tool_calls: msg.toolCalls.map((call) => ({
|
|
15
|
-
id: call.id,
|
|
16
|
-
type: "function",
|
|
17
|
-
function: {
|
|
18
|
-
name: call.name,
|
|
19
|
-
arguments: JSON.stringify(call.arguments)
|
|
20
|
-
}
|
|
21
|
-
}))
|
|
87
|
+
if (msg.role === "system") return {
|
|
88
|
+
role: "system",
|
|
89
|
+
content: toOpenAiText(msg.content, "system")
|
|
22
90
|
};
|
|
91
|
+
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
92
|
+
const content = toOpenAiContent(msg.content, "assistant");
|
|
93
|
+
return {
|
|
94
|
+
role: "assistant",
|
|
95
|
+
content: content === "" ? null : content,
|
|
96
|
+
tool_calls: msg.toolCalls.map((call) => ({
|
|
97
|
+
id: call.id,
|
|
98
|
+
type: "function",
|
|
99
|
+
function: {
|
|
100
|
+
name: call.name,
|
|
101
|
+
arguments: JSON.stringify(call.arguments)
|
|
102
|
+
}
|
|
103
|
+
}))
|
|
104
|
+
};
|
|
105
|
+
}
|
|
23
106
|
return {
|
|
24
107
|
role: msg.role,
|
|
25
|
-
content: msg.content
|
|
108
|
+
content: toOpenAiContent(msg.content, msg.role)
|
|
26
109
|
};
|
|
27
110
|
};
|
|
28
111
|
const toOpenAiTools = (tools) => tools.map((tool) => ({
|
|
@@ -68,7 +151,7 @@ var OpenAiCompatibleClient = class {
|
|
|
68
151
|
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
69
152
|
const decoder = new TextDecoder();
|
|
70
153
|
const reader = res.body.getReader();
|
|
71
|
-
|
|
154
|
+
const frames = createSseFrameReader();
|
|
72
155
|
let done = false;
|
|
73
156
|
const handleFrame = function* (data) {
|
|
74
157
|
if (data === "[DONE]") {
|
|
@@ -104,19 +187,12 @@ var OpenAiCompatibleClient = class {
|
|
|
104
187
|
try {
|
|
105
188
|
for (;;) {
|
|
106
189
|
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());
|
|
190
|
+
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
191
|
+
for (const payload of payloads) {
|
|
192
|
+
yield* handleFrame(payload);
|
|
117
193
|
if (done) break;
|
|
118
194
|
}
|
|
119
|
-
if (done) break;
|
|
195
|
+
if (done || readerDone) break;
|
|
120
196
|
}
|
|
121
197
|
} finally {
|
|
122
198
|
reader.releaseLock();
|
|
@@ -143,6 +219,7 @@ var OpenAiCompatibleClient = class {
|
|
|
143
219
|
}
|
|
144
220
|
async #postChat(input, stream) {
|
|
145
221
|
const { baseUrl, apiKey, model } = this.#requireConfig();
|
|
222
|
+
validateLlmMessages(input.messages);
|
|
146
223
|
const body = {
|
|
147
224
|
model: input.model ?? model,
|
|
148
225
|
messages: input.messages.map(toOpenAiMessage)
|
|
@@ -196,7 +273,7 @@ var OpenAiCompatibleClient = class {
|
|
|
196
273
|
});
|
|
197
274
|
const content = choice["content"];
|
|
198
275
|
return {
|
|
199
|
-
content: typeof content === "string" ? content : void 0,
|
|
276
|
+
content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
|
|
200
277
|
toolCalls: toolCalls?.length ? toolCalls : void 0,
|
|
201
278
|
raw: data
|
|
202
279
|
};
|
|
@@ -207,9 +284,41 @@ var OpenAiCompatibleClient = class {
|
|
|
207
284
|
};
|
|
208
285
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
209
286
|
const errorMessage$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
287
|
+
/** parts → Anthropic content blocks(image/document 均为 base64 source) */
|
|
288
|
+
const toAnthropicBlocks = (parts, where) => parts.map((part) => {
|
|
289
|
+
switch (part.type) {
|
|
290
|
+
case "text": return {
|
|
291
|
+
type: "text",
|
|
292
|
+
text: part.text
|
|
293
|
+
};
|
|
294
|
+
case "image": return {
|
|
295
|
+
type: "image",
|
|
296
|
+
source: {
|
|
297
|
+
type: "base64",
|
|
298
|
+
media_type: part.mimeType,
|
|
299
|
+
data: part.data
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
case "file": return {
|
|
303
|
+
type: "document",
|
|
304
|
+
source: {
|
|
305
|
+
type: "base64",
|
|
306
|
+
media_type: part.mimeType,
|
|
307
|
+
data: part.data
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
default: return rejectUnsupportedPart(part, "Anthropic", where);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
/** system 是独立的字符串参数,无多模态形态 */
|
|
314
|
+
const toAnthropicSystemText = (parts) => {
|
|
315
|
+
const nonText = parts.find((p) => p.type !== "text");
|
|
316
|
+
if (nonText) rejectUnsupportedPart(nonText, "Anthropic", "system");
|
|
317
|
+
return partsToText(parts);
|
|
318
|
+
};
|
|
210
319
|
/** LlmMessage 序列 → system 独立参数 + user/assistant 消息(tool 结果合并进 user 消息 tool_result blocks) */
|
|
211
320
|
function toAnthropicMessages(messages) {
|
|
212
|
-
const system = messages.filter((m) => m.role === "system").map((m) => m.content).join("\n");
|
|
321
|
+
const system = messages.filter((m) => m.role === "system").map((m) => toAnthropicSystemText(m.content)).join("\n");
|
|
213
322
|
const out = [];
|
|
214
323
|
for (const msg of messages) {
|
|
215
324
|
if (msg.role === "system") continue;
|
|
@@ -217,7 +326,7 @@ function toAnthropicMessages(messages) {
|
|
|
217
326
|
const block = {
|
|
218
327
|
type: "tool_result",
|
|
219
328
|
tool_use_id: msg.toolCallId ?? "",
|
|
220
|
-
content: msg.content
|
|
329
|
+
content: toAnthropicBlocks(msg.content, "tool")
|
|
221
330
|
};
|
|
222
331
|
const last = out.at(-1);
|
|
223
332
|
if (last && last.role === "user" && Array.isArray(last.content)) last.content.push(block);
|
|
@@ -228,11 +337,7 @@ function toAnthropicMessages(messages) {
|
|
|
228
337
|
continue;
|
|
229
338
|
}
|
|
230
339
|
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
231
|
-
const content =
|
|
232
|
-
if (msg.content !== "") content.push({
|
|
233
|
-
type: "text",
|
|
234
|
-
text: msg.content
|
|
235
|
-
});
|
|
340
|
+
const content = toAnthropicBlocks(msg.content, "assistant");
|
|
236
341
|
for (const call of msg.toolCalls) content.push({
|
|
237
342
|
type: "tool_use",
|
|
238
343
|
id: call.id,
|
|
@@ -247,7 +352,7 @@ function toAnthropicMessages(messages) {
|
|
|
247
352
|
}
|
|
248
353
|
out.push({
|
|
249
354
|
role: msg.role,
|
|
250
|
-
content: msg.content
|
|
355
|
+
content: toAnthropicBlocks(msg.content, msg.role)
|
|
251
356
|
});
|
|
252
357
|
}
|
|
253
358
|
return system === "" ? { messages: out } : {
|
|
@@ -296,7 +401,7 @@ var AnthropicClient = class {
|
|
|
296
401
|
const toolCallsByIndex = /* @__PURE__ */ new Map();
|
|
297
402
|
const decoder = new TextDecoder();
|
|
298
403
|
const reader = res.body.getReader();
|
|
299
|
-
|
|
404
|
+
const frames = createSseFrameReader();
|
|
300
405
|
const handleFrame = function* (data) {
|
|
301
406
|
let chunk;
|
|
302
407
|
try {
|
|
@@ -343,17 +448,9 @@ var AnthropicClient = class {
|
|
|
343
448
|
try {
|
|
344
449
|
for (;;) {
|
|
345
450
|
const { value, done: readerDone } = await reader.read();
|
|
451
|
+
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
452
|
+
for (const payload of payloads) yield* handleFrame(payload);
|
|
346
453
|
if (readerDone) break;
|
|
347
|
-
buffer += decoder.decode(value, { stream: true });
|
|
348
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
349
|
-
let newline;
|
|
350
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
351
|
-
const line = buffer.slice(0, newline).trim();
|
|
352
|
-
buffer = buffer.slice(newline + 1);
|
|
353
|
-
if (line === "" || line.startsWith(":") || line.startsWith("event:")) continue;
|
|
354
|
-
if (!line.startsWith("data:")) continue;
|
|
355
|
-
yield* handleFrame(line.slice(5).trim());
|
|
356
|
-
}
|
|
357
454
|
}
|
|
358
455
|
} finally {
|
|
359
456
|
reader.releaseLock();
|
|
@@ -379,6 +476,7 @@ var AnthropicClient = class {
|
|
|
379
476
|
yield { type: "done" };
|
|
380
477
|
}
|
|
381
478
|
async #post(input, stream) {
|
|
479
|
+
validateLlmMessages(input.messages);
|
|
382
480
|
const { system, messages } = toAnthropicMessages(input.messages);
|
|
383
481
|
const body = {
|
|
384
482
|
model: input.model ?? this.#config.model,
|
|
@@ -425,7 +523,7 @@ var AnthropicClient = class {
|
|
|
425
523
|
arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
|
|
426
524
|
}));
|
|
427
525
|
return {
|
|
428
|
-
content: text === "" ? void 0 : text,
|
|
526
|
+
content: text === "" ? void 0 : textParts(text),
|
|
429
527
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
430
528
|
raw: data
|
|
431
529
|
};
|
|
@@ -435,6 +533,24 @@ var AnthropicClient = class {
|
|
|
435
533
|
}
|
|
436
534
|
};
|
|
437
535
|
const errorMessage = (e) => e instanceof Error ? e.message : String(e);
|
|
536
|
+
/** parts → GenAI parts(二进制统一走 inlineData) */
|
|
537
|
+
const toGenAiParts = (parts, where) => parts.map((part) => {
|
|
538
|
+
switch (part.type) {
|
|
539
|
+
case "text": return { text: part.text };
|
|
540
|
+
case "image":
|
|
541
|
+
case "file": return { inlineData: {
|
|
542
|
+
mimeType: part.mimeType,
|
|
543
|
+
data: part.data
|
|
544
|
+
} };
|
|
545
|
+
default: return rejectUnsupportedPart(part, "Google GenAI", where);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
/** systemInstruction 与 functionResponse 只接受文本 */
|
|
549
|
+
const toGenAiText = (parts, where) => {
|
|
550
|
+
const nonText = parts.find((p) => p.type !== "text");
|
|
551
|
+
if (nonText) rejectUnsupportedPart(nonText, "Google GenAI", where);
|
|
552
|
+
return partsToText(parts);
|
|
553
|
+
};
|
|
438
554
|
/** tool 结果文本包装为 functionResponse.response 对象 */
|
|
439
555
|
function responseObject(content) {
|
|
440
556
|
try {
|
|
@@ -445,7 +561,7 @@ function responseObject(content) {
|
|
|
445
561
|
}
|
|
446
562
|
/** LlmMessage 序列 → systemInstruction + contents(tool 结果合并进 user 消息 functionResponse parts) */
|
|
447
563
|
function toGenAiContents(messages) {
|
|
448
|
-
const system = messages.filter((m) => m.role === "system").map((m) => m.content).join("\n");
|
|
564
|
+
const system = messages.filter((m) => m.role === "system").map((m) => toGenAiText(m.content, "system")).join("\n");
|
|
449
565
|
const nameByCallId = /* @__PURE__ */ new Map();
|
|
450
566
|
for (const msg of messages) if (msg.role === "assistant") for (const call of msg.toolCalls ?? []) nameByCallId.set(call.id, call.name);
|
|
451
567
|
const out = [];
|
|
@@ -455,7 +571,7 @@ function toGenAiContents(messages) {
|
|
|
455
571
|
const callId = msg.toolCallId ?? "";
|
|
456
572
|
const part = { functionResponse: {
|
|
457
573
|
name: nameByCallId.get(callId) ?? callId,
|
|
458
|
-
response: responseObject(msg.content)
|
|
574
|
+
response: responseObject(toGenAiText(msg.content, "tool"))
|
|
459
575
|
} };
|
|
460
576
|
const last = out.at(-1);
|
|
461
577
|
if (last && last.role === "user") last.parts.push(part);
|
|
@@ -466,8 +582,7 @@ function toGenAiContents(messages) {
|
|
|
466
582
|
continue;
|
|
467
583
|
}
|
|
468
584
|
if (msg.role === "assistant") {
|
|
469
|
-
const parts =
|
|
470
|
-
if (msg.content !== "") parts.push({ text: msg.content });
|
|
585
|
+
const parts = toGenAiParts(msg.content, "assistant");
|
|
471
586
|
for (const call of msg.toolCalls ?? []) parts.push({ functionCall: {
|
|
472
587
|
name: call.name,
|
|
473
588
|
args: call.arguments
|
|
@@ -480,7 +595,7 @@ function toGenAiContents(messages) {
|
|
|
480
595
|
}
|
|
481
596
|
out.push({
|
|
482
597
|
role: "user",
|
|
483
|
-
parts:
|
|
598
|
+
parts: toGenAiParts(msg.content, msg.role)
|
|
484
599
|
});
|
|
485
600
|
}
|
|
486
601
|
return system === "" ? { contents: out } : {
|
|
@@ -516,7 +631,7 @@ var GoogleGenAiClient = class {
|
|
|
516
631
|
const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
|
|
517
632
|
const toolCalls = this.#functionCalls(parts);
|
|
518
633
|
return {
|
|
519
|
-
content: text === "" ? void 0 : text,
|
|
634
|
+
content: text === "" ? void 0 : textParts(text),
|
|
520
635
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
521
636
|
raw: data
|
|
522
637
|
};
|
|
@@ -528,7 +643,7 @@ var GoogleGenAiClient = class {
|
|
|
528
643
|
const toolCalls = [];
|
|
529
644
|
const decoder = new TextDecoder();
|
|
530
645
|
const reader = res.body.getReader();
|
|
531
|
-
|
|
646
|
+
const frames = createSseFrameReader();
|
|
532
647
|
const handleFrame = function* (data) {
|
|
533
648
|
let chunk;
|
|
534
649
|
try {
|
|
@@ -556,17 +671,9 @@ var GoogleGenAiClient = class {
|
|
|
556
671
|
try {
|
|
557
672
|
for (;;) {
|
|
558
673
|
const { value, done: readerDone } = await reader.read();
|
|
674
|
+
const payloads = readerDone ? frames.flush() : frames.push(decoder.decode(value, { stream: true }));
|
|
675
|
+
for (const payload of payloads) yield* handleFrame(payload);
|
|
559
676
|
if (readerDone) break;
|
|
560
|
-
buffer += decoder.decode(value, { stream: true });
|
|
561
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
562
|
-
let newline;
|
|
563
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
564
|
-
const line = buffer.slice(0, newline).trim();
|
|
565
|
-
buffer = buffer.slice(newline + 1);
|
|
566
|
-
if (line === "" || line.startsWith(":") || line.startsWith("event:")) continue;
|
|
567
|
-
if (!line.startsWith("data:")) continue;
|
|
568
|
-
yield* handleFrame(line.slice(5).trim());
|
|
569
|
-
}
|
|
570
677
|
}
|
|
571
678
|
} finally {
|
|
572
679
|
reader.releaseLock();
|
|
@@ -580,6 +687,7 @@ var GoogleGenAiClient = class {
|
|
|
580
687
|
async #post(input, stream) {
|
|
581
688
|
const model = input.model ?? this.#config.model;
|
|
582
689
|
const action = stream ? ":streamGenerateContent?alt=sse" : ":generateContent";
|
|
690
|
+
validateLlmMessages(input.messages);
|
|
583
691
|
const { systemInstruction, contents } = toGenAiContents(input.messages);
|
|
584
692
|
const body = { contents };
|
|
585
693
|
if (systemInstruction) body["systemInstruction"] = systemInstruction;
|
|
@@ -655,7 +763,7 @@ function fromVercelResult(result) {
|
|
|
655
763
|
};
|
|
656
764
|
});
|
|
657
765
|
return {
|
|
658
|
-
content: typeof r.text === "string" && r.text !== "" ? r.text : void 0,
|
|
766
|
+
content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
|
|
659
767
|
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
660
768
|
raw: result
|
|
661
769
|
};
|
|
@@ -938,11 +1046,8 @@ function buildRenderResult(run, output, renderBlocks = []) {
|
|
|
938
1046
|
}
|
|
939
1047
|
const MAX_SURFACE_BYTES = 256 * 1024;
|
|
940
1048
|
const MAX_ACTIONS = 32;
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
const MAX_TABLE_COLUMNS = 128;
|
|
944
|
-
const MAX_TABLE_ROWS = 1e4;
|
|
945
|
-
const MAX_CHART_POINTS = 2e4;
|
|
1049
|
+
const MAX_NODES = 2e3;
|
|
1050
|
+
const MAX_NODE_DEPTH = 32;
|
|
946
1051
|
const MAX_JSON_DEPTH = 16;
|
|
947
1052
|
const MAX_PATCH_OPERATIONS = 128;
|
|
948
1053
|
const actionIntents = /* @__PURE__ */ new Set([
|
|
@@ -952,16 +1057,6 @@ const actionIntents = /* @__PURE__ */ new Set([
|
|
|
952
1057
|
"download",
|
|
953
1058
|
"refresh"
|
|
954
1059
|
]);
|
|
955
|
-
const fieldTypes = /* @__PURE__ */ new Set([
|
|
956
|
-
"text",
|
|
957
|
-
"number",
|
|
958
|
-
"date",
|
|
959
|
-
"textarea",
|
|
960
|
-
"select",
|
|
961
|
-
"multi-select",
|
|
962
|
-
"toggle",
|
|
963
|
-
"file"
|
|
964
|
-
]);
|
|
965
1060
|
function isRecord$1(value) {
|
|
966
1061
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
967
1062
|
}
|
|
@@ -982,98 +1077,39 @@ function isJsonValue(value, depth = 0) {
|
|
|
982
1077
|
if (!isRecord$1(value)) return false;
|
|
983
1078
|
return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
|
|
984
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Runtime 只做结构校验;组件名白名单与 props schema 属于 catalog 语义,
|
|
1082
|
+
* 留在 `@webskill/ui`——runtime 不得为了校验反向依赖 ui。
|
|
1083
|
+
*/
|
|
1084
|
+
function assertNode(value, path, depth, counter) {
|
|
1085
|
+
if (depth > MAX_NODE_DEPTH) reject(`A UI spec tree must not nest deeper than ${MAX_NODE_DEPTH} levels`);
|
|
1086
|
+
if (++counter.nodes > MAX_NODES) reject(`A UI spec tree must contain at most ${MAX_NODES} nodes`);
|
|
1087
|
+
if (!isRecord$1(value)) reject(`${path} must be an object`);
|
|
1088
|
+
requireString(value["component"], `${path}.component`);
|
|
1089
|
+
if (value["id"] !== void 0) requireString(value["id"], `${path}.id`);
|
|
1090
|
+
if (value["props"] !== void 0 && (!isRecord$1(value["props"]) || !isJsonValue(value["props"]))) reject(`${path}.props must be a JSON object`);
|
|
1091
|
+
const children = value["children"];
|
|
1092
|
+
if (children === void 0) return;
|
|
1093
|
+
if (!Array.isArray(children)) reject(`${path}.children must be an array`);
|
|
1094
|
+
children.forEach((child, index) => assertNode(child, `${path}.children[${index}]`, depth + 1, counter));
|
|
1095
|
+
}
|
|
985
1096
|
function assertActions(value) {
|
|
986
1097
|
if (value === void 0) return;
|
|
987
1098
|
if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
|
|
988
1099
|
for (const action of value) {
|
|
989
1100
|
if (!isRecord$1(action)) reject("A surface action must be an object");
|
|
990
1101
|
requireString(action["id"], "Surface action ID");
|
|
991
|
-
|
|
992
|
-
if (typeof
|
|
993
|
-
if (action["disabled"] !== void 0 && typeof action["disabled"] !== "boolean") reject("Surface action disabled must be a boolean");
|
|
1102
|
+
const intent = action["intent"];
|
|
1103
|
+
if (typeof intent !== "string" || !actionIntents.has(intent)) reject("Surface action intent is invalid");
|
|
994
1104
|
if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
|
|
995
1105
|
if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
|
|
996
1106
|
}
|
|
997
1107
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
requireString(field["label"], "Form field label");
|
|
1004
|
-
if (typeof field["type"] !== "string" || !fieldTypes.has(field["type"])) reject("Form field type is invalid");
|
|
1005
|
-
if (field["required"] !== void 0 && typeof field["required"] !== "boolean") reject("Form field required must be a boolean");
|
|
1006
|
-
if (field["description"] !== void 0 && typeof field["description"] !== "string") reject("Form field description must be a string");
|
|
1007
|
-
if (field["defaultValue"] !== void 0 && !isJsonValue(field["defaultValue"])) reject("Form field defaultValue must be JSON data");
|
|
1008
|
-
if (field["options"] !== void 0) {
|
|
1009
|
-
if (!Array.isArray(field["options"]) || field["options"].length > MAX_OPTIONS) reject(`Form field options must contain at most ${MAX_OPTIONS} items`);
|
|
1010
|
-
for (const option of field["options"]) {
|
|
1011
|
-
if (!isRecord$1(option)) reject("A form option must be an object");
|
|
1012
|
-
requireString(option["label"], "Form option label");
|
|
1013
|
-
if (!isJsonValue(option["value"])) reject("Form option value must be JSON data");
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
assertActions(surface["actions"]);
|
|
1018
|
-
}
|
|
1019
|
-
function assertChart(surface) {
|
|
1020
|
-
const chart = surface["chart"];
|
|
1021
|
-
if (!isRecord$1(chart)) reject("A chart surface requires a chart object");
|
|
1022
|
-
if (chart["kind"] !== "bar" && chart["kind"] !== "line" && chart["kind"] !== "pie") reject("Chart kind is invalid");
|
|
1023
|
-
if (!Array.isArray(chart["labels"]) || !chart["labels"].every((label) => typeof label === "string")) reject("Chart labels must be an array of strings");
|
|
1024
|
-
if (!Array.isArray(chart["series"])) reject("Chart series must be an array");
|
|
1025
|
-
let points = 0;
|
|
1026
|
-
for (const series of chart["series"]) {
|
|
1027
|
-
if (!isRecord$1(series) || !Array.isArray(series["data"]) || !series["data"].every((point) => typeof point === "number" && Number.isFinite(point))) reject("Chart series data must be finite numbers");
|
|
1028
|
-
if (series["name"] !== void 0 && typeof series["name"] !== "string") reject("Chart series name must be a string");
|
|
1029
|
-
points += series["data"].length;
|
|
1030
|
-
}
|
|
1031
|
-
if (points > MAX_CHART_POINTS) reject(`Chart data exceeds the ${MAX_CHART_POINTS}-point limit`);
|
|
1032
|
-
assertActions(surface["actions"]);
|
|
1033
|
-
}
|
|
1034
|
-
function assertTable(surface) {
|
|
1035
|
-
if (!Array.isArray(surface["columns"]) || surface["columns"].length > MAX_TABLE_COLUMNS || !surface["columns"].every((column) => typeof column === "string")) reject(`Table columns must be strings and contain at most ${MAX_TABLE_COLUMNS} items`);
|
|
1036
|
-
if (!Array.isArray(surface["rows"]) || surface["rows"].length > MAX_TABLE_ROWS) reject(`Table rows must contain at most ${MAX_TABLE_ROWS} items`);
|
|
1037
|
-
for (const row of surface["rows"]) if (!Array.isArray(row) || row.length > surface["columns"].length || !row.every((cell) => isJsonValue(cell))) reject("Table rows must contain JSON cells within the declared column count");
|
|
1038
|
-
assertActions(surface["actions"]);
|
|
1039
|
-
}
|
|
1040
|
-
/** Validates the allowlisted, data-only shape accepted by a UI surface renderer. @experimental */
|
|
1041
|
-
function validateUiSurface(value) {
|
|
1042
|
-
if (!isRecord$1(value)) reject("A UI surface must be an object");
|
|
1043
|
-
requireString(value["id"], "UI surface ID");
|
|
1044
|
-
if (value["title"] !== void 0 && typeof value["title"] !== "string") reject("UI surface title must be a string");
|
|
1045
|
-
switch (value["kind"]) {
|
|
1046
|
-
case "form":
|
|
1047
|
-
assertForm(value);
|
|
1048
|
-
break;
|
|
1049
|
-
case "chart":
|
|
1050
|
-
assertChart(value);
|
|
1051
|
-
break;
|
|
1052
|
-
case "table":
|
|
1053
|
-
assertTable(value);
|
|
1054
|
-
break;
|
|
1055
|
-
case "metric":
|
|
1056
|
-
requireString(value["label"], "Metric label");
|
|
1057
|
-
if (typeof value["value"] !== "string" && (typeof value["value"] !== "number" || !Number.isFinite(value["value"]))) reject("Metric value must be a string or finite number");
|
|
1058
|
-
if (value["trend"] !== void 0 && value["trend"] !== "up" && value["trend"] !== "down" && value["trend"] !== "neutral") reject("Metric trend is invalid");
|
|
1059
|
-
break;
|
|
1060
|
-
case "file": {
|
|
1061
|
-
requireString(value["path"], "File path");
|
|
1062
|
-
if (value["mimeType"] !== void 0 && typeof value["mimeType"] !== "string") reject("File mimeType must be a string");
|
|
1063
|
-
const fileSize = value["size"];
|
|
1064
|
-
if (fileSize !== void 0 && (typeof fileSize !== "number" || !Number.isSafeInteger(fileSize) || fileSize < 0)) reject("File size must be a non-negative integer");
|
|
1065
|
-
assertActions(value["actions"]);
|
|
1066
|
-
break;
|
|
1067
|
-
}
|
|
1068
|
-
case "custom":
|
|
1069
|
-
requireString(value["component"], "Custom surface component");
|
|
1070
|
-
if (!isJsonValue(value["props"])) reject("Custom surface props must be JSON data");
|
|
1071
|
-
assertActions(value["actions"]);
|
|
1072
|
-
break;
|
|
1073
|
-
default: reject("UI surface kind is invalid");
|
|
1074
|
-
}
|
|
1075
|
-
if (!isJsonValue(value)) reject("A UI surface must contain JSON data only");
|
|
1076
|
-
if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI surface exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
|
|
1108
|
+
/** Validates the allowlisted, data-only node tree accepted by a UI surface renderer. @experimental */
|
|
1109
|
+
function validateUiSpecNode(value) {
|
|
1110
|
+
assertNode(value, "root", 0, { nodes: 0 });
|
|
1111
|
+
if (!isJsonValue(value)) reject("A UI spec tree must contain JSON data only");
|
|
1112
|
+
if (JSON.stringify(value).length > MAX_SURFACE_BYTES) reject(`A UI spec tree exceeds the ${MAX_SURFACE_BYTES}-byte limit`);
|
|
1077
1113
|
return structuredClone(value);
|
|
1078
1114
|
}
|
|
1079
1115
|
function assertPatch(value) {
|
|
@@ -1084,15 +1120,20 @@ function assertPatch(value) {
|
|
|
1084
1120
|
if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
|
|
1085
1121
|
}
|
|
1086
1122
|
/** Validates an individual event in the framework-neutral surface stream. @experimental */
|
|
1087
|
-
function
|
|
1123
|
+
function validateUiSpecEvent(value) {
|
|
1088
1124
|
if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
|
|
1089
1125
|
if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
|
|
1090
1126
|
switch (value["type"]) {
|
|
1091
|
-
case "open":
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1127
|
+
case "open":
|
|
1128
|
+
requireString(value["id"], "UI surface ID");
|
|
1129
|
+
assertActions(value["actions"]);
|
|
1130
|
+
return {
|
|
1131
|
+
type: "open",
|
|
1132
|
+
...value["runId"] ? { runId: value["runId"] } : {},
|
|
1133
|
+
id: value["id"],
|
|
1134
|
+
node: validateUiSpecNode(value["node"]),
|
|
1135
|
+
...value["actions"] ? { actions: structuredClone(value["actions"]) } : {}
|
|
1136
|
+
};
|
|
1096
1137
|
case "patch":
|
|
1097
1138
|
requireString(value["id"], "Surface patch ID");
|
|
1098
1139
|
requireNonNegativeInteger(value["revision"], "Surface patch revision");
|
|
@@ -1137,10 +1178,10 @@ function validateUiSurfaceEvent(value) {
|
|
|
1137
1178
|
}
|
|
1138
1179
|
}
|
|
1139
1180
|
/** Extracts validated surface stream events from structured tool output. @experimental */
|
|
1140
|
-
function
|
|
1181
|
+
function extractUiSpecEvents(data) {
|
|
1141
1182
|
if (!isRecord$1(data) || data["$surface"] === void 0) return [];
|
|
1142
1183
|
const raw = data["$surface"];
|
|
1143
|
-
return (Array.isArray(raw) ? raw : [raw]).map((event) =>
|
|
1184
|
+
return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
|
|
1144
1185
|
}
|
|
1145
1186
|
/**
|
|
1146
1187
|
* JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
|
|
@@ -1287,6 +1328,148 @@ var TraceRecorder = class {
|
|
|
1287
1328
|
return [...this.#events];
|
|
1288
1329
|
}
|
|
1289
1330
|
};
|
|
1331
|
+
const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
|
|
1332
|
+
/** @experimental */
|
|
1333
|
+
function isUnsupportedRunSnapshot(entry) {
|
|
1334
|
+
return entry.unsupported === true;
|
|
1335
|
+
}
|
|
1336
|
+
const SNAPSHOT_SUFFIX = ".snapshot.json";
|
|
1337
|
+
/**
|
|
1338
|
+
* FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
|
|
1339
|
+
* 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
|
|
1340
|
+
* save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
|
|
1341
|
+
*
|
|
1342
|
+
* 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
|
|
1343
|
+
* 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
|
|
1344
|
+
* @experimental
|
|
1345
|
+
*/
|
|
1346
|
+
var FsRunSnapshotStore = class {
|
|
1347
|
+
#root;
|
|
1348
|
+
#fs;
|
|
1349
|
+
constructor(deps) {
|
|
1350
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
1351
|
+
this.#fs = deps.fs;
|
|
1352
|
+
}
|
|
1353
|
+
#path(runId) {
|
|
1354
|
+
return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
|
|
1355
|
+
}
|
|
1356
|
+
async save(snapshot) {
|
|
1357
|
+
await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
|
|
1358
|
+
await this.#pruneExpired();
|
|
1359
|
+
}
|
|
1360
|
+
async load(runId) {
|
|
1361
|
+
const path = this.#path(runId);
|
|
1362
|
+
if (!await this.#fs.exists(path)) return void 0;
|
|
1363
|
+
let parsed;
|
|
1364
|
+
try {
|
|
1365
|
+
parsed = JSON.parse(await this.#fs.readText(path));
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
1368
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1369
|
+
}
|
|
1370
|
+
const snapshot = parsed;
|
|
1371
|
+
if (typeof snapshot !== "object" || snapshot === null) {
|
|
1372
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
1373
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
1374
|
+
}
|
|
1375
|
+
const schemaVersion = typeof snapshot.schemaVersion === "number" ? snapshot.schemaVersion : 0;
|
|
1376
|
+
if (schemaVersion !== 2) throw new WebSkillError("RUN_SNAPSHOT_SCHEMA_UNSUPPORTED", `Snapshot for run "${runId}" uses schema version ${schemaVersion}; this runtime reads version 2. The file was kept for read-only inspection.`);
|
|
1377
|
+
if (snapshot.runId !== runId) {
|
|
1378
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
1379
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
1380
|
+
}
|
|
1381
|
+
return snapshot;
|
|
1382
|
+
}
|
|
1383
|
+
async delete(runId) {
|
|
1384
|
+
const path = this.#path(runId);
|
|
1385
|
+
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
1386
|
+
}
|
|
1387
|
+
async list() {
|
|
1388
|
+
await this.#pruneExpired();
|
|
1389
|
+
if (!await this.#fs.exists(this.#root)) return [];
|
|
1390
|
+
const out = [];
|
|
1391
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
1392
|
+
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
1393
|
+
try {
|
|
1394
|
+
const parsed = JSON.parse(await this.#fs.readText(entry.path));
|
|
1395
|
+
const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 0;
|
|
1396
|
+
if (schemaVersion === 2) {
|
|
1397
|
+
out.push(parsed);
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
out.push({
|
|
1401
|
+
unsupported: true,
|
|
1402
|
+
schemaVersion,
|
|
1403
|
+
runId: String(parsed.runId ?? entry.path),
|
|
1404
|
+
snapshotAt: typeof parsed.snapshotAt === "string" ? parsed.snapshotAt : "",
|
|
1405
|
+
...typeof parsed.sessionId === "string" ? { sessionId: parsed.sessionId } : {},
|
|
1406
|
+
...typeof parsed.userPrompt === "string" ? { userPrompt: parsed.userPrompt } : {},
|
|
1407
|
+
...typeof parsed.interactionExpiresAt === "string" ? { interactionExpiresAt: parsed.interactionExpiresAt } : {}
|
|
1408
|
+
});
|
|
1409
|
+
} catch {}
|
|
1410
|
+
}
|
|
1411
|
+
return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
|
|
1412
|
+
}
|
|
1413
|
+
/** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
|
|
1414
|
+
async #pruneExpired() {
|
|
1415
|
+
try {
|
|
1416
|
+
if (!await this.#fs.exists(this.#root)) return;
|
|
1417
|
+
const now = Date.now();
|
|
1418
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
1419
|
+
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
1420
|
+
try {
|
|
1421
|
+
const parsed = JSON.parse(await this.#fs.readText(entry.path));
|
|
1422
|
+
if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
|
|
1423
|
+
} catch {}
|
|
1424
|
+
}
|
|
1425
|
+
} catch {}
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
/**
|
|
1429
|
+
* LLM 可见名 → 作者在 SKILL.md 里写的形态。
|
|
1430
|
+
* `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
|
|
1431
|
+
* `mcp__x` → `mcp#x`,其余 `a__b` → `endpoint:a/b`。
|
|
1432
|
+
*/
|
|
1433
|
+
function canonicalToolName(llmToolName, activated) {
|
|
1434
|
+
const sep = llmToolName.indexOf("__");
|
|
1435
|
+
if (sep > 0 && activated.has(llmToolName.slice(0, sep))) return llmToolName;
|
|
1436
|
+
if (llmToolName.startsWith("mcp__")) return `mcp#${llmToolName.slice(5)}`;
|
|
1437
|
+
if (sep > 0) return `endpoint:${llmToolName.slice(0, sep)}/${llmToolName.slice(sep + 2)}`;
|
|
1438
|
+
return llmToolName;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* 只支持整段匹配与尾部 `/*`。通配符越自由,作者越容易写出一个自以为很窄、实际很宽的规则;
|
|
1442
|
+
* 裸 `endpoint:github`(不带 `/*`)判为未匹配——含糊写法应当报警而不是被善意解释。
|
|
1443
|
+
*/
|
|
1444
|
+
function matchesPattern(pattern, declaringSkill, llmToolName, canonical) {
|
|
1445
|
+
if (!pattern.includes(":") && !pattern.includes("#")) return llmToolName === `${declaringSkill}__${pattern}`;
|
|
1446
|
+
if (pattern === canonical) return true;
|
|
1447
|
+
if (pattern.endsWith("/*")) {
|
|
1448
|
+
const prefix = pattern.slice(0, -1);
|
|
1449
|
+
return canonical.startsWith(prefix) && canonical.length > prefix.length;
|
|
1450
|
+
}
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
function denialReason(state, canonical) {
|
|
1454
|
+
const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
|
|
1455
|
+
const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
|
|
1456
|
+
const slash = canonical.lastIndexOf("/");
|
|
1457
|
+
return `${subject} allowed-tools, but tool "${canonical}" is not listed and was rejected. Add "${canonical}"${slash > 0 ? ` or "${canonical.slice(0, slash)}/*"` : ""} to allowed-tools.`;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
|
|
1461
|
+
* 或存在一个**未声明**清单的激活技能(否则就是把 A 的声明施加到 B 头上)。
|
|
1462
|
+
*/
|
|
1463
|
+
function evaluateToolAccess(state, llmToolName) {
|
|
1464
|
+
if (state.skillAllowedTools.size === 0) return { allowed: true };
|
|
1465
|
+
for (const skill of state.activated) if (!state.skillAllowedTools.has(skill)) return { allowed: true };
|
|
1466
|
+
const canonical = canonicalToolName(llmToolName, state.activated);
|
|
1467
|
+
for (const [skill, patterns] of state.skillAllowedTools) for (const pattern of patterns) if (matchesPattern(pattern, skill, llmToolName, canonical)) return { allowed: true };
|
|
1468
|
+
return {
|
|
1469
|
+
allowed: false,
|
|
1470
|
+
reason: denialReason(state, canonical)
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1290
1473
|
const MAX_SURFACE_PATCHES_PER_SECOND = 240;
|
|
1291
1474
|
/** 交互终态(取消/超时):从工具执行深处直接终止 run */
|
|
1292
1475
|
var RunTerminated = class extends Error {
|
|
@@ -1312,7 +1495,6 @@ const summarizeArgs = (args) => {
|
|
|
1312
1495
|
const json = JSON.stringify(args);
|
|
1313
1496
|
return json.length > 100 ? `${json.slice(0, 100)}…` : json;
|
|
1314
1497
|
};
|
|
1315
|
-
const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
|
|
1316
1498
|
/**
|
|
1317
1499
|
* 多轮 Agent 循环。
|
|
1318
1500
|
* 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
|
|
@@ -1381,6 +1563,9 @@ var AgentLoop = class {
|
|
|
1381
1563
|
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
1382
1564
|
activated: /* @__PURE__ */ new Set(),
|
|
1383
1565
|
activatedTools: /* @__PURE__ */ new Map(),
|
|
1566
|
+
skillAllowedTools: /* @__PURE__ */ new Map(),
|
|
1567
|
+
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1568
|
+
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
1384
1569
|
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
1385
1570
|
now,
|
|
1386
1571
|
interactionSeq: 0,
|
|
@@ -1392,6 +1577,7 @@ var AgentLoop = class {
|
|
|
1392
1577
|
surfacePatchWindowStartedAt: Date.now(),
|
|
1393
1578
|
surfacePatchCount: 0,
|
|
1394
1579
|
processedSurfaceActionNonces: /* @__PURE__ */ new Set(),
|
|
1580
|
+
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
1395
1581
|
startMs,
|
|
1396
1582
|
pausedMs: 0,
|
|
1397
1583
|
maxTurns: this.#config.maxTurns,
|
|
@@ -1408,7 +1594,13 @@ var AgentLoop = class {
|
|
|
1408
1594
|
strategy: route.strategy,
|
|
1409
1595
|
skillCount: route.catalog.entries.length
|
|
1410
1596
|
} });
|
|
1411
|
-
await this.#lifecycle(
|
|
1597
|
+
await this.#lifecycle({
|
|
1598
|
+
phase: "route",
|
|
1599
|
+
data: {
|
|
1600
|
+
strategy: route.strategy,
|
|
1601
|
+
candidates: route.catalog.entries.map((e) => e.name)
|
|
1602
|
+
}
|
|
1603
|
+
}, state);
|
|
1412
1604
|
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
1413
1605
|
try {
|
|
1414
1606
|
return await source.listToolSpecs();
|
|
@@ -1417,15 +1609,25 @@ var AgentLoop = class {
|
|
|
1417
1609
|
return [];
|
|
1418
1610
|
}
|
|
1419
1611
|
}))).flat();
|
|
1612
|
+
const externalSystemPrompts = [];
|
|
1613
|
+
for (const source of this.#deps.externalTools ?? []) {
|
|
1614
|
+
if (source.systemPrompt === void 0) continue;
|
|
1615
|
+
try {
|
|
1616
|
+
const text = (await source.systemPrompt())?.trim();
|
|
1617
|
+
if (text !== void 0 && text !== "") externalSystemPrompts.push(text);
|
|
1618
|
+
} catch (e) {
|
|
1619
|
+
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1420
1622
|
state.messages = [
|
|
1421
1623
|
{
|
|
1422
1624
|
role: "system",
|
|
1423
|
-
content: route.systemPrompt
|
|
1625
|
+
content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
|
|
1424
1626
|
},
|
|
1425
1627
|
...(input.history ?? []).map((m) => ({ ...m })),
|
|
1426
1628
|
{
|
|
1427
1629
|
role: "user",
|
|
1428
|
-
content: input.userPrompt
|
|
1630
|
+
content: textParts(input.userPrompt)
|
|
1429
1631
|
}
|
|
1430
1632
|
];
|
|
1431
1633
|
try {
|
|
@@ -1455,6 +1657,27 @@ var AgentLoop = class {
|
|
|
1455
1657
|
state.timer = void 0;
|
|
1456
1658
|
}
|
|
1457
1659
|
}
|
|
1660
|
+
/**
|
|
1661
|
+
* D10 判定的唯一出口(暴露点 + 分发点共用)。
|
|
1662
|
+
* 0.4.0(D1)起两个调用点都看返回值:暴露点过滤、分发点回喂 TOOL_NOT_ALLOWED。
|
|
1663
|
+
* 判定语义与 0.3.0 一致,改的只是调用点与 trace 事件类型(warning → denied)。
|
|
1664
|
+
*/
|
|
1665
|
+
#checkToolAccess(state, toolName) {
|
|
1666
|
+
const verdict = evaluateToolAccess(state, toolName);
|
|
1667
|
+
if (verdict.allowed) return true;
|
|
1668
|
+
if (!state.warnedDeniedTools.has(toolName)) {
|
|
1669
|
+
state.warnedDeniedTools.add(toolName);
|
|
1670
|
+
state.trace.record("tool.denied", {
|
|
1671
|
+
message: verdict.reason ?? `Tool "${toolName}" is not allowed`,
|
|
1672
|
+
data: { name: toolName }
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
/** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
|
|
1678
|
+
#deniedToolError(state, toolName) {
|
|
1679
|
+
return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
|
|
1680
|
+
}
|
|
1458
1681
|
/** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
|
|
1459
1682
|
async #turnLoop(state, startTurn, externalSpecs) {
|
|
1460
1683
|
const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
|
|
@@ -1466,11 +1689,11 @@ var AgentLoop = class {
|
|
|
1466
1689
|
state.turn = turn;
|
|
1467
1690
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1468
1691
|
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1692
|
+
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
|
|
1469
1693
|
const toolSpecs = [
|
|
1470
1694
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1471
1695
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
1472
|
-
...
|
|
1473
|
-
...externalSpecs
|
|
1696
|
+
...skillToolSpecs
|
|
1474
1697
|
];
|
|
1475
1698
|
trace.record("llm.request", { data: {
|
|
1476
1699
|
turn,
|
|
@@ -1500,22 +1723,29 @@ var AgentLoop = class {
|
|
|
1500
1723
|
const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
|
|
1501
1724
|
return finish("failed", "llm-error", messageOf(e), code);
|
|
1502
1725
|
}
|
|
1726
|
+
const responseText = partsToText(response.content);
|
|
1503
1727
|
trace.record("llm.response", { data: {
|
|
1504
1728
|
turn,
|
|
1505
1729
|
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
1506
|
-
contentLength:
|
|
1730
|
+
contentLength: responseText.length
|
|
1507
1731
|
} });
|
|
1508
1732
|
if (!response.toolCalls?.length) {
|
|
1509
1733
|
messages.push({
|
|
1510
1734
|
role: "assistant",
|
|
1511
|
-
content: response.content ??
|
|
1735
|
+
content: response.content ?? []
|
|
1512
1736
|
});
|
|
1513
|
-
return finish("completed", "final-answer",
|
|
1737
|
+
return finish("completed", "final-answer", responseText);
|
|
1514
1738
|
}
|
|
1515
|
-
await this.#lifecycle(
|
|
1739
|
+
await this.#lifecycle({
|
|
1740
|
+
phase: "execute",
|
|
1741
|
+
data: {
|
|
1742
|
+
kind: "turn",
|
|
1743
|
+
turn
|
|
1744
|
+
}
|
|
1745
|
+
}, state);
|
|
1516
1746
|
messages.push({
|
|
1517
1747
|
role: "assistant",
|
|
1518
|
-
content: response.content ??
|
|
1748
|
+
content: response.content ?? [],
|
|
1519
1749
|
toolCalls: response.toolCalls
|
|
1520
1750
|
});
|
|
1521
1751
|
for (const call of response.toolCalls) {
|
|
@@ -1529,7 +1759,7 @@ var AgentLoop = class {
|
|
|
1529
1759
|
messages.push({
|
|
1530
1760
|
role: "tool",
|
|
1531
1761
|
toolCallId: call.id,
|
|
1532
|
-
content: await this.#serializeToolResult(call, result, state)
|
|
1762
|
+
content: textParts(await this.#serializeToolResult(call, result, state))
|
|
1533
1763
|
});
|
|
1534
1764
|
await this.#drainSurfaceAction(state);
|
|
1535
1765
|
}
|
|
@@ -1575,7 +1805,10 @@ var AgentLoop = class {
|
|
|
1575
1805
|
trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
|
|
1576
1806
|
}
|
|
1577
1807
|
try {
|
|
1578
|
-
await this.#lifecycle(
|
|
1808
|
+
await this.#lifecycle({
|
|
1809
|
+
phase: status === "completed" ? "complete" : "fail",
|
|
1810
|
+
data: { reason }
|
|
1811
|
+
}, state);
|
|
1579
1812
|
} catch (e) {
|
|
1580
1813
|
trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
|
|
1581
1814
|
}
|
|
@@ -1591,7 +1824,7 @@ var AgentLoop = class {
|
|
|
1591
1824
|
const store = this.#deps.snapshotStore;
|
|
1592
1825
|
if (!store) return;
|
|
1593
1826
|
const snapshot = {
|
|
1594
|
-
schemaVersion:
|
|
1827
|
+
schemaVersion: 2,
|
|
1595
1828
|
runId: state.runId,
|
|
1596
1829
|
sessionId: state.run.sessionId,
|
|
1597
1830
|
userPrompt: state.run.userPrompt,
|
|
@@ -1600,6 +1833,7 @@ var AgentLoop = class {
|
|
|
1600
1833
|
turn: state.turn,
|
|
1601
1834
|
activeSkillNames: [...state.activated].sort(),
|
|
1602
1835
|
activatedTools: [...state.activatedTools.values()],
|
|
1836
|
+
...state.skillAllowedTools.size > 0 ? { skillAllowedTools: Object.fromEntries([...state.skillAllowedTools].map(([k, v]) => [k, [...v]])) } : {},
|
|
1603
1837
|
...pending.interaction ? { pendingInteraction: pending.interaction } : {},
|
|
1604
1838
|
...pending.surfaceAction ? { pendingSurfaceAction: pending.surfaceAction } : {},
|
|
1605
1839
|
interactionExpiresAt: state.run.interruptExpiresAt,
|
|
@@ -1649,6 +1883,9 @@ var AgentLoop = class {
|
|
|
1649
1883
|
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
1650
1884
|
activated: new Set(snapshot.activeSkillNames),
|
|
1651
1885
|
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
1886
|
+
skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
|
|
1887
|
+
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1888
|
+
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
1652
1889
|
toolTimeoutMs: snapshot.config.toolTimeoutMs,
|
|
1653
1890
|
now,
|
|
1654
1891
|
interactionSeq: snapshot.interactionSeq ?? 0,
|
|
@@ -1660,6 +1897,7 @@ var AgentLoop = class {
|
|
|
1660
1897
|
surfacePatchWindowStartedAt: Date.now(),
|
|
1661
1898
|
surfacePatchCount: 0,
|
|
1662
1899
|
processedSurfaceActionNonces: new Set(snapshot.processedSurfaceActionNonces ?? []),
|
|
1900
|
+
emittedToolEvents: /* @__PURE__ */ new Set(),
|
|
1663
1901
|
startMs,
|
|
1664
1902
|
pausedMs: snapshot.pausedMs ?? 0,
|
|
1665
1903
|
maxTurns: snapshot.config.maxTurns,
|
|
@@ -1696,7 +1934,7 @@ var AgentLoop = class {
|
|
|
1696
1934
|
state.messages.push({
|
|
1697
1935
|
role: "tool",
|
|
1698
1936
|
toolCallId: pendingCall.id,
|
|
1699
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
1937
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1700
1938
|
});
|
|
1701
1939
|
await this.#drainSurfaceAction(state);
|
|
1702
1940
|
} else if (pending?.type === "ask" && pendingCall) {
|
|
@@ -1718,7 +1956,7 @@ var AgentLoop = class {
|
|
|
1718
1956
|
state.messages.push({
|
|
1719
1957
|
role: "tool",
|
|
1720
1958
|
toolCallId: pendingCall.id,
|
|
1721
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
1959
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1722
1960
|
});
|
|
1723
1961
|
await this.#drainSurfaceAction(state);
|
|
1724
1962
|
} else if (pendingCall) {
|
|
@@ -1726,7 +1964,7 @@ var AgentLoop = class {
|
|
|
1726
1964
|
state.messages.push({
|
|
1727
1965
|
role: "tool",
|
|
1728
1966
|
toolCallId: pendingCall.id,
|
|
1729
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
1967
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1730
1968
|
});
|
|
1731
1969
|
await this.#drainSurfaceAction(state);
|
|
1732
1970
|
}
|
|
@@ -1737,7 +1975,7 @@ var AgentLoop = class {
|
|
|
1737
1975
|
state.messages.push({
|
|
1738
1976
|
role: "tool",
|
|
1739
1977
|
toolCallId: next.id,
|
|
1740
|
-
content: await this.#serializeToolResult(next, result, state)
|
|
1978
|
+
content: textParts(await this.#serializeToolResult(next, result, state))
|
|
1741
1979
|
});
|
|
1742
1980
|
await this.#drainSurfaceAction(state);
|
|
1743
1981
|
}
|
|
@@ -1780,7 +2018,7 @@ var AgentLoop = class {
|
|
|
1780
2018
|
sessionId: state.run.sessionId,
|
|
1781
2019
|
ts: state.now(),
|
|
1782
2020
|
data: {
|
|
1783
|
-
|
|
2021
|
+
kind: "llm-delta",
|
|
1784
2022
|
delta
|
|
1785
2023
|
}
|
|
1786
2024
|
});
|
|
@@ -1794,23 +2032,23 @@ var AgentLoop = class {
|
|
|
1794
2032
|
emitDelta(event.delta);
|
|
1795
2033
|
} else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
|
|
1796
2034
|
else if (event.type === "done") doneContent = event.content;
|
|
2035
|
+
const text = doneContent ?? content;
|
|
1797
2036
|
return {
|
|
1798
|
-
content:
|
|
2037
|
+
content: text === "" ? void 0 : textParts(text),
|
|
1799
2038
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
1800
2039
|
};
|
|
1801
2040
|
}
|
|
1802
2041
|
/** 生命周期接线:更新 phase、发事件、跑钩子 */
|
|
1803
|
-
async #lifecycle(
|
|
1804
|
-
state.run.phase = phase;
|
|
2042
|
+
async #lifecycle(init, state) {
|
|
2043
|
+
state.run.phase = init.phase;
|
|
1805
2044
|
const event = {
|
|
1806
|
-
|
|
2045
|
+
...init,
|
|
1807
2046
|
runId: state.runId,
|
|
1808
2047
|
sessionId: state.run.sessionId,
|
|
1809
|
-
ts: state.now()
|
|
1810
|
-
...data ? { data } : {}
|
|
2048
|
+
ts: state.now()
|
|
1811
2049
|
};
|
|
1812
2050
|
this.#deps.eventBus?.emit(event);
|
|
1813
|
-
if (this.#deps.hooks) await this.#deps.hooks.run(phase, {
|
|
2051
|
+
if (this.#deps.hooks) await this.#deps.hooks.run(init.phase, {
|
|
1814
2052
|
event,
|
|
1815
2053
|
run: state.run
|
|
1816
2054
|
});
|
|
@@ -1837,10 +2075,14 @@ var AgentLoop = class {
|
|
|
1837
2075
|
run.status = "interrupted";
|
|
1838
2076
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
1839
2077
|
await this.#saveSnapshot(state, { interaction: request });
|
|
1840
|
-
await this.#lifecycle(
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2078
|
+
await this.#lifecycle({
|
|
2079
|
+
phase: "interact",
|
|
2080
|
+
data: {
|
|
2081
|
+
kind: "interaction",
|
|
2082
|
+
interactionId: request.id,
|
|
2083
|
+
interactionType: request.type
|
|
2084
|
+
}
|
|
2085
|
+
}, state);
|
|
1844
2086
|
state.trace.record("ui.requested", { data: {
|
|
1845
2087
|
interactionId: request.id,
|
|
1846
2088
|
type: request.type,
|
|
@@ -1873,7 +2115,13 @@ var AgentLoop = class {
|
|
|
1873
2115
|
interactionId: request.id,
|
|
1874
2116
|
type: request.type
|
|
1875
2117
|
} });
|
|
1876
|
-
await this.#lifecycle(
|
|
2118
|
+
await this.#lifecycle({
|
|
2119
|
+
phase: "execute",
|
|
2120
|
+
data: {
|
|
2121
|
+
kind: "interaction-resumed",
|
|
2122
|
+
interactionId: request.id
|
|
2123
|
+
}
|
|
2124
|
+
}, state);
|
|
1877
2125
|
await this.#appendParamHistory(state, request, response.value);
|
|
1878
2126
|
return response.value;
|
|
1879
2127
|
}
|
|
@@ -1907,6 +2155,7 @@ var AgentLoop = class {
|
|
|
1907
2155
|
if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
|
|
1908
2156
|
else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
|
|
1909
2157
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
2158
|
+
else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
1910
2159
|
else {
|
|
1911
2160
|
const resolution = resolveToolName(call.name, state.activated);
|
|
1912
2161
|
if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
@@ -1932,7 +2181,7 @@ var AgentLoop = class {
|
|
|
1932
2181
|
chart
|
|
1933
2182
|
});
|
|
1934
2183
|
try {
|
|
1935
|
-
for (const event of
|
|
2184
|
+
for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
|
|
1936
2185
|
} catch (e) {
|
|
1937
2186
|
state.trace.record("run.warning", {
|
|
1938
2187
|
message: `UI surface rejected: ${messageOf(e)}`,
|
|
@@ -1971,14 +2220,14 @@ var AgentLoop = class {
|
|
|
1971
2220
|
await bridge.renderSurface(attributed);
|
|
1972
2221
|
state.surfaceEvents.push(structuredClone(attributed));
|
|
1973
2222
|
if (attributed.type === "open") {
|
|
1974
|
-
const waiting =
|
|
2223
|
+
const waiting = (attributed.actions ?? []).filter((action) => action.awaitResponse);
|
|
1975
2224
|
if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
|
|
1976
2225
|
const action = waiting[0];
|
|
1977
2226
|
if (action?.nonce) if (!bridge.requestSurfaceAction) state.trace.record("run.warning", { message: "UiBridge does not support requestSurfaceAction; UI surface action will not pause the run" });
|
|
1978
2227
|
else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
|
|
1979
2228
|
else state.pendingSurfaceAction = {
|
|
1980
2229
|
runId: state.runId,
|
|
1981
|
-
surfaceId: attributed.
|
|
2230
|
+
surfaceId: attributed.id,
|
|
1982
2231
|
actionId: action.id,
|
|
1983
2232
|
intent: action.intent,
|
|
1984
2233
|
nonce: action.nonce
|
|
@@ -1996,21 +2245,17 @@ var AgentLoop = class {
|
|
|
1996
2245
|
}
|
|
1997
2246
|
/** Assigns unforgeable action nonces after model output has passed structural validation. */
|
|
1998
2247
|
#attributeSurfaceEvent(state, event) {
|
|
1999
|
-
|
|
2000
|
-
if (event.type !== "open" || actions.length === 0) return {
|
|
2248
|
+
if (event.type !== "open" || (event.actions ?? []).length === 0) return {
|
|
2001
2249
|
...event,
|
|
2002
2250
|
runId: state.runId
|
|
2003
2251
|
};
|
|
2004
2252
|
return {
|
|
2005
|
-
|
|
2253
|
+
...event,
|
|
2006
2254
|
runId: state.runId,
|
|
2007
|
-
|
|
2008
|
-
...
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
|
|
2012
|
-
}))
|
|
2013
|
-
}
|
|
2255
|
+
actions: (event.actions ?? []).map((action) => ({
|
|
2256
|
+
...action,
|
|
2257
|
+
nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
|
|
2258
|
+
}))
|
|
2014
2259
|
};
|
|
2015
2260
|
}
|
|
2016
2261
|
/** Awaits the single action emitted with the most recently persisted tool result. */
|
|
@@ -2037,13 +2282,13 @@ var AgentLoop = class {
|
|
|
2037
2282
|
state.processedSurfaceActionNonces.add(response.nonce);
|
|
2038
2283
|
state.messages.push({
|
|
2039
2284
|
role: "user",
|
|
2040
|
-
content: JSON.stringify({
|
|
2285
|
+
content: textParts(JSON.stringify({
|
|
2041
2286
|
type: "webskill_surface_action",
|
|
2042
2287
|
surfaceId: response.surfaceId,
|
|
2043
2288
|
actionId: response.actionId,
|
|
2044
2289
|
intent: response.intent,
|
|
2045
2290
|
value: response.value ?? null
|
|
2046
|
-
})
|
|
2291
|
+
}))
|
|
2047
2292
|
});
|
|
2048
2293
|
}
|
|
2049
2294
|
async #interactSurfaceAction(state, request, resumed) {
|
|
@@ -2055,20 +2300,26 @@ var AgentLoop = class {
|
|
|
2055
2300
|
const { run } = state;
|
|
2056
2301
|
run.status = "interrupted";
|
|
2057
2302
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
2303
|
+
const pendingResponse = bridge.requestSurfaceAction(request);
|
|
2304
|
+
pendingResponse.catch(() => void 0);
|
|
2058
2305
|
await this.#saveSnapshot(state, { surfaceAction: request });
|
|
2059
|
-
await this.#lifecycle(
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2306
|
+
await this.#lifecycle({
|
|
2307
|
+
phase: "interact",
|
|
2308
|
+
data: {
|
|
2309
|
+
kind: "surface-action",
|
|
2310
|
+
surfaceId: request.surfaceId,
|
|
2311
|
+
actionId: request.actionId,
|
|
2312
|
+
nonce: request.nonce,
|
|
2313
|
+
resumed
|
|
2314
|
+
}
|
|
2315
|
+
}, state);
|
|
2065
2316
|
state.trace.record("ui.surface-action.requested", { data: {
|
|
2066
2317
|
surfaceId: request.surfaceId,
|
|
2067
2318
|
actionId: request.actionId,
|
|
2068
2319
|
nonce: request.nonce,
|
|
2069
2320
|
...resumed ? { resumed: true } : {}
|
|
2070
2321
|
} });
|
|
2071
|
-
const response = await this.#withInteractionTimeout(
|
|
2322
|
+
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
2072
2323
|
run.status = "running";
|
|
2073
2324
|
run.interruptExpiresAt = void 0;
|
|
2074
2325
|
state.trace.record("ui.surface-action.resolved", { data: {
|
|
@@ -2076,9 +2327,17 @@ var AgentLoop = class {
|
|
|
2076
2327
|
actionId: request.actionId,
|
|
2077
2328
|
nonce: request.nonce
|
|
2078
2329
|
} });
|
|
2079
|
-
await this.#lifecycle(
|
|
2330
|
+
await this.#lifecycle({
|
|
2331
|
+
phase: "execute",
|
|
2332
|
+
data: {
|
|
2333
|
+
kind: "surface-action-resumed",
|
|
2334
|
+
surfaceId: request.surfaceId,
|
|
2335
|
+
actionId: request.actionId
|
|
2336
|
+
}
|
|
2337
|
+
}, state);
|
|
2080
2338
|
return response;
|
|
2081
2339
|
} catch (e) {
|
|
2340
|
+
bridge.cancelSurfaceAction?.(request.nonce);
|
|
2082
2341
|
state.run.status = "running";
|
|
2083
2342
|
state.run.interruptExpiresAt = void 0;
|
|
2084
2343
|
if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
|
|
@@ -2108,15 +2367,25 @@ var AgentLoop = class {
|
|
|
2108
2367
|
* 逐工具实时事件(execute 相位,data.type='tool'):chatbot 思维链工具行等的 live 状态源;
|
|
2109
2368
|
* 与既有 execute 相位事件({turn})并存,监听方按 data.type 区分。
|
|
2110
2369
|
* data.args 为参数摘要(JSON 截断 100 字符,展开详情用)。
|
|
2370
|
+
*
|
|
2371
|
+
* 幂等:同一 run 内每个 `(callId, status)` 至多投递一次。
|
|
2372
|
+
* 模型在不同轮次重发同一个 tool call id 是真实发生的,消费侧本来各自
|
|
2373
|
+
* 建去重表兑付;幂等是事件流自己的语义,不应该要求每个订阅者重建一次。
|
|
2374
|
+
*
|
|
2375
|
+
* 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
|
|
2376
|
+
* 消费者永远收不到它本来就没见过的事件。
|
|
2111
2377
|
*/
|
|
2112
2378
|
#emitTool(state, status, call) {
|
|
2379
|
+
const key = `${call.id}:${status}`;
|
|
2380
|
+
if (state.emittedToolEvents.has(key)) return;
|
|
2381
|
+
state.emittedToolEvents.add(key);
|
|
2113
2382
|
this.#deps.eventBus?.emit({
|
|
2114
2383
|
phase: "execute",
|
|
2115
2384
|
runId: state.runId,
|
|
2116
2385
|
sessionId: state.run.sessionId,
|
|
2117
2386
|
ts: state.now(),
|
|
2118
2387
|
data: {
|
|
2119
|
-
|
|
2388
|
+
kind: "tool",
|
|
2120
2389
|
status,
|
|
2121
2390
|
name: call.name,
|
|
2122
2391
|
callId: call.id,
|
|
@@ -2186,7 +2455,13 @@ var AgentLoop = class {
|
|
|
2186
2455
|
skillName: name,
|
|
2187
2456
|
source: "external"
|
|
2188
2457
|
} });
|
|
2189
|
-
await this.#lifecycle(
|
|
2458
|
+
await this.#lifecycle({
|
|
2459
|
+
phase: "activate",
|
|
2460
|
+
data: {
|
|
2461
|
+
skillName: name,
|
|
2462
|
+
source: "external"
|
|
2463
|
+
}
|
|
2464
|
+
}, state);
|
|
2190
2465
|
await this.#writeActivationMemory(name, state);
|
|
2191
2466
|
}
|
|
2192
2467
|
return {
|
|
@@ -2240,18 +2515,62 @@ var AgentLoop = class {
|
|
|
2240
2515
|
if (!check) return false;
|
|
2241
2516
|
return await check(skillName) === false;
|
|
2242
2517
|
}
|
|
2518
|
+
/**
|
|
2519
|
+
* D3 激活期完整性校验。结论按技能名在 run 内缓存,**失败也缓存**:
|
|
2520
|
+
* 校验失败的技能不会进 `activated` 集合,不缓存的话同一个坏技能每尝试激活一次
|
|
2521
|
+
* 就要把它的文件全扫一遍——成本随文件数线性放大,正是需求 §3 验收 3 要防的。
|
|
2522
|
+
*
|
|
2523
|
+
* 设计 §3.5 写的缓存键是 `(skillName, manifest.integrity.digest)`,但 digest 只有
|
|
2524
|
+
* **调用之后**才知道;runtime 读不到 manifest,两元组键在这一层无法实现。
|
|
2525
|
+
* 实际键是技能名,digest 作为结论的一部分留在 trace 里供事后对账。
|
|
2526
|
+
*
|
|
2527
|
+
* 失败不抛错:抛错会终止整个 run,而「某个技能被改过」不该让其余技能一起停摆。
|
|
2528
|
+
*/
|
|
2529
|
+
async #integrityOk(skillName, state) {
|
|
2530
|
+
const guard = this.#deps.skillIntegrityGuard;
|
|
2531
|
+
if (!guard?.verifyOnActivate) return true;
|
|
2532
|
+
let verdict = state.integrityVerdicts.get(skillName);
|
|
2533
|
+
if (verdict === void 0) {
|
|
2534
|
+
try {
|
|
2535
|
+
verdict = await guard.verifyOnActivate(skillName);
|
|
2536
|
+
} catch (e) {
|
|
2537
|
+
verdict = {
|
|
2538
|
+
ok: false,
|
|
2539
|
+
reason: `the integrity guard threw: ${messageOf(e)}`
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
state.integrityVerdicts.set(skillName, verdict);
|
|
2543
|
+
}
|
|
2544
|
+
if (verdict.ok) return true;
|
|
2545
|
+
state.trace.record("skill.integrity-failed", {
|
|
2546
|
+
message: `Skill "${skillName}" failed integrity verification on activation: ${verdict.reason ?? "no reason given"}`,
|
|
2547
|
+
data: {
|
|
2548
|
+
skillName,
|
|
2549
|
+
...verdict.digest !== void 0 ? { digest: verdict.digest } : {}
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
return false;
|
|
2553
|
+
}
|
|
2243
2554
|
/** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
|
|
2244
2555
|
async #activateSkill(skillName, state, via, skillMdText) {
|
|
2245
2556
|
if (await this.#guardDenied("canActivate", skillName)) {
|
|
2246
2557
|
state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
|
|
2247
2558
|
return "";
|
|
2248
2559
|
}
|
|
2560
|
+
if (!await this.#integrityOk(skillName, state)) return "";
|
|
2249
2561
|
state.activated.add(skillName);
|
|
2250
2562
|
state.trace.record("skill.activated", { data: {
|
|
2251
2563
|
skillName,
|
|
2252
2564
|
...via ? { via } : {}
|
|
2253
2565
|
} });
|
|
2254
|
-
await this.#lifecycle(
|
|
2566
|
+
await this.#lifecycle({
|
|
2567
|
+
phase: "activate",
|
|
2568
|
+
data: {
|
|
2569
|
+
skillName,
|
|
2570
|
+
source: "local",
|
|
2571
|
+
...via ? { via } : {}
|
|
2572
|
+
}
|
|
2573
|
+
}, state);
|
|
2255
2574
|
await this.#writeActivationMemory(skillName, state);
|
|
2256
2575
|
const root = this.#deps.skillIndex.get(skillName);
|
|
2257
2576
|
if (!root) return "";
|
|
@@ -2262,8 +2581,10 @@ var AgentLoop = class {
|
|
|
2262
2581
|
const rawDeps = metadata["dependencies"];
|
|
2263
2582
|
if (Array.isArray(rawDeps)) dependencies = rawDeps.filter((d) => typeof d === "string");
|
|
2264
2583
|
const rawAllowed = metadata["allowed-tools"];
|
|
2265
|
-
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed))
|
|
2266
|
-
|
|
2584
|
+
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) {
|
|
2585
|
+
allowedTools = rawAllowed.filter((e) => typeof e === "string");
|
|
2586
|
+
state.skillAllowedTools.set(skillName, allowedTools);
|
|
2587
|
+
} else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
2267
2588
|
} catch (e) {
|
|
2268
2589
|
state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf(e)}` });
|
|
2269
2590
|
}
|
|
@@ -2280,7 +2601,10 @@ var AgentLoop = class {
|
|
|
2280
2601
|
for (const file of scriptFiles) {
|
|
2281
2602
|
const match = /^(.*)\.(ts|js)$/.exec(file);
|
|
2282
2603
|
if (!match?.[1]) continue;
|
|
2283
|
-
if (allowedTools && !allowedTools.includes(match[1]))
|
|
2604
|
+
if (allowedTools && !allowedTools.includes(match[1])) {
|
|
2605
|
+
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.` });
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2284
2608
|
try {
|
|
2285
2609
|
const def = await executor.loadDefinition(root, match[1]);
|
|
2286
2610
|
await this.#enrichDefinition(root, match[1], def, state);
|
|
@@ -2380,11 +2704,41 @@ var AgentLoop = class {
|
|
|
2380
2704
|
if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
|
|
2381
2705
|
}
|
|
2382
2706
|
await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
|
|
2707
|
+
if (!result.ok) await this.#reportSkillFailure(skillName, state, {
|
|
2708
|
+
code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
|
|
2709
|
+
message: result.error?.message ?? `Tool "${call.name}" returned a failure result`
|
|
2710
|
+
});
|
|
2383
2711
|
return result;
|
|
2384
2712
|
} catch (e) {
|
|
2385
2713
|
if (e instanceof RunTerminated) throw e;
|
|
2386
2714
|
await this.#bumpSkillStat(skillName, "failures", state);
|
|
2387
|
-
|
|
2715
|
+
const code = e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED";
|
|
2716
|
+
const message = `Tool "${call.name}" failed: ${messageOf(e)}`;
|
|
2717
|
+
await this.#reportSkillFailure(skillName, state, {
|
|
2718
|
+
code,
|
|
2719
|
+
message
|
|
2720
|
+
});
|
|
2721
|
+
return toolError(code, message);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* F1 技能失败上报:喂给治理的失败计数器(`SkillStatePolicy.recordFailure`),
|
|
2726
|
+
* 达阈值即自动隔离。无注入即整段跳过(默认关闭)。
|
|
2727
|
+
*
|
|
2728
|
+
* 上报方抛错降级为 `run.warning`:治理写盘失败不该把一次「工具出错但已回喂给 LLM」
|
|
2729
|
+
* 的 run 变成崩溃——那会让引入治理反而降低可用性。
|
|
2730
|
+
*/
|
|
2731
|
+
async #reportSkillFailure(skillName, state, detail) {
|
|
2732
|
+
const report = this.#deps.skillOutcomeReporter?.onSkillFailed;
|
|
2733
|
+
if (report === void 0) return;
|
|
2734
|
+
try {
|
|
2735
|
+
await report.call(this.#deps.skillOutcomeReporter, {
|
|
2736
|
+
skillName,
|
|
2737
|
+
runId: state.runId,
|
|
2738
|
+
...detail
|
|
2739
|
+
});
|
|
2740
|
+
} catch (e) {
|
|
2741
|
+
state.trace.record("run.warning", { message: `Skill failure report for "${skillName}" was not recorded: ${messageOf(e)}` });
|
|
2388
2742
|
}
|
|
2389
2743
|
}
|
|
2390
2744
|
/** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
|
|
@@ -2494,79 +2848,6 @@ function mergeCatalogEntries(localEntries, providerEntries) {
|
|
|
2494
2848
|
for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
2495
2849
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2496
2850
|
}
|
|
2497
|
-
const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
2498
|
-
const SNAPSHOT_SUFFIX = ".snapshot.json";
|
|
2499
|
-
/**
|
|
2500
|
-
* FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
|
|
2501
|
-
* 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
|
|
2502
|
-
* save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
|
|
2503
|
-
*
|
|
2504
|
-
* 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
|
|
2505
|
-
* 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
|
|
2506
|
-
* @experimental
|
|
2507
|
-
*/
|
|
2508
|
-
var FsRunSnapshotStore = class {
|
|
2509
|
-
#root;
|
|
2510
|
-
#fs;
|
|
2511
|
-
constructor(deps) {
|
|
2512
|
-
this.#root = deps.root.replace(/\/+$/, "");
|
|
2513
|
-
this.#fs = deps.fs;
|
|
2514
|
-
}
|
|
2515
|
-
#path(runId) {
|
|
2516
|
-
return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
|
|
2517
|
-
}
|
|
2518
|
-
async save(snapshot) {
|
|
2519
|
-
await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
|
|
2520
|
-
await this.#pruneExpired();
|
|
2521
|
-
}
|
|
2522
|
-
async load(runId) {
|
|
2523
|
-
const path = this.#path(runId);
|
|
2524
|
-
if (!await this.#fs.exists(path)) return void 0;
|
|
2525
|
-
let parsed;
|
|
2526
|
-
try {
|
|
2527
|
-
parsed = JSON.parse(await this.#fs.readText(path));
|
|
2528
|
-
} catch (e) {
|
|
2529
|
-
await this.#fs.remove(path).catch(() => void 0);
|
|
2530
|
-
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
2531
|
-
}
|
|
2532
|
-
const snapshot = parsed;
|
|
2533
|
-
if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
|
|
2534
|
-
await this.#fs.remove(path).catch(() => void 0);
|
|
2535
|
-
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
2536
|
-
}
|
|
2537
|
-
return snapshot;
|
|
2538
|
-
}
|
|
2539
|
-
async delete(runId) {
|
|
2540
|
-
const path = this.#path(runId);
|
|
2541
|
-
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
2542
|
-
}
|
|
2543
|
-
async list() {
|
|
2544
|
-
await this.#pruneExpired();
|
|
2545
|
-
if (!await this.#fs.exists(this.#root)) return [];
|
|
2546
|
-
const out = [];
|
|
2547
|
-
for (const entry of await this.#fs.list(this.#root)) {
|
|
2548
|
-
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
2549
|
-
try {
|
|
2550
|
-
out.push(JSON.parse(await this.#fs.readText(entry.path)));
|
|
2551
|
-
} catch {}
|
|
2552
|
-
}
|
|
2553
|
-
return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
|
|
2554
|
-
}
|
|
2555
|
-
/** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
|
|
2556
|
-
async #pruneExpired() {
|
|
2557
|
-
try {
|
|
2558
|
-
if (!await this.#fs.exists(this.#root)) return;
|
|
2559
|
-
const now = Date.now();
|
|
2560
|
-
for (const entry of await this.#fs.list(this.#root)) {
|
|
2561
|
-
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
2562
|
-
try {
|
|
2563
|
-
const parsed = JSON.parse(await this.#fs.readText(entry.path));
|
|
2564
|
-
if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
|
|
2565
|
-
} catch {}
|
|
2566
|
-
}
|
|
2567
|
-
} catch {}
|
|
2568
|
-
}
|
|
2569
|
-
};
|
|
2570
2851
|
/**
|
|
2571
2852
|
* session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
|
|
2572
2853
|
* 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
|
|
@@ -2700,7 +2981,9 @@ var WebSkillRuntime = class {
|
|
|
2700
2981
|
skillProviders: this.#deps.skillProviders,
|
|
2701
2982
|
catalogFilter: this.#deps.catalogFilter,
|
|
2702
2983
|
snapshotStore: this.#deps.snapshotStore,
|
|
2703
|
-
skillStateGuard: this.#deps.skillStateGuard
|
|
2984
|
+
skillStateGuard: this.#deps.skillStateGuard,
|
|
2985
|
+
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
2986
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
2704
2987
|
}, this.#deps.config);
|
|
2705
2988
|
const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
2706
2989
|
this.#loops.set(runId, loop);
|
|
@@ -2737,14 +3020,14 @@ var WebSkillRuntime = class {
|
|
|
2737
3020
|
});
|
|
2738
3021
|
return result;
|
|
2739
3022
|
}
|
|
2740
|
-
/** D3
|
|
3023
|
+
/** D3:列出 interrupted run(供 UI 展示"未完成任务");版本不受支持的项带 unsupported 标记 */
|
|
2741
3024
|
async listInterruptedRuns() {
|
|
2742
3025
|
if (!this.#deps.snapshotStore) return [];
|
|
2743
3026
|
return this.#deps.snapshotStore.list();
|
|
2744
3027
|
}
|
|
2745
3028
|
/**
|
|
2746
3029
|
* D3 恢复 interrupted run:
|
|
2747
|
-
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;
|
|
3030
|
+
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;schema 版本不受支持 → RUN_SNAPSHOT_SCHEMA_UNSUPPORTED(由 store 抛出,不删文件);
|
|
2748
3031
|
* 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
|
|
2749
3032
|
* @experimental
|
|
2750
3033
|
*/
|
|
@@ -2752,10 +3035,6 @@ var WebSkillRuntime = class {
|
|
|
2752
3035
|
const store = this.#deps.snapshotStore;
|
|
2753
3036
|
const snapshot = store ? await store.load(runId) : void 0;
|
|
2754
3037
|
if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
|
|
2755
|
-
if (snapshot.schemaVersion !== 1) {
|
|
2756
|
-
await store.delete(runId);
|
|
2757
|
-
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
|
|
2758
|
-
}
|
|
2759
3038
|
if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
|
|
2760
3039
|
await store.delete(runId);
|
|
2761
3040
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2808,7 +3087,9 @@ var WebSkillRuntime = class {
|
|
|
2808
3087
|
skillProviders: this.#deps.skillProviders,
|
|
2809
3088
|
catalogFilter: this.#deps.catalogFilter,
|
|
2810
3089
|
snapshotStore: store,
|
|
2811
|
-
skillStateGuard: this.#deps.skillStateGuard
|
|
3090
|
+
skillStateGuard: this.#deps.skillStateGuard,
|
|
3091
|
+
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
3092
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
2812
3093
|
}, this.#deps.config);
|
|
2813
3094
|
this.#loops.set(runId, loop);
|
|
2814
3095
|
try {
|
|
@@ -2927,6 +3208,7 @@ function sourceFromUrl(url) {
|
|
|
2927
3208
|
/** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
|
|
2928
3209
|
var HookRunner = class {
|
|
2929
3210
|
#hooks = /* @__PURE__ */ new Map();
|
|
3211
|
+
/** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
|
|
2930
3212
|
timeoutMs;
|
|
2931
3213
|
failOnHookError;
|
|
2932
3214
|
onWarning;
|
|
@@ -2942,6 +3224,15 @@ var HookRunner = class {
|
|
|
2942
3224
|
this.#hooks.set(key, list);
|
|
2943
3225
|
return this;
|
|
2944
3226
|
}
|
|
3227
|
+
/**
|
|
3228
|
+
* 已注册钩子的**计数**,按注册相位分组(`'*'` 为全相位钩子)。
|
|
3229
|
+
* 刻意不返回函数引用:那会给 UI 一条调用宿主钩子的执行路径,而面板只需要「装没装上」。
|
|
3230
|
+
*/
|
|
3231
|
+
listRegisteredHooks() {
|
|
3232
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3233
|
+
for (const [phase, hooks] of this.#hooks) counts.set(phase, hooks.length);
|
|
3234
|
+
return counts;
|
|
3235
|
+
}
|
|
2945
3236
|
async run(phase, ctx) {
|
|
2946
3237
|
const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
|
|
2947
3238
|
for (const hook of hooks) try {
|
|
@@ -3046,7 +3337,7 @@ var FsMemoryStore = class {
|
|
|
3046
3337
|
for (const entry of await this.#fs.list(this.#root)) await this.#fs.remove(entry.path, { recursive: true });
|
|
3047
3338
|
}
|
|
3048
3339
|
};
|
|
3049
|
-
const INDEX_FILE = "index.json";
|
|
3340
|
+
const INDEX_FILE$1 = "index.json";
|
|
3050
3341
|
/**
|
|
3051
3342
|
* 基于 FileSystemProvider 的 ArtifactStore:产物落盘 <root>/<runId>/<path>,
|
|
3052
3343
|
* 每次写入同步更新 <root>/<runId>/index.json,新实例可凭索引恢复列表。
|
|
@@ -3080,7 +3371,7 @@ var FsArtifactStore = class {
|
|
|
3080
3371
|
}
|
|
3081
3372
|
async listArtifacts(runId) {
|
|
3082
3373
|
assertSafePathSegment(runId, "runId");
|
|
3083
|
-
const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
|
|
3374
|
+
const indexPath = `${this.#root}/${runId}/${INDEX_FILE$1}`;
|
|
3084
3375
|
if (!await this.#fs.exists(indexPath)) return [];
|
|
3085
3376
|
const raw = await this.#fs.readText(indexPath);
|
|
3086
3377
|
try {
|
|
@@ -3113,7 +3404,7 @@ var FsArtifactStore = class {
|
|
|
3113
3404
|
metadata: input.metadata
|
|
3114
3405
|
};
|
|
3115
3406
|
const next = [...(await this.listArtifacts(input.runId)).filter((a) => a.id !== artifact.id), artifact];
|
|
3116
|
-
await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE}`, JSON.stringify({ artifacts: next }, null, 2));
|
|
3407
|
+
await this.#fs.writeText(`${this.#root}/${input.runId}/${INDEX_FILE$1}`, JSON.stringify({ artifacts: next }, null, 2));
|
|
3117
3408
|
return artifact;
|
|
3118
3409
|
}
|
|
3119
3410
|
};
|
|
@@ -3332,6 +3623,417 @@ var CapabilityApproval = class CapabilityApproval {
|
|
|
3332
3623
|
return "allowed";
|
|
3333
3624
|
}
|
|
3334
3625
|
};
|
|
3626
|
+
const RUN_TRACE_SCHEMA_VERSION = 1;
|
|
3627
|
+
/** 终止原因:取最后一条 run.completed/cancelled/failed 事件的 data.reason */
|
|
3628
|
+
function extractEndReason(events) {
|
|
3629
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
3630
|
+
const event = events[i];
|
|
3631
|
+
if (event.type === "run.completed" || event.type === "run.cancelled" || event.type === "run.failed") {
|
|
3632
|
+
const reason = event.data?.["reason"];
|
|
3633
|
+
return typeof reason === "string" ? reason : void 0;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
function summarize(trace) {
|
|
3638
|
+
const endReason = extractEndReason(trace.events);
|
|
3639
|
+
const durationMs = trace.endedAt === void 0 ? void 0 : Date.parse(trace.endedAt) - Date.parse(trace.startedAt);
|
|
3640
|
+
return {
|
|
3641
|
+
runId: trace.runId,
|
|
3642
|
+
startedAt: trace.startedAt,
|
|
3643
|
+
status: trace.status,
|
|
3644
|
+
activeSkills: trace.activeSkills,
|
|
3645
|
+
eventCount: trace.events.length,
|
|
3646
|
+
turnCount: trace.events.filter((e) => e.type === "llm.request").length,
|
|
3647
|
+
...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
|
|
3648
|
+
...trace.endedAt !== void 0 ? { endedAt: trace.endedAt } : {},
|
|
3649
|
+
...endReason !== void 0 ? { endReason } : {},
|
|
3650
|
+
...durationMs !== void 0 && Number.isFinite(durationMs) && durationMs >= 0 ? { durationMs } : {}
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
function parseTraceFile(raw, path) {
|
|
3654
|
+
let data;
|
|
3655
|
+
try {
|
|
3656
|
+
data = JSON.parse(raw);
|
|
3657
|
+
} catch {
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (typeof data !== "object" || data === null) return void 0;
|
|
3661
|
+
if (typeof data.runId !== "string" || typeof data.startedAt !== "string") return void 0;
|
|
3662
|
+
const schemaVersion = typeof data.schemaVersion === "number" ? data.schemaVersion : 0;
|
|
3663
|
+
if (schemaVersion > 1) throw new WebSkillError("RUN_TRACE_INCOMPATIBLE", `Run trace "${path}" declares schemaVersion ${schemaVersion}, which is newer than the supported 1`);
|
|
3664
|
+
return {
|
|
3665
|
+
schemaVersion,
|
|
3666
|
+
runId: data.runId,
|
|
3667
|
+
...typeof data.sessionId === "string" ? { sessionId: data.sessionId } : {},
|
|
3668
|
+
startedAt: data.startedAt,
|
|
3669
|
+
...typeof data.endedAt === "string" ? { endedAt: data.endedAt } : {},
|
|
3670
|
+
status: typeof data.status === "string" ? data.status : "unknown",
|
|
3671
|
+
activeSkills: Array.isArray(data.activeSkills) ? data.activeSkills.filter((s) => typeof s === "string") : [],
|
|
3672
|
+
events: Array.isArray(data.events) ? data.events : []
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
const INDEX_FILE = "index.jsonl";
|
|
3676
|
+
/**
|
|
3677
|
+
* `FileSystemProvider` 后端:`<root>/<runId>.json` 存全量 trace,
|
|
3678
|
+
* `<root>/index.jsonl` 是 append-only 的摘要旁路索引。
|
|
3679
|
+
*
|
|
3680
|
+
* 索引存在的唯一理由是**读次数**:0.0.1 的实现为了拼一份列表要整读每个 trace
|
|
3681
|
+
* 文件,点一次指标再来一遍。有了索引,`list()` 与 `metrics()` 各只读一个文件。
|
|
3682
|
+
*
|
|
3683
|
+
* 索引与 trace 目录发散(写完 trace 崩在追加前)的检测不引入新的读放大:
|
|
3684
|
+
* 目录列举本来就要做,比较 `*.json` 个数与索引行数即可;不等则整体重建。
|
|
3685
|
+
* @stable
|
|
3686
|
+
*/
|
|
3687
|
+
var FsRunTraceStore = class {
|
|
3688
|
+
#root;
|
|
3689
|
+
#fs;
|
|
3690
|
+
#onError;
|
|
3691
|
+
constructor(deps) {
|
|
3692
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
3693
|
+
this.#fs = deps.fs;
|
|
3694
|
+
this.#onError = deps.onError ?? ((error, run) => {
|
|
3695
|
+
console.warn(`Failed to persist run trace "${run.id}": ${messageOf(error)}`);
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
#path(runId) {
|
|
3699
|
+
return resolveInsideRoot(this.#root, `${runId}.json`);
|
|
3700
|
+
}
|
|
3701
|
+
async put(run) {
|
|
3702
|
+
const trace = {
|
|
3703
|
+
schemaVersion: 1,
|
|
3704
|
+
runId: run.id,
|
|
3705
|
+
sessionId: run.sessionId,
|
|
3706
|
+
startedAt: run.startedAt,
|
|
3707
|
+
...run.endedAt !== void 0 ? { endedAt: run.endedAt } : {},
|
|
3708
|
+
status: run.status === "completed" || run.status === "cancelled" ? run.status : "failed",
|
|
3709
|
+
activeSkills: run.activeSkillNames,
|
|
3710
|
+
events: run.trace
|
|
3711
|
+
};
|
|
3712
|
+
try {
|
|
3713
|
+
await this.#fs.writeText(this.#path(run.id), JSON.stringify(trace, null, 2));
|
|
3714
|
+
await this.#fs.appendText(`${this.#root}/${INDEX_FILE}`, `${JSON.stringify(summarize(trace))}\n`);
|
|
3715
|
+
} catch (e) {
|
|
3716
|
+
this.#onError(e, run);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
async get(runId) {
|
|
3720
|
+
assertSafePathSegment(runId, "run id");
|
|
3721
|
+
const path = this.#path(runId);
|
|
3722
|
+
let raw;
|
|
3723
|
+
try {
|
|
3724
|
+
raw = await this.#fs.readText(path);
|
|
3725
|
+
} catch (e) {
|
|
3726
|
+
if (e instanceof WebSkillError) throw e;
|
|
3727
|
+
throw new WebSkillError("FS_NOT_FOUND", `Trace not found for run ${JSON.stringify(runId)}: ${path}`);
|
|
3728
|
+
}
|
|
3729
|
+
const trace = parseTraceFile(raw, path);
|
|
3730
|
+
if (!trace) throw new WebSkillError("VALIDATION_FAILED", `Trace file is corrupted or malformed: ${path}`);
|
|
3731
|
+
return trace;
|
|
3732
|
+
}
|
|
3733
|
+
async list(filter = {}) {
|
|
3734
|
+
const { summaries } = await this.#readIndex();
|
|
3735
|
+
const matched = applyFilter(summaries, filter);
|
|
3736
|
+
const offset = Math.max(0, filter.offset ?? 0);
|
|
3737
|
+
const sliced = matched.slice(offset);
|
|
3738
|
+
return filter.limit !== void 0 ? sliced.slice(0, Math.max(0, filter.limit)) : sliced;
|
|
3739
|
+
}
|
|
3740
|
+
async metrics(filter = {}) {
|
|
3741
|
+
const { summaries, skipped } = await this.#readIndex();
|
|
3742
|
+
const runs = applyFilter(summaries, filter);
|
|
3743
|
+
const succeeded = runs.filter((r) => r.status === "completed").length;
|
|
3744
|
+
const failedRuns = runs.filter((r) => r.status === "failed");
|
|
3745
|
+
const durations = runs.map((r) => r.durationMs).filter((ms) => ms !== void 0);
|
|
3746
|
+
return {
|
|
3747
|
+
totalRuns: runs.length,
|
|
3748
|
+
succeeded,
|
|
3749
|
+
failed: failedRuns.length,
|
|
3750
|
+
successRate: runs.length === 0 ? 0 : succeeded / runs.length,
|
|
3751
|
+
avgTurns: runs.length === 0 ? 0 : runs.reduce((sum, r) => sum + r.turnCount, 0) / runs.length,
|
|
3752
|
+
skippedRuns: skipped,
|
|
3753
|
+
avgDurationMs: durations.length === 0 ? 0 : durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
3754
|
+
recentFailures: failedRuns.slice(0, 5)
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
/** 健康路径恒为「一次目录列举 + 一次索引读」,与 run 数无关。 */
|
|
3758
|
+
async #readIndex() {
|
|
3759
|
+
if (!await this.#fs.exists(this.#root)) return {
|
|
3760
|
+
summaries: [],
|
|
3761
|
+
skipped: 0
|
|
3762
|
+
};
|
|
3763
|
+
const traceCount = (await this.#fs.list(this.#root)).filter((f) => f.type === "file" && f.path.endsWith(".json")).length;
|
|
3764
|
+
const indexPath = `${this.#root}/${INDEX_FILE}`;
|
|
3765
|
+
let lines = [];
|
|
3766
|
+
if (await this.#fs.exists(indexPath)) lines = (await this.#fs.readText(indexPath)).split("\n").filter((line) => line.trim() !== "");
|
|
3767
|
+
if (lines.length !== traceCount) return {
|
|
3768
|
+
summaries: await this.#rebuildIndex(),
|
|
3769
|
+
skipped: 0
|
|
3770
|
+
};
|
|
3771
|
+
const summaries = [];
|
|
3772
|
+
let skipped = 0;
|
|
3773
|
+
for (const line of lines) try {
|
|
3774
|
+
summaries.push(JSON.parse(line));
|
|
3775
|
+
} catch {
|
|
3776
|
+
skipped += 1;
|
|
3777
|
+
}
|
|
3778
|
+
if (skipped > 0) return {
|
|
3779
|
+
summaries: await this.#rebuildIndex(),
|
|
3780
|
+
skipped
|
|
3781
|
+
};
|
|
3782
|
+
return {
|
|
3783
|
+
summaries: sortByStartedAtDesc(summaries),
|
|
3784
|
+
skipped: 0
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
/** 异常路径,允许 O(N):整读全部 trace 文件后用 temp + rename 原子替换索引 */
|
|
3788
|
+
async #rebuildIndex() {
|
|
3789
|
+
const summaries = [];
|
|
3790
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
3791
|
+
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
3792
|
+
let trace;
|
|
3793
|
+
try {
|
|
3794
|
+
trace = parseTraceFile(await this.#fs.readText(entry.path), entry.path);
|
|
3795
|
+
} catch {
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
if (!trace) continue;
|
|
3799
|
+
summaries.push(summarize(trace));
|
|
3800
|
+
}
|
|
3801
|
+
const sorted = sortByStartedAtDesc(summaries);
|
|
3802
|
+
const indexPath = `${this.#root}/${INDEX_FILE}`;
|
|
3803
|
+
const tempPath = `${this.#root}/${INDEX_FILE}.rebuilding`;
|
|
3804
|
+
try {
|
|
3805
|
+
await this.#fs.writeText(tempPath, sorted.map((s) => `${JSON.stringify(s)}\n`).join(""));
|
|
3806
|
+
await this.#fs.rename(tempPath, indexPath);
|
|
3807
|
+
} catch {
|
|
3808
|
+
await this.#fs.remove(tempPath).catch(() => void 0);
|
|
3809
|
+
}
|
|
3810
|
+
return sorted;
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
/** ISO 时间戳可按字典序比较 */
|
|
3814
|
+
function sortByStartedAtDesc(summaries) {
|
|
3815
|
+
return [...summaries].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
3816
|
+
}
|
|
3817
|
+
function applyFilter(summaries, filter) {
|
|
3818
|
+
let runs = summaries;
|
|
3819
|
+
if (filter.status !== void 0 && filter.status !== "") runs = runs.filter((r) => r.status === filter.status);
|
|
3820
|
+
const q = filter.search?.trim().toLowerCase();
|
|
3821
|
+
if (q !== void 0 && q !== "") runs = runs.filter((r) => r.runId.toLowerCase().includes(q) || r.activeSkills.some((s) => s.toLowerCase().includes(q)));
|
|
3822
|
+
return runs;
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* 从 run 的 trace 推导终态工具调用列表。
|
|
3826
|
+
*
|
|
3827
|
+
* 消费者过去要靠自己遍历 trace 才能拿到这份列表,而遍历 trace 的同一段代码
|
|
3828
|
+
* 又顺手承担了「补发漏掉的 live 事件」的职责——两件事绑在一起,谁也删不掉。
|
|
3829
|
+
* 幂等归 runtime(见 `AgentLoop` 的 per-run 已发集合)之后,
|
|
3830
|
+
* 推导终态列表就是纯函数,独立导出。
|
|
3831
|
+
* @stable
|
|
3832
|
+
*/
|
|
3833
|
+
function summarizeToolCalls(run) {
|
|
3834
|
+
const calls = [];
|
|
3835
|
+
for (const event of run.trace) {
|
|
3836
|
+
if (event.type !== "tool.completed" && event.type !== "tool.failed") continue;
|
|
3837
|
+
const name = event.data?.["name"];
|
|
3838
|
+
const callId = event.data?.["callId"];
|
|
3839
|
+
if (typeof name !== "string" || typeof callId !== "string") continue;
|
|
3840
|
+
const args = event.data?.["args"];
|
|
3841
|
+
const durationMs = event.data?.["durationMs"];
|
|
3842
|
+
calls.push({
|
|
3843
|
+
callId,
|
|
3844
|
+
name,
|
|
3845
|
+
status: event.type === "tool.completed" ? "completed" : "failed",
|
|
3846
|
+
...typeof args === "string" ? { args } : {},
|
|
3847
|
+
...typeof durationMs === "number" ? { durationMs } : {}
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
return calls;
|
|
3851
|
+
}
|
|
3852
|
+
const SESSION_SCHEMA_VERSION = 1;
|
|
3853
|
+
/** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
|
|
3854
|
+
const FS_SESSION_PAGE_SIZE = 50;
|
|
3855
|
+
const toMeta = (record) => ({
|
|
3856
|
+
id: record.id,
|
|
3857
|
+
createdAt: record.createdAt,
|
|
3858
|
+
...record.title !== void 0 ? { title: record.title } : {},
|
|
3859
|
+
...record.titleLocked === true ? { titleLocked: true } : {},
|
|
3860
|
+
...record.archived === true ? { archived: true } : {},
|
|
3861
|
+
messageCount: record.messages.length
|
|
3862
|
+
});
|
|
3863
|
+
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3864
|
+
/**
|
|
3865
|
+
* 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
|
|
3866
|
+
* 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
|
|
3867
|
+
* 但它是不透明串,调用方不得自己构造(构造出来的越界值一律按 `VALIDATION_FAILED` 拒绝)。
|
|
3868
|
+
*/
|
|
3869
|
+
function takeTailPage(all, options, what) {
|
|
3870
|
+
const limit = options.limit ?? 50;
|
|
3871
|
+
if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
|
|
3872
|
+
let end = all.length;
|
|
3873
|
+
if (options.cursor !== void 0) {
|
|
3874
|
+
end = Number(options.cursor);
|
|
3875
|
+
if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
|
|
3876
|
+
}
|
|
3877
|
+
const start = Math.max(0, end - limit);
|
|
3878
|
+
return {
|
|
3879
|
+
items: all.slice(start, end),
|
|
3880
|
+
...start > 0 ? { nextCursor: String(start) } : {}
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
|
|
3885
|
+
* 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
|
|
3886
|
+
*/
|
|
3887
|
+
function parseSessionFile(raw, path) {
|
|
3888
|
+
let parsed;
|
|
3889
|
+
try {
|
|
3890
|
+
parsed = JSON.parse(raw);
|
|
3891
|
+
} catch (e) {
|
|
3892
|
+
throw new WebSkillError("VALIDATION_FAILED", `Session file is not valid JSON: ${path}`, e);
|
|
3893
|
+
}
|
|
3894
|
+
if (typeof parsed !== "object" || parsed === null) throw new WebSkillError("VALIDATION_FAILED", `Session file is not an object: ${path}`);
|
|
3895
|
+
const file = parsed;
|
|
3896
|
+
const schemaVersion = typeof file.schemaVersion === "number" ? file.schemaVersion : 0;
|
|
3897
|
+
if (schemaVersion > 1) throw new WebSkillError("SESSION_INCOMPATIBLE", `Session file at ${path} has schemaVersion ${schemaVersion}, but this build understands at most 1`);
|
|
3898
|
+
if (typeof file.id !== "string" || typeof file.createdAt !== "string") throw new WebSkillError("VALIDATION_FAILED", `Session file is missing "id" or "createdAt": ${path}`);
|
|
3899
|
+
return {
|
|
3900
|
+
schemaVersion,
|
|
3901
|
+
id: file.id,
|
|
3902
|
+
createdAt: file.createdAt,
|
|
3903
|
+
...typeof file.title === "string" ? { title: file.title } : {},
|
|
3904
|
+
...file.titleLocked === true ? { titleLocked: true } : {},
|
|
3905
|
+
...file.archived === true ? { archived: true } : {},
|
|
3906
|
+
messages: Array.isArray(file.messages) ? file.messages : [],
|
|
3907
|
+
messageCount: Array.isArray(file.messages) ? file.messages.length : 0
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* `FileSystemProvider` 后端的会话存储:`<root>/<id>.json`。
|
|
3912
|
+
*
|
|
3913
|
+
* 所有变更操作按 id 串到一条 promise 链上:会话文件是整读整写的,
|
|
3914
|
+
* 两个并发 `appendMessages` 若都先读后写,后写的会覆盖先写的那条消息。
|
|
3915
|
+
*/
|
|
3916
|
+
var FsSessionStore = class {
|
|
3917
|
+
#root;
|
|
3918
|
+
#fs;
|
|
3919
|
+
/** id → 该 id 上最后一次变更的完成时点,用于串行化读改写 */
|
|
3920
|
+
#writes = /* @__PURE__ */ new Map();
|
|
3921
|
+
constructor(deps) {
|
|
3922
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
3923
|
+
this.#fs = deps.fs;
|
|
3924
|
+
}
|
|
3925
|
+
#path(id) {
|
|
3926
|
+
assertSafePathSegment(id, "session id");
|
|
3927
|
+
return resolveInsideRoot(this.#root, `${id}.json`);
|
|
3928
|
+
}
|
|
3929
|
+
/** 把 mutation 排到该 id 的队尾;前一个失败不阻塞后一个 */
|
|
3930
|
+
async #serialize(id, work) {
|
|
3931
|
+
const next = (this.#writes.get(id) ?? Promise.resolve()).catch(() => void 0).then(work);
|
|
3932
|
+
this.#writes.set(id, next);
|
|
3933
|
+
try {
|
|
3934
|
+
return await next;
|
|
3935
|
+
} finally {
|
|
3936
|
+
if (this.#writes.get(id) === next) this.#writes.delete(id);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
async #write(record) {
|
|
3940
|
+
const file = {
|
|
3941
|
+
schemaVersion: 1,
|
|
3942
|
+
id: record.id,
|
|
3943
|
+
createdAt: record.createdAt,
|
|
3944
|
+
...record.title !== void 0 ? { title: record.title } : {},
|
|
3945
|
+
...record.titleLocked === true ? { titleLocked: true } : {},
|
|
3946
|
+
...record.archived === true ? { archived: true } : {},
|
|
3947
|
+
messages: record.messages
|
|
3948
|
+
};
|
|
3949
|
+
await this.#fs.writeText(this.#path(record.id), JSON.stringify(file, null, 2));
|
|
3950
|
+
}
|
|
3951
|
+
async #require(id) {
|
|
3952
|
+
const record = await this.get(id);
|
|
3953
|
+
if (!record) throw new WebSkillError("FS_NOT_FOUND", `Session "${id}" not found under ${this.#root}`);
|
|
3954
|
+
return record;
|
|
3955
|
+
}
|
|
3956
|
+
async list(options = {}) {
|
|
3957
|
+
if (!await this.#fs.exists(this.#root)) return { items: [] };
|
|
3958
|
+
const metas = [];
|
|
3959
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
3960
|
+
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
3961
|
+
let record;
|
|
3962
|
+
try {
|
|
3963
|
+
record = parseSessionFile(await this.#fs.readText(entry.path), entry.path);
|
|
3964
|
+
} catch (e) {
|
|
3965
|
+
console.warn(`Skipping unreadable session file "${entry.path}": ${e instanceof Error ? e.message : String(e)}`);
|
|
3966
|
+
continue;
|
|
3967
|
+
}
|
|
3968
|
+
if (record.archived === true && options.includeArchived !== true) continue;
|
|
3969
|
+
metas.push(toMeta(record));
|
|
3970
|
+
}
|
|
3971
|
+
metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3972
|
+
return takeTailPage(metas, options, "session");
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
|
|
3976
|
+
* 但**跨出端口的记录数**已是常量——这正是分页对上层的意义。
|
|
3977
|
+
*/
|
|
3978
|
+
async listMessages(id, options = {}) {
|
|
3979
|
+
return takeTailPage((await this.#require(id)).messages, options, "message");
|
|
3980
|
+
}
|
|
3981
|
+
async get(id) {
|
|
3982
|
+
const path = this.#path(id);
|
|
3983
|
+
if (!await this.#fs.exists(path)) return void 0;
|
|
3984
|
+
return parseSessionFile(await this.#fs.readText(path), path);
|
|
3985
|
+
}
|
|
3986
|
+
async create(init = {}) {
|
|
3987
|
+
const record = {
|
|
3988
|
+
schemaVersion: 1,
|
|
3989
|
+
id: init.id ?? newSessionId(),
|
|
3990
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3991
|
+
...init.title !== void 0 ? { title: init.title } : {},
|
|
3992
|
+
messages: [],
|
|
3993
|
+
messageCount: 0
|
|
3994
|
+
};
|
|
3995
|
+
return this.#serialize(record.id, async () => {
|
|
3996
|
+
await this.#write(record);
|
|
3997
|
+
return toMeta(record);
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
async appendMessages(id, messages) {
|
|
4001
|
+
if (messages.length === 0) return;
|
|
4002
|
+
await this.#serialize(id, async () => {
|
|
4003
|
+
const record = await this.#require(id);
|
|
4004
|
+
record.messages.push(...messages);
|
|
4005
|
+
await this.#write(record);
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
4008
|
+
async replaceMessages(id, messages) {
|
|
4009
|
+
await this.#serialize(id, async () => {
|
|
4010
|
+
const record = await this.#require(id);
|
|
4011
|
+
record.messages = [...messages];
|
|
4012
|
+
await this.#write(record);
|
|
4013
|
+
});
|
|
4014
|
+
}
|
|
4015
|
+
async setTitle(id, title, options = {}) {
|
|
4016
|
+
await this.#serialize(id, async () => {
|
|
4017
|
+
const record = await this.#require(id);
|
|
4018
|
+
record.title = title;
|
|
4019
|
+
if (options.lock === true) record.titleLocked = true;
|
|
4020
|
+
await this.#write(record);
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
async setArchived(id, archived) {
|
|
4024
|
+
await this.#serialize(id, async () => {
|
|
4025
|
+
const record = await this.#require(id);
|
|
4026
|
+
record.archived = archived;
|
|
4027
|
+
await this.#write(record);
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
async delete(id) {
|
|
4031
|
+
await this.#serialize(id, async () => {
|
|
4032
|
+
const path = this.#path(id);
|
|
4033
|
+
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
};
|
|
3335
4037
|
|
|
3336
4038
|
//#endregion
|
|
3337
|
-
export {
|
|
4039
|
+
export { createScriptContext as A, networkUrlHost as B, RUN_TRACE_SCHEMA_VERSION as C, WebSkillRuntime as D, TraceRecorder as E, fromVercelStreamPart as F, resolveToolName as G, normalizeToolContent as H, isNetworkAllowed as I, toLlmToolSpec as J, schemaToForm as K, isUnsupportedRunSnapshot as L, extractChartSpec as M, extractUiSpecEvents as N, bridgeError as O, fromVercelResult as P, mergeCatalogEntries as R, RUN_SNAPSHOT_SCHEMA_VERSION as S, SerializingMemoryStore as T, normalizeToolError as U, normalizeErrorCode as V, parseBridgeRequest as W, validateUiSpecEvent as X, toVercelToolSpecs as Y, validateUiSpecNode as Z, OpenAiCompatibleClient as _, AnthropicClient as a, READ_SKILL_FILE_TOOL as b, FS_SESSION_PAGE_SIZE as c, FsRunSnapshotStore as d, FsRunTraceStore as f, HookRunner as g, GoogleGenAiClient as h, AgentLoop as i, createWebSkillApi as j, buildRenderResult as k, FsArtifactStore as l, FullDisclosureRouter as m, ASK_USER_TOOL as n, CapabilityApproval as o, FsSessionStore as p, summarizeToolCalls as q, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsMemoryStore as u, ProgressiveRouter as v, SESSION_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL_NAME as x, READ_SKILL_FILE_INPUT_SCHEMA as y, networkPolicyLibSource as z };
|