@webskill/sdk 0.19.0 → 0.21.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 +3 -3
- package/dist/agent.js +277 -61
- package/dist/{approval-Bbh_Apwg.js → approval-DwN2o2QG.js} +433 -21
- package/dist/browser.d.ts +689 -4
- package/dist/browser.js +1637 -33
- package/dist/{eventTypes-CwciaqCU.js → eventTypes-DcTQXSV1.js} +7 -1
- package/dist/governance.d.ts +9 -3
- package/dist/governance.js +1 -1
- package/dist/{index-ad11wflI.d.ts → index-B8Tgmpwh.d.ts} +2 -2
- package/dist/{index-DQ3-6GFu.d.ts → index-CYLt5u4L.d.ts} +252 -3
- package/dist/{index-BDyXe-a5.d.ts → index-DHopiH_m.d.ts} +261 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +47 -41
- package/dist/{kind-DxgS8LM-.js → kind-DaKqLX2F.js} +3 -17
- package/dist/linkedDocument-C0gj1sMq.js +245 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/node.d.ts +8 -3
- package/dist/node.js +79 -4
- package/dist/processSandboxEntry.js +5 -0
- package/dist/sandboxWorkerEntry.js +5 -0
- package/dist/{skillVersionStore-B24Jjjry-v2CCU0Xo.d.ts → skillVersionStore-B24Jjjry-BkOk9wI_.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/toolSource-C9oNuKmr.js +565 -0
- package/dist/{types-Btpdd1y--BcxQ10Fa.d.ts → types-BvTV_05--BW8zDuEk.d.ts} +54 -3
- package/dist/ui-react.d.ts +3 -3
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/untrustedText-BIaRvPZK.js +16 -0
- package/dist/{webSkillApi-CsvA69qk.js → webSkillApi-Cib6G-94.js} +1 -1
- package/package.json +6 -5
- package/dist/linkedDocument-xNXF-z8n.js +0 -94
- package/dist/types-DOJI5YC3.js +0 -90
|
@@ -3,7 +3,7 @@ import { b as parseSkillMarkdown, i as buildCatalog, n as SkillDiscovery, r as S
|
|
|
3
3
|
import { a as atomicWriteText, r as resolveInsideRoot, t as assertSafePathSegment } from "./pathSecurity-B1owvJAF.js";
|
|
4
4
|
import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
|
|
5
5
|
import { c as findUnpairedToolCalls, d as partsToText, f as promptText, l as interruptedToolResult, p as textParts, u as sealToolCallPairs } from "./llm-eIQNO9tr.js";
|
|
6
|
-
import { a as fromBase64,
|
|
6
|
+
import { a as fromBase64, g as TEXT_BUDGETED_CONTENT_TYPES, i as UNSUPPORTED_DOCUMENT_MESSAGE, m as DEFAULT_MAX_DOCUMENT_TEXT_BYTES, n as READ_LINKED_DOCUMENT_TOOL_NAME, o as toBase64, p as DEFAULT_MAX_DOCUMENT_BYTES, r as SUPPORTED_DOCUMENT_MIME, s as encodeSpreadsheet, t as READ_LINKED_DOCUMENT_TOOL, u as SPREADSHEET_MIME_TYPE } from "./linkedDocument-C0gj1sMq.js";
|
|
7
7
|
import { t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
|
|
8
8
|
import { i as isArtifactCardHidden, n as extractChartSpec, r as admitArtifactMetadata, t as buildRenderResult } from "./renderResult-D9Q-Vu2x.js";
|
|
9
9
|
import { t as extractUiSpecEvents } from "./surface-DVGiCmwq.js";
|
|
@@ -304,6 +304,24 @@ function resolveSkillDir(skillRoot, dir, relativePath) {
|
|
|
304
304
|
function createScriptContext(deps) {
|
|
305
305
|
const { fs, artifactStore, skillName, skillRoot, runId, confirm, fetchData, uploadFiles, onWarning, onArtifactCreated } = deps;
|
|
306
306
|
const readFs = fs.withRoot?.(skillRoot) ?? fs;
|
|
307
|
+
const writeArtifact = async (path, content, options) => {
|
|
308
|
+
const metadata = options?.metadata === void 0 ? void 0 : admitArtifactMetadata(options.metadata);
|
|
309
|
+
const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
|
|
310
|
+
runId,
|
|
311
|
+
path,
|
|
312
|
+
content,
|
|
313
|
+
mimeType: options?.mimeType,
|
|
314
|
+
...metadata === void 0 ? {} : { metadata }
|
|
315
|
+
}) : await artifactStore.createBinaryArtifact({
|
|
316
|
+
runId,
|
|
317
|
+
path,
|
|
318
|
+
content,
|
|
319
|
+
mimeType: options?.mimeType,
|
|
320
|
+
...metadata === void 0 ? {} : { metadata }
|
|
321
|
+
});
|
|
322
|
+
onArtifactCreated?.(artifact);
|
|
323
|
+
return artifact;
|
|
324
|
+
};
|
|
307
325
|
return {
|
|
308
326
|
skillName,
|
|
309
327
|
runId,
|
|
@@ -316,24 +334,11 @@ function createScriptContext(deps) {
|
|
|
316
334
|
async readAssetBinary(relativePath) {
|
|
317
335
|
return readFs.readBinary(resolveSkillDir(skillRoot, "assets", relativePath));
|
|
318
336
|
},
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
content,
|
|
325
|
-
mimeType: options?.mimeType,
|
|
326
|
-
...metadata === void 0 ? {} : { metadata }
|
|
327
|
-
}) : await artifactStore.createBinaryArtifact({
|
|
328
|
-
runId,
|
|
329
|
-
path,
|
|
330
|
-
content,
|
|
331
|
-
mimeType: options?.mimeType,
|
|
332
|
-
...metadata === void 0 ? {} : { metadata }
|
|
333
|
-
});
|
|
334
|
-
onArtifactCreated?.(artifact);
|
|
335
|
-
return artifact;
|
|
336
|
-
},
|
|
337
|
+
writeArtifact,
|
|
338
|
+
...deps.writeSpreadsheet ? { writeSpreadsheet: async (path, spec, options) => writeArtifact(path, await encodeSpreadsheet(spec), {
|
|
339
|
+
mimeType: SPREADSHEET_MIME_TYPE,
|
|
340
|
+
...options?.metadata === void 0 ? {} : { metadata: options.metadata }
|
|
341
|
+
}) } : {},
|
|
337
342
|
...confirm ? { confirm } : {},
|
|
338
343
|
...fetchData ? { fetchData } : {},
|
|
339
344
|
...deps.documentSurface ? { documentSurface: true } : {},
|
|
@@ -687,6 +692,90 @@ function renderCurrentTimePrompt(nowIso) {
|
|
|
687
692
|
*/
|
|
688
693
|
const ON_DEMAND_TOOLS_HINT = "Some tools are disclosed on demand and are not listed above. If a capability you need seems missing, look for a related skill in the catalog and read its SKILL.md first — activating it may reveal additional tools. Do not conclude the capability does not exist.";
|
|
689
694
|
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region ../runtime/src/engine/visionBatch.ts
|
|
697
|
+
/**
|
|
698
|
+
* 视觉委派的分批并发调度(0.21.0 分册 26 · FR-26.5 ~ FR-26.7)。
|
|
699
|
+
*
|
|
700
|
+
* 从 `agentLoop` 里拆出来,是因为它有三条彼此独立的分支——并发、慢降级、失败隔离——
|
|
701
|
+
* 塞进那个已经三千行的文件里就只能靠端到端测,而这三条恰恰是最需要单独逼出来的。
|
|
702
|
+
* @experimental
|
|
703
|
+
*/
|
|
704
|
+
/** 并发度上限。再高只会让每一张都变慢,同时把端点的速率限制撞穿(FR-26.5) @experimental */
|
|
705
|
+
const VISION_MAX_CONCURRENCY = 3;
|
|
706
|
+
/** @experimental */
|
|
707
|
+
const VISION_DEFAULT_CONCURRENCY = 2;
|
|
708
|
+
/**
|
|
709
|
+
* 超过它就退回串行(FR-26.6)。云端视觉端点单张 3–15 秒,本地 Qwen-VL 一类 20–40 秒;
|
|
710
|
+
* 越过 25 秒说明端点已经在排队,继续并发只会让第一张有用的描述来得更晚。
|
|
711
|
+
* @experimental
|
|
712
|
+
*/
|
|
713
|
+
const VISION_SLOW_THRESHOLD_MS = 25e3;
|
|
714
|
+
/** 配错了应该退化成能用,不是让 run 失败(FR-26.5) @experimental */
|
|
715
|
+
function clampVisionConcurrency(value) {
|
|
716
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 2;
|
|
717
|
+
return Math.min(3, Math.max(1, Math.round(value)));
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* 逐张委派、按并发度并行。
|
|
721
|
+
*
|
|
722
|
+
* 第一波结算后测一次墙钟决定要不要降级:测第一波而不是第一张,
|
|
723
|
+
* 因为并发下第一张返回得早纯属运气,一波的完成时间才反映端点的实际吞吐。
|
|
724
|
+
* 降级是**单向**的,本次委派内不再回升。
|
|
725
|
+
* @experimental
|
|
726
|
+
*/
|
|
727
|
+
async function runVisionBatch(options) {
|
|
728
|
+
const total = options.images.length;
|
|
729
|
+
const now = options.now ?? (() => Date.now());
|
|
730
|
+
const slowThresholdMs = options.slowThresholdMs ?? 25e3;
|
|
731
|
+
const outcome = {
|
|
732
|
+
described: new Array(total).fill(void 0),
|
|
733
|
+
failures: [],
|
|
734
|
+
declined: false
|
|
735
|
+
};
|
|
736
|
+
let cursor = 0;
|
|
737
|
+
let done = 0;
|
|
738
|
+
let concurrency = clampVisionConcurrency(options.concurrency);
|
|
739
|
+
const settle = (index, result) => {
|
|
740
|
+
if (result.outcome === "described") {
|
|
741
|
+
outcome.described[index] = result.text;
|
|
742
|
+
outcome.endpoint ??= result.endpoint;
|
|
743
|
+
} else if (result.outcome === "declined") outcome.declined = true;
|
|
744
|
+
else outcome.failures.push({
|
|
745
|
+
index,
|
|
746
|
+
reason: result.reason
|
|
747
|
+
});
|
|
748
|
+
done += 1;
|
|
749
|
+
options.onProgress?.(done, total);
|
|
750
|
+
};
|
|
751
|
+
const stopped = () => outcome.declined || options.signal?.aborted === true;
|
|
752
|
+
const one = async (index) => {
|
|
753
|
+
try {
|
|
754
|
+
settle(index, await options.describe(options.images[index], index));
|
|
755
|
+
} catch (e) {
|
|
756
|
+
settle(index, {
|
|
757
|
+
outcome: "failed",
|
|
758
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const firstWave = [];
|
|
763
|
+
const startedAt = now();
|
|
764
|
+
for (; cursor < total && firstWave.length < concurrency; cursor += 1) firstWave.push(one(cursor));
|
|
765
|
+
await Promise.all(firstWave);
|
|
766
|
+
if (now() - startedAt > slowThresholdMs) concurrency = 1;
|
|
767
|
+
const worker = async () => {
|
|
768
|
+
while (!stopped()) {
|
|
769
|
+
const index = cursor;
|
|
770
|
+
if (index >= total) return;
|
|
771
|
+
cursor += 1;
|
|
772
|
+
await one(index);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, total - cursor) }, () => worker()));
|
|
776
|
+
return outcome;
|
|
777
|
+
}
|
|
778
|
+
|
|
690
779
|
//#endregion
|
|
691
780
|
//#region ../runtime/src/trace/traceRecorder.ts
|
|
692
781
|
/** 结构化 trace 记录器;时钟与 id 工厂可注入(golden trace 用固定值保证确定性) */
|
|
@@ -1047,6 +1136,11 @@ const LLM_TURN_CODES = /* @__PURE__ */ new Set([
|
|
|
1047
1136
|
"TOOL_SCHEMA_UNAVAILABLE"
|
|
1048
1137
|
]);
|
|
1049
1138
|
/**
|
|
1139
|
+
* 端点 200 但整轮 run 没给出任何答复时的说明。
|
|
1140
|
+
* 上下文撑爆只是最常见的成因,不是判据——判据是「一句回答都没有」这个事实本身。
|
|
1141
|
+
*/
|
|
1142
|
+
const EMPTY_RESPONSE_MESSAGE = "The model produced no answer and requested no tools, so this run has nothing to show. The most common cause is a conversation that has outgrown the model context window: start a new chat, remove large attachments, or switch to a model with a larger context window.";
|
|
1143
|
+
/**
|
|
1050
1144
|
* 模型侧图片通道收得下的位图格式(三家 provider 的交集)。
|
|
1051
1145
|
* SVG 是文本、`application/octet-stream` 是未知字节——两者编成 data URL 发出去,
|
|
1052
1146
|
* 端点当场 400(LM Studio:`'url' field must be a base64 encoded image.`),
|
|
@@ -1199,6 +1293,7 @@ var AgentLoop = class {
|
|
|
1199
1293
|
askUserMaxFields: config.askUserMaxFields ?? 20,
|
|
1200
1294
|
maxDocumentBytes: config.maxDocumentBytes ?? 256e5,
|
|
1201
1295
|
maxDocumentTextBytes: config.maxDocumentTextBytes ?? 512e3,
|
|
1296
|
+
visionConcurrency: clampVisionConcurrency(config.visionConcurrency),
|
|
1202
1297
|
temperature: config.temperature,
|
|
1203
1298
|
renderResult: config.renderResult,
|
|
1204
1299
|
remoteUrl: config.remoteUrl
|
|
@@ -1421,6 +1516,7 @@ var AgentLoop = class {
|
|
|
1421
1516
|
const { llm } = this.#deps;
|
|
1422
1517
|
const { trace, messages } = state;
|
|
1423
1518
|
this.#armDeadline(state);
|
|
1519
|
+
let producedOutput = false;
|
|
1424
1520
|
try {
|
|
1425
1521
|
for (let turn = startTurn;; turn++) {
|
|
1426
1522
|
state.turn = turn;
|
|
@@ -1502,12 +1598,14 @@ var AgentLoop = class {
|
|
|
1502
1598
|
outputTokens: response.usage.outputTokens
|
|
1503
1599
|
} });
|
|
1504
1600
|
if (!response.toolCalls?.length) {
|
|
1601
|
+
if (responseText === "" && !producedOutput) return finish("failed", "llm-error", EMPTY_RESPONSE_MESSAGE, "LLM_REQUEST_FAILED");
|
|
1505
1602
|
messages.push({
|
|
1506
1603
|
role: "assistant",
|
|
1507
1604
|
content: response.content ?? []
|
|
1508
1605
|
});
|
|
1509
1606
|
return finish("completed", "final-answer", responseText);
|
|
1510
1607
|
}
|
|
1608
|
+
producedOutput = true;
|
|
1511
1609
|
await this.#lifecycle({
|
|
1512
1610
|
phase: "execute",
|
|
1513
1611
|
data: {
|
|
@@ -2126,7 +2224,10 @@ var AgentLoop = class {
|
|
|
2126
2224
|
const source = resolution.kind === "script" ? void 0 : (this.#deps.externalTools ?? []).find((s) => s.canHandle(call.name));
|
|
2127
2225
|
if (source === void 0 && !this.#checkToolAccess(state, call.name)) result = this.#deniedToolError(state, call.name);
|
|
2128
2226
|
else if (resolution.kind === "script") result = await this.#handleScriptTool(call, resolution.skillName, resolution.scriptName, state);
|
|
2129
|
-
else if (source) result = await source.call(call.name, call.arguments, {
|
|
2227
|
+
else if (source) result = await source.call(call.name, call.arguments, {
|
|
2228
|
+
runId: state.runId,
|
|
2229
|
+
signal: state.controller.signal
|
|
2230
|
+
});
|
|
2130
2231
|
else {
|
|
2131
2232
|
if (resolution.kind === "not-found") state.unknownToolCalls += 1;
|
|
2132
2233
|
result = toolError(resolution.code, resolution.message, resolution.kind === "not-found" ? resolution.data : void 0);
|
|
@@ -2811,6 +2912,131 @@ var AgentLoop = class {
|
|
|
2811
2912
|
});
|
|
2812
2913
|
return dropped > 0;
|
|
2813
2914
|
}
|
|
2915
|
+
/**
|
|
2916
|
+
* 工具产出的图 → 第二个端点认出来的文本(0.21.0 分册 17)。
|
|
2917
|
+
*
|
|
2918
|
+
* 返回 `undefined` 表示「本次不委派」,调用方原样投图——注入位缺席、
|
|
2919
|
+
* 选中模型本来就看得了图、或者这批结果里根本没有图,都走这条。
|
|
2920
|
+
* 返回值里的 `kept` 是去掉图之后仍要投出的分片(PDF 等文件分片不受影响)。
|
|
2921
|
+
*
|
|
2922
|
+
* 四种失败一律**返回可读说明**而不是抛错:静默丢图会让主模型以为自己看过了
|
|
2923
|
+
* (FR-17.5a,与 `#dropRejectedImages` 同口径)。
|
|
2924
|
+
*/
|
|
2925
|
+
async #delegateVision(state, call, carried) {
|
|
2926
|
+
const delegate = this.#deps.visionDelegate;
|
|
2927
|
+
if (delegate === void 0) return void 0;
|
|
2928
|
+
const images = carried.filter(isImagePart);
|
|
2929
|
+
if (images.length === 0) return void 0;
|
|
2930
|
+
try {
|
|
2931
|
+
if (await delegate.applies() !== true) return void 0;
|
|
2932
|
+
} catch (e) {
|
|
2933
|
+
state.trace.record("run.warning", { message: `Vision delegation availability check failed: ${messageOf(e)}` });
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const kept = carried.filter((part) => !isImagePart(part));
|
|
2937
|
+
const subject = `the ${images.length === 1 ? "image" : `${images.length} images`} from "${call.name}"`;
|
|
2938
|
+
const dropped = `The ${images.length === 1 ? "image was" : "images were"} not attached; work from the surrounding text, and say so if the answer depends on what ${images.length === 1 ? "it shows" : "they show"}.`;
|
|
2939
|
+
const finish = (note) => {
|
|
2940
|
+
state.trace.record("run.warning", {
|
|
2941
|
+
message: note,
|
|
2942
|
+
data: {
|
|
2943
|
+
images: images.length,
|
|
2944
|
+
tool: call.name
|
|
2945
|
+
}
|
|
2946
|
+
});
|
|
2947
|
+
return {
|
|
2948
|
+
kept,
|
|
2949
|
+
note
|
|
2950
|
+
};
|
|
2951
|
+
};
|
|
2952
|
+
const remainingTurns = state.maxTurns - state.turn;
|
|
2953
|
+
const remainingMs = state.totalTimeoutMs - this.#elapsed(state);
|
|
2954
|
+
const timeoutMs = Math.min(state.toolTimeoutMs * images.length, remainingMs);
|
|
2955
|
+
if (remainingTurns < 1 || timeoutMs <= 0) return finish(`The parent run has no budget left to describe ${subject} (${remainingTurns} turns and ${Math.max(0, Math.round(remainingMs))} ms remaining). ${dropped}`);
|
|
2956
|
+
const payload = images.map((part) => ({
|
|
2957
|
+
mimeType: part.mimeType,
|
|
2958
|
+
data: part.data
|
|
2959
|
+
}));
|
|
2960
|
+
const cancelled = state.controller.signal;
|
|
2961
|
+
const describeOne = async (image) => {
|
|
2962
|
+
const left = state.totalTimeoutMs - this.#elapsed(state);
|
|
2963
|
+
const budget = Math.min(state.toolTimeoutMs, left);
|
|
2964
|
+
if (budget <= 0) return {
|
|
2965
|
+
outcome: "failed",
|
|
2966
|
+
reason: "the parent run has no time budget left"
|
|
2967
|
+
};
|
|
2968
|
+
const controller = new AbortController();
|
|
2969
|
+
const cancelParent = () => controller.abort(cancelled.reason);
|
|
2970
|
+
cancelled.addEventListener("abort", cancelParent, { once: true });
|
|
2971
|
+
let timedOut = false;
|
|
2972
|
+
const timer = setTimeout(() => {
|
|
2973
|
+
timedOut = true;
|
|
2974
|
+
controller.abort(/* @__PURE__ */ new Error("vision delegation timed out"));
|
|
2975
|
+
}, budget);
|
|
2976
|
+
try {
|
|
2977
|
+
const one = await delegate.describe({
|
|
2978
|
+
images: [image],
|
|
2979
|
+
task: state.run.userPrompt,
|
|
2980
|
+
source: call.name,
|
|
2981
|
+
batch: {
|
|
2982
|
+
id: call.id,
|
|
2983
|
+
total: payload.length
|
|
2984
|
+
},
|
|
2985
|
+
timeoutMs: budget,
|
|
2986
|
+
signal: controller.signal
|
|
2987
|
+
});
|
|
2988
|
+
return timedOut ? {
|
|
2989
|
+
outcome: "failed",
|
|
2990
|
+
reason: `exceeded its ${budget} ms budget`
|
|
2991
|
+
} : one;
|
|
2992
|
+
} catch (e) {
|
|
2993
|
+
return {
|
|
2994
|
+
outcome: "failed",
|
|
2995
|
+
reason: timedOut ? `exceeded its ${budget} ms budget` : messageOf(e)
|
|
2996
|
+
};
|
|
2997
|
+
} finally {
|
|
2998
|
+
clearTimeout(timer);
|
|
2999
|
+
cancelled.removeEventListener("abort", cancelParent);
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
const batch = await runVisionBatch({
|
|
3003
|
+
images: payload,
|
|
3004
|
+
concurrency: clampVisionConcurrency(this.#config.visionConcurrency),
|
|
3005
|
+
describe: describeOne,
|
|
3006
|
+
signal: cancelled,
|
|
3007
|
+
onProgress: (progressed, total) => {
|
|
3008
|
+
this.#lifecycle({
|
|
3009
|
+
phase: "execute",
|
|
3010
|
+
data: {
|
|
3011
|
+
kind: "vision-progress",
|
|
3012
|
+
source: call.name,
|
|
3013
|
+
done: progressed,
|
|
3014
|
+
total
|
|
3015
|
+
}
|
|
3016
|
+
}, state);
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
if (batch.declined) return finish(`The user declined to send ${subject} to another provider for description. ${dropped}`);
|
|
3020
|
+
const describedCount = batch.described.filter((text) => text !== void 0).length;
|
|
3021
|
+
if (describedCount === 0) return finish(`Due to image recognition errors, ${subject} could not be read: ${batch.failures[0]?.reason ?? "no description was produced"}. ${dropped}`);
|
|
3022
|
+
const lines = batch.described.map((text, index) => {
|
|
3023
|
+
const failure = batch.failures.find((entry) => entry.index === index);
|
|
3024
|
+
const body = text ?? `not read due to image recognition errors: ${failure?.reason ?? "the endpoint gave no reason"}`;
|
|
3025
|
+
return images.length === 1 ? body : `[${index + 1}/${images.length}] ${body}`;
|
|
3026
|
+
});
|
|
3027
|
+
if (describedCount < images.length) state.trace.record("run.warning", {
|
|
3028
|
+
message: `Due to image recognition errors, ${images.length - describedCount} of ${images.length} images from "${call.name}" were not read.`,
|
|
3029
|
+
data: {
|
|
3030
|
+
images: images.length,
|
|
3031
|
+
described: describedCount,
|
|
3032
|
+
tool: call.name
|
|
3033
|
+
}
|
|
3034
|
+
});
|
|
3035
|
+
return {
|
|
3036
|
+
kept,
|
|
3037
|
+
note: `--- ${images.length === 1 ? "Image" : `${images.length} images`} from "${call.name}", described by ${batch.endpoint ?? "another model"} because the assistant model cannot read images ---\n${lines.join("\n")}`
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
2814
3040
|
/** 单个 PDF 分片 → 文本;包装形态与 chatEngine 的附件标签同口径(FR-22.3 / FR-22.4) */
|
|
2815
3041
|
async #pdfAsText(state, part, extract) {
|
|
2816
3042
|
const name = part.name ?? "document.pdf";
|
|
@@ -2906,6 +3132,12 @@ var AgentLoop = class {
|
|
|
2906
3132
|
state.trace.record("run.warning", { message: note });
|
|
2907
3133
|
notes.push(note);
|
|
2908
3134
|
}
|
|
3135
|
+
const delegated = await this.#delegateVision(state, call, carried);
|
|
3136
|
+
if (delegated !== void 0) {
|
|
3137
|
+
carried.length = 0;
|
|
3138
|
+
carried.push(...delegated.kept);
|
|
3139
|
+
notes.push(delegated.note);
|
|
3140
|
+
}
|
|
2909
3141
|
return {
|
|
2910
3142
|
tool: [
|
|
2911
3143
|
{
|
|
@@ -3193,6 +3425,7 @@ var AgentLoop = class {
|
|
|
3193
3425
|
confirm: (message) => this.#confirm(message, state),
|
|
3194
3426
|
...this.#deps.fetchData ? { fetchData: this.#deps.fetchData } : {},
|
|
3195
3427
|
...this.#deps.documentSurface ? { documentSurface: true } : {},
|
|
3428
|
+
...this.#deps.writeSpreadsheet ? { writeSpreadsheet: true } : {},
|
|
3196
3429
|
...this.#deps.uploadFiles ? { uploadFiles: this.#uploadFilePort(this.#deps.uploadFiles, skillName, state) } : {},
|
|
3197
3430
|
onWarning: (message) => state.trace.record("run.warning", { message }),
|
|
3198
3431
|
onArtifactCreated: (artifact) => createdIds.add(artifact.id)
|
|
@@ -3535,6 +3768,148 @@ var AgentLoop = class {
|
|
|
3535
3768
|
}
|
|
3536
3769
|
};
|
|
3537
3770
|
|
|
3771
|
+
//#endregion
|
|
3772
|
+
//#region ../runtime/src/engine/compaction.ts
|
|
3773
|
+
/** 摘要消息的固定前缀:压缩留痕靠它识别,UI 与后续压缩都读这个标记 */
|
|
3774
|
+
const COMPACTION_PREFIX = "[Earlier conversation summary]";
|
|
3775
|
+
/** 触发比例默认值:留出四分之一余量,够装下这一轮的提问与工具结果 */
|
|
3776
|
+
const DEFAULT_COMPACTION_THRESHOLD = .75;
|
|
3777
|
+
/** 压缩后至少保留的原文消息数 */
|
|
3778
|
+
const DEFAULT_KEEP_RECENT_MESSAGES = 12;
|
|
3779
|
+
/** 保留原文的下界:压到比这更少就等于换了个会话(FR-20.6 第 3 条) */
|
|
3780
|
+
const MIN_KEEP_RECENT_MESSAGES = 6;
|
|
3781
|
+
/**
|
|
3782
|
+
* 要不要压缩。三个输入缺一不压(FR-20.6 第 4 条的「不猜」):
|
|
3783
|
+
* 没有窗口就没有分母,没有真实用量就没有分子——两者都不许估算。
|
|
3784
|
+
*/
|
|
3785
|
+
function shouldCompact(lastInputTokens, contextWindow, threshold) {
|
|
3786
|
+
if (lastInputTokens === void 0 || contextWindow === void 0) return false;
|
|
3787
|
+
if (!Number.isFinite(lastInputTokens) || !Number.isFinite(contextWindow) || contextWindow <= 0) return false;
|
|
3788
|
+
return lastInputTokens / contextWindow >= threshold;
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* 把想要的切点挪到不破坏 tool 契约的位置:
|
|
3792
|
+
* 切点之后不得以孤儿 `tool` 消息开头(它的 assistant 调用会被压掉),
|
|
3793
|
+
* 切点之前不得以未应答的 assistant `toolCalls` 结尾。
|
|
3794
|
+
*
|
|
3795
|
+
* 与 `trimSessionHistory` 是同一套边界规则,区别只在这里要的是**一个**切点。
|
|
3796
|
+
* 返回 0 表示没有安全切点,调用方应当放弃本次压缩。
|
|
3797
|
+
*/
|
|
3798
|
+
function alignCut(messages, desired) {
|
|
3799
|
+
let cut = Math.max(0, Math.min(desired, messages.length));
|
|
3800
|
+
while (cut < messages.length && messages[cut].role === "tool") cut += 1;
|
|
3801
|
+
if (cut >= messages.length) return 0;
|
|
3802
|
+
while (cut > 0) {
|
|
3803
|
+
const last = messages[cut - 1];
|
|
3804
|
+
if (last.role === "assistant" && last.toolCalls?.length) cut -= 1;
|
|
3805
|
+
else break;
|
|
3806
|
+
}
|
|
3807
|
+
return cut;
|
|
3808
|
+
}
|
|
3809
|
+
/** 把一段消息压成给模型看的纯文本转录;图片与文件只留占位,不塞 base64 */
|
|
3810
|
+
function transcribe(messages) {
|
|
3811
|
+
return messages.map((message) => {
|
|
3812
|
+
const body = message.content.map((part) => {
|
|
3813
|
+
if (part.type === "text") return part.text;
|
|
3814
|
+
if (part.type === "image") return "[image]";
|
|
3815
|
+
return `[file${part.name !== void 0 ? ` ${part.name}` : ""}]`;
|
|
3816
|
+
}).join("\n").trim();
|
|
3817
|
+
const calls = message.toolCalls?.map((call) => `[called ${call.name}]`).join(" ") ?? "";
|
|
3818
|
+
return `${message.role.toUpperCase()}: ${[body, calls].filter((s) => s !== "").join(" ")}`.trim();
|
|
3819
|
+
}).join("\n");
|
|
3820
|
+
}
|
|
3821
|
+
const SUMMARY_INSTRUCTION = "Summarize the conversation transcript below so that another assistant can continue the conversation without seeing the original messages. Keep every fact, decision, user preference, identifier, number and unfinished task. Drop greetings, acknowledgements and repetition. Write plain prose, no headings, no bullet lists, under 400 words. Do not add information that is not in the transcript.";
|
|
3822
|
+
/**
|
|
3823
|
+
* 压缩会话历史:最早的一段换成一条摘要消息,最近若干条保留原文。
|
|
3824
|
+
*
|
|
3825
|
+
* 返回 `undefined` 表示**没压**——历史还不够长、找不到安全切点、或摘要调用失败。
|
|
3826
|
+
* 压缩是优化,任何一步不顺都退回原历史,不能把一次正常提问变成一次失败(FR-20.6 第 5 条)。
|
|
3827
|
+
*
|
|
3828
|
+
* 摘要用 `user` 角色而不是 `system`:会话历史里本就不含 system(每轮 run 由路由重起),
|
|
3829
|
+
* 且 Anthropic 不接受非首条的 system 消息。
|
|
3830
|
+
*/
|
|
3831
|
+
async function compactHistory(history, options) {
|
|
3832
|
+
const keep = Math.max(6, options.keepRecentMessages ?? 12);
|
|
3833
|
+
const cut = alignCut(history, history.length - keep);
|
|
3834
|
+
if (cut <= 0) return void 0;
|
|
3835
|
+
const head = history.slice(0, cut);
|
|
3836
|
+
const tail = history.slice(cut);
|
|
3837
|
+
let summary;
|
|
3838
|
+
try {
|
|
3839
|
+
summary = ((await options.llm.complete({ messages: [{
|
|
3840
|
+
role: "system",
|
|
3841
|
+
content: [{
|
|
3842
|
+
type: "text",
|
|
3843
|
+
text: SUMMARY_INSTRUCTION
|
|
3844
|
+
}]
|
|
3845
|
+
}, {
|
|
3846
|
+
role: "user",
|
|
3847
|
+
content: [{
|
|
3848
|
+
type: "text",
|
|
3849
|
+
text: transcribe(head)
|
|
3850
|
+
}]
|
|
3851
|
+
}] })).content ?? []).map((part) => part.type === "text" ? part.text : "").join("").trim();
|
|
3852
|
+
} catch (e) {
|
|
3853
|
+
options.onWarn?.(`compaction summary request failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
if (summary === "") {
|
|
3857
|
+
options.onWarn?.("compaction summary was empty; history left untouched");
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
return {
|
|
3861
|
+
messages: [{
|
|
3862
|
+
role: "user",
|
|
3863
|
+
content: [{
|
|
3864
|
+
type: "text",
|
|
3865
|
+
text: `${COMPACTION_PREFIX}\n${summary}`
|
|
3866
|
+
}]
|
|
3867
|
+
}, ...tail],
|
|
3868
|
+
removed: head.length,
|
|
3869
|
+
kept: tail.length
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
//#endregion
|
|
3874
|
+
//#region ../runtime/src/engine/usage.ts
|
|
3875
|
+
/**
|
|
3876
|
+
* 从 run 的 trace 聚合大模型用量(请求次数 + 输入/输出 token)。
|
|
3877
|
+
* 与 `summarizeToolCalls` 同型:消费方不再各自遍历 trace。
|
|
3878
|
+
* @stable
|
|
3879
|
+
*/
|
|
3880
|
+
function summarizeRunUsage(trace) {
|
|
3881
|
+
let llmCalls = 0;
|
|
3882
|
+
let inputTokens = 0;
|
|
3883
|
+
let outputTokens = 0;
|
|
3884
|
+
let hasUsage = false;
|
|
3885
|
+
let lastInputTokens;
|
|
3886
|
+
for (const event of trace) {
|
|
3887
|
+
if (event.type === "llm.request") {
|
|
3888
|
+
llmCalls += 1;
|
|
3889
|
+
continue;
|
|
3890
|
+
}
|
|
3891
|
+
if (event.type !== "llm.usage") continue;
|
|
3892
|
+
const input = event.data?.["inputTokens"];
|
|
3893
|
+
const output = event.data?.["outputTokens"];
|
|
3894
|
+
const hasInput = typeof input === "number" && Number.isFinite(input);
|
|
3895
|
+
const hasOutput = typeof output === "number" && Number.isFinite(output);
|
|
3896
|
+
if (hasInput) {
|
|
3897
|
+
inputTokens += input;
|
|
3898
|
+
lastInputTokens = input;
|
|
3899
|
+
}
|
|
3900
|
+
if (hasOutput) outputTokens += output;
|
|
3901
|
+
if (hasInput || hasOutput) hasUsage = true;
|
|
3902
|
+
}
|
|
3903
|
+
return {
|
|
3904
|
+
llmCalls,
|
|
3905
|
+
...hasUsage ? {
|
|
3906
|
+
inputTokens,
|
|
3907
|
+
outputTokens
|
|
3908
|
+
} : {},
|
|
3909
|
+
...lastInputTokens !== void 0 ? { lastInputTokens } : {}
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3538
3913
|
//#endregion
|
|
3539
3914
|
//#region ../runtime/src/engine/runtime.ts
|
|
3540
3915
|
/**
|
|
@@ -3604,24 +3979,46 @@ var WebSkillRuntime = class {
|
|
|
3604
3979
|
* 既有 runtime.run(prompt) 保持无状态单次语义不变。
|
|
3605
3980
|
* 同 handle 并发 run 经 per-handle 队列串行化(防历史 last-writer-wins 丢轮次);
|
|
3606
3981
|
* maxHistoryMessages(默认取 `DEFAULT_LOOP_LIMITS`)超出时滚动裁剪中段(保留首尾;边界对齐 tool 契约)。
|
|
3982
|
+
*
|
|
3983
|
+
* 配了 `contextWindow` 时另加一道**主动压缩**(分册 20 FR-20.6):按上一轮的真实
|
|
3984
|
+
* `prompt_tokens` 与窗口的比值判定,超阈值就在下一轮发起前把最早的一段摘要掉。
|
|
3985
|
+
* 三个输入(窗口、用量、足够长的历史)缺一不压——不估算、不猜。
|
|
3607
3986
|
*/
|
|
3608
3987
|
createSession(options = {}) {
|
|
3609
3988
|
const sessionId = options.sessionId ?? `session-${Math.random().toString(36).slice(2, 10)}`;
|
|
3610
3989
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3611
3990
|
const maxHistory = options.maxHistoryMessages ?? DEFAULT_LOOP_LIMITS.maxHistoryMessages;
|
|
3991
|
+
const threshold = options.compactionThreshold ?? .75;
|
|
3612
3992
|
let history = [];
|
|
3613
3993
|
let queue = Promise.resolve();
|
|
3994
|
+
/** 上一轮结束时的上下文水位线;没拿到 usage 就一直是 undefined,届时不压缩 */
|
|
3995
|
+
let lastInputTokens;
|
|
3614
3996
|
return {
|
|
3615
3997
|
id: sessionId,
|
|
3616
3998
|
createdAt,
|
|
3617
3999
|
run: (prompt, runOptions = {}) => {
|
|
3618
4000
|
const result = queue.then(async () => {
|
|
4001
|
+
if (shouldCompact(lastInputTokens, options.contextWindow, threshold)) {
|
|
4002
|
+
const compacted = await compactHistory(history, {
|
|
4003
|
+
llm: this.#deps.llm,
|
|
4004
|
+
...options.keepRecentMessages !== void 0 ? { keepRecentMessages: options.keepRecentMessages } : {}
|
|
4005
|
+
});
|
|
4006
|
+
if (compacted) {
|
|
4007
|
+
history = compacted.messages;
|
|
4008
|
+
lastInputTokens = void 0;
|
|
4009
|
+
options.onCompacted?.({
|
|
4010
|
+
removed: compacted.removed,
|
|
4011
|
+
kept: compacted.kept
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
3619
4015
|
const r = await this.run(prompt, {
|
|
3620
4016
|
sessionId,
|
|
3621
4017
|
history,
|
|
3622
4018
|
...runOptions
|
|
3623
4019
|
});
|
|
3624
4020
|
history = trimSessionHistory(r.messages.slice(1), maxHistory);
|
|
4021
|
+
lastInputTokens = summarizeRunUsage(r.run.trace).lastInputTokens;
|
|
3625
4022
|
return r;
|
|
3626
4023
|
});
|
|
3627
4024
|
queue = result.then(() => void 0, () => void 0);
|
|
@@ -3686,8 +4083,10 @@ var WebSkillRuntime = class {
|
|
|
3686
4083
|
docxExtractor: this.#deps.docxExtractor,
|
|
3687
4084
|
xlsxExtractor: this.#deps.xlsxExtractor,
|
|
3688
4085
|
pdfExtractor: this.#deps.pdfExtractor,
|
|
4086
|
+
visionDelegate: this.#deps.visionDelegate,
|
|
3689
4087
|
documentAudit: this.#deps.documentAudit,
|
|
3690
4088
|
documentSurface: this.#deps.documentSurface,
|
|
4089
|
+
writeSpreadsheet: this.#deps.writeSpreadsheet,
|
|
3691
4090
|
uploadFileAudit: this.#deps.uploadFileAudit,
|
|
3692
4091
|
maxUploadFileBytes: this.#deps.maxUploadFileBytes,
|
|
3693
4092
|
uploadFiles: options.uploadFiles
|
|
@@ -3819,8 +4218,10 @@ var WebSkillRuntime = class {
|
|
|
3819
4218
|
docxExtractor: this.#deps.docxExtractor,
|
|
3820
4219
|
xlsxExtractor: this.#deps.xlsxExtractor,
|
|
3821
4220
|
pdfExtractor: this.#deps.pdfExtractor,
|
|
4221
|
+
visionDelegate: this.#deps.visionDelegate,
|
|
3822
4222
|
documentAudit: this.#deps.documentAudit,
|
|
3823
4223
|
documentSurface: this.#deps.documentSurface,
|
|
4224
|
+
writeSpreadsheet: this.#deps.writeSpreadsheet,
|
|
3824
4225
|
uploadFileAudit: this.#deps.uploadFileAudit,
|
|
3825
4226
|
maxUploadFileBytes: this.#deps.maxUploadFileBytes
|
|
3826
4227
|
}, this.#deps.config);
|
|
@@ -4030,6 +4431,17 @@ function parseBridgeRequest(data) {
|
|
|
4030
4431
|
..."metadata" in data ? { metadata: data["metadata"] } : {}
|
|
4031
4432
|
};
|
|
4032
4433
|
}
|
|
4434
|
+
case "writeSpreadsheet": {
|
|
4435
|
+
const { path, spec } = data;
|
|
4436
|
+
if (!isNonEmptyString(path) || !isRecord(spec)) return void 0;
|
|
4437
|
+
return {
|
|
4438
|
+
kind,
|
|
4439
|
+
id,
|
|
4440
|
+
path,
|
|
4441
|
+
spec,
|
|
4442
|
+
..."metadata" in data ? { metadata: data["metadata"] } : {}
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4033
4445
|
case "confirm": return isNonEmptyString(data["message"]) ? {
|
|
4034
4446
|
kind,
|
|
4035
4447
|
id,
|
|
@@ -4255,4 +4667,4 @@ var CapabilityApproval = class CapabilityApproval {
|
|
|
4255
4667
|
};
|
|
4256
4668
|
|
|
4257
4669
|
//#endregion
|
|
4258
|
-
export {
|
|
4670
|
+
export { READ_SKILL_FILE_TOOL as $, BEHAVIOR_RECORDS_KEY as A, renderUserProfileContext as B, extractTodoTraceEvents as C, VISION_SLOW_THRESHOLD_MS as D, VISION_MAX_CONCURRENCY as E, readProfileEntries as F, schemaToForm as G, EMPTY_USER_PROFILE as H, readUserProfile as I, ASK_USER_INPUT_SCHEMA as J, createScriptContext as K, sampleBehaviorRecords as L, appendBehaviorRecords as M, mergeProfileEntries as N, clampVisionConcurrency as O, readBehaviorRecords as P, READ_SKILL_FILE_INPUT_SCHEMA as Q, USER_PROFILE_NO_INVENTION_RULE as R, extractSkillCandidate as S, VISION_DEFAULT_CONCURRENCY as T, SerializingMemoryStore as U, DEFAULT_USER_PROFILE_LIMITS as V, EventBus as W, ASK_USER_TOOL as X, ASK_USER_MAX_FIELDS as Y, ASK_USER_TOOL_NAME as Z, FsRunSnapshotStore as _, networkPolicyLibSource as a, scriptToolName as at, DEFAULT_LOOP_LIMITS as b, bridgeError as c, ProgressiveRouter as ct, FsMemoryStore as d, xmlRenderer as dt, READ_SKILL_FILE_TOOL_NAME as et, WebSkillRuntime as f, redactToolStepArgs as g, SENSITIVE_ANNOTATION as h, isNetworkAllowed as i, schemaSourceLabel as it, USER_PROFILE_KEY as j, runVisionBatch as k, parseBridgeRequest as l, escapeXml as lt, AgentLoop as m, normalizeErrorCode as n, formatSkillScriptManifest as nt, networkUrlHost as o, resolveToolName as ot, summarizeRunUsage as p, ASK_USER_FIELD_TYPES as q, normalizeToolError as r, listSkillScripts as rt, UPLOAD_FILES_UNAVAILABLE as s, toLlmToolSpec as st, CapabilityApproval as t, ALLOWED_TOOLS_EXCLUSION_REASON as tt, FsArtifactStore as u, renderAvailableSkillsXml as ut, RUN_SNAPSHOT_SCHEMA_VERSION as v, TraceRecorder as w, MAX_TOOL_STEP_ARG_BYTES as x, isUnsupportedRunSnapshot as y, USER_PROFILE_PROMPT_HEADER as z };
|