@eddyskywalker/dsh-chatgpt-subscription 0.58.0 → 0.60.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/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/lib/client.js +1714 -255
- package/lib/client.js.map +1 -1
- package/lib/codemode.js +1952 -0
- package/lib/index.js +587 -243
- package/lib/llm-compat-u9mor0uu.js +110 -0
- package/lib/types/client/CodexImageToolView.d.ts.map +1 -1
- package/lib/types/client/codemode/CodeRunToolView.d.ts +68 -0
- package/lib/types/client/codemode/CodeRunToolView.d.ts.map +1 -0
- package/lib/types/client/codemode/highlight.d.ts +25 -0
- package/lib/types/client/codemode/highlight.d.ts.map +1 -0
- package/lib/types/client/codemode/styles.d.ts +11 -0
- package/lib/types/client/codemode/styles.d.ts.map +1 -0
- package/lib/types/client/codemode/tool-name.d.ts +10 -0
- package/lib/types/client/codemode/tool-name.d.ts.map +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales-ru/hub.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +23 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/progress-updates/progress-row.d.ts.map +1 -1
- package/lib/types/client/styles.d.ts.map +1 -1
- package/lib/types/compat.d.ts +10 -1
- package/lib/types/compat.d.ts.map +1 -1
- package/lib/types/entry-codemode.d.ts +37 -0
- package/lib/types/entry-codemode.d.ts.map +1 -0
- package/lib/types/host/agent-preset.d.ts +24 -2
- package/lib/types/host/agent-preset.d.ts.map +1 -1
- package/lib/types/host/antigravity/image-tool.d.ts +10 -2
- package/lib/types/host/antigravity/image-tool.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/install.d.ts +10 -4
- package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/projection.d.ts +17 -0
- package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
- package/lib/types/host/codemode/discovery.d.ts +33 -0
- package/lib/types/host/codemode/discovery.d.ts.map +1 -0
- package/lib/types/host/codemode/executor.d.ts +140 -0
- package/lib/types/host/codemode/executor.d.ts.map +1 -0
- package/lib/types/host/codemode/retention.d.ts +34 -0
- package/lib/types/host/codemode/retention.d.ts.map +1 -0
- package/lib/types/host/codemode/rules.d.ts +193 -0
- package/lib/types/host/codemode/rules.d.ts.map +1 -0
- package/lib/types/host/codemode/scope.d.ts +59 -0
- package/lib/types/host/codemode/scope.d.ts.map +1 -0
- package/lib/types/host/codemode/transport.d.ts +299 -0
- package/lib/types/host/codemode/transport.d.ts.map +1 -0
- package/lib/types/host/codex-images.d.ts +12 -0
- package/lib/types/host/codex-images.d.ts.map +1 -1
- package/lib/types/host/command-code/model-catalog.d.ts +14 -14
- package/lib/types/host/command-code/model-catalog.d.ts.map +1 -1
- package/lib/types/host/command-code/routes.d.ts.map +1 -1
- package/lib/types/host/preset-sync.d.ts +19 -4
- package/lib/types/host/preset-sync.d.ts.map +1 -1
- package/package.json +6 -1
- package/presets/dispatch/README.md +13 -6
- package/presets/dispatch/agent.cordis.yml +19 -16
- package/presets/dispatch/preset.yml +1 -1
package/lib/codemode.js
ADDED
|
@@ -0,0 +1,1952 @@
|
|
|
1
|
+
import { n as PLUGIN_MESSAGE_SOURCE_KIND } from "./llm-compat-u9mor0uu.js";
|
|
2
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { NativeToolExecutor } from "@deepseek-ai/dsh-tool-executor-native";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
//#region src/host/codemode/discovery.ts
|
|
6
|
+
/**
|
|
7
|
+
* 造三个发现工具(外加可选的 `code_debug`)。
|
|
8
|
+
*
|
|
9
|
+
* @param rules - 已归一化的规则。
|
|
10
|
+
* @param catalog - 当前清单的读取函数。
|
|
11
|
+
* @param debug - 调试信息读取函数(`code_debug` 用)。
|
|
12
|
+
* @returns 原始工具定义数组。
|
|
13
|
+
*/
|
|
14
|
+
function createDiscoveryDefinitions(rules, catalog, debug) {
|
|
15
|
+
const [listTool, searchTool, describeTool, debugTool] = rules.options.metaTools;
|
|
16
|
+
const definitions = [];
|
|
17
|
+
if (listTool !== void 0) definitions.push(listDefinition(listTool, catalog));
|
|
18
|
+
if (searchTool !== void 0) definitions.push(searchDefinition(searchTool, rules, catalog));
|
|
19
|
+
if (describeTool !== void 0) definitions.push(describeDefinition(describeTool, catalog));
|
|
20
|
+
if (rules.options.debug && debugTool !== void 0) definitions.push(debugDefinition(debugTool, debug));
|
|
21
|
+
return definitions;
|
|
22
|
+
}
|
|
23
|
+
/** `code_tools_list`:整份清单(含被遮蔽的)。 */
|
|
24
|
+
function listDefinition(name, catalog) {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
description: "List every tool this session can call from a program, including the ones hidden from the model list.",
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: { namespace: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Only tools of this namespace (MCP server name, or \"plugin\")."
|
|
34
|
+
} }
|
|
35
|
+
},
|
|
36
|
+
output: {
|
|
37
|
+
schema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
required: ["tools"],
|
|
41
|
+
properties: { tools: {
|
|
42
|
+
type: "array",
|
|
43
|
+
items: {
|
|
44
|
+
type: "object",
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
required: [
|
|
47
|
+
"name",
|
|
48
|
+
"description",
|
|
49
|
+
"namespace"
|
|
50
|
+
],
|
|
51
|
+
properties: {
|
|
52
|
+
name: { type: "string" },
|
|
53
|
+
description: { type: "string" },
|
|
54
|
+
namespace: { type: "string" }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} }
|
|
58
|
+
},
|
|
59
|
+
render: (_args, value) => [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: value.tools.map((hit) => hit.name + " [" + hit.namespace + "] " + hit.description).join("\n")
|
|
62
|
+
}]
|
|
63
|
+
},
|
|
64
|
+
async execute(args) {
|
|
65
|
+
const namespace = namespaceOf(args);
|
|
66
|
+
return { tools: catalog().filter((entry) => namespace === void 0 || namespaceOfName(entry.name) === namespace).map((entry) => ({
|
|
67
|
+
name: entry.name,
|
|
68
|
+
description: clip(entry.description),
|
|
69
|
+
namespace: namespaceOfName(entry.name)
|
|
70
|
+
})) };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** `code_tools_search`:按名字 / 描述排序检索,返回完整参数 schema。 */
|
|
75
|
+
function searchDefinition(name, rules, catalog) {
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
description: "Rank callable tools against a free-text query and return their full parameter schemas. The intended first step before calling a hidden tool.",
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
required: ["query"],
|
|
83
|
+
properties: {
|
|
84
|
+
query: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "What you need, in words."
|
|
87
|
+
},
|
|
88
|
+
limit: {
|
|
89
|
+
type: "number",
|
|
90
|
+
description: "Maximum rows (default 8)."
|
|
91
|
+
},
|
|
92
|
+
namespace: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Only tools of this namespace."
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
output: {
|
|
99
|
+
schema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
required: ["tools"],
|
|
103
|
+
properties: {
|
|
104
|
+
fallback: {
|
|
105
|
+
type: "boolean",
|
|
106
|
+
description: "True when nothing matched and the rows are a namespace browse instead."
|
|
107
|
+
},
|
|
108
|
+
tools: {
|
|
109
|
+
type: "array",
|
|
110
|
+
items: {
|
|
111
|
+
type: "object",
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
required: [
|
|
114
|
+
"name",
|
|
115
|
+
"description",
|
|
116
|
+
"parameters"
|
|
117
|
+
],
|
|
118
|
+
properties: {
|
|
119
|
+
name: { type: "string" },
|
|
120
|
+
description: { type: "string" },
|
|
121
|
+
parameters: {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
render: (_args, value) => [{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: value.tools.length === 0 ? "No tool matched that query, and this session has no tools to browse." : (value.fallback === true ? "No exact match. These are the callable tools, grouped by namespace — call one by exact name, or refine the query:" : "") + value.tools.map((hit) => hit.name + "\n " + hit.description + "\n parameters: " + safeJson(hit.parameters)).join("\n\n")
|
|
130
|
+
}]
|
|
131
|
+
},
|
|
132
|
+
async execute(args) {
|
|
133
|
+
const record = typeof args === "object" && args !== null ? args : {};
|
|
134
|
+
const query = typeof record["query"] === "string" ? record["query"] : "";
|
|
135
|
+
const rawLimit = record["limit"];
|
|
136
|
+
const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 8;
|
|
137
|
+
const namespace = namespaceOf(args);
|
|
138
|
+
const candidates = catalog().filter((entry) => namespace === void 0 || namespaceOfName(entry.name) === namespace);
|
|
139
|
+
const terms = [...new Set(tokenize(query))];
|
|
140
|
+
const { indexed, documentFrequency } = buildIndex(candidates);
|
|
141
|
+
const scored = [];
|
|
142
|
+
for (const item of indexed) {
|
|
143
|
+
const score = scoreOf(item, terms, documentFrequency, indexed.length, query);
|
|
144
|
+
if (terms.length > 0 && score <= 0) continue;
|
|
145
|
+
scored.push({
|
|
146
|
+
indexed: item,
|
|
147
|
+
score
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
scored.sort((left, right) => right.score - left.score || left.indexed.entry.name.localeCompare(right.indexed.entry.name));
|
|
151
|
+
const hits = scored.slice(0, limit);
|
|
152
|
+
if (hits.length > 0) return { tools: hits.map((row) => ({
|
|
153
|
+
name: row.indexed.entry.name,
|
|
154
|
+
description: clip(row.indexed.entry.description),
|
|
155
|
+
parameters: row.indexed.entry.parameters
|
|
156
|
+
})) };
|
|
157
|
+
return {
|
|
158
|
+
tools: browseByNamespace(candidates, limit),
|
|
159
|
+
fallback: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** `code_tools_describe`:一个工具的完整描述与参数 schema。 */
|
|
165
|
+
function describeDefinition(name, catalog) {
|
|
166
|
+
return {
|
|
167
|
+
name,
|
|
168
|
+
description: "Describe one callable tool by exact name: its description and complete parameter schema.",
|
|
169
|
+
parameters: {
|
|
170
|
+
type: "object",
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
required: ["name"],
|
|
173
|
+
properties: { name: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Exact tool name."
|
|
176
|
+
} }
|
|
177
|
+
},
|
|
178
|
+
output: {
|
|
179
|
+
schema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
additionalProperties: false,
|
|
182
|
+
required: ["found"],
|
|
183
|
+
properties: {
|
|
184
|
+
found: { type: "boolean" },
|
|
185
|
+
name: { type: "string" },
|
|
186
|
+
description: { type: "string" },
|
|
187
|
+
parameters: {}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
render: (_args, value) => [{
|
|
191
|
+
type: "text",
|
|
192
|
+
text: value.found !== true ? "No tool named that is callable from this session." : String(value.name) + "\n" + String(value.description) + "\nparameters: " + safeJson(value.parameters)
|
|
193
|
+
}]
|
|
194
|
+
},
|
|
195
|
+
async execute(args) {
|
|
196
|
+
const record = typeof args === "object" && args !== null ? args : {};
|
|
197
|
+
const wanted = typeof record["name"] === "string" ? record["name"] : "";
|
|
198
|
+
const entry = catalog().find((row) => row.name === wanted);
|
|
199
|
+
if (entry === void 0) return { found: false };
|
|
200
|
+
return {
|
|
201
|
+
found: true,
|
|
202
|
+
name: entry.name,
|
|
203
|
+
description: clip(entry.description),
|
|
204
|
+
parameters: entry.parameters
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** `code_debug`:最近若干次 describe 决策(只在程序内可见)。 */
|
|
210
|
+
function debugDefinition(name, debug) {
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
description: "Read the last few catalog decisions of this preset (visible vs hidden tool names). Diagnostics only.",
|
|
214
|
+
parameters: {
|
|
215
|
+
type: "object",
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
properties: { limit: {
|
|
218
|
+
type: "number",
|
|
219
|
+
description: "Maximum entries (default 5)."
|
|
220
|
+
} }
|
|
221
|
+
},
|
|
222
|
+
output: {
|
|
223
|
+
schema: {
|
|
224
|
+
type: "object",
|
|
225
|
+
additionalProperties: false,
|
|
226
|
+
required: ["entries"],
|
|
227
|
+
properties: { entries: {
|
|
228
|
+
type: "array",
|
|
229
|
+
items: {}
|
|
230
|
+
} }
|
|
231
|
+
},
|
|
232
|
+
render: (_args, value) => [{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: safeJson(value.entries)
|
|
235
|
+
}]
|
|
236
|
+
},
|
|
237
|
+
async execute(args) {
|
|
238
|
+
const record = typeof args === "object" && args !== null ? args : {};
|
|
239
|
+
const limit = typeof record["limit"] === "number" && record["limit"] > 0 ? Math.floor(record["limit"]) : 5;
|
|
240
|
+
const all = debug();
|
|
241
|
+
return { entries: Array.isArray(all) ? all.slice(-limit) : [all] };
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/** 读一个可选 namespace 参数。 */
|
|
246
|
+
function namespaceOf(args) {
|
|
247
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
248
|
+
const value = args["namespace"];
|
|
249
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 零命中时的兜底浏览:按 namespace 分组,每组给出若干名字。
|
|
253
|
+
*
|
|
254
|
+
* 一次失败的检索不该让模型空手而归——它多半只会换词再搜一次,或者干脆瞎猜名字。
|
|
255
|
+
* 这里直接给出分好组的清单,等于把「再搜」换成「挑一个」。
|
|
256
|
+
*
|
|
257
|
+
* @param entries - 当前清单。
|
|
258
|
+
* @param limit - 总行数上限。
|
|
259
|
+
* @returns 与命中同形的行。
|
|
260
|
+
*/
|
|
261
|
+
function browseByNamespace(entries, limit) {
|
|
262
|
+
const groups = /* @__PURE__ */ new Map();
|
|
263
|
+
for (const entry of entries) {
|
|
264
|
+
const namespace = namespaceOfName(entry.name);
|
|
265
|
+
const bucket = groups.get(namespace);
|
|
266
|
+
if (bucket === void 0) groups.set(namespace, [entry]);
|
|
267
|
+
else bucket.push(entry);
|
|
268
|
+
}
|
|
269
|
+
const ordered = [...groups.entries()].sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]));
|
|
270
|
+
const rows = [];
|
|
271
|
+
for (const [namespace, bucket] of ordered) for (const entry of bucket.slice(0, 2)) {
|
|
272
|
+
if (rows.length >= limit) return rows;
|
|
273
|
+
rows.push({
|
|
274
|
+
name: entry.name,
|
|
275
|
+
description: clip(namespace + ": " + entry.description),
|
|
276
|
+
parameters: entry.parameters
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return rows;
|
|
280
|
+
}
|
|
281
|
+
/** 一个工具名所属的 namespace。 */
|
|
282
|
+
function namespaceOfName(name) {
|
|
283
|
+
if (!name.startsWith("mcp__")) return "plugin";
|
|
284
|
+
const rest = name.slice(5);
|
|
285
|
+
const cut = rest.indexOf("__");
|
|
286
|
+
return cut < 0 ? rest : rest.slice(0, cut);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 中日韩文字范围:扩展 A、基本区、兼容表意文字、日文假名、韩文音节。
|
|
290
|
+
*
|
|
291
|
+
* 用 fromCharCode 拼而不写转义:这个字符类写错一层反斜杠不会报错,只会让中日韩
|
|
292
|
+
* 分词整条静默失效(查询被切成空 token,然后按字母序返回一堆无关工具)。
|
|
293
|
+
*/
|
|
294
|
+
const CJK_RUN = new RegExp("[" + String.fromCharCode(13312) + "-䶿一-鿿豈--ヿ가-]+", "g");
|
|
295
|
+
/** 英文 / 数字词。 */
|
|
296
|
+
const WORD_RUN = /[a-z0-9]+/g;
|
|
297
|
+
/**
|
|
298
|
+
* 分词:拉丁词按词切,中日韩按单字 + 二元组(作为分词器的廉价替代)。
|
|
299
|
+
*
|
|
300
|
+
* **刻意不做中英对照**。工具的检索词来自工具自己(名字、描述、参数名),跨语言
|
|
301
|
+
* 落差交给模型:说明段明确要求用英文关键词检索,而模型本来就会翻译。一张手写的
|
|
302
|
+
* 同义词表只能覆盖写表的人想到的那几个词,覆盖率永远是问号,维护成本却是常态。
|
|
303
|
+
*
|
|
304
|
+
* @param text - 任意文本(工具名、描述、参数名或查询)。
|
|
305
|
+
* @returns 查询词。
|
|
306
|
+
*/
|
|
307
|
+
function tokenize(text) {
|
|
308
|
+
const value = String(text ?? "").toLowerCase();
|
|
309
|
+
const tokens = [];
|
|
310
|
+
for (const word of value.match(WORD_RUN) ?? []) tokens.push(word);
|
|
311
|
+
for (const run of value.match(CJK_RUN) ?? []) {
|
|
312
|
+
if (run.length === 1) tokens.push(run);
|
|
313
|
+
for (let index = 0; index + 1 < run.length; index += 1) tokens.push(run.slice(index, index + 2));
|
|
314
|
+
}
|
|
315
|
+
return tokens;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 参数名也是强信号:`instId`、`sql`、`channel` 这类词往往只出现在参数里,而它恰恰是
|
|
319
|
+
* 用户或模型用来定位工具的那个词。
|
|
320
|
+
*
|
|
321
|
+
* @param parameters - 工具的参数 JSON Schema。
|
|
322
|
+
* @returns 参数名分词。
|
|
323
|
+
*/
|
|
324
|
+
function parameterTokens(parameters) {
|
|
325
|
+
const tokens = [];
|
|
326
|
+
for (const key of propertiesOf(parameters)) tokens.push(...tokenize(key));
|
|
327
|
+
return tokens;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 读出一个工具参数 schema 的属性名。
|
|
331
|
+
*
|
|
332
|
+
* @param parameters - 参数 JSON Schema。
|
|
333
|
+
* @returns 属性名数组(没有时为 `[]`)。
|
|
334
|
+
*/
|
|
335
|
+
function propertiesOf(parameters) {
|
|
336
|
+
if (typeof parameters !== "object" || parameters === null) return [];
|
|
337
|
+
const properties = parameters.properties;
|
|
338
|
+
if (typeof properties !== "object" || properties === null) return [];
|
|
339
|
+
return Object.keys(properties);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* 参数名里是否存在与查询词**完全相同**的键。
|
|
343
|
+
*
|
|
344
|
+
* 索引只留了参数名的分词,丢掉了「原始键名」,所以这里在索引项上多存一份原始键名集合,
|
|
345
|
+
* 只用于给完全匹配再加一点权重。
|
|
346
|
+
*
|
|
347
|
+
* @param indexed - 索引项。
|
|
348
|
+
* @param term - 查询词。
|
|
349
|
+
* @returns 命中时 1,否则 0。
|
|
350
|
+
*/
|
|
351
|
+
function rawParameterHit(indexed, term) {
|
|
352
|
+
return indexed.parameterNames.has(term) ? 1 : 0;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* 把一份清单建成检索索引,并给出每个词的文档频率(用于降噪加权)。
|
|
356
|
+
*
|
|
357
|
+
* @param entries - 当前清单。
|
|
358
|
+
* @returns 索引项与词 → 出现次数。
|
|
359
|
+
*/
|
|
360
|
+
function buildIndex(entries) {
|
|
361
|
+
const documentFrequency = /* @__PURE__ */ new Map();
|
|
362
|
+
const indexed = [];
|
|
363
|
+
for (const entry of entries) {
|
|
364
|
+
const name = countOf(tokenize(entry.name));
|
|
365
|
+
const description = countOf(tokenize(entry.description));
|
|
366
|
+
const parameters = new Set(parameterTokens(entry.parameters));
|
|
367
|
+
const parameterNames = new Set(propertiesOf(entry.parameters).map((key) => key.toLowerCase()));
|
|
368
|
+
indexed.push({
|
|
369
|
+
entry,
|
|
370
|
+
name,
|
|
371
|
+
description,
|
|
372
|
+
parameters,
|
|
373
|
+
parameterNames
|
|
374
|
+
});
|
|
375
|
+
const seen = /* @__PURE__ */ new Set([
|
|
376
|
+
...name.keys(),
|
|
377
|
+
...description.keys(),
|
|
378
|
+
...parameters
|
|
379
|
+
]);
|
|
380
|
+
for (const token of seen) documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
indexed,
|
|
384
|
+
documentFrequency
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/** 统计词频。 */
|
|
388
|
+
function countOf(tokens) {
|
|
389
|
+
const counts = /* @__PURE__ */ new Map();
|
|
390
|
+
for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
391
|
+
return counts;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 一个工具的命中分数。
|
|
395
|
+
*
|
|
396
|
+
* 三处命中权重递减:名字 > 参数名 > 描述。每个词再乘一个 idf 权重,让 `ticker`、
|
|
397
|
+
* `candle` 这类稀有词压过 `get`、`list`、`create` 这类几乎每个工具都有的泛词——否则
|
|
398
|
+
* 「看一下 K 线」会被 `get_balance` 抢走(两者都命中 `get`)。
|
|
399
|
+
*
|
|
400
|
+
* @param indexed - 索引项。
|
|
401
|
+
* @param terms - 去重后的查询词。
|
|
402
|
+
* @param documentFrequency - 词 → 文档频率。
|
|
403
|
+
* @param total - 清单规模。
|
|
404
|
+
* @param query - 原始查询(用于整串子串加分)。
|
|
405
|
+
* @returns 分数;0 表示不相关。
|
|
406
|
+
*/
|
|
407
|
+
function scoreOf(indexed, terms, documentFrequency, total, query) {
|
|
408
|
+
if (terms.length === 0) return 1;
|
|
409
|
+
let score = 0;
|
|
410
|
+
for (const term of terms) {
|
|
411
|
+
const inName = indexed.name.get(term) ?? 0;
|
|
412
|
+
const inDescription = indexed.description.get(term) ?? 0;
|
|
413
|
+
const inParameters = indexed.parameters.has(term) ? 1 : 0;
|
|
414
|
+
const exactParameter = Math.max(0, rawParameterHit(indexed, term));
|
|
415
|
+
if (inName === 0 && inDescription === 0 && inParameters === 0) continue;
|
|
416
|
+
const df = documentFrequency.get(term) ?? 1;
|
|
417
|
+
const weight = Math.log(1 + total / (1 + df));
|
|
418
|
+
score += weight * (inName > 0 ? 6 + Math.min(inName, 3) : 0);
|
|
419
|
+
score += weight * (inParameters === 1 ? 3 + exactParameter : 0);
|
|
420
|
+
score += weight * (inDescription > 0 ? 1 + Math.min(inDescription, 2) * .5 : 0);
|
|
421
|
+
}
|
|
422
|
+
const name = indexed.entry.name.toLowerCase();
|
|
423
|
+
const wanted = query.trim().toLowerCase();
|
|
424
|
+
if (wanted.length > 0 && (name === wanted || name.endsWith("__" + wanted))) score += 50;
|
|
425
|
+
else if (wanted.length > 1 && name.includes(wanted)) score += 12;
|
|
426
|
+
return score;
|
|
427
|
+
}
|
|
428
|
+
/** 截断描述,避免清单输出过长。 */
|
|
429
|
+
function clip(text, max = 240) {
|
|
430
|
+
const value = String(text ?? "");
|
|
431
|
+
return value.length <= max ? value : value.slice(0, max - 1) + "…";
|
|
432
|
+
}
|
|
433
|
+
/** 安全 JSON 化。 */
|
|
434
|
+
function safeJson(value) {
|
|
435
|
+
try {
|
|
436
|
+
return JSON.stringify(value);
|
|
437
|
+
} catch {
|
|
438
|
+
return String(value);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/** 发现工具名契约:`[list, search, describe, debug?]`。第一个同时用作 Code Mode 标记。 */
|
|
442
|
+
const DEFAULT_META_TOOLS = Object.freeze([
|
|
443
|
+
"code_tools_list",
|
|
444
|
+
"code_tools_search",
|
|
445
|
+
"code_tools_describe",
|
|
446
|
+
"code_debug"
|
|
447
|
+
]);
|
|
448
|
+
/** 默认额外遮蔽的名字前缀。 */
|
|
449
|
+
const DEFAULT_HIDDEN_PREFIXES = Object.freeze(["mcp__"]);
|
|
450
|
+
/**
|
|
451
|
+
* 默认始终原生呈现的名字。
|
|
452
|
+
*
|
|
453
|
+
* 恒定系统提示词或子代理运行时契约里点名要求直接调用这些工具(大输出预览补读、调度账本查询、
|
|
454
|
+
* 进展更新汇报、结构化子代理输出提交,以及 Windows 下替代被撤回 `pwsh` 的 `win_bash`):
|
|
455
|
+
* 遮蔽它们要么让提示词指向模型看不见的名字并触发整批 `UNKNOWN_TOOL` 重跑,要么让会话顶层
|
|
456
|
+
* 失去唯一可用的 Shell。若某个工具当前未在宿主/子会话注册,保留在此集合中不会凭空暴露它。
|
|
457
|
+
*/
|
|
458
|
+
const DEFAULT_NATIVE_TOOLS = Object.freeze([
|
|
459
|
+
"cache_warmup_result",
|
|
460
|
+
"dispatch_ledger",
|
|
461
|
+
"progress_update",
|
|
462
|
+
"structured_output",
|
|
463
|
+
"win_bash"
|
|
464
|
+
]);
|
|
465
|
+
/**
|
|
466
|
+
* 把原始配置归一化。
|
|
467
|
+
*
|
|
468
|
+
* @param config - 行配置;`null` / 省略表示默认值。
|
|
469
|
+
* @returns 冻结的配置与集合。
|
|
470
|
+
*/
|
|
471
|
+
function createRules(config) {
|
|
472
|
+
const options = normalizeConfig(config);
|
|
473
|
+
return {
|
|
474
|
+
options,
|
|
475
|
+
hidden: /* @__PURE__ */ new Set([...options.hiddenTools, ...options.metaTools]),
|
|
476
|
+
native: /* @__PURE__ */ new Set([...options.nativeTools, options.runToolName])
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 校验并冻结一份配置。
|
|
481
|
+
*
|
|
482
|
+
* @param config - 原始配置。
|
|
483
|
+
* @returns 规范化后的只读配置。
|
|
484
|
+
*/
|
|
485
|
+
function normalizeConfig(config) {
|
|
486
|
+
const source = config ?? {};
|
|
487
|
+
const runToolName = nonEmptyString$1(source.runToolName) ?? "code_run";
|
|
488
|
+
const metaTools = stringList(source.metaTools, DEFAULT_META_TOOLS);
|
|
489
|
+
if (metaTools.length === 0) throw new TypeError("codemode: metaTools must list at least one tool name");
|
|
490
|
+
if (metaTools.includes(runToolName)) throw new TypeError("codemode: metaTools must not repeat the transport name");
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
nativeScope: source.nativeScope !== false,
|
|
493
|
+
hiddenPrefixes: Object.freeze(stringList(source.hiddenPrefixes, DEFAULT_HIDDEN_PREFIXES)),
|
|
494
|
+
nativeTools: Object.freeze(stringList(source.nativeTools, DEFAULT_NATIVE_TOOLS)),
|
|
495
|
+
hiddenTools: Object.freeze(stringList(source.hiddenTools, [])),
|
|
496
|
+
metaTools: Object.freeze(metaTools),
|
|
497
|
+
runToolName,
|
|
498
|
+
maxParallelSubCalls: positiveInteger(source.maxParallelSubCalls, 10),
|
|
499
|
+
maxSubCalls: positiveInteger(source.maxSubCalls, 512),
|
|
500
|
+
explicitMaxSubCalls: typeof source.maxSubCalls === "number" && Number.isInteger(source.maxSubCalls) && source.maxSubCalls > 0,
|
|
501
|
+
maxSubCallsPerMinute: positiveInteger(source.maxSubCallsPerMinute, 1024),
|
|
502
|
+
guard: source.guard !== false,
|
|
503
|
+
sdkSection: source.sdkSection !== false,
|
|
504
|
+
debug: source.debug !== false
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/** 作用域不可用时的空视图。 */
|
|
508
|
+
const DISABLED_VIEW = Object.freeze({
|
|
509
|
+
enabled: false,
|
|
510
|
+
globalNames: /* @__PURE__ */ new Set(),
|
|
511
|
+
presetNames: /* @__PURE__ */ new Set()
|
|
512
|
+
});
|
|
513
|
+
/**
|
|
514
|
+
* 判定一个名字是否要从模型清单里剥离。
|
|
515
|
+
*
|
|
516
|
+
* 顺序:显式例外(`nativeTools` / 传输工具 / 发现工具)→ 显式遮蔽
|
|
517
|
+
* (`hiddenTools`)→ 名字前缀 → 作用域归属。
|
|
518
|
+
*
|
|
519
|
+
* 作用域归属只在能确认归属时遮蔽:
|
|
520
|
+
*
|
|
521
|
+
* - 在全局视图里解析得到 → 全局层注册的,遮蔽;
|
|
522
|
+
* - 只在预设视图里解析得到 → 本预设自己注册的,保持原生;
|
|
523
|
+
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层),遮蔽。
|
|
524
|
+
*
|
|
525
|
+
* @param name - 工具名。
|
|
526
|
+
* @param rules - 已归一化的规则。
|
|
527
|
+
* @param view - 归属视图。
|
|
528
|
+
* @returns 要遮蔽时为 `true`。
|
|
529
|
+
*/
|
|
530
|
+
function isMaskedName(name, rules, view) {
|
|
531
|
+
if (typeof name !== "string" || name.length === 0) return false;
|
|
532
|
+
if (rules.native.has(name)) return false;
|
|
533
|
+
if (rules.hidden.has(name)) return true;
|
|
534
|
+
for (const prefix of rules.options.hiddenPrefixes) if (prefix.length > 0 && name.startsWith(prefix)) return true;
|
|
535
|
+
if (!rules.options.nativeScope || !view.enabled) return false;
|
|
536
|
+
if (view.globalNames.has(name)) return true;
|
|
537
|
+
if (view.presetNames.has(name)) return false;
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 汇总被遮蔽的工具。
|
|
542
|
+
*
|
|
543
|
+
* @param names - 被遮蔽的名字。
|
|
544
|
+
* @returns 计数与分组。
|
|
545
|
+
*/
|
|
546
|
+
function summarizeHidden(names) {
|
|
547
|
+
const groups = /* @__PURE__ */ new Map();
|
|
548
|
+
for (const name of names) {
|
|
549
|
+
const mcp = name.startsWith("mcp__") ? name.split("__")[1] : void 0;
|
|
550
|
+
const namespace = mcp !== void 0 && mcp.length > 0 ? mcp : "plugin";
|
|
551
|
+
const kind = mcp !== void 0 && mcp.length > 0 ? "mcp" : "plugin";
|
|
552
|
+
const key = kind + ":" + namespace;
|
|
553
|
+
const entry = groups.get(key);
|
|
554
|
+
if (entry === void 0) groups.set(key, {
|
|
555
|
+
namespace,
|
|
556
|
+
kind,
|
|
557
|
+
count: 1
|
|
558
|
+
});
|
|
559
|
+
else entry.count += 1;
|
|
560
|
+
}
|
|
561
|
+
const namespaces = [...groups.values()].sort((left, right) => right.count - left.count || left.namespace.localeCompare(right.namespace));
|
|
562
|
+
return {
|
|
563
|
+
total: names.length,
|
|
564
|
+
namespaces
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* 生成 `tools:sdk` 段的正文。
|
|
569
|
+
*
|
|
570
|
+
* ## 恒定契约
|
|
571
|
+
*
|
|
572
|
+
* 这段正文**只**依赖入参里的两个工具名与部署级的 `language` / `programFormat`。它刻意不含遮蔽
|
|
573
|
+
* 工具的数量、namespace 分布、会话 id 或时间,因此同一部署下逐字节恒定:挂载或卸载
|
|
574
|
+
* MCP 服务端不会改写它,前缀缓存不会因为它失效。
|
|
575
|
+
*
|
|
576
|
+
* @param input - 传输工具名、发现工具名与程序形态。
|
|
577
|
+
* @returns 说明段正文。
|
|
578
|
+
*/
|
|
579
|
+
function buildCodemodeSectionText(input) {
|
|
580
|
+
const python = input.language === "python";
|
|
581
|
+
const retained = input.programFormat === "repl";
|
|
582
|
+
const listTool = input.metaTools[0] ?? "code_tools_list";
|
|
583
|
+
const searchTool = input.metaTools[1] ?? "code_tools_search";
|
|
584
|
+
const describeTool = input.metaTools[2] ?? "code_tools_describe";
|
|
585
|
+
const run = input.runToolName;
|
|
586
|
+
const example = python ? [
|
|
587
|
+
"```py",
|
|
588
|
+
"# standard tools and hidden tools alike",
|
|
589
|
+
"import asyncio",
|
|
590
|
+
"files = await tools.glob({\"pattern\": \"src/**/*.ts\"})",
|
|
591
|
+
"rows = await asyncio.gather(*(tools.read({\"file_path\": f}) for f in files))",
|
|
592
|
+
"[r.splitlines()[0] for r in rows if \"TODO\" in r]",
|
|
593
|
+
"```"
|
|
594
|
+
] : [
|
|
595
|
+
"```js",
|
|
596
|
+
"// standard tools and hidden tools alike",
|
|
597
|
+
"const files = await tools.glob({ pattern: \"src/**/*.ts\" })",
|
|
598
|
+
"const rows = await Promise.all(files.map((f) => tools.read({ file_path: f })))",
|
|
599
|
+
retained ? "rows.filter((r) => r.includes(\"TODO\")).map((r) => r.split(\"\\n\")[0])" : "return rows.filter((r) => r.includes(\"TODO\")).map((r) => r.split(\"\\n\")[0])",
|
|
600
|
+
"```"
|
|
601
|
+
];
|
|
602
|
+
return [
|
|
603
|
+
"## Code Mode",
|
|
604
|
+
"",
|
|
605
|
+
"This session runs in Code Mode. The tools in your tool list are called directly, exactly as usual. A second, larger set of tools is deliberately **not** in your context: every MCP tool, and every tool contributed by a plugin outside this preset. Reach them from inside " + run + ".",
|
|
606
|
+
"",
|
|
607
|
+
"### Calling tools from a program",
|
|
608
|
+
"",
|
|
609
|
+
...example,
|
|
610
|
+
"",
|
|
611
|
+
python ? retained ? "- A retained Python REPL: top-level await and a final expression. Its variables and imported modules survive into your later programs in this session." : "- Async Python program: top-level await works; print or evaluate a final expression to produce output." : retained ? "- A retained TypeScript REPL: top-level declarations, await, and a final expression. Its variables and imported modules survive into your later programs in this session." : "- Erasable TypeScript, run as the body of an async function: top-level await and return work.",
|
|
612
|
+
"- Only what you return or console.log reaches the conversation; every intermediate result stays in the program, so filter and aggregate there instead of returning raw payloads.",
|
|
613
|
+
"- Prefer batching multi-step read/search chains inside one " + run + " program instead of issuing many sequential single-tool turns.",
|
|
614
|
+
"- Independent read-only calls may overlap under Promise.all; mutating calls run alone, in submission order.",
|
|
615
|
+
"- A failed sub-call rejects; try/catch or Promise.allSettled it to keep the calls that succeeded. Completed calls are real side effects and are not rolled back.",
|
|
616
|
+
"",
|
|
617
|
+
"### Discovery API (callable from inside a program)",
|
|
618
|
+
"",
|
|
619
|
+
"- tools." + listTool + "({ namespace? }) - every callable tool as { name, description, namespace }, hidden ones included.",
|
|
620
|
+
"- tools." + searchTool + "({ query, limit?, namespace? }) - ranked matches with their full parameter schemas; the intended first step. Query with the English keywords the tool is likely named and described with (tool names and descriptions are English); a phrase in another language is only matched literally.",
|
|
621
|
+
"- tools." + describeTool + "({ name }) - one tool description and complete parameter schema.",
|
|
622
|
+
"",
|
|
623
|
+
"If a hidden tool name is called directly instead of from a program, the call is refused and the refusal names the discovery call to make."
|
|
624
|
+
].join("\n");
|
|
625
|
+
}
|
|
626
|
+
/** 一个非空字符串,或 `undefined`。 */
|
|
627
|
+
function nonEmptyString$1(value) {
|
|
628
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
629
|
+
}
|
|
630
|
+
/** 名字列表:省略时用缺省值,拒绝非字符串项与重复项。 */
|
|
631
|
+
function stringList(value, fallback) {
|
|
632
|
+
if (value === void 0 || value === null) return [...fallback];
|
|
633
|
+
if (!Array.isArray(value)) throw new TypeError("codemode: expected an array of tool names");
|
|
634
|
+
const out = [];
|
|
635
|
+
for (const entry of value) {
|
|
636
|
+
const name = nonEmptyString$1(entry);
|
|
637
|
+
if (name === void 0) throw new TypeError("codemode: tool names must be non-empty strings");
|
|
638
|
+
if (!out.includes(name)) out.push(name);
|
|
639
|
+
}
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
/** 正整数,越界或非法时用缺省值。 */
|
|
643
|
+
function positiveInteger(value, fallback) {
|
|
644
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return fallback;
|
|
645
|
+
return value;
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/host/codemode/executor.ts
|
|
649
|
+
/**
|
|
650
|
+
* Code Mode 的工具执行器:本包遮蔽与拒绝的唯一接缝。
|
|
651
|
+
*
|
|
652
|
+
* ## 为什么是 executor
|
|
653
|
+
*
|
|
654
|
+
* DSH 0.2.2 把「模型看到的工具清单」的归属权从 `system-prompt/assemble` 瀑布搬到了
|
|
655
|
+
* per-scope `ToolExecutor`:注册表向当前作用域选中的 executor 要 schema,所以
|
|
656
|
+
* `describe()` 就是请求面。它同时解决了归属问题——executor **按作用域链选择**,只有挂
|
|
657
|
+
* 在本预设里的这一份会服务本预设的会话,因此不需要判断「这个 agent 是不是我的」。
|
|
658
|
+
*
|
|
659
|
+
* 三件事:
|
|
660
|
+
*
|
|
661
|
+
* 1. `describe()` 丢掉一切不该出现在模型清单里的名字,其余交给原生 executor 投影;
|
|
662
|
+
* 2. `sections` 提供**恒定**的 Code Mode 说明段(见 rules.ts 的恒定契约);
|
|
663
|
+
* 3. `execute()` 在派发**之前**拒绝模型对被遮蔽名字的直接调用,其余整批委托给原生
|
|
664
|
+
* executor——委托而不是重写,是因为手写 pre-policy / 审批 / around-dispatch /
|
|
665
|
+
* post-execute 管线正是插件静默丢策略的方式。
|
|
666
|
+
*
|
|
667
|
+
* ## 程序内子调用怎么与模型直呼区分
|
|
668
|
+
*
|
|
669
|
+
* 批量 `execute()` 的入参没有 `parent` 字段(执行令牌由注册表自己的管线铸造,插件
|
|
670
|
+
* 造不出来),所以传输层把自己持有的 `AbortSignal` 打上 {@link NESTED_MARK},并让子调用 id
|
|
671
|
+
* 形如 `<outer>:ptc:<n>`。signal 是模型无法伪造的对象,call id 不可改,两个标记互为兜底。
|
|
672
|
+
*
|
|
673
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/executor
|
|
674
|
+
*/
|
|
675
|
+
/** 传输层打在 AbortSignal 上的嵌套标记。 */
|
|
676
|
+
const NESTED_MARK = Symbol.for("dsh-chatgpt-subscription.codemode.nested");
|
|
677
|
+
/** 子调用 id 里的一段:`<outer>:ptc:<n>`,与内核自己的命名一致。 */
|
|
678
|
+
const NESTED_CALL_ID_MARKER = ":ptc:";
|
|
679
|
+
/**
|
|
680
|
+
* 给一个 signal 打上嵌套标记。
|
|
681
|
+
*
|
|
682
|
+
* @param signal - 传给 `tools.execute()` 的那一个 signal。
|
|
683
|
+
* @returns 同一个 signal(便于链式使用)。
|
|
684
|
+
*/
|
|
685
|
+
function markNested(signal) {
|
|
686
|
+
if (signal === void 0) return void 0;
|
|
687
|
+
try {
|
|
688
|
+
Object.defineProperty(signal, NESTED_MARK, {
|
|
689
|
+
value: true,
|
|
690
|
+
enumerable: false,
|
|
691
|
+
configurable: true
|
|
692
|
+
});
|
|
693
|
+
} catch {}
|
|
694
|
+
return signal;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* 判断一批调用是不是来自程序内部。
|
|
698
|
+
*
|
|
699
|
+
* @param call - 批里的一条调用(结构探测)。
|
|
700
|
+
* @param batchSignal - 批级 signal(`tools.execute()` 只收一份,注册表会复制给每条执行)。
|
|
701
|
+
* @returns 来自程序内部时为 `true`。
|
|
702
|
+
*/
|
|
703
|
+
function isNestedCall(call, batchSignal) {
|
|
704
|
+
if (isMarked(batchSignal)) return true;
|
|
705
|
+
if (typeof call !== "object" || call === null) return false;
|
|
706
|
+
if (isMarked(call.signal)) return true;
|
|
707
|
+
const id = call.id;
|
|
708
|
+
return typeof id === "string" && id.includes(":ptc:");
|
|
709
|
+
}
|
|
710
|
+
/** 一个值是否带着本包的嵌套标记。 */
|
|
711
|
+
function isMarked(value) {
|
|
712
|
+
return typeof value === "object" && value !== null && value[NESTED_MARK] === true;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* 造一个 Code Mode 执行器。
|
|
716
|
+
*
|
|
717
|
+
* @param ctx - 本包的行上下文(读取系统提示段的排序位)。
|
|
718
|
+
* @param rules - 已归一化的规则。
|
|
719
|
+
* @param hooks - 归属视图、说明段与委托目标。
|
|
720
|
+
* @returns 执行器。
|
|
721
|
+
*/
|
|
722
|
+
function createCodemodeExecutor(ctx, rules, hooks) {
|
|
723
|
+
const sectionOrder = sectionOrderOf(ctx);
|
|
724
|
+
const sections = rules.options.sdkSection ? [{
|
|
725
|
+
name: "tools:sdk",
|
|
726
|
+
order: sectionOrder,
|
|
727
|
+
interpolate: false,
|
|
728
|
+
text: () => hooks.sectionText()
|
|
729
|
+
}] : [];
|
|
730
|
+
const visibleNames = /* @__PURE__ */ new Set();
|
|
731
|
+
return {
|
|
732
|
+
sections,
|
|
733
|
+
describe(tools) {
|
|
734
|
+
hooks.onCatalog?.(tools ?? []);
|
|
735
|
+
const view = hooks.view();
|
|
736
|
+
const visible = [];
|
|
737
|
+
const masked = [];
|
|
738
|
+
visibleNames.clear();
|
|
739
|
+
for (const tool of tools ?? []) {
|
|
740
|
+
const name = nameOf(tool);
|
|
741
|
+
if (name === void 0) continue;
|
|
742
|
+
if (isMaskedName(name, rules, view)) masked.push(name);
|
|
743
|
+
else {
|
|
744
|
+
visible.push(tool);
|
|
745
|
+
visibleNames.add(name);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
hooks.onDescribe?.({
|
|
749
|
+
visible: [...visibleNames],
|
|
750
|
+
masked
|
|
751
|
+
});
|
|
752
|
+
return hooks.delegate.describe(visible);
|
|
753
|
+
},
|
|
754
|
+
async execute(input) {
|
|
755
|
+
const calls = Array.isArray(input?.calls) ? input.calls : [];
|
|
756
|
+
const denials = /* @__PURE__ */ new Map();
|
|
757
|
+
if (rules.options.guard) for (let index = 0; index < calls.length; index += 1) {
|
|
758
|
+
const reason = denialFor(calls[index], rules, hooks, input?.signal);
|
|
759
|
+
if (reason !== void 0) denials.set(index, reason);
|
|
760
|
+
}
|
|
761
|
+
if (denials.size === 0) return hooks.delegate.execute(input);
|
|
762
|
+
return {
|
|
763
|
+
results: calls.map((call, index) => {
|
|
764
|
+
const callId = idOf(call) ?? "call-" + String(index);
|
|
765
|
+
const reason = denials.get(index) ?? "This call was not run: the batch also contained a call that is not part of this session tool list. Re-issue it on its own.";
|
|
766
|
+
return {
|
|
767
|
+
callId,
|
|
768
|
+
result: {
|
|
769
|
+
isError: true,
|
|
770
|
+
error: {
|
|
771
|
+
name: "ToolNotFoundError",
|
|
772
|
+
code: "UNKNOWN_TOOL",
|
|
773
|
+
message: reason
|
|
774
|
+
},
|
|
775
|
+
content: [{
|
|
776
|
+
type: "text",
|
|
777
|
+
text: "Error: " + reason
|
|
778
|
+
}]
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}),
|
|
782
|
+
additionalContexts: [],
|
|
783
|
+
concluded: false
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* 一条模型直呼调用的拒绝理由,或 `undefined` 表示放行。
|
|
790
|
+
*
|
|
791
|
+
* @param call - 批里的一条调用。
|
|
792
|
+
* @param rules - 已归一化的规则。
|
|
793
|
+
* @param hooks - 归属视图与说明段。
|
|
794
|
+
* @param batchSignal - 批级 signal。
|
|
795
|
+
* @returns 拒绝理由或 `undefined`。
|
|
796
|
+
*
|
|
797
|
+
* 判定不读「本步可见名清单」:那份集合是**执行器级**的,多个会话并发时会被后一次 describe
|
|
798
|
+
* 覆盖,用它放行等于给另一个会话开一个不该开的口子。这里只按名字自身的归属判一次。
|
|
799
|
+
*/
|
|
800
|
+
function denialFor(call, rules, hooks, batchSignal) {
|
|
801
|
+
if (typeof call !== "object" || call === null) return void 0;
|
|
802
|
+
const name = call.name;
|
|
803
|
+
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
804
|
+
if (isNestedCall(call, batchSignal)) return void 0;
|
|
805
|
+
if (name === rules.options.runToolName) return void 0;
|
|
806
|
+
if (!isMaskedName(name, rules, hooks.viewForName(name))) return void 0;
|
|
807
|
+
if (rules.options.metaTools.includes(name)) return "The discovery tool \"" + name + "\" is callable only from inside " + rules.options.runToolName + ": run a program and call await tools." + name + "(...) there.";
|
|
808
|
+
const searchTool = rules.options.metaTools[1] ?? rules.options.metaTools[0] ?? "code_tools_search";
|
|
809
|
+
return "The tool \"" + name + "\" is not part of this session tool list. In Code Mode, tools this preset does not own (MCP servers, plugins outside the preset) are reachable only from inside " + rules.options.runToolName + ": call tools." + searchTool + "({ query: \"...\" }) there to discover it, then call it by exact name.";
|
|
810
|
+
}
|
|
811
|
+
/** 读一段工具定义的 ``name``。 */
|
|
812
|
+
function nameOf(tool) {
|
|
813
|
+
if (typeof tool !== "object" || tool === null) return void 0;
|
|
814
|
+
const name = tool.name;
|
|
815
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
816
|
+
}
|
|
817
|
+
/** 读一条调用的 id。 */
|
|
818
|
+
function idOf(call) {
|
|
819
|
+
if (typeof call !== "object" || call === null) return void 0;
|
|
820
|
+
const id = call.id;
|
|
821
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
822
|
+
}
|
|
823
|
+
/** 取 `tools:sdk` 的排序位;取不到时退回 5000(内核 TOOLS_SDK 的默认位)。 */
|
|
824
|
+
function sectionOrderOf(ctx) {
|
|
825
|
+
try {
|
|
826
|
+
const order = (ctx?.get?.("systemPrompt"))?.getSectionOrder?.("TOOLS_SDK");
|
|
827
|
+
return typeof order === "number" && Number.isFinite(order) ? order : 5e3;
|
|
828
|
+
} catch {
|
|
829
|
+
return 5e3;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/host/codemode/retention.ts
|
|
834
|
+
/**
|
|
835
|
+
* 造一个常驻环境管理器。
|
|
836
|
+
*
|
|
837
|
+
* @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
|
|
838
|
+
* @returns 管理句柄。
|
|
839
|
+
*/
|
|
840
|
+
function createRetention(runtimeOf) {
|
|
841
|
+
const live = /* @__PURE__ */ new Set();
|
|
842
|
+
const released = /* @__PURE__ */ new WeakSet();
|
|
843
|
+
const release = async (owner) => {
|
|
844
|
+
if (released.has(owner)) return;
|
|
845
|
+
released.add(owner);
|
|
846
|
+
live.delete(owner);
|
|
847
|
+
const runtime = runtimeOf();
|
|
848
|
+
if (runtime?.release === void 0) return;
|
|
849
|
+
try {
|
|
850
|
+
await runtime.release(owner);
|
|
851
|
+
} catch {}
|
|
852
|
+
};
|
|
853
|
+
return {
|
|
854
|
+
owner(agent) {
|
|
855
|
+
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
856
|
+
released.delete(owner);
|
|
857
|
+
live.add(owner);
|
|
858
|
+
return owner;
|
|
859
|
+
},
|
|
860
|
+
release,
|
|
861
|
+
async dispose() {
|
|
862
|
+
const owners = [...live];
|
|
863
|
+
await Promise.all(owners.map((owner) => release(owner)));
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
|
|
868
|
+
const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/host/codemode/scope.ts
|
|
871
|
+
/**
|
|
872
|
+
* 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
|
|
873
|
+
*
|
|
874
|
+
* ## 为什么要这么绕
|
|
875
|
+
*
|
|
876
|
+
* 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
|
|
877
|
+
* 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
|
|
878
|
+
* 都不导出。能用的只有两件事:
|
|
879
|
+
*
|
|
880
|
+
* 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
|
|
881
|
+
* 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
|
|
882
|
+
*
|
|
883
|
+
* 于是归属判定变成两次公开查询:
|
|
884
|
+
*
|
|
885
|
+
* - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
|
|
886
|
+
* - 只有本预设视图能解析 → 本预设自己注册的;
|
|
887
|
+
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
|
|
888
|
+
*
|
|
889
|
+
* 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
|
|
890
|
+
* 纯名字匹配,绝不因为读不到归属就误遮蔽。
|
|
891
|
+
*
|
|
892
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
|
|
893
|
+
*/
|
|
894
|
+
/** 内核写作用域标签用的符号描述。 */
|
|
895
|
+
const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
|
|
896
|
+
/**
|
|
897
|
+
* 从一个 ctx 上找出「本包所在作用域」的 key。
|
|
898
|
+
*
|
|
899
|
+
* Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
|
|
900
|
+
* 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
|
|
901
|
+
* 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
|
|
902
|
+
*
|
|
903
|
+
* @param ctx - 本包的行上下文。
|
|
904
|
+
* @returns 作用域 key;读不到时为 `undefined`。
|
|
905
|
+
*/
|
|
906
|
+
function findScopeKey(ctx) {
|
|
907
|
+
if (typeof ctx !== "object" || ctx === null) return void 0;
|
|
908
|
+
return scanForScope(ctx, 0);
|
|
909
|
+
}
|
|
910
|
+
/** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
|
|
911
|
+
function scanForScope(source, depth) {
|
|
912
|
+
if (depth > 4) return void 0;
|
|
913
|
+
let symbols;
|
|
914
|
+
try {
|
|
915
|
+
symbols = Object.getOwnPropertySymbols(source);
|
|
916
|
+
} catch {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
for (const symbol of symbols) {
|
|
920
|
+
if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
|
|
921
|
+
let value;
|
|
922
|
+
try {
|
|
923
|
+
value = source[symbol];
|
|
924
|
+
} catch {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (typeof value === "object" && value !== null) return value;
|
|
928
|
+
}
|
|
929
|
+
const proto = Object.getPrototypeOf(source);
|
|
930
|
+
if (proto === null || proto === Object.prototype) return void 0;
|
|
931
|
+
return scanForScope(proto, depth + 1);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* 为一个名字清单建立归属视图。
|
|
935
|
+
*
|
|
936
|
+
* @param tools - 工具注册表(结构探测,不看类身份)。
|
|
937
|
+
* @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
|
|
938
|
+
* @param names - 要判定的名字(通常是本步的模型可见清单)。
|
|
939
|
+
* @returns 归属视图;读不到任何归属时返回禁用视图。
|
|
940
|
+
*/
|
|
941
|
+
function resolveCatalogView(tools, scopeKey, names) {
|
|
942
|
+
const registry = asRegistry(tools);
|
|
943
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
944
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
945
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
946
|
+
for (const name of names) {
|
|
947
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
948
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
949
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
enabled: true,
|
|
953
|
+
globalNames,
|
|
954
|
+
presetNames
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* 判定一个名字在**调用时**是否属于本预设之外的层。
|
|
959
|
+
*
|
|
960
|
+
* 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
|
|
961
|
+
* 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
|
|
962
|
+
*
|
|
963
|
+
* @param tools - 工具注册表。
|
|
964
|
+
* @param scopeKey - 本预设的作用域 key。
|
|
965
|
+
* @param name - 工具名。
|
|
966
|
+
* @returns 归属视图(只含这一个名字)。
|
|
967
|
+
*/
|
|
968
|
+
function resolveNameView(tools, scopeKey, name) {
|
|
969
|
+
const registry = asRegistry(tools);
|
|
970
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
971
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
972
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
973
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
974
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
975
|
+
return {
|
|
976
|
+
enabled: true,
|
|
977
|
+
globalNames,
|
|
978
|
+
presetNames
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/** 一次公开查询:解析得到定义时为 `true`。 */
|
|
982
|
+
function probes(registry, name, scope) {
|
|
983
|
+
try {
|
|
984
|
+
return registry.get(name, scope) !== void 0;
|
|
985
|
+
} catch {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/** 结构探测:读得到 `get` 就当注册表用。 */
|
|
990
|
+
function asRegistry(tools) {
|
|
991
|
+
if (typeof tools !== "object" || tools === null) return void 0;
|
|
992
|
+
if (typeof tools.get !== "function") return void 0;
|
|
993
|
+
return tools;
|
|
994
|
+
}
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/host/codemode/transport.ts
|
|
997
|
+
/**
|
|
998
|
+
* Code Mode 的程序传输工具 `code_run`。
|
|
999
|
+
*
|
|
1000
|
+
* 工具名不是 `run_code`:那个名字被内核保留(`tools.register()` 会抛错),所以本包
|
|
1001
|
+
* 用自己的名字注册同一件事——把模型写的一段程序交给 `ctx.ptcRuntime` 执行,程序里的每次
|
|
1002
|
+
* `tools.<name>(args)` 都是一次真实的工具调用。
|
|
1003
|
+
*
|
|
1004
|
+
* ## 与内核 PTC `run_code` 对齐的使用配置
|
|
1005
|
+
*
|
|
1006
|
+
* 1. **运行时感知 Schema**:按 `ptcRuntime` 的语言(TypeScript / Python)、程序形态
|
|
1007
|
+
* (`function-body` / 常驻 REPL)、超时预算(`defaultMs` / `maxMs`)与沙箱支持动态投影
|
|
1008
|
+
* 描述和参数(含单次提权的 `sandbox_permissions` + `justification`);同一部署下静态恒定,
|
|
1009
|
+
* 不会扰动前缀缓存。
|
|
1010
|
+
* 2. **完整子调用调度与上下文转发**:只读并发上限受 `maxParallelSubCalls` 约束,写类调用等待
|
|
1011
|
+
* 在飞调用排空后独占执行;程序结算时中止悬空请求并等待已启动子调用落盘;子调用产生的图片块、
|
|
1012
|
+
* `additionalContexts` 与 `concludesTurn` 原样转交给外层步骤。
|
|
1013
|
+
* 3. **展示元数据与输出格式化**:声明 `presentCall` 与 `output.presentationMeta`(记录语言),
|
|
1014
|
+
* 深层 JSON 结果控制缩进上限,失败时保留已捕获输出与沙箱诊断信息。
|
|
1015
|
+
*
|
|
1016
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/transport
|
|
1017
|
+
*/
|
|
1018
|
+
/**
|
|
1019
|
+
* 本次程序运行看到的工具清单快照。
|
|
1020
|
+
*
|
|
1021
|
+
* 绑定函数在程序开始那一刻按当时的清单建好,此后程序内**只能**调这份清单里的名字。检索却
|
|
1022
|
+
* 读的是执行器级的“最新清单”:同一预设下主代理与子代理并发时,另一个会话的一次装配就会把
|
|
1023
|
+
* 它覆盖掉,于是程序可能搜到一个自己调不到的名字(`tools.xxx is not a function`)。
|
|
1024
|
+
*
|
|
1025
|
+
* 用 AsyncLocalStorage 把快照挂到这次运行上,程序内的检索与绑定因此读同一份清单:搜得到
|
|
1026
|
+
* 就调得动,调不到就搜不到。
|
|
1027
|
+
*/
|
|
1028
|
+
const catalogScope = new AsyncLocalStorage();
|
|
1029
|
+
/**
|
|
1030
|
+
* 读当前程序运行的清单快照。
|
|
1031
|
+
*
|
|
1032
|
+
* @returns 程序运行期间是这次运行的快照;程序之外为 `undefined`。
|
|
1033
|
+
*/
|
|
1034
|
+
function currentCatalogSnapshot() {
|
|
1035
|
+
return catalogScope.getStore();
|
|
1036
|
+
}
|
|
1037
|
+
/** 允许单次提权的目标沙箱模式(对齐 `@deepseek-ai/dsh-sandbox` 的 `ESCALATION_TARGETS`)。 */
|
|
1038
|
+
const ESCALATION_TARGETS = Object.freeze(["workspace-write", "danger-full-access"]);
|
|
1039
|
+
/** 与内核 `run_code` 一致的 `description` 参数说明。 */
|
|
1040
|
+
const DESCRIPTION_PARAM_DESCRIPTION = "Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\".";
|
|
1041
|
+
function buildFlavor(languageName, execution) {
|
|
1042
|
+
return {
|
|
1043
|
+
description: `Execute a ${languageName} program against the available tools. Takes two required arguments: \`code\`, ${execution.programDescription}, and \`description\`, a short summary of what the program does. Call tools as \`await tools.name(args)\`. ${execution.outputInstruction} Image-bearing subtool results are attached after the run.`,
|
|
1044
|
+
codeDescription: execution.codeDescription
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const TYPESCRIPT_REPL_FLAVOR = buildFlavor("TypeScript", {
|
|
1048
|
+
programDescription: "the program to execute",
|
|
1049
|
+
outputInstruction: "Curate the program output.",
|
|
1050
|
+
codeDescription: "The TypeScript program to execute, following this tool's execution instructions."
|
|
1051
|
+
});
|
|
1052
|
+
const TYPESCRIPT_FUNCTION_FLAVOR = buildFlavor("TypeScript", {
|
|
1053
|
+
programDescription: "the BODY of an async function (erasable syntax only; top-level `await` and `return` work)",
|
|
1054
|
+
outputInstruction: "Only what you print or return is program output — curate it.",
|
|
1055
|
+
codeDescription: "The program: the body of an async TypeScript function."
|
|
1056
|
+
});
|
|
1057
|
+
const PYTHON_FLAVOR = buildFlavor("Python", {
|
|
1058
|
+
programDescription: "the program to execute",
|
|
1059
|
+
outputInstruction: "Curate the program output.",
|
|
1060
|
+
codeDescription: "The Python program to execute, following this tool's execution instructions."
|
|
1061
|
+
});
|
|
1062
|
+
function resolveFlavor(runtime) {
|
|
1063
|
+
if (runtime?.language === "python") return PYTHON_FLAVOR;
|
|
1064
|
+
if (runtime?.language === "typescript" && runtime.programFormat === "repl") return TYPESCRIPT_REPL_FLAVOR;
|
|
1065
|
+
return TYPESCRIPT_FUNCTION_FLAVOR;
|
|
1066
|
+
}
|
|
1067
|
+
function escalationGuidance(sandboxMode) {
|
|
1068
|
+
return sandboxMode === void 0 ? "" : " A sandbox escalation approves this complete program for one execution only. Nested tools retain their own policies and approvals. Request wider access only after evidence of a denial. Earlier effects may already have completed: inspect them before explicitly retrying. Programs are never replayed automatically.";
|
|
1069
|
+
}
|
|
1070
|
+
function buildTransportDescription(runtime) {
|
|
1071
|
+
const flavor = resolveFlavor(runtime);
|
|
1072
|
+
const instructions = typeof runtime?.executionInstructions === "string" && runtime.executionInstructions.length > 0 ? ` ${runtime.executionInstructions}` : "";
|
|
1073
|
+
return flavor.description + instructions + " Each program starts in the Session's current directory. Running programs keep their initial directory." + escalationGuidance(runtime?.sandboxMode);
|
|
1074
|
+
}
|
|
1075
|
+
function buildTransportParameters(runtime) {
|
|
1076
|
+
const flavor = resolveFlavor(runtime);
|
|
1077
|
+
const timeoutDescription = runtime?.timeout !== void 0 ? `Positive elapsed-time budget in milliseconds, including nested tool and approval waits. Default ${String(runtime.timeout.defaultMs)}; capped at ${String(runtime.timeout.maxMs)}. Zero does not disable the deadline.` : "Positive elapsed-time budget in milliseconds, capped by the runtime maximum.";
|
|
1078
|
+
const properties = {
|
|
1079
|
+
code: {
|
|
1080
|
+
type: "string",
|
|
1081
|
+
description: flavor.codeDescription
|
|
1082
|
+
},
|
|
1083
|
+
description: {
|
|
1084
|
+
type: "string",
|
|
1085
|
+
description: DESCRIPTION_PARAM_DESCRIPTION
|
|
1086
|
+
},
|
|
1087
|
+
timeoutMs: {
|
|
1088
|
+
type: "number",
|
|
1089
|
+
description: timeoutDescription
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
if (runtime?.sandboxMode !== void 0) {
|
|
1093
|
+
properties["sandbox_permissions"] = {
|
|
1094
|
+
type: "string",
|
|
1095
|
+
enum: [...ESCALATION_TARGETS],
|
|
1096
|
+
description: "Wider sandbox mode for this complete program execution; requires justification and approval."
|
|
1097
|
+
};
|
|
1098
|
+
properties["justification"] = {
|
|
1099
|
+
type: "string",
|
|
1100
|
+
description: "Reason this complete program needs wider access, shown to the user for approval. Use the language of the user’s current request."
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
type: "object",
|
|
1105
|
+
additionalProperties: false,
|
|
1106
|
+
required: ["code", "description"],
|
|
1107
|
+
properties
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* 造 `code_run` 的工具定义。
|
|
1112
|
+
*
|
|
1113
|
+
* @param host - 运行环境句柄。
|
|
1114
|
+
* @param rules - 已归一化的规则。
|
|
1115
|
+
* @returns 可直接交给 `ctx.tools.register()` 的原始定义。
|
|
1116
|
+
*/
|
|
1117
|
+
function createTransportDefinition(host, rules) {
|
|
1118
|
+
const runToolName = rules.options.runToolName;
|
|
1119
|
+
const definition = {
|
|
1120
|
+
name: runToolName,
|
|
1121
|
+
description: buildTransportDescription(void 0),
|
|
1122
|
+
parameters: buildTransportParameters(void 0),
|
|
1123
|
+
output: {
|
|
1124
|
+
schema: {
|
|
1125
|
+
type: "object",
|
|
1126
|
+
additionalProperties: false,
|
|
1127
|
+
required: ["logs"],
|
|
1128
|
+
properties: {
|
|
1129
|
+
logs: {
|
|
1130
|
+
type: "array",
|
|
1131
|
+
items: { type: "string" }
|
|
1132
|
+
},
|
|
1133
|
+
result: {},
|
|
1134
|
+
sandbox: {
|
|
1135
|
+
type: "object",
|
|
1136
|
+
additionalProperties: false,
|
|
1137
|
+
required: ["mode", "denied"],
|
|
1138
|
+
properties: {
|
|
1139
|
+
mode: { type: "string" },
|
|
1140
|
+
denied: { type: "boolean" },
|
|
1141
|
+
enforcement: {
|
|
1142
|
+
type: "string",
|
|
1143
|
+
enum: ["full", "partial"]
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
render: (_args, value) => {
|
|
1150
|
+
const parts = [(value.logs ?? []).join("\n"), value.result === void 0 ? "" : renderProgramValue(value.result)].filter((part) => part.length > 0);
|
|
1151
|
+
if (value.sandbox?.enforcement === "partial") parts.push("File sandbox enforcement is partial on this host.");
|
|
1152
|
+
if (value.sandbox?.denied === true) parts.push("The " + value.sandbox.mode + " file sandbox denied an operation." + escalationGuidance(host.runtime()?.sandboxMode));
|
|
1153
|
+
return [{
|
|
1154
|
+
type: "text",
|
|
1155
|
+
text: parts.length > 0 ? parts.join("\n") : "(" + runToolName + " completed with no output)"
|
|
1156
|
+
}];
|
|
1157
|
+
},
|
|
1158
|
+
presentationMeta: () => {
|
|
1159
|
+
return { language: host.runtime()?.language === "python" ? "python" : "typescript" };
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
presentCall(args) {
|
|
1163
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
1164
|
+
const record = args;
|
|
1165
|
+
const code = record["code"];
|
|
1166
|
+
const description = record["description"];
|
|
1167
|
+
if (typeof code !== "string" || typeof description !== "string" || description.trim().length === 0) return;
|
|
1168
|
+
return {
|
|
1169
|
+
card: "generic",
|
|
1170
|
+
title: description,
|
|
1171
|
+
kind: "execute",
|
|
1172
|
+
rawInput: code
|
|
1173
|
+
};
|
|
1174
|
+
},
|
|
1175
|
+
async execute(args, exec) {
|
|
1176
|
+
const input = readArguments(args);
|
|
1177
|
+
const runtime = host.runtime();
|
|
1178
|
+
if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
|
|
1179
|
+
if (input.timeoutMs !== void 0 && runtime.timeout !== void 0 === false && isStrictTimeoutUnsupported(runtime)) throw new Error("timeoutMs is not available for this PTC runtime");
|
|
1180
|
+
const catalog = host.catalog();
|
|
1181
|
+
const runController = new AbortController();
|
|
1182
|
+
const onOuterAbort = () => {
|
|
1183
|
+
runController.abort(exec.signal.reason);
|
|
1184
|
+
};
|
|
1185
|
+
if (exec.signal.aborted) runController.abort(exec.signal.reason);
|
|
1186
|
+
else exec.signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
1187
|
+
const nestedSignal = markNested(runController.signal) ?? runController.signal;
|
|
1188
|
+
const owner = host.stateOwner(exec.agent);
|
|
1189
|
+
let maxSubCalls = rules.options.maxSubCalls;
|
|
1190
|
+
let attempts = 0;
|
|
1191
|
+
const scheduler = createSubCallScheduler(rules.options.maxParallelSubCalls, () => runController.signal.aborted);
|
|
1192
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
1193
|
+
for (const entry of catalog) definitions.set(entry.name, entry);
|
|
1194
|
+
const functions = Object.create(null);
|
|
1195
|
+
for (const entry of catalog) {
|
|
1196
|
+
if (entry.name === runToolName) continue;
|
|
1197
|
+
const invoke = async (rawArgs) => {
|
|
1198
|
+
if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " not dispatched");
|
|
1199
|
+
const n = ++attempts;
|
|
1200
|
+
if (n > maxSubCalls) {
|
|
1201
|
+
const limitErr = /* @__PURE__ */ new Error("program sub-call limit exceeded: " + String(maxSubCalls) + " calls");
|
|
1202
|
+
runController.abort(limitErr);
|
|
1203
|
+
throw limitErr;
|
|
1204
|
+
}
|
|
1205
|
+
const parsed = losslessArgs(rawArgs);
|
|
1206
|
+
const loggedArgs = losslessArgs(parsed);
|
|
1207
|
+
const callId = String(exec.callId) + NESTED_CALL_ID_MARKER + String(n);
|
|
1208
|
+
const dispatchLog = createDispatchLog(exec.agent, {
|
|
1209
|
+
rootCallId: rootCallIdOf(String(exec.callId)),
|
|
1210
|
+
parentCallId: String(exec.callId),
|
|
1211
|
+
subCallId: callId,
|
|
1212
|
+
name: entry.name,
|
|
1213
|
+
arguments: loggedArgs
|
|
1214
|
+
});
|
|
1215
|
+
const run = async () => {
|
|
1216
|
+
dispatchLog.start();
|
|
1217
|
+
let outcome;
|
|
1218
|
+
try {
|
|
1219
|
+
outcome = await host.callTool({
|
|
1220
|
+
name: entry.name,
|
|
1221
|
+
args: parsed,
|
|
1222
|
+
callId,
|
|
1223
|
+
signal: nestedSignal,
|
|
1224
|
+
agent: exec.agent,
|
|
1225
|
+
toolSet: exec.toolSet
|
|
1226
|
+
});
|
|
1227
|
+
} catch (error) {
|
|
1228
|
+
dispatchLog.settle({ error: error instanceof Error ? error.message : String(error) });
|
|
1229
|
+
throw error;
|
|
1230
|
+
}
|
|
1231
|
+
dispatchLog.settle(outcome);
|
|
1232
|
+
if (outcome.error === void 0 && Array.isArray(outcome.content) && hasImageBlock(outcome.content)) {
|
|
1233
|
+
const imageMsg = host.createImageContext?.(outcome.content);
|
|
1234
|
+
if (imageMsg !== void 0) exec.deferContext?.(imageMsg);
|
|
1235
|
+
}
|
|
1236
|
+
if (Array.isArray(outcome.additionalContexts)) for (const contextMsg of outcome.additionalContexts) exec.deferContext?.(contextMsg);
|
|
1237
|
+
if (outcome.error === void 0 && outcome.concluded === true) exec.concludeTurn?.();
|
|
1238
|
+
if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " result discarded");
|
|
1239
|
+
if (outcome.error !== void 0) throw new Error(outcome.error);
|
|
1240
|
+
return outcome.value !== void 0 ? outcome.value : outcome.text ?? null;
|
|
1241
|
+
};
|
|
1242
|
+
const mode = isConcurrencySafe(definitions.get(entry.name), parsed) ? "parallel" : "exclusive";
|
|
1243
|
+
return scheduler.schedule(mode, run, () => {
|
|
1244
|
+
throw new Error(runToolName + " run is over; " + entry.name + " tool call abandoned");
|
|
1245
|
+
});
|
|
1246
|
+
};
|
|
1247
|
+
Object.defineProperty(functions, entry.name, {
|
|
1248
|
+
enumerable: true,
|
|
1249
|
+
configurable: true,
|
|
1250
|
+
writable: true,
|
|
1251
|
+
value: invoke
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
/**
|
|
1256
|
+
* 跑一次程序:每次重取会话授权,因为重试发生在策略变更之后。
|
|
1257
|
+
*
|
|
1258
|
+
* 单次沙箱提权(`sandbox_permissions`)只在第一次进入前审批一次,且提权后的单次执行不传
|
|
1259
|
+
* `stateOwner`,避免把单次授权固化到常驻环境中(对齐内核 `ptc.ts`)。
|
|
1260
|
+
*/
|
|
1261
|
+
let approvedEscalatedMode;
|
|
1262
|
+
const attempt = async () => {
|
|
1263
|
+
const authority = await host.runAuthority?.(exec.agent, nestedSignal);
|
|
1264
|
+
const standingPolicy = authority?.sandboxPolicy;
|
|
1265
|
+
let effectivePolicy = standingPolicy;
|
|
1266
|
+
let escalated = false;
|
|
1267
|
+
if (input.sandboxPermissions !== void 0 && input.justification !== void 0) {
|
|
1268
|
+
if (standingPolicy === void 0 || typeof host.escalateSandbox !== "function") throw new Error("sandbox_permissions is not available for this PTC runtime");
|
|
1269
|
+
if (approvedEscalatedMode === void 0) {
|
|
1270
|
+
const nextPolicy = await host.escalateSandbox({
|
|
1271
|
+
requestedMode: input.sandboxPermissions,
|
|
1272
|
+
justification: input.justification,
|
|
1273
|
+
standingPolicy,
|
|
1274
|
+
agent: exec.agent,
|
|
1275
|
+
callId: String(exec.callId),
|
|
1276
|
+
toolName: runToolName,
|
|
1277
|
+
signal: exec.signal
|
|
1278
|
+
});
|
|
1279
|
+
effectivePolicy = nextPolicy;
|
|
1280
|
+
approvedEscalatedMode = modeOfPolicy(nextPolicy);
|
|
1281
|
+
} else if (typeof standingPolicy === "object" && standingPolicy !== null) effectivePolicy = {
|
|
1282
|
+
...standingPolicy,
|
|
1283
|
+
mode: approvedEscalatedMode
|
|
1284
|
+
};
|
|
1285
|
+
escalated = modeOfPolicy(effectivePolicy) !== modeOfPolicy(standingPolicy);
|
|
1286
|
+
}
|
|
1287
|
+
const request = {
|
|
1288
|
+
program: input.code,
|
|
1289
|
+
bindings: [{
|
|
1290
|
+
global: "tools",
|
|
1291
|
+
functions,
|
|
1292
|
+
errorClass: {
|
|
1293
|
+
name: "ToolCallError",
|
|
1294
|
+
memberNameProperty: "toolName"
|
|
1295
|
+
}
|
|
1296
|
+
}],
|
|
1297
|
+
...escalated ? {} : { stateOwner: owner },
|
|
1298
|
+
...authority?.cwd === void 0 ? {} : { cwd: authority.cwd },
|
|
1299
|
+
...effectivePolicy === void 0 ? {} : { sandboxPolicy: effectivePolicy },
|
|
1300
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
1301
|
+
signal: nestedSignal
|
|
1302
|
+
};
|
|
1303
|
+
const spec = typeof runtime.resolve === "function" ? runtime.resolve(request) : request;
|
|
1304
|
+
maxSubCalls = resolveSubCallBudget(spec, rules);
|
|
1305
|
+
return await catalogScope.run(catalog, () => runtime.run(spec));
|
|
1306
|
+
};
|
|
1307
|
+
let result;
|
|
1308
|
+
try {
|
|
1309
|
+
try {
|
|
1310
|
+
result = await attempt();
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
if (!isRetainedPolicyChange(error)) throw error;
|
|
1313
|
+
await host.releaseOwner(owner);
|
|
1314
|
+
host.stateOwner(exec.agent);
|
|
1315
|
+
result = await attempt();
|
|
1316
|
+
}
|
|
1317
|
+
} finally {
|
|
1318
|
+
runController.abort(runToolName + " settled");
|
|
1319
|
+
await scheduler.drain();
|
|
1320
|
+
}
|
|
1321
|
+
if (result.error !== void 0) {
|
|
1322
|
+
const logsText = Array.isArray(result.logs) && result.logs.length > 0 ? "\nCaptured output:\n" + result.logs.join("\n") : "";
|
|
1323
|
+
const sandboxText = result.sandbox === void 0 ? "" : "\nFile sandbox: " + result.sandbox.mode + (result.sandbox.enforcement === void 0 ? "" : "; enforcement: " + result.sandbox.enforcement) + (result.sandbox.denied ? "; operation denied" : "") + ".";
|
|
1324
|
+
const hint = result.sandbox?.denied ? escalationGuidance(runtime.sandboxMode) : "";
|
|
1325
|
+
throw new Error("program failed (" + result.error.kind + "): " + result.error.message + logsText + sandboxText + hint);
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
logs: [...result.logs ?? []],
|
|
1329
|
+
...result.value === void 0 ? {} : { result: result.value },
|
|
1330
|
+
...result.sandbox === void 0 ? {} : { sandbox: {
|
|
1331
|
+
mode: result.sandbox.mode,
|
|
1332
|
+
denied: result.sandbox.denied,
|
|
1333
|
+
...result.sandbox.enforcement === void 0 ? {} : { enforcement: result.sandbox.enforcement }
|
|
1334
|
+
} }
|
|
1335
|
+
};
|
|
1336
|
+
} finally {
|
|
1337
|
+
exec.signal.removeEventListener("abort", onOuterAbort);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
Object.defineProperty(definition, "description", {
|
|
1342
|
+
enumerable: true,
|
|
1343
|
+
configurable: true,
|
|
1344
|
+
get: () => buildTransportDescription(safeRuntime(host))
|
|
1345
|
+
});
|
|
1346
|
+
Object.defineProperty(definition, "parameters", {
|
|
1347
|
+
enumerable: true,
|
|
1348
|
+
configurable: true,
|
|
1349
|
+
get: () => buildTransportParameters(safeRuntime(host))
|
|
1350
|
+
});
|
|
1351
|
+
return definition;
|
|
1352
|
+
}
|
|
1353
|
+
function safeRuntime(host) {
|
|
1354
|
+
try {
|
|
1355
|
+
return host.runtime();
|
|
1356
|
+
} catch {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
/** 只有明确声明了 `timeout: undefined` 自身属性(而非测试假对象省略字段)才视作不支持 `timeoutMs`。 */
|
|
1361
|
+
function isStrictTimeoutUnsupported(runtime) {
|
|
1362
|
+
return Object.prototype.hasOwnProperty.call(runtime, "timeout") && runtime.timeout === void 0;
|
|
1363
|
+
}
|
|
1364
|
+
function modeOfPolicy(policy) {
|
|
1365
|
+
if (typeof policy !== "object" || policy === null) return void 0;
|
|
1366
|
+
const mode = policy.mode;
|
|
1367
|
+
return typeof mode === "string" ? mode : void 0;
|
|
1368
|
+
}
|
|
1369
|
+
function hasImageBlock(content) {
|
|
1370
|
+
return content.some((block) => typeof block === "object" && block !== null && block.type === "image");
|
|
1371
|
+
}
|
|
1372
|
+
function resolveSubCallBudget(spec, rules) {
|
|
1373
|
+
if (rules.options.explicitMaxSubCalls) return rules.options.maxSubCalls;
|
|
1374
|
+
if (spec.timeoutMs === null) return Infinity;
|
|
1375
|
+
if (typeof spec.timeoutMs === "number" && Number.isFinite(spec.timeoutMs) && spec.timeoutMs > 0) return Math.max(1, Math.floor(spec.timeoutMs / 6e4 * rules.options.maxSubCallsPerMinute));
|
|
1376
|
+
return rules.options.maxSubCalls;
|
|
1377
|
+
}
|
|
1378
|
+
/** 没有会话可写时的空记录。 */
|
|
1379
|
+
const NO_DISPATCH_LOG = Object.freeze({
|
|
1380
|
+
start() {},
|
|
1381
|
+
settle() {}
|
|
1382
|
+
});
|
|
1383
|
+
/** 回放里单个文本块的上限:日志要有界,程序拿到的值不受影响。 */
|
|
1384
|
+
const DISPATCH_LOG_TEXT_LIMIT = 8e3;
|
|
1385
|
+
/**
|
|
1386
|
+
* 造一条子调用的回放记录。
|
|
1387
|
+
*/
|
|
1388
|
+
function createDispatchLog(agent, entry) {
|
|
1389
|
+
const session = typeof agent === "object" && agent !== null ? agent.session : void 0;
|
|
1390
|
+
const append = session?.append;
|
|
1391
|
+
if (typeof append !== "function") return NO_DISPATCH_LOG;
|
|
1392
|
+
const base = {
|
|
1393
|
+
rootCallId: entry.rootCallId,
|
|
1394
|
+
parentCallId: entry.parentCallId,
|
|
1395
|
+
subCallId: entry.subCallId,
|
|
1396
|
+
name: entry.name,
|
|
1397
|
+
arguments: entry.arguments
|
|
1398
|
+
};
|
|
1399
|
+
const write = (type, data) => {
|
|
1400
|
+
try {
|
|
1401
|
+
append.call(session, type, data);
|
|
1402
|
+
} catch {}
|
|
1403
|
+
};
|
|
1404
|
+
return {
|
|
1405
|
+
start() {
|
|
1406
|
+
write("tool/ptc-dispatch-start", { ...base });
|
|
1407
|
+
},
|
|
1408
|
+
settle(outcome) {
|
|
1409
|
+
write("tool/ptc-dispatch", {
|
|
1410
|
+
...base,
|
|
1411
|
+
isError: outcome.error !== void 0,
|
|
1412
|
+
content: clipContent(outcome.content),
|
|
1413
|
+
...outcome.meta === void 0 ? {} : { meta: outcome.meta },
|
|
1414
|
+
...outcome.errorInfo === void 0 ? {} : { error: outcome.errorInfo }
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* 把内容块裁到日志能承受的长度。
|
|
1421
|
+
*/
|
|
1422
|
+
function clipContent(content) {
|
|
1423
|
+
if (content === void 0) return [];
|
|
1424
|
+
return content.map((block) => {
|
|
1425
|
+
if (typeof block !== "object" || block === null) return block;
|
|
1426
|
+
const record = block;
|
|
1427
|
+
const text = record["text"];
|
|
1428
|
+
if (typeof text !== "string" || text.length <= DISPATCH_LOG_TEXT_LIMIT) return block;
|
|
1429
|
+
return {
|
|
1430
|
+
...record,
|
|
1431
|
+
text: text.slice(0, DISPATCH_LOG_TEXT_LIMIT) + "…[truncated in dispatch log]"
|
|
1432
|
+
};
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* 一条调用所属调用树的根 id。
|
|
1437
|
+
*/
|
|
1438
|
+
function rootCallIdOf(callId) {
|
|
1439
|
+
const at = callId.indexOf(NESTED_CALL_ID_MARKER);
|
|
1440
|
+
return at > 0 ? callId.slice(0, at) : callId;
|
|
1441
|
+
}
|
|
1442
|
+
/** 内核在常驻环境遇到策略变更时的拒绝文本;其余异常一律原样上抛。 */
|
|
1443
|
+
function isRetainedPolicyChange(error) {
|
|
1444
|
+
return (error instanceof Error ? error.message : String(error)).includes("release the retained environment before changing its sandbox policy");
|
|
1445
|
+
}
|
|
1446
|
+
/** 校验单次沙箱提权参数配对(对齐 `@deepseek-ai/dsh-sandbox` 的 `validateEscalationArgs`)。 */
|
|
1447
|
+
function validateEscalationArgs(sandboxPermissions, justification) {
|
|
1448
|
+
if (sandboxPermissions !== void 0 && justification === void 0) throw new Error("invalid escalation: sandbox_permissions requires a justification");
|
|
1449
|
+
if (justification !== void 0 && sandboxPermissions === void 0) throw new Error("invalid escalation: justification is only valid together with sandbox_permissions");
|
|
1450
|
+
if (sandboxPermissions === void 0 && justification === void 0) return {};
|
|
1451
|
+
if (typeof sandboxPermissions !== "string" || !ESCALATION_TARGETS.includes(sandboxPermissions)) throw new Error("invalid sandbox_permissions: expected \"workspace-write\" or \"danger-full-access\"");
|
|
1452
|
+
if (typeof justification !== "string" || justification.trim().length === 0) throw new Error("invalid justification: expected a non-empty sentence");
|
|
1453
|
+
return {
|
|
1454
|
+
sandboxPermissions,
|
|
1455
|
+
justification
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
/** 校验并读出工具参数。 */
|
|
1459
|
+
function readArguments(args) {
|
|
1460
|
+
if (typeof args !== "object" || args === null) throw new Error("codemode: expected an arguments object");
|
|
1461
|
+
const record = args;
|
|
1462
|
+
const code = record["code"];
|
|
1463
|
+
const description = record["description"];
|
|
1464
|
+
if (typeof code !== "string" || code.trim().length === 0) throw new Error("codemode: invalid code: expected a non-empty string");
|
|
1465
|
+
if (typeof description !== "string" || description.trim().length === 0) throw new Error("codemode: invalid description: expected a non-empty string");
|
|
1466
|
+
const escalation = validateEscalationArgs(record["sandbox_permissions"], record["justification"]);
|
|
1467
|
+
const timeoutMs = record["timeoutMs"];
|
|
1468
|
+
if (timeoutMs === void 0) return {
|
|
1469
|
+
code,
|
|
1470
|
+
description,
|
|
1471
|
+
...escalation
|
|
1472
|
+
};
|
|
1473
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
|
|
1474
|
+
return {
|
|
1475
|
+
code,
|
|
1476
|
+
description,
|
|
1477
|
+
timeoutMs,
|
|
1478
|
+
...escalation
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
/** 子调用参数必须是可无损 JSON 化的值。 */
|
|
1482
|
+
function losslessArgs(value) {
|
|
1483
|
+
if (value === void 0) return {};
|
|
1484
|
+
try {
|
|
1485
|
+
return JSON.parse(JSON.stringify(value));
|
|
1486
|
+
} catch {
|
|
1487
|
+
throw new Error("codemode: tool arguments must be lossless JSON");
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/** 一个工具的调用是否可以与兄弟调用重叠(只有明确 true 才算)。 */
|
|
1491
|
+
function isConcurrencySafe(entry, args) {
|
|
1492
|
+
if (entry === void 0 || typeof entry.isConcurrencySafe !== "function") return false;
|
|
1493
|
+
try {
|
|
1494
|
+
return entry.isConcurrencySafe(args) === true;
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* 子调用并发与互斥调度器(对齐内核 `ptc.ts` 的读写屏障):
|
|
1501
|
+
*
|
|
1502
|
+
* - `parallel` 调用可并发重叠,最多 `maxParallel` 个同时在飞;
|
|
1503
|
+
* - `exclusive` 调用必须等待所有已在飞调用结算后独占运行,并在自身结算后才放行后续调用;
|
|
1504
|
+
* - 排队期间若外层运行已中止,直接放弃且不启动任务(保证未启动调用零 `start` 事件);
|
|
1505
|
+
* - `drain()` 等待全部已启动任务结算。
|
|
1506
|
+
*/
|
|
1507
|
+
function createSubCallScheduler(maxParallel, isAborted) {
|
|
1508
|
+
const cap = Math.max(1, maxParallel);
|
|
1509
|
+
const queue = [];
|
|
1510
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1511
|
+
let exclusiveActive = false;
|
|
1512
|
+
const pump = () => {
|
|
1513
|
+
while (queue.length > 0) {
|
|
1514
|
+
const head = queue[0];
|
|
1515
|
+
if (isAborted()) {
|
|
1516
|
+
queue.shift();
|
|
1517
|
+
head.abandon();
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (exclusiveActive) return;
|
|
1521
|
+
if (head.mode === "exclusive") {
|
|
1522
|
+
if (inFlight.size > 0) return;
|
|
1523
|
+
exclusiveActive = true;
|
|
1524
|
+
queue.shift();
|
|
1525
|
+
head.start();
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (inFlight.size >= cap) return;
|
|
1529
|
+
queue.shift();
|
|
1530
|
+
head.start();
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
return {
|
|
1534
|
+
schedule(mode, task, onAbandon) {
|
|
1535
|
+
return new Promise((resolve, reject) => {
|
|
1536
|
+
queue.push({
|
|
1537
|
+
mode,
|
|
1538
|
+
abandon() {
|
|
1539
|
+
try {
|
|
1540
|
+
onAbandon();
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
reject(error);
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
start() {
|
|
1546
|
+
const tracked = task().then(resolve, reject).finally(() => {
|
|
1547
|
+
inFlight.delete(tracked);
|
|
1548
|
+
if (mode === "exclusive") exclusiveActive = false;
|
|
1549
|
+
pump();
|
|
1550
|
+
}).then(() => void 0, () => void 0);
|
|
1551
|
+
inFlight.add(tracked);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
pump();
|
|
1555
|
+
});
|
|
1556
|
+
},
|
|
1557
|
+
async drain() {
|
|
1558
|
+
pump();
|
|
1559
|
+
while (inFlight.size > 0) {
|
|
1560
|
+
await Promise.allSettled([...inFlight]);
|
|
1561
|
+
pump();
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
const JSON_INDENT = " ";
|
|
1567
|
+
const MAX_JSON_INDENT_CHARS = 10;
|
|
1568
|
+
/** 迭代式 JSON 渲染:与内核 `ptc.ts` 一致,深于 5 层后自动紧凑化,防止深层缩进膨胀。 */
|
|
1569
|
+
function renderProgramValue(value) {
|
|
1570
|
+
if (typeof value === "string") return value;
|
|
1571
|
+
let normalized;
|
|
1572
|
+
try {
|
|
1573
|
+
normalized = JSON.parse(JSON.stringify(value));
|
|
1574
|
+
} catch {
|
|
1575
|
+
return String(value);
|
|
1576
|
+
}
|
|
1577
|
+
if (typeof normalized === "string") return normalized;
|
|
1578
|
+
if (normalized === void 0) return "";
|
|
1579
|
+
const chunks = [];
|
|
1580
|
+
const tasks = [{
|
|
1581
|
+
kind: "value",
|
|
1582
|
+
value: normalized,
|
|
1583
|
+
depth: 0,
|
|
1584
|
+
compact: false
|
|
1585
|
+
}];
|
|
1586
|
+
for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
|
|
1587
|
+
if (task.kind === "text") {
|
|
1588
|
+
chunks.push(task.text);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const current = task.value;
|
|
1592
|
+
if (current === null || typeof current === "boolean" || typeof current === "number") {
|
|
1593
|
+
chunks.push(String(current));
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (typeof current === "string") {
|
|
1597
|
+
chunks.push(JSON.stringify(current));
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const compact = task.compact || (task.depth + 1) * 2 > MAX_JSON_INDENT_CHARS;
|
|
1601
|
+
const childDepth = task.depth + 1;
|
|
1602
|
+
if (Array.isArray(current)) {
|
|
1603
|
+
chunks.push("[");
|
|
1604
|
+
if (current.length === 0) {
|
|
1605
|
+
chunks.push("]");
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
tasks.push({
|
|
1609
|
+
kind: "text",
|
|
1610
|
+
text: compact ? "]" : `\n${JSON_INDENT.repeat(task.depth)}]`
|
|
1611
|
+
});
|
|
1612
|
+
for (let index = current.length - 1; index >= 0; index -= 1) {
|
|
1613
|
+
tasks.push({
|
|
1614
|
+
kind: "value",
|
|
1615
|
+
value: current[index],
|
|
1616
|
+
depth: childDepth,
|
|
1617
|
+
compact
|
|
1618
|
+
});
|
|
1619
|
+
tasks.push({
|
|
1620
|
+
kind: "text",
|
|
1621
|
+
text: compact ? index === 0 ? "" : "," : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}`
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
if (typeof current === "object") {
|
|
1627
|
+
const record = current;
|
|
1628
|
+
const keys = Object.keys(record);
|
|
1629
|
+
chunks.push("{");
|
|
1630
|
+
if (keys.length === 0) {
|
|
1631
|
+
chunks.push("}");
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
tasks.push({
|
|
1635
|
+
kind: "text",
|
|
1636
|
+
text: compact ? "}" : `\n${JSON_INDENT.repeat(task.depth)}}`
|
|
1637
|
+
});
|
|
1638
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
1639
|
+
const key = keys[index];
|
|
1640
|
+
tasks.push({
|
|
1641
|
+
kind: "value",
|
|
1642
|
+
value: record[key],
|
|
1643
|
+
depth: childDepth,
|
|
1644
|
+
compact
|
|
1645
|
+
});
|
|
1646
|
+
tasks.push({
|
|
1647
|
+
kind: "text",
|
|
1648
|
+
text: compact ? `${index === 0 ? "" : ","}${JSON.stringify(key)}:` : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return chunks.join("");
|
|
1654
|
+
}
|
|
1655
|
+
//#endregion
|
|
1656
|
+
//#region src/entry-codemode.ts
|
|
1657
|
+
/**
|
|
1658
|
+
* Code Mode 预设行 —— 本包自己的「原生工具 + 程序传输」呈现方式。
|
|
1659
|
+
*
|
|
1660
|
+
* 这一行挂在 `dispatch` 预设里(见 presets/dispatch/agent.cordis.yml 与 src/host/agent-preset.ts),
|
|
1661
|
+
* 因此它注册的工具、它选中的 executor、它提供的提示词段都属于**这个预设的会话**,其它预设
|
|
1662
|
+
* 完全不受影响。
|
|
1663
|
+
*
|
|
1664
|
+
* ## 它替掉了什么
|
|
1665
|
+
*
|
|
1666
|
+
* 内核的 `ptc` 呈现方式只把 `run_code` 交给模型,其余工具一律硬拒(UNKNOWN_TOOL),
|
|
1667
|
+
* `standard` 又完全没有程序传输——「原生工具 + 程序」这个组合在内核里表达不出来。本行用
|
|
1668
|
+
* `standard` 的原生工具面 + 自己的 `code_run` 把这个组合重建,再把预设清单以外的工具
|
|
1669
|
+
* 挡在上下文之外,只留程序内可达。
|
|
1670
|
+
*
|
|
1671
|
+
* ## 四件事
|
|
1672
|
+
*
|
|
1673
|
+
* 1. 注册发现工具(list / search / describe / debug);
|
|
1674
|
+
* 2. 注册程序传输工具 `code_run`;
|
|
1675
|
+
* 3. 选一个执行器:`describe()` 丢掉预设清单以外的名字,`execute()` 派发前拒绝模型直呼,
|
|
1676
|
+
* 其余整批委托给内核原生执行器(完整策略 / 审批 / post-execute 管线);
|
|
1677
|
+
* 4. 提供**恒定**的 Code Mode 说明段,替换内核那份 SDK 声明段。
|
|
1678
|
+
*
|
|
1679
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/codemode
|
|
1680
|
+
*/
|
|
1681
|
+
/** Cordis 插件名。 */
|
|
1682
|
+
const name = "codemode";
|
|
1683
|
+
/** 依赖的服务。 */
|
|
1684
|
+
const inject = ["tools", "systemPrompt"];
|
|
1685
|
+
/** 决策环长度:只留给 `code_debug`。 */
|
|
1686
|
+
const DEBUG_RING = 50;
|
|
1687
|
+
/** 严格加宽阶梯(对齐 `@deepseek-ai/dsh-sandbox` 的 `WIDER_MODES`)。 */
|
|
1688
|
+
const WIDER_SANDBOX_MODES = {
|
|
1689
|
+
"read-only": ["workspace-write", "danger-full-access"],
|
|
1690
|
+
"workspace-write": ["danger-full-access"]
|
|
1691
|
+
};
|
|
1692
|
+
/**
|
|
1693
|
+
* 装配本行。
|
|
1694
|
+
*
|
|
1695
|
+
* @param ctx - 预设作用域上的行上下文。
|
|
1696
|
+
* @param config - 行配置({@link CodemodeConfig})。
|
|
1697
|
+
*/
|
|
1698
|
+
function apply(ctx, config = {}) {
|
|
1699
|
+
const context = ctx;
|
|
1700
|
+
const log = (level, message) => {
|
|
1701
|
+
try {
|
|
1702
|
+
context.logger?.[level]?.(message);
|
|
1703
|
+
} catch {}
|
|
1704
|
+
};
|
|
1705
|
+
const tools = context.tools;
|
|
1706
|
+
const runtimeOf = () => {
|
|
1707
|
+
try {
|
|
1708
|
+
return context.get?.("ptcRuntime");
|
|
1709
|
+
} catch {
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
/** 读一个内核服务(缺失或未挂载时为 undefined)。 */
|
|
1714
|
+
const serviceOf = (name) => {
|
|
1715
|
+
try {
|
|
1716
|
+
return context.get?.(name);
|
|
1717
|
+
} catch {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
/**
|
|
1722
|
+
* 取会话级运行授权:目录与文件策略都读内核服务,本行只转交、不自己造。
|
|
1723
|
+
*
|
|
1724
|
+
* `ptcRuntime.run()` 在 DSH 0.2.2 起要求已解析的策略与目录;缺了这两项,程序会带着部署
|
|
1725
|
+
* 默认值(通常是 read-only 与部署根目录)执行,或者干脆被运行时拒绝。老代 harness 没有这两个
|
|
1726
|
+
* 服务,返回 undefined 让它照旧兜底。
|
|
1727
|
+
*/
|
|
1728
|
+
const runAuthority = async (agent, signal) => {
|
|
1729
|
+
const authority = {};
|
|
1730
|
+
const session = typeof agent === "object" && agent !== null ? agent.session : void 0;
|
|
1731
|
+
const policy = serviceOf("sandboxPolicy");
|
|
1732
|
+
if (typeof policy?.resolve === "function") try {
|
|
1733
|
+
const resolved = policy.resolve(session === void 0 ? {} : { session });
|
|
1734
|
+
if (resolved !== void 0) authority.sandboxPolicy = resolved;
|
|
1735
|
+
} catch {}
|
|
1736
|
+
const directories = serviceOf("workingDirectory");
|
|
1737
|
+
if (agent !== void 0 && typeof directories?.ensure === "function") try {
|
|
1738
|
+
authority.cwd = await directories.ensure(agent, signal);
|
|
1739
|
+
} catch {}
|
|
1740
|
+
return authority;
|
|
1741
|
+
};
|
|
1742
|
+
const rules = createRules(config);
|
|
1743
|
+
const retention = createRetention(runtimeOf);
|
|
1744
|
+
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1745
|
+
retention.dispose();
|
|
1746
|
+
}, "codemode retained environments");
|
|
1747
|
+
const scopeKey = findScopeKey(ctx);
|
|
1748
|
+
if (rules.options.nativeScope && scopeKey === void 0) log("warn", "[codemode] no registry scope tag on this context: masking falls back to name rules only (" + rules.options.hiddenPrefixes.join(", ") + "). Mount this row inside an agent preset to hide every tool the preset does not own.");
|
|
1749
|
+
/** 最近一次 describe 收到的完整定义集(含被遮蔽的)。 */
|
|
1750
|
+
let catalog = [];
|
|
1751
|
+
const debugRing = [];
|
|
1752
|
+
let firstDescribeLogged = false;
|
|
1753
|
+
const escalateSandbox = async (input) => {
|
|
1754
|
+
const record = typeof input.standingPolicy === "object" && input.standingPolicy !== null ? input.standingPolicy : void 0;
|
|
1755
|
+
const effectiveMode = typeof record?.["mode"] === "string" ? record["mode"] : void 0;
|
|
1756
|
+
if (record === void 0 || effectiveMode === void 0) throw new Error("sandbox_permissions is not available for this PTC runtime");
|
|
1757
|
+
if (input.requestedMode === effectiveMode) return input.standingPolicy;
|
|
1758
|
+
if (!(WIDER_SANDBOX_MODES[effectiveMode] ?? []).includes(input.requestedMode)) throw new Error(`sandbox escalation to "${input.requestedMode}" is not strictly wider than this call's current "${effectiveMode}" mode`);
|
|
1759
|
+
const approver = serviceOf("approval");
|
|
1760
|
+
if (typeof approver?.request !== "function") throw new Error(`sandbox escalation to "${input.requestedMode}" requires approval, but no approval service is composed`);
|
|
1761
|
+
if (input.agent === void 0) throw new Error(`sandbox escalation to "${input.requestedMode}" requires approval, but the call has no agent to route it through`);
|
|
1762
|
+
const outcome = await approver.request({
|
|
1763
|
+
agent: input.agent,
|
|
1764
|
+
toolName: input.toolName,
|
|
1765
|
+
callId: input.callId,
|
|
1766
|
+
reason: `escalate sandbox to ${input.requestedMode}: ${input.justification}`,
|
|
1767
|
+
displayReason: {
|
|
1768
|
+
en: `Allow this program to run once with ${input.requestedMode} access: ${input.justification}`,
|
|
1769
|
+
zh: `允许本次程序以 ${input.requestedMode} 权限单次运行:${input.justification}`
|
|
1770
|
+
},
|
|
1771
|
+
signal: input.signal
|
|
1772
|
+
});
|
|
1773
|
+
switch (outcome) {
|
|
1774
|
+
case "allowed-once": return {
|
|
1775
|
+
...record,
|
|
1776
|
+
mode: input.requestedMode
|
|
1777
|
+
};
|
|
1778
|
+
case "rejected": throw new Error(`the user rejected escalating this program to "${input.requestedMode}"; it stays denied, so stop and explain instead of working around it`);
|
|
1779
|
+
case "cancelled": throw new Error(`approval for escalating to "${input.requestedMode}" was cancelled`);
|
|
1780
|
+
case "unavailable": throw new Error(`sandbox escalation to "${input.requestedMode}" requires approval, but no approval channel is available`);
|
|
1781
|
+
default: throw new Error(`unexpected approval outcome: ${String(outcome)}`);
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
const host = {
|
|
1785
|
+
catalog: () => catalog,
|
|
1786
|
+
runtime: runtimeOf,
|
|
1787
|
+
stateOwner: (agent) => retention.owner(agent),
|
|
1788
|
+
releaseOwner: (owner) => retention.release(owner),
|
|
1789
|
+
runAuthority,
|
|
1790
|
+
escalateSandbox,
|
|
1791
|
+
createImageContext(content) {
|
|
1792
|
+
return createUserMessage({
|
|
1793
|
+
content,
|
|
1794
|
+
source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
|
|
1795
|
+
});
|
|
1796
|
+
},
|
|
1797
|
+
async callTool(request) {
|
|
1798
|
+
const call = {
|
|
1799
|
+
type: "tool-call",
|
|
1800
|
+
id: request.callId,
|
|
1801
|
+
name: request.name,
|
|
1802
|
+
arguments: stringifyArguments(request.args)
|
|
1803
|
+
};
|
|
1804
|
+
let batch;
|
|
1805
|
+
try {
|
|
1806
|
+
batch = await tools.execute({
|
|
1807
|
+
calls: [call],
|
|
1808
|
+
signal: request.signal,
|
|
1809
|
+
...request.agent === void 0 ? {} : { agent: request.agent },
|
|
1810
|
+
...request.toolSet === void 0 ? {} : { toolSet: request.toolSet }
|
|
1811
|
+
});
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
return { error: messageOf(error) };
|
|
1814
|
+
}
|
|
1815
|
+
const result = batch.results?.[0]?.result;
|
|
1816
|
+
if (result === void 0) return { error: "sub-call produced no result" };
|
|
1817
|
+
const content = result.content ?? [];
|
|
1818
|
+
const additionalContexts = Array.isArray(batch.additionalContexts) && batch.additionalContexts.length > 0 ? batch.additionalContexts : void 0;
|
|
1819
|
+
if (result.isError === true) return {
|
|
1820
|
+
error: errorTextOf(result),
|
|
1821
|
+
content,
|
|
1822
|
+
...errorInfoOf(result),
|
|
1823
|
+
...additionalContexts === void 0 ? {} : { additionalContexts }
|
|
1824
|
+
};
|
|
1825
|
+
const meta = result.meta;
|
|
1826
|
+
return {
|
|
1827
|
+
...result.value !== void 0 ? { value: result.value } : { text: textOf(result.content) },
|
|
1828
|
+
content,
|
|
1829
|
+
...meta === void 0 ? {} : { meta },
|
|
1830
|
+
...additionalContexts === void 0 ? {} : { additionalContexts },
|
|
1831
|
+
...batch.concluded === true ? { concluded: true } : {}
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
for (const definition of createDiscoveryDefinitions(rules, () => currentCatalogSnapshot() ?? catalog, () => debugRing)) tools.register(definition);
|
|
1836
|
+
tools.register(createTransportDefinition(host, rules));
|
|
1837
|
+
if (typeof tools.setExecutor !== "function") {
|
|
1838
|
+
log("warn", "[codemode] this DSH build has no ctx.tools.setExecutor(); Code Mode requires DSH 0.2.2 or later. Switch this session to the built-in ptc preset.");
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const delegate = new NativeToolExecutor(ctx);
|
|
1842
|
+
const view = () => resolveCatalogView(tools, scopeKey, catalog.map((entry) => entry.name));
|
|
1843
|
+
const executor = createCodemodeExecutor(ctx, rules, {
|
|
1844
|
+
delegate,
|
|
1845
|
+
view,
|
|
1846
|
+
viewForName: (toolName) => resolveNameView(tools, scopeKey, toolName),
|
|
1847
|
+
sectionText: () => {
|
|
1848
|
+
const runtime = runtimeOf();
|
|
1849
|
+
return buildCodemodeSectionText({
|
|
1850
|
+
runToolName: rules.options.runToolName,
|
|
1851
|
+
metaTools: rules.options.metaTools,
|
|
1852
|
+
language: runtime?.language,
|
|
1853
|
+
programFormat: runtime?.programFormat
|
|
1854
|
+
});
|
|
1855
|
+
},
|
|
1856
|
+
onCatalog: (definitions) => {
|
|
1857
|
+
catalog = definitions.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
1858
|
+
},
|
|
1859
|
+
onDescribe: (decision) => {
|
|
1860
|
+
const hidden = summarizeHidden(decision.masked);
|
|
1861
|
+
debugRing.push({
|
|
1862
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1863
|
+
visible: decision.visible,
|
|
1864
|
+
hidden
|
|
1865
|
+
});
|
|
1866
|
+
while (debugRing.length > DEBUG_RING) debugRing.shift();
|
|
1867
|
+
const namespaces = hidden.namespaces.map((entry) => entry.namespace + "=" + String(entry.count)).join(", ");
|
|
1868
|
+
const line = "[codemode] masked " + String(hidden.total) + " non-standard tool(s)" + (namespaces === "" ? "" : " (" + namespaces + ")") + "; request tools: " + decision.visible.join(", ");
|
|
1869
|
+
if (!firstDescribeLogged) {
|
|
1870
|
+
firstDescribeLogged = true;
|
|
1871
|
+
log("info", line);
|
|
1872
|
+
} else log("debug", line);
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
tools.setExecutor(executor);
|
|
1876
|
+
const scopeNote = scopeKey === void 0 ? "" : ", everything outside this preset scope";
|
|
1877
|
+
log("info", "[codemode] active: transport [" + rules.options.runToolName + "], discovery [" + rules.options.metaTools.join(", ") + "], hiding [" + rules.options.hiddenPrefixes.join(", ") + "]" + scopeNote);
|
|
1878
|
+
}
|
|
1879
|
+
/** 把一个工具定义投影成程序清单条目。 */
|
|
1880
|
+
function toCatalogEntry(definition) {
|
|
1881
|
+
if (typeof definition !== "object" || definition === null) return void 0;
|
|
1882
|
+
const record = definition;
|
|
1883
|
+
const toolName = record["name"];
|
|
1884
|
+
if (typeof toolName !== "string" || toolName.length === 0) return void 0;
|
|
1885
|
+
const description = typeof record["description"] === "string" ? record["description"] : "";
|
|
1886
|
+
const safe = record["isConcurrencySafe"];
|
|
1887
|
+
return {
|
|
1888
|
+
name: toolName,
|
|
1889
|
+
description,
|
|
1890
|
+
parameters: record["parameters"],
|
|
1891
|
+
...typeof safe === "function" ? { isConcurrencySafe: safe } : {}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
/** 参数序列化成线上形状(`ToolCallBlock.arguments` 是字符串)。 */
|
|
1895
|
+
function stringifyArguments(args) {
|
|
1896
|
+
if (args === void 0) return "{}";
|
|
1897
|
+
try {
|
|
1898
|
+
return JSON.stringify(args);
|
|
1899
|
+
} catch {
|
|
1900
|
+
return "{}";
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
/** 一条失败结果的面向前模型文本。 */
|
|
1904
|
+
function errorTextOf(result) {
|
|
1905
|
+
const message = typeof result.error?.message === "string" ? result.error.message : void 0;
|
|
1906
|
+
if (message !== void 0 && message.length > 0) return message;
|
|
1907
|
+
const text = textOf(result.content);
|
|
1908
|
+
return text.length > 0 ? text : "tool call failed";
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* 读出结构化失败身份。
|
|
1912
|
+
*
|
|
1913
|
+
* 内核把 `result.error.info` 原样放进回放事件的 `error` 字段;缺任一必填项时给空
|
|
1914
|
+
* 对象,让调用方可以无条件展开。
|
|
1915
|
+
*
|
|
1916
|
+
* @param result - 一条失败结果。
|
|
1917
|
+
* @returns 带 `errorInfo` 的对象,或空对象。
|
|
1918
|
+
*/
|
|
1919
|
+
function errorInfoOf(result) {
|
|
1920
|
+
const info = result.error?.info;
|
|
1921
|
+
if (info === void 0 || info === null) return {};
|
|
1922
|
+
const name = nonEmptyString(info.name);
|
|
1923
|
+
const code = nonEmptyString(info.code);
|
|
1924
|
+
if (name === void 0 || code === void 0) return {};
|
|
1925
|
+
const reason = nonEmptyString(info.reason);
|
|
1926
|
+
return { errorInfo: {
|
|
1927
|
+
name,
|
|
1928
|
+
code,
|
|
1929
|
+
...reason === void 0 ? {} : { reason }
|
|
1930
|
+
} };
|
|
1931
|
+
}
|
|
1932
|
+
/** 非空字符串,或 `undefined`。 */
|
|
1933
|
+
function nonEmptyString(value) {
|
|
1934
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1935
|
+
}
|
|
1936
|
+
/** 把内容块里的文本拼起来。 */
|
|
1937
|
+
function textOf(content) {
|
|
1938
|
+
if (!Array.isArray(content)) return "";
|
|
1939
|
+
const parts = [];
|
|
1940
|
+
for (const block of content) {
|
|
1941
|
+
if (typeof block !== "object" || block === null) continue;
|
|
1942
|
+
const record = block;
|
|
1943
|
+
if (record["type"] === "text" && typeof record["text"] === "string") parts.push(record["text"]);
|
|
1944
|
+
}
|
|
1945
|
+
return parts.join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
/** 一个抛出值的消息。 */
|
|
1948
|
+
function messageOf(error) {
|
|
1949
|
+
return error instanceof Error ? error.message : String(error);
|
|
1950
|
+
}
|
|
1951
|
+
//#endregion
|
|
1952
|
+
export { apply, inject, name };
|