@webskill/sdk 0.3.0 → 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/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -6
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-KsujmL4b-Clx1kCnU.js} +278 -122
- package/dist/{dist-rorEJsNi.js → dist-C-Sh0MDU.js} +498 -282
- package/dist/{dist-ZKaM8j06.js → dist-D9Lcn5Pp.js} +527 -837
- package/dist/governance.d.ts +45 -10
- package/dist/governance.js +151 -24
- package/dist/{index-wiV5X8Rz.d.ts → index-CHXxDccV.d.ts} +62 -144
- package/dist/{index-8d-oEDww.d.ts → index-DLfR2Y6I.d.ts} +215 -23
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
- package/dist/node.d.ts +8 -4
- package/dist/node.js +1 -1
- package/dist/{openUiLibrary-B8-Cvou9-BbpNTXS3.js → openUiLibrary-YLS-cxyT-C96jWDQq.js} +6 -5
- package/dist/{skillVersionStore-DOEI9ptb-BxbYL70B.d.ts → skillVersionStore-uyefLPR1-DXOzbksv.d.ts} +41 -10
- package/dist/{testing-CsrG3XLz.js → testing-DDCJWvgA.js} +7 -5
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-AmKCKJn_-VGabeXK4.d.ts → types-7Wcg--Vh-1YlQ4jF9.d.ts} +85 -71
- package/dist/ui-react.d.ts +329 -18
- package/dist/ui-react.js +3714 -3463
- 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-BslMcxRZ.js → webskillLitCatalog-CSTbhBe_-CYIs5BX8.js} +312 -122
- package/package.json +1 -1
- package/dist/jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js +0 -2468
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
|
@@ -1,5 +1,5 @@
|
|
|
1
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 { t as MemoryArtifactStore } from "./memoryArtifactStore-
|
|
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
5
|
function createSseFrameReader() {
|
|
@@ -47,27 +47,65 @@ function createSseFrameReader() {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
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
|
+
};
|
|
50
81
|
const toOpenAiMessage = (msg) => {
|
|
51
82
|
if (msg.role === "tool") return {
|
|
52
83
|
role: "tool",
|
|
53
84
|
tool_call_id: msg.toolCallId,
|
|
54
|
-
content: msg.content
|
|
85
|
+
content: toOpenAiText(msg.content, "tool")
|
|
55
86
|
};
|
|
56
|
-
if (msg.role === "
|
|
57
|
-
role: "
|
|
58
|
-
content: msg.content
|
|
59
|
-
tool_calls: msg.toolCalls.map((call) => ({
|
|
60
|
-
id: call.id,
|
|
61
|
-
type: "function",
|
|
62
|
-
function: {
|
|
63
|
-
name: call.name,
|
|
64
|
-
arguments: JSON.stringify(call.arguments)
|
|
65
|
-
}
|
|
66
|
-
}))
|
|
87
|
+
if (msg.role === "system") return {
|
|
88
|
+
role: "system",
|
|
89
|
+
content: toOpenAiText(msg.content, "system")
|
|
67
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
|
+
}
|
|
68
106
|
return {
|
|
69
107
|
role: msg.role,
|
|
70
|
-
content: msg.content
|
|
108
|
+
content: toOpenAiContent(msg.content, msg.role)
|
|
71
109
|
};
|
|
72
110
|
};
|
|
73
111
|
const toOpenAiTools = (tools) => tools.map((tool) => ({
|
|
@@ -181,6 +219,7 @@ var OpenAiCompatibleClient = class {
|
|
|
181
219
|
}
|
|
182
220
|
async #postChat(input, stream) {
|
|
183
221
|
const { baseUrl, apiKey, model } = this.#requireConfig();
|
|
222
|
+
validateLlmMessages(input.messages);
|
|
184
223
|
const body = {
|
|
185
224
|
model: input.model ?? model,
|
|
186
225
|
messages: input.messages.map(toOpenAiMessage)
|
|
@@ -234,7 +273,7 @@ var OpenAiCompatibleClient = class {
|
|
|
234
273
|
});
|
|
235
274
|
const content = choice["content"];
|
|
236
275
|
return {
|
|
237
|
-
content: typeof content === "string" ? content : void 0,
|
|
276
|
+
content: typeof content === "string" && content !== "" ? textParts(content) : void 0,
|
|
238
277
|
toolCalls: toolCalls?.length ? toolCalls : void 0,
|
|
239
278
|
raw: data
|
|
240
279
|
};
|
|
@@ -245,9 +284,41 @@ var OpenAiCompatibleClient = class {
|
|
|
245
284
|
};
|
|
246
285
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
247
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
|
+
};
|
|
248
319
|
/** LlmMessage 序列 → system 独立参数 + user/assistant 消息(tool 结果合并进 user 消息 tool_result blocks) */
|
|
249
320
|
function toAnthropicMessages(messages) {
|
|
250
|
-
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");
|
|
251
322
|
const out = [];
|
|
252
323
|
for (const msg of messages) {
|
|
253
324
|
if (msg.role === "system") continue;
|
|
@@ -255,7 +326,7 @@ function toAnthropicMessages(messages) {
|
|
|
255
326
|
const block = {
|
|
256
327
|
type: "tool_result",
|
|
257
328
|
tool_use_id: msg.toolCallId ?? "",
|
|
258
|
-
content: msg.content
|
|
329
|
+
content: toAnthropicBlocks(msg.content, "tool")
|
|
259
330
|
};
|
|
260
331
|
const last = out.at(-1);
|
|
261
332
|
if (last && last.role === "user" && Array.isArray(last.content)) last.content.push(block);
|
|
@@ -266,11 +337,7 @@ function toAnthropicMessages(messages) {
|
|
|
266
337
|
continue;
|
|
267
338
|
}
|
|
268
339
|
if (msg.role === "assistant" && msg.toolCalls?.length) {
|
|
269
|
-
const content =
|
|
270
|
-
if (msg.content !== "") content.push({
|
|
271
|
-
type: "text",
|
|
272
|
-
text: msg.content
|
|
273
|
-
});
|
|
340
|
+
const content = toAnthropicBlocks(msg.content, "assistant");
|
|
274
341
|
for (const call of msg.toolCalls) content.push({
|
|
275
342
|
type: "tool_use",
|
|
276
343
|
id: call.id,
|
|
@@ -285,7 +352,7 @@ function toAnthropicMessages(messages) {
|
|
|
285
352
|
}
|
|
286
353
|
out.push({
|
|
287
354
|
role: msg.role,
|
|
288
|
-
content: msg.content
|
|
355
|
+
content: toAnthropicBlocks(msg.content, msg.role)
|
|
289
356
|
});
|
|
290
357
|
}
|
|
291
358
|
return system === "" ? { messages: out } : {
|
|
@@ -409,6 +476,7 @@ var AnthropicClient = class {
|
|
|
409
476
|
yield { type: "done" };
|
|
410
477
|
}
|
|
411
478
|
async #post(input, stream) {
|
|
479
|
+
validateLlmMessages(input.messages);
|
|
412
480
|
const { system, messages } = toAnthropicMessages(input.messages);
|
|
413
481
|
const body = {
|
|
414
482
|
model: input.model ?? this.#config.model,
|
|
@@ -455,7 +523,7 @@ var AnthropicClient = class {
|
|
|
455
523
|
arguments: typeof b["input"] === "object" && b["input"] !== null ? b["input"] : {}
|
|
456
524
|
}));
|
|
457
525
|
return {
|
|
458
|
-
content: text === "" ? void 0 : text,
|
|
526
|
+
content: text === "" ? void 0 : textParts(text),
|
|
459
527
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
460
528
|
raw: data
|
|
461
529
|
};
|
|
@@ -465,6 +533,24 @@ var AnthropicClient = class {
|
|
|
465
533
|
}
|
|
466
534
|
};
|
|
467
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
|
+
};
|
|
468
554
|
/** tool 结果文本包装为 functionResponse.response 对象 */
|
|
469
555
|
function responseObject(content) {
|
|
470
556
|
try {
|
|
@@ -475,7 +561,7 @@ function responseObject(content) {
|
|
|
475
561
|
}
|
|
476
562
|
/** LlmMessage 序列 → systemInstruction + contents(tool 结果合并进 user 消息 functionResponse parts) */
|
|
477
563
|
function toGenAiContents(messages) {
|
|
478
|
-
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");
|
|
479
565
|
const nameByCallId = /* @__PURE__ */ new Map();
|
|
480
566
|
for (const msg of messages) if (msg.role === "assistant") for (const call of msg.toolCalls ?? []) nameByCallId.set(call.id, call.name);
|
|
481
567
|
const out = [];
|
|
@@ -485,7 +571,7 @@ function toGenAiContents(messages) {
|
|
|
485
571
|
const callId = msg.toolCallId ?? "";
|
|
486
572
|
const part = { functionResponse: {
|
|
487
573
|
name: nameByCallId.get(callId) ?? callId,
|
|
488
|
-
response: responseObject(msg.content)
|
|
574
|
+
response: responseObject(toGenAiText(msg.content, "tool"))
|
|
489
575
|
} };
|
|
490
576
|
const last = out.at(-1);
|
|
491
577
|
if (last && last.role === "user") last.parts.push(part);
|
|
@@ -496,8 +582,7 @@ function toGenAiContents(messages) {
|
|
|
496
582
|
continue;
|
|
497
583
|
}
|
|
498
584
|
if (msg.role === "assistant") {
|
|
499
|
-
const parts =
|
|
500
|
-
if (msg.content !== "") parts.push({ text: msg.content });
|
|
585
|
+
const parts = toGenAiParts(msg.content, "assistant");
|
|
501
586
|
for (const call of msg.toolCalls ?? []) parts.push({ functionCall: {
|
|
502
587
|
name: call.name,
|
|
503
588
|
args: call.arguments
|
|
@@ -510,7 +595,7 @@ function toGenAiContents(messages) {
|
|
|
510
595
|
}
|
|
511
596
|
out.push({
|
|
512
597
|
role: "user",
|
|
513
|
-
parts:
|
|
598
|
+
parts: toGenAiParts(msg.content, msg.role)
|
|
514
599
|
});
|
|
515
600
|
}
|
|
516
601
|
return system === "" ? { contents: out } : {
|
|
@@ -546,7 +631,7 @@ var GoogleGenAiClient = class {
|
|
|
546
631
|
const text = parts.filter((p) => typeof p["text"] === "string").map((p) => String(p["text"])).join("");
|
|
547
632
|
const toolCalls = this.#functionCalls(parts);
|
|
548
633
|
return {
|
|
549
|
-
content: text === "" ? void 0 : text,
|
|
634
|
+
content: text === "" ? void 0 : textParts(text),
|
|
550
635
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
551
636
|
raw: data
|
|
552
637
|
};
|
|
@@ -602,6 +687,7 @@ var GoogleGenAiClient = class {
|
|
|
602
687
|
async #post(input, stream) {
|
|
603
688
|
const model = input.model ?? this.#config.model;
|
|
604
689
|
const action = stream ? ":streamGenerateContent?alt=sse" : ":generateContent";
|
|
690
|
+
validateLlmMessages(input.messages);
|
|
605
691
|
const { systemInstruction, contents } = toGenAiContents(input.messages);
|
|
606
692
|
const body = { contents };
|
|
607
693
|
if (systemInstruction) body["systemInstruction"] = systemInstruction;
|
|
@@ -677,7 +763,7 @@ function fromVercelResult(result) {
|
|
|
677
763
|
};
|
|
678
764
|
});
|
|
679
765
|
return {
|
|
680
|
-
content: typeof r.text === "string" && r.text !== "" ? r.text : void 0,
|
|
766
|
+
content: typeof r.text === "string" && r.text !== "" ? textParts(r.text) : void 0,
|
|
681
767
|
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
682
768
|
raw: result
|
|
683
769
|
};
|
|
@@ -960,11 +1046,8 @@ function buildRenderResult(run, output, renderBlocks = []) {
|
|
|
960
1046
|
}
|
|
961
1047
|
const MAX_SURFACE_BYTES = 256 * 1024;
|
|
962
1048
|
const MAX_ACTIONS = 32;
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
const MAX_TABLE_COLUMNS = 128;
|
|
966
|
-
const MAX_TABLE_ROWS = 1e4;
|
|
967
|
-
const MAX_CHART_POINTS = 2e4;
|
|
1049
|
+
const MAX_NODES = 2e3;
|
|
1050
|
+
const MAX_NODE_DEPTH = 32;
|
|
968
1051
|
const MAX_JSON_DEPTH = 16;
|
|
969
1052
|
const MAX_PATCH_OPERATIONS = 128;
|
|
970
1053
|
const actionIntents = /* @__PURE__ */ new Set([
|
|
@@ -974,16 +1057,6 @@ const actionIntents = /* @__PURE__ */ new Set([
|
|
|
974
1057
|
"download",
|
|
975
1058
|
"refresh"
|
|
976
1059
|
]);
|
|
977
|
-
const fieldTypes = /* @__PURE__ */ new Set([
|
|
978
|
-
"text",
|
|
979
|
-
"number",
|
|
980
|
-
"date",
|
|
981
|
-
"textarea",
|
|
982
|
-
"select",
|
|
983
|
-
"multi-select",
|
|
984
|
-
"toggle",
|
|
985
|
-
"file"
|
|
986
|
-
]);
|
|
987
1060
|
function isRecord$1(value) {
|
|
988
1061
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
989
1062
|
}
|
|
@@ -1004,98 +1077,39 @@ function isJsonValue(value, depth = 0) {
|
|
|
1004
1077
|
if (!isRecord$1(value)) return false;
|
|
1005
1078
|
return Object.keys(value).every((key) => key !== "__proto__" && key !== "constructor" && isJsonValue(value[key], depth + 1));
|
|
1006
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
|
+
}
|
|
1007
1096
|
function assertActions(value) {
|
|
1008
1097
|
if (value === void 0) return;
|
|
1009
1098
|
if (!Array.isArray(value) || value.length > MAX_ACTIONS) reject(`Surface actions must contain at most ${MAX_ACTIONS} items`);
|
|
1010
1099
|
for (const action of value) {
|
|
1011
1100
|
if (!isRecord$1(action)) reject("A surface action must be an object");
|
|
1012
1101
|
requireString(action["id"], "Surface action ID");
|
|
1013
|
-
|
|
1014
|
-
if (typeof
|
|
1015
|
-
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");
|
|
1016
1104
|
if (action["awaitResponse"] !== void 0 && typeof action["awaitResponse"] !== "boolean") reject("Surface action awaitResponse must be a boolean");
|
|
1017
1105
|
if (action["nonce"] !== void 0 && (typeof action["nonce"] !== "string" || action["nonce"] === "")) reject("Surface action nonce must be a non-empty string");
|
|
1018
1106
|
}
|
|
1019
1107
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
requireString(field["label"], "Form field label");
|
|
1026
|
-
if (typeof field["type"] !== "string" || !fieldTypes.has(field["type"])) reject("Form field type is invalid");
|
|
1027
|
-
if (field["required"] !== void 0 && typeof field["required"] !== "boolean") reject("Form field required must be a boolean");
|
|
1028
|
-
if (field["description"] !== void 0 && typeof field["description"] !== "string") reject("Form field description must be a string");
|
|
1029
|
-
if (field["defaultValue"] !== void 0 && !isJsonValue(field["defaultValue"])) reject("Form field defaultValue must be JSON data");
|
|
1030
|
-
if (field["options"] !== void 0) {
|
|
1031
|
-
if (!Array.isArray(field["options"]) || field["options"].length > MAX_OPTIONS) reject(`Form field options must contain at most ${MAX_OPTIONS} items`);
|
|
1032
|
-
for (const option of field["options"]) {
|
|
1033
|
-
if (!isRecord$1(option)) reject("A form option must be an object");
|
|
1034
|
-
requireString(option["label"], "Form option label");
|
|
1035
|
-
if (!isJsonValue(option["value"])) reject("Form option value must be JSON data");
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
assertActions(surface["actions"]);
|
|
1040
|
-
}
|
|
1041
|
-
function assertChart(surface) {
|
|
1042
|
-
const chart = surface["chart"];
|
|
1043
|
-
if (!isRecord$1(chart)) reject("A chart surface requires a chart object");
|
|
1044
|
-
if (chart["kind"] !== "bar" && chart["kind"] !== "line" && chart["kind"] !== "pie") reject("Chart kind is invalid");
|
|
1045
|
-
if (!Array.isArray(chart["labels"]) || !chart["labels"].every((label) => typeof label === "string")) reject("Chart labels must be an array of strings");
|
|
1046
|
-
if (!Array.isArray(chart["series"])) reject("Chart series must be an array");
|
|
1047
|
-
let points = 0;
|
|
1048
|
-
for (const series of chart["series"]) {
|
|
1049
|
-
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");
|
|
1050
|
-
if (series["name"] !== void 0 && typeof series["name"] !== "string") reject("Chart series name must be a string");
|
|
1051
|
-
points += series["data"].length;
|
|
1052
|
-
}
|
|
1053
|
-
if (points > MAX_CHART_POINTS) reject(`Chart data exceeds the ${MAX_CHART_POINTS}-point limit`);
|
|
1054
|
-
assertActions(surface["actions"]);
|
|
1055
|
-
}
|
|
1056
|
-
function assertTable(surface) {
|
|
1057
|
-
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`);
|
|
1058
|
-
if (!Array.isArray(surface["rows"]) || surface["rows"].length > MAX_TABLE_ROWS) reject(`Table rows must contain at most ${MAX_TABLE_ROWS} items`);
|
|
1059
|
-
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");
|
|
1060
|
-
assertActions(surface["actions"]);
|
|
1061
|
-
}
|
|
1062
|
-
/** Validates the allowlisted, data-only shape accepted by a UI surface renderer. @experimental */
|
|
1063
|
-
function validateUiSurface(value) {
|
|
1064
|
-
if (!isRecord$1(value)) reject("A UI surface must be an object");
|
|
1065
|
-
requireString(value["id"], "UI surface ID");
|
|
1066
|
-
if (value["title"] !== void 0 && typeof value["title"] !== "string") reject("UI surface title must be a string");
|
|
1067
|
-
switch (value["kind"]) {
|
|
1068
|
-
case "form":
|
|
1069
|
-
assertForm(value);
|
|
1070
|
-
break;
|
|
1071
|
-
case "chart":
|
|
1072
|
-
assertChart(value);
|
|
1073
|
-
break;
|
|
1074
|
-
case "table":
|
|
1075
|
-
assertTable(value);
|
|
1076
|
-
break;
|
|
1077
|
-
case "metric":
|
|
1078
|
-
requireString(value["label"], "Metric label");
|
|
1079
|
-
if (typeof value["value"] !== "string" && (typeof value["value"] !== "number" || !Number.isFinite(value["value"]))) reject("Metric value must be a string or finite number");
|
|
1080
|
-
if (value["trend"] !== void 0 && value["trend"] !== "up" && value["trend"] !== "down" && value["trend"] !== "neutral") reject("Metric trend is invalid");
|
|
1081
|
-
break;
|
|
1082
|
-
case "file": {
|
|
1083
|
-
requireString(value["path"], "File path");
|
|
1084
|
-
if (value["mimeType"] !== void 0 && typeof value["mimeType"] !== "string") reject("File mimeType must be a string");
|
|
1085
|
-
const fileSize = value["size"];
|
|
1086
|
-
if (fileSize !== void 0 && (typeof fileSize !== "number" || !Number.isSafeInteger(fileSize) || fileSize < 0)) reject("File size must be a non-negative integer");
|
|
1087
|
-
assertActions(value["actions"]);
|
|
1088
|
-
break;
|
|
1089
|
-
}
|
|
1090
|
-
case "custom":
|
|
1091
|
-
requireString(value["component"], "Custom surface component");
|
|
1092
|
-
if (!isJsonValue(value["props"])) reject("Custom surface props must be JSON data");
|
|
1093
|
-
assertActions(value["actions"]);
|
|
1094
|
-
break;
|
|
1095
|
-
default: reject("UI surface kind is invalid");
|
|
1096
|
-
}
|
|
1097
|
-
if (!isJsonValue(value)) reject("A UI surface must contain JSON data only");
|
|
1098
|
-
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`);
|
|
1099
1113
|
return structuredClone(value);
|
|
1100
1114
|
}
|
|
1101
1115
|
function assertPatch(value) {
|
|
@@ -1106,15 +1120,20 @@ function assertPatch(value) {
|
|
|
1106
1120
|
if (!isJsonValue(value["value"])) reject("Surface patch value must be JSON data");
|
|
1107
1121
|
}
|
|
1108
1122
|
/** Validates an individual event in the framework-neutral surface stream. @experimental */
|
|
1109
|
-
function
|
|
1123
|
+
function validateUiSpecEvent(value) {
|
|
1110
1124
|
if (!isRecord$1(value) || typeof value["type"] !== "string") reject("A UI surface event must have a type");
|
|
1111
1125
|
if (value["runId"] !== void 0) requireString(value["runId"], "Surface event run ID");
|
|
1112
1126
|
switch (value["type"]) {
|
|
1113
|
-
case "open":
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
+
};
|
|
1118
1137
|
case "patch":
|
|
1119
1138
|
requireString(value["id"], "Surface patch ID");
|
|
1120
1139
|
requireNonNegativeInteger(value["revision"], "Surface patch revision");
|
|
@@ -1159,10 +1178,10 @@ function validateUiSurfaceEvent(value) {
|
|
|
1159
1178
|
}
|
|
1160
1179
|
}
|
|
1161
1180
|
/** Extracts validated surface stream events from structured tool output. @experimental */
|
|
1162
|
-
function
|
|
1181
|
+
function extractUiSpecEvents(data) {
|
|
1163
1182
|
if (!isRecord$1(data) || data["$surface"] === void 0) return [];
|
|
1164
1183
|
const raw = data["$surface"];
|
|
1165
|
-
return (Array.isArray(raw) ? raw : [raw]).map((event) =>
|
|
1184
|
+
return (Array.isArray(raw) ? raw : [raw]).map((event) => validateUiSpecEvent(event));
|
|
1166
1185
|
}
|
|
1167
1186
|
/**
|
|
1168
1187
|
* JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
|
|
@@ -1309,6 +1328,103 @@ var TraceRecorder = class {
|
|
|
1309
1328
|
return [...this.#events];
|
|
1310
1329
|
}
|
|
1311
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
|
+
};
|
|
1312
1428
|
/**
|
|
1313
1429
|
* LLM 可见名 → 作者在 SKILL.md 里写的形态。
|
|
1314
1430
|
* `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
|
|
@@ -1338,7 +1454,7 @@ function denialReason(state, canonical) {
|
|
|
1338
1454
|
const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
|
|
1339
1455
|
const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
|
|
1340
1456
|
const slash = canonical.lastIndexOf("/");
|
|
1341
|
-
return `${subject} allowed-tools, but tool "${canonical}" is not listed
|
|
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.`;
|
|
1342
1458
|
}
|
|
1343
1459
|
/**
|
|
1344
1460
|
* 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
|
|
@@ -1379,7 +1495,6 @@ const summarizeArgs = (args) => {
|
|
|
1379
1495
|
const json = JSON.stringify(args);
|
|
1380
1496
|
return json.length > 100 ? `${json.slice(0, 100)}…` : json;
|
|
1381
1497
|
};
|
|
1382
|
-
const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
|
|
1383
1498
|
/**
|
|
1384
1499
|
* 多轮 Agent 循环。
|
|
1385
1500
|
* 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
|
|
@@ -1450,6 +1565,7 @@ var AgentLoop = class {
|
|
|
1450
1565
|
activatedTools: /* @__PURE__ */ new Map(),
|
|
1451
1566
|
skillAllowedTools: /* @__PURE__ */ new Map(),
|
|
1452
1567
|
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1568
|
+
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
1453
1569
|
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
1454
1570
|
now,
|
|
1455
1571
|
interactionSeq: 0,
|
|
@@ -1478,7 +1594,13 @@ var AgentLoop = class {
|
|
|
1478
1594
|
strategy: route.strategy,
|
|
1479
1595
|
skillCount: route.catalog.entries.length
|
|
1480
1596
|
} });
|
|
1481
|
-
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);
|
|
1482
1604
|
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
1483
1605
|
try {
|
|
1484
1606
|
return await source.listToolSpecs();
|
|
@@ -1487,15 +1609,25 @@ var AgentLoop = class {
|
|
|
1487
1609
|
return [];
|
|
1488
1610
|
}
|
|
1489
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
|
+
}
|
|
1490
1622
|
state.messages = [
|
|
1491
1623
|
{
|
|
1492
1624
|
role: "system",
|
|
1493
|
-
content: route.systemPrompt
|
|
1625
|
+
content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
|
|
1494
1626
|
},
|
|
1495
1627
|
...(input.history ?? []).map((m) => ({ ...m })),
|
|
1496
1628
|
{
|
|
1497
1629
|
role: "user",
|
|
1498
|
-
content: input.userPrompt
|
|
1630
|
+
content: textParts(input.userPrompt)
|
|
1499
1631
|
}
|
|
1500
1632
|
];
|
|
1501
1633
|
try {
|
|
@@ -1527,17 +1659,25 @@ var AgentLoop = class {
|
|
|
1527
1659
|
}
|
|
1528
1660
|
/**
|
|
1529
1661
|
* D10 判定的唯一出口(暴露点 + 分发点共用)。
|
|
1530
|
-
* 0.
|
|
1662
|
+
* 0.4.0(D1)起两个调用点都看返回值:暴露点过滤、分发点回喂 TOOL_NOT_ALLOWED。
|
|
1663
|
+
* 判定语义与 0.3.0 一致,改的只是调用点与 trace 事件类型(warning → denied)。
|
|
1531
1664
|
*/
|
|
1532
1665
|
#checkToolAccess(state, toolName) {
|
|
1533
1666
|
const verdict = evaluateToolAccess(state, toolName);
|
|
1534
1667
|
if (verdict.allowed) return true;
|
|
1535
1668
|
if (!state.warnedDeniedTools.has(toolName)) {
|
|
1536
1669
|
state.warnedDeniedTools.add(toolName);
|
|
1537
|
-
state.trace.record("
|
|
1670
|
+
state.trace.record("tool.denied", {
|
|
1671
|
+
message: verdict.reason ?? `Tool "${toolName}" is not allowed`,
|
|
1672
|
+
data: { name: toolName }
|
|
1673
|
+
});
|
|
1538
1674
|
}
|
|
1539
1675
|
return false;
|
|
1540
1676
|
}
|
|
1677
|
+
/** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
|
|
1678
|
+
#deniedToolError(state, toolName) {
|
|
1679
|
+
return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
|
|
1680
|
+
}
|
|
1541
1681
|
/** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
|
|
1542
1682
|
async #turnLoop(state, startTurn, externalSpecs) {
|
|
1543
1683
|
const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
|
|
@@ -1549,8 +1689,7 @@ var AgentLoop = class {
|
|
|
1549
1689
|
state.turn = turn;
|
|
1550
1690
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1551
1691
|
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1552
|
-
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs];
|
|
1553
|
-
for (const spec of skillToolSpecs) this.#checkToolAccess(state, spec.name);
|
|
1692
|
+
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
|
|
1554
1693
|
const toolSpecs = [
|
|
1555
1694
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1556
1695
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
@@ -1584,22 +1723,29 @@ var AgentLoop = class {
|
|
|
1584
1723
|
const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
|
|
1585
1724
|
return finish("failed", "llm-error", messageOf(e), code);
|
|
1586
1725
|
}
|
|
1726
|
+
const responseText = partsToText(response.content);
|
|
1587
1727
|
trace.record("llm.response", { data: {
|
|
1588
1728
|
turn,
|
|
1589
1729
|
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
1590
|
-
contentLength:
|
|
1730
|
+
contentLength: responseText.length
|
|
1591
1731
|
} });
|
|
1592
1732
|
if (!response.toolCalls?.length) {
|
|
1593
1733
|
messages.push({
|
|
1594
1734
|
role: "assistant",
|
|
1595
|
-
content: response.content ??
|
|
1735
|
+
content: response.content ?? []
|
|
1596
1736
|
});
|
|
1597
|
-
return finish("completed", "final-answer",
|
|
1737
|
+
return finish("completed", "final-answer", responseText);
|
|
1598
1738
|
}
|
|
1599
|
-
await this.#lifecycle(
|
|
1739
|
+
await this.#lifecycle({
|
|
1740
|
+
phase: "execute",
|
|
1741
|
+
data: {
|
|
1742
|
+
kind: "turn",
|
|
1743
|
+
turn
|
|
1744
|
+
}
|
|
1745
|
+
}, state);
|
|
1600
1746
|
messages.push({
|
|
1601
1747
|
role: "assistant",
|
|
1602
|
-
content: response.content ??
|
|
1748
|
+
content: response.content ?? [],
|
|
1603
1749
|
toolCalls: response.toolCalls
|
|
1604
1750
|
});
|
|
1605
1751
|
for (const call of response.toolCalls) {
|
|
@@ -1613,7 +1759,7 @@ var AgentLoop = class {
|
|
|
1613
1759
|
messages.push({
|
|
1614
1760
|
role: "tool",
|
|
1615
1761
|
toolCallId: call.id,
|
|
1616
|
-
content: await this.#serializeToolResult(call, result, state)
|
|
1762
|
+
content: textParts(await this.#serializeToolResult(call, result, state))
|
|
1617
1763
|
});
|
|
1618
1764
|
await this.#drainSurfaceAction(state);
|
|
1619
1765
|
}
|
|
@@ -1659,7 +1805,10 @@ var AgentLoop = class {
|
|
|
1659
1805
|
trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
|
|
1660
1806
|
}
|
|
1661
1807
|
try {
|
|
1662
|
-
await this.#lifecycle(
|
|
1808
|
+
await this.#lifecycle({
|
|
1809
|
+
phase: status === "completed" ? "complete" : "fail",
|
|
1810
|
+
data: { reason }
|
|
1811
|
+
}, state);
|
|
1663
1812
|
} catch (e) {
|
|
1664
1813
|
trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
|
|
1665
1814
|
}
|
|
@@ -1675,7 +1824,7 @@ var AgentLoop = class {
|
|
|
1675
1824
|
const store = this.#deps.snapshotStore;
|
|
1676
1825
|
if (!store) return;
|
|
1677
1826
|
const snapshot = {
|
|
1678
|
-
schemaVersion:
|
|
1827
|
+
schemaVersion: 2,
|
|
1679
1828
|
runId: state.runId,
|
|
1680
1829
|
sessionId: state.run.sessionId,
|
|
1681
1830
|
userPrompt: state.run.userPrompt,
|
|
@@ -1736,6 +1885,7 @@ var AgentLoop = class {
|
|
|
1736
1885
|
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
1737
1886
|
skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
|
|
1738
1887
|
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1888
|
+
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
1739
1889
|
toolTimeoutMs: snapshot.config.toolTimeoutMs,
|
|
1740
1890
|
now,
|
|
1741
1891
|
interactionSeq: snapshot.interactionSeq ?? 0,
|
|
@@ -1784,7 +1934,7 @@ var AgentLoop = class {
|
|
|
1784
1934
|
state.messages.push({
|
|
1785
1935
|
role: "tool",
|
|
1786
1936
|
toolCallId: pendingCall.id,
|
|
1787
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
1937
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1788
1938
|
});
|
|
1789
1939
|
await this.#drainSurfaceAction(state);
|
|
1790
1940
|
} else if (pending?.type === "ask" && pendingCall) {
|
|
@@ -1806,7 +1956,7 @@ var AgentLoop = class {
|
|
|
1806
1956
|
state.messages.push({
|
|
1807
1957
|
role: "tool",
|
|
1808
1958
|
toolCallId: pendingCall.id,
|
|
1809
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
1959
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1810
1960
|
});
|
|
1811
1961
|
await this.#drainSurfaceAction(state);
|
|
1812
1962
|
} else if (pendingCall) {
|
|
@@ -1814,7 +1964,7 @@ var AgentLoop = class {
|
|
|
1814
1964
|
state.messages.push({
|
|
1815
1965
|
role: "tool",
|
|
1816
1966
|
toolCallId: pendingCall.id,
|
|
1817
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
1967
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1818
1968
|
});
|
|
1819
1969
|
await this.#drainSurfaceAction(state);
|
|
1820
1970
|
}
|
|
@@ -1825,7 +1975,7 @@ var AgentLoop = class {
|
|
|
1825
1975
|
state.messages.push({
|
|
1826
1976
|
role: "tool",
|
|
1827
1977
|
toolCallId: next.id,
|
|
1828
|
-
content: await this.#serializeToolResult(next, result, state)
|
|
1978
|
+
content: textParts(await this.#serializeToolResult(next, result, state))
|
|
1829
1979
|
});
|
|
1830
1980
|
await this.#drainSurfaceAction(state);
|
|
1831
1981
|
}
|
|
@@ -1868,7 +2018,7 @@ var AgentLoop = class {
|
|
|
1868
2018
|
sessionId: state.run.sessionId,
|
|
1869
2019
|
ts: state.now(),
|
|
1870
2020
|
data: {
|
|
1871
|
-
|
|
2021
|
+
kind: "llm-delta",
|
|
1872
2022
|
delta
|
|
1873
2023
|
}
|
|
1874
2024
|
});
|
|
@@ -1882,23 +2032,23 @@ var AgentLoop = class {
|
|
|
1882
2032
|
emitDelta(event.delta);
|
|
1883
2033
|
} else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
|
|
1884
2034
|
else if (event.type === "done") doneContent = event.content;
|
|
2035
|
+
const text = doneContent ?? content;
|
|
1885
2036
|
return {
|
|
1886
|
-
content:
|
|
2037
|
+
content: text === "" ? void 0 : textParts(text),
|
|
1887
2038
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
1888
2039
|
};
|
|
1889
2040
|
}
|
|
1890
2041
|
/** 生命周期接线:更新 phase、发事件、跑钩子 */
|
|
1891
|
-
async #lifecycle(
|
|
1892
|
-
state.run.phase = phase;
|
|
2042
|
+
async #lifecycle(init, state) {
|
|
2043
|
+
state.run.phase = init.phase;
|
|
1893
2044
|
const event = {
|
|
1894
|
-
|
|
2045
|
+
...init,
|
|
1895
2046
|
runId: state.runId,
|
|
1896
2047
|
sessionId: state.run.sessionId,
|
|
1897
|
-
ts: state.now()
|
|
1898
|
-
...data ? { data } : {}
|
|
2048
|
+
ts: state.now()
|
|
1899
2049
|
};
|
|
1900
2050
|
this.#deps.eventBus?.emit(event);
|
|
1901
|
-
if (this.#deps.hooks) await this.#deps.hooks.run(phase, {
|
|
2051
|
+
if (this.#deps.hooks) await this.#deps.hooks.run(init.phase, {
|
|
1902
2052
|
event,
|
|
1903
2053
|
run: state.run
|
|
1904
2054
|
});
|
|
@@ -1925,10 +2075,14 @@ var AgentLoop = class {
|
|
|
1925
2075
|
run.status = "interrupted";
|
|
1926
2076
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
1927
2077
|
await this.#saveSnapshot(state, { interaction: request });
|
|
1928
|
-
await this.#lifecycle(
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2078
|
+
await this.#lifecycle({
|
|
2079
|
+
phase: "interact",
|
|
2080
|
+
data: {
|
|
2081
|
+
kind: "interaction",
|
|
2082
|
+
interactionId: request.id,
|
|
2083
|
+
interactionType: request.type
|
|
2084
|
+
}
|
|
2085
|
+
}, state);
|
|
1932
2086
|
state.trace.record("ui.requested", { data: {
|
|
1933
2087
|
interactionId: request.id,
|
|
1934
2088
|
type: request.type,
|
|
@@ -1961,7 +2115,13 @@ var AgentLoop = class {
|
|
|
1961
2115
|
interactionId: request.id,
|
|
1962
2116
|
type: request.type
|
|
1963
2117
|
} });
|
|
1964
|
-
await this.#lifecycle(
|
|
2118
|
+
await this.#lifecycle({
|
|
2119
|
+
phase: "execute",
|
|
2120
|
+
data: {
|
|
2121
|
+
kind: "interaction-resumed",
|
|
2122
|
+
interactionId: request.id
|
|
2123
|
+
}
|
|
2124
|
+
}, state);
|
|
1965
2125
|
await this.#appendParamHistory(state, request, response.value);
|
|
1966
2126
|
return response.value;
|
|
1967
2127
|
}
|
|
@@ -1995,8 +2155,8 @@ var AgentLoop = class {
|
|
|
1995
2155
|
if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
|
|
1996
2156
|
else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
|
|
1997
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);
|
|
1998
2159
|
else {
|
|
1999
|
-
this.#checkToolAccess(state, call.name);
|
|
2000
2160
|
const resolution = resolveToolName(call.name, state.activated);
|
|
2001
2161
|
if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
2002
2162
|
else {
|
|
@@ -2021,7 +2181,7 @@ var AgentLoop = class {
|
|
|
2021
2181
|
chart
|
|
2022
2182
|
});
|
|
2023
2183
|
try {
|
|
2024
|
-
for (const event of
|
|
2184
|
+
for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
|
|
2025
2185
|
} catch (e) {
|
|
2026
2186
|
state.trace.record("run.warning", {
|
|
2027
2187
|
message: `UI surface rejected: ${messageOf(e)}`,
|
|
@@ -2060,14 +2220,14 @@ var AgentLoop = class {
|
|
|
2060
2220
|
await bridge.renderSurface(attributed);
|
|
2061
2221
|
state.surfaceEvents.push(structuredClone(attributed));
|
|
2062
2222
|
if (attributed.type === "open") {
|
|
2063
|
-
const waiting =
|
|
2223
|
+
const waiting = (attributed.actions ?? []).filter((action) => action.awaitResponse);
|
|
2064
2224
|
if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
|
|
2065
2225
|
const action = waiting[0];
|
|
2066
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" });
|
|
2067
2227
|
else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
|
|
2068
2228
|
else state.pendingSurfaceAction = {
|
|
2069
2229
|
runId: state.runId,
|
|
2070
|
-
surfaceId: attributed.
|
|
2230
|
+
surfaceId: attributed.id,
|
|
2071
2231
|
actionId: action.id,
|
|
2072
2232
|
intent: action.intent,
|
|
2073
2233
|
nonce: action.nonce
|
|
@@ -2085,21 +2245,17 @@ var AgentLoop = class {
|
|
|
2085
2245
|
}
|
|
2086
2246
|
/** Assigns unforgeable action nonces after model output has passed structural validation. */
|
|
2087
2247
|
#attributeSurfaceEvent(state, event) {
|
|
2088
|
-
|
|
2089
|
-
if (event.type !== "open" || actions.length === 0) return {
|
|
2248
|
+
if (event.type !== "open" || (event.actions ?? []).length === 0) return {
|
|
2090
2249
|
...event,
|
|
2091
2250
|
runId: state.runId
|
|
2092
2251
|
};
|
|
2093
2252
|
return {
|
|
2094
|
-
|
|
2253
|
+
...event,
|
|
2095
2254
|
runId: state.runId,
|
|
2096
|
-
|
|
2097
|
-
...
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
|
|
2101
|
-
}))
|
|
2102
|
-
}
|
|
2255
|
+
actions: (event.actions ?? []).map((action) => ({
|
|
2256
|
+
...action,
|
|
2257
|
+
nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
|
|
2258
|
+
}))
|
|
2103
2259
|
};
|
|
2104
2260
|
}
|
|
2105
2261
|
/** Awaits the single action emitted with the most recently persisted tool result. */
|
|
@@ -2126,13 +2282,13 @@ var AgentLoop = class {
|
|
|
2126
2282
|
state.processedSurfaceActionNonces.add(response.nonce);
|
|
2127
2283
|
state.messages.push({
|
|
2128
2284
|
role: "user",
|
|
2129
|
-
content: JSON.stringify({
|
|
2285
|
+
content: textParts(JSON.stringify({
|
|
2130
2286
|
type: "webskill_surface_action",
|
|
2131
2287
|
surfaceId: response.surfaceId,
|
|
2132
2288
|
actionId: response.actionId,
|
|
2133
2289
|
intent: response.intent,
|
|
2134
2290
|
value: response.value ?? null
|
|
2135
|
-
})
|
|
2291
|
+
}))
|
|
2136
2292
|
});
|
|
2137
2293
|
}
|
|
2138
2294
|
async #interactSurfaceAction(state, request, resumed) {
|
|
@@ -2144,20 +2300,26 @@ var AgentLoop = class {
|
|
|
2144
2300
|
const { run } = state;
|
|
2145
2301
|
run.status = "interrupted";
|
|
2146
2302
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
2303
|
+
const pendingResponse = bridge.requestSurfaceAction(request);
|
|
2304
|
+
pendingResponse.catch(() => void 0);
|
|
2147
2305
|
await this.#saveSnapshot(state, { surfaceAction: request });
|
|
2148
|
-
await this.#lifecycle(
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
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);
|
|
2154
2316
|
state.trace.record("ui.surface-action.requested", { data: {
|
|
2155
2317
|
surfaceId: request.surfaceId,
|
|
2156
2318
|
actionId: request.actionId,
|
|
2157
2319
|
nonce: request.nonce,
|
|
2158
2320
|
...resumed ? { resumed: true } : {}
|
|
2159
2321
|
} });
|
|
2160
|
-
const response = await this.#withInteractionTimeout(
|
|
2322
|
+
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
2161
2323
|
run.status = "running";
|
|
2162
2324
|
run.interruptExpiresAt = void 0;
|
|
2163
2325
|
state.trace.record("ui.surface-action.resolved", { data: {
|
|
@@ -2165,9 +2327,17 @@ var AgentLoop = class {
|
|
|
2165
2327
|
actionId: request.actionId,
|
|
2166
2328
|
nonce: request.nonce
|
|
2167
2329
|
} });
|
|
2168
|
-
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);
|
|
2169
2338
|
return response;
|
|
2170
2339
|
} catch (e) {
|
|
2340
|
+
bridge.cancelSurfaceAction?.(request.nonce);
|
|
2171
2341
|
state.run.status = "running";
|
|
2172
2342
|
state.run.interruptExpiresAt = void 0;
|
|
2173
2343
|
if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
|
|
@@ -2215,7 +2385,7 @@ var AgentLoop = class {
|
|
|
2215
2385
|
sessionId: state.run.sessionId,
|
|
2216
2386
|
ts: state.now(),
|
|
2217
2387
|
data: {
|
|
2218
|
-
|
|
2388
|
+
kind: "tool",
|
|
2219
2389
|
status,
|
|
2220
2390
|
name: call.name,
|
|
2221
2391
|
callId: call.id,
|
|
@@ -2285,7 +2455,13 @@ var AgentLoop = class {
|
|
|
2285
2455
|
skillName: name,
|
|
2286
2456
|
source: "external"
|
|
2287
2457
|
} });
|
|
2288
|
-
await this.#lifecycle(
|
|
2458
|
+
await this.#lifecycle({
|
|
2459
|
+
phase: "activate",
|
|
2460
|
+
data: {
|
|
2461
|
+
skillName: name,
|
|
2462
|
+
source: "external"
|
|
2463
|
+
}
|
|
2464
|
+
}, state);
|
|
2289
2465
|
await this.#writeActivationMemory(name, state);
|
|
2290
2466
|
}
|
|
2291
2467
|
return {
|
|
@@ -2339,18 +2515,62 @@ var AgentLoop = class {
|
|
|
2339
2515
|
if (!check) return false;
|
|
2340
2516
|
return await check(skillName) === false;
|
|
2341
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
|
+
}
|
|
2342
2554
|
/** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
|
|
2343
2555
|
async #activateSkill(skillName, state, via, skillMdText) {
|
|
2344
2556
|
if (await this.#guardDenied("canActivate", skillName)) {
|
|
2345
2557
|
state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
|
|
2346
2558
|
return "";
|
|
2347
2559
|
}
|
|
2560
|
+
if (!await this.#integrityOk(skillName, state)) return "";
|
|
2348
2561
|
state.activated.add(skillName);
|
|
2349
2562
|
state.trace.record("skill.activated", { data: {
|
|
2350
2563
|
skillName,
|
|
2351
2564
|
...via ? { via } : {}
|
|
2352
2565
|
} });
|
|
2353
|
-
await this.#lifecycle(
|
|
2566
|
+
await this.#lifecycle({
|
|
2567
|
+
phase: "activate",
|
|
2568
|
+
data: {
|
|
2569
|
+
skillName,
|
|
2570
|
+
source: "local",
|
|
2571
|
+
...via ? { via } : {}
|
|
2572
|
+
}
|
|
2573
|
+
}, state);
|
|
2354
2574
|
await this.#writeActivationMemory(skillName, state);
|
|
2355
2575
|
const root = this.#deps.skillIndex.get(skillName);
|
|
2356
2576
|
if (!root) return "";
|
|
@@ -2484,11 +2704,41 @@ var AgentLoop = class {
|
|
|
2484
2704
|
if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
|
|
2485
2705
|
}
|
|
2486
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
|
+
});
|
|
2487
2711
|
return result;
|
|
2488
2712
|
} catch (e) {
|
|
2489
2713
|
if (e instanceof RunTerminated) throw e;
|
|
2490
2714
|
await this.#bumpSkillStat(skillName, "failures", state);
|
|
2491
|
-
|
|
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)}` });
|
|
2492
2742
|
}
|
|
2493
2743
|
}
|
|
2494
2744
|
/** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
|
|
@@ -2598,79 +2848,6 @@ function mergeCatalogEntries(localEntries, providerEntries) {
|
|
|
2598
2848
|
for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
2599
2849
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2600
2850
|
}
|
|
2601
|
-
const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
2602
|
-
const SNAPSHOT_SUFFIX = ".snapshot.json";
|
|
2603
|
-
/**
|
|
2604
|
-
* FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
|
|
2605
|
-
* 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
|
|
2606
|
-
* save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
|
|
2607
|
-
*
|
|
2608
|
-
* 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
|
|
2609
|
-
* 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
|
|
2610
|
-
* @experimental
|
|
2611
|
-
*/
|
|
2612
|
-
var FsRunSnapshotStore = class {
|
|
2613
|
-
#root;
|
|
2614
|
-
#fs;
|
|
2615
|
-
constructor(deps) {
|
|
2616
|
-
this.#root = deps.root.replace(/\/+$/, "");
|
|
2617
|
-
this.#fs = deps.fs;
|
|
2618
|
-
}
|
|
2619
|
-
#path(runId) {
|
|
2620
|
-
return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
|
|
2621
|
-
}
|
|
2622
|
-
async save(snapshot) {
|
|
2623
|
-
await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
|
|
2624
|
-
await this.#pruneExpired();
|
|
2625
|
-
}
|
|
2626
|
-
async load(runId) {
|
|
2627
|
-
const path = this.#path(runId);
|
|
2628
|
-
if (!await this.#fs.exists(path)) return void 0;
|
|
2629
|
-
let parsed;
|
|
2630
|
-
try {
|
|
2631
|
-
parsed = JSON.parse(await this.#fs.readText(path));
|
|
2632
|
-
} catch (e) {
|
|
2633
|
-
await this.#fs.remove(path).catch(() => void 0);
|
|
2634
|
-
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
2635
|
-
}
|
|
2636
|
-
const snapshot = parsed;
|
|
2637
|
-
if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
|
|
2638
|
-
await this.#fs.remove(path).catch(() => void 0);
|
|
2639
|
-
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
2640
|
-
}
|
|
2641
|
-
return snapshot;
|
|
2642
|
-
}
|
|
2643
|
-
async delete(runId) {
|
|
2644
|
-
const path = this.#path(runId);
|
|
2645
|
-
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
2646
|
-
}
|
|
2647
|
-
async list() {
|
|
2648
|
-
await this.#pruneExpired();
|
|
2649
|
-
if (!await this.#fs.exists(this.#root)) return [];
|
|
2650
|
-
const out = [];
|
|
2651
|
-
for (const entry of await this.#fs.list(this.#root)) {
|
|
2652
|
-
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
2653
|
-
try {
|
|
2654
|
-
out.push(JSON.parse(await this.#fs.readText(entry.path)));
|
|
2655
|
-
} catch {}
|
|
2656
|
-
}
|
|
2657
|
-
return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
|
|
2658
|
-
}
|
|
2659
|
-
/** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
|
|
2660
|
-
async #pruneExpired() {
|
|
2661
|
-
try {
|
|
2662
|
-
if (!await this.#fs.exists(this.#root)) return;
|
|
2663
|
-
const now = Date.now();
|
|
2664
|
-
for (const entry of await this.#fs.list(this.#root)) {
|
|
2665
|
-
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
2666
|
-
try {
|
|
2667
|
-
const parsed = JSON.parse(await this.#fs.readText(entry.path));
|
|
2668
|
-
if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
|
|
2669
|
-
} catch {}
|
|
2670
|
-
}
|
|
2671
|
-
} catch {}
|
|
2672
|
-
}
|
|
2673
|
-
};
|
|
2674
2851
|
/**
|
|
2675
2852
|
* session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
|
|
2676
2853
|
* 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
|
|
@@ -2804,7 +2981,9 @@ var WebSkillRuntime = class {
|
|
|
2804
2981
|
skillProviders: this.#deps.skillProviders,
|
|
2805
2982
|
catalogFilter: this.#deps.catalogFilter,
|
|
2806
2983
|
snapshotStore: this.#deps.snapshotStore,
|
|
2807
|
-
skillStateGuard: this.#deps.skillStateGuard
|
|
2984
|
+
skillStateGuard: this.#deps.skillStateGuard,
|
|
2985
|
+
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
2986
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
2808
2987
|
}, this.#deps.config);
|
|
2809
2988
|
const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
2810
2989
|
this.#loops.set(runId, loop);
|
|
@@ -2841,14 +3020,14 @@ var WebSkillRuntime = class {
|
|
|
2841
3020
|
});
|
|
2842
3021
|
return result;
|
|
2843
3022
|
}
|
|
2844
|
-
/** D3
|
|
3023
|
+
/** D3:列出 interrupted run(供 UI 展示"未完成任务");版本不受支持的项带 unsupported 标记 */
|
|
2845
3024
|
async listInterruptedRuns() {
|
|
2846
3025
|
if (!this.#deps.snapshotStore) return [];
|
|
2847
3026
|
return this.#deps.snapshotStore.list();
|
|
2848
3027
|
}
|
|
2849
3028
|
/**
|
|
2850
3029
|
* D3 恢复 interrupted run:
|
|
2851
|
-
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;
|
|
3030
|
+
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;schema 版本不受支持 → RUN_SNAPSHOT_SCHEMA_UNSUPPORTED(由 store 抛出,不删文件);
|
|
2852
3031
|
* 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
|
|
2853
3032
|
* @experimental
|
|
2854
3033
|
*/
|
|
@@ -2856,10 +3035,6 @@ var WebSkillRuntime = class {
|
|
|
2856
3035
|
const store = this.#deps.snapshotStore;
|
|
2857
3036
|
const snapshot = store ? await store.load(runId) : void 0;
|
|
2858
3037
|
if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
|
|
2859
|
-
if (snapshot.schemaVersion !== 1) {
|
|
2860
|
-
await store.delete(runId);
|
|
2861
|
-
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
|
|
2862
|
-
}
|
|
2863
3038
|
if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
|
|
2864
3039
|
await store.delete(runId);
|
|
2865
3040
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2912,7 +3087,9 @@ var WebSkillRuntime = class {
|
|
|
2912
3087
|
skillProviders: this.#deps.skillProviders,
|
|
2913
3088
|
catalogFilter: this.#deps.catalogFilter,
|
|
2914
3089
|
snapshotStore: store,
|
|
2915
|
-
skillStateGuard: this.#deps.skillStateGuard
|
|
3090
|
+
skillStateGuard: this.#deps.skillStateGuard,
|
|
3091
|
+
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
3092
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
2916
3093
|
}, this.#deps.config);
|
|
2917
3094
|
this.#loops.set(runId, loop);
|
|
2918
3095
|
try {
|
|
@@ -3031,6 +3208,7 @@ function sourceFromUrl(url) {
|
|
|
3031
3208
|
/** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
|
|
3032
3209
|
var HookRunner = class {
|
|
3033
3210
|
#hooks = /* @__PURE__ */ new Map();
|
|
3211
|
+
/** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
|
|
3034
3212
|
timeoutMs;
|
|
3035
3213
|
failOnHookError;
|
|
3036
3214
|
onWarning;
|
|
@@ -3046,6 +3224,15 @@ var HookRunner = class {
|
|
|
3046
3224
|
this.#hooks.set(key, list);
|
|
3047
3225
|
return this;
|
|
3048
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
|
+
}
|
|
3049
3236
|
async run(phase, ctx) {
|
|
3050
3237
|
const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
|
|
3051
3238
|
for (const hook of hooks) try {
|
|
@@ -3663,6 +3850,8 @@ function summarizeToolCalls(run) {
|
|
|
3663
3850
|
return calls;
|
|
3664
3851
|
}
|
|
3665
3852
|
const SESSION_SCHEMA_VERSION = 1;
|
|
3853
|
+
/** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
|
|
3854
|
+
const FS_SESSION_PAGE_SIZE = 50;
|
|
3666
3855
|
const toMeta = (record) => ({
|
|
3667
3856
|
id: record.id,
|
|
3668
3857
|
createdAt: record.createdAt,
|
|
@@ -3673,6 +3862,25 @@ const toMeta = (record) => ({
|
|
|
3673
3862
|
});
|
|
3674
3863
|
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3675
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
|
+
/**
|
|
3676
3884
|
* 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
|
|
3677
3885
|
* 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
|
|
3678
3886
|
*/
|
|
@@ -3746,7 +3954,7 @@ var FsSessionStore = class {
|
|
|
3746
3954
|
return record;
|
|
3747
3955
|
}
|
|
3748
3956
|
async list(options = {}) {
|
|
3749
|
-
if (!await this.#fs.exists(this.#root)) return [];
|
|
3957
|
+
if (!await this.#fs.exists(this.#root)) return { items: [] };
|
|
3750
3958
|
const metas = [];
|
|
3751
3959
|
for (const entry of await this.#fs.list(this.#root)) {
|
|
3752
3960
|
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
@@ -3760,7 +3968,15 @@ var FsSessionStore = class {
|
|
|
3760
3968
|
if (record.archived === true && options.includeArchived !== true) continue;
|
|
3761
3969
|
metas.push(toMeta(record));
|
|
3762
3970
|
}
|
|
3763
|
-
|
|
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");
|
|
3764
3980
|
}
|
|
3765
3981
|
async get(id) {
|
|
3766
3982
|
const path = this.#path(id);
|
|
@@ -3820,4 +4036,4 @@ var FsSessionStore = class {
|
|
|
3820
4036
|
};
|
|
3821
4037
|
|
|
3822
4038
|
//#endregion
|
|
3823
|
-
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 };
|