@eddyskywalker/dsh-chatgpt-subscription 0.58.0 → 0.59.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 +17 -0
- package/lib/client.js +136 -23
- package/lib/client.js.map +1 -1
- package/lib/codemode.js +1565 -0
- package/lib/index.js +414 -64
- package/lib/types/client/CodexImageToolView.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 +183 -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 +258 -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/package.json +6 -1
- package/presets/dispatch/README.md +12 -3
- package/presets/dispatch/agent.cordis.yml +10 -9
- package/presets/dispatch/preset.yml +1 -1
package/lib/codemode.js
ADDED
|
@@ -0,0 +1,1565 @@
|
|
|
1
|
+
import { NativeToolExecutor } from "@deepseek-ai/dsh-tool-executor-native";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/host/codemode/discovery.ts
|
|
4
|
+
/**
|
|
5
|
+
* 造三个发现工具(外加可选的 `code_debug`)。
|
|
6
|
+
*
|
|
7
|
+
* @param rules - 已归一化的规则。
|
|
8
|
+
* @param catalog - 当前清单的读取函数。
|
|
9
|
+
* @param debug - 调试信息读取函数(`code_debug` 用)。
|
|
10
|
+
* @returns 原始工具定义数组。
|
|
11
|
+
*/
|
|
12
|
+
function createDiscoveryDefinitions(rules, catalog, debug) {
|
|
13
|
+
const [listTool, searchTool, describeTool, debugTool] = rules.options.metaTools;
|
|
14
|
+
const definitions = [];
|
|
15
|
+
if (listTool !== void 0) definitions.push(listDefinition(listTool, catalog));
|
|
16
|
+
if (searchTool !== void 0) definitions.push(searchDefinition(searchTool, rules, catalog));
|
|
17
|
+
if (describeTool !== void 0) definitions.push(describeDefinition(describeTool, catalog));
|
|
18
|
+
if (rules.options.debug && debugTool !== void 0) definitions.push(debugDefinition(debugTool, debug));
|
|
19
|
+
return definitions;
|
|
20
|
+
}
|
|
21
|
+
/** `code_tools_list`:整份清单(含被遮蔽的)。 */
|
|
22
|
+
function listDefinition(name, catalog) {
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
description: "List every tool this session can call from a program, including the ones hidden from the model list.",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
additionalProperties: false,
|
|
29
|
+
properties: { namespace: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Only tools of this namespace (MCP server name, or \"plugin\")."
|
|
32
|
+
} }
|
|
33
|
+
},
|
|
34
|
+
output: {
|
|
35
|
+
schema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
required: ["tools"],
|
|
39
|
+
properties: { tools: {
|
|
40
|
+
type: "array",
|
|
41
|
+
items: {
|
|
42
|
+
type: "object",
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
required: [
|
|
45
|
+
"name",
|
|
46
|
+
"description",
|
|
47
|
+
"namespace"
|
|
48
|
+
],
|
|
49
|
+
properties: {
|
|
50
|
+
name: { type: "string" },
|
|
51
|
+
description: { type: "string" },
|
|
52
|
+
namespace: { type: "string" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} }
|
|
56
|
+
},
|
|
57
|
+
render: (_args, value) => [{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: value.tools.map((hit) => hit.name + " [" + hit.namespace + "] " + hit.description).join("\n")
|
|
60
|
+
}]
|
|
61
|
+
},
|
|
62
|
+
async execute(args) {
|
|
63
|
+
const namespace = namespaceOf(args);
|
|
64
|
+
return { tools: catalog().filter((entry) => namespace === void 0 || namespaceOfName(entry.name) === namespace).map((entry) => ({
|
|
65
|
+
name: entry.name,
|
|
66
|
+
description: clip(entry.description),
|
|
67
|
+
namespace: namespaceOfName(entry.name)
|
|
68
|
+
})) };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** `code_tools_search`:按名字 / 描述排序检索,返回完整参数 schema。 */
|
|
73
|
+
function searchDefinition(name, rules, catalog) {
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
description: "Rank callable tools against a free-text query and return their full parameter schemas. The intended first step before calling a hidden tool.",
|
|
77
|
+
parameters: {
|
|
78
|
+
type: "object",
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
required: ["query"],
|
|
81
|
+
properties: {
|
|
82
|
+
query: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "What you need, in words."
|
|
85
|
+
},
|
|
86
|
+
limit: {
|
|
87
|
+
type: "number",
|
|
88
|
+
description: "Maximum rows (default 8)."
|
|
89
|
+
},
|
|
90
|
+
namespace: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Only tools of this namespace."
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
output: {
|
|
97
|
+
schema: {
|
|
98
|
+
type: "object",
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
required: ["tools"],
|
|
101
|
+
properties: {
|
|
102
|
+
fallback: {
|
|
103
|
+
type: "boolean",
|
|
104
|
+
description: "True when nothing matched and the rows are a namespace browse instead."
|
|
105
|
+
},
|
|
106
|
+
tools: {
|
|
107
|
+
type: "array",
|
|
108
|
+
items: {
|
|
109
|
+
type: "object",
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
required: [
|
|
112
|
+
"name",
|
|
113
|
+
"description",
|
|
114
|
+
"parameters"
|
|
115
|
+
],
|
|
116
|
+
properties: {
|
|
117
|
+
name: { type: "string" },
|
|
118
|
+
description: { type: "string" },
|
|
119
|
+
parameters: {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
render: (_args, value) => [{
|
|
126
|
+
type: "text",
|
|
127
|
+
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")
|
|
128
|
+
}]
|
|
129
|
+
},
|
|
130
|
+
async execute(args) {
|
|
131
|
+
const record = typeof args === "object" && args !== null ? args : {};
|
|
132
|
+
const query = typeof record["query"] === "string" ? record["query"] : "";
|
|
133
|
+
const rawLimit = record["limit"];
|
|
134
|
+
const limit = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 8;
|
|
135
|
+
const namespace = namespaceOf(args);
|
|
136
|
+
const candidates = catalog().filter((entry) => namespace === void 0 || namespaceOfName(entry.name) === namespace);
|
|
137
|
+
const terms = [...new Set(tokenize(query))];
|
|
138
|
+
const { indexed, documentFrequency } = buildIndex(candidates);
|
|
139
|
+
const scored = [];
|
|
140
|
+
for (const item of indexed) {
|
|
141
|
+
const score = scoreOf(item, terms, documentFrequency, indexed.length, query);
|
|
142
|
+
if (terms.length > 0 && score <= 0) continue;
|
|
143
|
+
scored.push({
|
|
144
|
+
indexed: item,
|
|
145
|
+
score
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
scored.sort((left, right) => right.score - left.score || left.indexed.entry.name.localeCompare(right.indexed.entry.name));
|
|
149
|
+
const hits = scored.slice(0, limit);
|
|
150
|
+
if (hits.length > 0) return { tools: hits.map((row) => ({
|
|
151
|
+
name: row.indexed.entry.name,
|
|
152
|
+
description: clip(row.indexed.entry.description),
|
|
153
|
+
parameters: row.indexed.entry.parameters
|
|
154
|
+
})) };
|
|
155
|
+
return {
|
|
156
|
+
tools: browseByNamespace(candidates, limit),
|
|
157
|
+
fallback: true
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** `code_tools_describe`:一个工具的完整描述与参数 schema。 */
|
|
163
|
+
function describeDefinition(name, catalog) {
|
|
164
|
+
return {
|
|
165
|
+
name,
|
|
166
|
+
description: "Describe one callable tool by exact name: its description and complete parameter schema.",
|
|
167
|
+
parameters: {
|
|
168
|
+
type: "object",
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
required: ["name"],
|
|
171
|
+
properties: { name: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "Exact tool name."
|
|
174
|
+
} }
|
|
175
|
+
},
|
|
176
|
+
output: {
|
|
177
|
+
schema: {
|
|
178
|
+
type: "object",
|
|
179
|
+
additionalProperties: false,
|
|
180
|
+
required: ["found"],
|
|
181
|
+
properties: {
|
|
182
|
+
found: { type: "boolean" },
|
|
183
|
+
name: { type: "string" },
|
|
184
|
+
description: { type: "string" },
|
|
185
|
+
parameters: {}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
render: (_args, value) => [{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: value.found !== true ? "No tool named that is callable from this session." : String(value.name) + "\n" + String(value.description) + "\nparameters: " + safeJson(value.parameters)
|
|
191
|
+
}]
|
|
192
|
+
},
|
|
193
|
+
async execute(args) {
|
|
194
|
+
const record = typeof args === "object" && args !== null ? args : {};
|
|
195
|
+
const wanted = typeof record["name"] === "string" ? record["name"] : "";
|
|
196
|
+
const entry = catalog().find((row) => row.name === wanted);
|
|
197
|
+
if (entry === void 0) return { found: false };
|
|
198
|
+
return {
|
|
199
|
+
found: true,
|
|
200
|
+
name: entry.name,
|
|
201
|
+
description: clip(entry.description),
|
|
202
|
+
parameters: entry.parameters
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/** `code_debug`:最近若干次 describe 决策(只在程序内可见)。 */
|
|
208
|
+
function debugDefinition(name, debug) {
|
|
209
|
+
return {
|
|
210
|
+
name,
|
|
211
|
+
description: "Read the last few catalog decisions of this preset (visible vs hidden tool names). Diagnostics only.",
|
|
212
|
+
parameters: {
|
|
213
|
+
type: "object",
|
|
214
|
+
additionalProperties: false,
|
|
215
|
+
properties: { limit: {
|
|
216
|
+
type: "number",
|
|
217
|
+
description: "Maximum entries (default 5)."
|
|
218
|
+
} }
|
|
219
|
+
},
|
|
220
|
+
output: {
|
|
221
|
+
schema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
required: ["entries"],
|
|
225
|
+
properties: { entries: {
|
|
226
|
+
type: "array",
|
|
227
|
+
items: {}
|
|
228
|
+
} }
|
|
229
|
+
},
|
|
230
|
+
render: (_args, value) => [{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: safeJson(value.entries)
|
|
233
|
+
}]
|
|
234
|
+
},
|
|
235
|
+
async execute(args) {
|
|
236
|
+
const record = typeof args === "object" && args !== null ? args : {};
|
|
237
|
+
const limit = typeof record["limit"] === "number" && record["limit"] > 0 ? Math.floor(record["limit"]) : 5;
|
|
238
|
+
const all = debug();
|
|
239
|
+
return { entries: Array.isArray(all) ? all.slice(-limit) : [all] };
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/** 读一个可选 namespace 参数。 */
|
|
244
|
+
function namespaceOf(args) {
|
|
245
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
246
|
+
const value = args["namespace"];
|
|
247
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 零命中时的兜底浏览:按 namespace 分组,每组给出若干名字。
|
|
251
|
+
*
|
|
252
|
+
* 一次失败的检索不该让模型空手而归——它多半只会换词再搜一次,或者干脆瞎猜名字。
|
|
253
|
+
* 这里直接给出分好组的清单,等于把「再搜」换成「挑一个」。
|
|
254
|
+
*
|
|
255
|
+
* @param entries - 当前清单。
|
|
256
|
+
* @param limit - 总行数上限。
|
|
257
|
+
* @returns 与命中同形的行。
|
|
258
|
+
*/
|
|
259
|
+
function browseByNamespace(entries, limit) {
|
|
260
|
+
const groups = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
const namespace = namespaceOfName(entry.name);
|
|
263
|
+
const bucket = groups.get(namespace);
|
|
264
|
+
if (bucket === void 0) groups.set(namespace, [entry]);
|
|
265
|
+
else bucket.push(entry);
|
|
266
|
+
}
|
|
267
|
+
const ordered = [...groups.entries()].sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]));
|
|
268
|
+
const rows = [];
|
|
269
|
+
for (const [namespace, bucket] of ordered) for (const entry of bucket.slice(0, 2)) {
|
|
270
|
+
if (rows.length >= limit) return rows;
|
|
271
|
+
rows.push({
|
|
272
|
+
name: entry.name,
|
|
273
|
+
description: clip(namespace + ": " + entry.description),
|
|
274
|
+
parameters: entry.parameters
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return rows;
|
|
278
|
+
}
|
|
279
|
+
/** 一个工具名所属的 namespace。 */
|
|
280
|
+
function namespaceOfName(name) {
|
|
281
|
+
if (!name.startsWith("mcp__")) return "plugin";
|
|
282
|
+
const rest = name.slice(5);
|
|
283
|
+
const cut = rest.indexOf("__");
|
|
284
|
+
return cut < 0 ? rest : rest.slice(0, cut);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 中日韩文字范围:扩展 A、基本区、兼容表意文字、日文假名、韩文音节。
|
|
288
|
+
*
|
|
289
|
+
* 用 fromCharCode 拼而不写转义:这个字符类写错一层反斜杠不会报错,只会让中日韩
|
|
290
|
+
* 分词整条静默失效(查询被切成空 token,然后按字母序返回一堆无关工具)。
|
|
291
|
+
*/
|
|
292
|
+
const CJK_RUN = new RegExp("[" + String.fromCharCode(13312) + "-䶿一-鿿豈--ヿ가-]+", "g");
|
|
293
|
+
/** 英文 / 数字词。 */
|
|
294
|
+
const WORD_RUN = /[a-z0-9]+/g;
|
|
295
|
+
/**
|
|
296
|
+
* 分词:拉丁词按词切,中日韩按单字 + 二元组(作为分词器的廉价替代)。
|
|
297
|
+
*
|
|
298
|
+
* **刻意不做中英对照**。工具的检索词来自工具自己(名字、描述、参数名),跨语言
|
|
299
|
+
* 落差交给模型:说明段明确要求用英文关键词检索,而模型本来就会翻译。一张手写的
|
|
300
|
+
* 同义词表只能覆盖写表的人想到的那几个词,覆盖率永远是问号,维护成本却是常态。
|
|
301
|
+
*
|
|
302
|
+
* @param text - 任意文本(工具名、描述、参数名或查询)。
|
|
303
|
+
* @returns 查询词。
|
|
304
|
+
*/
|
|
305
|
+
function tokenize(text) {
|
|
306
|
+
const value = String(text ?? "").toLowerCase();
|
|
307
|
+
const tokens = [];
|
|
308
|
+
for (const word of value.match(WORD_RUN) ?? []) tokens.push(word);
|
|
309
|
+
for (const run of value.match(CJK_RUN) ?? []) {
|
|
310
|
+
if (run.length === 1) tokens.push(run);
|
|
311
|
+
for (let index = 0; index + 1 < run.length; index += 1) tokens.push(run.slice(index, index + 2));
|
|
312
|
+
}
|
|
313
|
+
return tokens;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 参数名也是强信号:`instId`、`sql`、`channel` 这类词往往只出现在参数里,而它恰恰是
|
|
317
|
+
* 用户或模型用来定位工具的那个词。
|
|
318
|
+
*
|
|
319
|
+
* @param parameters - 工具的参数 JSON Schema。
|
|
320
|
+
* @returns 参数名分词。
|
|
321
|
+
*/
|
|
322
|
+
function parameterTokens(parameters) {
|
|
323
|
+
const tokens = [];
|
|
324
|
+
for (const key of propertiesOf(parameters)) tokens.push(...tokenize(key));
|
|
325
|
+
return tokens;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 读出一个工具参数 schema 的属性名。
|
|
329
|
+
*
|
|
330
|
+
* @param parameters - 参数 JSON Schema。
|
|
331
|
+
* @returns 属性名数组(没有时为 `[]`)。
|
|
332
|
+
*/
|
|
333
|
+
function propertiesOf(parameters) {
|
|
334
|
+
if (typeof parameters !== "object" || parameters === null) return [];
|
|
335
|
+
const properties = parameters.properties;
|
|
336
|
+
if (typeof properties !== "object" || properties === null) return [];
|
|
337
|
+
return Object.keys(properties);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 参数名里是否存在与查询词**完全相同**的键。
|
|
341
|
+
*
|
|
342
|
+
* 索引只留了参数名的分词,丢掉了「原始键名」,所以这里在索引项上多存一份原始键名集合,
|
|
343
|
+
* 只用于给完全匹配再加一点权重。
|
|
344
|
+
*
|
|
345
|
+
* @param indexed - 索引项。
|
|
346
|
+
* @param term - 查询词。
|
|
347
|
+
* @returns 命中时 1,否则 0。
|
|
348
|
+
*/
|
|
349
|
+
function rawParameterHit(indexed, term) {
|
|
350
|
+
return indexed.parameterNames.has(term) ? 1 : 0;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 把一份清单建成检索索引,并给出每个词的文档频率(用于降噪加权)。
|
|
354
|
+
*
|
|
355
|
+
* @param entries - 当前清单。
|
|
356
|
+
* @returns 索引项与词 → 出现次数。
|
|
357
|
+
*/
|
|
358
|
+
function buildIndex(entries) {
|
|
359
|
+
const documentFrequency = /* @__PURE__ */ new Map();
|
|
360
|
+
const indexed = [];
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
const name = countOf(tokenize(entry.name));
|
|
363
|
+
const description = countOf(tokenize(entry.description));
|
|
364
|
+
const parameters = new Set(parameterTokens(entry.parameters));
|
|
365
|
+
const parameterNames = new Set(propertiesOf(entry.parameters).map((key) => key.toLowerCase()));
|
|
366
|
+
indexed.push({
|
|
367
|
+
entry,
|
|
368
|
+
name,
|
|
369
|
+
description,
|
|
370
|
+
parameters,
|
|
371
|
+
parameterNames
|
|
372
|
+
});
|
|
373
|
+
const seen = /* @__PURE__ */ new Set([
|
|
374
|
+
...name.keys(),
|
|
375
|
+
...description.keys(),
|
|
376
|
+
...parameters
|
|
377
|
+
]);
|
|
378
|
+
for (const token of seen) documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
indexed,
|
|
382
|
+
documentFrequency
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** 统计词频。 */
|
|
386
|
+
function countOf(tokens) {
|
|
387
|
+
const counts = /* @__PURE__ */ new Map();
|
|
388
|
+
for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
389
|
+
return counts;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* 一个工具的命中分数。
|
|
393
|
+
*
|
|
394
|
+
* 三处命中权重递减:名字 > 参数名 > 描述。每个词再乘一个 idf 权重,让 `ticker`、
|
|
395
|
+
* `candle` 这类稀有词压过 `get`、`list`、`create` 这类几乎每个工具都有的泛词——否则
|
|
396
|
+
* 「看一下 K 线」会被 `get_balance` 抢走(两者都命中 `get`)。
|
|
397
|
+
*
|
|
398
|
+
* @param indexed - 索引项。
|
|
399
|
+
* @param terms - 去重后的查询词。
|
|
400
|
+
* @param documentFrequency - 词 → 文档频率。
|
|
401
|
+
* @param total - 清单规模。
|
|
402
|
+
* @param query - 原始查询(用于整串子串加分)。
|
|
403
|
+
* @returns 分数;0 表示不相关。
|
|
404
|
+
*/
|
|
405
|
+
function scoreOf(indexed, terms, documentFrequency, total, query) {
|
|
406
|
+
if (terms.length === 0) return 1;
|
|
407
|
+
let score = 0;
|
|
408
|
+
for (const term of terms) {
|
|
409
|
+
const inName = indexed.name.get(term) ?? 0;
|
|
410
|
+
const inDescription = indexed.description.get(term) ?? 0;
|
|
411
|
+
const inParameters = indexed.parameters.has(term) ? 1 : 0;
|
|
412
|
+
const exactParameter = Math.max(0, rawParameterHit(indexed, term));
|
|
413
|
+
if (inName === 0 && inDescription === 0 && inParameters === 0) continue;
|
|
414
|
+
const df = documentFrequency.get(term) ?? 1;
|
|
415
|
+
const weight = Math.log(1 + total / (1 + df));
|
|
416
|
+
score += weight * (inName > 0 ? 6 + Math.min(inName, 3) : 0);
|
|
417
|
+
score += weight * (inParameters === 1 ? 3 + exactParameter : 0);
|
|
418
|
+
score += weight * (inDescription > 0 ? 1 + Math.min(inDescription, 2) * .5 : 0);
|
|
419
|
+
}
|
|
420
|
+
const name = indexed.entry.name.toLowerCase();
|
|
421
|
+
const wanted = query.trim().toLowerCase();
|
|
422
|
+
if (wanted.length > 0 && (name === wanted || name.endsWith("__" + wanted))) score += 50;
|
|
423
|
+
else if (wanted.length > 1 && name.includes(wanted)) score += 12;
|
|
424
|
+
return score;
|
|
425
|
+
}
|
|
426
|
+
/** 截断描述,避免清单输出过长。 */
|
|
427
|
+
function clip(text, max = 240) {
|
|
428
|
+
const value = String(text ?? "");
|
|
429
|
+
return value.length <= max ? value : value.slice(0, max - 1) + "…";
|
|
430
|
+
}
|
|
431
|
+
/** 安全 JSON 化。 */
|
|
432
|
+
function safeJson(value) {
|
|
433
|
+
try {
|
|
434
|
+
return JSON.stringify(value);
|
|
435
|
+
} catch {
|
|
436
|
+
return String(value);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** 发现工具名契约:`[list, search, describe, debug?]`。第一个同时用作 Code Mode 标记。 */
|
|
440
|
+
const DEFAULT_META_TOOLS = Object.freeze([
|
|
441
|
+
"code_tools_list",
|
|
442
|
+
"code_tools_search",
|
|
443
|
+
"code_tools_describe",
|
|
444
|
+
"code_debug"
|
|
445
|
+
]);
|
|
446
|
+
/** 默认额外遮蔽的名字前缀。 */
|
|
447
|
+
const DEFAULT_HIDDEN_PREFIXES = Object.freeze(["mcp__"]);
|
|
448
|
+
/**
|
|
449
|
+
* 默认始终原生呈现的名字。
|
|
450
|
+
*
|
|
451
|
+
* 恒定系统提示词里点名了这两个工具(大输出预览的补读工具、调度账本的查询工具),
|
|
452
|
+
* 遮蔽它们会让提示词指向一个模型看不见的名字,所以默认留在原生清单里。
|
|
453
|
+
*/
|
|
454
|
+
const DEFAULT_NATIVE_TOOLS = Object.freeze(["cache_warmup_result", "dispatch_ledger"]);
|
|
455
|
+
/**
|
|
456
|
+
* 把原始配置归一化。
|
|
457
|
+
*
|
|
458
|
+
* @param config - 行配置;`null` / 省略表示默认值。
|
|
459
|
+
* @returns 冻结的配置与集合。
|
|
460
|
+
*/
|
|
461
|
+
function createRules(config) {
|
|
462
|
+
const options = normalizeConfig(config);
|
|
463
|
+
return {
|
|
464
|
+
options,
|
|
465
|
+
hidden: new Set(options.hiddenTools),
|
|
466
|
+
native: /* @__PURE__ */ new Set([
|
|
467
|
+
...options.nativeTools,
|
|
468
|
+
options.runToolName,
|
|
469
|
+
...options.metaTools
|
|
470
|
+
])
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* 校验并冻结一份配置。
|
|
475
|
+
*
|
|
476
|
+
* @param config - 原始配置。
|
|
477
|
+
* @returns 规范化后的只读配置。
|
|
478
|
+
*/
|
|
479
|
+
function normalizeConfig(config) {
|
|
480
|
+
const source = config ?? {};
|
|
481
|
+
const runToolName = nonEmptyString$1(source.runToolName) ?? "code_run";
|
|
482
|
+
const metaTools = stringList(source.metaTools, DEFAULT_META_TOOLS);
|
|
483
|
+
if (metaTools.length === 0) throw new TypeError("codemode: metaTools must list at least one tool name");
|
|
484
|
+
if (metaTools.includes(runToolName)) throw new TypeError("codemode: metaTools must not repeat the transport name");
|
|
485
|
+
return Object.freeze({
|
|
486
|
+
nativeScope: source.nativeScope !== false,
|
|
487
|
+
hiddenPrefixes: Object.freeze(stringList(source.hiddenPrefixes, DEFAULT_HIDDEN_PREFIXES)),
|
|
488
|
+
nativeTools: Object.freeze(stringList(source.nativeTools, DEFAULT_NATIVE_TOOLS)),
|
|
489
|
+
hiddenTools: Object.freeze(stringList(source.hiddenTools, [])),
|
|
490
|
+
metaTools: Object.freeze(metaTools),
|
|
491
|
+
runToolName,
|
|
492
|
+
maxParallelSubCalls: positiveInteger(source.maxParallelSubCalls, 10),
|
|
493
|
+
maxSubCalls: positiveInteger(source.maxSubCalls, 512),
|
|
494
|
+
guard: source.guard !== false,
|
|
495
|
+
sdkSection: source.sdkSection !== false,
|
|
496
|
+
debug: source.debug !== false
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/** 作用域不可用时的空视图。 */
|
|
500
|
+
const DISABLED_VIEW = Object.freeze({
|
|
501
|
+
enabled: false,
|
|
502
|
+
globalNames: /* @__PURE__ */ new Set(),
|
|
503
|
+
presetNames: /* @__PURE__ */ new Set()
|
|
504
|
+
});
|
|
505
|
+
/**
|
|
506
|
+
* 判定一个名字是否要从模型清单里剥离。
|
|
507
|
+
*
|
|
508
|
+
* 顺序:显式例外(`nativeTools` / 传输工具 / 发现工具)→ 显式遮蔽
|
|
509
|
+
* (`hiddenTools`)→ 名字前缀 → 作用域归属。
|
|
510
|
+
*
|
|
511
|
+
* 作用域归属只在能确认归属时遮蔽:
|
|
512
|
+
*
|
|
513
|
+
* - 在全局视图里解析得到 → 全局层注册的,遮蔽;
|
|
514
|
+
* - 只在预设视图里解析得到 → 本预设自己注册的,保持原生;
|
|
515
|
+
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层),遮蔽。
|
|
516
|
+
*
|
|
517
|
+
* @param name - 工具名。
|
|
518
|
+
* @param rules - 已归一化的规则。
|
|
519
|
+
* @param view - 归属视图。
|
|
520
|
+
* @returns 要遮蔽时为 `true`。
|
|
521
|
+
*/
|
|
522
|
+
function isMaskedName(name, rules, view) {
|
|
523
|
+
if (typeof name !== "string" || name.length === 0) return false;
|
|
524
|
+
if (rules.native.has(name)) return false;
|
|
525
|
+
if (rules.hidden.has(name)) return true;
|
|
526
|
+
for (const prefix of rules.options.hiddenPrefixes) if (prefix.length > 0 && name.startsWith(prefix)) return true;
|
|
527
|
+
if (!rules.options.nativeScope || !view.enabled) return false;
|
|
528
|
+
if (view.globalNames.has(name)) return true;
|
|
529
|
+
if (view.presetNames.has(name)) return false;
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* 汇总被遮蔽的工具。
|
|
534
|
+
*
|
|
535
|
+
* @param names - 被遮蔽的名字。
|
|
536
|
+
* @returns 计数与分组。
|
|
537
|
+
*/
|
|
538
|
+
function summarizeHidden(names) {
|
|
539
|
+
const groups = /* @__PURE__ */ new Map();
|
|
540
|
+
for (const name of names) {
|
|
541
|
+
const mcp = name.startsWith("mcp__") ? name.split("__")[1] : void 0;
|
|
542
|
+
const namespace = mcp !== void 0 && mcp.length > 0 ? mcp : "plugin";
|
|
543
|
+
const kind = mcp !== void 0 && mcp.length > 0 ? "mcp" : "plugin";
|
|
544
|
+
const key = kind + ":" + namespace;
|
|
545
|
+
const entry = groups.get(key);
|
|
546
|
+
if (entry === void 0) groups.set(key, {
|
|
547
|
+
namespace,
|
|
548
|
+
kind,
|
|
549
|
+
count: 1
|
|
550
|
+
});
|
|
551
|
+
else entry.count += 1;
|
|
552
|
+
}
|
|
553
|
+
const namespaces = [...groups.values()].sort((left, right) => right.count - left.count || left.namespace.localeCompare(right.namespace));
|
|
554
|
+
return {
|
|
555
|
+
total: names.length,
|
|
556
|
+
namespaces
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 生成 `tools:sdk` 段的正文。
|
|
561
|
+
*
|
|
562
|
+
* ## 恒定契约
|
|
563
|
+
*
|
|
564
|
+
* 这段正文**只**依赖入参里的两个工具名与部署级的 `programFormat`。它刻意不含遮蔽
|
|
565
|
+
* 工具的数量、namespace 分布、会话 id 或时间,因此同一部署下逐字节恒定:挂载或卸载
|
|
566
|
+
* MCP 服务端不会改写它,前缀缓存不会因为它失效。
|
|
567
|
+
*
|
|
568
|
+
* @param input - 传输工具名、发现工具名与程序形态。
|
|
569
|
+
* @returns 说明段正文。
|
|
570
|
+
*/
|
|
571
|
+
function buildCodemodeSectionText(input) {
|
|
572
|
+
const retained = input.programFormat === "repl";
|
|
573
|
+
const listTool = input.metaTools[0] ?? "code_tools_list";
|
|
574
|
+
const searchTool = input.metaTools[1] ?? "code_tools_search";
|
|
575
|
+
const describeTool = input.metaTools[2] ?? "code_tools_describe";
|
|
576
|
+
return [
|
|
577
|
+
"## Code Mode",
|
|
578
|
+
"",
|
|
579
|
+
"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 " + input.runToolName + ".",
|
|
580
|
+
"",
|
|
581
|
+
"### Calling tools from a program",
|
|
582
|
+
"",
|
|
583
|
+
"```js",
|
|
584
|
+
"// standard tools and hidden tools alike",
|
|
585
|
+
"const files = await tools.glob({ pattern: \"src/**/*.ts\" })",
|
|
586
|
+
"const rows = await Promise.all(files.map((f) => tools.read({ file_path: f })))",
|
|
587
|
+
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])",
|
|
588
|
+
"```",
|
|
589
|
+
"",
|
|
590
|
+
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.",
|
|
591
|
+
"- 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.",
|
|
592
|
+
"- Independent read-only calls may overlap under Promise.all; mutating calls run alone, in submission order.",
|
|
593
|
+
"- 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.",
|
|
594
|
+
"",
|
|
595
|
+
"### Discovery API (callable from inside a program)",
|
|
596
|
+
"",
|
|
597
|
+
"- tools." + listTool + "({ namespace? }) - every callable tool as { name, description, namespace }, hidden ones included.",
|
|
598
|
+
"- 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.",
|
|
599
|
+
"- tools." + describeTool + "({ name }) - one tool description and complete parameter schema.",
|
|
600
|
+
"",
|
|
601
|
+
"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."
|
|
602
|
+
].join("\n");
|
|
603
|
+
}
|
|
604
|
+
/** 一个非空字符串,或 `undefined`。 */
|
|
605
|
+
function nonEmptyString$1(value) {
|
|
606
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
607
|
+
}
|
|
608
|
+
/** 名字列表:省略时用缺省值,拒绝非字符串项与重复项。 */
|
|
609
|
+
function stringList(value, fallback) {
|
|
610
|
+
if (value === void 0 || value === null) return [...fallback];
|
|
611
|
+
if (!Array.isArray(value)) throw new TypeError("codemode: expected an array of tool names");
|
|
612
|
+
const out = [];
|
|
613
|
+
for (const entry of value) {
|
|
614
|
+
const name = nonEmptyString$1(entry);
|
|
615
|
+
if (name === void 0) throw new TypeError("codemode: tool names must be non-empty strings");
|
|
616
|
+
if (!out.includes(name)) out.push(name);
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
/** 正整数,越界或非法时用缺省值。 */
|
|
621
|
+
function positiveInteger(value, fallback) {
|
|
622
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return fallback;
|
|
623
|
+
return value;
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/host/codemode/executor.ts
|
|
627
|
+
/**
|
|
628
|
+
* Code Mode 的工具执行器:本包遮蔽与拒绝的唯一接缝。
|
|
629
|
+
*
|
|
630
|
+
* ## 为什么是 executor
|
|
631
|
+
*
|
|
632
|
+
* DSH 0.2.2 把「模型看到的工具清单」的归属权从 `system-prompt/assemble` 瀑布搬到了
|
|
633
|
+
* per-scope `ToolExecutor`:注册表向当前作用域选中的 executor 要 schema,所以
|
|
634
|
+
* `describe()` 就是请求面。它同时解决了归属问题——executor **按作用域链选择**,只有挂
|
|
635
|
+
* 在本预设里的这一份会服务本预设的会话,因此不需要判断「这个 agent 是不是我的」。
|
|
636
|
+
*
|
|
637
|
+
* 三件事:
|
|
638
|
+
*
|
|
639
|
+
* 1. `describe()` 丢掉一切不该出现在模型清单里的名字,其余交给原生 executor 投影;
|
|
640
|
+
* 2. `sections` 提供**恒定**的 Code Mode 说明段(见 rules.ts 的恒定契约);
|
|
641
|
+
* 3. `execute()` 在派发**之前**拒绝模型对被遮蔽名字的直接调用,其余整批委托给原生
|
|
642
|
+
* executor——委托而不是重写,是因为手写 pre-policy / 审批 / around-dispatch /
|
|
643
|
+
* post-execute 管线正是插件静默丢策略的方式。
|
|
644
|
+
*
|
|
645
|
+
* ## 程序内子调用怎么与模型直呼区分
|
|
646
|
+
*
|
|
647
|
+
* 批量 `execute()` 的入参没有 `parent` 字段(执行令牌由注册表自己的管线铸造,插件
|
|
648
|
+
* 造不出来),所以传输层把自己持有的 `AbortSignal` 打上 {@link NESTED_MARK},并让子调用 id
|
|
649
|
+
* 形如 `<outer>:ptc:<n>`。signal 是模型无法伪造的对象,call id 不可改,两个标记互为兜底。
|
|
650
|
+
*
|
|
651
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/executor
|
|
652
|
+
*/
|
|
653
|
+
/** 传输层打在 AbortSignal 上的嵌套标记。 */
|
|
654
|
+
const NESTED_MARK = Symbol.for("dsh-chatgpt-subscription.codemode.nested");
|
|
655
|
+
/** 子调用 id 里的一段:`<outer>:ptc:<n>`,与内核自己的命名一致。 */
|
|
656
|
+
const NESTED_CALL_ID_MARKER = ":ptc:";
|
|
657
|
+
/**
|
|
658
|
+
* 给一个 signal 打上嵌套标记。
|
|
659
|
+
*
|
|
660
|
+
* @param signal - 传给 `tools.execute()` 的那一个 signal。
|
|
661
|
+
* @returns 同一个 signal(便于链式使用)。
|
|
662
|
+
*/
|
|
663
|
+
function markNested(signal) {
|
|
664
|
+
if (signal === void 0) return void 0;
|
|
665
|
+
try {
|
|
666
|
+
Object.defineProperty(signal, NESTED_MARK, {
|
|
667
|
+
value: true,
|
|
668
|
+
enumerable: false,
|
|
669
|
+
configurable: true
|
|
670
|
+
});
|
|
671
|
+
} catch {}
|
|
672
|
+
return signal;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* 判断一批调用是不是来自程序内部。
|
|
676
|
+
*
|
|
677
|
+
* @param call - 批里的一条调用(结构探测)。
|
|
678
|
+
* @param batchSignal - 批级 signal(`tools.execute()` 只收一份,注册表会复制给每条执行)。
|
|
679
|
+
* @returns 来自程序内部时为 `true`。
|
|
680
|
+
*/
|
|
681
|
+
function isNestedCall(call, batchSignal) {
|
|
682
|
+
if (isMarked(batchSignal)) return true;
|
|
683
|
+
if (typeof call !== "object" || call === null) return false;
|
|
684
|
+
if (isMarked(call.signal)) return true;
|
|
685
|
+
const id = call.id;
|
|
686
|
+
return typeof id === "string" && id.includes(":ptc:");
|
|
687
|
+
}
|
|
688
|
+
/** 一个值是否带着本包的嵌套标记。 */
|
|
689
|
+
function isMarked(value) {
|
|
690
|
+
return typeof value === "object" && value !== null && value[NESTED_MARK] === true;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* 造一个 Code Mode 执行器。
|
|
694
|
+
*
|
|
695
|
+
* @param ctx - 本包的行上下文(读取系统提示段的排序位)。
|
|
696
|
+
* @param rules - 已归一化的规则。
|
|
697
|
+
* @param hooks - 归属视图、说明段与委托目标。
|
|
698
|
+
* @returns 执行器。
|
|
699
|
+
*/
|
|
700
|
+
function createCodemodeExecutor(ctx, rules, hooks) {
|
|
701
|
+
const sectionOrder = sectionOrderOf(ctx);
|
|
702
|
+
const sections = rules.options.sdkSection ? [{
|
|
703
|
+
name: "tools:sdk",
|
|
704
|
+
order: sectionOrder,
|
|
705
|
+
interpolate: false,
|
|
706
|
+
text: () => hooks.sectionText()
|
|
707
|
+
}] : [];
|
|
708
|
+
const visibleNames = /* @__PURE__ */ new Set();
|
|
709
|
+
return {
|
|
710
|
+
sections,
|
|
711
|
+
describe(tools) {
|
|
712
|
+
hooks.onCatalog?.(tools ?? []);
|
|
713
|
+
const view = hooks.view();
|
|
714
|
+
const visible = [];
|
|
715
|
+
const masked = [];
|
|
716
|
+
visibleNames.clear();
|
|
717
|
+
for (const tool of tools ?? []) {
|
|
718
|
+
const name = nameOf(tool);
|
|
719
|
+
if (name === void 0) continue;
|
|
720
|
+
if (isMaskedName(name, rules, view)) masked.push(name);
|
|
721
|
+
else {
|
|
722
|
+
visible.push(tool);
|
|
723
|
+
visibleNames.add(name);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
hooks.onDescribe?.({
|
|
727
|
+
visible: [...visibleNames],
|
|
728
|
+
masked
|
|
729
|
+
});
|
|
730
|
+
return hooks.delegate.describe(visible);
|
|
731
|
+
},
|
|
732
|
+
async execute(input) {
|
|
733
|
+
const calls = Array.isArray(input?.calls) ? input.calls : [];
|
|
734
|
+
const denials = /* @__PURE__ */ new Map();
|
|
735
|
+
if (rules.options.guard) for (let index = 0; index < calls.length; index += 1) {
|
|
736
|
+
const reason = denialFor(calls[index], rules, hooks, input?.signal);
|
|
737
|
+
if (reason !== void 0) denials.set(index, reason);
|
|
738
|
+
}
|
|
739
|
+
if (denials.size === 0) return hooks.delegate.execute(input);
|
|
740
|
+
return {
|
|
741
|
+
results: calls.map((call, index) => {
|
|
742
|
+
const callId = idOf(call) ?? "call-" + String(index);
|
|
743
|
+
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.";
|
|
744
|
+
return {
|
|
745
|
+
callId,
|
|
746
|
+
result: {
|
|
747
|
+
isError: true,
|
|
748
|
+
error: {
|
|
749
|
+
name: "ToolNotFoundError",
|
|
750
|
+
code: "UNKNOWN_TOOL",
|
|
751
|
+
message: reason
|
|
752
|
+
},
|
|
753
|
+
content: [{
|
|
754
|
+
type: "text",
|
|
755
|
+
text: "Error: " + reason
|
|
756
|
+
}]
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}),
|
|
760
|
+
additionalContexts: [],
|
|
761
|
+
concluded: false
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* 一条模型直呼调用的拒绝理由,或 `undefined` 表示放行。
|
|
768
|
+
*
|
|
769
|
+
* @param call - 批里的一条调用。
|
|
770
|
+
* @param rules - 已归一化的规则。
|
|
771
|
+
* @param hooks - 归属视图与说明段。
|
|
772
|
+
* @param batchSignal - 批级 signal。
|
|
773
|
+
* @returns 拒绝理由或 `undefined`。
|
|
774
|
+
*
|
|
775
|
+
* 判定不读「本步可见名清单」:那份集合是**执行器级**的,多个会话并发时会被后一次 describe
|
|
776
|
+
* 覆盖,用它放行等于给另一个会话开一个不该开的口子。这里只按名字自身的归属判一次。
|
|
777
|
+
*/
|
|
778
|
+
function denialFor(call, rules, hooks, batchSignal) {
|
|
779
|
+
if (typeof call !== "object" || call === null) return void 0;
|
|
780
|
+
const name = call.name;
|
|
781
|
+
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
782
|
+
if (isNestedCall(call, batchSignal)) return void 0;
|
|
783
|
+
if (name === rules.options.runToolName) return void 0;
|
|
784
|
+
if (!isMaskedName(name, rules, hooks.viewForName(name))) return void 0;
|
|
785
|
+
const searchTool = rules.options.metaTools[1] ?? rules.options.metaTools[0] ?? "code_tools_search";
|
|
786
|
+
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.";
|
|
787
|
+
}
|
|
788
|
+
/** 读一段工具定义的 ``name``。 */
|
|
789
|
+
function nameOf(tool) {
|
|
790
|
+
if (typeof tool !== "object" || tool === null) return void 0;
|
|
791
|
+
const name = tool.name;
|
|
792
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
793
|
+
}
|
|
794
|
+
/** 读一条调用的 id。 */
|
|
795
|
+
function idOf(call) {
|
|
796
|
+
if (typeof call !== "object" || call === null) return void 0;
|
|
797
|
+
const id = call.id;
|
|
798
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
799
|
+
}
|
|
800
|
+
/** 取 `tools:sdk` 的排序位;取不到时退回 5000(内核 TOOLS_SDK 的默认位)。 */
|
|
801
|
+
function sectionOrderOf(ctx) {
|
|
802
|
+
try {
|
|
803
|
+
const order = (ctx?.get?.("systemPrompt"))?.getSectionOrder?.("TOOLS_SDK");
|
|
804
|
+
return typeof order === "number" && Number.isFinite(order) ? order : 5e3;
|
|
805
|
+
} catch {
|
|
806
|
+
return 5e3;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/host/codemode/retention.ts
|
|
811
|
+
/**
|
|
812
|
+
* 造一个常驻环境管理器。
|
|
813
|
+
*
|
|
814
|
+
* @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
|
|
815
|
+
* @returns 管理句柄。
|
|
816
|
+
*/
|
|
817
|
+
function createRetention(runtimeOf) {
|
|
818
|
+
const live = /* @__PURE__ */ new Set();
|
|
819
|
+
const released = /* @__PURE__ */ new WeakSet();
|
|
820
|
+
const release = async (owner) => {
|
|
821
|
+
if (released.has(owner)) return;
|
|
822
|
+
released.add(owner);
|
|
823
|
+
live.delete(owner);
|
|
824
|
+
const runtime = runtimeOf();
|
|
825
|
+
if (runtime?.release === void 0) return;
|
|
826
|
+
try {
|
|
827
|
+
await runtime.release(owner);
|
|
828
|
+
} catch {}
|
|
829
|
+
};
|
|
830
|
+
return {
|
|
831
|
+
owner(agent) {
|
|
832
|
+
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
833
|
+
released.delete(owner);
|
|
834
|
+
live.add(owner);
|
|
835
|
+
return owner;
|
|
836
|
+
},
|
|
837
|
+
release,
|
|
838
|
+
async dispose() {
|
|
839
|
+
const owners = [...live];
|
|
840
|
+
await Promise.all(owners.map((owner) => release(owner)));
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
/** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
|
|
845
|
+
const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/host/codemode/scope.ts
|
|
848
|
+
/**
|
|
849
|
+
* 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
|
|
850
|
+
*
|
|
851
|
+
* ## 为什么要这么绕
|
|
852
|
+
*
|
|
853
|
+
* 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
|
|
854
|
+
* 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
|
|
855
|
+
* 都不导出。能用的只有两件事:
|
|
856
|
+
*
|
|
857
|
+
* 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
|
|
858
|
+
* 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
|
|
859
|
+
*
|
|
860
|
+
* 于是归属判定变成两次公开查询:
|
|
861
|
+
*
|
|
862
|
+
* - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
|
|
863
|
+
* - 只有本预设视图能解析 → 本预设自己注册的;
|
|
864
|
+
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
|
|
865
|
+
*
|
|
866
|
+
* 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
|
|
867
|
+
* 纯名字匹配,绝不因为读不到归属就误遮蔽。
|
|
868
|
+
*
|
|
869
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
|
|
870
|
+
*/
|
|
871
|
+
/** 内核写作用域标签用的符号描述。 */
|
|
872
|
+
const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
|
|
873
|
+
/**
|
|
874
|
+
* 从一个 ctx 上找出「本包所在作用域」的 key。
|
|
875
|
+
*
|
|
876
|
+
* Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
|
|
877
|
+
* 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
|
|
878
|
+
* 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
|
|
879
|
+
*
|
|
880
|
+
* @param ctx - 本包的行上下文。
|
|
881
|
+
* @returns 作用域 key;读不到时为 `undefined`。
|
|
882
|
+
*/
|
|
883
|
+
function findScopeKey(ctx) {
|
|
884
|
+
if (typeof ctx !== "object" || ctx === null) return void 0;
|
|
885
|
+
return scanForScope(ctx, 0);
|
|
886
|
+
}
|
|
887
|
+
/** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
|
|
888
|
+
function scanForScope(source, depth) {
|
|
889
|
+
if (depth > 4) return void 0;
|
|
890
|
+
let symbols;
|
|
891
|
+
try {
|
|
892
|
+
symbols = Object.getOwnPropertySymbols(source);
|
|
893
|
+
} catch {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
for (const symbol of symbols) {
|
|
897
|
+
if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
|
|
898
|
+
let value;
|
|
899
|
+
try {
|
|
900
|
+
value = source[symbol];
|
|
901
|
+
} catch {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (typeof value === "object" && value !== null) return value;
|
|
905
|
+
}
|
|
906
|
+
const proto = Object.getPrototypeOf(source);
|
|
907
|
+
if (proto === null || proto === Object.prototype) return void 0;
|
|
908
|
+
return scanForScope(proto, depth + 1);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* 为一个名字清单建立归属视图。
|
|
912
|
+
*
|
|
913
|
+
* @param tools - 工具注册表(结构探测,不看类身份)。
|
|
914
|
+
* @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
|
|
915
|
+
* @param names - 要判定的名字(通常是本步的模型可见清单)。
|
|
916
|
+
* @returns 归属视图;读不到任何归属时返回禁用视图。
|
|
917
|
+
*/
|
|
918
|
+
function resolveCatalogView(tools, scopeKey, names) {
|
|
919
|
+
const registry = asRegistry(tools);
|
|
920
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
921
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
922
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
923
|
+
for (const name of names) {
|
|
924
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
925
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
926
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
enabled: true,
|
|
930
|
+
globalNames,
|
|
931
|
+
presetNames
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* 判定一个名字在**调用时**是否属于本预设之外的层。
|
|
936
|
+
*
|
|
937
|
+
* 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
|
|
938
|
+
* 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
|
|
939
|
+
*
|
|
940
|
+
* @param tools - 工具注册表。
|
|
941
|
+
* @param scopeKey - 本预设的作用域 key。
|
|
942
|
+
* @param name - 工具名。
|
|
943
|
+
* @returns 归属视图(只含这一个名字)。
|
|
944
|
+
*/
|
|
945
|
+
function resolveNameView(tools, scopeKey, name) {
|
|
946
|
+
const registry = asRegistry(tools);
|
|
947
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
948
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
949
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
950
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
951
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
952
|
+
return {
|
|
953
|
+
enabled: true,
|
|
954
|
+
globalNames,
|
|
955
|
+
presetNames
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
/** 一次公开查询:解析得到定义时为 `true`。 */
|
|
959
|
+
function probes(registry, name, scope) {
|
|
960
|
+
try {
|
|
961
|
+
return registry.get(name, scope) !== void 0;
|
|
962
|
+
} catch {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/** 结构探测:读得到 `get` 就当注册表用。 */
|
|
967
|
+
function asRegistry(tools) {
|
|
968
|
+
if (typeof tools !== "object" || tools === null) return void 0;
|
|
969
|
+
if (typeof tools.get !== "function") return void 0;
|
|
970
|
+
return tools;
|
|
971
|
+
}
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/host/codemode/transport.ts
|
|
974
|
+
/**
|
|
975
|
+
* Code Mode 的程序传输工具 `code_run`。
|
|
976
|
+
*
|
|
977
|
+
* 工具名不是 `run_code`:那个名字被内核保留(`tools.register()` 会抛错),所以本包
|
|
978
|
+
* 用自己的名字注册同一件事——把模型写的一段程序交给 `ctx.ptcRuntime` 执行,程序里的每次
|
|
979
|
+
* `tools.<name>(args)` 都是一次真实的工具调用。
|
|
980
|
+
*
|
|
981
|
+
* ## 为什么执行委托给注册表而不是自己跑管线
|
|
982
|
+
*
|
|
983
|
+
* 子调用走公开的 `ctx.tools.execute()`,因此落到**注册表选中的执行器**上:在本预设里就是
|
|
984
|
+
* 原生 executor 的完整管线(pre-policy、审批、around-dispatch、post-execute 一个不少)。
|
|
985
|
+
* 手写这条管线是插件静默丢策略的典型方式,所以这里一行都不抄。
|
|
986
|
+
*
|
|
987
|
+
* ## 子调度的取舍
|
|
988
|
+
*
|
|
989
|
+
* 程序自己用 `Promise.all` 决定并发。本模块只做一件额外的事:把 `isConcurrencySafe` 不为
|
|
990
|
+
* `true` 的调用串行化成互斥段,只读调用放行到 `maxParallelSubCalls` 个并发。与内核的
|
|
991
|
+
* 差别是「只排序同一程序内的子调用」,不与同一模型步的其它调用互相排序。
|
|
992
|
+
*
|
|
993
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/transport
|
|
994
|
+
*/
|
|
995
|
+
/**
|
|
996
|
+
* 本次程序运行看到的工具清单快照。
|
|
997
|
+
*
|
|
998
|
+
* 绑定函数在程序开始那一刻按当时的清单建好,此后程序内**只能**调这份清单里的名字。检索却
|
|
999
|
+
* 读的是执行器级的“最新清单”:同一预设下主代理与子代理并发时,另一个会话的一次装配就会把
|
|
1000
|
+
* 它覆盖掉,于是程序可能搜到一个自己调不到的名字(`tools.xxx is not a function`)。
|
|
1001
|
+
*
|
|
1002
|
+
* 用 AsyncLocalStorage 把快照挂到这次运行上,程序内的检索与绑定因此读同一份清单:搜得到
|
|
1003
|
+
* 就调得动,调不到就搜不到。
|
|
1004
|
+
*/
|
|
1005
|
+
const catalogScope = new AsyncLocalStorage();
|
|
1006
|
+
/**
|
|
1007
|
+
* 读当前程序运行的清单快照。
|
|
1008
|
+
*
|
|
1009
|
+
* @returns 程序运行期间是这次运行的快照;程序之外为 `undefined`。
|
|
1010
|
+
*/
|
|
1011
|
+
function currentCatalogSnapshot() {
|
|
1012
|
+
return catalogScope.getStore();
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* 造 `code_run` 的工具定义。
|
|
1016
|
+
*
|
|
1017
|
+
* @param host - 运行环境句柄。
|
|
1018
|
+
* @param rules - 已归一化的规则。
|
|
1019
|
+
* @returns 可直接交给 `ctx.tools.register()` 的原始定义。
|
|
1020
|
+
*/
|
|
1021
|
+
function createTransportDefinition(host, rules) {
|
|
1022
|
+
const runToolName = rules.options.runToolName;
|
|
1023
|
+
return {
|
|
1024
|
+
name: runToolName,
|
|
1025
|
+
description: "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the body of an async function (erasable syntax only; top-level await and return work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)`. Only what you print or return is program output.",
|
|
1026
|
+
parameters: {
|
|
1027
|
+
type: "object",
|
|
1028
|
+
additionalProperties: false,
|
|
1029
|
+
required: ["code", "description"],
|
|
1030
|
+
properties: {
|
|
1031
|
+
code: {
|
|
1032
|
+
type: "string",
|
|
1033
|
+
description: "The program: the body of an async TypeScript function."
|
|
1034
|
+
},
|
|
1035
|
+
description: {
|
|
1036
|
+
type: "string",
|
|
1037
|
+
description: "Clear, concise description of what this program does in active voice, 5-10 words."
|
|
1038
|
+
},
|
|
1039
|
+
timeoutMs: {
|
|
1040
|
+
type: "number",
|
|
1041
|
+
description: "Positive elapsed-time budget in milliseconds, capped by the runtime maximum."
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
output: {
|
|
1046
|
+
schema: {
|
|
1047
|
+
type: "object",
|
|
1048
|
+
additionalProperties: false,
|
|
1049
|
+
required: ["logs"],
|
|
1050
|
+
properties: {
|
|
1051
|
+
logs: {
|
|
1052
|
+
type: "array",
|
|
1053
|
+
items: { type: "string" }
|
|
1054
|
+
},
|
|
1055
|
+
result: {},
|
|
1056
|
+
sandbox: {
|
|
1057
|
+
type: "object",
|
|
1058
|
+
additionalProperties: false,
|
|
1059
|
+
required: ["mode", "denied"],
|
|
1060
|
+
properties: {
|
|
1061
|
+
mode: { type: "string" },
|
|
1062
|
+
denied: { type: "boolean" }
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
render: (_args, value) => {
|
|
1068
|
+
const parts = [];
|
|
1069
|
+
for (const line of value.logs ?? []) if (line.length > 0) parts.push(line);
|
|
1070
|
+
if (value.result !== void 0) try {
|
|
1071
|
+
parts.push(typeof value.result === "string" ? value.result : JSON.stringify(value.result, null, 2));
|
|
1072
|
+
} catch {
|
|
1073
|
+
parts.push(String(value.result));
|
|
1074
|
+
}
|
|
1075
|
+
if (value.sandbox?.denied === true) parts.push("The " + value.sandbox.mode + " file sandbox denied an operation.");
|
|
1076
|
+
return [{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
text: parts.length > 0 ? parts.join("\n") : "(" + runToolName + " completed with no output)"
|
|
1079
|
+
}];
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
async execute(args, exec) {
|
|
1083
|
+
const input = readArguments(args);
|
|
1084
|
+
const runtime = host.runtime();
|
|
1085
|
+
if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
|
|
1086
|
+
const catalog = host.catalog();
|
|
1087
|
+
const runSignal = AbortSignal.any([exec.signal, new AbortController().signal]);
|
|
1088
|
+
const nestedSignal = markNested(runSignal) ?? runSignal;
|
|
1089
|
+
const owner = host.stateOwner(exec.agent);
|
|
1090
|
+
const limit = rules.options.maxSubCalls;
|
|
1091
|
+
let attempts = 0;
|
|
1092
|
+
const exclusive = createMutex();
|
|
1093
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
1094
|
+
for (const entry of catalog) definitions.set(entry.name, entry);
|
|
1095
|
+
const functions = {};
|
|
1096
|
+
for (const entry of catalog) {
|
|
1097
|
+
if (entry.name === runToolName) continue;
|
|
1098
|
+
functions[entry.name] = async (rawArgs) => {
|
|
1099
|
+
const n = ++attempts;
|
|
1100
|
+
if (n > limit) throw new Error("program sub-call limit exceeded: " + String(limit) + " calls");
|
|
1101
|
+
const parsed = losslessArgs(rawArgs);
|
|
1102
|
+
const callId = String(exec.callId) + NESTED_CALL_ID_MARKER + String(n);
|
|
1103
|
+
const dispatchLog = createDispatchLog(exec.agent, {
|
|
1104
|
+
rootCallId: rootCallIdOf(String(exec.callId)),
|
|
1105
|
+
parentCallId: String(exec.callId),
|
|
1106
|
+
subCallId: callId,
|
|
1107
|
+
name: entry.name,
|
|
1108
|
+
arguments: parsed
|
|
1109
|
+
});
|
|
1110
|
+
const run = async () => {
|
|
1111
|
+
dispatchLog.start();
|
|
1112
|
+
let outcome;
|
|
1113
|
+
try {
|
|
1114
|
+
outcome = await host.callTool({
|
|
1115
|
+
name: entry.name,
|
|
1116
|
+
args: parsed,
|
|
1117
|
+
callId,
|
|
1118
|
+
signal: nestedSignal,
|
|
1119
|
+
agent: exec.agent,
|
|
1120
|
+
toolSet: exec.toolSet
|
|
1121
|
+
});
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
dispatchLog.settle({ error: error instanceof Error ? error.message : String(error) });
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
1126
|
+
dispatchLog.settle(outcome);
|
|
1127
|
+
if (outcome.error !== void 0) throw new Error(outcome.error);
|
|
1128
|
+
return outcome.value !== void 0 ? outcome.value : outcome.text ?? null;
|
|
1129
|
+
};
|
|
1130
|
+
return isConcurrencySafe(definitions.get(entry.name), parsed) ? run() : exclusive.run(run);
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* 跑一次程序:每次重取会话授权,因为重试发生在策略变更之后。
|
|
1135
|
+
*
|
|
1136
|
+
* `resolve()` 是新代内核的前置步骤(目录 / 超时 / 文件策略)。老代运行时没有它,
|
|
1137
|
+
* 那时把原始请求直接交给 `run()` 才是当时的契约。
|
|
1138
|
+
*/
|
|
1139
|
+
const attempt = async () => {
|
|
1140
|
+
const authority = await host.runAuthority?.(exec.agent, nestedSignal);
|
|
1141
|
+
const request = {
|
|
1142
|
+
program: input.code,
|
|
1143
|
+
bindings: [{
|
|
1144
|
+
global: "tools",
|
|
1145
|
+
functions
|
|
1146
|
+
}],
|
|
1147
|
+
stateOwner: owner,
|
|
1148
|
+
...authority?.cwd === void 0 ? {} : { cwd: authority.cwd },
|
|
1149
|
+
...authority?.sandboxPolicy === void 0 ? {} : { sandboxPolicy: authority.sandboxPolicy },
|
|
1150
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
1151
|
+
signal: nestedSignal
|
|
1152
|
+
};
|
|
1153
|
+
const spec = typeof runtime.resolve === "function" ? runtime.resolve(request) : request;
|
|
1154
|
+
return await catalogScope.run(catalog, () => runtime.run(spec));
|
|
1155
|
+
};
|
|
1156
|
+
let result;
|
|
1157
|
+
try {
|
|
1158
|
+
result = await attempt();
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
if (!isRetainedPolicyChange(error)) throw error;
|
|
1161
|
+
await host.releaseOwner(owner);
|
|
1162
|
+
host.stateOwner(exec.agent);
|
|
1163
|
+
result = await attempt();
|
|
1164
|
+
}
|
|
1165
|
+
if (result.error !== void 0) throw new Error("program failed (" + result.error.kind + "): " + result.error.message);
|
|
1166
|
+
return {
|
|
1167
|
+
logs: [...result.logs ?? []],
|
|
1168
|
+
...result.value === void 0 ? {} : { result: result.value },
|
|
1169
|
+
...result.sandbox === void 0 ? {} : { sandbox: {
|
|
1170
|
+
mode: result.sandbox.mode,
|
|
1171
|
+
denied: result.sandbox.denied
|
|
1172
|
+
} }
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
/** 没有会话可写时的空记录。 */
|
|
1178
|
+
const NO_DISPATCH_LOG = Object.freeze({
|
|
1179
|
+
start() {},
|
|
1180
|
+
settle() {}
|
|
1181
|
+
});
|
|
1182
|
+
/** 回放里单个文本块的上限:日志要有界,程序拿到的值不受影响。 */
|
|
1183
|
+
const DISPATCH_LOG_TEXT_LIMIT = 8e3;
|
|
1184
|
+
/**
|
|
1185
|
+
* 造一条子调用的回放记录。
|
|
1186
|
+
*
|
|
1187
|
+
* 内核自己的 PTC 传输会为每条子调用 append `tool/ptc-dispatch-start` /
|
|
1188
|
+
* `tool/ptc-dispatch`,界面(`tool-call-tree`)靠这一对事件把子调用画到父调用下面。
|
|
1189
|
+
* 本行的程序交给内核运行时执行,而那两条事件不在运行时的对外契约里,只能在这里补。
|
|
1190
|
+
*
|
|
1191
|
+
* 三条约束:
|
|
1192
|
+
*
|
|
1193
|
+
* 1. **时序诚实**:`-start` 只在真正进入执行 lane 之后写,排队中的调用不写;
|
|
1194
|
+
* 2. **必须成对**:只写 start 会留下永远不收尾的行,比不显示更糟;
|
|
1195
|
+
* 3. **绝不带崩程序**:append 只是日志侧工作,异常一律吞掉——老代 harness 没有这两
|
|
1196
|
+
* 种事件类型,那里应当表现为「没有回放」,而不是 `code_run` 失败。
|
|
1197
|
+
*
|
|
1198
|
+
* @param agent - 调用方 agent(`agent.session.append` 的宿主)。
|
|
1199
|
+
* @param entry - 这次子调用的身份与参数。
|
|
1200
|
+
* @returns 回放记录;没有会话可写时是空实现。
|
|
1201
|
+
*/
|
|
1202
|
+
function createDispatchLog(agent, entry) {
|
|
1203
|
+
const session = typeof agent === "object" && agent !== null ? agent.session : void 0;
|
|
1204
|
+
const append = session?.append;
|
|
1205
|
+
if (typeof append !== "function") return NO_DISPATCH_LOG;
|
|
1206
|
+
const base = {
|
|
1207
|
+
rootCallId: entry.rootCallId,
|
|
1208
|
+
parentCallId: entry.parentCallId,
|
|
1209
|
+
subCallId: entry.subCallId,
|
|
1210
|
+
name: entry.name,
|
|
1211
|
+
arguments: entry.arguments
|
|
1212
|
+
};
|
|
1213
|
+
const write = (type, data) => {
|
|
1214
|
+
try {
|
|
1215
|
+
append.call(session, type, data);
|
|
1216
|
+
} catch {}
|
|
1217
|
+
};
|
|
1218
|
+
return {
|
|
1219
|
+
start() {
|
|
1220
|
+
write("tool/ptc-dispatch-start", { ...base });
|
|
1221
|
+
},
|
|
1222
|
+
settle(outcome) {
|
|
1223
|
+
write("tool/ptc-dispatch", {
|
|
1224
|
+
...base,
|
|
1225
|
+
isError: outcome.error !== void 0,
|
|
1226
|
+
content: clipContent(outcome.content),
|
|
1227
|
+
...outcome.meta === void 0 ? {} : { meta: outcome.meta },
|
|
1228
|
+
...outcome.errorInfo === void 0 ? {} : { error: outcome.errorInfo }
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* 把内容块裁到日志能承受的长度。
|
|
1235
|
+
*
|
|
1236
|
+
* 程序本身拿到的值不受影响;这里只是回放副本,所以宁可截断也不让一条子调用把整份
|
|
1237
|
+
* 大文件写进会话记录。
|
|
1238
|
+
*
|
|
1239
|
+
* @param content - 原始内容块。
|
|
1240
|
+
* @returns 可写进事件的副本。
|
|
1241
|
+
*/
|
|
1242
|
+
function clipContent(content) {
|
|
1243
|
+
if (content === void 0) return [];
|
|
1244
|
+
return content.map((block) => {
|
|
1245
|
+
if (typeof block !== "object" || block === null) return block;
|
|
1246
|
+
const record = block;
|
|
1247
|
+
const text = record["text"];
|
|
1248
|
+
if (typeof text !== "string" || text.length <= DISPATCH_LOG_TEXT_LIMIT) return block;
|
|
1249
|
+
return {
|
|
1250
|
+
...record,
|
|
1251
|
+
text: text.slice(0, DISPATCH_LOG_TEXT_LIMIT) + "…[truncated in dispatch log]"
|
|
1252
|
+
};
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* 一条调用所属调用树的根 id。
|
|
1257
|
+
*
|
|
1258
|
+
* `code_run` 不会被本行自己的程序嵌套调用,所以顶层工作量下就是它自己;这里仍按
|
|
1259
|
+
* `:ptc:` 前缀剥一层,避免将来出现嵌套时把根写错。
|
|
1260
|
+
*
|
|
1261
|
+
* @param callId - 本次调用的 id。
|
|
1262
|
+
* @returns 根 id。
|
|
1263
|
+
*/
|
|
1264
|
+
function rootCallIdOf(callId) {
|
|
1265
|
+
const at = callId.indexOf(NESTED_CALL_ID_MARKER);
|
|
1266
|
+
return at > 0 ? callId.slice(0, at) : callId;
|
|
1267
|
+
}
|
|
1268
|
+
/** 内核在常驻环境遇到策略变更时的拒绝文本;其余异常一律原样上抛。 */
|
|
1269
|
+
function isRetainedPolicyChange(error) {
|
|
1270
|
+
return (error instanceof Error ? error.message : String(error)).includes("release the retained environment before changing its sandbox policy");
|
|
1271
|
+
}
|
|
1272
|
+
/** 校验并读出工具参数。 */
|
|
1273
|
+
function readArguments(args) {
|
|
1274
|
+
if (typeof args !== "object" || args === null) throw new Error("codemode: expected an arguments object");
|
|
1275
|
+
const record = args;
|
|
1276
|
+
const code = record["code"];
|
|
1277
|
+
const description = record["description"];
|
|
1278
|
+
if (typeof code !== "string" || code.trim().length === 0) throw new Error("codemode: invalid code: expected a non-empty string");
|
|
1279
|
+
if (typeof description !== "string" || description.trim().length === 0) throw new Error("codemode: invalid description: expected a non-empty string");
|
|
1280
|
+
const timeoutMs = record["timeoutMs"];
|
|
1281
|
+
if (timeoutMs === void 0) return {
|
|
1282
|
+
code,
|
|
1283
|
+
description
|
|
1284
|
+
};
|
|
1285
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
|
|
1286
|
+
return {
|
|
1287
|
+
code,
|
|
1288
|
+
description,
|
|
1289
|
+
timeoutMs
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
/** 子调用参数必须是可无损 JSON 化的值。 */
|
|
1293
|
+
function losslessArgs(value) {
|
|
1294
|
+
if (value === void 0) return {};
|
|
1295
|
+
try {
|
|
1296
|
+
return JSON.parse(JSON.stringify(value));
|
|
1297
|
+
} catch {
|
|
1298
|
+
throw new Error("codemode: tool arguments must be lossless JSON");
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
/** 一个工具的调用是否可以与兄弟调用重叠(只有明确 true 才算)。 */
|
|
1302
|
+
function isConcurrencySafe(entry, args) {
|
|
1303
|
+
if (entry === void 0 || typeof entry.isConcurrencySafe !== "function") return false;
|
|
1304
|
+
try {
|
|
1305
|
+
return entry.isConcurrencySafe(args) === true;
|
|
1306
|
+
} catch {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/** 一个 FIFO 互斥段:写类调用串行执行。 */
|
|
1311
|
+
function createMutex() {
|
|
1312
|
+
let tail = Promise.resolve();
|
|
1313
|
+
return { run(task) {
|
|
1314
|
+
const next = tail.then(task, task);
|
|
1315
|
+
tail = next.catch(() => void 0);
|
|
1316
|
+
return next;
|
|
1317
|
+
} };
|
|
1318
|
+
}
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/entry-codemode.ts
|
|
1321
|
+
/**
|
|
1322
|
+
* Code Mode 预设行 —— 本包自己的「原生工具 + 程序传输」呈现方式。
|
|
1323
|
+
*
|
|
1324
|
+
* 这一行挂在 `dispatch` 预设里(见 presets/dispatch/agent.cordis.yml 与 src/host/agent-preset.ts),
|
|
1325
|
+
* 因此它注册的工具、它选中的 executor、它提供的提示词段都属于**这个预设的会话**,其它预设
|
|
1326
|
+
* 完全不受影响。
|
|
1327
|
+
*
|
|
1328
|
+
* ## 它替掉了什么
|
|
1329
|
+
*
|
|
1330
|
+
* 内核的 `ptc` 呈现方式只把 `run_code` 交给模型,其余工具一律硬拒(UNKNOWN_TOOL),
|
|
1331
|
+
* `standard` 又完全没有程序传输——「原生工具 + 程序」这个组合在内核里表达不出来。本行用
|
|
1332
|
+
* `standard` 的原生工具面 + 自己的 `code_run` 把这个组合重建,再把预设清单以外的工具
|
|
1333
|
+
* 挡在上下文之外,只留程序内可达。
|
|
1334
|
+
*
|
|
1335
|
+
* ## 四件事
|
|
1336
|
+
*
|
|
1337
|
+
* 1. 注册发现工具(list / search / describe / debug);
|
|
1338
|
+
* 2. 注册程序传输工具 `code_run`;
|
|
1339
|
+
* 3. 选一个执行器:`describe()` 丢掉预设清单以外的名字,`execute()` 派发前拒绝模型直呼,
|
|
1340
|
+
* 其余整批委托给内核原生执行器(完整策略 / 审批 / post-execute 管线);
|
|
1341
|
+
* 4. 提供**恒定**的 Code Mode 说明段,替换内核那份 SDK 声明段。
|
|
1342
|
+
*
|
|
1343
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/codemode
|
|
1344
|
+
*/
|
|
1345
|
+
/** Cordis 插件名。 */
|
|
1346
|
+
const name = "codemode";
|
|
1347
|
+
/** 依赖的服务。 */
|
|
1348
|
+
const inject = ["tools", "systemPrompt"];
|
|
1349
|
+
/** 决策环长度:只留给 `code_debug`。 */
|
|
1350
|
+
const DEBUG_RING = 50;
|
|
1351
|
+
/**
|
|
1352
|
+
* 装配本行。
|
|
1353
|
+
*
|
|
1354
|
+
* @param ctx - 预设作用域上的行上下文。
|
|
1355
|
+
* @param config - 行配置({@link CodemodeConfig})。
|
|
1356
|
+
*/
|
|
1357
|
+
function apply(ctx, config = {}) {
|
|
1358
|
+
const context = ctx;
|
|
1359
|
+
const log = (level, message) => {
|
|
1360
|
+
try {
|
|
1361
|
+
context.logger?.[level]?.(message);
|
|
1362
|
+
} catch {}
|
|
1363
|
+
};
|
|
1364
|
+
const tools = context.tools;
|
|
1365
|
+
const runtimeOf = () => {
|
|
1366
|
+
try {
|
|
1367
|
+
return context.get?.("ptcRuntime");
|
|
1368
|
+
} catch {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
/** 读一个内核服务(缺失或未挂载时为 undefined)。 */
|
|
1373
|
+
const serviceOf = (name) => {
|
|
1374
|
+
try {
|
|
1375
|
+
return context.get?.(name);
|
|
1376
|
+
} catch {
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
/**
|
|
1381
|
+
* 取会话级运行授权:目录与文件策略都读内核服务,本行只转交、不自己造。
|
|
1382
|
+
*
|
|
1383
|
+
* `ptcRuntime.run()` 在 DSH 0.2.2 起要求已解析的策略与目录;缺了这两项,程序会带着部署
|
|
1384
|
+
* 默认值(通常是 read-only 与部署根目录)执行,或者干脆被运行时拒绝。老代 harness 没有这两个
|
|
1385
|
+
* 服务,返回 undefined 让它照旧兜底。
|
|
1386
|
+
*/
|
|
1387
|
+
const runAuthority = async (agent, signal) => {
|
|
1388
|
+
const authority = {};
|
|
1389
|
+
const session = typeof agent === "object" && agent !== null ? agent.session : void 0;
|
|
1390
|
+
const policy = serviceOf("sandboxPolicy");
|
|
1391
|
+
if (typeof policy?.resolve === "function") try {
|
|
1392
|
+
const resolved = policy.resolve(session === void 0 ? {} : { session });
|
|
1393
|
+
if (resolved !== void 0) authority.sandboxPolicy = resolved;
|
|
1394
|
+
} catch {}
|
|
1395
|
+
const directories = serviceOf("workingDirectory");
|
|
1396
|
+
if (agent !== void 0 && typeof directories?.ensure === "function") try {
|
|
1397
|
+
authority.cwd = await directories.ensure(agent, signal);
|
|
1398
|
+
} catch {}
|
|
1399
|
+
return authority;
|
|
1400
|
+
};
|
|
1401
|
+
const rules = createRules(config);
|
|
1402
|
+
const retention = createRetention(runtimeOf);
|
|
1403
|
+
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1404
|
+
retention.dispose();
|
|
1405
|
+
}, "codemode retained environments");
|
|
1406
|
+
const scopeKey = findScopeKey(ctx);
|
|
1407
|
+
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.");
|
|
1408
|
+
/** 最近一次 describe 收到的完整定义集(含被遮蔽的)。 */
|
|
1409
|
+
let catalog = [];
|
|
1410
|
+
const debugRing = [];
|
|
1411
|
+
let firstDescribeLogged = false;
|
|
1412
|
+
const host = {
|
|
1413
|
+
catalog: () => catalog,
|
|
1414
|
+
runtime: runtimeOf,
|
|
1415
|
+
stateOwner: (agent) => retention.owner(agent),
|
|
1416
|
+
releaseOwner: (owner) => retention.release(owner),
|
|
1417
|
+
runAuthority,
|
|
1418
|
+
async callTool(request) {
|
|
1419
|
+
const call = {
|
|
1420
|
+
type: "tool-call",
|
|
1421
|
+
id: request.callId,
|
|
1422
|
+
name: request.name,
|
|
1423
|
+
arguments: stringifyArguments(request.args)
|
|
1424
|
+
};
|
|
1425
|
+
let batch;
|
|
1426
|
+
try {
|
|
1427
|
+
batch = await tools.execute({
|
|
1428
|
+
calls: [call],
|
|
1429
|
+
signal: request.signal,
|
|
1430
|
+
...request.agent === void 0 ? {} : { agent: request.agent },
|
|
1431
|
+
...request.toolSet === void 0 ? {} : { toolSet: request.toolSet }
|
|
1432
|
+
});
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
return { error: messageOf(error) };
|
|
1435
|
+
}
|
|
1436
|
+
const result = batch.results?.[0]?.result;
|
|
1437
|
+
if (result === void 0) return { error: "sub-call produced no result" };
|
|
1438
|
+
const content = result.content ?? [];
|
|
1439
|
+
if (result.isError === true) return {
|
|
1440
|
+
error: errorTextOf(result),
|
|
1441
|
+
content,
|
|
1442
|
+
...errorInfoOf(result)
|
|
1443
|
+
};
|
|
1444
|
+
const meta = result.meta;
|
|
1445
|
+
return {
|
|
1446
|
+
...result.value !== void 0 ? { value: result.value } : { text: textOf(result.content) },
|
|
1447
|
+
content,
|
|
1448
|
+
...meta === void 0 ? {} : { meta }
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
for (const definition of createDiscoveryDefinitions(rules, () => currentCatalogSnapshot() ?? catalog, () => debugRing)) tools.register(definition);
|
|
1453
|
+
tools.register(createTransportDefinition(host, rules));
|
|
1454
|
+
if (typeof tools.setExecutor !== "function") {
|
|
1455
|
+
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.");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const delegate = new NativeToolExecutor(ctx);
|
|
1459
|
+
const view = () => resolveCatalogView(tools, scopeKey, catalog.map((entry) => entry.name));
|
|
1460
|
+
const executor = createCodemodeExecutor(ctx, rules, {
|
|
1461
|
+
delegate,
|
|
1462
|
+
view,
|
|
1463
|
+
viewForName: (toolName) => resolveNameView(tools, scopeKey, toolName),
|
|
1464
|
+
sectionText: () => buildCodemodeSectionText({
|
|
1465
|
+
runToolName: rules.options.runToolName,
|
|
1466
|
+
metaTools: rules.options.metaTools,
|
|
1467
|
+
programFormat: runtimeOf()?.programFormat
|
|
1468
|
+
}),
|
|
1469
|
+
onCatalog: (definitions) => {
|
|
1470
|
+
catalog = definitions.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
1471
|
+
},
|
|
1472
|
+
onDescribe: (decision) => {
|
|
1473
|
+
const hidden = summarizeHidden(decision.masked);
|
|
1474
|
+
debugRing.push({
|
|
1475
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1476
|
+
visible: decision.visible,
|
|
1477
|
+
hidden
|
|
1478
|
+
});
|
|
1479
|
+
while (debugRing.length > DEBUG_RING) debugRing.shift();
|
|
1480
|
+
const namespaces = hidden.namespaces.map((entry) => entry.namespace + "=" + String(entry.count)).join(", ");
|
|
1481
|
+
const line = "[codemode] masked " + String(hidden.total) + " non-standard tool(s)" + (namespaces === "" ? "" : " (" + namespaces + ")") + "; request tools: " + decision.visible.join(", ");
|
|
1482
|
+
if (!firstDescribeLogged) {
|
|
1483
|
+
firstDescribeLogged = true;
|
|
1484
|
+
log("info", line);
|
|
1485
|
+
} else log("debug", line);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
tools.setExecutor(executor);
|
|
1489
|
+
const scopeNote = scopeKey === void 0 ? "" : ", everything outside this preset scope";
|
|
1490
|
+
log("info", "[codemode] active: transport [" + rules.options.runToolName + "], discovery [" + rules.options.metaTools.join(", ") + "], hiding [" + rules.options.hiddenPrefixes.join(", ") + "]" + scopeNote);
|
|
1491
|
+
}
|
|
1492
|
+
/** 把一个工具定义投影成程序清单条目。 */
|
|
1493
|
+
function toCatalogEntry(definition) {
|
|
1494
|
+
if (typeof definition !== "object" || definition === null) return void 0;
|
|
1495
|
+
const record = definition;
|
|
1496
|
+
const toolName = record["name"];
|
|
1497
|
+
if (typeof toolName !== "string" || toolName.length === 0) return void 0;
|
|
1498
|
+
const description = typeof record["description"] === "string" ? record["description"] : "";
|
|
1499
|
+
const safe = record["isConcurrencySafe"];
|
|
1500
|
+
return {
|
|
1501
|
+
name: toolName,
|
|
1502
|
+
description,
|
|
1503
|
+
parameters: record["parameters"],
|
|
1504
|
+
...typeof safe === "function" ? { isConcurrencySafe: safe } : {}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
/** 参数序列化成线上形状(`ToolCallBlock.arguments` 是字符串)。 */
|
|
1508
|
+
function stringifyArguments(args) {
|
|
1509
|
+
if (args === void 0) return "{}";
|
|
1510
|
+
try {
|
|
1511
|
+
return JSON.stringify(args);
|
|
1512
|
+
} catch {
|
|
1513
|
+
return "{}";
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/** 一条失败结果的面向前模型文本。 */
|
|
1517
|
+
function errorTextOf(result) {
|
|
1518
|
+
const message = typeof result.error?.message === "string" ? result.error.message : void 0;
|
|
1519
|
+
if (message !== void 0 && message.length > 0) return message;
|
|
1520
|
+
const text = textOf(result.content);
|
|
1521
|
+
return text.length > 0 ? text : "tool call failed";
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* 读出结构化失败身份。
|
|
1525
|
+
*
|
|
1526
|
+
* 内核把 `result.error.info` 原样放进回放事件的 `error` 字段;缺任一必填项时给空
|
|
1527
|
+
* 对象,让调用方可以无条件展开。
|
|
1528
|
+
*
|
|
1529
|
+
* @param result - 一条失败结果。
|
|
1530
|
+
* @returns 带 `errorInfo` 的对象,或空对象。
|
|
1531
|
+
*/
|
|
1532
|
+
function errorInfoOf(result) {
|
|
1533
|
+
const info = result.error?.info;
|
|
1534
|
+
if (info === void 0 || info === null) return {};
|
|
1535
|
+
const name = nonEmptyString(info.name);
|
|
1536
|
+
const code = nonEmptyString(info.code);
|
|
1537
|
+
if (name === void 0 || code === void 0) return {};
|
|
1538
|
+
const reason = nonEmptyString(info.reason);
|
|
1539
|
+
return { errorInfo: {
|
|
1540
|
+
name,
|
|
1541
|
+
code,
|
|
1542
|
+
...reason === void 0 ? {} : { reason }
|
|
1543
|
+
} };
|
|
1544
|
+
}
|
|
1545
|
+
/** 非空字符串,或 `undefined`。 */
|
|
1546
|
+
function nonEmptyString(value) {
|
|
1547
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1548
|
+
}
|
|
1549
|
+
/** 把内容块里的文本拼起来。 */
|
|
1550
|
+
function textOf(content) {
|
|
1551
|
+
if (!Array.isArray(content)) return "";
|
|
1552
|
+
const parts = [];
|
|
1553
|
+
for (const block of content) {
|
|
1554
|
+
if (typeof block !== "object" || block === null) continue;
|
|
1555
|
+
const record = block;
|
|
1556
|
+
if (record["type"] === "text" && typeof record["text"] === "string") parts.push(record["text"]);
|
|
1557
|
+
}
|
|
1558
|
+
return parts.join("\n");
|
|
1559
|
+
}
|
|
1560
|
+
/** 一个抛出值的消息。 */
|
|
1561
|
+
function messageOf(error) {
|
|
1562
|
+
return error instanceof Error ? error.message : String(error);
|
|
1563
|
+
}
|
|
1564
|
+
//#endregion
|
|
1565
|
+
export { apply, inject, name };
|