@webskill/sdk 0.3.0 → 0.5.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/agent.d.ts +2 -0
- package/dist/agent.js +867 -0
- package/dist/browser.d.ts +137 -4
- package/dist/browser.js +458 -22
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-DV7cPpUm-C77AEEx9.js} +477 -157
- package/dist/{dist-rorEJsNi.js → dist-6C03DShK.js} +654 -298
- package/dist/{dist-ZKaM8j06.js → dist-bewtXYlO.js} +1061 -807
- package/dist/governance.d.ts +87 -10
- package/dist/governance.js +194 -24
- package/dist/{index-wiV5X8Rz.d.ts → index-Bsqg4ftU.d.ts} +151 -143
- package/dist/index-D_7ZZjkl.d.ts +411 -0
- package/dist/{index-8d-oEDww.d.ts → index-vBz_FC9w.d.ts} +289 -24
- 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-W3Ce896k-ClFTRZFs.js} +6 -5
- package/dist/{skillVersionStore-DOEI9ptb-BxbYL70B.d.ts → skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts} +43 -11
- 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-D_hoCri8-BnNPiZCi.d.ts} +111 -72
- package/dist/ui-react.d.ts +352 -20
- package/dist/ui-react.js +3804 -3478
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +25 -6
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CNaUpasU-BslMcxRZ.js → webskillLitCatalog-_mugzRHx-DiuJpCuf.js} +398 -122
- package/package.json +6 -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
|
};
|
|
@@ -905,22 +991,22 @@ function createScriptContext(deps) {
|
|
|
905
991
|
...onWarning ? { onWarning } : {}
|
|
906
992
|
};
|
|
907
993
|
}
|
|
908
|
-
const isRecord$
|
|
994
|
+
const isRecord$3 = (v) => typeof v === "object" && v !== null;
|
|
909
995
|
/**
|
|
910
996
|
* $chart 约定的形状校验:JSON content 的 data 含 $chart 键且形状合法 → ChartSpec;
|
|
911
997
|
* 任何畸形(kind 非法 / labels 非字符串数组 / series 项缺数值 data)→ undefined(忽略不炸)。
|
|
912
998
|
*/
|
|
913
999
|
function extractChartSpec(data) {
|
|
914
|
-
if (!isRecord$
|
|
1000
|
+
if (!isRecord$3(data)) return void 0;
|
|
915
1001
|
const raw = data["$chart"];
|
|
916
|
-
if (!isRecord$
|
|
1002
|
+
if (!isRecord$3(raw)) return void 0;
|
|
917
1003
|
const { kind, labels, series } = raw;
|
|
918
1004
|
if (kind !== "bar" && kind !== "line" && kind !== "pie") return void 0;
|
|
919
1005
|
if (!Array.isArray(labels) || !labels.every((l) => typeof l === "string")) return void 0;
|
|
920
1006
|
if (!Array.isArray(series)) return void 0;
|
|
921
1007
|
const validSeries = [];
|
|
922
1008
|
for (const item of series) {
|
|
923
|
-
if (!isRecord$
|
|
1009
|
+
if (!isRecord$3(item) || !Array.isArray(item["data"]) || !item["data"].every((n) => typeof n === "number")) return;
|
|
924
1010
|
const name = item["name"];
|
|
925
1011
|
validSeries.push({
|
|
926
1012
|
...typeof name === "string" ? { name } : {},
|
|
@@ -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,17 +1057,7 @@ const actionIntents = /* @__PURE__ */ new Set([
|
|
|
974
1057
|
"download",
|
|
975
1058
|
"refresh"
|
|
976
1059
|
]);
|
|
977
|
-
|
|
978
|
-
"text",
|
|
979
|
-
"number",
|
|
980
|
-
"date",
|
|
981
|
-
"textarea",
|
|
982
|
-
"select",
|
|
983
|
-
"multi-select",
|
|
984
|
-
"toggle",
|
|
985
|
-
"file"
|
|
986
|
-
]);
|
|
987
|
-
function isRecord$1(value) {
|
|
1060
|
+
function isRecord$2(value) {
|
|
988
1061
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
989
1062
|
}
|
|
990
1063
|
function reject(message) {
|
|
@@ -1001,120 +1074,66 @@ function isJsonValue(value, depth = 0) {
|
|
|
1001
1074
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1002
1075
|
if (typeof value === "number") return Number.isFinite(value);
|
|
1003
1076
|
if (Array.isArray(value)) return value.every((item) => isJsonValue(item, depth + 1));
|
|
1004
|
-
if (!isRecord$
|
|
1077
|
+
if (!isRecord$2(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$2(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$2(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
|
-
if (!isRecord$
|
|
1100
|
+
if (!isRecord$2(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) {
|
|
1102
|
-
if (!isRecord$
|
|
1116
|
+
if (!isRecord$2(value)) reject("A surface patch operation must be an object");
|
|
1103
1117
|
if (value["op"] !== "replace" && value["op"] !== "merge" && value["op"] !== "append") reject("Surface patch operation is invalid");
|
|
1104
1118
|
requireString(value["path"], "Surface patch path");
|
|
1105
1119
|
if (!value["path"].startsWith("/")) reject("Surface patch path must be a JSON pointer");
|
|
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
|
|
1110
|
-
if (!isRecord$
|
|
1123
|
+
function validateUiSpecEvent(value) {
|
|
1124
|
+
if (!isRecord$2(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,17 +1178,75 @@ function validateUiSurfaceEvent(value) {
|
|
|
1159
1178
|
}
|
|
1160
1179
|
}
|
|
1161
1180
|
/** Extracts validated surface stream events from structured tool output. @experimental */
|
|
1162
|
-
function
|
|
1163
|
-
if (!isRecord$
|
|
1181
|
+
function extractUiSpecEvents(data) {
|
|
1182
|
+
if (!isRecord$2(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));
|
|
1185
|
+
}
|
|
1186
|
+
/** 跨会话表单填写值在 `user:{userId}` scope 下的 key(FR-5.7) @experimental */
|
|
1187
|
+
const FORM_VALUES_KEY = "formValues";
|
|
1188
|
+
/**
|
|
1189
|
+
* 跨会话稳定的字段标识(FR-5.6)。必须带技能名:
|
|
1190
|
+
* 只有字段名时,两个技能各自的 `email` 会互相串号。
|
|
1191
|
+
* @experimental
|
|
1192
|
+
*/
|
|
1193
|
+
function formFieldKey(skillName, fieldName) {
|
|
1194
|
+
return `${skillName}#${fieldName}`;
|
|
1195
|
+
}
|
|
1196
|
+
/** memory 里的原始值形状不受控(宿主可能手改文件),逐条过滤而不是整体信任 @experimental */
|
|
1197
|
+
function readFormValues(raw) {
|
|
1198
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
1199
|
+
const out = {};
|
|
1200
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
1201
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1202
|
+
const record = entry;
|
|
1203
|
+
if (typeof record.ts !== "number" || !("value" in record)) continue;
|
|
1204
|
+
out[key] = {
|
|
1205
|
+
value: record.value,
|
|
1206
|
+
ts: record.ts
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
return out;
|
|
1210
|
+
}
|
|
1211
|
+
/** 合并本次提交并按上限裁剪最旧(FR-5.12) @experimental */
|
|
1212
|
+
function putFormValues(current, updates, limit) {
|
|
1213
|
+
const merged = {
|
|
1214
|
+
...current,
|
|
1215
|
+
...updates
|
|
1216
|
+
};
|
|
1217
|
+
const keys = Object.keys(merged);
|
|
1218
|
+
if (limit <= 0) return {};
|
|
1219
|
+
if (keys.length <= limit) return merged;
|
|
1220
|
+
const kept = keys.sort((a, b) => merged[a].ts - merged[b].ts).slice(keys.length - limit);
|
|
1221
|
+
return Object.fromEntries(kept.map((key) => [key, merged[key]]));
|
|
1222
|
+
}
|
|
1223
|
+
/** 清除单个字段;不传 fieldKey 即全部清除(FR-5.11) @experimental */
|
|
1224
|
+
function clearFormValues(current, fieldKey) {
|
|
1225
|
+
if (fieldKey === void 0) return {};
|
|
1226
|
+
const { [fieldKey]: _removed, ...rest } = current;
|
|
1227
|
+
return rest;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* 宿主侧的清除入口(FR-5.11):设置面板的「清除填写历史」直接调它,
|
|
1231
|
+
* 不必自己知道 scope 与 key 的约定。
|
|
1232
|
+
* @experimental
|
|
1233
|
+
*/
|
|
1234
|
+
async function clearStoredFormValues(memory, userId, fieldKey) {
|
|
1235
|
+
const scope = `user:${userId}`;
|
|
1236
|
+
if (fieldKey === void 0) {
|
|
1237
|
+
await memory.delete(scope, FORM_VALUES_KEY);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const next = clearFormValues(readFormValues(await memory.get(scope, FORM_VALUES_KEY)), fieldKey);
|
|
1241
|
+
await memory.set(scope, FORM_VALUES_KEY, next);
|
|
1166
1242
|
}
|
|
1167
1243
|
/**
|
|
1168
1244
|
* JsonSchema → 表单模型:按 properties 生成字段,required 标记必填;
|
|
1169
1245
|
* providedArgs 已有的值作为 defaultValue 预填(表单只为补齐缺失项服务)。
|
|
1170
1246
|
* type 映射:string→text、number/integer→number、boolean→boolean、enum→select、其余→textarea。
|
|
1247
|
+
* 传入 skillName 时给每个字段带上跨会话稳定的 `fieldKey`(FR-5.6)。
|
|
1171
1248
|
*/
|
|
1172
|
-
function schemaToForm(schema, providedArgs) {
|
|
1249
|
+
function schemaToForm(schema, providedArgs, options) {
|
|
1173
1250
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
1174
1251
|
const fields = [];
|
|
1175
1252
|
for (const [name, prop] of Object.entries(schema.properties ?? {})) {
|
|
@@ -1180,6 +1257,7 @@ function schemaToForm(schema, providedArgs) {
|
|
|
1180
1257
|
type: mapFieldType(prop),
|
|
1181
1258
|
...required.has(name) ? { required: true } : {},
|
|
1182
1259
|
...typeof prop.description === "string" ? { description: prop.description } : {},
|
|
1260
|
+
...options?.skillName !== void 0 ? { fieldKey: formFieldKey(options.skillName, name) } : {},
|
|
1183
1261
|
...provided !== void 0 ? { defaultValue: provided } : prop.default !== void 0 ? { defaultValue: prop.default } : {}
|
|
1184
1262
|
};
|
|
1185
1263
|
if (Array.isArray(prop.enum)) field.options = prop.enum.map((v) => ({
|
|
@@ -1309,6 +1387,133 @@ var TraceRecorder = class {
|
|
|
1309
1387
|
return [...this.#events];
|
|
1310
1388
|
}
|
|
1311
1389
|
};
|
|
1390
|
+
const isRecord$1 = (v) => typeof v === "object" && v !== null;
|
|
1391
|
+
/** 工具结果可经 `$todo` 标记记入 trace 的事件类型;其余类型不接受,防止工具源伪造 run.* */
|
|
1392
|
+
const TODO_TRACE_TYPES = /* @__PURE__ */ new Set([
|
|
1393
|
+
"todo.created",
|
|
1394
|
+
"todo.updated",
|
|
1395
|
+
"todo.cleared"
|
|
1396
|
+
]);
|
|
1397
|
+
/**
|
|
1398
|
+
* `$todo` 约定的形状校验:JSON content 的 data 含 `$todo` 键(单条或数组)→ trace 事件。
|
|
1399
|
+
*
|
|
1400
|
+
* 与 `$chart` / `$surface` 同一条既有通道——待办清单的状态机全部在 `@webskill/agent`,
|
|
1401
|
+
* runtime 只认这三个事件类型名,不含任何计划态逻辑。畸形条目忽略不炸。
|
|
1402
|
+
* @experimental
|
|
1403
|
+
*/
|
|
1404
|
+
function extractTodoTraceEvents(data) {
|
|
1405
|
+
if (!isRecord$1(data) || data["$todo"] === void 0) return [];
|
|
1406
|
+
const raw = data["$todo"];
|
|
1407
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
1408
|
+
const events = [];
|
|
1409
|
+
for (const entry of entries) {
|
|
1410
|
+
if (!isRecord$1(entry)) continue;
|
|
1411
|
+
const { type, ...rest } = entry;
|
|
1412
|
+
if (typeof type !== "string" || !TODO_TRACE_TYPES.has(type)) continue;
|
|
1413
|
+
events.push({
|
|
1414
|
+
type,
|
|
1415
|
+
data: rest
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
return events;
|
|
1419
|
+
}
|
|
1420
|
+
const RUN_SNAPSHOT_SCHEMA_VERSION = 2;
|
|
1421
|
+
/** @experimental */
|
|
1422
|
+
function isUnsupportedRunSnapshot(entry) {
|
|
1423
|
+
return entry.unsupported === true;
|
|
1424
|
+
}
|
|
1425
|
+
const SNAPSHOT_SUFFIX = ".snapshot.json";
|
|
1426
|
+
/**
|
|
1427
|
+
* FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
|
|
1428
|
+
* 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
|
|
1429
|
+
* save/list 时顺带清理已过 interactionExpiresAt 的过期快照。
|
|
1430
|
+
*
|
|
1431
|
+
* 数据敏感性说明:快照含完整对话历史(用户输入、工具结果、可能的凭据片段),
|
|
1432
|
+
* 以明文 JSON 落盘于宿主提供的 fs;宿主应将其视为会话数据同等保护。
|
|
1433
|
+
* @experimental
|
|
1434
|
+
*/
|
|
1435
|
+
var FsRunSnapshotStore = class {
|
|
1436
|
+
#root;
|
|
1437
|
+
#fs;
|
|
1438
|
+
constructor(deps) {
|
|
1439
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
1440
|
+
this.#fs = deps.fs;
|
|
1441
|
+
}
|
|
1442
|
+
#path(runId) {
|
|
1443
|
+
return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
|
|
1444
|
+
}
|
|
1445
|
+
async save(snapshot) {
|
|
1446
|
+
await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
|
|
1447
|
+
await this.#pruneExpired();
|
|
1448
|
+
}
|
|
1449
|
+
async load(runId) {
|
|
1450
|
+
const path = this.#path(runId);
|
|
1451
|
+
if (!await this.#fs.exists(path)) return void 0;
|
|
1452
|
+
let parsed;
|
|
1453
|
+
try {
|
|
1454
|
+
parsed = JSON.parse(await this.#fs.readText(path));
|
|
1455
|
+
} catch (e) {
|
|
1456
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
1457
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1458
|
+
}
|
|
1459
|
+
const snapshot = parsed;
|
|
1460
|
+
if (typeof snapshot !== "object" || snapshot === null) {
|
|
1461
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
1462
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
1463
|
+
}
|
|
1464
|
+
const schemaVersion = typeof snapshot.schemaVersion === "number" ? snapshot.schemaVersion : 0;
|
|
1465
|
+
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.`);
|
|
1466
|
+
if (snapshot.runId !== runId) {
|
|
1467
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
1468
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
1469
|
+
}
|
|
1470
|
+
return snapshot;
|
|
1471
|
+
}
|
|
1472
|
+
async delete(runId) {
|
|
1473
|
+
const path = this.#path(runId);
|
|
1474
|
+
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
1475
|
+
}
|
|
1476
|
+
async list() {
|
|
1477
|
+
await this.#pruneExpired();
|
|
1478
|
+
if (!await this.#fs.exists(this.#root)) return [];
|
|
1479
|
+
const out = [];
|
|
1480
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
1481
|
+
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
1482
|
+
try {
|
|
1483
|
+
const parsed = JSON.parse(await this.#fs.readText(entry.path));
|
|
1484
|
+
const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 0;
|
|
1485
|
+
if (schemaVersion === 2) {
|
|
1486
|
+
out.push(parsed);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
out.push({
|
|
1490
|
+
unsupported: true,
|
|
1491
|
+
schemaVersion,
|
|
1492
|
+
runId: String(parsed.runId ?? entry.path),
|
|
1493
|
+
snapshotAt: typeof parsed.snapshotAt === "string" ? parsed.snapshotAt : "",
|
|
1494
|
+
...typeof parsed.sessionId === "string" ? { sessionId: parsed.sessionId } : {},
|
|
1495
|
+
...typeof parsed.userPrompt === "string" ? { userPrompt: parsed.userPrompt } : {},
|
|
1496
|
+
...typeof parsed.interactionExpiresAt === "string" ? { interactionExpiresAt: parsed.interactionExpiresAt } : {}
|
|
1497
|
+
});
|
|
1498
|
+
} catch {}
|
|
1499
|
+
}
|
|
1500
|
+
return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
|
|
1501
|
+
}
|
|
1502
|
+
/** 过期快照清理(save/list 时顺带;失败静默不阻断主流程) */
|
|
1503
|
+
async #pruneExpired() {
|
|
1504
|
+
try {
|
|
1505
|
+
if (!await this.#fs.exists(this.#root)) return;
|
|
1506
|
+
const now = Date.now();
|
|
1507
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
1508
|
+
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
1509
|
+
try {
|
|
1510
|
+
const parsed = JSON.parse(await this.#fs.readText(entry.path));
|
|
1511
|
+
if (parsed.interactionExpiresAt !== void 0 && Date.parse(parsed.interactionExpiresAt) < now) await this.#fs.remove(entry.path);
|
|
1512
|
+
} catch {}
|
|
1513
|
+
}
|
|
1514
|
+
} catch {}
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1312
1517
|
/**
|
|
1313
1518
|
* LLM 可见名 → 作者在 SKILL.md 里写的形态。
|
|
1314
1519
|
* `<已激活技能>__<脚本>` 保持原样(裸标识符按技能作用域单独匹配),
|
|
@@ -1338,7 +1543,7 @@ function denialReason(state, canonical) {
|
|
|
1338
1543
|
const skills = [...state.skillAllowedTools.keys()].sort().map((s) => `"${s}"`);
|
|
1339
1544
|
const subject = skills.length === 1 ? `Skill ${skills[0]} declares` : `Skills ${skills.join(", ")} declare`;
|
|
1340
1545
|
const slash = canonical.lastIndexOf("/");
|
|
1341
|
-
return `${subject} allowed-tools, but tool "${canonical}" is not listed
|
|
1546
|
+
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
1547
|
}
|
|
1343
1548
|
/**
|
|
1344
1549
|
* 多技能语义:无人声明 → 不受限;有人声明 → 命中任一清单,
|
|
@@ -1379,7 +1584,6 @@ const summarizeArgs = (args) => {
|
|
|
1379
1584
|
const json = JSON.stringify(args);
|
|
1380
1585
|
return json.length > 100 ? `${json.slice(0, 100)}…` : json;
|
|
1381
1586
|
};
|
|
1382
|
-
const surfaceActions = (surface) => "actions" in surface ? surface.actions ?? [] : [];
|
|
1383
1587
|
/**
|
|
1384
1588
|
* 多轮 Agent 循环。
|
|
1385
1589
|
* 工具失败一律转为 ToolResult{ok:false} 回喂 LLM 继续循环;
|
|
@@ -1404,6 +1608,7 @@ var AgentLoop = class {
|
|
|
1404
1608
|
toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
|
|
1405
1609
|
toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
|
|
1406
1610
|
paramHistoryLimit: config.paramHistoryLimit ?? 50,
|
|
1611
|
+
formValueLimit: config.formValueLimit ?? 100,
|
|
1407
1612
|
temperature: config.temperature,
|
|
1408
1613
|
renderResult: config.renderResult
|
|
1409
1614
|
};
|
|
@@ -1450,6 +1655,7 @@ var AgentLoop = class {
|
|
|
1450
1655
|
activatedTools: /* @__PURE__ */ new Map(),
|
|
1451
1656
|
skillAllowedTools: /* @__PURE__ */ new Map(),
|
|
1452
1657
|
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1658
|
+
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
1453
1659
|
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
1454
1660
|
now,
|
|
1455
1661
|
interactionSeq: 0,
|
|
@@ -1478,7 +1684,13 @@ var AgentLoop = class {
|
|
|
1478
1684
|
strategy: route.strategy,
|
|
1479
1685
|
skillCount: route.catalog.entries.length
|
|
1480
1686
|
} });
|
|
1481
|
-
await this.#lifecycle(
|
|
1687
|
+
await this.#lifecycle({
|
|
1688
|
+
phase: "route",
|
|
1689
|
+
data: {
|
|
1690
|
+
strategy: route.strategy,
|
|
1691
|
+
candidates: route.catalog.entries.map((e) => e.name)
|
|
1692
|
+
}
|
|
1693
|
+
}, state);
|
|
1482
1694
|
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
1483
1695
|
try {
|
|
1484
1696
|
return await source.listToolSpecs();
|
|
@@ -1487,15 +1699,25 @@ var AgentLoop = class {
|
|
|
1487
1699
|
return [];
|
|
1488
1700
|
}
|
|
1489
1701
|
}))).flat();
|
|
1702
|
+
const externalSystemPrompts = [];
|
|
1703
|
+
for (const source of this.#deps.externalTools ?? []) {
|
|
1704
|
+
if (source.systemPrompt === void 0) continue;
|
|
1705
|
+
try {
|
|
1706
|
+
const text = (await source.systemPrompt())?.trim();
|
|
1707
|
+
if (text !== void 0 && text !== "") externalSystemPrompts.push(text);
|
|
1708
|
+
} catch (e) {
|
|
1709
|
+
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to build a system prompt: ${messageOf(e)}` });
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1490
1712
|
state.messages = [
|
|
1491
1713
|
{
|
|
1492
1714
|
role: "system",
|
|
1493
|
-
content: route.systemPrompt
|
|
1715
|
+
content: textParts([route.systemPrompt, ...externalSystemPrompts].join("\n\n"))
|
|
1494
1716
|
},
|
|
1495
1717
|
...(input.history ?? []).map((m) => ({ ...m })),
|
|
1496
1718
|
{
|
|
1497
1719
|
role: "user",
|
|
1498
|
-
content: input.userPrompt
|
|
1720
|
+
content: textParts(input.userPrompt)
|
|
1499
1721
|
}
|
|
1500
1722
|
];
|
|
1501
1723
|
try {
|
|
@@ -1527,17 +1749,25 @@ var AgentLoop = class {
|
|
|
1527
1749
|
}
|
|
1528
1750
|
/**
|
|
1529
1751
|
* D10 判定的唯一出口(暴露点 + 分发点共用)。
|
|
1530
|
-
* 0.
|
|
1752
|
+
* 0.4.0(D1)起两个调用点都看返回值:暴露点过滤、分发点回喂 TOOL_NOT_ALLOWED。
|
|
1753
|
+
* 判定语义与 0.3.0 一致,改的只是调用点与 trace 事件类型(warning → denied)。
|
|
1531
1754
|
*/
|
|
1532
1755
|
#checkToolAccess(state, toolName) {
|
|
1533
1756
|
const verdict = evaluateToolAccess(state, toolName);
|
|
1534
1757
|
if (verdict.allowed) return true;
|
|
1535
1758
|
if (!state.warnedDeniedTools.has(toolName)) {
|
|
1536
1759
|
state.warnedDeniedTools.add(toolName);
|
|
1537
|
-
state.trace.record("
|
|
1760
|
+
state.trace.record("tool.denied", {
|
|
1761
|
+
message: verdict.reason ?? `Tool "${toolName}" is not allowed`,
|
|
1762
|
+
data: { name: toolName }
|
|
1763
|
+
});
|
|
1538
1764
|
}
|
|
1539
1765
|
return false;
|
|
1540
1766
|
}
|
|
1767
|
+
/** 分发点被拒时回喂给模型的结构化错误(不抛异常:模型造名字是常态,抛异常会终止整个 run) */
|
|
1768
|
+
#deniedToolError(state, toolName) {
|
|
1769
|
+
return toolError("TOOL_NOT_ALLOWED", evaluateToolAccess(state, toolName).reason ?? `Tool "${toolName}" is not allowed`);
|
|
1770
|
+
}
|
|
1541
1771
|
/** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
|
|
1542
1772
|
async #turnLoop(state, startTurn, externalSpecs) {
|
|
1543
1773
|
const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
|
|
@@ -1549,8 +1779,7 @@ var AgentLoop = class {
|
|
|
1549
1779
|
state.turn = turn;
|
|
1550
1780
|
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1551
1781
|
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);
|
|
1782
|
+
const skillToolSpecs = [...[...state.activatedTools.values()].map(toLlmToolSpec), ...externalSpecs].filter((spec) => this.#checkToolAccess(state, spec.name));
|
|
1554
1783
|
const toolSpecs = [
|
|
1555
1784
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1556
1785
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
@@ -1584,22 +1813,29 @@ var AgentLoop = class {
|
|
|
1584
1813
|
const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
|
|
1585
1814
|
return finish("failed", "llm-error", messageOf(e), code);
|
|
1586
1815
|
}
|
|
1816
|
+
const responseText = partsToText(response.content);
|
|
1587
1817
|
trace.record("llm.response", { data: {
|
|
1588
1818
|
turn,
|
|
1589
1819
|
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
1590
|
-
contentLength:
|
|
1820
|
+
contentLength: responseText.length
|
|
1591
1821
|
} });
|
|
1592
1822
|
if (!response.toolCalls?.length) {
|
|
1593
1823
|
messages.push({
|
|
1594
1824
|
role: "assistant",
|
|
1595
|
-
content: response.content ??
|
|
1825
|
+
content: response.content ?? []
|
|
1596
1826
|
});
|
|
1597
|
-
return finish("completed", "final-answer",
|
|
1827
|
+
return finish("completed", "final-answer", responseText);
|
|
1598
1828
|
}
|
|
1599
|
-
await this.#lifecycle(
|
|
1829
|
+
await this.#lifecycle({
|
|
1830
|
+
phase: "execute",
|
|
1831
|
+
data: {
|
|
1832
|
+
kind: "turn",
|
|
1833
|
+
turn
|
|
1834
|
+
}
|
|
1835
|
+
}, state);
|
|
1600
1836
|
messages.push({
|
|
1601
1837
|
role: "assistant",
|
|
1602
|
-
content: response.content ??
|
|
1838
|
+
content: response.content ?? [],
|
|
1603
1839
|
toolCalls: response.toolCalls
|
|
1604
1840
|
});
|
|
1605
1841
|
for (const call of response.toolCalls) {
|
|
@@ -1613,7 +1849,7 @@ var AgentLoop = class {
|
|
|
1613
1849
|
messages.push({
|
|
1614
1850
|
role: "tool",
|
|
1615
1851
|
toolCallId: call.id,
|
|
1616
|
-
content: await this.#serializeToolResult(call, result, state)
|
|
1852
|
+
content: textParts(await this.#serializeToolResult(call, result, state))
|
|
1617
1853
|
});
|
|
1618
1854
|
await this.#drainSurfaceAction(state);
|
|
1619
1855
|
}
|
|
@@ -1659,7 +1895,10 @@ var AgentLoop = class {
|
|
|
1659
1895
|
trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
|
|
1660
1896
|
}
|
|
1661
1897
|
try {
|
|
1662
|
-
await this.#lifecycle(
|
|
1898
|
+
await this.#lifecycle({
|
|
1899
|
+
phase: status === "completed" ? "complete" : "fail",
|
|
1900
|
+
data: { reason }
|
|
1901
|
+
}, state);
|
|
1663
1902
|
} catch (e) {
|
|
1664
1903
|
trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
|
|
1665
1904
|
}
|
|
@@ -1675,7 +1914,7 @@ var AgentLoop = class {
|
|
|
1675
1914
|
const store = this.#deps.snapshotStore;
|
|
1676
1915
|
if (!store) return;
|
|
1677
1916
|
const snapshot = {
|
|
1678
|
-
schemaVersion:
|
|
1917
|
+
schemaVersion: 2,
|
|
1679
1918
|
runId: state.runId,
|
|
1680
1919
|
sessionId: state.run.sessionId,
|
|
1681
1920
|
userPrompt: state.run.userPrompt,
|
|
@@ -1736,6 +1975,7 @@ var AgentLoop = class {
|
|
|
1736
1975
|
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
1737
1976
|
skillAllowedTools: new Map(Object.entries(snapshot.skillAllowedTools ?? {})),
|
|
1738
1977
|
warnedDeniedTools: /* @__PURE__ */ new Set(),
|
|
1978
|
+
integrityVerdicts: /* @__PURE__ */ new Map(),
|
|
1739
1979
|
toolTimeoutMs: snapshot.config.toolTimeoutMs,
|
|
1740
1980
|
now,
|
|
1741
1981
|
interactionSeq: snapshot.interactionSeq ?? 0,
|
|
@@ -1784,7 +2024,7 @@ var AgentLoop = class {
|
|
|
1784
2024
|
state.messages.push({
|
|
1785
2025
|
role: "tool",
|
|
1786
2026
|
toolCallId: pendingCall.id,
|
|
1787
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
2027
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1788
2028
|
});
|
|
1789
2029
|
await this.#drainSurfaceAction(state);
|
|
1790
2030
|
} else if (pending?.type === "ask" && pendingCall) {
|
|
@@ -1806,7 +2046,7 @@ var AgentLoop = class {
|
|
|
1806
2046
|
state.messages.push({
|
|
1807
2047
|
role: "tool",
|
|
1808
2048
|
toolCallId: pendingCall.id,
|
|
1809
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
2049
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1810
2050
|
});
|
|
1811
2051
|
await this.#drainSurfaceAction(state);
|
|
1812
2052
|
} else if (pendingCall) {
|
|
@@ -1814,7 +2054,7 @@ var AgentLoop = class {
|
|
|
1814
2054
|
state.messages.push({
|
|
1815
2055
|
role: "tool",
|
|
1816
2056
|
toolCallId: pendingCall.id,
|
|
1817
|
-
content: await this.#serializeToolResult(pendingCall, result, state)
|
|
2057
|
+
content: textParts(await this.#serializeToolResult(pendingCall, result, state))
|
|
1818
2058
|
});
|
|
1819
2059
|
await this.#drainSurfaceAction(state);
|
|
1820
2060
|
}
|
|
@@ -1825,7 +2065,7 @@ var AgentLoop = class {
|
|
|
1825
2065
|
state.messages.push({
|
|
1826
2066
|
role: "tool",
|
|
1827
2067
|
toolCallId: next.id,
|
|
1828
|
-
content: await this.#serializeToolResult(next, result, state)
|
|
2068
|
+
content: textParts(await this.#serializeToolResult(next, result, state))
|
|
1829
2069
|
});
|
|
1830
2070
|
await this.#drainSurfaceAction(state);
|
|
1831
2071
|
}
|
|
@@ -1868,7 +2108,7 @@ var AgentLoop = class {
|
|
|
1868
2108
|
sessionId: state.run.sessionId,
|
|
1869
2109
|
ts: state.now(),
|
|
1870
2110
|
data: {
|
|
1871
|
-
|
|
2111
|
+
kind: "llm-delta",
|
|
1872
2112
|
delta
|
|
1873
2113
|
}
|
|
1874
2114
|
});
|
|
@@ -1882,23 +2122,23 @@ var AgentLoop = class {
|
|
|
1882
2122
|
emitDelta(event.delta);
|
|
1883
2123
|
} else if (event.type === "tool-calls") toolCalls.push(...event.toolCalls);
|
|
1884
2124
|
else if (event.type === "done") doneContent = event.content;
|
|
2125
|
+
const text = doneContent ?? content;
|
|
1885
2126
|
return {
|
|
1886
|
-
content:
|
|
2127
|
+
content: text === "" ? void 0 : textParts(text),
|
|
1887
2128
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0
|
|
1888
2129
|
};
|
|
1889
2130
|
}
|
|
1890
2131
|
/** 生命周期接线:更新 phase、发事件、跑钩子 */
|
|
1891
|
-
async #lifecycle(
|
|
1892
|
-
state.run.phase = phase;
|
|
2132
|
+
async #lifecycle(init, state) {
|
|
2133
|
+
state.run.phase = init.phase;
|
|
1893
2134
|
const event = {
|
|
1894
|
-
|
|
2135
|
+
...init,
|
|
1895
2136
|
runId: state.runId,
|
|
1896
2137
|
sessionId: state.run.sessionId,
|
|
1897
|
-
ts: state.now()
|
|
1898
|
-
...data ? { data } : {}
|
|
2138
|
+
ts: state.now()
|
|
1899
2139
|
};
|
|
1900
2140
|
this.#deps.eventBus?.emit(event);
|
|
1901
|
-
if (this.#deps.hooks) await this.#deps.hooks.run(phase, {
|
|
2141
|
+
if (this.#deps.hooks) await this.#deps.hooks.run(init.phase, {
|
|
1902
2142
|
event,
|
|
1903
2143
|
run: state.run
|
|
1904
2144
|
});
|
|
@@ -1925,10 +2165,14 @@ var AgentLoop = class {
|
|
|
1925
2165
|
run.status = "interrupted";
|
|
1926
2166
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
1927
2167
|
await this.#saveSnapshot(state, { interaction: request });
|
|
1928
|
-
await this.#lifecycle(
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2168
|
+
await this.#lifecycle({
|
|
2169
|
+
phase: "interact",
|
|
2170
|
+
data: {
|
|
2171
|
+
kind: "interaction",
|
|
2172
|
+
interactionId: request.id,
|
|
2173
|
+
interactionType: request.type
|
|
2174
|
+
}
|
|
2175
|
+
}, state);
|
|
1932
2176
|
state.trace.record("ui.requested", { data: {
|
|
1933
2177
|
interactionId: request.id,
|
|
1934
2178
|
type: request.type,
|
|
@@ -1961,7 +2205,13 @@ var AgentLoop = class {
|
|
|
1961
2205
|
interactionId: request.id,
|
|
1962
2206
|
type: request.type
|
|
1963
2207
|
} });
|
|
1964
|
-
await this.#lifecycle(
|
|
2208
|
+
await this.#lifecycle({
|
|
2209
|
+
phase: "execute",
|
|
2210
|
+
data: {
|
|
2211
|
+
kind: "interaction-resumed",
|
|
2212
|
+
interactionId: request.id
|
|
2213
|
+
}
|
|
2214
|
+
}, state);
|
|
1965
2215
|
await this.#appendParamHistory(state, request, response.value);
|
|
1966
2216
|
return response.value;
|
|
1967
2217
|
}
|
|
@@ -1995,8 +2245,8 @@ var AgentLoop = class {
|
|
|
1995
2245
|
if (call.argumentsParseError) result = toolError("VALIDATION_FAILED", `Tool arguments were not valid JSON: ${call.argumentsParseError}`);
|
|
1996
2246
|
else if (call.name === "read_skill_file") result = await this.#handleReadSkillFile(call, state);
|
|
1997
2247
|
else if (call.name === "ask_user") result = await this.#handleAskUser(call, state);
|
|
2248
|
+
else if (!this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
1998
2249
|
else {
|
|
1999
|
-
this.#checkToolAccess(state, call.name);
|
|
2000
2250
|
const resolution = resolveToolName(call.name, state.activated);
|
|
2001
2251
|
if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
2002
2252
|
else {
|
|
@@ -2020,8 +2270,9 @@ var AgentLoop = class {
|
|
|
2020
2270
|
type: "chart",
|
|
2021
2271
|
chart
|
|
2022
2272
|
});
|
|
2273
|
+
for (const todo of extractTodoTraceEvents(item.data)) state.trace.record(todo.type, { data: todo.data });
|
|
2023
2274
|
try {
|
|
2024
|
-
for (const event of
|
|
2275
|
+
for (const event of extractUiSpecEvents(item.data)) await this.#renderSurface(state, event);
|
|
2025
2276
|
} catch (e) {
|
|
2026
2277
|
state.trace.record("run.warning", {
|
|
2027
2278
|
message: `UI surface rejected: ${messageOf(e)}`,
|
|
@@ -2040,7 +2291,7 @@ var AgentLoop = class {
|
|
|
2040
2291
|
durationMs
|
|
2041
2292
|
}
|
|
2042
2293
|
});
|
|
2043
|
-
this.#emitTool(state, "failed", call);
|
|
2294
|
+
this.#emitTool(state, "failed", call, result.error?.code);
|
|
2044
2295
|
}
|
|
2045
2296
|
for (const artifact of result.artifacts ?? []) state.trace.record("artifact.created", { data: {
|
|
2046
2297
|
artifactId: artifact.id,
|
|
@@ -2060,14 +2311,14 @@ var AgentLoop = class {
|
|
|
2060
2311
|
await bridge.renderSurface(attributed);
|
|
2061
2312
|
state.surfaceEvents.push(structuredClone(attributed));
|
|
2062
2313
|
if (attributed.type === "open") {
|
|
2063
|
-
const waiting =
|
|
2314
|
+
const waiting = (attributed.actions ?? []).filter((action) => action.awaitResponse);
|
|
2064
2315
|
if (waiting.length > 1) throw new WebSkillError("VALIDATION_FAILED", "A UI surface can wait for only one action");
|
|
2065
2316
|
const action = waiting[0];
|
|
2066
2317
|
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
2318
|
else if (state.pendingSurfaceAction) throw new WebSkillError("VALIDATION_FAILED", "Only one UI surface action can be pending at a time");
|
|
2068
2319
|
else state.pendingSurfaceAction = {
|
|
2069
2320
|
runId: state.runId,
|
|
2070
|
-
surfaceId: attributed.
|
|
2321
|
+
surfaceId: attributed.id,
|
|
2071
2322
|
actionId: action.id,
|
|
2072
2323
|
intent: action.intent,
|
|
2073
2324
|
nonce: action.nonce
|
|
@@ -2085,21 +2336,17 @@ var AgentLoop = class {
|
|
|
2085
2336
|
}
|
|
2086
2337
|
/** Assigns unforgeable action nonces after model output has passed structural validation. */
|
|
2087
2338
|
#attributeSurfaceEvent(state, event) {
|
|
2088
|
-
|
|
2089
|
-
if (event.type !== "open" || actions.length === 0) return {
|
|
2339
|
+
if (event.type !== "open" || (event.actions ?? []).length === 0) return {
|
|
2090
2340
|
...event,
|
|
2091
2341
|
runId: state.runId
|
|
2092
2342
|
};
|
|
2093
2343
|
return {
|
|
2094
|
-
|
|
2344
|
+
...event,
|
|
2095
2345
|
runId: state.runId,
|
|
2096
|
-
|
|
2097
|
-
...
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
|
|
2101
|
-
}))
|
|
2102
|
-
}
|
|
2346
|
+
actions: (event.actions ?? []).map((action) => ({
|
|
2347
|
+
...action,
|
|
2348
|
+
nonce: `surface-${state.runId}-${++state.surfaceActionSeq}`
|
|
2349
|
+
}))
|
|
2103
2350
|
};
|
|
2104
2351
|
}
|
|
2105
2352
|
/** Awaits the single action emitted with the most recently persisted tool result. */
|
|
@@ -2126,13 +2373,13 @@ var AgentLoop = class {
|
|
|
2126
2373
|
state.processedSurfaceActionNonces.add(response.nonce);
|
|
2127
2374
|
state.messages.push({
|
|
2128
2375
|
role: "user",
|
|
2129
|
-
content: JSON.stringify({
|
|
2376
|
+
content: textParts(JSON.stringify({
|
|
2130
2377
|
type: "webskill_surface_action",
|
|
2131
2378
|
surfaceId: response.surfaceId,
|
|
2132
2379
|
actionId: response.actionId,
|
|
2133
2380
|
intent: response.intent,
|
|
2134
2381
|
value: response.value ?? null
|
|
2135
|
-
})
|
|
2382
|
+
}))
|
|
2136
2383
|
});
|
|
2137
2384
|
}
|
|
2138
2385
|
async #interactSurfaceAction(state, request, resumed) {
|
|
@@ -2144,20 +2391,26 @@ var AgentLoop = class {
|
|
|
2144
2391
|
const { run } = state;
|
|
2145
2392
|
run.status = "interrupted";
|
|
2146
2393
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
2394
|
+
const pendingResponse = bridge.requestSurfaceAction(request);
|
|
2395
|
+
pendingResponse.catch(() => void 0);
|
|
2147
2396
|
await this.#saveSnapshot(state, { surfaceAction: request });
|
|
2148
|
-
await this.#lifecycle(
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2397
|
+
await this.#lifecycle({
|
|
2398
|
+
phase: "interact",
|
|
2399
|
+
data: {
|
|
2400
|
+
kind: "surface-action",
|
|
2401
|
+
surfaceId: request.surfaceId,
|
|
2402
|
+
actionId: request.actionId,
|
|
2403
|
+
nonce: request.nonce,
|
|
2404
|
+
resumed
|
|
2405
|
+
}
|
|
2406
|
+
}, state);
|
|
2154
2407
|
state.trace.record("ui.surface-action.requested", { data: {
|
|
2155
2408
|
surfaceId: request.surfaceId,
|
|
2156
2409
|
actionId: request.actionId,
|
|
2157
2410
|
nonce: request.nonce,
|
|
2158
2411
|
...resumed ? { resumed: true } : {}
|
|
2159
2412
|
} });
|
|
2160
|
-
const response = await this.#withInteractionTimeout(
|
|
2413
|
+
const response = await this.#withInteractionTimeout(pendingResponse, this.#policy.interactionTimeoutMs, () => bridge.cancelSurfaceAction?.(request.nonce));
|
|
2161
2414
|
run.status = "running";
|
|
2162
2415
|
run.interruptExpiresAt = void 0;
|
|
2163
2416
|
state.trace.record("ui.surface-action.resolved", { data: {
|
|
@@ -2165,9 +2418,17 @@ var AgentLoop = class {
|
|
|
2165
2418
|
actionId: request.actionId,
|
|
2166
2419
|
nonce: request.nonce
|
|
2167
2420
|
} });
|
|
2168
|
-
await this.#lifecycle(
|
|
2421
|
+
await this.#lifecycle({
|
|
2422
|
+
phase: "execute",
|
|
2423
|
+
data: {
|
|
2424
|
+
kind: "surface-action-resumed",
|
|
2425
|
+
surfaceId: request.surfaceId,
|
|
2426
|
+
actionId: request.actionId
|
|
2427
|
+
}
|
|
2428
|
+
}, state);
|
|
2169
2429
|
return response;
|
|
2170
2430
|
} catch (e) {
|
|
2431
|
+
bridge.cancelSurfaceAction?.(request.nonce);
|
|
2171
2432
|
state.run.status = "running";
|
|
2172
2433
|
state.run.interruptExpiresAt = void 0;
|
|
2173
2434
|
if (e instanceof WebSkillError && e.code === "RUN_INTERACTION_TIMEOUT") throw new RunTerminated({
|
|
@@ -2205,7 +2466,7 @@ var AgentLoop = class {
|
|
|
2205
2466
|
* 集合挂在 LoopState 上、**不写进快照**:写进快照会让跨进程恢复的
|
|
2206
2467
|
* 消费者永远收不到它本来就没见过的事件。
|
|
2207
2468
|
*/
|
|
2208
|
-
#emitTool(state, status, call) {
|
|
2469
|
+
#emitTool(state, status, call, errorCode) {
|
|
2209
2470
|
const key = `${call.id}:${status}`;
|
|
2210
2471
|
if (state.emittedToolEvents.has(key)) return;
|
|
2211
2472
|
state.emittedToolEvents.add(key);
|
|
@@ -2215,11 +2476,12 @@ var AgentLoop = class {
|
|
|
2215
2476
|
sessionId: state.run.sessionId,
|
|
2216
2477
|
ts: state.now(),
|
|
2217
2478
|
data: {
|
|
2218
|
-
|
|
2479
|
+
kind: "tool",
|
|
2219
2480
|
status,
|
|
2220
2481
|
name: call.name,
|
|
2221
2482
|
callId: call.id,
|
|
2222
|
-
args: summarizeArgs(call.arguments)
|
|
2483
|
+
args: summarizeArgs(call.arguments),
|
|
2484
|
+
...errorCode !== void 0 ? { errorCode } : {}
|
|
2223
2485
|
}
|
|
2224
2486
|
});
|
|
2225
2487
|
}
|
|
@@ -2285,7 +2547,13 @@ var AgentLoop = class {
|
|
|
2285
2547
|
skillName: name,
|
|
2286
2548
|
source: "external"
|
|
2287
2549
|
} });
|
|
2288
|
-
await this.#lifecycle(
|
|
2550
|
+
await this.#lifecycle({
|
|
2551
|
+
phase: "activate",
|
|
2552
|
+
data: {
|
|
2553
|
+
skillName: name,
|
|
2554
|
+
source: "external"
|
|
2555
|
+
}
|
|
2556
|
+
}, state);
|
|
2289
2557
|
await this.#writeActivationMemory(name, state);
|
|
2290
2558
|
}
|
|
2291
2559
|
return {
|
|
@@ -2339,18 +2607,62 @@ var AgentLoop = class {
|
|
|
2339
2607
|
if (!check) return false;
|
|
2340
2608
|
return await check(skillName) === false;
|
|
2341
2609
|
}
|
|
2610
|
+
/**
|
|
2611
|
+
* D3 激活期完整性校验。结论按技能名在 run 内缓存,**失败也缓存**:
|
|
2612
|
+
* 校验失败的技能不会进 `activated` 集合,不缓存的话同一个坏技能每尝试激活一次
|
|
2613
|
+
* 就要把它的文件全扫一遍——成本随文件数线性放大,正是需求 §3 验收 3 要防的。
|
|
2614
|
+
*
|
|
2615
|
+
* 设计 §3.5 写的缓存键是 `(skillName, manifest.integrity.digest)`,但 digest 只有
|
|
2616
|
+
* **调用之后**才知道;runtime 读不到 manifest,两元组键在这一层无法实现。
|
|
2617
|
+
* 实际键是技能名,digest 作为结论的一部分留在 trace 里供事后对账。
|
|
2618
|
+
*
|
|
2619
|
+
* 失败不抛错:抛错会终止整个 run,而「某个技能被改过」不该让其余技能一起停摆。
|
|
2620
|
+
*/
|
|
2621
|
+
async #integrityOk(skillName, state) {
|
|
2622
|
+
const guard = this.#deps.skillIntegrityGuard;
|
|
2623
|
+
if (!guard?.verifyOnActivate) return true;
|
|
2624
|
+
let verdict = state.integrityVerdicts.get(skillName);
|
|
2625
|
+
if (verdict === void 0) {
|
|
2626
|
+
try {
|
|
2627
|
+
verdict = await guard.verifyOnActivate(skillName);
|
|
2628
|
+
} catch (e) {
|
|
2629
|
+
verdict = {
|
|
2630
|
+
ok: false,
|
|
2631
|
+
reason: `the integrity guard threw: ${messageOf(e)}`
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
state.integrityVerdicts.set(skillName, verdict);
|
|
2635
|
+
}
|
|
2636
|
+
if (verdict.ok) return true;
|
|
2637
|
+
state.trace.record("skill.integrity-failed", {
|
|
2638
|
+
message: `Skill "${skillName}" failed integrity verification on activation: ${verdict.reason ?? "no reason given"}`,
|
|
2639
|
+
data: {
|
|
2640
|
+
skillName,
|
|
2641
|
+
...verdict.digest !== void 0 ? { digest: verdict.digest } : {}
|
|
2642
|
+
}
|
|
2643
|
+
});
|
|
2644
|
+
return false;
|
|
2645
|
+
}
|
|
2342
2646
|
/** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
|
|
2343
2647
|
async #activateSkill(skillName, state, via, skillMdText) {
|
|
2344
2648
|
if (await this.#guardDenied("canActivate", skillName)) {
|
|
2345
2649
|
state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
|
|
2346
2650
|
return "";
|
|
2347
2651
|
}
|
|
2652
|
+
if (!await this.#integrityOk(skillName, state)) return "";
|
|
2348
2653
|
state.activated.add(skillName);
|
|
2349
2654
|
state.trace.record("skill.activated", { data: {
|
|
2350
2655
|
skillName,
|
|
2351
2656
|
...via ? { via } : {}
|
|
2352
2657
|
} });
|
|
2353
|
-
await this.#lifecycle(
|
|
2658
|
+
await this.#lifecycle({
|
|
2659
|
+
phase: "activate",
|
|
2660
|
+
data: {
|
|
2661
|
+
skillName,
|
|
2662
|
+
source: "local",
|
|
2663
|
+
...via ? { via } : {}
|
|
2664
|
+
}
|
|
2665
|
+
}, state);
|
|
2354
2666
|
await this.#writeActivationMemory(skillName, state);
|
|
2355
2667
|
const root = this.#deps.skillIndex.get(skillName);
|
|
2356
2668
|
if (!root) return "";
|
|
@@ -2441,11 +2753,13 @@ var AgentLoop = class {
|
|
|
2441
2753
|
let args = call.arguments;
|
|
2442
2754
|
const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
|
|
2443
2755
|
if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
|
|
2756
|
+
const fields = schemaToForm(def.inputSchema, args, { skillName });
|
|
2757
|
+
await this.#attachFormSuggestions(fields, state);
|
|
2444
2758
|
const value = await this.#interact(state, {
|
|
2445
2759
|
type: "form",
|
|
2446
2760
|
id: this.#nextInteractionId(state),
|
|
2447
2761
|
title: `Missing parameters for ${call.name}`,
|
|
2448
|
-
fields
|
|
2762
|
+
fields
|
|
2449
2763
|
}, {
|
|
2450
2764
|
tool: call.name,
|
|
2451
2765
|
missing
|
|
@@ -2484,11 +2798,41 @@ var AgentLoop = class {
|
|
|
2484
2798
|
if (fresh.length > 0) result.artifacts = [...result.artifacts ?? [], ...fresh];
|
|
2485
2799
|
}
|
|
2486
2800
|
await this.#bumpSkillStat(skillName, result.ok ? "successes" : "failures", state);
|
|
2801
|
+
if (!result.ok) await this.#reportSkillFailure(skillName, state, {
|
|
2802
|
+
code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
|
|
2803
|
+
message: result.error?.message ?? `Tool "${call.name}" returned a failure result`
|
|
2804
|
+
});
|
|
2487
2805
|
return result;
|
|
2488
2806
|
} catch (e) {
|
|
2489
2807
|
if (e instanceof RunTerminated) throw e;
|
|
2490
2808
|
await this.#bumpSkillStat(skillName, "failures", state);
|
|
2491
|
-
|
|
2809
|
+
const code = e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED";
|
|
2810
|
+
const message = `Tool "${call.name}" failed: ${messageOf(e)}`;
|
|
2811
|
+
await this.#reportSkillFailure(skillName, state, {
|
|
2812
|
+
code,
|
|
2813
|
+
message
|
|
2814
|
+
});
|
|
2815
|
+
return toolError(code, message);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* F1 技能失败上报:喂给治理的失败计数器(`SkillStatePolicy.recordFailure`),
|
|
2820
|
+
* 达阈值即自动隔离。无注入即整段跳过(默认关闭)。
|
|
2821
|
+
*
|
|
2822
|
+
* 上报方抛错降级为 `run.warning`:治理写盘失败不该把一次「工具出错但已回喂给 LLM」
|
|
2823
|
+
* 的 run 变成崩溃——那会让引入治理反而降低可用性。
|
|
2824
|
+
*/
|
|
2825
|
+
async #reportSkillFailure(skillName, state, detail) {
|
|
2826
|
+
const report = this.#deps.skillOutcomeReporter?.onSkillFailed;
|
|
2827
|
+
if (report === void 0) return;
|
|
2828
|
+
try {
|
|
2829
|
+
await report.call(this.#deps.skillOutcomeReporter, {
|
|
2830
|
+
skillName,
|
|
2831
|
+
runId: state.runId,
|
|
2832
|
+
...detail
|
|
2833
|
+
});
|
|
2834
|
+
} catch (e) {
|
|
2835
|
+
state.trace.record("run.warning", { message: `Skill failure report for "${skillName}" was not recorded: ${messageOf(e)}` });
|
|
2492
2836
|
}
|
|
2493
2837
|
}
|
|
2494
2838
|
/** context.confirm 触发点:默认真实询问;auto-approve 直通;无 bridge 降级直通 + warning */
|
|
@@ -2587,6 +2931,47 @@ var AgentLoop = class {
|
|
|
2587
2931
|
});
|
|
2588
2932
|
return history.slice(-limit);
|
|
2589
2933
|
});
|
|
2934
|
+
await this.#rememberFormValues(state, request, value);
|
|
2935
|
+
}
|
|
2936
|
+
/**
|
|
2937
|
+
* 跨会话字段值的写入(FR-5.6/5.12)。与 `paramHistory` 双写而不是合并:
|
|
2938
|
+
* 那边是按时间的追加序列(运行观测),这边是按字段的最新值(召回),形态不同。
|
|
2939
|
+
*/
|
|
2940
|
+
async #rememberFormValues(state, request, value) {
|
|
2941
|
+
const autofill = this.#deps.formAutofill;
|
|
2942
|
+
if (!autofill || request.type !== "form") return;
|
|
2943
|
+
if (typeof value !== "object" || value === null) return;
|
|
2944
|
+
const submitted = value;
|
|
2945
|
+
const ts = Date.parse(state.now());
|
|
2946
|
+
const updates = {};
|
|
2947
|
+
for (const field of request.fields) {
|
|
2948
|
+
if (field.fieldKey === void 0) continue;
|
|
2949
|
+
const next = submitted[field.name];
|
|
2950
|
+
if (next === void 0 || next === "") continue;
|
|
2951
|
+
updates[field.fieldKey] = {
|
|
2952
|
+
value: next,
|
|
2953
|
+
ts
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
if (Object.keys(updates).length === 0) return;
|
|
2957
|
+
await this.#memoryMutate(`user:${autofill.userId}`, FORM_VALUES_KEY, state, (current) => putFormValues(readFormValues(current), updates, this.#config.formValueLimit));
|
|
2958
|
+
}
|
|
2959
|
+
/**
|
|
2960
|
+
* 召回(FR-5.8):命中的历史值挂在 `suggestion` 上,**绝不写进 `defaultValue`**——
|
|
2961
|
+
* 后者会被渲染器直接填进控件,等于静默预填(AC-5.6 禁止)。
|
|
2962
|
+
*/
|
|
2963
|
+
async #attachFormSuggestions(fields, state) {
|
|
2964
|
+
const autofill = this.#deps.formAutofill;
|
|
2965
|
+
if (!autofill) return;
|
|
2966
|
+
const stored = readFormValues(await this.#memoryGet(`user:${autofill.userId}`, FORM_VALUES_KEY, state));
|
|
2967
|
+
for (const field of fields) {
|
|
2968
|
+
if (field.fieldKey === void 0 || field.defaultValue !== void 0) continue;
|
|
2969
|
+
const hit = stored[field.fieldKey];
|
|
2970
|
+
if (hit !== void 0) field.suggestion = {
|
|
2971
|
+
value: hit.value,
|
|
2972
|
+
ts: hit.ts
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2590
2975
|
}
|
|
2591
2976
|
};
|
|
2592
2977
|
/**
|
|
@@ -2598,79 +2983,6 @@ function mergeCatalogEntries(localEntries, providerEntries) {
|
|
|
2598
2983
|
for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
2599
2984
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2600
2985
|
}
|
|
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
2986
|
/**
|
|
2675
2987
|
* session history 滚动裁剪:超出预算时裁掉中段,保留首尾。
|
|
2676
2988
|
* 边界对齐 LLM tool 契约:head 不以未应答的 assistant toolCalls 结尾,
|
|
@@ -2800,11 +3112,14 @@ var WebSkillRuntime = class {
|
|
|
2800
3112
|
hooks: this.#deps.hooks,
|
|
2801
3113
|
eventBus: this.#events,
|
|
2802
3114
|
longTerm: this.#deps.longTerm,
|
|
3115
|
+
formAutofill: this.#deps.formAutofill,
|
|
2803
3116
|
externalTools: this.#deps.externalTools,
|
|
2804
3117
|
skillProviders: this.#deps.skillProviders,
|
|
2805
3118
|
catalogFilter: this.#deps.catalogFilter,
|
|
2806
3119
|
snapshotStore: this.#deps.snapshotStore,
|
|
2807
|
-
skillStateGuard: this.#deps.skillStateGuard
|
|
3120
|
+
skillStateGuard: this.#deps.skillStateGuard,
|
|
3121
|
+
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
3122
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
2808
3123
|
}, this.#deps.config);
|
|
2809
3124
|
const runId = `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
2810
3125
|
this.#loops.set(runId, loop);
|
|
@@ -2841,14 +3156,14 @@ var WebSkillRuntime = class {
|
|
|
2841
3156
|
});
|
|
2842
3157
|
return result;
|
|
2843
3158
|
}
|
|
2844
|
-
/** D3
|
|
3159
|
+
/** D3:列出 interrupted run(供 UI 展示"未完成任务");版本不受支持的项带 unsupported 标记 */
|
|
2845
3160
|
async listInterruptedRuns() {
|
|
2846
3161
|
if (!this.#deps.snapshotStore) return [];
|
|
2847
3162
|
return this.#deps.snapshotStore.list();
|
|
2848
3163
|
}
|
|
2849
3164
|
/**
|
|
2850
3165
|
* D3 恢复 interrupted run:
|
|
2851
|
-
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;
|
|
3166
|
+
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;schema 版本不受支持 → RUN_SNAPSHOT_SCHEMA_UNSUPPORTED(由 store 抛出,不删文件);
|
|
2852
3167
|
* 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
|
|
2853
3168
|
* @experimental
|
|
2854
3169
|
*/
|
|
@@ -2856,10 +3171,6 @@ var WebSkillRuntime = class {
|
|
|
2856
3171
|
const store = this.#deps.snapshotStore;
|
|
2857
3172
|
const snapshot = store ? await store.load(runId) : void 0;
|
|
2858
3173
|
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
3174
|
if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
|
|
2864
3175
|
await store.delete(runId);
|
|
2865
3176
|
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2908,11 +3219,14 @@ var WebSkillRuntime = class {
|
|
|
2908
3219
|
hooks: this.#deps.hooks,
|
|
2909
3220
|
eventBus: this.#events,
|
|
2910
3221
|
longTerm: this.#deps.longTerm,
|
|
3222
|
+
formAutofill: this.#deps.formAutofill,
|
|
2911
3223
|
externalTools: this.#deps.externalTools,
|
|
2912
3224
|
skillProviders: this.#deps.skillProviders,
|
|
2913
3225
|
catalogFilter: this.#deps.catalogFilter,
|
|
2914
3226
|
snapshotStore: store,
|
|
2915
|
-
skillStateGuard: this.#deps.skillStateGuard
|
|
3227
|
+
skillStateGuard: this.#deps.skillStateGuard,
|
|
3228
|
+
skillIntegrityGuard: this.#deps.skillIntegrityGuard,
|
|
3229
|
+
skillOutcomeReporter: this.#deps.skillOutcomeReporter
|
|
2916
3230
|
}, this.#deps.config);
|
|
2917
3231
|
this.#loops.set(runId, loop);
|
|
2918
3232
|
try {
|
|
@@ -3031,6 +3345,7 @@ function sourceFromUrl(url) {
|
|
|
3031
3345
|
/** 受控钩子执行器:逐个执行,超时/异常默认降级为 warning,可切严格模式 */
|
|
3032
3346
|
var HookRunner = class {
|
|
3033
3347
|
#hooks = /* @__PURE__ */ new Map();
|
|
3348
|
+
/** 宿主可在装配后按 RuntimeConfig 调整(console Settings › Agent Runtime 即经此生效) */
|
|
3034
3349
|
timeoutMs;
|
|
3035
3350
|
failOnHookError;
|
|
3036
3351
|
onWarning;
|
|
@@ -3046,6 +3361,15 @@ var HookRunner = class {
|
|
|
3046
3361
|
this.#hooks.set(key, list);
|
|
3047
3362
|
return this;
|
|
3048
3363
|
}
|
|
3364
|
+
/**
|
|
3365
|
+
* 已注册钩子的**计数**,按注册相位分组(`'*'` 为全相位钩子)。
|
|
3366
|
+
* 刻意不返回函数引用:那会给 UI 一条调用宿主钩子的执行路径,而面板只需要「装没装上」。
|
|
3367
|
+
*/
|
|
3368
|
+
listRegisteredHooks() {
|
|
3369
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3370
|
+
for (const [phase, hooks] of this.#hooks) counts.set(phase, hooks.length);
|
|
3371
|
+
return counts;
|
|
3372
|
+
}
|
|
3049
3373
|
async run(phase, ctx) {
|
|
3050
3374
|
const hooks = [...this.#hooks.get(phase) ?? [], ...this.#hooks.get("*") ?? []];
|
|
3051
3375
|
for (const hook of hooks) try {
|
|
@@ -3652,17 +3976,22 @@ function summarizeToolCalls(run) {
|
|
|
3652
3976
|
if (typeof name !== "string" || typeof callId !== "string") continue;
|
|
3653
3977
|
const args = event.data?.["args"];
|
|
3654
3978
|
const durationMs = event.data?.["durationMs"];
|
|
3979
|
+
const errorCode = event.data?.["code"];
|
|
3655
3980
|
calls.push({
|
|
3656
3981
|
callId,
|
|
3657
3982
|
name,
|
|
3658
3983
|
status: event.type === "tool.completed" ? "completed" : "failed",
|
|
3659
3984
|
...typeof args === "string" ? { args } : {},
|
|
3660
|
-
...typeof durationMs === "number" ? { durationMs } : {}
|
|
3985
|
+
...typeof durationMs === "number" ? { durationMs } : {},
|
|
3986
|
+
...typeof errorCode === "string" ? { errorCode } : {},
|
|
3987
|
+
...event.type === "tool.failed" && typeof event.message === "string" ? { errorMessage: event.message } : {}
|
|
3661
3988
|
});
|
|
3662
3989
|
}
|
|
3663
3990
|
return calls;
|
|
3664
3991
|
}
|
|
3665
3992
|
const SESSION_SCHEMA_VERSION = 1;
|
|
3993
|
+
/** `FsSessionStore` 的缺省页长。缺省值属于实现,不属于调用方——否则「下推」只推了一半 */
|
|
3994
|
+
const FS_SESSION_PAGE_SIZE = 50;
|
|
3666
3995
|
const toMeta = (record) => ({
|
|
3667
3996
|
id: record.id,
|
|
3668
3997
|
createdAt: record.createdAt,
|
|
@@ -3673,6 +4002,25 @@ const toMeta = (record) => ({
|
|
|
3673
4002
|
});
|
|
3674
4003
|
const newSessionId = () => `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3675
4004
|
/**
|
|
4005
|
+
* 从新到旧的一页:无游标取末尾 `limit` 条,有游标取该位置**之前**的 `limit` 条。
|
|
4006
|
+
* 游标编码成「本页起点下标」——对会话文件这种整体重写的存储来说下标是稳定的,
|
|
4007
|
+
* 但它是不透明串,调用方不得自己构造(构造出来的越界值一律按 `VALIDATION_FAILED` 拒绝)。
|
|
4008
|
+
*/
|
|
4009
|
+
function takeTailPage(all, options, what) {
|
|
4010
|
+
const limit = options.limit ?? 50;
|
|
4011
|
+
if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
|
|
4012
|
+
let end = all.length;
|
|
4013
|
+
if (options.cursor !== void 0) {
|
|
4014
|
+
end = Number(options.cursor);
|
|
4015
|
+
if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
|
|
4016
|
+
}
|
|
4017
|
+
const start = Math.max(0, end - limit);
|
|
4018
|
+
return {
|
|
4019
|
+
items: all.slice(start, end),
|
|
4020
|
+
...start > 0 ? { nextCursor: String(start) } : {}
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
/**
|
|
3676
4024
|
* 解析会话文件。缺 `schemaVersion` 视为 0(0.0.1 时代文件,兼容读);
|
|
3677
4025
|
* 高于当前版本拒绝读,避免新版写的字段被旧版静默丢弃。
|
|
3678
4026
|
*/
|
|
@@ -3746,7 +4094,7 @@ var FsSessionStore = class {
|
|
|
3746
4094
|
return record;
|
|
3747
4095
|
}
|
|
3748
4096
|
async list(options = {}) {
|
|
3749
|
-
if (!await this.#fs.exists(this.#root)) return [];
|
|
4097
|
+
if (!await this.#fs.exists(this.#root)) return { items: [] };
|
|
3750
4098
|
const metas = [];
|
|
3751
4099
|
for (const entry of await this.#fs.list(this.#root)) {
|
|
3752
4100
|
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
@@ -3760,7 +4108,15 @@ var FsSessionStore = class {
|
|
|
3760
4108
|
if (record.archived === true && options.includeArchived !== true) continue;
|
|
3761
4109
|
metas.push(toMeta(record));
|
|
3762
4110
|
}
|
|
3763
|
-
|
|
4111
|
+
metas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
4112
|
+
return takeTailPage(metas, options, "session");
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* 整文件读后切片。磁盘 I/O 复杂度没有改善(备案 D25),
|
|
4116
|
+
* 但**跨出端口的记录数**已是常量——这正是分页对上层的意义。
|
|
4117
|
+
*/
|
|
4118
|
+
async listMessages(id, options = {}) {
|
|
4119
|
+
return takeTailPage((await this.#require(id)).messages, options, "message");
|
|
3764
4120
|
}
|
|
3765
4121
|
async get(id) {
|
|
3766
4122
|
const path = this.#path(id);
|
|
@@ -3820,4 +4176,4 @@ var FsSessionStore = class {
|
|
|
3820
4176
|
};
|
|
3821
4177
|
|
|
3822
4178
|
//#endregion
|
|
3823
|
-
export {
|
|
4179
|
+
export { schemaToForm as $, buildRenderResult as A, fromVercelStreamPart as B, RUN_SNAPSHOT_SCHEMA_VERSION as C, TraceRecorder as D, SerializingMemoryStore as E, extractChartSpec as F, networkUrlHost as G, isUnsupportedRunSnapshot as H, extractTodoTraceEvents as I, normalizeToolError as J, normalizeErrorCode as K, extractUiSpecEvents as L, clearStoredFormValues as M, createScriptContext as N, WebSkillRuntime as O, createWebSkillApi as P, resolveToolName as Q, formFieldKey as R, READ_SKILL_FILE_TOOL_NAME as S, SESSION_SCHEMA_VERSION as T, mergeCatalogEntries as U, isNetworkAllowed as V, networkPolicyLibSource as W, putFormValues as X, parseBridgeRequest as Y, readFormValues as Z, HookRunner as _, AnthropicClient as a, READ_SKILL_FILE_INPUT_SCHEMA as b, FORM_VALUES_KEY as c, FsMemoryStore as d, summarizeToolCalls as et, FsRunSnapshotStore as f, GoogleGenAiClient as g, FullDisclosureRouter as h, AgentLoop as i, validateUiSpecNode as it, clearFormValues as j, bridgeError as k, FS_SESSION_PAGE_SIZE as l, FsSessionStore as m, ASK_USER_TOOL as n, toVercelToolSpecs as nt, CapabilityApproval as o, FsRunTraceStore as p, normalizeToolContent as q, ASK_USER_TOOL_NAME as r, validateUiSpecEvent as rt, EventBus as s, ASK_USER_INPUT_SCHEMA as t, toLlmToolSpec as tt, FsArtifactStore as u, OpenAiCompatibleClient as v, RUN_TRACE_SCHEMA_VERSION as w, READ_SKILL_FILE_TOOL as x, ProgressiveRouter as y, fromVercelResult as z };
|