@webskill/sdk 0.0.3 → 0.0.5
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 +26 -4
- package/dist/browser.js +233 -20
- package/dist/{dist-RcqvzAGF.js → dist-BM-VcQ90.js} +31 -7
- package/dist/{dist-hMMxARXK.js → dist-D405AlPD.js} +503 -113
- package/dist/{dist-C1p26Ap9.js → dist-Ixnb-hNR.js} +60 -12
- package/dist/governance.d.ts +2 -2
- package/dist/governance.js +2 -2
- package/dist/{index-DnI0BTEp.d.ts → index-BQDc-U1x.d.ts} +141 -7
- package/dist/{index-D65Jk0ju.d.ts → index-aEple804.d.ts} +9 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +2 -2
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +2 -2
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/package.json +1 -1
|
@@ -259,6 +259,15 @@ function checkSkillRules(input) {
|
|
|
259
259
|
severity: "error",
|
|
260
260
|
message: `Unsupported script file in scripts/ of directory ${dirName}: ${fileName} (only .ts/.js and .schema.json sidecars allowed)`
|
|
261
261
|
});
|
|
262
|
+
const allowedTools = metadata?.["allowed-tools"];
|
|
263
|
+
if (Array.isArray(allowedTools)) {
|
|
264
|
+
const scriptNames = new Set((input.scriptFileNames ?? []).filter((f) => SCRIPT_EXTENSION_RE.test(f)).map((f) => f.replace(SCRIPT_EXTENSION_RE, "")));
|
|
265
|
+
for (const entry of allowedTools) if (typeof entry === "string" && !scriptNames.has(entry)) issues.push({
|
|
266
|
+
code: "SKILL_UNKNOWN_ALLOWED_TOOL",
|
|
267
|
+
severity: "warning",
|
|
268
|
+
message: `allowed-tools entry ${JSON.stringify(entry)} of directory ${dirName} does not match any script in scripts/`
|
|
269
|
+
});
|
|
270
|
+
}
|
|
262
271
|
return issues;
|
|
263
272
|
}
|
|
264
273
|
/** 由条目列表构建 Catalog,保证按名称排序 */
|
|
@@ -908,7 +917,7 @@ const ASK_USER_TOOL = {
|
|
|
908
917
|
* 不暴露 fs 本体(沙箱语义,对齐 deferred-items D1)。
|
|
909
918
|
*/
|
|
910
919
|
function createScriptContext(deps) {
|
|
911
|
-
const { fs, artifactStore, skillName, skillRoot, runId, confirm } = deps;
|
|
920
|
+
const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning } = deps;
|
|
912
921
|
return {
|
|
913
922
|
skillName,
|
|
914
923
|
runId,
|
|
@@ -928,7 +937,8 @@ function createScriptContext(deps) {
|
|
|
928
937
|
mimeType: options?.mimeType
|
|
929
938
|
});
|
|
930
939
|
},
|
|
931
|
-
...confirm ? { confirm } : {}
|
|
940
|
+
...confirm ? { confirm } : {},
|
|
941
|
+
...onWarning ? { onWarning } : {}
|
|
932
942
|
};
|
|
933
943
|
}
|
|
934
944
|
/**
|
|
@@ -1310,6 +1320,91 @@ function bridgeError(id, code, message) {
|
|
|
1310
1320
|
}
|
|
1311
1321
|
};
|
|
1312
1322
|
}
|
|
1323
|
+
/**
|
|
1324
|
+
* 判定 URL 是否被策略放行:
|
|
1325
|
+
* - 'deny-all'(默认)全拒;'allow-all' 全放
|
|
1326
|
+
* - { allow } 条目支持三种写法:
|
|
1327
|
+
* - 精确域名 'api.example.com'(忽略端口与路径)
|
|
1328
|
+
* - 通配 '*.example.com'(含 apex 与任意深度子域)
|
|
1329
|
+
* - 源 'http://localhost:3000'(协议 + host + port 全等)
|
|
1330
|
+
* - URL 解析失败一律拒绝
|
|
1331
|
+
*/
|
|
1332
|
+
function isNetworkAllowed(policy, url) {
|
|
1333
|
+
if (policy === "allow-all") return true;
|
|
1334
|
+
if (policy === "deny-all" || !policy || typeof policy !== "object") return false;
|
|
1335
|
+
var allow = policy.allow;
|
|
1336
|
+
if (!Array.isArray(allow)) return false;
|
|
1337
|
+
var parsed;
|
|
1338
|
+
try {
|
|
1339
|
+
parsed = new URL(url);
|
|
1340
|
+
} catch {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
var host = parsed.hostname.toLowerCase();
|
|
1344
|
+
for (var i = 0; i < allow.length; i++) {
|
|
1345
|
+
var entry = allow[i];
|
|
1346
|
+
if (typeof entry !== "string" || entry === "") continue;
|
|
1347
|
+
if (entry.indexOf("://") !== -1) {
|
|
1348
|
+
try {
|
|
1349
|
+
if (new URL(entry).origin === parsed.origin) return true;
|
|
1350
|
+
} catch {}
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
var rule = entry.toLowerCase();
|
|
1354
|
+
if (rule.indexOf("*.") === 0) {
|
|
1355
|
+
var suffix = rule.slice(2);
|
|
1356
|
+
if (host === suffix || host.endsWith("." + suffix)) return true;
|
|
1357
|
+
} else if (host === rule) return true;
|
|
1358
|
+
}
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
/** 阻断 trace 用的脱敏 host(解析失败返回占位,不记录完整 URL) */
|
|
1362
|
+
function networkUrlHost(url) {
|
|
1363
|
+
try {
|
|
1364
|
+
return new URL(url).hostname;
|
|
1365
|
+
} catch {
|
|
1366
|
+
return "(unparseable-url)";
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
|
|
1371
|
+
* 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
|
|
1372
|
+
* once-per-run(默认)在 run 内记忆已批准能力,every-call 每次调用都问。
|
|
1373
|
+
*/
|
|
1374
|
+
var CapabilityApproval = class {
|
|
1375
|
+
#uiBridge;
|
|
1376
|
+
#scope;
|
|
1377
|
+
/** runId → 已批准能力集合(once-per-run 记忆) */
|
|
1378
|
+
#approved = /* @__PURE__ */ new Map();
|
|
1379
|
+
#seq = 0;
|
|
1380
|
+
constructor(deps = {}) {
|
|
1381
|
+
this.#uiBridge = deps.uiBridge;
|
|
1382
|
+
this.#scope = deps.scope ?? "once-per-run";
|
|
1383
|
+
}
|
|
1384
|
+
async authorize(input) {
|
|
1385
|
+
const { runId, capability, mode } = input;
|
|
1386
|
+
if (mode === false) return "disabled";
|
|
1387
|
+
if (mode !== "require-approval") return "allowed";
|
|
1388
|
+
if (this.#scope === "once-per-run" && this.#approved.get(runId)?.has(capability)) return "allowed";
|
|
1389
|
+
if (!this.#uiBridge) return "denied";
|
|
1390
|
+
if ((await this.#uiBridge.request({
|
|
1391
|
+
type: "authorize",
|
|
1392
|
+
id: `authorize-${runId}-${capability}-${++this.#seq}`,
|
|
1393
|
+
capability,
|
|
1394
|
+
message: input.message,
|
|
1395
|
+
...input.details !== void 0 ? { details: input.details } : {}
|
|
1396
|
+
})).value !== true) return "denied";
|
|
1397
|
+
if (this.#scope === "once-per-run") {
|
|
1398
|
+
let set = this.#approved.get(runId);
|
|
1399
|
+
if (!set) {
|
|
1400
|
+
set = /* @__PURE__ */ new Set();
|
|
1401
|
+
this.#approved.set(runId, set);
|
|
1402
|
+
}
|
|
1403
|
+
set.add(capability);
|
|
1404
|
+
}
|
|
1405
|
+
return "allowed";
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1313
1408
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
1314
1409
|
var TraceRecorder = class {
|
|
1315
1410
|
#runId;
|
|
@@ -1384,34 +1479,35 @@ var AgentLoop = class {
|
|
|
1384
1479
|
};
|
|
1385
1480
|
}
|
|
1386
1481
|
async run(input) {
|
|
1387
|
-
|
|
1388
|
-
|
|
1482
|
+
this.#deps.llm;
|
|
1483
|
+
this.#deps.artifactStore;
|
|
1389
1484
|
const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1390
1485
|
const runId = input.runId ?? `run-${Math.random().toString(36).slice(2, 10)}`;
|
|
1391
1486
|
const trace = new TraceRecorder(runId, this.#deps.clock);
|
|
1392
1487
|
const startedAt = now();
|
|
1393
1488
|
const startMs = Date.parse(startedAt);
|
|
1394
|
-
const run = {
|
|
1395
|
-
id: runId,
|
|
1396
|
-
sessionId: input.sessionId,
|
|
1397
|
-
status: "running",
|
|
1398
|
-
phase: "route",
|
|
1399
|
-
userPrompt: input.userPrompt,
|
|
1400
|
-
startedAt,
|
|
1401
|
-
activeSkillNames: [],
|
|
1402
|
-
artifacts: [],
|
|
1403
|
-
trace: []
|
|
1404
|
-
};
|
|
1405
1489
|
const state = {
|
|
1406
1490
|
runId,
|
|
1407
|
-
run
|
|
1491
|
+
run: {
|
|
1492
|
+
id: runId,
|
|
1493
|
+
sessionId: input.sessionId,
|
|
1494
|
+
status: "running",
|
|
1495
|
+
phase: "route",
|
|
1496
|
+
userPrompt: input.userPrompt,
|
|
1497
|
+
startedAt,
|
|
1498
|
+
activeSkillNames: [],
|
|
1499
|
+
artifacts: [],
|
|
1500
|
+
trace: []
|
|
1501
|
+
},
|
|
1408
1502
|
trace,
|
|
1409
1503
|
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
1410
1504
|
activated: /* @__PURE__ */ new Set(),
|
|
1411
1505
|
activatedTools: /* @__PURE__ */ new Map(),
|
|
1412
1506
|
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
1413
1507
|
now,
|
|
1414
|
-
interactionSeq: 0
|
|
1508
|
+
interactionSeq: 0,
|
|
1509
|
+
messages: [],
|
|
1510
|
+
turn: 0
|
|
1415
1511
|
};
|
|
1416
1512
|
if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
|
|
1417
1513
|
const route = this.#deps.catalogFilter ? {
|
|
@@ -1431,114 +1527,273 @@ var AgentLoop = class {
|
|
|
1431
1527
|
return [];
|
|
1432
1528
|
}
|
|
1433
1529
|
}))).flat();
|
|
1434
|
-
|
|
1530
|
+
state.messages = [{
|
|
1435
1531
|
role: "system",
|
|
1436
1532
|
content: route.systemPrompt
|
|
1437
1533
|
}, {
|
|
1438
1534
|
role: "user",
|
|
1439
1535
|
content: input.userPrompt
|
|
1440
1536
|
}];
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1537
|
+
try {
|
|
1538
|
+
return await this.#turnLoop(state, startMs, 1, externalSpecs);
|
|
1539
|
+
} catch (e) {
|
|
1540
|
+
if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
|
|
1541
|
+
throw e;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
/** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
|
|
1545
|
+
async #turnLoop(state, startMs, startTurn, externalSpecs) {
|
|
1546
|
+
const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
|
|
1547
|
+
const { llm } = this.#deps;
|
|
1548
|
+
const { trace, messages } = state;
|
|
1549
|
+
for (let turn = startTurn;; turn++) {
|
|
1550
|
+
state.turn = turn;
|
|
1551
|
+
if (turn > this.#config.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${this.#config.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1552
|
+
if (Date.parse(state.now()) - startMs > this.#config.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${this.#config.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1553
|
+
const toolSpecs = [
|
|
1554
|
+
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1555
|
+
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
1556
|
+
...[...state.activatedTools.values()].map(toLlmToolSpec),
|
|
1557
|
+
...externalSpecs
|
|
1558
|
+
];
|
|
1559
|
+
trace.record("llm.request", { data: {
|
|
1560
|
+
turn,
|
|
1561
|
+
messageCount: messages.length,
|
|
1562
|
+
toolCount: toolSpecs.length
|
|
1563
|
+
} });
|
|
1564
|
+
let response;
|
|
1467
1565
|
try {
|
|
1468
|
-
|
|
1566
|
+
response = llm.stream ? await this.#completeStreaming(llm, state, {
|
|
1567
|
+
model: this.#deps.model,
|
|
1568
|
+
messages,
|
|
1569
|
+
tools: toolSpecs,
|
|
1570
|
+
temperature: this.#config.temperature
|
|
1571
|
+
}) : await llm.complete({
|
|
1572
|
+
model: this.#deps.model,
|
|
1573
|
+
messages,
|
|
1574
|
+
tools: toolSpecs,
|
|
1575
|
+
temperature: this.#config.temperature
|
|
1576
|
+
});
|
|
1469
1577
|
} catch (e) {
|
|
1470
|
-
|
|
1578
|
+
const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
|
|
1579
|
+
return finish("failed", "llm-error", messageOf(e), code);
|
|
1471
1580
|
}
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
};
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
...externalSpecs
|
|
1487
|
-
];
|
|
1488
|
-
trace.record("llm.request", { data: {
|
|
1489
|
-
turn,
|
|
1490
|
-
messageCount: messages.length,
|
|
1491
|
-
toolCount: toolSpecs.length
|
|
1492
|
-
} });
|
|
1493
|
-
let response;
|
|
1581
|
+
trace.record("llm.response", { data: {
|
|
1582
|
+
turn,
|
|
1583
|
+
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
1584
|
+
contentLength: response.content?.length ?? 0
|
|
1585
|
+
} });
|
|
1586
|
+
if (!response.toolCalls?.length) return finish("completed", "final-answer", response.content ?? "");
|
|
1587
|
+
await this.#lifecycle("execute", state, { turn });
|
|
1588
|
+
messages.push({
|
|
1589
|
+
role: "assistant",
|
|
1590
|
+
content: response.content ?? "",
|
|
1591
|
+
toolCalls: response.toolCalls
|
|
1592
|
+
});
|
|
1593
|
+
for (const call of response.toolCalls) {
|
|
1594
|
+
let result;
|
|
1494
1595
|
try {
|
|
1495
|
-
|
|
1496
|
-
model: this.#deps.model,
|
|
1497
|
-
messages,
|
|
1498
|
-
tools: toolSpecs,
|
|
1499
|
-
temperature: this.#config.temperature
|
|
1500
|
-
}) : await llm.complete({
|
|
1501
|
-
model: this.#deps.model,
|
|
1502
|
-
messages,
|
|
1503
|
-
tools: toolSpecs,
|
|
1504
|
-
temperature: this.#config.temperature
|
|
1505
|
-
});
|
|
1596
|
+
result = await this.#executeCall(call, state);
|
|
1506
1597
|
} catch (e) {
|
|
1507
|
-
|
|
1508
|
-
|
|
1598
|
+
if (e instanceof RunTerminated) return finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
1599
|
+
throw e;
|
|
1509
1600
|
}
|
|
1510
|
-
trace.record("llm.response", { data: {
|
|
1511
|
-
turn,
|
|
1512
|
-
hasToolCalls: Boolean(response.toolCalls?.length),
|
|
1513
|
-
contentLength: response.content?.length ?? 0
|
|
1514
|
-
} });
|
|
1515
|
-
if (!response.toolCalls?.length) return await finish("completed", "final-answer", response.content ?? "");
|
|
1516
|
-
await this.#lifecycle("execute", state, { turn });
|
|
1517
1601
|
messages.push({
|
|
1518
|
-
role: "
|
|
1519
|
-
|
|
1520
|
-
|
|
1602
|
+
role: "tool",
|
|
1603
|
+
toolCallId: call.id,
|
|
1604
|
+
content: JSON.stringify(result)
|
|
1521
1605
|
});
|
|
1522
|
-
for (const call of response.toolCalls) {
|
|
1523
|
-
let result;
|
|
1524
|
-
try {
|
|
1525
|
-
result = await this.#executeCall(call, state);
|
|
1526
|
-
} catch (e) {
|
|
1527
|
-
if (e instanceof RunTerminated) return await finish(e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
1528
|
-
throw e;
|
|
1529
|
-
}
|
|
1530
|
-
messages.push({
|
|
1531
|
-
role: "tool",
|
|
1532
|
-
toolCallId: call.id,
|
|
1533
|
-
content: JSON.stringify(result)
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
1606
|
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
/** 统一终态处理:completed/failed/cancelled;终态即删快照(快照是续命机制,审计归治理) */
|
|
1610
|
+
async #finish(state, status, reason, output, errorCode) {
|
|
1611
|
+
const { run, trace } = state;
|
|
1612
|
+
run.status = status;
|
|
1613
|
+
run.terminationReason = reason;
|
|
1614
|
+
run.endedAt = state.now();
|
|
1615
|
+
run.activeSkillNames = [...state.activated].sort();
|
|
1616
|
+
run.artifacts = await this.#deps.artifactStore.listArtifacts(state.runId);
|
|
1617
|
+
if (status === "completed") trace.record("run.completed", { data: { reason } });
|
|
1618
|
+
else if (status === "cancelled") trace.record("run.cancelled", {
|
|
1619
|
+
message: output,
|
|
1620
|
+
data: { reason }
|
|
1621
|
+
});
|
|
1622
|
+
else trace.record("run.failed", {
|
|
1623
|
+
message: output,
|
|
1624
|
+
data: {
|
|
1625
|
+
reason,
|
|
1626
|
+
...errorCode ? { code: errorCode } : {}
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
const bridge = this.#deps.uiBridge;
|
|
1630
|
+
if (status === "completed" && bridge?.renderResult && this.#config.renderResult !== "off") try {
|
|
1631
|
+
const request = buildRenderResult(run, output);
|
|
1632
|
+
await bridge.renderResult(request);
|
|
1633
|
+
trace.record("ui.rendered", { data: { blockCount: request.blocks.length } });
|
|
1537
1634
|
} catch (e) {
|
|
1538
|
-
|
|
1635
|
+
trace.record("run.warning", { message: `renderResult failed: ${messageOf(e)}` });
|
|
1636
|
+
}
|
|
1637
|
+
if (this.#deps.snapshotStore) try {
|
|
1638
|
+
await this.#deps.snapshotStore.delete(state.runId);
|
|
1639
|
+
} catch (e) {
|
|
1640
|
+
trace.record("run.warning", { message: `Failed to delete run snapshot: ${messageOf(e)}` });
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
await this.#lifecycle(status === "completed" ? "complete" : "fail", state, { reason });
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
trace.record("run.warning", { message: `Terminal lifecycle hook failed: ${messageOf(e)}` });
|
|
1646
|
+
}
|
|
1647
|
+
run.trace = trace.list();
|
|
1648
|
+
return {
|
|
1649
|
+
output,
|
|
1650
|
+
run
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
/** D3 快照写入(含 pendingInteraction 与过期时间;写失败降级 run.warning) */
|
|
1654
|
+
async #saveSnapshot(state, request) {
|
|
1655
|
+
const store = this.#deps.snapshotStore;
|
|
1656
|
+
if (!store) return;
|
|
1657
|
+
const snapshot = {
|
|
1658
|
+
schemaVersion: 1,
|
|
1659
|
+
runId: state.runId,
|
|
1660
|
+
sessionId: state.run.sessionId,
|
|
1661
|
+
userPrompt: state.run.userPrompt,
|
|
1662
|
+
startedAt: state.run.startedAt,
|
|
1663
|
+
messages: state.messages.map((m) => ({ ...m })),
|
|
1664
|
+
turn: state.turn,
|
|
1665
|
+
activeSkillNames: [...state.activated].sort(),
|
|
1666
|
+
activatedTools: [...state.activatedTools.values()],
|
|
1667
|
+
pendingInteraction: request,
|
|
1668
|
+
interactionExpiresAt: state.run.interruptExpiresAt,
|
|
1669
|
+
config: {
|
|
1670
|
+
maxTurns: this.#config.maxTurns,
|
|
1671
|
+
totalTimeoutMs: this.#config.totalTimeoutMs,
|
|
1672
|
+
toolTimeoutMs: this.#config.toolTimeoutMs,
|
|
1673
|
+
...this.#config.temperature !== void 0 ? { temperature: this.#config.temperature } : {}
|
|
1674
|
+
},
|
|
1675
|
+
snapshotAt: state.now()
|
|
1676
|
+
};
|
|
1677
|
+
try {
|
|
1678
|
+
await store.save(snapshot);
|
|
1679
|
+
} catch (e) {
|
|
1680
|
+
state.trace.record("run.warning", { message: `Failed to save run snapshot: ${messageOf(e)}` });
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* D3 恢复:以快照重建 LoopState,重新发起等待中的交互(表单重新渲染),
|
|
1685
|
+
* 应答后从快照轮次续跑主循环;totalTimeout 以快照 startedAt 续算。
|
|
1686
|
+
*/
|
|
1687
|
+
async resume(snapshot) {
|
|
1688
|
+
const now = this.#deps.clock?.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1689
|
+
const runId = snapshot.runId;
|
|
1690
|
+
const trace = new TraceRecorder(runId, this.#deps.clock);
|
|
1691
|
+
const startMs = Date.parse(snapshot.startedAt);
|
|
1692
|
+
const state = {
|
|
1693
|
+
runId,
|
|
1694
|
+
run: {
|
|
1695
|
+
id: runId,
|
|
1696
|
+
sessionId: snapshot.sessionId,
|
|
1697
|
+
status: "running",
|
|
1698
|
+
phase: "execute",
|
|
1699
|
+
userPrompt: snapshot.userPrompt,
|
|
1700
|
+
startedAt: snapshot.startedAt,
|
|
1701
|
+
activeSkillNames: snapshot.activeSkillNames,
|
|
1702
|
+
artifacts: [],
|
|
1703
|
+
trace: []
|
|
1704
|
+
},
|
|
1705
|
+
trace,
|
|
1706
|
+
reader: new SkillReader(this.#deps.fs, this.#deps.skillIndex),
|
|
1707
|
+
activated: new Set(snapshot.activeSkillNames),
|
|
1708
|
+
activatedTools: new Map(snapshot.activatedTools.map((d) => [d.name, d])),
|
|
1709
|
+
toolTimeoutMs: snapshot.config.toolTimeoutMs,
|
|
1710
|
+
now,
|
|
1711
|
+
interactionSeq: 0,
|
|
1712
|
+
messages: snapshot.messages.map((m) => ({ ...m })),
|
|
1713
|
+
turn: snapshot.turn
|
|
1714
|
+
};
|
|
1715
|
+
if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
|
|
1716
|
+
trace.record("run.resumed", { data: {
|
|
1717
|
+
snapshotAt: snapshot.snapshotAt,
|
|
1718
|
+
turn: snapshot.turn,
|
|
1719
|
+
interactionType: snapshot.pendingInteraction.type
|
|
1720
|
+
} });
|
|
1721
|
+
const pending = snapshot.pendingInteraction;
|
|
1722
|
+
const pendingCall = this.#findPendingToolCall(state.messages);
|
|
1723
|
+
try {
|
|
1724
|
+
if ((pending.type === "form" || pending.type === "select") && pendingCall) {
|
|
1725
|
+
const value = await this.#interact(state, pending, {
|
|
1726
|
+
tool: pendingCall.name,
|
|
1727
|
+
resumed: true
|
|
1728
|
+
});
|
|
1729
|
+
const args = {
|
|
1730
|
+
...pendingCall.arguments,
|
|
1731
|
+
...typeof value === "object" && value !== null ? value : {}
|
|
1732
|
+
};
|
|
1733
|
+
const result = await this.#executeCall({
|
|
1734
|
+
...pendingCall,
|
|
1735
|
+
arguments: args
|
|
1736
|
+
}, state);
|
|
1737
|
+
state.messages.push({
|
|
1738
|
+
role: "tool",
|
|
1739
|
+
toolCallId: pendingCall.id,
|
|
1740
|
+
content: JSON.stringify(result)
|
|
1741
|
+
});
|
|
1742
|
+
} else if (pending.type === "ask" && pendingCall) {
|
|
1743
|
+
const value = await this.#interact(state, pending, {
|
|
1744
|
+
tool: pendingCall.name,
|
|
1745
|
+
resumed: true
|
|
1746
|
+
});
|
|
1747
|
+
const result = {
|
|
1748
|
+
ok: true,
|
|
1749
|
+
content: [{
|
|
1750
|
+
type: "text",
|
|
1751
|
+
text: typeof value === "string" ? value : JSON.stringify(value ?? "")
|
|
1752
|
+
}]
|
|
1753
|
+
};
|
|
1754
|
+
state.trace.record("tool.completed", { data: {
|
|
1755
|
+
name: pendingCall.name,
|
|
1756
|
+
callId: pendingCall.id
|
|
1757
|
+
} });
|
|
1758
|
+
state.messages.push({
|
|
1759
|
+
role: "tool",
|
|
1760
|
+
toolCallId: pendingCall.id,
|
|
1761
|
+
content: JSON.stringify(result)
|
|
1762
|
+
});
|
|
1763
|
+
} else if (pendingCall) {
|
|
1764
|
+
const result = await this.#executeCall(pendingCall, state);
|
|
1765
|
+
state.messages.push({
|
|
1766
|
+
role: "tool",
|
|
1767
|
+
toolCallId: pendingCall.id,
|
|
1768
|
+
content: JSON.stringify(result)
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
if (e instanceof RunTerminated) return this.#finish(state, e.outcome.status, e.outcome.reason, e.outcome.message, e.outcome.code);
|
|
1773
|
+
throw e;
|
|
1774
|
+
}
|
|
1775
|
+
const externalSpecs = (await Promise.all((this.#deps.externalTools ?? []).map(async (source) => {
|
|
1776
|
+
try {
|
|
1777
|
+
return await source.listToolSpecs();
|
|
1778
|
+
} catch (e) {
|
|
1779
|
+
trace.record("run.warning", { message: `External tool source "${source.kind}" failed to list specs: ${messageOf(e)}` });
|
|
1780
|
+
return [];
|
|
1781
|
+
}
|
|
1782
|
+
}))).flat();
|
|
1783
|
+
try {
|
|
1784
|
+
return await this.#turnLoop(state, startMs, snapshot.turn + 1, externalSpecs);
|
|
1785
|
+
} catch (e) {
|
|
1786
|
+
if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf(e), "RUN_FAILED");
|
|
1539
1787
|
throw e;
|
|
1540
1788
|
}
|
|
1541
1789
|
}
|
|
1790
|
+
/** 最后一条 assistant 消息里第一个尚无 tool 响应的工具调用(即中断时正在处理的那个) */
|
|
1791
|
+
#findPendingToolCall(messages) {
|
|
1792
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant" && m.toolCalls?.length);
|
|
1793
|
+
if (!lastAssistant?.toolCalls) return void 0;
|
|
1794
|
+
const answered = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
|
|
1795
|
+
return lastAssistant.toolCalls.find((call) => !answered.has(call.id));
|
|
1796
|
+
}
|
|
1542
1797
|
/**
|
|
1543
1798
|
* 流式 LLM 调用:text-delta 仅在 EventBus 有订阅者时发射 llm.delta 事件,
|
|
1544
1799
|
* 并同步转发 uiBridge.onTextDelta;tool-calls/done 组装为 LlmResponse 走原有分支。
|
|
@@ -1598,6 +1853,7 @@ var AgentLoop = class {
|
|
|
1598
1853
|
const { run } = state;
|
|
1599
1854
|
run.status = "interrupted";
|
|
1600
1855
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
1856
|
+
await this.#saveSnapshot(state, request);
|
|
1601
1857
|
await this.#lifecycle("interact", state, {
|
|
1602
1858
|
interactionId: request.id,
|
|
1603
1859
|
type: request.type
|
|
@@ -1777,6 +2033,13 @@ var AgentLoop = class {
|
|
|
1777
2033
|
if (!root) return "";
|
|
1778
2034
|
const executor = this.#deps.executor;
|
|
1779
2035
|
if (!executor) return "";
|
|
2036
|
+
let allowedTools;
|
|
2037
|
+
try {
|
|
2038
|
+
const { metadata } = parseSkillMarkdown(await this.#deps.fs.readText(`${root}/SKILL.md`));
|
|
2039
|
+
const raw = metadata["allowed-tools"];
|
|
2040
|
+
if (raw !== void 0) if (Array.isArray(raw)) allowedTools = raw.filter((e) => typeof e === "string");
|
|
2041
|
+
else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
2042
|
+
} catch {}
|
|
1780
2043
|
const loaded = [];
|
|
1781
2044
|
let scriptFiles = [];
|
|
1782
2045
|
try {
|
|
@@ -1787,6 +2050,7 @@ var AgentLoop = class {
|
|
|
1787
2050
|
for (const file of scriptFiles) {
|
|
1788
2051
|
const match = /^(.*)\.(ts|js)$/.exec(file);
|
|
1789
2052
|
if (!match?.[1]) continue;
|
|
2053
|
+
if (allowedTools && !allowedTools.includes(match[1])) continue;
|
|
1790
2054
|
try {
|
|
1791
2055
|
const def = await executor.loadDefinition(root, match[1]);
|
|
1792
2056
|
await this.#enrichDefinition(root, match[1], def, state);
|
|
@@ -1821,10 +2085,12 @@ var AgentLoop = class {
|
|
|
1821
2085
|
async #handleScriptTool(call, skillName, scriptName, state) {
|
|
1822
2086
|
const root = this.#deps.skillIndex.get(skillName);
|
|
1823
2087
|
if (!root) return toolError("TOOL_NOT_FOUND", `Skill "${skillName}" is not in the catalog`);
|
|
1824
|
-
|
|
2088
|
+
if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
|
|
1825
2089
|
const def = state.activatedTools.get(call.name);
|
|
1826
|
-
|
|
1827
|
-
|
|
2090
|
+
if (!def) return toolError("TOOL_NOT_FOUND", `Tool "${call.name}" is not available for skill "${skillName}"`);
|
|
2091
|
+
let args = call.arguments;
|
|
2092
|
+
const missing = (def.inputSchema?.required ?? []).filter((key) => args[key] === void 0);
|
|
2093
|
+
if (missing.length > 0 && this.#deps.uiBridge && this.#policy.missingParams === "user" && def.inputSchema) try {
|
|
1828
2094
|
const value = await this.#interact(state, {
|
|
1829
2095
|
type: "form",
|
|
1830
2096
|
id: this.#nextInteractionId(state),
|
|
@@ -1842,14 +2108,14 @@ var AgentLoop = class {
|
|
|
1842
2108
|
if (e instanceof BridgeRequestError) return toolError("UI_UNAVAILABLE", `Parameter form failed: ${e.message}`);
|
|
1843
2109
|
throw e;
|
|
1844
2110
|
}
|
|
1845
|
-
if (!this.#deps.executor) return toolError("TOOL_UNSUPPORTED", `No script executor is configured; script tool "${call.name}" cannot run`);
|
|
1846
2111
|
const context = createScriptContext({
|
|
1847
2112
|
fs: this.#deps.fs,
|
|
1848
2113
|
artifactStore: this.#deps.artifactStore,
|
|
1849
2114
|
skillName,
|
|
1850
2115
|
skillRoot: root,
|
|
1851
2116
|
runId: state.runId,
|
|
1852
|
-
confirm: (message) => this.#confirm(message, state)
|
|
2117
|
+
confirm: (message) => this.#confirm(message, state),
|
|
2118
|
+
onWarning: (message) => state.trace.record("run.warning", { message })
|
|
1853
2119
|
});
|
|
1854
2120
|
try {
|
|
1855
2121
|
const before = new Set((await this.#deps.artifactStore.listArtifacts(state.runId)).map((a) => a.id));
|
|
@@ -1954,6 +2220,58 @@ function mergeCatalogEntries(localEntries, providerEntries) {
|
|
|
1954
2220
|
for (const entry of providerEntries) if (!byName.has(entry.name)) byName.set(entry.name, entry);
|
|
1955
2221
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1956
2222
|
}
|
|
2223
|
+
const RUN_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
2224
|
+
const SNAPSHOT_SUFFIX = ".snapshot.json";
|
|
2225
|
+
/**
|
|
2226
|
+
* FileSystemProvider 后端的快照存储:<root>/<runId>.snapshot.json。
|
|
2227
|
+
* 坏 JSON → RUN_SNAPSHOT_INCOMPATIBLE 并自动清理坏文件;runId 过路径安全校验。
|
|
2228
|
+
*/
|
|
2229
|
+
var FsRunSnapshotStore = class {
|
|
2230
|
+
#root;
|
|
2231
|
+
#fs;
|
|
2232
|
+
constructor(deps) {
|
|
2233
|
+
this.#root = deps.root.replace(/\/+$/, "");
|
|
2234
|
+
this.#fs = deps.fs;
|
|
2235
|
+
}
|
|
2236
|
+
#path(runId) {
|
|
2237
|
+
return resolveInsideRoot(this.#root, `${runId}${SNAPSHOT_SUFFIX}`);
|
|
2238
|
+
}
|
|
2239
|
+
async save(snapshot) {
|
|
2240
|
+
await this.#fs.writeText(this.#path(snapshot.runId), JSON.stringify(snapshot, null, 2));
|
|
2241
|
+
}
|
|
2242
|
+
async load(runId) {
|
|
2243
|
+
const path = this.#path(runId);
|
|
2244
|
+
if (!await this.#fs.exists(path)) return void 0;
|
|
2245
|
+
let parsed;
|
|
2246
|
+
try {
|
|
2247
|
+
parsed = JSON.parse(await this.#fs.readText(path));
|
|
2248
|
+
} catch (e) {
|
|
2249
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
2250
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" is corrupted and was deleted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
2251
|
+
}
|
|
2252
|
+
const snapshot = parsed;
|
|
2253
|
+
if (typeof snapshot !== "object" || snapshot === null || snapshot.runId !== runId) {
|
|
2254
|
+
await this.#fs.remove(path).catch(() => void 0);
|
|
2255
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has an unexpected shape and was deleted`);
|
|
2256
|
+
}
|
|
2257
|
+
return snapshot;
|
|
2258
|
+
}
|
|
2259
|
+
async delete(runId) {
|
|
2260
|
+
const path = this.#path(runId);
|
|
2261
|
+
if (await this.#fs.exists(path)) await this.#fs.remove(path);
|
|
2262
|
+
}
|
|
2263
|
+
async list() {
|
|
2264
|
+
if (!await this.#fs.exists(this.#root)) return [];
|
|
2265
|
+
const out = [];
|
|
2266
|
+
for (const entry of await this.#fs.list(this.#root)) {
|
|
2267
|
+
if (entry.type !== "file" || !entry.path.endsWith(SNAPSHOT_SUFFIX)) continue;
|
|
2268
|
+
try {
|
|
2269
|
+
out.push(JSON.parse(await this.#fs.readText(entry.path)));
|
|
2270
|
+
} catch {}
|
|
2271
|
+
}
|
|
2272
|
+
return out.sort((a, b) => a.snapshotAt.localeCompare(b.snapshotAt));
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
1957
2275
|
/** runtime 门面:组合 discovery / router / agent loop / lifecycle */
|
|
1958
2276
|
var WebSkillRuntime = class {
|
|
1959
2277
|
#deps;
|
|
@@ -2017,7 +2335,8 @@ var WebSkillRuntime = class {
|
|
|
2017
2335
|
longTerm: this.#deps.longTerm,
|
|
2018
2336
|
externalTools: this.#deps.externalTools,
|
|
2019
2337
|
skillProviders: this.#deps.skillProviders,
|
|
2020
|
-
catalogFilter: this.#deps.catalogFilter
|
|
2338
|
+
catalogFilter: this.#deps.catalogFilter,
|
|
2339
|
+
snapshotStore: this.#deps.snapshotStore
|
|
2021
2340
|
}, this.#deps.config).run({
|
|
2022
2341
|
sessionId: this.#session.id,
|
|
2023
2342
|
userPrompt,
|
|
@@ -2037,7 +2356,78 @@ var WebSkillRuntime = class {
|
|
|
2037
2356
|
});
|
|
2038
2357
|
return result;
|
|
2039
2358
|
}
|
|
2359
|
+
/** D3:列出可恢复的 interrupted run(供 UI 展示"未完成任务") */
|
|
2360
|
+
async listInterruptedRuns() {
|
|
2361
|
+
if (!this.#deps.snapshotStore) return [];
|
|
2362
|
+
return this.#deps.snapshotStore.list();
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* D3 恢复 interrupted run:
|
|
2366
|
+
* 不存在 → RUN_SNAPSHOT_NOT_FOUND;schemaVersion 不匹配 → 删除 + RUN_SNAPSHOT_INCOMPATIBLE;
|
|
2367
|
+
* 已过期 → interaction-timeout 终态并删快照;否则重建 LoopState 重新发起交互续跑。
|
|
2368
|
+
*/
|
|
2369
|
+
async resumeRun(runId) {
|
|
2370
|
+
const store = this.#deps.snapshotStore;
|
|
2371
|
+
const snapshot = store ? await store.load(runId) : void 0;
|
|
2372
|
+
if (!snapshot) throw new WebSkillError("RUN_SNAPSHOT_NOT_FOUND", `No snapshot found for run "${runId}"`);
|
|
2373
|
+
if (snapshot.schemaVersion !== 1) {
|
|
2374
|
+
await store.delete(runId);
|
|
2375
|
+
throw new WebSkillError("RUN_SNAPSHOT_INCOMPATIBLE", `Snapshot for run "${runId}" has incompatible schemaVersion ${String(snapshot.schemaVersion)}`);
|
|
2376
|
+
}
|
|
2377
|
+
if (Date.now() > Date.parse(snapshot.interactionExpiresAt)) {
|
|
2378
|
+
await store.delete(runId);
|
|
2379
|
+
const endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2380
|
+
return {
|
|
2381
|
+
output: "Interaction timed out before the run was resumed",
|
|
2382
|
+
run: {
|
|
2383
|
+
id: runId,
|
|
2384
|
+
sessionId: snapshot.sessionId,
|
|
2385
|
+
status: "failed",
|
|
2386
|
+
phase: "fail",
|
|
2387
|
+
userPrompt: snapshot.userPrompt,
|
|
2388
|
+
startedAt: snapshot.startedAt,
|
|
2389
|
+
endedAt,
|
|
2390
|
+
terminationReason: "interaction-timeout",
|
|
2391
|
+
activeSkillNames: snapshot.activeSkillNames,
|
|
2392
|
+
artifacts: [],
|
|
2393
|
+
trace: [{
|
|
2394
|
+
id: `evt-expired-${runId}`,
|
|
2395
|
+
runId,
|
|
2396
|
+
ts: endedAt,
|
|
2397
|
+
type: "run.failed",
|
|
2398
|
+
message: `Interaction timed out before the run was resumed (expired at ${snapshot.interactionExpiresAt})`,
|
|
2399
|
+
data: {
|
|
2400
|
+
reason: "interaction-timeout",
|
|
2401
|
+
code: "RUN_INTERACTION_TIMEOUT"
|
|
2402
|
+
}
|
|
2403
|
+
}]
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
if (!this.#catalogCache) await this.discover();
|
|
2408
|
+
const cache = this.#catalogCache;
|
|
2409
|
+
if (!cache) throw new WebSkillError("RUN_FAILED", "discover() did not populate the catalog cache");
|
|
2410
|
+
return new AgentLoop({
|
|
2411
|
+
llm: this.#deps.llm,
|
|
2412
|
+
executor: this.#deps.executor,
|
|
2413
|
+
schemaInferer: this.#deps.schemaInferer,
|
|
2414
|
+
artifactStore: this.#deps.artifactStore,
|
|
2415
|
+
fs: this.#deps.fs,
|
|
2416
|
+
skillIndex: cache.index,
|
|
2417
|
+
model: this.#deps.model,
|
|
2418
|
+
uiBridge: this.#deps.uiBridge,
|
|
2419
|
+
interaction: this.#deps.interaction,
|
|
2420
|
+
memory: this.#deps.memory,
|
|
2421
|
+
hooks: this.#deps.hooks,
|
|
2422
|
+
eventBus: this.#events,
|
|
2423
|
+
longTerm: this.#deps.longTerm,
|
|
2424
|
+
externalTools: this.#deps.externalTools,
|
|
2425
|
+
skillProviders: this.#deps.skillProviders,
|
|
2426
|
+
catalogFilter: this.#deps.catalogFilter,
|
|
2427
|
+
snapshotStore: store
|
|
2428
|
+
}, this.#deps.config).resume(snapshot);
|
|
2429
|
+
}
|
|
2040
2430
|
};
|
|
2041
2431
|
|
|
2042
2432
|
//#endregion
|
|
2043
|
-
export {
|
|
2433
|
+
export { normalizePath as $, mergeCatalogEntries as A, SKILL_MANIFEST_FILE as B, WebSkillRuntime as C, fromVercelResult as D, createScriptContext as E, schemaToForm as F, WebSkillError as G, SKILL_NAME_PATTERN as H, toLlmToolSpec as I, checkSkillRules as J, buildCatalog as K, toVercelToolSpecs as L, normalizeToolContent as M, parseBridgeRequest as N, fromVercelStreamPart as O, resolveToolName as P, jsonRenderer as Q, MemoryFS as R, TraceRecorder as S, buildRenderResult as T, SkillDiscovery as U, SKILL_NAME_MAX_LENGTH as V, SkillReader as W, escapeXml as X, computeDigest as Y, isValidSkillName as Z, ProgressiveRouter as _, CapabilityApproval as a, verifyManifest as at, READ_SKILL_FILE_TOOL_NAME as b, FsMemoryStore as c, HookRunner as d, parseSkillMarkdown as et, InMemoryStore as f, OpenAiCompatibleClient as g, MockUiBridge as h, AgentLoop as i, validateSkills as it, networkUrlHost as j, isNetworkAllowed as k, FsRunSnapshotStore as l, MockLlmClient as m, ASK_USER_TOOL as n, renderCatalogJson as nt, EventBus as o, xmlRenderer as ot, MemoryArtifactStore as p, buildManifest as q, ASK_USER_TOOL_NAME as r, resolveInsideRoot as rt, FsArtifactStore as s, ASK_USER_INPUT_SCHEMA as t, renderAvailableSkillsXml as tt, FullDisclosureRouter as u, READ_SKILL_FILE_INPUT_SCHEMA as v, bridgeError as w, RUN_SNAPSHOT_SCHEMA_VERSION as x, READ_SKILL_FILE_TOOL as y, SKILLS_LOCKFILE as z };
|