@eddyskywalker/dsh-chatgpt-subscription 0.62.0 → 0.63.1
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 +38 -0
- package/lib/client.js.map +1 -1
- package/lib/codemode.js +939 -326
- package/lib/index.js +663 -342
- 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 +16 -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/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,72 +1403,441 @@ function validateEscalationArgs(sandboxPermissions, justification) {
|
|
|
1504
1403
|
justification
|
|
1505
1404
|
};
|
|
1506
1405
|
}
|
|
1507
|
-
/**
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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;
|
|
1516
1433
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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;
|
|
1526
1489
|
return {
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
timeoutMs,
|
|
1530
|
-
...escalation
|
|
1490
|
+
title: title ?? message.slice(0, 40),
|
|
1491
|
+
message: message ?? title
|
|
1531
1492
|
};
|
|
1532
1493
|
}
|
|
1533
|
-
/**
|
|
1534
|
-
function
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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);
|
|
1541
1520
|
}
|
|
1542
|
-
/**
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1521
|
+
/**
|
|
1522
|
+
* 参数文本像不像一次**被切坏**的工具调用。
|
|
1523
|
+
*
|
|
1524
|
+
* 审计里量到的真实形态(`opendesign/deepseek-v4.1-flash-eco`):
|
|
1525
|
+
* `{"code_badge_none">\n</||DSML||: "…", "code": "<完整程序>"}` —— 对象头被网关或模型的
|
|
1526
|
+
* DSML 残留污染,但 `"code"` 的值本身是好的。这种文本如果落到「纯程序文本」兜底,会被整段
|
|
1527
|
+
* 当源码编译,报一个与真实原因无关的语法错,还丢掉那段能跑的程序。
|
|
1528
|
+
*
|
|
1529
|
+
* 只认两种证据:以 `{"` 开头的对象形态(合法游离程序不会这样开头),或正文里带 DSML 残留。
|
|
1530
|
+
* 不用泛化的 `</` 判据——一段真程序里写 HTML 字符串是正常的。
|
|
1531
|
+
*
|
|
1532
|
+
* @param text - 已裁剪首尾空白的参数文本。
|
|
1533
|
+
* @returns 是否按「损坏负载」处理。
|
|
1534
|
+
*/
|
|
1535
|
+
function looksDamagedArguments(text) {
|
|
1536
|
+
return text.startsWith("{\"") || text.includes("DSML") || text.includes("|");
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* 从一段不保证合法的文本里取出一个 JSON 字符串成员的值。
|
|
1540
|
+
*
|
|
1541
|
+
* 键名必须带引号(`"code": "…"`),值必须是**完整闭合**的 JSON 字符串字面量;扫描器按
|
|
1542
|
+
* JSON 的转义规则走,遇到未转义的 `"` 才收尾,再从该字面量 `JSON.parse` 解码,
|
|
1543
|
+
* 因此不会被值内部的引号或反斜杠骗到。
|
|
1544
|
+
*
|
|
1545
|
+
* @param text - 待扫描的文本。
|
|
1546
|
+
* @param key - 成员键名(不带引号)。
|
|
1547
|
+
* @returns 解码后的字符串,或找不到 / 解不开时为 `undefined`。
|
|
1548
|
+
*/
|
|
1549
|
+
function splicedMember(text, key) {
|
|
1550
|
+
const needle = "\"" + key + "\"";
|
|
1551
|
+
let from = 0;
|
|
1552
|
+
for (;;) {
|
|
1553
|
+
const at = text.indexOf(needle, from);
|
|
1554
|
+
if (at < 0) return void 0;
|
|
1555
|
+
from = at + needle.length;
|
|
1556
|
+
let index = from;
|
|
1557
|
+
while (index < text.length && /\s/.test(text.charAt(index))) index += 1;
|
|
1558
|
+
if (text.charAt(index) !== ":") continue;
|
|
1559
|
+
index += 1;
|
|
1560
|
+
while (index < text.length && /\s/.test(text.charAt(index))) index += 1;
|
|
1561
|
+
if (text.charAt(index) !== "\"") continue;
|
|
1562
|
+
const start = index;
|
|
1563
|
+
index += 1;
|
|
1564
|
+
let escaped = false;
|
|
1565
|
+
while (index < text.length) {
|
|
1566
|
+
const ch = text.charAt(index);
|
|
1567
|
+
if (escaped) escaped = false;
|
|
1568
|
+
else if (ch === "\\") escaped = true;
|
|
1569
|
+
else if (ch === "\"") {
|
|
1570
|
+
const value = tryJson(text.slice(start, index + 1));
|
|
1571
|
+
if (typeof value === "string") return value;
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
index += 1;
|
|
1575
|
+
}
|
|
1549
1576
|
}
|
|
1550
1577
|
}
|
|
1578
|
+
/** 参数是「被切坏的 JSON」时的错误:说明形态,并把下一步写清楚。 */
|
|
1579
|
+
function damagedArgumentsError(args, raw) {
|
|
1580
|
+
return /* @__PURE__ */ new Error(invalidArgumentsError(args, raw).message + "; the payload looks damaged (embedded tool-call markup or a truncated JSON object) — resend one complete call with \"code\" holding the program source");
|
|
1581
|
+
}
|
|
1582
|
+
/** 从源码首行兜底出一句描述。 */
|
|
1583
|
+
function descriptionFromCode(code) {
|
|
1584
|
+
const cleaned = (code.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0) ?? "").replace(/^(\/\/|#)\s*/, "").slice(0, 80);
|
|
1585
|
+
return cleaned.length > 0 ? cleaned : "code program";
|
|
1586
|
+
}
|
|
1551
1587
|
/**
|
|
1552
|
-
*
|
|
1588
|
+
* 把工具收到的原始参数读成一个对象。
|
|
1553
1589
|
*
|
|
1554
|
-
*
|
|
1555
|
-
* 可以并发重叠,最多 `maxParallel` 个同时在飞;
|
|
1556
|
-
* - `commit()`(记 settle 事件 + 转发图片/附加上下文 + `concludeTurn` + 放行互斥屏障)
|
|
1557
|
-
* 按**提交顺序**逐条执行:前一条没提交完,后一条的结果不会先落到会话里;
|
|
1558
|
-
* - `exclusive` 调用等所有在飞 body 结算后独占运行,并独占到自己 commit 完成才放行后续;
|
|
1559
|
-
* - 排队期间若外层运行已中止,直接放弃且不启动任务(未启动调用零 `start` 事件);
|
|
1560
|
-
* - `drain()` 等到 lane 彻底静止(所有 body 结算、所有 commit 完成)才返回。
|
|
1590
|
+
* ## 为什么要容错
|
|
1561
1591
|
*
|
|
1562
|
-
*
|
|
1592
|
+
* 模型有四种真实存在的发送形态,照搬内核 PTC 的严格 schema 会把它们全判死:
|
|
1563
1593
|
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1594
|
+
* 1. **原生对象**(正常);
|
|
1595
|
+
* 2. **字符串里的 JSON**(执行器把解析失败的文本原样传下来);
|
|
1596
|
+
* 3. **两段 JSON 拼接**——`{"title":…,"message":…}{"code":…}`,模型想在同一次调用里
|
|
1597
|
+
* 既汇报又执行,整段解析必失败,但按花括号切开后两段都合法(实测 7/7 可救);
|
|
1598
|
+
* 4. **只有一段纯程序**(模型直接把源码当参数发)。
|
|
1599
|
+
*
|
|
1600
|
+
* @param args - 工具收到的原始参数。
|
|
1601
|
+
* @returns 参数对象、可选的简报与来源形态。
|
|
1567
1602
|
*/
|
|
1568
|
-
function
|
|
1569
|
-
|
|
1570
|
-
|
|
1603
|
+
function parseArguments(args) {
|
|
1604
|
+
if (isRecord(args)) return {
|
|
1605
|
+
record: args,
|
|
1606
|
+
origin: "object",
|
|
1607
|
+
raw: ""
|
|
1608
|
+
};
|
|
1609
|
+
if (typeof args !== "string") throw invalidArgumentsError(args, "");
|
|
1610
|
+
const raw = args;
|
|
1611
|
+
const trimmed = raw.trim();
|
|
1612
|
+
if (trimmed.length === 0) throw invalidArgumentsError(args, raw);
|
|
1613
|
+
const parsed = tryJson(trimmed);
|
|
1614
|
+
if (isRecord(parsed)) {
|
|
1615
|
+
const narration = narrationOf(parsed);
|
|
1616
|
+
return {
|
|
1617
|
+
record: parsed,
|
|
1618
|
+
origin: "json",
|
|
1619
|
+
raw,
|
|
1620
|
+
...narration === void 0 ? {} : { narration }
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
if (Array.isArray(parsed)) {
|
|
1624
|
+
const hit = parsed.find((entry) => isRecord(entry) && codeOf(entry) !== void 0);
|
|
1625
|
+
if (isRecord(hit)) {
|
|
1626
|
+
const narration = narrationOf(hit);
|
|
1627
|
+
return {
|
|
1628
|
+
record: hit,
|
|
1629
|
+
origin: "json",
|
|
1630
|
+
raw,
|
|
1631
|
+
...narration === void 0 ? {} : { narration }
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
const fragments = topLevelJsonObjects(trimmed);
|
|
1636
|
+
let chosen;
|
|
1637
|
+
let narration;
|
|
1638
|
+
for (const fragment of fragments) {
|
|
1639
|
+
const value = tryJson(fragment);
|
|
1640
|
+
if (!isRecord(value)) continue;
|
|
1641
|
+
if (codeOf(value) !== void 0) {
|
|
1642
|
+
chosen = value;
|
|
1643
|
+
narration ??= narrationOf(value);
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
narration ??= narrationOf(value);
|
|
1647
|
+
}
|
|
1648
|
+
if (chosen !== void 0) return {
|
|
1649
|
+
record: chosen,
|
|
1650
|
+
origin: fragments.length > 1 ? "fused" : "json",
|
|
1651
|
+
raw,
|
|
1652
|
+
...narration === void 0 ? {} : { narration }
|
|
1653
|
+
};
|
|
1654
|
+
if (looksDamagedArguments(trimmed)) {
|
|
1655
|
+
const code = splicedMember(trimmed, "code");
|
|
1656
|
+
if (code !== void 0 && code.trim().length > 0) {
|
|
1657
|
+
const description = stringOf(splicedMember(trimmed, "description"))?.trim() ?? stringOf(splicedMember(trimmed, "summary"))?.trim();
|
|
1658
|
+
return {
|
|
1659
|
+
record: {
|
|
1660
|
+
code,
|
|
1661
|
+
...description === void 0 ? {} : { description }
|
|
1662
|
+
},
|
|
1663
|
+
origin: "spliced",
|
|
1664
|
+
raw
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
throw damagedArgumentsError(args, raw);
|
|
1668
|
+
}
|
|
1669
|
+
if (looksLikeProgram(trimmed)) return {
|
|
1670
|
+
record: { code: raw },
|
|
1671
|
+
origin: "bare",
|
|
1672
|
+
raw
|
|
1673
|
+
};
|
|
1674
|
+
throw invalidArgumentsError(args, raw, keysOf(fragments));
|
|
1675
|
+
}
|
|
1676
|
+
/** 把若干 JSON 片段里出现过的键并起来(错误回显用)。 */
|
|
1677
|
+
function keysOf(fragments) {
|
|
1678
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1679
|
+
for (const fragment of fragments) {
|
|
1680
|
+
const value = tryJson(fragment);
|
|
1681
|
+
if (!isRecord(value)) continue;
|
|
1682
|
+
for (const key of Object.keys(value)) keys.add(key);
|
|
1683
|
+
}
|
|
1684
|
+
return [...keys];
|
|
1685
|
+
}
|
|
1686
|
+
/** 校验并读出工具参数。 */
|
|
1687
|
+
function readArguments(args) {
|
|
1688
|
+
const parsed = parseArguments(args);
|
|
1689
|
+
const record = parsed.record;
|
|
1690
|
+
const code = codeOf(record);
|
|
1691
|
+
if (code === void 0) throw missingCodeError(record, parsed.raw);
|
|
1692
|
+
const description = stringOf(record["description"])?.trim() ?? stringOf(record["desc"])?.trim() ?? stringOf(record["summary"])?.trim() ?? parsed.narration?.title ?? descriptionFromCode(code);
|
|
1693
|
+
const escalation = validateEscalationArgs(record["sandbox_permissions"], record["justification"]);
|
|
1694
|
+
const timeoutMs = record["timeoutMs"];
|
|
1695
|
+
if (timeoutMs !== void 0) {
|
|
1696
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("codemode: invalid timeoutMs: expected a positive finite number");
|
|
1697
|
+
}
|
|
1698
|
+
return {
|
|
1699
|
+
code,
|
|
1700
|
+
description,
|
|
1701
|
+
...timeoutMs === void 0 ? {} : { timeoutMs },
|
|
1702
|
+
...escalation,
|
|
1703
|
+
...parsed.narration === void 0 ? {} : { narration: parsed.narration }
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* 把救回来的进展简报补发成一次 `progress_update`。
|
|
1708
|
+
*
|
|
1709
|
+
* 简报只影响界面,因此这里的任何失败都被吞掉:发不出去(没有投影、没有服务端、外层已取消)
|
|
1710
|
+
* 绝不能连累这次程序。
|
|
1711
|
+
*
|
|
1712
|
+
* @param host - 运行环境句柄。
|
|
1713
|
+
* @param narration - 从融合参数里救回来的简报。
|
|
1714
|
+
* @param exec - 本次执行上下文。
|
|
1715
|
+
* @param signal - 已带嵌套标记的取消信号。
|
|
1716
|
+
*/
|
|
1717
|
+
async function forwardNarration(host, narration, exec, signal) {
|
|
1718
|
+
try {
|
|
1719
|
+
if (!host.catalog().some((entry) => entry.name === PROGRESS_UPDATE_TOOL)) return;
|
|
1720
|
+
await host.callTool({
|
|
1721
|
+
name: PROGRESS_UPDATE_TOOL,
|
|
1722
|
+
args: {
|
|
1723
|
+
title: narration.title,
|
|
1724
|
+
message: narration.message
|
|
1725
|
+
},
|
|
1726
|
+
callId: String(exec.callId) + ":ptc:0",
|
|
1727
|
+
signal,
|
|
1728
|
+
agent: exec.agent,
|
|
1729
|
+
toolSet: exec.toolSet
|
|
1730
|
+
});
|
|
1731
|
+
} catch {}
|
|
1732
|
+
}
|
|
1733
|
+
/** 子调用参数必须是可无损 JSON 化的值。 */
|
|
1734
|
+
function losslessArgs(value) {
|
|
1735
|
+
if (value === void 0) return {};
|
|
1736
|
+
try {
|
|
1737
|
+
return JSON.parse(JSON.stringify(value));
|
|
1738
|
+
} catch {
|
|
1739
|
+
throw new Error("codemode: tool arguments must be lossless JSON");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
/** 内置按单文件 `file_path` 寻址的工具名。 */
|
|
1743
|
+
const FILE_PATH_TOOLS = /* @__PURE__ */ new Set([
|
|
1744
|
+
"read",
|
|
1745
|
+
"edit",
|
|
1746
|
+
"write",
|
|
1747
|
+
"read_image"
|
|
1748
|
+
]);
|
|
1749
|
+
/** 内置按目录/目标 `path` 寻址的检索工具名。 */
|
|
1750
|
+
const SEARCH_PATH_TOOLS = /* @__PURE__ */ new Set(["glob", "grep"]);
|
|
1751
|
+
/**
|
|
1752
|
+
* 宿主层子调用入参别名容错:把模型在 `code_run` 内高频写错的参数键名映射到工具真实声明的键名。
|
|
1753
|
+
*
|
|
1754
|
+
* 审计证据(`session-e190c243` seq 171-182 等):模型在 `Promise.all` 里容易把 `tools.read` 的
|
|
1755
|
+
* `file_path` 写成 `glob`/`grep` 的 `path`(或反过来),或者把 snake_case 写成 camelCase,导致整段
|
|
1756
|
+
* 程序因单个属性名不匹配而抛 `ToolCallError: invalid arguments: missing required property "file_path"`。
|
|
1757
|
+
*
|
|
1758
|
+
* 规则:仅当目标规范键尚未提供(`=== undefined`)、别名键存在且类型匹配、且别名键本身不是该工具声明的
|
|
1759
|
+
* 独立参数时,才把值搬运到规范键并删掉多余别名键(防止 `additionalProperties: false` 拒收)。
|
|
1760
|
+
*
|
|
1761
|
+
* @param entry - 工具清单条目(含参数 schema)。
|
|
1762
|
+
* @param value - 已脱离引用的 JSON 参数值。
|
|
1763
|
+
* @returns 归一化后的参数值。
|
|
1764
|
+
*/
|
|
1765
|
+
function normalizeSubCallArgs(entry, value) {
|
|
1766
|
+
if (!isRecord(value)) return value;
|
|
1767
|
+
const name = entry?.name ?? "";
|
|
1768
|
+
const schemaProps = isRecord(entry?.parameters) && isRecord(entry.parameters.properties) ? entry.parameters.properties : void 0;
|
|
1769
|
+
const declares = (key) => schemaProps !== void 0 && Object.prototype.hasOwnProperty.call(schemaProps, key);
|
|
1770
|
+
const out = { ...value };
|
|
1771
|
+
const remap = (target, aliases, predicate) => {
|
|
1772
|
+
if (out[target] !== void 0) return;
|
|
1773
|
+
for (const alias of aliases) {
|
|
1774
|
+
if (declares(alias)) continue;
|
|
1775
|
+
const candidate = out[alias];
|
|
1776
|
+
if (predicate(candidate)) {
|
|
1777
|
+
out[target] = candidate;
|
|
1778
|
+
delete out[alias];
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
const isString = (v) => typeof v === "string";
|
|
1784
|
+
const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1785
|
+
const isBoolean = (v) => typeof v === "boolean";
|
|
1786
|
+
if (FILE_PATH_TOOLS.has(name) || declares("file_path") && !declares("path")) remap("file_path", ["path", "filePath"], isString);
|
|
1787
|
+
else if (SEARCH_PATH_TOOLS.has(name) || declares("path") && !declares("file_path")) remap("path", ["file_path", "filePath"], isString);
|
|
1788
|
+
if (name === "edit" || declares("old_string") || declares("new_string")) {
|
|
1789
|
+
remap("old_string", ["oldString"], isString);
|
|
1790
|
+
remap("new_string", ["newString"], isString);
|
|
1791
|
+
remap("replace_all", ["replaceAll"], isBoolean);
|
|
1792
|
+
}
|
|
1793
|
+
if (name === "win_bash" || declares("workdir")) {
|
|
1794
|
+
remap("workdir", [
|
|
1795
|
+
"cwd",
|
|
1796
|
+
"workingDirectory",
|
|
1797
|
+
"working_directory"
|
|
1798
|
+
], isString);
|
|
1799
|
+
remap("timeoutMs", ["timeout_ms"], isNumber);
|
|
1800
|
+
remap("run_in_background", ["runInBackground"], isBoolean);
|
|
1801
|
+
}
|
|
1802
|
+
if (name === "job_output" || name === "job_kill" || declares("job_id")) {
|
|
1803
|
+
remap("job_id", ["jobId"], isString);
|
|
1804
|
+
if (name === "job_output" || declares("timeout_ms")) remap("timeout_ms", ["timeoutMs"], isNumber);
|
|
1805
|
+
}
|
|
1806
|
+
return out;
|
|
1807
|
+
}
|
|
1808
|
+
/** 一个工具的调用是否可以与兄弟调用重叠(只有明确 true 才算)。 */
|
|
1809
|
+
function isConcurrencySafe(entry, args) {
|
|
1810
|
+
if (entry === void 0 || typeof entry.isConcurrencySafe !== "function") return false;
|
|
1811
|
+
try {
|
|
1812
|
+
return entry.isConcurrencySafe(args) === true;
|
|
1813
|
+
} catch {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* 子调用的并发、提交顺序与互斥屏障(对齐内核 `ptc.ts` 的 ordered commit lane + `logWork` 背压)。
|
|
1819
|
+
*
|
|
1820
|
+
* - `classify()` 在队列头部真正出队启动时惰性求值,保证前序调用产生的状态变更可被观察;
|
|
1821
|
+
* - `start()`(记 start 事件 + 启动 body)在 lane 里按提交顺序执行:`parallel` body 之间
|
|
1822
|
+
* 可以并发重叠,最多 `maxParallel` 个同时在飞;
|
|
1823
|
+
* - `commit()`(转发图片/附加上下文 + `concludeTurn` + 放行互斥屏障)按**提交顺序**逐条执行,
|
|
1824
|
+
* 并在同步完成上下文挂载后**立刻**将结果返回给程序;`shapeDispatchLog` 与 settle 事件作为
|
|
1825
|
+
* 有序后台 `logWork` 执行,超出 `maxParallel` 时在提交车道施加背压;
|
|
1826
|
+
* - `exclusive` 调用等所有在飞 body 结算后独占运行,并独占到自己 commit 完成才放行后续;
|
|
1827
|
+
* - 排队期间若外层运行已中止,直接放弃且不启动任务(未启动调用零 `start` 事件);
|
|
1828
|
+
* - `drain()` 等到 lane 彻底静止且所有后台 `logWork` 落盘完成才返回。
|
|
1829
|
+
*
|
|
1830
|
+
* @param maxParallel - 同时在飞的上限(同时作为 `logWork` 背压上限)。
|
|
1831
|
+
* @param isAborted - 外层运行是否已中止。
|
|
1832
|
+
* @returns 调度入口与排空。
|
|
1833
|
+
*/
|
|
1834
|
+
function createCommitLane(maxParallel, isAborted) {
|
|
1835
|
+
const cap = Math.max(1, maxParallel);
|
|
1836
|
+
const queue = [];
|
|
1571
1837
|
const commitQueue = [];
|
|
1572
1838
|
const inFlight = /* @__PURE__ */ new Set();
|
|
1839
|
+
const logWork = /* @__PURE__ */ new Set();
|
|
1840
|
+
let logTail = Promise.resolve();
|
|
1573
1841
|
let exclusiveActive = false;
|
|
1574
1842
|
let driving = false;
|
|
1575
1843
|
let driverRun = Promise.resolve();
|
|
@@ -1579,6 +1847,13 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1579
1847
|
wake = void 0;
|
|
1580
1848
|
release?.();
|
|
1581
1849
|
};
|
|
1850
|
+
const enqueueLog = (work) => {
|
|
1851
|
+
logTail = logTail.then(work, work).catch(() => void 0);
|
|
1852
|
+
const tracked = logTail.finally(() => {
|
|
1853
|
+
logWork.delete(tracked);
|
|
1854
|
+
});
|
|
1855
|
+
logWork.add(tracked);
|
|
1856
|
+
};
|
|
1582
1857
|
/**
|
|
1583
1858
|
* 唯一的有序车道:每一轮先提交队首已结算的调用,再在有空位时启动下一个排队调用,
|
|
1584
1859
|
* 否则睡到「有 body 结算或有新提交」为止。三条队列全空即静止。
|
|
@@ -1606,8 +1881,10 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1606
1881
|
head.abandon();
|
|
1607
1882
|
continue;
|
|
1608
1883
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1884
|
+
const mode = head.classify();
|
|
1885
|
+
if (!exclusiveActive && (mode === "exclusive" ? inFlight.size === 0 : inFlight.size < cap)) {
|
|
1886
|
+
head.mode = mode;
|
|
1887
|
+
if (mode === "exclusive") exclusiveActive = true;
|
|
1611
1888
|
queue.shift();
|
|
1612
1889
|
commitQueue.push(head);
|
|
1613
1890
|
head.start();
|
|
@@ -1630,11 +1907,18 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1630
1907
|
return driverRun;
|
|
1631
1908
|
};
|
|
1632
1909
|
return {
|
|
1633
|
-
schedule(
|
|
1910
|
+
schedule(task) {
|
|
1634
1911
|
return new Promise((resolve, reject) => {
|
|
1635
1912
|
let parked;
|
|
1636
1913
|
const entry = {
|
|
1637
|
-
mode,
|
|
1914
|
+
mode: "exclusive",
|
|
1915
|
+
classify() {
|
|
1916
|
+
try {
|
|
1917
|
+
return task.classify ? task.classify() : "exclusive";
|
|
1918
|
+
} catch {
|
|
1919
|
+
return "exclusive";
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1638
1922
|
settled: false,
|
|
1639
1923
|
flight: Promise.resolve(),
|
|
1640
1924
|
start() {
|
|
@@ -1659,10 +1943,11 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1659
1943
|
const result = parked;
|
|
1660
1944
|
if (result === void 0) return;
|
|
1661
1945
|
try {
|
|
1662
|
-
resolve(await task.commit(result));
|
|
1946
|
+
resolve(await task.commit(result, enqueueLog));
|
|
1663
1947
|
} catch (error) {
|
|
1664
1948
|
reject(error);
|
|
1665
1949
|
}
|
|
1950
|
+
while (logWork.size > cap) await Promise.race(logWork);
|
|
1666
1951
|
},
|
|
1667
1952
|
abandon() {
|
|
1668
1953
|
try {
|
|
@@ -1679,6 +1964,7 @@ function createCommitLane(maxParallel, isAborted) {
|
|
|
1679
1964
|
},
|
|
1680
1965
|
async drain() {
|
|
1681
1966
|
await drive();
|
|
1967
|
+
while (logWork.size > 0) await Promise.allSettled([...logWork]);
|
|
1682
1968
|
}
|
|
1683
1969
|
};
|
|
1684
1970
|
}
|
|
@@ -1697,7 +1983,7 @@ async function shapeDispatchContent(host, dispatch) {
|
|
|
1697
1983
|
}
|
|
1698
1984
|
}
|
|
1699
1985
|
/** 一个抛出值的消息。 */
|
|
1700
|
-
function messageOf$
|
|
1986
|
+
function messageOf$2(error) {
|
|
1701
1987
|
return error instanceof Error ? error.message : String(error);
|
|
1702
1988
|
}
|
|
1703
1989
|
const JSON_INDENT = " ";
|
|
@@ -1790,6 +2076,358 @@ function renderProgramValue(value) {
|
|
|
1790
2076
|
return chunks.join("");
|
|
1791
2077
|
}
|
|
1792
2078
|
//#endregion
|
|
2079
|
+
//#region src/host/codemode/retention.ts
|
|
2080
|
+
/**
|
|
2081
|
+
* 常驻执行环境的生命周期与会话级调用协调(对齐内核 `ptc.ts` 与 `PtcCalls`)。
|
|
2082
|
+
*
|
|
2083
|
+
* 部署把 `@deepseek-ai/dsh-ptc-runtime-node` 配成 `executionMode: persistent-in-memory` 时,
|
|
2084
|
+
* 同一个 agent 的多次 `code_run` 会共用一个常驻 REPL:顶层变量、函数与 `import` 的模块
|
|
2085
|
+
* 跨调用存活。可以不去用,但**不能只注册不释放**——常驻环境没有空闲过期,忘了释放会让子进程
|
|
2086
|
+
* 活到进程结束。
|
|
2087
|
+
*
|
|
2088
|
+
* 所以这里把「传了 owner」与「注册了释放」写进同一个动作:结构性无法表达「传了却没释放」。
|
|
2089
|
+
* 清理有三条路,覆盖所有生命周期终点与显式重置:
|
|
2090
|
+
*
|
|
2091
|
+
* 1. 挂在 **agent.ctx** 上的释放(对齐 `ptc.ts` 的 `stateOwners`):展开 Cordis 包装链后按原初
|
|
2092
|
+
* runtime provider 去重登记 `agent.ctx.effect`,即使本包早已卸载,agent 销毁也会释放它的环境;
|
|
2093
|
+
* 2. 挂在**插件**上的 teardown:卸载或热重载本包时先中止并排空在飞调用,再释放所有遗留环境;
|
|
2094
|
+
* 3. 会话级 `PtcCalls` 协调:新调用进入前等待同会话已取消的前序调用与进行中的 `reset` 彻底排空,
|
|
2095
|
+
* 避免前一次取消的收尾清理撞上后一次调用的 `assertIdle()`。
|
|
2096
|
+
*
|
|
2097
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/retention
|
|
2098
|
+
*/
|
|
2099
|
+
/** Cordis 服务包装链指向原初实现的符号描述。 */
|
|
2100
|
+
const CORDIS_ORIGINAL_DESCRIPTION = "cordis.original";
|
|
2101
|
+
/**
|
|
2102
|
+
* 造一个常驻环境与会话调用协调管理器。
|
|
2103
|
+
*
|
|
2104
|
+
* @param runtimeOf - 读当前运行时的函数(每次调用重读,运行时可能晚于本插件挂载)。
|
|
2105
|
+
* @returns 管理句柄。
|
|
2106
|
+
*/
|
|
2107
|
+
function createRetention(runtimeOf) {
|
|
2108
|
+
const live = /* @__PURE__ */ new Set();
|
|
2109
|
+
const released = /* @__PURE__ */ new WeakSet();
|
|
2110
|
+
const stateOwners = /* @__PURE__ */ new WeakMap();
|
|
2111
|
+
const calls = /* @__PURE__ */ new Map();
|
|
2112
|
+
const resets = /* @__PURE__ */ new Map();
|
|
2113
|
+
let disposed = false;
|
|
2114
|
+
const release = async (owner, options) => {
|
|
2115
|
+
const strict = options?.strict === true;
|
|
2116
|
+
if (!strict && released.has(owner)) return;
|
|
2117
|
+
released.add(owner);
|
|
2118
|
+
live.delete(owner);
|
|
2119
|
+
const runtime = runtimeOf();
|
|
2120
|
+
if (runtime?.release === void 0) return;
|
|
2121
|
+
try {
|
|
2122
|
+
await runtime.release(owner);
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
if (strict) {
|
|
2125
|
+
released.delete(owner);
|
|
2126
|
+
live.add(owner);
|
|
2127
|
+
throw error;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
/** 在 `agent.ctx` 上按原初 runtime provider 去重挂一次释放钩子(对齐 `ptc.ts:669-684`)。 */
|
|
2132
|
+
const bindAgentLifecycle = (owner) => {
|
|
2133
|
+
const runtime = runtimeOf();
|
|
2134
|
+
if (runtime === void 0 || typeof runtime !== "object" || runtime === null) return;
|
|
2135
|
+
const ctx = owner.ctx;
|
|
2136
|
+
if (typeof ctx?.effect !== "function") return;
|
|
2137
|
+
let runtimes = stateOwners.get(owner);
|
|
2138
|
+
if (runtimes === void 0) {
|
|
2139
|
+
runtimes = /* @__PURE__ */ new WeakSet();
|
|
2140
|
+
stateOwners.set(owner, runtimes);
|
|
2141
|
+
}
|
|
2142
|
+
const provider = unwrapOriginalProvider(runtime);
|
|
2143
|
+
if (runtimes.has(provider)) return;
|
|
2144
|
+
try {
|
|
2145
|
+
ctx.effect(() => () => release(owner), "PTC retained environment");
|
|
2146
|
+
runtimes.add(provider);
|
|
2147
|
+
} catch {}
|
|
2148
|
+
};
|
|
2149
|
+
return {
|
|
2150
|
+
owner(agent) {
|
|
2151
|
+
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
2152
|
+
released.delete(owner);
|
|
2153
|
+
live.add(owner);
|
|
2154
|
+
if (owner !== fallbackOwner) bindAgentLifecycle(owner);
|
|
2155
|
+
return owner;
|
|
2156
|
+
},
|
|
2157
|
+
release,
|
|
2158
|
+
async awaitPredecessors(agent, signal) {
|
|
2159
|
+
const key = sessionKeyOf(agent);
|
|
2160
|
+
if (key !== void 0) for (;;) {
|
|
2161
|
+
const reset = resets.get(key);
|
|
2162
|
+
if (reset !== void 0) {
|
|
2163
|
+
try {
|
|
2164
|
+
await reset.done;
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
throw new CodeRunFailedError("Program environment unavailable: " + messageOf$1(error) + ". Ask the host to reset it before running more code.");
|
|
2167
|
+
}
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
const pending = [...calls.get(key) ?? []].filter((call) => call.signal.aborted);
|
|
2171
|
+
if (pending.length === 0) break;
|
|
2172
|
+
await Promise.all(pending.map((call) => call.done));
|
|
2173
|
+
}
|
|
2174
|
+
if (signal.aborted) throw signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? "tool call aborted"));
|
|
2175
|
+
if (disposed) throw new Error("PTC tool executor disposed");
|
|
2176
|
+
},
|
|
2177
|
+
trackRun(run) {
|
|
2178
|
+
const key = sessionKeyOf(run.agent);
|
|
2179
|
+
if (key === void 0) return () => void 0;
|
|
2180
|
+
let bucket = calls.get(key);
|
|
2181
|
+
if (bucket === void 0) {
|
|
2182
|
+
bucket = /* @__PURE__ */ new Set();
|
|
2183
|
+
calls.set(key, bucket);
|
|
2184
|
+
}
|
|
2185
|
+
for (const existing of bucket) if (existing.controller === run.controller) {
|
|
2186
|
+
existing.mode = run.mode;
|
|
2187
|
+
return () => {
|
|
2188
|
+
const current = calls.get(key);
|
|
2189
|
+
if (current !== void 0) {
|
|
2190
|
+
current.delete(existing);
|
|
2191
|
+
if (current.size === 0) calls.delete(key);
|
|
2192
|
+
}
|
|
2193
|
+
existing.resolveDone();
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
let resolveDone = () => void 0;
|
|
2197
|
+
const done = new Promise((resolve) => {
|
|
2198
|
+
resolveDone = resolve;
|
|
2199
|
+
});
|
|
2200
|
+
const record = {
|
|
2201
|
+
controller: run.controller,
|
|
2202
|
+
signal: run.signal ?? run.controller.signal,
|
|
2203
|
+
...run.mode === void 0 ? {} : { mode: run.mode },
|
|
2204
|
+
done,
|
|
2205
|
+
resolveDone
|
|
2206
|
+
};
|
|
2207
|
+
bucket.add(record);
|
|
2208
|
+
return () => {
|
|
2209
|
+
const current = calls.get(key);
|
|
2210
|
+
if (current !== void 0) {
|
|
2211
|
+
current.delete(record);
|
|
2212
|
+
if (current.size === 0) calls.delete(key);
|
|
2213
|
+
}
|
|
2214
|
+
record.resolveDone();
|
|
2215
|
+
};
|
|
2216
|
+
},
|
|
2217
|
+
cancelOnModeChange(session, event) {
|
|
2218
|
+
if (typeof event !== "object" || event === null) return;
|
|
2219
|
+
const record = event;
|
|
2220
|
+
if (record.type !== "sandbox/mode") return;
|
|
2221
|
+
const mode = (typeof record.data === "object" && record.data !== null ? record.data : void 0)?.mode;
|
|
2222
|
+
if (typeof mode !== "string") return;
|
|
2223
|
+
const key = sessionKeyOf(session);
|
|
2224
|
+
if (key === void 0) return;
|
|
2225
|
+
for (const call of calls.get(key) ?? []) if (call.mode !== void 0 && call.mode !== mode) call.controller.abort(/* @__PURE__ */ new Error("sandbox mode changed"));
|
|
2226
|
+
},
|
|
2227
|
+
reset(agent, notify) {
|
|
2228
|
+
if (disposed) return Promise.reject(/* @__PURE__ */ new Error("PTC tool executor disposed"));
|
|
2229
|
+
const key = sessionKeyOf(agent) ?? fallbackOwner;
|
|
2230
|
+
const previous = resets.get(key);
|
|
2231
|
+
if (previous !== void 0 && !previous.settled) return previous.done;
|
|
2232
|
+
let resolveDone = () => void 0;
|
|
2233
|
+
let rejectDone = () => void 0;
|
|
2234
|
+
const done = new Promise((resolve, reject) => {
|
|
2235
|
+
resolveDone = resolve;
|
|
2236
|
+
rejectDone = reject;
|
|
2237
|
+
});
|
|
2238
|
+
done.catch(() => void 0);
|
|
2239
|
+
const resetRecord = {
|
|
2240
|
+
done,
|
|
2241
|
+
settled: false
|
|
2242
|
+
};
|
|
2243
|
+
resets.set(key, resetRecord);
|
|
2244
|
+
const active = [...calls.get(key) ?? []];
|
|
2245
|
+
for (const call of active) call.controller.abort(/* @__PURE__ */ new Error("PTC environment reset"));
|
|
2246
|
+
const owner = typeof agent === "object" && agent !== null ? agent : fallbackOwner;
|
|
2247
|
+
Promise.allSettled([Promise.resolve().then(() => release(owner, { strict: true })), ...active.map((call) => call.done)]).then((results) => {
|
|
2248
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
2249
|
+
if (failure !== void 0 && failure.status === "rejected") throw failure.reason;
|
|
2250
|
+
notify?.();
|
|
2251
|
+
}).then(() => {
|
|
2252
|
+
resetRecord.settled = true;
|
|
2253
|
+
resets.delete(key);
|
|
2254
|
+
resolveDone();
|
|
2255
|
+
}, (error) => {
|
|
2256
|
+
resetRecord.settled = true;
|
|
2257
|
+
rejectDone(error);
|
|
2258
|
+
});
|
|
2259
|
+
return done;
|
|
2260
|
+
},
|
|
2261
|
+
async dispose() {
|
|
2262
|
+
disposed = true;
|
|
2263
|
+
const activeCalls = [...calls.values()].flatMap((bucket) => [...bucket]);
|
|
2264
|
+
for (const call of activeCalls) call.controller.abort(/* @__PURE__ */ new Error("PTC tool executor disposed"));
|
|
2265
|
+
await Promise.allSettled([...activeCalls.map((call) => call.done), ...[...resets.values()].map((reset) => reset.done)]);
|
|
2266
|
+
const owners = [...live];
|
|
2267
|
+
await Promise.all(owners.map((owner) => release(owner)));
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* 展开 Cordis 服务代理上的 `[symbols.original]` 链,定位原初 provider 对象。
|
|
2273
|
+
*/
|
|
2274
|
+
function unwrapOriginalProvider(runtime) {
|
|
2275
|
+
let current = runtime;
|
|
2276
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
2277
|
+
let next;
|
|
2278
|
+
for (const sym of Object.getOwnPropertySymbols(current)) if (sym.description === CORDIS_ORIGINAL_DESCRIPTION) {
|
|
2279
|
+
next = current[sym];
|
|
2280
|
+
break;
|
|
2281
|
+
}
|
|
2282
|
+
if (typeof next !== "object" || next === null || next === current) break;
|
|
2283
|
+
current = next;
|
|
2284
|
+
}
|
|
2285
|
+
return current;
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* 一个会话的身份键(优先用 `session.id`,无 id 时退回会话对象本身)。
|
|
2289
|
+
*/
|
|
2290
|
+
function sessionKeyOf(value) {
|
|
2291
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2292
|
+
const nested = value.session;
|
|
2293
|
+
const session = typeof nested === "object" && nested !== null ? nested : value;
|
|
2294
|
+
const id = session.id;
|
|
2295
|
+
if (typeof id === "string" && id.length > 0) return id;
|
|
2296
|
+
return session;
|
|
2297
|
+
}
|
|
2298
|
+
/** 一个抛出值的消息。 */
|
|
2299
|
+
function messageOf$1(error) {
|
|
2300
|
+
return error instanceof Error ? error.message : String(error);
|
|
2301
|
+
}
|
|
2302
|
+
/** 没有 agent 的调用(例如服务级直调)共用的 owner。 */
|
|
2303
|
+
const fallbackOwner = Object.freeze({ kind: "codemode-sessionless" });
|
|
2304
|
+
//#endregion
|
|
2305
|
+
//#region src/host/codemode/scope.ts
|
|
2306
|
+
/**
|
|
2307
|
+
* 工具归属判定:一个名字是**全局层**注册的,还是**本预设**注册的。
|
|
2308
|
+
*
|
|
2309
|
+
* ## 为什么要这么绕
|
|
2310
|
+
*
|
|
2311
|
+
* 注册表没有公开「某个工具注册在哪个作用域」的 API。`@deepseek-ai/dsh-scope` 把作用域标签
|
|
2312
|
+
* 写在一个模块私有的 `Symbol('dsh.scope')` 上,链关系(parent)也在私有 WeakMap 里,
|
|
2313
|
+
* 都不导出。能用的只有两件事:
|
|
2314
|
+
*
|
|
2315
|
+
* 1. 本包自己所在作用域的 key —— 按符号描述从 ctx 上扫出来({@link findScopeKey});
|
|
2316
|
+
* 2. 公开的 `ctx.tools.get(name)`(全局视图)与 `ctx.tools.get(name, key)`(该作用域视图)。
|
|
2317
|
+
*
|
|
2318
|
+
* 于是归属判定变成两次公开查询:
|
|
2319
|
+
*
|
|
2320
|
+
* - 全局视图能解析 → 全局层(MCP 服务端、profile 级 bundle);
|
|
2321
|
+
* - 只有本预设视图能解析 → 本预设自己注册的;
|
|
2322
|
+
* - 两处都解析不到 → 只可能注册在本预设之下(agent 层)。
|
|
2323
|
+
*
|
|
2324
|
+
* 探测失败(本包被挂在 host 层而不是预设里)时返回 `enabled: false`:规则整体降级为
|
|
2325
|
+
* 纯名字匹配,绝不因为读不到归属就误遮蔽。
|
|
2326
|
+
*
|
|
2327
|
+
* @module @eddyskywalker/dsh-chatgpt-subscription/host/codemode/scope
|
|
2328
|
+
*/
|
|
2329
|
+
/** 内核写作用域标签用的符号描述。 */
|
|
2330
|
+
const SCOPE_SYMBOL_DESCRIPTION = "dsh.scope";
|
|
2331
|
+
/**
|
|
2332
|
+
* 从一个 ctx 上找出「本包所在作用域」的 key。
|
|
2333
|
+
*
|
|
2334
|
+
* Cordis 的 scoped context 把作用域标签写成自有(或原型上)的一个 symbol 属性,
|
|
2335
|
+
* 描述是 `dsh.scope`。这里按描述扫,不猜符号本身——写法与参考实现一致,因为
|
|
2336
|
+
* 直接 `import` 内核的 scope 包在 profile 插件里解析不到。
|
|
2337
|
+
*
|
|
2338
|
+
* @param ctx - 本包的行上下文。
|
|
2339
|
+
* @returns 作用域 key;读不到时为 `undefined`。
|
|
2340
|
+
*/
|
|
2341
|
+
function findScopeKey(ctx) {
|
|
2342
|
+
if (typeof ctx !== "object" || ctx === null) return void 0;
|
|
2343
|
+
return scanForScope(ctx, 0);
|
|
2344
|
+
}
|
|
2345
|
+
/** 在对象自身及其原型链上找作用域标签(只看 symbol 属性,深度上限 4)。 */
|
|
2346
|
+
function scanForScope(source, depth) {
|
|
2347
|
+
if (depth > 4) return void 0;
|
|
2348
|
+
let symbols;
|
|
2349
|
+
try {
|
|
2350
|
+
symbols = Object.getOwnPropertySymbols(source);
|
|
2351
|
+
} catch {
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
for (const symbol of symbols) {
|
|
2355
|
+
if (symbol.description !== SCOPE_SYMBOL_DESCRIPTION) continue;
|
|
2356
|
+
let value;
|
|
2357
|
+
try {
|
|
2358
|
+
value = source[symbol];
|
|
2359
|
+
} catch {
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
if (typeof value === "object" && value !== null) return value;
|
|
2363
|
+
}
|
|
2364
|
+
const proto = Object.getPrototypeOf(source);
|
|
2365
|
+
if (proto === null || proto === Object.prototype) return void 0;
|
|
2366
|
+
return scanForScope(proto, depth + 1);
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* 为一个名字清单建立归属视图。
|
|
2370
|
+
*
|
|
2371
|
+
* @param tools - 工具注册表(结构探测,不看类身份)。
|
|
2372
|
+
* @param scopeKey - 本预设的作用域 key;`undefined` 表示探测失败。
|
|
2373
|
+
* @param names - 要判定的名字(通常是本步的模型可见清单)。
|
|
2374
|
+
* @returns 归属视图;读不到任何归属时返回禁用视图。
|
|
2375
|
+
*/
|
|
2376
|
+
function resolveCatalogView(tools, scopeKey, names) {
|
|
2377
|
+
const registry = asRegistry(tools);
|
|
2378
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
2379
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
2380
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
2381
|
+
for (const name of names) {
|
|
2382
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
2383
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
2384
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
2385
|
+
}
|
|
2386
|
+
return {
|
|
2387
|
+
enabled: true,
|
|
2388
|
+
globalNames,
|
|
2389
|
+
presetNames
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* 判定一个名字在**调用时**是否属于本预设之外的层。
|
|
2394
|
+
*
|
|
2395
|
+
* 拒绝路径(模型直呼被遮蔽的名字)没有当步清单可用,只能按同一个视图单独判一次;
|
|
2396
|
+
* 判定规则与 {@link resolveCatalogView} 完全一致,两处因此不可能给出互相矛盾的结论。
|
|
2397
|
+
*
|
|
2398
|
+
* @param tools - 工具注册表。
|
|
2399
|
+
* @param scopeKey - 本预设的作用域 key。
|
|
2400
|
+
* @param name - 工具名。
|
|
2401
|
+
* @returns 归属视图(只含这一个名字)。
|
|
2402
|
+
*/
|
|
2403
|
+
function resolveNameView(tools, scopeKey, name) {
|
|
2404
|
+
const registry = asRegistry(tools);
|
|
2405
|
+
if (scopeKey === void 0 || registry === void 0) return DISABLED_VIEW;
|
|
2406
|
+
const globalNames = /* @__PURE__ */ new Set();
|
|
2407
|
+
const presetNames = /* @__PURE__ */ new Set();
|
|
2408
|
+
if (probes(registry, name, void 0)) globalNames.add(name);
|
|
2409
|
+
if (probes(registry, name, scopeKey)) presetNames.add(name);
|
|
2410
|
+
return {
|
|
2411
|
+
enabled: true,
|
|
2412
|
+
globalNames,
|
|
2413
|
+
presetNames
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
/** 一次公开查询:解析得到定义时为 `true`。 */
|
|
2417
|
+
function probes(registry, name, scope) {
|
|
2418
|
+
try {
|
|
2419
|
+
return registry.get(name, scope) !== void 0;
|
|
2420
|
+
} catch {
|
|
2421
|
+
return false;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
/** 结构探测:读得到 `get` 就当注册表用。 */
|
|
2425
|
+
function asRegistry(tools) {
|
|
2426
|
+
if (typeof tools !== "object" || tools === null) return void 0;
|
|
2427
|
+
if (typeof tools.get !== "function") return void 0;
|
|
2428
|
+
return tools;
|
|
2429
|
+
}
|
|
2430
|
+
//#endregion
|
|
1793
2431
|
//#region src/entry-codemode.ts
|
|
1794
2432
|
/**
|
|
1795
2433
|
* Code Mode 预设行 —— 本包自己的「原生工具 + 程序传输」呈现方式。
|
|
@@ -1881,52 +2519,8 @@ function apply(ctx, config = {}) {
|
|
|
1881
2519
|
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1882
2520
|
retention.dispose();
|
|
1883
2521
|
}, "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
2522
|
if (typeof context.on === "function") try {
|
|
1929
|
-
const off = context.on("session/event", cancelOnModeChange, { global: true });
|
|
2523
|
+
const off = context.on("session/event", retention.cancelOnModeChange, { global: true });
|
|
1930
2524
|
if (typeof context.effect === "function") context.effect(() => () => {
|
|
1931
2525
|
if (typeof off === "function") off();
|
|
1932
2526
|
}, "codemode sandbox mode cancellation");
|
|
@@ -1953,10 +2547,27 @@ function apply(ctx, config = {}) {
|
|
|
1953
2547
|
};
|
|
1954
2548
|
const scopeKey = findScopeKey(ctx);
|
|
1955
2549
|
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
|
|
2550
|
+
/** 最近一次 describe 收到的完整定义集(含被遮蔽的),作为无 `tools.definitions` 时的回退快照。 */
|
|
1957
2551
|
let catalog = [];
|
|
1958
2552
|
const debugRing = [];
|
|
1959
2553
|
let firstDescribeLogged = false;
|
|
2554
|
+
/**
|
|
2555
|
+
* 解析当前调用方 agent 与步骤准入下的工具清单(对齐内核 `ptc.ts:662`)。
|
|
2556
|
+
*
|
|
2557
|
+
* 优先通过 `ctx.tools.definitions(agent, toolSet)` 实时读取调用方自己的可见集合(含
|
|
2558
|
+
* `agent.ctx` 专属工具与 `tools.restrict` 过滤),避免多会话并发时被另一个会话的
|
|
2559
|
+
* `describe()` 覆盖;测试替身未提供 `definitions` 时回退到最近一次 `catalog`。
|
|
2560
|
+
*/
|
|
2561
|
+
const resolveCatalog = (agent, toolSet) => {
|
|
2562
|
+
if (agent !== void 0 && typeof tools.definitions === "function") try {
|
|
2563
|
+
const defs = tools.definitions(agent, toolSet);
|
|
2564
|
+
if (Array.isArray(defs) && defs.length > 0) {
|
|
2565
|
+
const entries = defs.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
2566
|
+
if (entries.length > 0) return entries;
|
|
2567
|
+
}
|
|
2568
|
+
} catch {}
|
|
2569
|
+
return catalog;
|
|
2570
|
+
};
|
|
1960
2571
|
const escalateSandbox = async (input) => {
|
|
1961
2572
|
const record = typeof input.standingPolicy === "object" && input.standingPolicy !== null ? input.standingPolicy : void 0;
|
|
1962
2573
|
const effectiveMode = typeof record?.["mode"] === "string" ? record["mode"] : void 0;
|
|
@@ -1989,10 +2600,11 @@ function apply(ctx, config = {}) {
|
|
|
1989
2600
|
}
|
|
1990
2601
|
};
|
|
1991
2602
|
const host = {
|
|
1992
|
-
catalog: () =>
|
|
2603
|
+
catalog: (agent, toolSet) => resolveCatalog(agent, toolSet),
|
|
1993
2604
|
runtime: runtimeOf,
|
|
1994
2605
|
stateOwner: (agent) => retention.owner(agent),
|
|
1995
2606
|
releaseOwner: (owner) => retention.release(owner),
|
|
2607
|
+
awaitPredecessors: (agent, signal) => retention.awaitPredecessors(agent, signal),
|
|
1996
2608
|
runAuthority,
|
|
1997
2609
|
escalateSandbox,
|
|
1998
2610
|
createImageContext(content) {
|
|
@@ -2001,7 +2613,7 @@ function apply(ctx, config = {}) {
|
|
|
2001
2613
|
source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
|
|
2002
2614
|
});
|
|
2003
2615
|
},
|
|
2004
|
-
trackRun,
|
|
2616
|
+
trackRun: (run) => retention.trackRun(run),
|
|
2005
2617
|
shapeDispatchLog,
|
|
2006
2618
|
async callTool(request) {
|
|
2007
2619
|
const call = {
|
|
@@ -2065,6 +2677,25 @@ function apply(ctx, config = {}) {
|
|
|
2065
2677
|
onCatalog: (definitions) => {
|
|
2066
2678
|
catalog = definitions.map(toCatalogEntry).filter((entry) => entry !== void 0);
|
|
2067
2679
|
},
|
|
2680
|
+
async reset(agent) {
|
|
2681
|
+
const runtime = runtimeOf();
|
|
2682
|
+
if (runtime === void 0) throw new Error("codemode: no PTC runtime is mounted (ctx.ptcRuntime); the program transport is unavailable");
|
|
2683
|
+
if (!runtime.retainsState) throw new CodeResetFailedError("The PTC runtime does not retain an environment");
|
|
2684
|
+
try {
|
|
2685
|
+
await retention.reset(agent, () => {
|
|
2686
|
+
const inject = typeof agent === "object" && agent !== null ? agent.inject : void 0;
|
|
2687
|
+
if (typeof inject === "function") inject.call(agent, createUserMessage({
|
|
2688
|
+
content: [{
|
|
2689
|
+
type: "text",
|
|
2690
|
+
text: "Program environment reset; retained variables cleared. Completed tool effects remain."
|
|
2691
|
+
}],
|
|
2692
|
+
source: { kind: PLUGIN_MESSAGE_SOURCE_KIND }
|
|
2693
|
+
}));
|
|
2694
|
+
});
|
|
2695
|
+
} catch (error) {
|
|
2696
|
+
throw new CodeResetFailedError("Program environment reset failed: " + messageOf(error) + ". State may not be cleared; retry the host reset before running more code.");
|
|
2697
|
+
}
|
|
2698
|
+
},
|
|
2068
2699
|
onDescribe: (decision) => {
|
|
2069
2700
|
const hidden = summarizeHidden(decision.masked);
|
|
2070
2701
|
debugRing.push({
|
|
@@ -2145,24 +2776,6 @@ function errorInfoOf(result) {
|
|
|
2145
2776
|
function nonEmptyString(value) {
|
|
2146
2777
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2147
2778
|
}
|
|
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
2779
|
/** 把内容块里的文本拼起来。 */
|
|
2167
2780
|
function textOf(content) {
|
|
2168
2781
|
if (!Array.isArray(content)) return "";
|