@eddyskywalker/dsh-chatgpt-subscription 0.59.0 → 0.61.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 +42 -0
- package/README.md +1 -1
- package/lib/client.js +1578 -232
- package/lib/client.js.map +1 -1
- package/lib/codemode.js +795 -178
- package/lib/entry-plan-mode-CLRKkgYG.js +1787 -0
- package/lib/{entry-progress-updates-CxQewio9.js → entry-progress-updates-DZ0ZRus4.js} +14 -1
- package/lib/index.js +1079 -754
- package/lib/llm-compat-u9mor0uu.js +110 -0
- package/lib/plan-mode.js +2 -779
- package/lib/progress-updates.js +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/compat.d.ts +1 -1
- package/lib/types/entry-codemode.d.ts.map +1 -1
- package/lib/types/entry-plan-mode.d.ts +136 -0
- package/lib/types/entry-plan-mode.d.ts.map +1 -1
- package/lib/types/host/agent-preset.d.ts.map +1 -1
- package/lib/types/host/claude/adapter.d.ts +16 -0
- package/lib/types/host/claude/adapter.d.ts.map +1 -1
- package/lib/types/host/claude/client.d.ts +7 -1
- package/lib/types/host/claude/client.d.ts.map +1 -1
- package/lib/types/host/claude/mapper.d.ts +9 -0
- package/lib/types/host/claude/mapper.d.ts.map +1 -1
- package/lib/types/host/claude/types.d.ts +10 -0
- package/lib/types/host/claude/types.d.ts.map +1 -1
- package/lib/types/host/cline-pass/mapper.d.ts.map +1 -1
- package/lib/types/host/codemode/discovery.d.ts.map +1 -1
- package/lib/types/host/codemode/executor.d.ts.map +1 -1
- package/lib/types/host/codemode/rules.d.ts +16 -6
- package/lib/types/host/codemode/rules.d.ts.map +1 -1
- package/lib/types/host/codemode/transport.d.ts +120 -41
- package/lib/types/host/codemode/transport.d.ts.map +1 -1
- package/lib/types/host/command-code/mapper.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/common/orphan-tool-calls.d.ts +48 -0
- package/lib/types/host/common/orphan-tool-calls.d.ts.map +1 -0
- package/lib/types/host/dispatch-execution-guard.d.ts +32 -2
- package/lib/types/host/dispatch-execution-guard.d.ts.map +1 -1
- package/lib/types/host/glm-coding-plan/mapper-anthropic.d.ts.map +1 -1
- package/lib/types/host/glm-coding-plan/mapper-openai.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/minimax-code/mapper.d.ts.map +1 -1
- package/lib/types/host/model-catalog.d.ts +21 -0
- package/lib/types/host/model-catalog.d.ts.map +1 -1
- package/lib/types/host/opendesign/mapper.d.ts.map +1 -1
- package/lib/types/host/plan-mode-cache-guard.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/lib/types/host/progress-updates/nudge.d.ts +13 -1
- package/lib/types/host/progress-updates/nudge.d.ts.map +1 -1
- package/lib/types/host/qoder/mapper.d.ts.map +1 -1
- package/lib/types/host/responses-client.d.ts.map +1 -1
- package/lib/types/host/responses-mapper.d.ts +12 -0
- package/lib/types/host/responses-mapper.d.ts.map +1 -1
- package/lib/types/host/settled-job-guard.d.ts +77 -0
- package/lib/types/host/settled-job-guard.d.ts.map +1 -0
- package/lib/types/host/step-code/mapper.d.ts.map +1 -1
- package/lib/types/host/workbuddy/mapper.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/presets/dispatch/README.md +8 -13
- package/presets/dispatch/agent.cordis.yml +11 -16
package/lib/codemode.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { n as PLUGIN_MESSAGE_SOURCE_KIND } from "./llm-compat-u9mor0uu.js";
|
|
2
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
1
3
|
import { NativeToolExecutor } from "@deepseek-ai/dsh-tool-executor-native";
|
|
2
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
5
|
//#region src/host/codemode/discovery.ts
|
|
@@ -110,13 +112,19 @@ function searchDefinition(name, rules, catalog) {
|
|
|
110
112
|
additionalProperties: false,
|
|
111
113
|
required: [
|
|
112
114
|
"name",
|
|
115
|
+
"namespace",
|
|
113
116
|
"description",
|
|
114
117
|
"parameters"
|
|
115
118
|
],
|
|
116
119
|
properties: {
|
|
117
120
|
name: { type: "string" },
|
|
121
|
+
namespace: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Namespace of the tool: an MCP server name, or \"plugin\"."
|
|
124
|
+
},
|
|
118
125
|
description: { type: "string" },
|
|
119
|
-
parameters: {}
|
|
126
|
+
parameters: {},
|
|
127
|
+
output: { description: "The tool's declared output schema, when it has one." }
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
}
|
|
@@ -124,7 +132,7 @@ function searchDefinition(name, rules, catalog) {
|
|
|
124
132
|
},
|
|
125
133
|
render: (_args, value) => [{
|
|
126
134
|
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")
|
|
135
|
+
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 + " [" + hit.namespace + "]\n " + hit.description + "\n parameters: " + safeJson(hit.parameters) + (hit.output === void 0 ? "" : "\n output: " + safeJson(hit.output))).join("\n\n")
|
|
128
136
|
}]
|
|
129
137
|
},
|
|
130
138
|
async execute(args) {
|
|
@@ -149,8 +157,10 @@ function searchDefinition(name, rules, catalog) {
|
|
|
149
157
|
const hits = scored.slice(0, limit);
|
|
150
158
|
if (hits.length > 0) return { tools: hits.map((row) => ({
|
|
151
159
|
name: row.indexed.entry.name,
|
|
160
|
+
namespace: namespaceOfName(row.indexed.entry.name),
|
|
152
161
|
description: clip(row.indexed.entry.description),
|
|
153
|
-
parameters: row.indexed.entry.parameters
|
|
162
|
+
parameters: row.indexed.entry.parameters,
|
|
163
|
+
...row.indexed.entry.output === void 0 ? {} : { output: row.indexed.entry.output }
|
|
154
164
|
})) };
|
|
155
165
|
return {
|
|
156
166
|
tools: browseByNamespace(candidates, limit),
|
|
@@ -181,13 +191,15 @@ function describeDefinition(name, catalog) {
|
|
|
181
191
|
properties: {
|
|
182
192
|
found: { type: "boolean" },
|
|
183
193
|
name: { type: "string" },
|
|
194
|
+
namespace: { type: "string" },
|
|
184
195
|
description: { type: "string" },
|
|
185
|
-
parameters: {}
|
|
196
|
+
parameters: {},
|
|
197
|
+
output: { description: "The tool's declared output schema, when it has one." }
|
|
186
198
|
}
|
|
187
199
|
},
|
|
188
200
|
render: (_args, value) => [{
|
|
189
201
|
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)
|
|
202
|
+
text: value.found !== true ? "No tool named that is callable from this session." : String(value.name) + " [" + String(value.namespace) + "]\n" + String(value.description) + "\nparameters: " + safeJson(value.parameters) + (value.output === void 0 ? "" : "\noutput: " + safeJson(value.output))
|
|
191
203
|
}]
|
|
192
204
|
},
|
|
193
205
|
async execute(args) {
|
|
@@ -198,8 +210,10 @@ function describeDefinition(name, catalog) {
|
|
|
198
210
|
return {
|
|
199
211
|
found: true,
|
|
200
212
|
name: entry.name,
|
|
213
|
+
namespace: namespaceOfName(entry.name),
|
|
201
214
|
description: clip(entry.description),
|
|
202
|
-
parameters: entry.parameters
|
|
215
|
+
parameters: entry.parameters,
|
|
216
|
+
...entry.output === void 0 ? {} : { output: entry.output }
|
|
203
217
|
};
|
|
204
218
|
}
|
|
205
219
|
};
|
|
@@ -270,8 +284,10 @@ function browseByNamespace(entries, limit) {
|
|
|
270
284
|
if (rows.length >= limit) return rows;
|
|
271
285
|
rows.push({
|
|
272
286
|
name: entry.name,
|
|
287
|
+
namespace,
|
|
273
288
|
description: clip(namespace + ": " + entry.description),
|
|
274
|
-
parameters: entry.parameters
|
|
289
|
+
parameters: entry.parameters,
|
|
290
|
+
...entry.output === void 0 ? {} : { output: entry.output }
|
|
275
291
|
});
|
|
276
292
|
}
|
|
277
293
|
return rows;
|
|
@@ -448,10 +464,18 @@ const DEFAULT_HIDDEN_PREFIXES = Object.freeze(["mcp__"]);
|
|
|
448
464
|
/**
|
|
449
465
|
* 默认始终原生呈现的名字。
|
|
450
466
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
467
|
+
* 恒定系统提示词或子代理运行时契约里点名要求直接调用这些工具(大输出预览补读、调度账本查询、
|
|
468
|
+
* 进展更新汇报、结构化子代理输出提交,以及 Windows 下替代被撤回 `pwsh` 的 `win_bash`):
|
|
469
|
+
* 遮蔽它们要么让提示词指向模型看不见的名字并触发整批 `UNKNOWN_TOOL` 重跑,要么让会话顶层
|
|
470
|
+
* 失去唯一可用的 Shell。若某个工具当前未在宿主/子会话注册,保留在此集合中不会凭空暴露它。
|
|
453
471
|
*/
|
|
454
|
-
const DEFAULT_NATIVE_TOOLS = Object.freeze([
|
|
472
|
+
const DEFAULT_NATIVE_TOOLS = Object.freeze([
|
|
473
|
+
"cache_warmup_result",
|
|
474
|
+
"dispatch_ledger",
|
|
475
|
+
"progress_update",
|
|
476
|
+
"structured_output",
|
|
477
|
+
"win_bash"
|
|
478
|
+
]);
|
|
455
479
|
/**
|
|
456
480
|
* 把原始配置归一化。
|
|
457
481
|
*
|
|
@@ -462,12 +486,8 @@ function createRules(config) {
|
|
|
462
486
|
const options = normalizeConfig(config);
|
|
463
487
|
return {
|
|
464
488
|
options,
|
|
465
|
-
hidden: new Set(options.hiddenTools),
|
|
466
|
-
native: /* @__PURE__ */ new Set([
|
|
467
|
-
...options.nativeTools,
|
|
468
|
-
options.runToolName,
|
|
469
|
-
...options.metaTools
|
|
470
|
-
])
|
|
489
|
+
hidden: /* @__PURE__ */ new Set([...options.hiddenTools, ...options.metaTools]),
|
|
490
|
+
native: /* @__PURE__ */ new Set([...options.nativeTools, options.runToolName])
|
|
471
491
|
};
|
|
472
492
|
}
|
|
473
493
|
/**
|
|
@@ -491,6 +511,8 @@ function normalizeConfig(config) {
|
|
|
491
511
|
runToolName,
|
|
492
512
|
maxParallelSubCalls: positiveInteger(source.maxParallelSubCalls, 10),
|
|
493
513
|
maxSubCalls: positiveInteger(source.maxSubCalls, 512),
|
|
514
|
+
explicitMaxSubCalls: typeof source.maxSubCalls === "number" && Number.isInteger(source.maxSubCalls) && source.maxSubCalls > 0,
|
|
515
|
+
maxSubCallsPerMinute: positiveInteger(source.maxSubCallsPerMinute, 1024),
|
|
494
516
|
guard: source.guard !== false,
|
|
495
517
|
sdkSection: source.sdkSection !== false,
|
|
496
518
|
debug: source.debug !== false
|
|
@@ -561,7 +583,7 @@ function summarizeHidden(names) {
|
|
|
561
583
|
*
|
|
562
584
|
* ## 恒定契约
|
|
563
585
|
*
|
|
564
|
-
* 这段正文**只**依赖入参里的两个工具名与部署级的 `programFormat`。它刻意不含遮蔽
|
|
586
|
+
* 这段正文**只**依赖入参里的两个工具名与部署级的 `language` / `programFormat`。它刻意不含遮蔽
|
|
565
587
|
* 工具的数量、namespace 分布、会话 id 或时间,因此同一部署下逐字节恒定:挂载或卸载
|
|
566
588
|
* MCP 服务端不会改写它,前缀缓存不会因为它失效。
|
|
567
589
|
*
|
|
@@ -569,34 +591,51 @@ function summarizeHidden(names) {
|
|
|
569
591
|
* @returns 说明段正文。
|
|
570
592
|
*/
|
|
571
593
|
function buildCodemodeSectionText(input) {
|
|
594
|
+
const python = input.language === "python";
|
|
572
595
|
const retained = input.programFormat === "repl";
|
|
573
596
|
const listTool = input.metaTools[0] ?? "code_tools_list";
|
|
574
597
|
const searchTool = input.metaTools[1] ?? "code_tools_search";
|
|
575
598
|
const describeTool = input.metaTools[2] ?? "code_tools_describe";
|
|
599
|
+
const run = input.runToolName;
|
|
600
|
+
const example = python ? [
|
|
601
|
+
"```py",
|
|
602
|
+
"# standard tools and hidden tools alike",
|
|
603
|
+
"import asyncio",
|
|
604
|
+
"found = await tools.glob({\"pattern\": \"src/**/*.ts\"})",
|
|
605
|
+
"pages = await asyncio.gather(*(tools.read({\"file_path\": p}) for p in found[\"paths\"]))",
|
|
606
|
+
"[l[\"text\"] for page in pages for l in page[\"lines\"] if \"TODO\" in l[\"text\"]]",
|
|
607
|
+
"```"
|
|
608
|
+
] : [
|
|
609
|
+
"```js",
|
|
610
|
+
"// standard tools and hidden tools alike",
|
|
611
|
+
"const { paths } = await tools.glob({ pattern: \"src/**/*.ts\" })",
|
|
612
|
+
"const pages = await Promise.all(paths.map((p) => tools.read({ file_path: p })))",
|
|
613
|
+
retained ? "pages.flatMap((r) => r.lines.filter((l) => l.text.includes(\"TODO\")).map((l) => l.text))" : "return pages.flatMap((r) => r.lines.filter((l) => l.text.includes(\"TODO\")).map((l) => l.text))",
|
|
614
|
+
"```"
|
|
615
|
+
];
|
|
576
616
|
return [
|
|
577
617
|
"## Code Mode",
|
|
578
618
|
"",
|
|
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 " +
|
|
619
|
+
"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 + ".",
|
|
580
620
|
"",
|
|
581
621
|
"### Calling tools from a program",
|
|
582
622
|
"",
|
|
583
|
-
|
|
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
|
-
"```",
|
|
623
|
+
...example,
|
|
589
624
|
"",
|
|
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.",
|
|
625
|
+
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.",
|
|
591
626
|
"- 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.",
|
|
627
|
+
"- File, search, and shell tool results are named-field objects, never bare strings (whereas `job_list` returns a bare array): `glob` answers `{ root, paths }`, `grep` answers `{ matches }` with matches shaped `{ path, lineNumber, line }`, `read` answers `{ path, offset, lines, totalLines }` with `lines` items shaped `{ number, text }`, and `win_bash` answers `{ stdout, stderr, exitCode }`. A named-field object carries its list or text inside those fields — `r.lines.map((l) => l.text)` — so never call `.map`, `.filter`, `.split`, `.splitlines` or `in` on the envelope itself.",
|
|
628
|
+
"- Discovery returns whole objects too: `" + listTool + "` answers `{ tools: [{ name, description, namespace }] }`, `" + searchTool + "` answers `{ tools: [{ name, namespace, description, parameters, output? }], fallback? }`, and `" + describeTool + "` answers `{ found, name?, namespace?, description?, parameters?, output? }`.",
|
|
629
|
+
"- Tool argument objects and `return` values must be lossless JSON: **never leave `undefined` in an object property value** — `{ a: x.opt }` fails the call when `x.opt === undefined`. Omit the key, write `x.opt ?? null`, or print it with `console.log`.",
|
|
630
|
+
"- Prefer batching multi-step read/search chains inside one " + run + " program instead of issuing many sequential single-tool turns.",
|
|
592
631
|
"- Independent read-only calls may overlap under Promise.all; mutating calls run alone, in submission order.",
|
|
593
632
|
"- 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
633
|
"",
|
|
595
634
|
"### Discovery API (callable from inside a program)",
|
|
596
635
|
"",
|
|
597
636
|
"- 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
|
|
637
|
+
"- tools." + searchTool + "({ query, limit?, namespace? }) - ranked matches with their full parameter schemas and output schema when the tool declares one; 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.",
|
|
638
|
+
"- tools." + describeTool + "({ name }) - one tool description, its complete parameter schema, and its output schema when declared.",
|
|
600
639
|
"",
|
|
601
640
|
"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
641
|
].join("\n");
|
|
@@ -782,6 +821,7 @@ function denialFor(call, rules, hooks, batchSignal) {
|
|
|
782
821
|
if (isNestedCall(call, batchSignal)) return void 0;
|
|
783
822
|
if (name === rules.options.runToolName) return void 0;
|
|
784
823
|
if (!isMaskedName(name, rules, hooks.viewForName(name))) return void 0;
|
|
824
|
+
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.";
|
|
785
825
|
const searchTool = rules.options.metaTools[1] ?? rules.options.metaTools[0] ?? "code_tools_search";
|
|
786
826
|
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
827
|
}
|
|
@@ -978,17 +1018,17 @@ function asRegistry(tools) {
|
|
|
978
1018
|
* 用自己的名字注册同一件事——把模型写的一段程序交给 `ctx.ptcRuntime` 执行,程序里的每次
|
|
979
1019
|
* `tools.<name>(args)` 都是一次真实的工具调用。
|
|
980
1020
|
*
|
|
981
|
-
* ##
|
|
982
|
-
*
|
|
983
|
-
* 子调用走公开的 `ctx.tools.execute()`,因此落到**注册表选中的执行器**上:在本预设里就是
|
|
984
|
-
* 原生 executor 的完整管线(pre-policy、审批、around-dispatch、post-execute 一个不少)。
|
|
985
|
-
* 手写这条管线是插件静默丢策略的典型方式,所以这里一行都不抄。
|
|
1021
|
+
* ## 与内核 PTC `run_code` 对齐的使用配置
|
|
986
1022
|
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
1023
|
+
* 1. **运行时感知 Schema**:按 `ptcRuntime` 的语言(TypeScript / Python)、程序形态
|
|
1024
|
+
* (`function-body` / 常驻 REPL)、超时预算(`defaultMs` / `maxMs`)与沙箱支持动态投影
|
|
1025
|
+
* 描述和参数(含单次提权的 `sandbox_permissions` + `justification`);同一部署下静态恒定,
|
|
1026
|
+
* 不会扰动前缀缓存。
|
|
1027
|
+
* 2. **完整子调用调度与上下文转发**:只读并发上限受 `maxParallelSubCalls` 约束,写类调用等待
|
|
1028
|
+
* 在飞调用排空后独占执行;程序结算时中止悬空请求并等待已启动子调用落盘;子调用产生的图片块、
|
|
1029
|
+
* `additionalContexts` 与 `concludesTurn` 原样转交给外层步骤。
|
|
1030
|
+
* 3. **展示元数据与输出格式化**:声明 `presentCall` 与 `output.presentationMeta`(记录语言),
|
|
1031
|
+
* 深层 JSON 结果控制缩进上限,失败时保留已捕获输出与沙箱诊断信息。
|
|
992
1032
|
*
|
|
993
1033
|
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/transport
|
|
994
1034
|
*/
|
|
@@ -1011,6 +1051,79 @@ const catalogScope = new AsyncLocalStorage();
|
|
|
1011
1051
|
function currentCatalogSnapshot() {
|
|
1012
1052
|
return catalogScope.getStore();
|
|
1013
1053
|
}
|
|
1054
|
+
/** 允许单次提权的目标沙箱模式(对齐 `@deepseek-ai/dsh-sandbox` 的 `ESCALATION_TARGETS`)。 */
|
|
1055
|
+
const ESCALATION_TARGETS = Object.freeze(["workspace-write", "danger-full-access"]);
|
|
1056
|
+
/** 与内核 `run_code` 一致的 `description` 参数说明。 */
|
|
1057
|
+
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\".";
|
|
1058
|
+
function buildFlavor(languageName, execution) {
|
|
1059
|
+
return {
|
|
1060
|
+
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.`,
|
|
1061
|
+
codeDescription: execution.codeDescription
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const TYPESCRIPT_REPL_FLAVOR = buildFlavor("TypeScript", {
|
|
1065
|
+
programDescription: "the program to execute",
|
|
1066
|
+
outputInstruction: "Curate the program output.",
|
|
1067
|
+
codeDescription: "The TypeScript program to execute, following this tool's execution instructions."
|
|
1068
|
+
});
|
|
1069
|
+
const TYPESCRIPT_FUNCTION_FLAVOR = buildFlavor("TypeScript", {
|
|
1070
|
+
programDescription: "the BODY of an async function (erasable syntax only; top-level `await` and `return` work)",
|
|
1071
|
+
outputInstruction: "Only what you print or return is program output — curate it.",
|
|
1072
|
+
codeDescription: "The program: the body of an async TypeScript function."
|
|
1073
|
+
});
|
|
1074
|
+
const PYTHON_FLAVOR = buildFlavor("Python", {
|
|
1075
|
+
programDescription: "the program to execute",
|
|
1076
|
+
outputInstruction: "Curate the program output.",
|
|
1077
|
+
codeDescription: "The Python program to execute, following this tool's execution instructions."
|
|
1078
|
+
});
|
|
1079
|
+
function resolveFlavor(runtime) {
|
|
1080
|
+
if (runtime?.language === "python") return PYTHON_FLAVOR;
|
|
1081
|
+
if (runtime?.language === "typescript" && runtime.programFormat === "repl") return TYPESCRIPT_REPL_FLAVOR;
|
|
1082
|
+
return TYPESCRIPT_FUNCTION_FLAVOR;
|
|
1083
|
+
}
|
|
1084
|
+
function escalationGuidance(sandboxMode) {
|
|
1085
|
+
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.";
|
|
1086
|
+
}
|
|
1087
|
+
function buildTransportDescription(runtime) {
|
|
1088
|
+
const flavor = resolveFlavor(runtime);
|
|
1089
|
+
const instructions = typeof runtime?.executionInstructions === "string" && runtime.executionInstructions.length > 0 ? ` ${runtime.executionInstructions}` : "";
|
|
1090
|
+
return flavor.description + instructions + " Each program starts in the Session's current directory. Running programs keep their initial directory." + escalationGuidance(runtime?.sandboxMode);
|
|
1091
|
+
}
|
|
1092
|
+
function buildTransportParameters(runtime) {
|
|
1093
|
+
const flavor = resolveFlavor(runtime);
|
|
1094
|
+
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.";
|
|
1095
|
+
const properties = {
|
|
1096
|
+
code: {
|
|
1097
|
+
type: "string",
|
|
1098
|
+
description: flavor.codeDescription
|
|
1099
|
+
},
|
|
1100
|
+
description: {
|
|
1101
|
+
type: "string",
|
|
1102
|
+
description: DESCRIPTION_PARAM_DESCRIPTION
|
|
1103
|
+
},
|
|
1104
|
+
timeoutMs: {
|
|
1105
|
+
type: "number",
|
|
1106
|
+
description: timeoutDescription
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
if (runtime?.sandboxMode !== void 0) {
|
|
1110
|
+
properties["sandbox_permissions"] = {
|
|
1111
|
+
type: "string",
|
|
1112
|
+
enum: [...ESCALATION_TARGETS],
|
|
1113
|
+
description: "Wider sandbox mode for this complete program execution; requires justification and approval."
|
|
1114
|
+
};
|
|
1115
|
+
properties["justification"] = {
|
|
1116
|
+
type: "string",
|
|
1117
|
+
description: "Reason this complete program needs wider access, shown to the user for approval. Use the language of the user’s current request."
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
type: "object",
|
|
1122
|
+
additionalProperties: false,
|
|
1123
|
+
required: ["code", "description"],
|
|
1124
|
+
properties
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1014
1127
|
/**
|
|
1015
1128
|
* 造 `code_run` 的工具定义。
|
|
1016
1129
|
*
|
|
@@ -1020,28 +1133,10 @@ function currentCatalogSnapshot() {
|
|
|
1020
1133
|
*/
|
|
1021
1134
|
function createTransportDefinition(host, rules) {
|
|
1022
1135
|
const runToolName = rules.options.runToolName;
|
|
1023
|
-
|
|
1136
|
+
const definition = {
|
|
1024
1137
|
name: runToolName,
|
|
1025
|
-
description:
|
|
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
|
-
},
|
|
1138
|
+
description: buildTransportDescription(void 0),
|
|
1139
|
+
parameters: buildTransportParameters(void 0),
|
|
1045
1140
|
output: {
|
|
1046
1141
|
schema: {
|
|
1047
1142
|
type: "object",
|
|
@@ -1059,120 +1154,275 @@ function createTransportDefinition(host, rules) {
|
|
|
1059
1154
|
required: ["mode", "denied"],
|
|
1060
1155
|
properties: {
|
|
1061
1156
|
mode: { type: "string" },
|
|
1062
|
-
denied: { type: "boolean" }
|
|
1157
|
+
denied: { type: "boolean" },
|
|
1158
|
+
enforcement: {
|
|
1159
|
+
type: "string",
|
|
1160
|
+
enum: ["full", "partial"]
|
|
1161
|
+
}
|
|
1063
1162
|
}
|
|
1064
1163
|
}
|
|
1065
1164
|
}
|
|
1066
1165
|
},
|
|
1067
1166
|
render: (_args, value) => {
|
|
1068
|
-
const parts = [];
|
|
1069
|
-
|
|
1070
|
-
if (value.
|
|
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.");
|
|
1167
|
+
const parts = [(value.logs ?? []).join("\n"), value.result === void 0 ? "" : renderProgramValue(value.result)].filter((part) => part.length > 0);
|
|
1168
|
+
if (value.sandbox?.enforcement === "partial") parts.push("File sandbox enforcement is partial on this host.");
|
|
1169
|
+
if (value.sandbox?.denied === true) parts.push("The " + value.sandbox.mode + " file sandbox denied an operation." + escalationGuidance(host.runtime()?.sandboxMode));
|
|
1076
1170
|
return [{
|
|
1077
1171
|
type: "text",
|
|
1078
1172
|
text: parts.length > 0 ? parts.join("\n") : "(" + runToolName + " completed with no output)"
|
|
1079
1173
|
}];
|
|
1174
|
+
},
|
|
1175
|
+
presentationMeta: () => {
|
|
1176
|
+
return { language: host.runtime()?.language === "python" ? "python" : "typescript" };
|
|
1080
1177
|
}
|
|
1081
1178
|
},
|
|
1179
|
+
presentCall(args) {
|
|
1180
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
1181
|
+
const record = args;
|
|
1182
|
+
const code = record["code"];
|
|
1183
|
+
const description = record["description"];
|
|
1184
|
+
if (typeof code !== "string" || typeof description !== "string" || description.trim().length === 0) return;
|
|
1185
|
+
return {
|
|
1186
|
+
card: "generic",
|
|
1187
|
+
title: description,
|
|
1188
|
+
kind: "execute",
|
|
1189
|
+
rawInput: code
|
|
1190
|
+
};
|
|
1191
|
+
},
|
|
1082
1192
|
async execute(args, exec) {
|
|
1083
1193
|
const input = readArguments(args);
|
|
1084
1194
|
const runtime = host.runtime();
|
|
1085
1195
|
if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
|
|
1196
|
+
if (input.timeoutMs !== void 0 && runtime.timeout !== void 0 === false && isStrictTimeoutUnsupported(runtime)) throw new Error("timeoutMs is not available for this PTC runtime");
|
|
1086
1197
|
const catalog = host.catalog();
|
|
1087
|
-
const
|
|
1088
|
-
const
|
|
1198
|
+
const runController = new AbortController();
|
|
1199
|
+
const onOuterAbort = () => {
|
|
1200
|
+
runController.abort(exec.signal.reason);
|
|
1201
|
+
};
|
|
1202
|
+
if (exec.signal.aborted) runController.abort(exec.signal.reason);
|
|
1203
|
+
else exec.signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
1204
|
+
const nestedSignal = markNested(runController.signal) ?? runController.signal;
|
|
1089
1205
|
const owner = host.stateOwner(exec.agent);
|
|
1090
|
-
|
|
1206
|
+
let maxSubCalls = rules.options.maxSubCalls;
|
|
1091
1207
|
let attempts = 0;
|
|
1092
|
-
const
|
|
1208
|
+
const lane = createCommitLane(rules.options.maxParallelSubCalls, () => runController.signal.aborted);
|
|
1093
1209
|
const definitions = /* @__PURE__ */ new Map();
|
|
1094
1210
|
for (const entry of catalog) definitions.set(entry.name, entry);
|
|
1095
|
-
const functions =
|
|
1211
|
+
const functions = Object.create(null);
|
|
1096
1212
|
for (const entry of catalog) {
|
|
1097
1213
|
if (entry.name === runToolName) continue;
|
|
1098
|
-
|
|
1214
|
+
const invoke = async (rawArgs) => {
|
|
1215
|
+
if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " not dispatched");
|
|
1099
1216
|
const n = ++attempts;
|
|
1100
|
-
if (n >
|
|
1217
|
+
if (n > maxSubCalls) {
|
|
1218
|
+
const limitErr = /* @__PURE__ */ new Error("program sub-call limit exceeded: " + String(maxSubCalls) + " calls");
|
|
1219
|
+
runController.abort(limitErr);
|
|
1220
|
+
throw limitErr;
|
|
1221
|
+
}
|
|
1101
1222
|
const parsed = losslessArgs(rawArgs);
|
|
1223
|
+
const loggedArgs = losslessArgs(parsed);
|
|
1102
1224
|
const callId = String(exec.callId) + NESTED_CALL_ID_MARKER + String(n);
|
|
1103
1225
|
const dispatchLog = createDispatchLog(exec.agent, {
|
|
1104
1226
|
rootCallId: rootCallIdOf(String(exec.callId)),
|
|
1105
1227
|
parentCallId: String(exec.callId),
|
|
1106
1228
|
subCallId: callId,
|
|
1107
1229
|
name: entry.name,
|
|
1108
|
-
arguments:
|
|
1230
|
+
arguments: loggedArgs
|
|
1109
1231
|
});
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1232
|
+
const mode = isConcurrencySafe(definitions.get(entry.name), parsed) ? "parallel" : "exclusive";
|
|
1233
|
+
return lane.schedule(mode, {
|
|
1234
|
+
start() {
|
|
1235
|
+
dispatchLog.start();
|
|
1236
|
+
},
|
|
1237
|
+
body: async () => await host.callTool({
|
|
1238
|
+
name: entry.name,
|
|
1239
|
+
args: parsed,
|
|
1240
|
+
callId,
|
|
1241
|
+
signal: nestedSignal,
|
|
1242
|
+
agent: exec.agent,
|
|
1243
|
+
toolSet: exec.toolSet
|
|
1244
|
+
}),
|
|
1245
|
+
/**
|
|
1246
|
+
* 提交阶段:按**提交顺序**逐条执行,落在会话里的副作用因此与提交顺序一致。
|
|
1247
|
+
*
|
|
1248
|
+
* 内核把这几步全部放在同一条有序 lane 里(`ptc.ts` 的 commit 游标):回放内容
|
|
1249
|
+
* 投影、settle 事件、图片与附加上下文、`concludeTurn`、放行互斥屏障。并发的
|
|
1250
|
+
* body 谁先跑完都不影响这一串的顺序——否则界面里的子调用记录会错位,延迟上下文
|
|
1251
|
+
* 也会插到错误的位置。
|
|
1252
|
+
*/
|
|
1253
|
+
async commit(parked) {
|
|
1254
|
+
if (!parked.ok) {
|
|
1255
|
+
dispatchLog.settle({ error: messageOf$1(parked.error) });
|
|
1256
|
+
throw parked.error;
|
|
1257
|
+
}
|
|
1258
|
+
const outcome = parked.value;
|
|
1259
|
+
const isError = outcome.error !== void 0;
|
|
1260
|
+
const content = await shapeDispatchContent(host, {
|
|
1119
1261
|
agent: exec.agent,
|
|
1120
|
-
|
|
1262
|
+
name: entry.name,
|
|
1263
|
+
arguments: loggedArgs,
|
|
1264
|
+
isError,
|
|
1265
|
+
content: clipContent(outcome.content)
|
|
1121
1266
|
});
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1267
|
+
dispatchLog.settle({
|
|
1268
|
+
...outcome,
|
|
1269
|
+
content
|
|
1270
|
+
});
|
|
1271
|
+
if (!isError && Array.isArray(outcome.content) && hasImageBlock(outcome.content)) {
|
|
1272
|
+
const imageMsg = host.createImageContext?.(outcome.content);
|
|
1273
|
+
if (imageMsg !== void 0) exec.deferContext?.(imageMsg);
|
|
1274
|
+
}
|
|
1275
|
+
if (Array.isArray(outcome.additionalContexts)) for (const contextMsg of outcome.additionalContexts) exec.deferContext?.(contextMsg);
|
|
1276
|
+
if (!isError && outcome.concluded === true) exec.concludeTurn?.();
|
|
1277
|
+
if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " result discarded");
|
|
1278
|
+
if (outcome.error !== void 0) throw new Error(outcome.error);
|
|
1279
|
+
return outcome.value !== void 0 ? outcome.value : outcome.text ?? null;
|
|
1280
|
+
},
|
|
1281
|
+
abandon() {
|
|
1282
|
+
throw new Error(runToolName + " run is over; " + entry.name + " tool call abandoned");
|
|
1125
1283
|
}
|
|
1126
|
-
|
|
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);
|
|
1284
|
+
});
|
|
1131
1285
|
};
|
|
1286
|
+
Object.defineProperty(functions, entry.name, {
|
|
1287
|
+
enumerable: true,
|
|
1288
|
+
configurable: true,
|
|
1289
|
+
writable: true,
|
|
1290
|
+
value: invoke
|
|
1291
|
+
});
|
|
1132
1292
|
}
|
|
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
1293
|
try {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1294
|
+
/**
|
|
1295
|
+
* 跑一次程序:每次重取会话授权,因为重试发生在策略变更之后。
|
|
1296
|
+
*
|
|
1297
|
+
* 单次沙箱提权(`sandbox_permissions`)只在第一次进入前审批一次,且提权后的单次执行不传
|
|
1298
|
+
* `stateOwner`,避免把单次授权固化到常驻环境中(对齐内核 `ptc.ts`)。
|
|
1299
|
+
*/
|
|
1300
|
+
let approvedEscalatedMode;
|
|
1301
|
+
/** 这次运行在入口那边的登记;结算时释放(重跑会重新登记一次)。 */
|
|
1302
|
+
let untrackRun = () => {};
|
|
1303
|
+
const attempt = async () => {
|
|
1304
|
+
const authority = await host.runAuthority?.(exec.agent, nestedSignal);
|
|
1305
|
+
const standingPolicy = authority?.sandboxPolicy;
|
|
1306
|
+
let effectivePolicy = standingPolicy;
|
|
1307
|
+
let escalated = false;
|
|
1308
|
+
if (input.sandboxPermissions !== void 0 && input.justification !== void 0) {
|
|
1309
|
+
if (standingPolicy === void 0 || typeof host.escalateSandbox !== "function") throw new Error("sandbox_permissions is not available for this PTC runtime");
|
|
1310
|
+
if (approvedEscalatedMode === void 0) {
|
|
1311
|
+
const nextPolicy = await host.escalateSandbox({
|
|
1312
|
+
requestedMode: input.sandboxPermissions,
|
|
1313
|
+
justification: input.justification,
|
|
1314
|
+
standingPolicy,
|
|
1315
|
+
agent: exec.agent,
|
|
1316
|
+
callId: String(exec.callId),
|
|
1317
|
+
toolName: runToolName,
|
|
1318
|
+
signal: exec.signal
|
|
1319
|
+
});
|
|
1320
|
+
effectivePolicy = nextPolicy;
|
|
1321
|
+
approvedEscalatedMode = modeOfPolicy(nextPolicy);
|
|
1322
|
+
} else if (typeof standingPolicy === "object" && standingPolicy !== null) effectivePolicy = {
|
|
1323
|
+
...standingPolicy,
|
|
1324
|
+
mode: approvedEscalatedMode
|
|
1325
|
+
};
|
|
1326
|
+
escalated = modeOfPolicy(effectivePolicy) !== modeOfPolicy(standingPolicy);
|
|
1327
|
+
}
|
|
1328
|
+
untrackRun();
|
|
1329
|
+
const runMode = modeOfPolicy(effectivePolicy);
|
|
1330
|
+
untrackRun = host.trackRun?.({
|
|
1331
|
+
agent: exec.agent,
|
|
1332
|
+
controller: runController,
|
|
1333
|
+
...runMode === void 0 ? {} : { mode: runMode }
|
|
1334
|
+
}) ?? (() => {});
|
|
1335
|
+
const request = {
|
|
1336
|
+
program: input.code,
|
|
1337
|
+
bindings: [{
|
|
1338
|
+
global: "tools",
|
|
1339
|
+
functions,
|
|
1340
|
+
errorClass: {
|
|
1341
|
+
name: "ToolCallError",
|
|
1342
|
+
memberNameProperty: "toolName"
|
|
1343
|
+
}
|
|
1344
|
+
}],
|
|
1345
|
+
...escalated ? {} : { stateOwner: owner },
|
|
1346
|
+
...authority?.cwd === void 0 ? {} : { cwd: authority.cwd },
|
|
1347
|
+
...effectivePolicy === void 0 ? {} : { sandboxPolicy: effectivePolicy },
|
|
1348
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
1349
|
+
signal: nestedSignal
|
|
1350
|
+
};
|
|
1351
|
+
const spec = typeof runtime.resolve === "function" ? runtime.resolve(request) : request;
|
|
1352
|
+
maxSubCalls = resolveSubCallBudget(spec, rules);
|
|
1353
|
+
return await catalogScope.run(catalog, () => runtime.run(spec));
|
|
1354
|
+
};
|
|
1355
|
+
let result;
|
|
1356
|
+
try {
|
|
1357
|
+
try {
|
|
1358
|
+
result = await attempt();
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (!isRetainedPolicyChange(error)) throw error;
|
|
1361
|
+
await host.releaseOwner(owner);
|
|
1362
|
+
host.stateOwner(exec.agent);
|
|
1363
|
+
result = await attempt();
|
|
1364
|
+
}
|
|
1365
|
+
} finally {
|
|
1366
|
+
runController.abort(runToolName + " settled");
|
|
1367
|
+
await lane.drain();
|
|
1368
|
+
untrackRun();
|
|
1369
|
+
}
|
|
1370
|
+
if (result.error !== void 0) {
|
|
1371
|
+
const logsText = Array.isArray(result.logs) && result.logs.length > 0 ? "\nCaptured output:\n" + result.logs.join("\n") : "";
|
|
1372
|
+
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" : "") + ".";
|
|
1373
|
+
const hint = result.sandbox?.denied ? escalationGuidance(runtime.sandboxMode) : "";
|
|
1374
|
+
throw new Error("program failed (" + result.error.kind + "): " + result.error.message + logsText + sandboxText + hint);
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
logs: [...result.logs ?? []],
|
|
1378
|
+
...result.value === void 0 ? {} : { result: result.value },
|
|
1379
|
+
...result.sandbox === void 0 ? {} : { sandbox: {
|
|
1380
|
+
mode: result.sandbox.mode,
|
|
1381
|
+
denied: result.sandbox.denied,
|
|
1382
|
+
...result.sandbox.enforcement === void 0 ? {} : { enforcement: result.sandbox.enforcement }
|
|
1383
|
+
} }
|
|
1384
|
+
};
|
|
1385
|
+
} finally {
|
|
1386
|
+
exec.signal.removeEventListener("abort", onOuterAbort);
|
|
1164
1387
|
}
|
|
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
1388
|
}
|
|
1175
1389
|
};
|
|
1390
|
+
Object.defineProperty(definition, "description", {
|
|
1391
|
+
enumerable: true,
|
|
1392
|
+
configurable: true,
|
|
1393
|
+
get: () => buildTransportDescription(safeRuntime(host))
|
|
1394
|
+
});
|
|
1395
|
+
Object.defineProperty(definition, "parameters", {
|
|
1396
|
+
enumerable: true,
|
|
1397
|
+
configurable: true,
|
|
1398
|
+
get: () => buildTransportParameters(safeRuntime(host))
|
|
1399
|
+
});
|
|
1400
|
+
return definition;
|
|
1401
|
+
}
|
|
1402
|
+
function safeRuntime(host) {
|
|
1403
|
+
try {
|
|
1404
|
+
return host.runtime();
|
|
1405
|
+
} catch {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/** 只有明确声明了 `timeout: undefined` 自身属性(而非测试假对象省略字段)才视作不支持 `timeoutMs`。 */
|
|
1410
|
+
function isStrictTimeoutUnsupported(runtime) {
|
|
1411
|
+
return Object.prototype.hasOwnProperty.call(runtime, "timeout") && runtime.timeout === void 0;
|
|
1412
|
+
}
|
|
1413
|
+
function modeOfPolicy(policy) {
|
|
1414
|
+
if (typeof policy !== "object" || policy === null) return void 0;
|
|
1415
|
+
const mode = policy.mode;
|
|
1416
|
+
return typeof mode === "string" ? mode : void 0;
|
|
1417
|
+
}
|
|
1418
|
+
function hasImageBlock(content) {
|
|
1419
|
+
return content.some((block) => typeof block === "object" && block !== null && block.type === "image");
|
|
1420
|
+
}
|
|
1421
|
+
function resolveSubCallBudget(spec, rules) {
|
|
1422
|
+
if (rules.options.explicitMaxSubCalls) return rules.options.maxSubCalls;
|
|
1423
|
+
if (spec.timeoutMs === null) return Infinity;
|
|
1424
|
+
if (typeof spec.timeoutMs === "number" && Number.isFinite(spec.timeoutMs) && spec.timeoutMs > 0) return Math.max(1, Math.floor(spec.timeoutMs / 6e4 * rules.options.maxSubCallsPerMinute));
|
|
1425
|
+
return rules.options.maxSubCalls;
|
|
1176
1426
|
}
|
|
1177
1427
|
/** 没有会话可写时的空记录。 */
|
|
1178
1428
|
const NO_DISPATCH_LOG = Object.freeze({
|
|
@@ -1183,21 +1433,6 @@ const NO_DISPATCH_LOG = Object.freeze({
|
|
|
1183
1433
|
const DISPATCH_LOG_TEXT_LIMIT = 8e3;
|
|
1184
1434
|
/**
|
|
1185
1435
|
* 造一条子调用的回放记录。
|
|
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
1436
|
*/
|
|
1202
1437
|
function createDispatchLog(agent, entry) {
|
|
1203
1438
|
const session = typeof agent === "object" && agent !== null ? agent.session : void 0;
|
|
@@ -1223,7 +1458,7 @@ function createDispatchLog(agent, entry) {
|
|
|
1223
1458
|
write("tool/ptc-dispatch", {
|
|
1224
1459
|
...base,
|
|
1225
1460
|
isError: outcome.error !== void 0,
|
|
1226
|
-
content:
|
|
1461
|
+
content: Array.isArray(outcome.content) ? outcome.content : [],
|
|
1227
1462
|
...outcome.meta === void 0 ? {} : { meta: outcome.meta },
|
|
1228
1463
|
...outcome.errorInfo === void 0 ? {} : { error: outcome.errorInfo }
|
|
1229
1464
|
});
|
|
@@ -1232,12 +1467,6 @@ function createDispatchLog(agent, entry) {
|
|
|
1232
1467
|
}
|
|
1233
1468
|
/**
|
|
1234
1469
|
* 把内容块裁到日志能承受的长度。
|
|
1235
|
-
*
|
|
1236
|
-
* 程序本身拿到的值不受影响;这里只是回放副本,所以宁可截断也不让一条子调用把整份
|
|
1237
|
-
* 大文件写进会话记录。
|
|
1238
|
-
*
|
|
1239
|
-
* @param content - 原始内容块。
|
|
1240
|
-
* @returns 可写进事件的副本。
|
|
1241
1470
|
*/
|
|
1242
1471
|
function clipContent(content) {
|
|
1243
1472
|
if (content === void 0) return [];
|
|
@@ -1254,12 +1483,6 @@ function clipContent(content) {
|
|
|
1254
1483
|
}
|
|
1255
1484
|
/**
|
|
1256
1485
|
* 一条调用所属调用树的根 id。
|
|
1257
|
-
*
|
|
1258
|
-
* `code_run` 不会被本行自己的程序嵌套调用,所以顶层工作量下就是它自己;这里仍按
|
|
1259
|
-
* `:ptc:` 前缀剥一层,避免将来出现嵌套时把根写错。
|
|
1260
|
-
*
|
|
1261
|
-
* @param callId - 本次调用的 id。
|
|
1262
|
-
* @returns 根 id。
|
|
1263
1486
|
*/
|
|
1264
1487
|
function rootCallIdOf(callId) {
|
|
1265
1488
|
const at = callId.indexOf(NESTED_CALL_ID_MARKER);
|
|
@@ -1269,24 +1492,42 @@ function rootCallIdOf(callId) {
|
|
|
1269
1492
|
function isRetainedPolicyChange(error) {
|
|
1270
1493
|
return (error instanceof Error ? error.message : String(error)).includes("release the retained environment before changing its sandbox policy");
|
|
1271
1494
|
}
|
|
1495
|
+
/** 校验单次沙箱提权参数配对(对齐 `@deepseek-ai/dsh-sandbox` 的 `validateEscalationArgs`)。 */
|
|
1496
|
+
function validateEscalationArgs(sandboxPermissions, justification) {
|
|
1497
|
+
if (sandboxPermissions !== void 0 && justification === void 0) throw new Error("invalid escalation: sandbox_permissions requires a justification");
|
|
1498
|
+
if (justification !== void 0 && sandboxPermissions === void 0) throw new Error("invalid escalation: justification is only valid together with sandbox_permissions");
|
|
1499
|
+
if (sandboxPermissions === void 0 && justification === void 0) return {};
|
|
1500
|
+
if (typeof sandboxPermissions !== "string" || !ESCALATION_TARGETS.includes(sandboxPermissions)) throw new Error("invalid sandbox_permissions: expected \"workspace-write\" or \"danger-full-access\"");
|
|
1501
|
+
if (typeof justification !== "string" || justification.trim().length === 0) throw new Error("invalid justification: expected a non-empty sentence");
|
|
1502
|
+
return {
|
|
1503
|
+
sandboxPermissions,
|
|
1504
|
+
justification
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1272
1507
|
/** 校验并读出工具参数。 */
|
|
1273
1508
|
function readArguments(args) {
|
|
1274
1509
|
if (typeof args !== "object" || args === null) throw new Error("codemode: expected an arguments object");
|
|
1275
1510
|
const record = args;
|
|
1276
1511
|
const code = record["code"];
|
|
1277
1512
|
const description = record["description"];
|
|
1278
|
-
if (typeof code !== "string" || code.trim().length === 0)
|
|
1513
|
+
if (typeof code !== "string" || code.trim().length === 0) {
|
|
1514
|
+
const received = Object.keys(record);
|
|
1515
|
+
throw new Error("codemode: invalid code: expected a non-empty string" + (received.length === 0 ? "" : " (received keys: " + received.join(", ") + "; pass program source in \"code\")"));
|
|
1516
|
+
}
|
|
1279
1517
|
if (typeof description !== "string" || description.trim().length === 0) throw new Error("codemode: invalid description: expected a non-empty string");
|
|
1518
|
+
const escalation = validateEscalationArgs(record["sandbox_permissions"], record["justification"]);
|
|
1280
1519
|
const timeoutMs = record["timeoutMs"];
|
|
1281
1520
|
if (timeoutMs === void 0) return {
|
|
1282
1521
|
code,
|
|
1283
|
-
description
|
|
1522
|
+
description,
|
|
1523
|
+
...escalation
|
|
1284
1524
|
};
|
|
1285
1525
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
|
|
1286
1526
|
return {
|
|
1287
1527
|
code,
|
|
1288
1528
|
description,
|
|
1289
|
-
timeoutMs
|
|
1529
|
+
timeoutMs,
|
|
1530
|
+
...escalation
|
|
1290
1531
|
};
|
|
1291
1532
|
}
|
|
1292
1533
|
/** 子调用参数必须是可无损 JSON 化的值。 */
|
|
@@ -1307,14 +1548,246 @@ function isConcurrencySafe(entry, args) {
|
|
|
1307
1548
|
return false;
|
|
1308
1549
|
}
|
|
1309
1550
|
}
|
|
1310
|
-
/**
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1551
|
+
/**
|
|
1552
|
+
* 子调用的并发、提交顺序与互斥屏障(对齐内核 `ptc.ts` 的 ordered commit lane)。
|
|
1553
|
+
*
|
|
1554
|
+
* - `start()`(记 start 事件 + 启动 body)在 lane 里按提交顺序执行:`parallel` body 之间
|
|
1555
|
+
* 可以并发重叠,最多 `maxParallel` 个同时在飞;
|
|
1556
|
+
* - `commit()`(记 settle 事件 + 转发图片/附加上下文 + `concludeTurn` + 放行互斥屏障)
|
|
1557
|
+
* 按**提交顺序**逐条执行:前一条没提交完,后一条的结果不会先落到会话里;
|
|
1558
|
+
* - `exclusive` 调用等所有在飞 body 结算后独占运行,并独占到自己 commit 完成才放行后续;
|
|
1559
|
+
* - 排队期间若外层运行已中止,直接放弃且不启动任务(未启动调用零 `start` 事件);
|
|
1560
|
+
* - `drain()` 等到 lane 彻底静止(所有 body 结算、所有 commit 完成)才返回。
|
|
1561
|
+
*
|
|
1562
|
+
* 程序拿到的值在 commit 里结算(与内核同):body 结束不保证顺序,提交才是顺序。
|
|
1563
|
+
*
|
|
1564
|
+
* @param maxParallel - 同时在飞的上限。
|
|
1565
|
+
* @param isAborted - 外层运行是否已中止。
|
|
1566
|
+
* @returns 调度入口与排空。
|
|
1567
|
+
*/
|
|
1568
|
+
function createCommitLane(maxParallel, isAborted) {
|
|
1569
|
+
const cap = Math.max(1, maxParallel);
|
|
1570
|
+
const queue = [];
|
|
1571
|
+
const commitQueue = [];
|
|
1572
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1573
|
+
let exclusiveActive = false;
|
|
1574
|
+
let driving = false;
|
|
1575
|
+
let driverRun = Promise.resolve();
|
|
1576
|
+
let wake;
|
|
1577
|
+
const wakeup = () => {
|
|
1578
|
+
const release = wake;
|
|
1579
|
+
wake = void 0;
|
|
1580
|
+
release?.();
|
|
1581
|
+
};
|
|
1582
|
+
/**
|
|
1583
|
+
* 唯一的有序车道:每一轮先提交队首已结算的调用,再在有空位时启动下一个排队调用,
|
|
1584
|
+
* 否则睡到「有 body 结算或有新提交」为止。三条队列全空即静止。
|
|
1585
|
+
*/
|
|
1586
|
+
const drive = () => {
|
|
1587
|
+
if (driving) return driverRun;
|
|
1588
|
+
driving = true;
|
|
1589
|
+
driverRun = (async () => {
|
|
1590
|
+
try {
|
|
1591
|
+
for (;;) {
|
|
1592
|
+
const signal = new Promise((resolve) => {
|
|
1593
|
+
wake = resolve;
|
|
1594
|
+
});
|
|
1595
|
+
const commitHead = commitQueue[0];
|
|
1596
|
+
if (commitHead !== void 0 && commitHead.settled) {
|
|
1597
|
+
commitQueue.shift();
|
|
1598
|
+
await commitHead.commit();
|
|
1599
|
+
if (commitHead.mode === "exclusive") exclusiveActive = false;
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
const head = queue[0];
|
|
1603
|
+
if (head !== void 0) {
|
|
1604
|
+
if (isAborted()) {
|
|
1605
|
+
queue.shift();
|
|
1606
|
+
head.abandon();
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (!exclusiveActive && (head.mode === "exclusive" ? inFlight.size === 0 : inFlight.size < cap)) {
|
|
1610
|
+
if (head.mode === "exclusive") exclusiveActive = true;
|
|
1611
|
+
queue.shift();
|
|
1612
|
+
commitQueue.push(head);
|
|
1613
|
+
head.start();
|
|
1614
|
+
const flight = head.flight.finally(() => {
|
|
1615
|
+
inFlight.delete(flight);
|
|
1616
|
+
wakeup();
|
|
1617
|
+
});
|
|
1618
|
+
inFlight.add(flight);
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (queue.length === 0 && commitQueue.length === 0 && inFlight.size === 0) return;
|
|
1623
|
+
await signal;
|
|
1624
|
+
}
|
|
1625
|
+
} finally {
|
|
1626
|
+
driving = false;
|
|
1627
|
+
wake = void 0;
|
|
1628
|
+
}
|
|
1629
|
+
})();
|
|
1630
|
+
return driverRun;
|
|
1631
|
+
};
|
|
1632
|
+
return {
|
|
1633
|
+
schedule(mode, task) {
|
|
1634
|
+
return new Promise((resolve, reject) => {
|
|
1635
|
+
let parked;
|
|
1636
|
+
const entry = {
|
|
1637
|
+
mode,
|
|
1638
|
+
settled: false,
|
|
1639
|
+
flight: Promise.resolve(),
|
|
1640
|
+
start() {
|
|
1641
|
+
task.start();
|
|
1642
|
+
entry.flight = (async () => {
|
|
1643
|
+
try {
|
|
1644
|
+
parked = {
|
|
1645
|
+
ok: true,
|
|
1646
|
+
value: await task.body()
|
|
1647
|
+
};
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
parked = {
|
|
1650
|
+
ok: false,
|
|
1651
|
+
error
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
entry.settled = true;
|
|
1655
|
+
wakeup();
|
|
1656
|
+
})();
|
|
1657
|
+
},
|
|
1658
|
+
async commit() {
|
|
1659
|
+
const result = parked;
|
|
1660
|
+
if (result === void 0) return;
|
|
1661
|
+
try {
|
|
1662
|
+
resolve(await task.commit(result));
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
reject(error);
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1667
|
+
abandon() {
|
|
1668
|
+
try {
|
|
1669
|
+
task.abandon();
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
reject(error);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
queue.push(entry);
|
|
1676
|
+
wakeup();
|
|
1677
|
+
drive();
|
|
1678
|
+
});
|
|
1679
|
+
},
|
|
1680
|
+
async drain() {
|
|
1681
|
+
await drive();
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* 回放内容的投影:先按日志上限裁剪,再交给入口的 `tools/ptc-dispatch-log` 瀑布。
|
|
1687
|
+
*
|
|
1688
|
+
* 瀑布失败**不**影响这条记录:回落裁剪后的原始内容(内核 `ptc.ts` 同此)。
|
|
1689
|
+
*/
|
|
1690
|
+
async function shapeDispatchContent(host, dispatch) {
|
|
1691
|
+
if (typeof host.shapeDispatchLog !== "function") return dispatch.content;
|
|
1692
|
+
try {
|
|
1693
|
+
const shaped = await host.shapeDispatchLog(dispatch);
|
|
1694
|
+
return Array.isArray(shaped) ? shaped : dispatch.content;
|
|
1695
|
+
} catch {
|
|
1696
|
+
return dispatch.content;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
/** 一个抛出值的消息。 */
|
|
1700
|
+
function messageOf$1(error) {
|
|
1701
|
+
return error instanceof Error ? error.message : String(error);
|
|
1702
|
+
}
|
|
1703
|
+
const JSON_INDENT = " ";
|
|
1704
|
+
const MAX_JSON_INDENT_CHARS = 10;
|
|
1705
|
+
/** 迭代式 JSON 渲染:与内核 `ptc.ts` 一致,深于 5 层后自动紧凑化,防止深层缩进膨胀。 */
|
|
1706
|
+
function renderProgramValue(value) {
|
|
1707
|
+
if (typeof value === "string") return value;
|
|
1708
|
+
let normalized;
|
|
1709
|
+
try {
|
|
1710
|
+
normalized = JSON.parse(JSON.stringify(value));
|
|
1711
|
+
} catch {
|
|
1712
|
+
return String(value);
|
|
1713
|
+
}
|
|
1714
|
+
if (typeof normalized === "string") return normalized;
|
|
1715
|
+
if (normalized === void 0) return "";
|
|
1716
|
+
const chunks = [];
|
|
1717
|
+
const tasks = [{
|
|
1718
|
+
kind: "value",
|
|
1719
|
+
value: normalized,
|
|
1720
|
+
depth: 0,
|
|
1721
|
+
compact: false
|
|
1722
|
+
}];
|
|
1723
|
+
for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
|
|
1724
|
+
if (task.kind === "text") {
|
|
1725
|
+
chunks.push(task.text);
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const current = task.value;
|
|
1729
|
+
if (current === null || typeof current === "boolean" || typeof current === "number") {
|
|
1730
|
+
chunks.push(String(current));
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (typeof current === "string") {
|
|
1734
|
+
chunks.push(JSON.stringify(current));
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
const compact = task.compact || (task.depth + 1) * 2 > MAX_JSON_INDENT_CHARS;
|
|
1738
|
+
const childDepth = task.depth + 1;
|
|
1739
|
+
if (Array.isArray(current)) {
|
|
1740
|
+
chunks.push("[");
|
|
1741
|
+
if (current.length === 0) {
|
|
1742
|
+
chunks.push("]");
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
tasks.push({
|
|
1746
|
+
kind: "text",
|
|
1747
|
+
text: compact ? "]" : `\n${JSON_INDENT.repeat(task.depth)}]`
|
|
1748
|
+
});
|
|
1749
|
+
for (let index = current.length - 1; index >= 0; index -= 1) {
|
|
1750
|
+
tasks.push({
|
|
1751
|
+
kind: "value",
|
|
1752
|
+
value: current[index],
|
|
1753
|
+
depth: childDepth,
|
|
1754
|
+
compact
|
|
1755
|
+
});
|
|
1756
|
+
tasks.push({
|
|
1757
|
+
kind: "text",
|
|
1758
|
+
text: compact ? index === 0 ? "" : "," : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}`
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
if (typeof current === "object") {
|
|
1764
|
+
const record = current;
|
|
1765
|
+
const keys = Object.keys(record);
|
|
1766
|
+
chunks.push("{");
|
|
1767
|
+
if (keys.length === 0) {
|
|
1768
|
+
chunks.push("}");
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
tasks.push({
|
|
1772
|
+
kind: "text",
|
|
1773
|
+
text: compact ? "}" : `\n${JSON_INDENT.repeat(task.depth)}}`
|
|
1774
|
+
});
|
|
1775
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
1776
|
+
const key = keys[index];
|
|
1777
|
+
tasks.push({
|
|
1778
|
+
kind: "value",
|
|
1779
|
+
value: record[key],
|
|
1780
|
+
depth: childDepth,
|
|
1781
|
+
compact
|
|
1782
|
+
});
|
|
1783
|
+
tasks.push({
|
|
1784
|
+
kind: "text",
|
|
1785
|
+
text: compact ? `${index === 0 ? "" : ","}${JSON.stringify(key)}:` : `${index === 0 ? "\n" : ",\n"}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return chunks.join("");
|
|
1318
1791
|
}
|
|
1319
1792
|
//#endregion
|
|
1320
1793
|
//#region src/entry-codemode.ts
|
|
@@ -1348,6 +1821,11 @@ const name = "codemode";
|
|
|
1348
1821
|
const inject = ["tools", "systemPrompt"];
|
|
1349
1822
|
/** 决策环长度:只留给 `code_debug`。 */
|
|
1350
1823
|
const DEBUG_RING = 50;
|
|
1824
|
+
/** 严格加宽阶梯(对齐 `@deepseek-ai/dsh-sandbox` 的 `WIDER_MODES`)。 */
|
|
1825
|
+
const WIDER_SANDBOX_MODES = {
|
|
1826
|
+
"read-only": ["workspace-write", "danger-full-access"],
|
|
1827
|
+
"workspace-write": ["danger-full-access"]
|
|
1828
|
+
};
|
|
1351
1829
|
/**
|
|
1352
1830
|
* 装配本行。
|
|
1353
1831
|
*
|
|
@@ -1403,18 +1881,128 @@ function apply(ctx, config = {}) {
|
|
|
1403
1881
|
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1404
1882
|
retention.dispose();
|
|
1405
1883
|
}, "codemode retained environments");
|
|
1884
|
+
/** 在飞运行:会话键 → 该会话里正在跑的程序。 */
|
|
1885
|
+
const activeRuns = /* @__PURE__ */ new Map();
|
|
1886
|
+
/**
|
|
1887
|
+
* 登记一只在飞运行,返回运行结算时的 disposer。
|
|
1888
|
+
*
|
|
1889
|
+
* 传输层每轮运行(含常驻环境换策略后的重跑)都登记一次,入口因此知道「这一轮跑在哪个
|
|
1890
|
+
* 会话、当时的沙箱模式是什么」。会话切模式时,模式不匹配的运行必须停:常驻环境换策略
|
|
1891
|
+
* 会抛错,而已经跑起来的那一轮不能继续用旧权限往下写。
|
|
1892
|
+
*/
|
|
1893
|
+
const trackRun = (run) => {
|
|
1894
|
+
const key = sessionKeyOf(run.agent);
|
|
1895
|
+
if (key === void 0) return () => void 0;
|
|
1896
|
+
const record = {
|
|
1897
|
+
controller: run.controller,
|
|
1898
|
+
...run.mode === void 0 ? {} : { mode: run.mode }
|
|
1899
|
+
};
|
|
1900
|
+
let bucket = activeRuns.get(key);
|
|
1901
|
+
if (bucket === void 0) {
|
|
1902
|
+
bucket = /* @__PURE__ */ new Set();
|
|
1903
|
+
activeRuns.set(key, bucket);
|
|
1904
|
+
}
|
|
1905
|
+
bucket.add(record);
|
|
1906
|
+
return () => {
|
|
1907
|
+
const current = activeRuns.get(key);
|
|
1908
|
+
if (current === void 0) return;
|
|
1909
|
+
current.delete(record);
|
|
1910
|
+
if (current.size === 0) activeRuns.delete(key);
|
|
1911
|
+
};
|
|
1912
|
+
};
|
|
1913
|
+
/**
|
|
1914
|
+
* 会话的 `sandbox/mode` 事件:生效模式已经变了的在飞运行直接取消(内核 `PtcCalls` 同此)。
|
|
1915
|
+
*
|
|
1916
|
+
* 只取消「能确认模式变了」的那些:拿不到模式(运行时不管沙箱)或模式相同的运行一律不动。
|
|
1917
|
+
*/
|
|
1918
|
+
const cancelOnModeChange = (session, event) => {
|
|
1919
|
+
if (typeof event !== "object" || event === null) return;
|
|
1920
|
+
const record = event;
|
|
1921
|
+
if (record.type !== "sandbox/mode") return;
|
|
1922
|
+
const mode = (typeof record.data === "object" && record.data !== null ? record.data : void 0)?.mode;
|
|
1923
|
+
if (typeof mode !== "string") return;
|
|
1924
|
+
const key = sessionKeyOf(session);
|
|
1925
|
+
if (key === void 0) return;
|
|
1926
|
+
for (const run of activeRuns.get(key) ?? []) if (run.mode !== void 0 && run.mode !== mode) run.controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
|
|
1927
|
+
};
|
|
1928
|
+
if (typeof context.on === "function") try {
|
|
1929
|
+
const off = context.on("session/event", cancelOnModeChange, { global: true });
|
|
1930
|
+
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1931
|
+
if (typeof off === "function") off();
|
|
1932
|
+
}, "codemode sandbox mode cancellation");
|
|
1933
|
+
} catch (error) {
|
|
1934
|
+
log("warn", "[codemode] could not subscribe to session/event; an in-flight code_run keeps running after the session sandbox mode changes: " + messageOf(error));
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* 把一条子调用回放记录交给 `tools/ptc-dispatch-log` 瀑布(内核 `ptc.ts` 的同名钩子)。
|
|
1938
|
+
*
|
|
1939
|
+
* 监听器的失败必须在这里吞掉:它决定的只是日志副本长什么样,不是程序拿到的结果。内核
|
|
1940
|
+
* 用 `scopeTarget(tools, agent)` 当接收方;本包不依赖 `@deepseek-ai/dsh-scope`,直接以
|
|
1941
|
+
* `tools` 为接收方——作用域过滤由宿主自己的监听器完成。
|
|
1942
|
+
*/
|
|
1943
|
+
const shapeDispatchLog = async (dispatch) => {
|
|
1944
|
+
const waterfall = context.waterfall;
|
|
1945
|
+
if (typeof waterfall !== "function") return dispatch.content;
|
|
1946
|
+
try {
|
|
1947
|
+
const shaped = await waterfall.call(context, tools, "tools/ptc-dispatch-log", dispatch, () => Promise.resolve(dispatch.content));
|
|
1948
|
+
return Array.isArray(shaped) ? shaped : dispatch.content;
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
log("warn", "tools: ptc-dispatch-log listener failed for " + dispatch.name + ": " + messageOf(error) + "; logging the original settled content");
|
|
1951
|
+
return dispatch.content;
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1406
1954
|
const scopeKey = findScopeKey(ctx);
|
|
1407
1955
|
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
1956
|
/** 最近一次 describe 收到的完整定义集(含被遮蔽的)。 */
|
|
1409
1957
|
let catalog = [];
|
|
1410
1958
|
const debugRing = [];
|
|
1411
1959
|
let firstDescribeLogged = false;
|
|
1960
|
+
const escalateSandbox = async (input) => {
|
|
1961
|
+
const record = typeof input.standingPolicy === "object" && input.standingPolicy !== null ? input.standingPolicy : void 0;
|
|
1962
|
+
const effectiveMode = typeof record?.["mode"] === "string" ? record["mode"] : void 0;
|
|
1963
|
+
if (record === void 0 || effectiveMode === void 0) throw new Error("sandbox_permissions is not available for this PTC runtime");
|
|
1964
|
+
if (input.requestedMode === effectiveMode) return input.standingPolicy;
|
|
1965
|
+
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`);
|
|
1966
|
+
const approver = serviceOf("approval");
|
|
1967
|
+
if (typeof approver?.request !== "function") throw new Error(`sandbox escalation to "${input.requestedMode}" requires approval, but no approval service is composed`);
|
|
1968
|
+
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`);
|
|
1969
|
+
const outcome = await approver.request({
|
|
1970
|
+
agent: input.agent,
|
|
1971
|
+
toolName: input.toolName,
|
|
1972
|
+
callId: input.callId,
|
|
1973
|
+
reason: `escalate sandbox to ${input.requestedMode}: ${input.justification}`,
|
|
1974
|
+
displayReason: {
|
|
1975
|
+
en: `Allow this program to run once with ${input.requestedMode} access: ${input.justification}`,
|
|
1976
|
+
zh: `允许本次程序以 ${input.requestedMode} 权限单次运行:${input.justification}`
|
|
1977
|
+
},
|
|
1978
|
+
signal: input.signal
|
|
1979
|
+
});
|
|
1980
|
+
switch (outcome) {
|
|
1981
|
+
case "allowed-once": return {
|
|
1982
|
+
...record,
|
|
1983
|
+
mode: input.requestedMode
|
|
1984
|
+
};
|
|
1985
|
+
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`);
|
|
1986
|
+
case "cancelled": throw new Error(`approval for escalating to "${input.requestedMode}" was cancelled`);
|
|
1987
|
+
case "unavailable": throw new Error(`sandbox escalation to "${input.requestedMode}" requires approval, but no approval channel is available`);
|
|
1988
|
+
default: throw new Error(`unexpected approval outcome: ${String(outcome)}`);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1412
1991
|
const host = {
|
|
1413
1992
|
catalog: () => catalog,
|
|
1414
1993
|
runtime: runtimeOf,
|
|
1415
1994
|
stateOwner: (agent) => retention.owner(agent),
|
|
1416
1995
|
releaseOwner: (owner) => retention.release(owner),
|
|
1417
1996
|
runAuthority,
|
|
1997
|
+
escalateSandbox,
|
|
1998
|
+
createImageContext(content) {
|
|
1999
|
+
return createUserMessage({
|
|
2000
|
+
content,
|
|
2001
|
+
source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
|
|
2002
|
+
});
|
|
2003
|
+
},
|
|
2004
|
+
trackRun,
|
|
2005
|
+
shapeDispatchLog,
|
|
1418
2006
|
async callTool(request) {
|
|
1419
2007
|
const call = {
|
|
1420
2008
|
type: "tool-call",
|
|
@@ -1436,16 +2024,20 @@ function apply(ctx, config = {}) {
|
|
|
1436
2024
|
const result = batch.results?.[0]?.result;
|
|
1437
2025
|
if (result === void 0) return { error: "sub-call produced no result" };
|
|
1438
2026
|
const content = result.content ?? [];
|
|
2027
|
+
const additionalContexts = Array.isArray(batch.additionalContexts) && batch.additionalContexts.length > 0 ? batch.additionalContexts : void 0;
|
|
1439
2028
|
if (result.isError === true) return {
|
|
1440
2029
|
error: errorTextOf(result),
|
|
1441
2030
|
content,
|
|
1442
|
-
...errorInfoOf(result)
|
|
2031
|
+
...errorInfoOf(result),
|
|
2032
|
+
...additionalContexts === void 0 ? {} : { additionalContexts }
|
|
1443
2033
|
};
|
|
1444
2034
|
const meta = result.meta;
|
|
1445
2035
|
return {
|
|
1446
2036
|
...result.value !== void 0 ? { value: result.value } : { text: textOf(result.content) },
|
|
1447
2037
|
content,
|
|
1448
|
-
...meta === void 0 ? {} : { meta }
|
|
2038
|
+
...meta === void 0 ? {} : { meta },
|
|
2039
|
+
...additionalContexts === void 0 ? {} : { additionalContexts },
|
|
2040
|
+
...batch.concluded === true ? { concluded: true } : {}
|
|
1449
2041
|
};
|
|
1450
2042
|
}
|
|
1451
2043
|
};
|
|
@@ -1461,11 +2053,15 @@ function apply(ctx, config = {}) {
|
|
|
1461
2053
|
delegate,
|
|
1462
2054
|
view,
|
|
1463
2055
|
viewForName: (toolName) => resolveNameView(tools, scopeKey, toolName),
|
|
1464
|
-
sectionText: () =>
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
2056
|
+
sectionText: () => {
|
|
2057
|
+
const runtime = runtimeOf();
|
|
2058
|
+
return buildCodemodeSectionText({
|
|
2059
|
+
runToolName: rules.options.runToolName,
|
|
2060
|
+
metaTools: rules.options.metaTools,
|
|
2061
|
+
language: runtime?.language,
|
|
2062
|
+
programFormat: runtime?.programFormat
|
|
2063
|
+
});
|
|
2064
|
+
},
|
|
1469
2065
|
onCatalog: (definitions) => {
|
|
1470
2066
|
catalog = definitions.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
1471
2067
|
},
|
|
@@ -1497,10 +2093,13 @@ function toCatalogEntry(definition) {
|
|
|
1497
2093
|
if (typeof toolName !== "string" || toolName.length === 0) return void 0;
|
|
1498
2094
|
const description = typeof record["description"] === "string" ? record["description"] : "";
|
|
1499
2095
|
const safe = record["isConcurrencySafe"];
|
|
2096
|
+
const declaredOutput = record["output"];
|
|
2097
|
+
const output = typeof declaredOutput === "object" && declaredOutput !== null ? declaredOutput.schema : void 0;
|
|
1500
2098
|
return {
|
|
1501
2099
|
name: toolName,
|
|
1502
2100
|
description,
|
|
1503
2101
|
parameters: record["parameters"],
|
|
2102
|
+
...output === void 0 ? {} : { output },
|
|
1504
2103
|
...typeof safe === "function" ? { isConcurrencySafe: safe } : {}
|
|
1505
2104
|
};
|
|
1506
2105
|
}
|
|
@@ -1546,6 +2145,24 @@ function errorInfoOf(result) {
|
|
|
1546
2145
|
function nonEmptyString(value) {
|
|
1547
2146
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1548
2147
|
}
|
|
2148
|
+
/**
|
|
2149
|
+
* 一个会话的身份键。
|
|
2150
|
+
*
|
|
2151
|
+
* 优先用会话 id(与内核 `session/event` 监听器里的 `session.id` 对齐);取不到 id 时退回
|
|
2152
|
+
* 会话对象本身——调用方 agent 上的 `session` 与事件里的会话是同一个对象,靠身份比对同样
|
|
2153
|
+
* 成立。连会话都没有时返回 `undefined`,这类运行不参与取消。
|
|
2154
|
+
*
|
|
2155
|
+
* @param value - 一个 agent,或一个会话对象。
|
|
2156
|
+
* @returns 会话 id / 会话对象,或 `undefined`。
|
|
2157
|
+
*/
|
|
2158
|
+
function sessionKeyOf(value) {
|
|
2159
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2160
|
+
const nested = value.session;
|
|
2161
|
+
const session = typeof nested === "object" && nested !== null ? nested : value;
|
|
2162
|
+
const id = session.id;
|
|
2163
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
2164
|
+
return session;
|
|
2165
|
+
}
|
|
1549
2166
|
/** 把内容块里的文本拼起来。 */
|
|
1550
2167
|
function textOf(content) {
|
|
1551
2168
|
if (!Array.isArray(content)) return "";
|