@eddyskywalker/dsh-chatgpt-subscription 0.61.0 → 0.63.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 +31 -0
- package/README.md +1 -1
- package/lib/client.js.map +1 -1
- package/lib/codemode.js +842 -305
- package/lib/index.js +733 -344
- package/lib/types/compat.d.ts +1 -1
- package/lib/types/entry-codemode.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/retrieval-intent.d.ts +2 -2
- package/lib/types/host/cache-warmup/retrieval-intent.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/usage.d.ts.map +1 -1
- package/lib/types/host/codemode/executor.d.ts +4 -0
- package/lib/types/host/codemode/executor.d.ts.map +1 -1
- package/lib/types/host/codemode/retention.d.ts +28 -11
- package/lib/types/host/codemode/retention.d.ts.map +1 -1
- package/lib/types/host/codemode/rules.d.ts +4 -0
- package/lib/types/host/codemode/rules.d.ts.map +1 -1
- package/lib/types/host/codemode/transport.d.ts +45 -3
- package/lib/types/host/codemode/transport.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts +53 -0
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/mapper.d.ts +33 -1
- package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
- package/lib/types/host/opendesign/types.d.ts +7 -2
- package/lib/types/host/opendesign/types.d.ts.map +1 -1
- package/lib/types/host/proxy-manager.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/codemode.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as PLUGIN_MESSAGE_SOURCE_KIND } from "./llm-compat-u9mor0uu.js";
|
|
2
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { NativeToolExecutor } from "@deepseek-ai/dsh-tool-executor-native";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
//#region src/host/codemode/discovery.ts
|
|
@@ -587,6 +587,10 @@ function summarizeHidden(names) {
|
|
|
587
587
|
* 工具的数量、namespace 分布、会话 id 或时间,因此同一部署下逐字节恒定:挂载或卸载
|
|
588
588
|
* MCP 服务端不会改写它,前缀缓存不会因为它失效。
|
|
589
589
|
*
|
|
590
|
+
* 结果形状索引(`read` / `glob` / `grep` … 的顶层字段)也**必须是静态的**:它照抄自各工具
|
|
591
|
+
* 声明过的 output schema,不读活 catalog。按 catalog 生成会让「挂一个 MCP 服务端」改写系统
|
|
592
|
+
* 提示词首段,正好毁掉这一层要保住的前缀缓存。
|
|
593
|
+
*
|
|
590
594
|
* @param input - 传输工具名、发现工具名与程序形态。
|
|
591
595
|
* @returns 说明段正文。
|
|
592
596
|
*/
|
|
@@ -624,9 +628,18 @@ function buildCodemodeSectionText(input) {
|
|
|
624
628
|
"",
|
|
625
629
|
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.",
|
|
626
630
|
"- 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
|
|
631
|
+
"- File, search, and shell tool results are named-field objects, never bare strings — read the field, not the envelope (`job_list` and `list_agents` are the only bare arrays):",
|
|
632
|
+
" `read` → `{ path, offset, lines: [{ number, text }], totalLines }` · `glob` → `{ root, paths }` · `grep` → `{ matches: [{ path, lineNumber, line }] }` · `edit` → `{ path, before, after }` · `write` → `{ path, operation, before, after }` · `win_bash` → `{ stdout, stderr, exitCode }` · `web_fetch` → `{ url, statusCode, body, truncated }` · `job_output` → `{ text, job }` · `read_image` → `{ path, image }` · `skill` → `{ name, content }`.",
|
|
633
|
+
python ? "- Read the field: `page[\"lines\"]`, `match[\"text\"]`. Calling `.split()`, `.splitlines()` or `in` on the envelope itself is always wrong." : "- Read the field: `r.lines.map((l) => l.text)`. Calling `.map`, `.filter`, `.split` or `in` on the envelope itself is always wrong.",
|
|
634
|
+
"- Anything not listed above (MCP tools, hidden plugin tools) answers with its **declared output schema**: call `" + describeTool + "({ name })` and read the top-level keys before picking a field. One discovery call is far cheaper than the `TypeError` that loses the whole program.",
|
|
628
635
|
"- 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
636
|
"- 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`.",
|
|
637
|
+
"- Parameter keys: `read`, `edit`, `write`, and `read_image` take `file_path` (not `path`); `glob` and `grep` take `path`; `win_bash` takes `workdir` (not `cwd`).",
|
|
638
|
+
...python ? [] : [
|
|
639
|
+
"- Paths and regexes: write a Windows path with forward slashes (`C:/Users/...`) or wrap it in `String.raw`; a literal `C:\\Users` inside an ordinary string is read as the escape `\\U` and the program never starts (`Expected unicode escape`). Regex backslashes (`\\d`, `\\.`) have the same trap — double them or use `String.raw`.",
|
|
640
|
+
"- A program is not a module: `import('./src/x.ts')` and `import('vscode')` cannot resolve — the program runs in a fresh process with no module graph. Read files with `tools.read`; `await import('node:fs')` works only for Node built-ins.",
|
|
641
|
+
"- `process.env` is empty inside the program: `os.tmpdir()` and credentialed CLIs (`gh`, `git`, `npm`) spawned via `node:child_process` miss `TEMP`, `HOME`, `APPDATA`, and auth tokens. Run shell and CLI commands through `await tools.win_bash({ command })` instead."
|
|
642
|
+
],
|
|
630
643
|
"- Prefer batching multi-step read/search chains inside one " + run + " program instead of issuing many sequential single-tool turns.",
|
|
631
644
|
"- Independent read-only calls may overlap under Promise.all; mutating calls run alone, in submission order.",
|
|
632
645
|
"- 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.",
|
|
@@ -799,7 +812,10 @@ function createCodemodeExecutor(ctx, rules, hooks) {
|
|
|
799
812
|
additionalContexts: [],
|
|
800
813
|
concluded: false
|
|
801
814
|
};
|
|
802
|
-
}
|
|
815
|
+
},
|
|
816
|
+
...typeof hooks.reset === "function" ? { async reset(agent) {
|
|
817
|
+
await hooks.reset(agent);
|
|
818
|
+
} } : {}
|
|
803
819
|
};
|
|
804
820
|
}
|
|
805
821
|
/**
|
|
@@ -847,169 +863,6 @@ function sectionOrderOf(ctx) {
|
|
|
847
863
|
}
|
|
848
864
|
}
|
|
849
865
|
//#endregion
|
|
850
|
-
//#region src/host/codemode/retention.ts
|
|
851
|
-
/**
|
|
852
|
-
* 造一个常驻环境管理器。
|
|
853
|
-
*
|
|
854
|
-
* @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
|
|
855
|
-
* @returns 管理句柄。
|
|
856
|
-
*/
|
|
857
|
-
function createRetention(runtimeOf) {
|
|
858
|
-
const live = /* @__PURE__ */ new Set();
|
|
859
|
-
const released = /* @__PURE__ */ new WeakSet();
|
|
860
|
-
const release = async (owner) => {
|
|
861
|
-
if (released.has(owner)) return;
|
|
862
|
-
released.add(owner);
|
|
863
|
-
live.delete(owner);
|
|
864
|
-
const runtime = runtimeOf();
|
|
865
|
-
if (runtime?.release === void 0) return;
|
|
866
|
-
try {
|
|
867
|
-
await runtime.release(owner);
|
|
868
|
-
} catch {}
|
|
869
|
-
};
|
|
870
|
-
return {
|
|
871
|
-
owner(agent) {
|
|
872
|
-
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
873
|
-
released.delete(owner);
|
|
874
|
-
live.add(owner);
|
|
875
|
-
return owner;
|
|
876
|
-
},
|
|
877
|
-
release,
|
|
878
|
-
async dispose() {
|
|
879
|
-
const owners = [...live];
|
|
880
|
-
await Promise.all(owners.map((owner) => release(owner)));
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
/** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
|
|
885
|
-
const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
|
|
886
|
-
//#endregion
|
|
887
|
-
//#region src/host/codemode/scope.ts
|
|
888
|
-
/**
|
|
889
|
-
* 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
|
|
890
|
-
*
|
|
891
|
-
* ## 为什么要这么绕
|
|
892
|
-
*
|
|
893
|
-
* 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
|
|
894
|
-
* 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
|
|
895
|
-
* 都不导出。能用的只有两件事:
|
|
896
|
-
*
|
|
897
|
-
* 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
|
|
898
|
-
* 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
|
|
899
|
-
*
|
|
900
|
-
* 于是归属判定变成两次公开查询:
|
|
901
|
-
*
|
|
902
|
-
* - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
|
|
903
|
-
* - 只有本预设视图能解析 → 本预设自己注册的;
|
|
904
|
-
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
|
|
905
|
-
*
|
|
906
|
-
* 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
|
|
907
|
-
* 纯名字匹配,绝不因为读不到归属就误遮蔽。
|
|
908
|
-
*
|
|
909
|
-
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
|
|
910
|
-
*/
|
|
911
|
-
/** 内核写作用域标签用的符号描述。 */
|
|
912
|
-
const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
|
|
913
|
-
/**
|
|
914
|
-
* 从一个 ctx 上找出「本包所在作用域」的 key。
|
|
915
|
-
*
|
|
916
|
-
* Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
|
|
917
|
-
* 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
|
|
918
|
-
* 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
|
|
919
|
-
*
|
|
920
|
-
* @param ctx - 本包的行上下文。
|
|
921
|
-
* @returns 作用域 key;读不到时为 `undefined`。
|
|
922
|
-
*/
|
|
923
|
-
function findScopeKey(ctx) {
|
|
924
|
-
if (typeof ctx !== "object" || ctx === null) return void 0;
|
|
925
|
-
return scanForScope(ctx, 0);
|
|
926
|
-
}
|
|
927
|
-
/** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
|
|
928
|
-
function scanForScope(source, depth) {
|
|
929
|
-
if (depth > 4) return void 0;
|
|
930
|
-
let symbols;
|
|
931
|
-
try {
|
|
932
|
-
symbols = Object.getOwnPropertySymbols(source);
|
|
933
|
-
} catch {
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
for (const symbol of symbols) {
|
|
937
|
-
if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
|
|
938
|
-
let value;
|
|
939
|
-
try {
|
|
940
|
-
value = source[symbol];
|
|
941
|
-
} catch {
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
if (typeof value === "object" && value !== null) return value;
|
|
945
|
-
}
|
|
946
|
-
const proto = Object.getPrototypeOf(source);
|
|
947
|
-
if (proto === null || proto === Object.prototype) return void 0;
|
|
948
|
-
return scanForScope(proto, depth + 1);
|
|
949
|
-
}
|
|
950
|
-
/**
|
|
951
|
-
* 为一个名字清单建立归属视图。
|
|
952
|
-
*
|
|
953
|
-
* @param tools - 工具注册表(结构探测,不看类身份)。
|
|
954
|
-
* @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
|
|
955
|
-
* @param names - 要判定的名字(通常是本步的模型可见清单)。
|
|
956
|
-
* @returns 归属视图;读不到任何归属时返回禁用视图。
|
|
957
|
-
*/
|
|
958
|
-
function resolveCatalogView(tools, scopeKey, names) {
|
|
959
|
-
const registry = asRegistry(tools);
|
|
960
|
-
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
961
|
-
const globalNames = /* @__PURE__ */ new Set();
|
|
962
|
-
const presetNames = /* @__PURE__ */ new Set();
|
|
963
|
-
for (const name of names) {
|
|
964
|
-
if (typeof name !== "string" || name.length === 0) continue;
|
|
965
|
-
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
966
|
-
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
967
|
-
}
|
|
968
|
-
return {
|
|
969
|
-
enabled: true,
|
|
970
|
-
globalNames,
|
|
971
|
-
presetNames
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* 判定一个名字在**调用时**是否属于本预设之外的层。
|
|
976
|
-
*
|
|
977
|
-
* 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
|
|
978
|
-
* 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
|
|
979
|
-
*
|
|
980
|
-
* @param tools - 工具注册表。
|
|
981
|
-
* @param scopeKey - 本预设的作用域 key。
|
|
982
|
-
* @param name - 工具名。
|
|
983
|
-
* @returns 归属视图(只含这一个名字)。
|
|
984
|
-
*/
|
|
985
|
-
function resolveNameView(tools, scopeKey, name) {
|
|
986
|
-
const registry = asRegistry(tools);
|
|
987
|
-
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
988
|
-
const globalNames = /* @__PURE__ */ new Set();
|
|
989
|
-
const presetNames = /* @__PURE__ */ new Set();
|
|
990
|
-
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
991
|
-
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
992
|
-
return {
|
|
993
|
-
enabled: true,
|
|
994
|
-
globalNames,
|
|
995
|
-
presetNames
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
/** 一次公开查询:解析得到定义时为 `true`。 */
|
|
999
|
-
function probes(registry, name, scope) {
|
|
1000
|
-
try {
|
|
1001
|
-
return registry.get(name, scope) !== void 0;
|
|
1002
|
-
} catch {
|
|
1003
|
-
return false;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
/** 结构探测:读得到 `get` 就当注册表用。 */
|
|
1007
|
-
function asRegistry(tools) {
|
|
1008
|
-
if (typeof tools !== "object" || tools === null) return void 0;
|
|
1009
|
-
if (typeof tools.get !== "function") return void 0;
|
|
1010
|
-
return tools;
|
|
1011
|
-
}
|
|
1012
|
-
//#endregion
|
|
1013
866
|
//#region src/host/codemode/transport.ts
|
|
1014
867
|
/**
|
|
1015
868
|
* Code Mode 的程序传输工具 `code_run`。
|
|
@@ -1051,8 +904,41 @@ const catalogScope = new AsyncLocalStorage();
|
|
|
1051
904
|
function currentCatalogSnapshot() {
|
|
1052
905
|
return catalogScope.getStore();
|
|
1053
906
|
}
|
|
907
|
+
/** 传输工具名(与 {@link DEFAULT_RUN_TOOL_NAME} 一致,导出方便测试引用)。 */
|
|
908
|
+
const TRANSPORT_TOOL_NAME = "code_run";
|
|
909
|
+
/** 进展汇报工具名:融合参数里救回来的简报用它补发。 */
|
|
910
|
+
const PROGRESS_UPDATE_TOOL = "progress_update";
|
|
1054
911
|
/** 允许单次提权的目标沙箱模式(对齐 `@deepseek-ai/dsh-sandbox` 的 `ESCALATION_TARGETS`)。 */
|
|
1055
912
|
const ESCALATION_TARGETS = Object.freeze(["workspace-write", "danger-full-access"]);
|
|
913
|
+
/**
|
|
914
|
+
* 子调用配额耗尽时的宿主中止原因(对齐 `@deepseek-ai/dsh-tool-executor-ptc` 的 `ToolCallLimitExceededError`)。
|
|
915
|
+
*
|
|
916
|
+
* 超出的那一次调用不会派发;结算阶段识别此错误并保留完整诊断,防止常驻模式下的泛化取消文案吞掉真实原因。
|
|
917
|
+
*/
|
|
918
|
+
var ToolCallLimitExceededError = class extends Error {
|
|
919
|
+
constructor(limit, callNumber, callsPerMinute, runToolName = TRANSPORT_TOOL_NAME) {
|
|
920
|
+
super(`${runToolName} exceeded its limit of ${String(limit)} tool calls (${String(callsPerMinute)} per minute of its timeout); call ${String(callNumber)} was not dispatched. The ${runToolName} program may contain an infinite loop. Check loop termination or split the work into smaller ${runToolName} calls.`);
|
|
921
|
+
this.name = "ToolCallLimitExceededError";
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
/**
|
|
925
|
+
* 程序运行自身失败时抛出的结构化错误(对齐内核 `CodeRunFailedError`,`code: 'CODE_RUN_FAILED'`)。
|
|
926
|
+
*/
|
|
927
|
+
var CodeRunFailedError = class extends HarnessError {
|
|
928
|
+
constructor(message) {
|
|
929
|
+
super(message, "CODE_RUN_FAILED");
|
|
930
|
+
this.name = "CodeRunFailedError";
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
/**
|
|
934
|
+
* 常驻环境重置不支持或失败时抛出的结构化错误(对齐内核 `CodeResetFailedError`,`code: 'CODE_RESET_FAILED'`)。
|
|
935
|
+
*/
|
|
936
|
+
var CodeResetFailedError = class extends HarnessError {
|
|
937
|
+
constructor(message) {
|
|
938
|
+
super(message, "CODE_RESET_FAILED");
|
|
939
|
+
this.name = "CodeResetFailedError";
|
|
940
|
+
}
|
|
941
|
+
};
|
|
1056
942
|
/** 与内核 `run_code` 一致的 `description` 参数说明。 */
|
|
1057
943
|
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
944
|
function buildFlavor(languageName, execution) {
|
|
@@ -1177,24 +1063,25 @@ function createTransportDefinition(host, rules) {
|
|
|
1177
1063
|
}
|
|
1178
1064
|
},
|
|
1179
1065
|
presentCall(args) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1066
|
+
try {
|
|
1067
|
+
const input = readArguments(args);
|
|
1068
|
+
return {
|
|
1069
|
+
card: "generic",
|
|
1070
|
+
title: input.description,
|
|
1071
|
+
kind: "execute",
|
|
1072
|
+
rawInput: input.code
|
|
1073
|
+
};
|
|
1074
|
+
} catch {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1191
1077
|
},
|
|
1192
1078
|
async execute(args, exec) {
|
|
1193
1079
|
const input = readArguments(args);
|
|
1194
1080
|
const runtime = host.runtime();
|
|
1195
1081
|
if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
|
|
1196
1082
|
if (input.timeoutMs !== void 0 && runtime.timeout !== void 0 === false && isStrictTimeoutUnsupported(runtime)) throw new Error("timeoutMs is not available for this PTC runtime");
|
|
1197
|
-
|
|
1083
|
+
await host.awaitPredecessors?.(exec.agent, exec.signal);
|
|
1084
|
+
const catalog = host.catalog(exec.agent, exec.toolSet);
|
|
1198
1085
|
const runController = new AbortController();
|
|
1199
1086
|
const onOuterAbort = () => {
|
|
1200
1087
|
runController.abort(exec.signal.reason);
|
|
@@ -1202,6 +1089,7 @@ function createTransportDefinition(host, rules) {
|
|
|
1202
1089
|
if (exec.signal.aborted) runController.abort(exec.signal.reason);
|
|
1203
1090
|
else exec.signal.addEventListener("abort", onOuterAbort, { once: true });
|
|
1204
1091
|
const nestedSignal = markNested(runController.signal) ?? runController.signal;
|
|
1092
|
+
if (input.narration !== void 0) await forwardNarration(host, input.narration, exec, nestedSignal);
|
|
1205
1093
|
const owner = host.stateOwner(exec.agent);
|
|
1206
1094
|
let maxSubCalls = rules.options.maxSubCalls;
|
|
1207
1095
|
let attempts = 0;
|
|
@@ -1215,11 +1103,11 @@ function createTransportDefinition(host, rules) {
|
|
|
1215
1103
|
if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " not dispatched");
|
|
1216
1104
|
const n = ++attempts;
|
|
1217
1105
|
if (n > maxSubCalls) {
|
|
1218
|
-
const limitErr =
|
|
1106
|
+
const limitErr = new ToolCallLimitExceededError(maxSubCalls, n, rules.options.maxSubCallsPerMinute, runToolName);
|
|
1219
1107
|
runController.abort(limitErr);
|
|
1220
1108
|
throw limitErr;
|
|
1221
1109
|
}
|
|
1222
|
-
const parsed = losslessArgs(rawArgs);
|
|
1110
|
+
const parsed = normalizeSubCallArgs(definitions.get(entry.name), losslessArgs(rawArgs));
|
|
1223
1111
|
const loggedArgs = losslessArgs(parsed);
|
|
1224
1112
|
const callId = String(exec.callId) + NESTED_CALL_ID_MARKER + String(n);
|
|
1225
1113
|
const dispatchLog = createDispatchLog(exec.agent, {
|
|
@@ -1229,8 +1117,8 @@ function createTransportDefinition(host, rules) {
|
|
|
1229
1117
|
name: entry.name,
|
|
1230
1118
|
arguments: loggedArgs
|
|
1231
1119
|
});
|
|
1232
|
-
|
|
1233
|
-
|
|
1120
|
+
return lane.schedule({
|
|
1121
|
+
classify: () => isConcurrencySafe(definitions.get(entry.name), parsed) ? "parallel" : "exclusive",
|
|
1234
1122
|
start() {
|
|
1235
1123
|
dispatchLog.start();
|
|
1236
1124
|
},
|
|
@@ -1245,35 +1133,39 @@ function createTransportDefinition(host, rules) {
|
|
|
1245
1133
|
/**
|
|
1246
1134
|
* 提交阶段:按**提交顺序**逐条执行,落在会话里的副作用因此与提交顺序一致。
|
|
1247
1135
|
*
|
|
1248
|
-
*
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
*
|
|
1136
|
+
* 对齐内核 `ptc.ts:537-634`:延迟上下文(图片与 `additionalContexts`)与 `concludeTurn`
|
|
1137
|
+
* 按提交顺序同步生效后,**立刻**向程序返回子调用结果,避免慢速日志落盘监听器阻塞程序
|
|
1138
|
+
* 后续执行;`shapeDispatchLog` 与 `tool/ptc-dispatch` 事件通过带背压的有序 `logWork`
|
|
1139
|
+
* 队列异步结算,并在 `lane.drain()` 中完整排空。
|
|
1252
1140
|
*/
|
|
1253
|
-
|
|
1141
|
+
commit(parked, enqueueLog) {
|
|
1254
1142
|
if (!parked.ok) {
|
|
1255
|
-
|
|
1143
|
+
enqueueLog(async () => {
|
|
1144
|
+
dispatchLog.settle({ error: messageOf$2(parked.error) });
|
|
1145
|
+
});
|
|
1256
1146
|
throw parked.error;
|
|
1257
1147
|
}
|
|
1258
1148
|
const outcome = parked.value;
|
|
1259
1149
|
const isError = outcome.error !== void 0;
|
|
1260
|
-
const content = await shapeDispatchContent(host, {
|
|
1261
|
-
agent: exec.agent,
|
|
1262
|
-
name: entry.name,
|
|
1263
|
-
arguments: loggedArgs,
|
|
1264
|
-
isError,
|
|
1265
|
-
content: clipContent(outcome.content)
|
|
1266
|
-
});
|
|
1267
|
-
dispatchLog.settle({
|
|
1268
|
-
...outcome,
|
|
1269
|
-
content
|
|
1270
|
-
});
|
|
1271
1150
|
if (!isError && Array.isArray(outcome.content) && hasImageBlock(outcome.content)) {
|
|
1272
1151
|
const imageMsg = host.createImageContext?.(outcome.content);
|
|
1273
1152
|
if (imageMsg !== void 0) exec.deferContext?.(imageMsg);
|
|
1274
1153
|
}
|
|
1275
1154
|
if (Array.isArray(outcome.additionalContexts)) for (const contextMsg of outcome.additionalContexts) exec.deferContext?.(contextMsg);
|
|
1276
1155
|
if (!isError && outcome.concluded === true) exec.concludeTurn?.();
|
|
1156
|
+
enqueueLog(async () => {
|
|
1157
|
+
const content = await shapeDispatchContent(host, {
|
|
1158
|
+
agent: exec.agent,
|
|
1159
|
+
name: entry.name,
|
|
1160
|
+
arguments: loggedArgs,
|
|
1161
|
+
isError,
|
|
1162
|
+
content: clipContent(outcome.content)
|
|
1163
|
+
});
|
|
1164
|
+
dispatchLog.settle({
|
|
1165
|
+
...outcome,
|
|
1166
|
+
content
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1277
1169
|
if (runController.signal.aborted) throw new Error(runToolName + " run is over; " + entry.name + " result discarded");
|
|
1278
1170
|
if (outcome.error !== void 0) throw new Error(outcome.error);
|
|
1279
1171
|
return outcome.value !== void 0 ? outcome.value : outcome.text ?? null;
|
|
@@ -1298,7 +1190,7 @@ function createTransportDefinition(host, rules) {
|
|
|
1298
1190
|
* `stateOwner`,避免把单次授权固化到常驻环境中(对齐内核 `ptc.ts`)。
|
|
1299
1191
|
*/
|
|
1300
1192
|
let approvedEscalatedMode;
|
|
1301
|
-
/**
|
|
1193
|
+
/** 这次运行在入口那边的登记;结算时释放(重跑原位更新 mode,外层 finally 统一释放)。 */
|
|
1302
1194
|
let untrackRun = () => {};
|
|
1303
1195
|
const attempt = async () => {
|
|
1304
1196
|
const authority = await host.runAuthority?.(exec.agent, nestedSignal);
|
|
@@ -1325,11 +1217,11 @@ function createTransportDefinition(host, rules) {
|
|
|
1325
1217
|
};
|
|
1326
1218
|
escalated = modeOfPolicy(effectivePolicy) !== modeOfPolicy(standingPolicy);
|
|
1327
1219
|
}
|
|
1328
|
-
untrackRun();
|
|
1329
1220
|
const runMode = modeOfPolicy(effectivePolicy);
|
|
1330
1221
|
untrackRun = host.trackRun?.({
|
|
1331
1222
|
agent: exec.agent,
|
|
1332
1223
|
controller: runController,
|
|
1224
|
+
signal: nestedSignal,
|
|
1333
1225
|
...runMode === void 0 ? {} : { mode: runMode }
|
|
1334
1226
|
}) ?? (() => {});
|
|
1335
1227
|
const request = {
|
|
@@ -1368,10 +1260,17 @@ function createTransportDefinition(host, rules) {
|
|
|
1368
1260
|
untrackRun();
|
|
1369
1261
|
}
|
|
1370
1262
|
if (result.error !== void 0) {
|
|
1263
|
+
const limitError = runController.signal.reason instanceof ToolCallLimitExceededError ? runController.signal.reason : void 0;
|
|
1264
|
+
if (result.error.kind === "abort" && exec.signal.aborted && limitError === void 0) {
|
|
1265
|
+
const abortError = new HarnessError(runtime.retainsState ? `tool call aborted: ${result.error.message}` : "tool call aborted", "ABORTED");
|
|
1266
|
+
abortError.name = "AbortError";
|
|
1267
|
+
throw abortError;
|
|
1268
|
+
}
|
|
1371
1269
|
const logsText = Array.isArray(result.logs) && result.logs.length > 0 ? "\nCaptured output:\n" + result.logs.join("\n") : "";
|
|
1372
1270
|
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
1271
|
const hint = result.sandbox?.denied ? escalationGuidance(runtime.sandboxMode) : "";
|
|
1374
|
-
|
|
1272
|
+
const failureText = limitError === void 0 || limitError.message === result.error.message ? result.error.message : `${limitError.message}\n${result.error.message}`;
|
|
1273
|
+
throw new CodeRunFailedError("program failed (" + result.error.kind + "): " + failureText + logsText + sandboxText + hint);
|
|
1375
1274
|
}
|
|
1376
1275
|
return {
|
|
1377
1276
|
logs: [...result.logs ?? []],
|
|
@@ -1504,32 +1403,257 @@ function validateEscalationArgs(sandboxPermissions, justification) {
|
|
|
1504
1403
|
justification
|
|
1505
1404
|
};
|
|
1506
1405
|
}
|
|
1406
|
+
/** 程序源码可能出现的键名。模型偶尔把源码写进 `program` / `source` / `script`。 */
|
|
1407
|
+
const CODE_KEYS = Object.freeze([
|
|
1408
|
+
"code",
|
|
1409
|
+
"program",
|
|
1410
|
+
"source",
|
|
1411
|
+
"script",
|
|
1412
|
+
"code_run",
|
|
1413
|
+
"codecode",
|
|
1414
|
+
"program_source",
|
|
1415
|
+
"programSource"
|
|
1416
|
+
]);
|
|
1417
|
+
/** 模型把「先汇报再执行」写成一次调用时,简报落在 `progress_update` 的那两个键上。 */
|
|
1418
|
+
const NARRATION_TITLE_KEY = "title";
|
|
1419
|
+
/** 见 {@link NARRATION_TITLE_KEY}。 */
|
|
1420
|
+
const NARRATION_MESSAGE_KEY = "message";
|
|
1421
|
+
/** 回显原始参数时最多带多少字符。 */
|
|
1422
|
+
const RAW_EXCERPT_LIMIT = 120;
|
|
1423
|
+
/** 一个值是不是普通对象。 */
|
|
1424
|
+
function isRecord(value) {
|
|
1425
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1426
|
+
}
|
|
1427
|
+
/** 尝试解析一段 JSON 文本。 */
|
|
1428
|
+
function tryJson(text) {
|
|
1429
|
+
try {
|
|
1430
|
+
return JSON.parse(text);
|
|
1431
|
+
} catch {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* 切出一段文本里的顶层平衡 JSON 对象片段。
|
|
1437
|
+
*
|
|
1438
|
+
* 模型偶尔把两次调用的参数写进同一段文本(`{"title":…,"message":…}{"code":…}`),
|
|
1439
|
+
* 整段 `JSON.parse` 必然失败;按花括号配对切出来的每一段却都还能解析,程序因此可救。
|
|
1440
|
+
*
|
|
1441
|
+
* @param text - 待切分的原始参数文本。
|
|
1442
|
+
* @returns 按出现顺序排列的片段(字符串内的花括号不算)。
|
|
1443
|
+
*/
|
|
1444
|
+
function topLevelJsonObjects(text) {
|
|
1445
|
+
const fragments = [];
|
|
1446
|
+
let depth = 0;
|
|
1447
|
+
let start = -1;
|
|
1448
|
+
let inString = false;
|
|
1449
|
+
let escaped = false;
|
|
1450
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1451
|
+
const ch = text[index];
|
|
1452
|
+
if (inString) {
|
|
1453
|
+
if (escaped) escaped = false;
|
|
1454
|
+
else if (ch === "\\") escaped = true;
|
|
1455
|
+
else if (ch === "\"") inString = false;
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
if (ch === "\"") {
|
|
1459
|
+
inString = true;
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (ch === "{") {
|
|
1463
|
+
if (depth === 0) start = index;
|
|
1464
|
+
depth += 1;
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (ch !== "}") continue;
|
|
1468
|
+
if (depth === 0) continue;
|
|
1469
|
+
depth -= 1;
|
|
1470
|
+
if (depth === 0 && start >= 0) {
|
|
1471
|
+
fragments.push(text.slice(start, index + 1));
|
|
1472
|
+
start = -1;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return fragments;
|
|
1476
|
+
}
|
|
1477
|
+
/** 从一个参数对象里读出程序源码。 */
|
|
1478
|
+
function codeOf(record) {
|
|
1479
|
+
for (const key of CODE_KEYS) {
|
|
1480
|
+
const value = record[key];
|
|
1481
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/** 从一个参数对象里读出进展简报(`title` / `message` 至少有一个)。 */
|
|
1485
|
+
function narrationOf(record) {
|
|
1486
|
+
const title = stringOf(record[NARRATION_TITLE_KEY]);
|
|
1487
|
+
const message = stringOf(record[NARRATION_MESSAGE_KEY]);
|
|
1488
|
+
if (title === void 0 && message === void 0) return void 0;
|
|
1489
|
+
return {
|
|
1490
|
+
title: title ?? message.slice(0, 40),
|
|
1491
|
+
message: message ?? title
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
/** 一个非空字符串(**不裁剪**内容,源码要原样送出)。 */
|
|
1495
|
+
function stringOf(value) {
|
|
1496
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
1497
|
+
}
|
|
1498
|
+
/** 错误回显里的一段原始文本。 */
|
|
1499
|
+
function excerpt(text) {
|
|
1500
|
+
const flat = text.replace(/[\r\n]+/g, "⏎").replace(/\s+/g, " ").trim();
|
|
1501
|
+
return flat.length <= RAW_EXCERPT_LIMIT ? flat : flat.slice(0, RAW_EXCERPT_LIMIT) + "…";
|
|
1502
|
+
}
|
|
1503
|
+
/** 参数完全不成形时的错误。 */
|
|
1504
|
+
function invalidArgumentsError(args, raw, keys) {
|
|
1505
|
+
if (args === null) return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received null)");
|
|
1506
|
+
if (typeof args !== "string") return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received " + typeof args + ")");
|
|
1507
|
+
if (keys !== void 0 && keys.length > 0) return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received keys: " + keys.join(", ") + ")");
|
|
1508
|
+
if (raw.trim().length === 0) return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received an empty string)");
|
|
1509
|
+
return /* @__PURE__ */ new Error("codemode: invalid arguments: expected an object with \"code\" and \"description\" (received a string that is not a JSON object: " + excerpt(raw) + ")");
|
|
1510
|
+
}
|
|
1511
|
+
/** 参数里没有程序源码、又不是可救形态时的错误。 */
|
|
1512
|
+
function missingCodeError(record, raw) {
|
|
1513
|
+
const keys = Object.keys(record);
|
|
1514
|
+
const note = keys.length === 0 ? "" : " (received keys: " + keys.join(", ") + (raw.trim().length > 0 ? "; raw: " + excerpt(raw) : "") + "; pass program source in \"code\")";
|
|
1515
|
+
return /* @__PURE__ */ new Error("codemode: invalid code: expected a non-empty string" + note);
|
|
1516
|
+
}
|
|
1517
|
+
/** 一段文本像不像程序源码(模型把源码直接发成字符串时判这个)。 */
|
|
1518
|
+
function looksLikeProgram(text) {
|
|
1519
|
+
return text.includes("\n") || /\b(await|return|const|let|function|import|console\.)\b/.test(text);
|
|
1520
|
+
}
|
|
1521
|
+
/** 从源码首行兜底出一句描述。 */
|
|
1522
|
+
function descriptionFromCode(code) {
|
|
1523
|
+
const cleaned = (code.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0) ?? "").replace(/^(\/\/|#)\s*/, "").slice(0, 80);
|
|
1524
|
+
return cleaned.length > 0 ? cleaned : "code program";
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* 把工具收到的原始参数读成一个对象。
|
|
1528
|
+
*
|
|
1529
|
+
* ## 为什么要容错
|
|
1530
|
+
*
|
|
1531
|
+
* 模型有四种真实存在的发送形态,照搬内核 PTC 的严格 schema 会把它们全判死:
|
|
1532
|
+
*
|
|
1533
|
+
* 1. **原生对象**(正常);
|
|
1534
|
+
* 2. **字符串里的 JSON**(执行器把解析失败的文本原样传下来);
|
|
1535
|
+
* 3. **两段 JSON 拼接**——`{"title":…,"message":…}{"code":…}`,模型想在同一次调用里
|
|
1536
|
+
* 既汇报又执行,整段解析必失败,但按花括号切开后两段都合法(实测 7/7 可救);
|
|
1537
|
+
* 4. **只有一段纯程序**(模型直接把源码当参数发)。
|
|
1538
|
+
*
|
|
1539
|
+
* @param args - 工具收到的原始参数。
|
|
1540
|
+
* @returns 参数对象、可选的简报与来源形态。
|
|
1541
|
+
*/
|
|
1542
|
+
function parseArguments(args) {
|
|
1543
|
+
if (isRecord(args)) return {
|
|
1544
|
+
record: args,
|
|
1545
|
+
origin: "object",
|
|
1546
|
+
raw: ""
|
|
1547
|
+
};
|
|
1548
|
+
if (typeof args !== "string") throw invalidArgumentsError(args, "");
|
|
1549
|
+
const raw = args;
|
|
1550
|
+
const trimmed = raw.trim();
|
|
1551
|
+
if (trimmed.length === 0) throw invalidArgumentsError(args, raw);
|
|
1552
|
+
const parsed = tryJson(trimmed);
|
|
1553
|
+
if (isRecord(parsed)) {
|
|
1554
|
+
const narration = narrationOf(parsed);
|
|
1555
|
+
return {
|
|
1556
|
+
record: parsed,
|
|
1557
|
+
origin: "json",
|
|
1558
|
+
raw,
|
|
1559
|
+
...narration === void 0 ? {} : { narration }
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
if (Array.isArray(parsed)) {
|
|
1563
|
+
const hit = parsed.find((entry) => isRecord(entry) && codeOf(entry) !== void 0);
|
|
1564
|
+
if (isRecord(hit)) {
|
|
1565
|
+
const narration = narrationOf(hit);
|
|
1566
|
+
return {
|
|
1567
|
+
record: hit,
|
|
1568
|
+
origin: "json",
|
|
1569
|
+
raw,
|
|
1570
|
+
...narration === void 0 ? {} : { narration }
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const fragments = topLevelJsonObjects(trimmed);
|
|
1575
|
+
let chosen;
|
|
1576
|
+
let narration;
|
|
1577
|
+
for (const fragment of fragments) {
|
|
1578
|
+
const value = tryJson(fragment);
|
|
1579
|
+
if (!isRecord(value)) continue;
|
|
1580
|
+
if (codeOf(value) !== void 0) {
|
|
1581
|
+
chosen = value;
|
|
1582
|
+
narration ??= narrationOf(value);
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
narration ??= narrationOf(value);
|
|
1586
|
+
}
|
|
1587
|
+
if (chosen !== void 0) return {
|
|
1588
|
+
record: chosen,
|
|
1589
|
+
origin: fragments.length > 1 ? "fused" : "json",
|
|
1590
|
+
raw,
|
|
1591
|
+
...narration === void 0 ? {} : { narration }
|
|
1592
|
+
};
|
|
1593
|
+
if (looksLikeProgram(trimmed)) return {
|
|
1594
|
+
record: { code: raw },
|
|
1595
|
+
origin: "bare",
|
|
1596
|
+
raw
|
|
1597
|
+
};
|
|
1598
|
+
throw invalidArgumentsError(args, raw, keysOf(fragments));
|
|
1599
|
+
}
|
|
1600
|
+
/** 把若干 JSON 片段里出现过的键并起来(错误回显用)。 */
|
|
1601
|
+
function keysOf(fragments) {
|
|
1602
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1603
|
+
for (const fragment of fragments) {
|
|
1604
|
+
const value = tryJson(fragment);
|
|
1605
|
+
if (!isRecord(value)) continue;
|
|
1606
|
+
for (const key of Object.keys(value)) keys.add(key);
|
|
1607
|
+
}
|
|
1608
|
+
return [...keys];
|
|
1609
|
+
}
|
|
1507
1610
|
/** 校验并读出工具参数。 */
|
|
1508
1611
|
function readArguments(args) {
|
|
1509
|
-
|
|
1510
|
-
const record =
|
|
1511
|
-
const code = record
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
-
}
|
|
1517
|
-
if (typeof description !== "string" || description.trim().length === 0) throw new Error("codemode: invalid description: expected a non-empty string");
|
|
1612
|
+
const parsed = parseArguments(args);
|
|
1613
|
+
const record = parsed.record;
|
|
1614
|
+
const code = codeOf(record);
|
|
1615
|
+
if (code === void 0) throw missingCodeError(record, parsed.raw);
|
|
1616
|
+
const description = stringOf(record["description"])?.trim() ?? stringOf(record["desc"])?.trim() ?? stringOf(record["summary"])?.trim() ?? parsed.narration?.title ?? descriptionFromCode(code);
|
|
1518
1617
|
const escalation = validateEscalationArgs(record["sandbox_permissions"], record["justification"]);
|
|
1519
1618
|
const timeoutMs = record["timeoutMs"];
|
|
1520
|
-
if (timeoutMs
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
...escalation
|
|
1524
|
-
};
|
|
1525
|
-
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
|
|
1619
|
+
if (timeoutMs !== void 0) {
|
|
1620
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
|
|
1621
|
+
}
|
|
1526
1622
|
return {
|
|
1527
1623
|
code,
|
|
1528
1624
|
description,
|
|
1529
|
-
timeoutMs,
|
|
1530
|
-
...escalation
|
|
1625
|
+
...timeoutMs === void 0 ? {} : { timeoutMs },
|
|
1626
|
+
...escalation,
|
|
1627
|
+
...parsed.narration === void 0 ? {} : { narration: parsed.narration }
|
|
1531
1628
|
};
|
|
1532
1629
|
}
|
|
1630
|
+
/**
|
|
1631
|
+
* 把救回来的进展简报补发成一次 `progress_update`。
|
|
1632
|
+
*
|
|
1633
|
+
* 简报只影响界面,因此这里的任何失败都被吞掉:发不出去(没有投影、没有服务端、外层已取消)
|
|
1634
|
+
* 绝不能连累这次程序。
|
|
1635
|
+
*
|
|
1636
|
+
* @param host - 运行环境句柄。
|
|
1637
|
+
* @param narration - 从融合参数里救回来的简报。
|
|
1638
|
+
* @param exec - 本次执行上下文。
|
|
1639
|
+
* @param signal - 已带嵌套标记的取消信号。
|
|
1640
|
+
*/
|
|
1641
|
+
async function forwardNarration(host, narration, exec, signal) {
|
|
1642
|
+
try {
|
|
1643
|
+
if (!host.catalog().some((entry) => entry.name === PROGRESS_UPDATE_TOOL)) return;
|
|
1644
|
+
await host.callTool({
|
|
1645
|
+
name: PROGRESS_UPDATE_TOOL,
|
|
1646
|
+
args: {
|
|
1647
|
+
title: narration.title,
|
|
1648
|
+
message: narration.message
|
|
1649
|
+
},
|
|
1650
|
+
callId: String(exec.callId) + ":ptc:0",
|
|
1651
|
+
signal,
|
|
1652
|
+
agent: exec.agent,
|
|
1653
|
+
toolSet: exec.toolSet
|
|
1654
|
+
});
|
|
1655
|
+
} catch {}
|
|
1656
|
+
}
|
|
1533
1657
|
/** 子调用参数必须是可无损 JSON 化的值。 */
|
|
1534
1658
|
function losslessArgs(value) {
|
|
1535
1659
|
if (value === void 0) return {};
|
|
@@ -1539,29 +1663,95 @@ function losslessArgs(value) {
|
|
|
1539
1663
|
throw new Error("codemode: tool arguments must be lossless JSON");
|
|
1540
1664
|
}
|
|
1541
1665
|
}
|
|
1542
|
-
/**
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1666
|
+
/** 内置按单文件 `file_path` 寻址的工具名。 */
|
|
1667
|
+
const FILE_PATH_TOOLS = /* @__PURE__ */ new Set([
|
|
1668
|
+
"read",
|
|
1669
|
+
"edit",
|
|
1670
|
+
"write",
|
|
1671
|
+
"read_image"
|
|
1672
|
+
]);
|
|
1673
|
+
/** 内置按目录/目标 `path` 寻址的检索工具名。 */
|
|
1674
|
+
const SEARCH_PATH_TOOLS = /* @__PURE__ */ new Set(["glob", "grep"]);
|
|
1675
|
+
/**
|
|
1676
|
+
* 宿主层子调用入参别名容错:把模型在 `code_run` 内高频写错的参数键名映射到工具真实声明的键名。
|
|
1677
|
+
*
|
|
1678
|
+
* 审计证据(`session-e190c243` seq 171-182 等):模型在 `Promise.all` 里容易把 `tools.read` 的
|
|
1679
|
+
* `file_path` 写成 `glob`/`grep` 的 `path`(或反过来),或者把 snake_case 写成 camelCase,导致整段
|
|
1680
|
+
* 程序因单个属性名不匹配而抛 `ToolCallError: invalid arguments: missing required property "file_path"`。
|
|
1681
|
+
*
|
|
1682
|
+
* 规则:仅当目标规范键尚未提供(`=== undefined`)、别名键存在且类型匹配、且别名键本身不是该工具声明的
|
|
1683
|
+
* 独立参数时,才把值搬运到规范键并删掉多余别名键(防止 `additionalProperties: false` 拒收)。
|
|
1684
|
+
*
|
|
1685
|
+
* @param entry - 工具清单条目(含参数 schema)。
|
|
1686
|
+
* @param value - 已脱离引用的 JSON 参数值。
|
|
1687
|
+
* @returns 归一化后的参数值。
|
|
1688
|
+
*/
|
|
1689
|
+
function normalizeSubCallArgs(entry, value) {
|
|
1690
|
+
if (!isRecord(value)) return value;
|
|
1691
|
+
const name = entry?.name ?? "";
|
|
1692
|
+
const schemaProps = isRecord(entry?.parameters) && isRecord(entry.parameters.properties) ? entry.parameters.properties : void 0;
|
|
1693
|
+
const declares = (key) => schemaProps !== void 0 && Object.prototype.hasOwnProperty.call(schemaProps, key);
|
|
1694
|
+
const out = { ...value };
|
|
1695
|
+
const remap = (target, aliases, predicate) => {
|
|
1696
|
+
if (out[target] !== void 0) return;
|
|
1697
|
+
for (const alias of aliases) {
|
|
1698
|
+
if (declares(alias)) continue;
|
|
1699
|
+
const candidate = out[alias];
|
|
1700
|
+
if (predicate(candidate)) {
|
|
1701
|
+
out[target] = candidate;
|
|
1702
|
+
delete out[alias];
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
const isString = (v) => typeof v === "string";
|
|
1708
|
+
const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1709
|
+
const isBoolean = (v) => typeof v === "boolean";
|
|
1710
|
+
if (FILE_PATH_TOOLS.has(name) || declares("file_path") && !declares("path")) remap("file_path", ["path", "filePath"], isString);
|
|
1711
|
+
else if (SEARCH_PATH_TOOLS.has(name) || declares("path") && !declares("file_path")) remap("path", ["file_path", "filePath"], isString);
|
|
1712
|
+
if (name === "edit" || declares("old_string") || declares("new_string")) {
|
|
1713
|
+
remap("old_string", ["oldString"], isString);
|
|
1714
|
+
remap("new_string", ["newString"], isString);
|
|
1715
|
+
remap("replace_all", ["replaceAll"], isBoolean);
|
|
1716
|
+
}
|
|
1717
|
+
if (name === "win_bash" || declares("workdir")) {
|
|
1718
|
+
remap("workdir", [
|
|
1719
|
+
"cwd",
|
|
1720
|
+
"workingDirectory",
|
|
1721
|
+
"working_directory"
|
|
1722
|
+
], isString);
|
|
1723
|
+
remap("timeoutMs", ["timeout_ms"], isNumber);
|
|
1724
|
+
remap("run_in_background", ["runInBackground"], isBoolean);
|
|
1725
|
+
}
|
|
1726
|
+
if (name === "job_output" || name === "job_kill" || declares("job_id")) {
|
|
1727
|
+
remap("job_id", ["jobId"], isString);
|
|
1728
|
+
if (name === "job_output" || declares("timeout_ms")) remap("timeout_ms", ["timeoutMs"], isNumber);
|
|
1729
|
+
}
|
|
1730
|
+
return out;
|
|
1731
|
+
}
|
|
1732
|
+
/** 一个工具的调用是否可以与兄弟调用重叠(只有明确 true 才算)。 */
|
|
1733
|
+
function isConcurrencySafe(entry, args) {
|
|
1734
|
+
if (entry === void 0 || typeof entry.isConcurrencySafe !== "function") return false;
|
|
1735
|
+
try {
|
|
1736
|
+
return entry.isConcurrencySafe(args) === true;
|
|
1737
|
+
} catch {
|
|
1738
|
+
return false;
|
|
1549
1739
|
}
|
|
1550
1740
|
}
|
|
1551
1741
|
/**
|
|
1552
|
-
* 子调用的并发、提交顺序与互斥屏障(对齐内核 `ptc.ts` 的 ordered commit lane
|
|
1742
|
+
* 子调用的并发、提交顺序与互斥屏障(对齐内核 `ptc.ts` 的 ordered commit lane + `logWork` 背压)。
|
|
1553
1743
|
*
|
|
1744
|
+
* - `classify()` 在队列头部真正出队启动时惰性求值,保证前序调用产生的状态变更可被观察;
|
|
1554
1745
|
* - `start()`(记 start 事件 + 启动 body)在 lane 里按提交顺序执行:`parallel` body 之间
|
|
1555
1746
|
* 可以并发重叠,最多 `maxParallel` 个同时在飞;
|
|
1556
|
-
* - `commit()
|
|
1557
|
-
*
|
|
1747
|
+
* - `commit()`(转发图片/附加上下文 + `concludeTurn` + 放行互斥屏障)按**提交顺序**逐条执行,
|
|
1748
|
+
* 并在同步完成上下文挂载后**立刻**将结果返回给程序;`shapeDispatchLog` 与 settle 事件作为
|
|
1749
|
+
* 有序后台 `logWork` 执行,超出 `maxParallel` 时在提交车道施加背压;
|
|
1558
1750
|
* - `exclusive` 调用等所有在飞 body 结算后独占运行,并独占到自己 commit 完成才放行后续;
|
|
1559
1751
|
* - 排队期间若外层运行已中止,直接放弃且不启动任务(未启动调用零 `start` 事件);
|
|
1560
|
-
* - `drain()` 等到 lane
|
|
1752
|
+
* - `drain()` 等到 lane 彻底静止且所有后台 `logWork` 落盘完成才返回。
|
|
1561
1753
|
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
* @param maxParallel - 同时在飞的上限。
|
|
1754
|
+
* @param maxParallel - 同时在飞的上限(同时作为 `logWork` 背压上限)。
|
|
1565
1755
|
* @param isAborted - 外层运行是否已中止。
|
|
1566
1756
|
* @returns 调度入口与排空。
|
|
1567
1757
|
*/
|
|
@@ -1570,6 +1760,8 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1570
1760
|
const queue = [];
|
|
1571
1761
|
const commitQueue = [];
|
|
1572
1762
|
const inFlight = /* @__PURE__ */ new Set();
|
|
1763
|
+
const logWork = /* @__PURE__ */ new Set();
|
|
1764
|
+
let logTail = Promise.resolve();
|
|
1573
1765
|
let exclusiveActive = false;
|
|
1574
1766
|
let driving = false;
|
|
1575
1767
|
let driverRun = Promise.resolve();
|
|
@@ -1579,6 +1771,13 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1579
1771
|
wake = void 0;
|
|
1580
1772
|
release?.();
|
|
1581
1773
|
};
|
|
1774
|
+
const enqueueLog = (work) => {
|
|
1775
|
+
logTail = logTail.then(work, work).catch(() => void 0);
|
|
1776
|
+
const tracked = logTail.finally(() => {
|
|
1777
|
+
logWork.delete(tracked);
|
|
1778
|
+
});
|
|
1779
|
+
logWork.add(tracked);
|
|
1780
|
+
};
|
|
1582
1781
|
/**
|
|
1583
1782
|
* 唯一的有序车道:每一轮先提交队首已结算的调用,再在有空位时启动下一个排队调用,
|
|
1584
1783
|
* 否则睡到「有 body 结算或有新提交」为止。三条队列全空即静止。
|
|
@@ -1606,8 +1805,10 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1606
1805
|
head.abandon();
|
|
1607
1806
|
continue;
|
|
1608
1807
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1808
|
+
const mode = head.classify();
|
|
1809
|
+
if (!exclusiveActive && (mode === "exclusive" ? inFlight.size === 0 : inFlight.size < cap)) {
|
|
1810
|
+
head.mode = mode;
|
|
1811
|
+
if (mode === "exclusive") exclusiveActive = true;
|
|
1611
1812
|
queue.shift();
|
|
1612
1813
|
commitQueue.push(head);
|
|
1613
1814
|
head.start();
|
|
@@ -1630,11 +1831,18 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1630
1831
|
return driverRun;
|
|
1631
1832
|
};
|
|
1632
1833
|
return {
|
|
1633
|
-
schedule(
|
|
1834
|
+
schedule(task) {
|
|
1634
1835
|
return new Promise((resolve, reject) => {
|
|
1635
1836
|
let parked;
|
|
1636
1837
|
const entry = {
|
|
1637
|
-
mode,
|
|
1838
|
+
mode: "exclusive",
|
|
1839
|
+
classify() {
|
|
1840
|
+
try {
|
|
1841
|
+
return task.classify ? task.classify() : "exclusive";
|
|
1842
|
+
} catch {
|
|
1843
|
+
return "exclusive";
|
|
1844
|
+
}
|
|
1845
|
+
},
|
|
1638
1846
|
settled: false,
|
|
1639
1847
|
flight: Promise.resolve(),
|
|
1640
1848
|
start() {
|
|
@@ -1659,10 +1867,11 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1659
1867
|
const result = parked;
|
|
1660
1868
|
if (result === void 0) return;
|
|
1661
1869
|
try {
|
|
1662
|
-
resolve(await task.commit(result));
|
|
1870
|
+
resolve(await task.commit(result, enqueueLog));
|
|
1663
1871
|
} catch (error) {
|
|
1664
1872
|
reject(error);
|
|
1665
1873
|
}
|
|
1874
|
+
while (logWork.size > cap) await Promise.race(logWork);
|
|
1666
1875
|
},
|
|
1667
1876
|
abandon() {
|
|
1668
1877
|
try {
|
|
@@ -1679,6 +1888,7 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1679
1888
|
},
|
|
1680
1889
|
async drain() {
|
|
1681
1890
|
await drive();
|
|
1891
|
+
while (logWork.size > 0) await Promise.allSettled([...logWork]);
|
|
1682
1892
|
}
|
|
1683
1893
|
};
|
|
1684
1894
|
}
|
|
@@ -1697,7 +1907,7 @@ async function shapeDispatchContent(host, dispatch) {
|
|
|
1697
1907
|
}
|
|
1698
1908
|
}
|
|
1699
1909
|
/** 一个抛出值的消息。 */
|
|
1700
|
-
function messageOf$
|
|
1910
|
+
function messageOf$2(error) {
|
|
1701
1911
|
return error instanceof Error ? error.message : String(error);
|
|
1702
1912
|
}
|
|
1703
1913
|
const JSON_INDENT = " ";
|
|
@@ -1790,6 +2000,358 @@ function renderProgramValue(value) {
|
|
|
1790
2000
|
return chunks.join("");
|
|
1791
2001
|
}
|
|
1792
2002
|
//#endregion
|
|
2003
|
+
//#region src/host/codemode/retention.ts
|
|
2004
|
+
/**
|
|
2005
|
+
* 常驻执行环境的生命周期与会话级调用协调(对齐内核 `ptc.ts` 与 `PtcCalls`)。
|
|
2006
|
+
*
|
|
2007
|
+
* 部署把 `@deepseek-ai/dsh-ptc-runtime-node` 配成 `executionMode: persistent-in-memory` 时,
|
|
2008
|
+
* 同一个 agent 的多次 `code_run` 会共用一个常驻 REPL:顶层变量、函数与 `import` 的模块
|
|
2009
|
+
* 跨调用存活。可以不去用,但**不能只注册不释放**——常驻环境没有空闲过期,忘了释放会让子进程
|
|
2010
|
+
* 活到进程结束。
|
|
2011
|
+
*
|
|
2012
|
+
* 所以这里把「传了 owner」与「注册了释放」写进同一个动作:结构性无法表达「传了却没释放」。
|
|
2013
|
+
* 清理有三条路,覆盖所有生命周期终点与显式重置:
|
|
2014
|
+
*
|
|
2015
|
+
* 1. 挂在 **agent.ctx** 上的释放(对齐 `ptc.ts` 的 `stateOwners`):展开 Cordis 包装链后按原初
|
|
2016
|
+
* runtime provider 去重登记 `agent.ctx.effect`,即使本包早已卸载,agent 销毁也会释放它的环境;
|
|
2017
|
+
* 2. 挂在**插件**上的 teardown:卸载或热重载本包时先中止并排空在飞调用,再释放所有遗留环境;
|
|
2018
|
+
* 3. 会话级 `PtcCalls` 协调:新调用进入前等待同会话已取消的前序调用与进行中的 `reset` 彻底排空,
|
|
2019
|
+
* 避免前一次取消的收尾清理撞上后一次调用的 `assertIdle()`。
|
|
2020
|
+
*
|
|
2021
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/retention
|
|
2022
|
+
*/
|
|
2023
|
+
/** Cordis 服务包装链指向原初实现的符号描述。 */
|
|
2024
|
+
const CORDIS_ORIGINAL_DESCRIPTION = "cordis.original";
|
|
2025
|
+
/**
|
|
2026
|
+
* 造一个常驻环境与会话调用协调管理器。
|
|
2027
|
+
*
|
|
2028
|
+
* @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
|
|
2029
|
+
* @returns 管理句柄。
|
|
2030
|
+
*/
|
|
2031
|
+
function createRetention(runtimeOf) {
|
|
2032
|
+
const live = /* @__PURE__ */ new Set();
|
|
2033
|
+
const released = /* @__PURE__ */ new WeakSet();
|
|
2034
|
+
const stateOwners = /* @__PURE__ */ new WeakMap();
|
|
2035
|
+
const calls = /* @__PURE__ */ new Map();
|
|
2036
|
+
const resets = /* @__PURE__ */ new Map();
|
|
2037
|
+
let disposed = false;
|
|
2038
|
+
const release = async (owner, options) => {
|
|
2039
|
+
const strict = options?.strict === true;
|
|
2040
|
+
if (!strict && released.has(owner)) return;
|
|
2041
|
+
released.add(owner);
|
|
2042
|
+
live.delete(owner);
|
|
2043
|
+
const runtime = runtimeOf();
|
|
2044
|
+
if (runtime?.release === void 0) return;
|
|
2045
|
+
try {
|
|
2046
|
+
await runtime.release(owner);
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
if (strict) {
|
|
2049
|
+
released.delete(owner);
|
|
2050
|
+
live.add(owner);
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
/** 在 `agent.ctx` 上按原初 runtime provider 去重挂一次释放钩子(对齐 `ptc.ts:669-684`)。 */
|
|
2056
|
+
const bindAgentLifecycle = (owner) => {
|
|
2057
|
+
const runtime = runtimeOf();
|
|
2058
|
+
if (runtime === void 0 || typeof runtime !== "object" || runtime === null) return;
|
|
2059
|
+
const ctx = owner.ctx;
|
|
2060
|
+
if (typeof ctx?.effect !== "function") return;
|
|
2061
|
+
let runtimes = stateOwners.get(owner);
|
|
2062
|
+
if (runtimes === void 0) {
|
|
2063
|
+
runtimes = /* @__PURE__ */ new WeakSet();
|
|
2064
|
+
stateOwners.set(owner, runtimes);
|
|
2065
|
+
}
|
|
2066
|
+
const provider = unwrapOriginalProvider(runtime);
|
|
2067
|
+
if (runtimes.has(provider)) return;
|
|
2068
|
+
try {
|
|
2069
|
+
ctx.effect(() => () => release(owner), "PTC retained environment");
|
|
2070
|
+
runtimes.add(provider);
|
|
2071
|
+
} catch {}
|
|
2072
|
+
};
|
|
2073
|
+
return {
|
|
2074
|
+
owner(agent) {
|
|
2075
|
+
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
2076
|
+
released.delete(owner);
|
|
2077
|
+
live.add(owner);
|
|
2078
|
+
if (owner !== fallbackOwner) bindAgentLifecycle(owner);
|
|
2079
|
+
return owner;
|
|
2080
|
+
},
|
|
2081
|
+
release,
|
|
2082
|
+
async awaitPredecessors(agent, signal) {
|
|
2083
|
+
const key = sessionKeyOf(agent);
|
|
2084
|
+
if (key !== void 0) for (;;) {
|
|
2085
|
+
const reset = resets.get(key);
|
|
2086
|
+
if (reset !== void 0) {
|
|
2087
|
+
try {
|
|
2088
|
+
await reset.done;
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
throw new CodeRunFailedError("Program environment unavailable: " + messageOf$1(error) + ". Ask the host to reset it before running more code.");
|
|
2091
|
+
}
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
const pending = [...calls.get(key) ?? []].filter((call) => call.signal.aborted);
|
|
2095
|
+
if (pending.length === 0) break;
|
|
2096
|
+
await Promise.all(pending.map((call) => call.done));
|
|
2097
|
+
}
|
|
2098
|
+
if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "tool call aborted"));
|
|
2099
|
+
if (disposed) throw new Error("PTC tool executor disposed");
|
|
2100
|
+
},
|
|
2101
|
+
trackRun(run) {
|
|
2102
|
+
const key = sessionKeyOf(run.agent);
|
|
2103
|
+
if (key === void 0) return () => void 0;
|
|
2104
|
+
let bucket = calls.get(key);
|
|
2105
|
+
if (bucket === void 0) {
|
|
2106
|
+
bucket = /* @__PURE__ */ new Set();
|
|
2107
|
+
calls.set(key, bucket);
|
|
2108
|
+
}
|
|
2109
|
+
for (const existing of bucket) if (existing.controller === run.controller) {
|
|
2110
|
+
existing.mode = run.mode;
|
|
2111
|
+
return () => {
|
|
2112
|
+
const current = calls.get(key);
|
|
2113
|
+
if (current !== void 0) {
|
|
2114
|
+
current.delete(existing);
|
|
2115
|
+
if (current.size === 0) calls.delete(key);
|
|
2116
|
+
}
|
|
2117
|
+
existing.resolveDone();
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
let resolveDone = () => void 0;
|
|
2121
|
+
const done = new Promise((resolve) => {
|
|
2122
|
+
resolveDone = resolve;
|
|
2123
|
+
});
|
|
2124
|
+
const record = {
|
|
2125
|
+
controller: run.controller,
|
|
2126
|
+
signal: run.signal ?? run.controller.signal,
|
|
2127
|
+
...run.mode === void 0 ? {} : { mode: run.mode },
|
|
2128
|
+
done,
|
|
2129
|
+
resolveDone
|
|
2130
|
+
};
|
|
2131
|
+
bucket.add(record);
|
|
2132
|
+
return () => {
|
|
2133
|
+
const current = calls.get(key);
|
|
2134
|
+
if (current !== void 0) {
|
|
2135
|
+
current.delete(record);
|
|
2136
|
+
if (current.size === 0) calls.delete(key);
|
|
2137
|
+
}
|
|
2138
|
+
record.resolveDone();
|
|
2139
|
+
};
|
|
2140
|
+
},
|
|
2141
|
+
cancelOnModeChange(session, event) {
|
|
2142
|
+
if (typeof event !== "object" || event === null) return;
|
|
2143
|
+
const record = event;
|
|
2144
|
+
if (record.type !== "sandbox/mode") return;
|
|
2145
|
+
const mode = (typeof record.data === "object" && record.data !== null ? record.data : void 0)?.mode;
|
|
2146
|
+
if (typeof mode !== "string") return;
|
|
2147
|
+
const key = sessionKeyOf(session);
|
|
2148
|
+
if (key === void 0) return;
|
|
2149
|
+
for (const call of calls.get(key) ?? []) if (call.mode !== void 0 && call.mode !== mode) call.controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
|
|
2150
|
+
},
|
|
2151
|
+
reset(agent, notify) {
|
|
2152
|
+
if (disposed) return Promise.reject(/* @__PURE__ */ new Error("PTC tool executor disposed"));
|
|
2153
|
+
const key = sessionKeyOf(agent) ?? fallbackOwner;
|
|
2154
|
+
const previous = resets.get(key);
|
|
2155
|
+
if (previous !== void 0 && !previous.settled) return previous.done;
|
|
2156
|
+
let resolveDone = () => void 0;
|
|
2157
|
+
let rejectDone = () => void 0;
|
|
2158
|
+
const done = new Promise((resolve, reject) => {
|
|
2159
|
+
resolveDone = resolve;
|
|
2160
|
+
rejectDone = reject;
|
|
2161
|
+
});
|
|
2162
|
+
done.catch(() => void 0);
|
|
2163
|
+
const resetRecord = {
|
|
2164
|
+
done,
|
|
2165
|
+
settled: false
|
|
2166
|
+
};
|
|
2167
|
+
resets.set(key, resetRecord);
|
|
2168
|
+
const active = [...calls.get(key) ?? []];
|
|
2169
|
+
for (const call of active) call.controller.abort(/* @__PURE__ */ new Error("PTC environment reset"));
|
|
2170
|
+
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
2171
|
+
Promise.allSettled([Promise.resolve().then(() => release(owner, { strict: true })), ...active.map((call) => call.done)]).then((results) => {
|
|
2172
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
2173
|
+
if (failure !== void 0 && failure.status === "rejected") throw failure.reason;
|
|
2174
|
+
notify?.();
|
|
2175
|
+
}).then(() => {
|
|
2176
|
+
resetRecord.settled = true;
|
|
2177
|
+
resets.delete(key);
|
|
2178
|
+
resolveDone();
|
|
2179
|
+
}, (error) => {
|
|
2180
|
+
resetRecord.settled = true;
|
|
2181
|
+
rejectDone(error);
|
|
2182
|
+
});
|
|
2183
|
+
return done;
|
|
2184
|
+
},
|
|
2185
|
+
async dispose() {
|
|
2186
|
+
disposed = true;
|
|
2187
|
+
const activeCalls = [...calls.values()].flatMap((bucket) => [...bucket]);
|
|
2188
|
+
for (const call of activeCalls) call.controller.abort(/* @__PURE__ */ new Error("PTC tool executor disposed"));
|
|
2189
|
+
await Promise.allSettled([...activeCalls.map((call) => call.done), ...[...resets.values()].map((reset) => reset.done)]);
|
|
2190
|
+
const owners = [...live];
|
|
2191
|
+
await Promise.all(owners.map((owner) => release(owner)));
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* 展开 Cordis 服务代理上的 `[symbols.original]` 链,定位原初 provider 对象。
|
|
2197
|
+
*/
|
|
2198
|
+
function unwrapOriginalProvider(runtime) {
|
|
2199
|
+
let current = runtime;
|
|
2200
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
2201
|
+
let next;
|
|
2202
|
+
for (const sym of Object.getOwnPropertySymbols(current)) if (sym.description === CORDIS_ORIGINAL_DESCRIPTION) {
|
|
2203
|
+
next = current[sym];
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
if (typeof next !== "object" || next === null || next === current) break;
|
|
2207
|
+
current = next;
|
|
2208
|
+
}
|
|
2209
|
+
return current;
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* 一个会话的身份键(优先用 `session.id`,无 id 时退回会话对象本身)。
|
|
2213
|
+
*/
|
|
2214
|
+
function sessionKeyOf(value) {
|
|
2215
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2216
|
+
const nested = value.session;
|
|
2217
|
+
const session = typeof nested === "object" && nested !== null ? nested : value;
|
|
2218
|
+
const id = session.id;
|
|
2219
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
2220
|
+
return session;
|
|
2221
|
+
}
|
|
2222
|
+
/** 一个抛出值的消息。 */
|
|
2223
|
+
function messageOf$1(error) {
|
|
2224
|
+
return error instanceof Error ? error.message : String(error);
|
|
2225
|
+
}
|
|
2226
|
+
/** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
|
|
2227
|
+
const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region src/host/codemode/scope.ts
|
|
2230
|
+
/**
|
|
2231
|
+
* 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
|
|
2232
|
+
*
|
|
2233
|
+
* ## 为什么要这么绕
|
|
2234
|
+
*
|
|
2235
|
+
* 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
|
|
2236
|
+
* 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
|
|
2237
|
+
* 都不导出。能用的只有两件事:
|
|
2238
|
+
*
|
|
2239
|
+
* 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
|
|
2240
|
+
* 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
|
|
2241
|
+
*
|
|
2242
|
+
* 于是归属判定变成两次公开查询:
|
|
2243
|
+
*
|
|
2244
|
+
* - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
|
|
2245
|
+
* - 只有本预设视图能解析 → 本预设自己注册的;
|
|
2246
|
+
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
|
|
2247
|
+
*
|
|
2248
|
+
* 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
|
|
2249
|
+
* 纯名字匹配,绝不因为读不到归属就误遮蔽。
|
|
2250
|
+
*
|
|
2251
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
|
|
2252
|
+
*/
|
|
2253
|
+
/** 内核写作用域标签用的符号描述。 */
|
|
2254
|
+
const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
|
|
2255
|
+
/**
|
|
2256
|
+
* 从一个 ctx 上找出「本包所在作用域」的 key。
|
|
2257
|
+
*
|
|
2258
|
+
* Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
|
|
2259
|
+
* 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
|
|
2260
|
+
* 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
|
|
2261
|
+
*
|
|
2262
|
+
* @param ctx - 本包的行上下文。
|
|
2263
|
+
* @returns 作用域 key;读不到时为 `undefined`。
|
|
2264
|
+
*/
|
|
2265
|
+
function findScopeKey(ctx) {
|
|
2266
|
+
if (typeof ctx !== "object" || ctx === null) return void 0;
|
|
2267
|
+
return scanForScope(ctx, 0);
|
|
2268
|
+
}
|
|
2269
|
+
/** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
|
|
2270
|
+
function scanForScope(source, depth) {
|
|
2271
|
+
if (depth > 4) return void 0;
|
|
2272
|
+
let symbols;
|
|
2273
|
+
try {
|
|
2274
|
+
symbols = Object.getOwnPropertySymbols(source);
|
|
2275
|
+
} catch {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
for (const symbol of symbols) {
|
|
2279
|
+
if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
|
|
2280
|
+
let value;
|
|
2281
|
+
try {
|
|
2282
|
+
value = source[symbol];
|
|
2283
|
+
} catch {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
if (typeof value === "object" && value !== null) return value;
|
|
2287
|
+
}
|
|
2288
|
+
const proto = Object.getPrototypeOf(source);
|
|
2289
|
+
if (proto === null || proto === Object.prototype) return void 0;
|
|
2290
|
+
return scanForScope(proto, depth + 1);
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* 为一个名字清单建立归属视图。
|
|
2294
|
+
*
|
|
2295
|
+
* @param tools - 工具注册表(结构探测,不看类身份)。
|
|
2296
|
+
* @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
|
|
2297
|
+
* @param names - 要判定的名字(通常是本步的模型可见清单)。
|
|
2298
|
+
* @returns 归属视图;读不到任何归属时返回禁用视图。
|
|
2299
|
+
*/
|
|
2300
|
+
function resolveCatalogView(tools, scopeKey, names) {
|
|
2301
|
+
const registry = asRegistry(tools);
|
|
2302
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
2303
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
2304
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
2305
|
+
for (const name of names) {
|
|
2306
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
2307
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
2308
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
2309
|
+
}
|
|
2310
|
+
return {
|
|
2311
|
+
enabled: true,
|
|
2312
|
+
globalNames,
|
|
2313
|
+
presetNames
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* 判定一个名字在**调用时**是否属于本预设之外的层。
|
|
2318
|
+
*
|
|
2319
|
+
* 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
|
|
2320
|
+
* 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
|
|
2321
|
+
*
|
|
2322
|
+
* @param tools - 工具注册表。
|
|
2323
|
+
* @param scopeKey - 本预设的作用域 key。
|
|
2324
|
+
* @param name - 工具名。
|
|
2325
|
+
* @returns 归属视图(只含这一个名字)。
|
|
2326
|
+
*/
|
|
2327
|
+
function resolveNameView(tools, scopeKey, name) {
|
|
2328
|
+
const registry = asRegistry(tools);
|
|
2329
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
2330
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
2331
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
2332
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
2333
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
2334
|
+
return {
|
|
2335
|
+
enabled: true,
|
|
2336
|
+
globalNames,
|
|
2337
|
+
presetNames
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
/** 一次公开查询:解析得到定义时为 `true`。 */
|
|
2341
|
+
function probes(registry, name, scope) {
|
|
2342
|
+
try {
|
|
2343
|
+
return registry.get(name, scope) !== void 0;
|
|
2344
|
+
} catch {
|
|
2345
|
+
return false;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
/** 结构探测:读得到 `get` 就当注册表用。 */
|
|
2349
|
+
function asRegistry(tools) {
|
|
2350
|
+
if (typeof tools !== "object" || tools === null) return void 0;
|
|
2351
|
+
if (typeof tools.get !== "function") return void 0;
|
|
2352
|
+
return tools;
|
|
2353
|
+
}
|
|
2354
|
+
//#endregion
|
|
1793
2355
|
//#region src/entry-codemode.ts
|
|
1794
2356
|
/**
|
|
1795
2357
|
* Code Mode 预设行 —— 本包自己的「原生工具 + 程序传输」呈现方式。
|
|
@@ -1881,52 +2443,8 @@ function apply(ctx, config = {}) {
|
|
|
1881
2443
|
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1882
2444
|
retention.dispose();
|
|
1883
2445
|
}, "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
2446
|
if (typeof context.on === "function") try {
|
|
1929
|
-
const off = context.on("session/event", cancelOnModeChange, { global: true });
|
|
2447
|
+
const off = context.on("session/event", retention.cancelOnModeChange, { global: true });
|
|
1930
2448
|
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1931
2449
|
if (typeof off === "function") off();
|
|
1932
2450
|
}, "codemode sandbox mode cancellation");
|
|
@@ -1953,10 +2471,27 @@ function apply(ctx, config = {}) {
|
|
|
1953
2471
|
};
|
|
1954
2472
|
const scopeKey = findScopeKey(ctx);
|
|
1955
2473
|
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.");
|
|
1956
|
-
/** 最近一次 describe
|
|
2474
|
+
/** 最近一次 describe 收到的完整定义集(含被遮蔽的),作为无 `tools.definitions` 时的回退快照。 */
|
|
1957
2475
|
let catalog = [];
|
|
1958
2476
|
const debugRing = [];
|
|
1959
2477
|
let firstDescribeLogged = false;
|
|
2478
|
+
/**
|
|
2479
|
+
* 解析当前调用方 agent 与步骤准入下的工具清单(对齐内核 `ptc.ts:662`)。
|
|
2480
|
+
*
|
|
2481
|
+
* 优先通过 `ctx.tools.definitions(agent, toolSet)` 实时读取调用方自己的可见集合(含
|
|
2482
|
+
* `agent.ctx` 专属工具与 `tools.restrict` 过滤),避免多会话并发时被另一个会话的
|
|
2483
|
+
* `describe()` 覆盖;测试替身未提供 `definitions` 时回退到最近一次 `catalog`。
|
|
2484
|
+
*/
|
|
2485
|
+
const resolveCatalog = (agent, toolSet) => {
|
|
2486
|
+
if (agent !== void 0 && typeof tools.definitions === "function") try {
|
|
2487
|
+
const defs = tools.definitions(agent, toolSet);
|
|
2488
|
+
if (Array.isArray(defs) && defs.length > 0) {
|
|
2489
|
+
const entries = defs.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
2490
|
+
if (entries.length > 0) return entries;
|
|
2491
|
+
}
|
|
2492
|
+
} catch {}
|
|
2493
|
+
return catalog;
|
|
2494
|
+
};
|
|
1960
2495
|
const escalateSandbox = async (input) => {
|
|
1961
2496
|
const record = typeof input.standingPolicy === "object" && input.standingPolicy !== null ? input.standingPolicy : void 0;
|
|
1962
2497
|
const effectiveMode = typeof record?.["mode"] === "string" ? record["mode"] : void 0;
|
|
@@ -1989,10 +2524,11 @@ function apply(ctx, config = {}) {
|
|
|
1989
2524
|
}
|
|
1990
2525
|
};
|
|
1991
2526
|
const host = {
|
|
1992
|
-
catalog: () =>
|
|
2527
|
+
catalog: (agent, toolSet) => resolveCatalog(agent, toolSet),
|
|
1993
2528
|
runtime: runtimeOf,
|
|
1994
2529
|
stateOwner: (agent) => retention.owner(agent),
|
|
1995
2530
|
releaseOwner: (owner) => retention.release(owner),
|
|
2531
|
+
awaitPredecessors: (agent, signal) => retention.awaitPredecessors(agent, signal),
|
|
1996
2532
|
runAuthority,
|
|
1997
2533
|
escalateSandbox,
|
|
1998
2534
|
createImageContext(content) {
|
|
@@ -2001,7 +2537,7 @@ function apply(ctx, config = {}) {
|
|
|
2001
2537
|
source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
|
|
2002
2538
|
});
|
|
2003
2539
|
},
|
|
2004
|
-
trackRun,
|
|
2540
|
+
trackRun: (run) => retention.trackRun(run),
|
|
2005
2541
|
shapeDispatchLog,
|
|
2006
2542
|
async callTool(request) {
|
|
2007
2543
|
const call = {
|
|
@@ -2065,6 +2601,25 @@ function apply(ctx, config = {}) {
|
|
|
2065
2601
|
onCatalog: (definitions) => {
|
|
2066
2602
|
catalog = definitions.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
2067
2603
|
},
|
|
2604
|
+
async reset(agent) {
|
|
2605
|
+
const runtime = runtimeOf();
|
|
2606
|
+
if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
|
|
2607
|
+
if (!runtime.retainsState) throw new CodeResetFailedError("The PTC runtime does not retain an environment");
|
|
2608
|
+
try {
|
|
2609
|
+
await retention.reset(agent, () => {
|
|
2610
|
+
const inject = typeof agent === "object" && agent !== null ? agent.inject : void 0;
|
|
2611
|
+
if (typeof inject === "function") inject.call(agent, createUserMessage({
|
|
2612
|
+
content: [{
|
|
2613
|
+
type: "text",
|
|
2614
|
+
text: "Program environment reset; retained variables cleared. Completed tool effects remain."
|
|
2615
|
+
}],
|
|
2616
|
+
source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
|
|
2617
|
+
}));
|
|
2618
|
+
});
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
throw new CodeResetFailedError("Program environment reset failed: " + messageOf(error) + ". State may not be cleared; retry the host reset before running more code.");
|
|
2621
|
+
}
|
|
2622
|
+
},
|
|
2068
2623
|
onDescribe: (decision) => {
|
|
2069
2624
|
const hidden = summarizeHidden(decision.masked);
|
|
2070
2625
|
debugRing.push({
|
|
@@ -2145,24 +2700,6 @@ function errorInfoOf(result) {
|
|
|
2145
2700
|
function nonEmptyString(value) {
|
|
2146
2701
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2147
2702
|
}
|
|
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
|
-
}
|
|
2166
2703
|
/** 把内容块里的文本拼起来。 */
|
|
2167
2704
|
function textOf(content) {
|
|
2168
2705
|
if (!Array.isArray(content)) return "";
|