@feng0531/dsh-execution-bus 0.12.0 → 1.0.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.
Files changed (47) hide show
  1. package/README.md +47 -264
  2. package/cordis.patch.yml +1 -1
  3. package/dist/error-codes.d.ts +14 -0
  4. package/dist/error-codes.js +15 -0
  5. package/dist/mode.d.ts +2 -2
  6. package/dist/mode.js +7 -7
  7. package/dist/plugin.d.ts +2 -6
  8. package/dist/plugin.js +28 -232
  9. package/dist/preset.d.ts +1 -1
  10. package/dist/preset.js +4 -6
  11. package/dist/product.d.ts +91 -195
  12. package/dist/product.js +66 -146
  13. package/dist/runtime/executor.d.ts +18 -0
  14. package/dist/runtime/executor.js +239 -0
  15. package/dist/runtime/plan.d.ts +30 -0
  16. package/dist/runtime/plan.js +185 -0
  17. package/dist/runtime/service.d.ts +66 -0
  18. package/dist/runtime/service.js +185 -0
  19. package/dist/runtime/store.d.ts +45 -0
  20. package/dist/runtime/store.js +102 -0
  21. package/docs/DECLARATIVE-RUNTIME.md +44 -0
  22. package/docs/FLOW-TEST.md +7 -54
  23. package/docs/INTEGRATION.md +4 -2
  24. package/docs/OPTIMIZATION.md +2 -0
  25. package/docs/PRODUCT.md +15 -40
  26. package/docs/RETROSPECTIVE.md +51 -0
  27. package/docs/host-compatibility.md +11 -74
  28. package/docs/protocol.md +27 -120
  29. package/docs/releases/0.11.1.md +2 -0
  30. package/docs/releases/0.12.1.md +30 -0
  31. package/docs/releases/1.0.0.md +33 -0
  32. package/docs/validation.md +23 -61
  33. package/package.json +4 -6
  34. package/dist/bus.d.ts +0 -148
  35. package/dist/bus.js +0 -773
  36. package/dist/diagnostics.d.ts +0 -41
  37. package/dist/diagnostics.js +0 -32
  38. package/dist/execution.d.ts +0 -70
  39. package/dist/execution.js +0 -193
  40. package/dist/handlers.d.ts +0 -25
  41. package/dist/handlers.js +0 -194
  42. package/dist/json.d.ts +0 -4
  43. package/dist/json.js +0 -46
  44. package/dist/limits.d.ts +0 -20
  45. package/dist/limits.js +0 -23
  46. package/dist/protocol.d.ts +0 -62
  47. package/dist/protocol.js +0 -54
package/dist/product.js CHANGED
@@ -1,165 +1,85 @@
1
- /** Stable product identities and policy. Historical IDs are compatibility contracts, not display names. */
1
+ /** Stable product identities and model-visible runtime policy. */
2
2
  export const PRODUCT = Object.freeze({
3
3
  name: "萧何 / Xiao He",
4
4
  packageName: "@feng0531/dsh-execution-bus",
5
- presetId: "execution-bus-test",
6
- presetRowId: "preset-execution-bus-test",
5
+ presetId: "xiao-he-runtime",
6
+ presetRowId: "preset-xiao-he-runtime",
7
7
  modeEntry: "@feng0531/dsh-execution-bus/mode",
8
- modePluginName: "execution-bus-mode",
8
+ modePluginName: "xiao-he-runtime-mode",
9
9
  });
10
- export const CORE_DEFAULTS = Object.freeze({
11
- concurrency: 2,
12
- queueLimit: 32,
13
- recordLimit: 256,
14
- resultBytes: 65536,
15
- retentionMs: 3600000,
16
- });
17
- export const ADMISSION_DEFAULTS = Object.freeze({
18
- sessionQueueLimit: 16,
19
- maxQueueWaitMs: 120000,
20
- });
21
- export const HANDLER_LIMITS = Object.freeze({
22
- readLines: 100,
23
- readPathChars: 2048,
24
- diagnosticDelayMs: 30000,
25
- });
26
- /** 调度策略:类别优先级 + 等待老化 + 长任务诊断阈值。 */
27
- export const SCHEDULER_POLICY = Object.freeze({
28
- longRunningMs: 30000,
29
- queueAgingMs: 30000,
30
- queueClassOrder: Object.freeze({
31
- fs: 0,
32
- diagnostic: 1,
33
- command: 2,
34
- default: 3,
35
- }),
36
- });
37
- /** 每个 Handler 的模型可见输入声明。只用于工具 schema/SDK 生成;运行时仍由 Handler.validate 严格校验。 */
38
- const INPUT_SCHEMAS = {
39
- test: {
40
- type: "object",
41
- additionalProperties: false,
42
- required: ["delayMs", "fail"],
43
- properties: {
44
- delayMs: { type: "integer", description: "Diagnostic delay in milliseconds (0-30000)." },
45
- fail: { type: "boolean", description: "Settle as a controlled failure when true." },
46
- },
47
- },
48
- "host.read": {
49
- type: "object",
50
- additionalProperties: false,
51
- required: ["file_path"],
52
- properties: {
53
- file_path: { type: "string", description: "Path resolved against the session working directory." },
54
- offset: { type: "integer", description: "1-based first line to read." },
55
- limit: { type: "integer", description: "Maximum lines to read (<=100)." },
56
- },
57
- },
58
- "host.glob": {
59
- type: "object",
60
- additionalProperties: false,
61
- required: ["pattern"],
62
- properties: {
63
- pattern: { type: "string", description: "Glob pattern; a bare pattern matches basenames at any depth." },
64
- path: { type: "string", description: "Directory to search; defaults to the session working directory." },
65
- },
66
- },
67
- "host.grep": {
68
- type: "object",
69
- additionalProperties: false,
70
- required: ["pattern"],
71
- properties: {
72
- pattern: { type: "string", description: "Ripgrep regular expression." },
73
- path: { type: "string", description: "File or directory to search; defaults to the session working directory." },
74
- include: { type: "string", description: "One positive glob filter; negation and comma lists are rejected." },
75
- },
76
- },
77
- "host.edit": {
78
- type: "object",
79
- additionalProperties: false,
80
- required: ["file_path", "old_string", "new_string"],
81
- properties: {
82
- file_path: { type: "string", description: "File to edit." },
83
- old_string: { type: "string", description: "Literal text to replace." },
84
- new_string: { type: "string", description: "Replacement text." },
85
- replace_all: { type: "boolean", description: "Replace every occurrence." },
10
+ const RUNTIME_STEP_SCHEMA = {
11
+ type: "object",
12
+ properties: {
13
+ id: { type: "string", description: "步骤唯一 ID(字母开头,可含字母/数字/_-)" },
14
+ tool: {
15
+ type: "string",
16
+ enum: ["read", "glob", "grep", "edit", "write", "pwsh"],
17
+ description: "允许的宿主工具",
86
18
  },
87
- },
88
- "host.write": {
89
- type: "object",
90
- additionalProperties: false,
91
- required: ["file_path", "content"],
92
- properties: {
93
- file_path: { type: "string", description: "File to write." },
94
- content: { type: "string", description: "Full file content." },
19
+ input: { type: "object", description: "宿主工具参数,按工具 schema 校验" },
20
+ depends_on: {
21
+ type: "array",
22
+ items: { type: "string" },
23
+ description: "依赖步骤 ID;全部成功后本步骤才执行",
95
24
  },
96
- },
97
- "host.pwsh": {
98
- type: "object",
99
- additionalProperties: false,
100
- required: ["description", "command"],
101
- properties: {
102
- description: { type: "string", description: "Clear, concise description of what the command does (shown in the UI)." },
103
- command: { type: "string", description: "PowerShell command to execute." },
104
- timeoutMs: { type: "number", description: "Positive timeout in milliseconds." },
105
- workdir: { type: "string", description: "Working directory; defaults to the session working directory." },
106
- run_in_background: { type: "boolean", description: "Return a host Job id immediately instead of waiting." },
107
- sandbox_permissions: { type: "string", enum: ["workspace-write", "danger-full-access"], description: "Narrowest wider sandbox mode; must be paired with justification." },
108
- justification: { type: "string", description: "Required with sandbox_permissions; one sentence of reason." },
25
+ retry: {
26
+ type: "integer",
27
+ minimum: 0,
28
+ maximum: 3,
29
+ description: "仅幂等工具 read/glob/grep 允许 >0;重试次数不含首次",
109
30
  },
31
+ timeout_ms: { type: "integer", minimum: 1000, maximum: 600000 },
110
32
  },
33
+ required: ["id", "tool", "input"],
34
+ additionalProperties: false,
111
35
  };
112
- /** Handler 名列表同时决定 request payload 的联合分支顺序。 */
113
- const HANDLER_NAMES = [
114
- "host.read",
115
- "host.glob",
116
- "host.grep",
117
- "host.edit",
118
- "host.write",
119
- "host.pwsh",
120
- "test",
121
- ];
122
- /** 简化调用(0.11.0):{requestId, handler, input} 或 {requestId, type};
123
- * 完整 v1 消息仍被运行时接受,但不再进入模型可见 schema。 */
124
- const REQUEST_ID_PROP = { type: "string" };
125
- const MESSAGE_ID_PROP = { type: "string" };
126
- export const REQUEST_PARAMETERS = {
127
- oneOf: [
128
- ...HANDLER_NAMES.map((handler) => ({
129
- type: "object",
130
- additionalProperties: false,
131
- required: ["requestId", "handler", "input"],
132
- properties: {
133
- requestId: REQUEST_ID_PROP,
134
- messageId: MESSAGE_ID_PROP,
135
- handler: { type: "string", const: handler },
136
- input: INPUT_SCHEMAS[handler],
137
- },
138
- })),
139
- {
36
+ export const RUNTIME_RUN_PARAMETERS = {
37
+ type: "object",
38
+ properties: {
39
+ plan: {
140
40
  type: "object",
141
- additionalProperties: false,
142
- required: ["requestId", "type"],
143
41
  properties: {
144
- requestId: REQUEST_ID_PROP,
145
- messageId: MESSAGE_ID_PROP,
146
- type: {
147
- type: "string",
148
- enum: ["status", "result", "cancel"],
149
- description: "Read the saved snapshot or request cancellation for this requestId.",
42
+ goal: { type: "string", maxLength: 500 },
43
+ concurrency: { type: "integer", minimum: 1, maximum: 4 },
44
+ steps: {
45
+ type: "array",
46
+ items: RUNTIME_STEP_SCHEMA,
47
+ minItems: 1,
48
+ maxItems: 32,
150
49
  },
151
50
  },
51
+ required: ["steps"],
52
+ additionalProperties: false,
53
+ },
54
+ async: {
55
+ type: "boolean",
56
+ description: "true 时立即返回 runId,用 runtime_status/runtime_result 取回",
152
57
  },
153
- ],
58
+ },
59
+ required: ["plan"],
60
+ additionalProperties: false,
61
+ };
62
+ export const RUNTIME_RUN_ID_PARAMETERS = {
63
+ type: "object",
64
+ properties: {
65
+ runId: { type: "string", description: "runtime_run 返回的运行 ID" },
66
+ },
67
+ required: ["runId"],
68
+ additionalProperties: false,
154
69
  };
155
- export const STATE_PARAMETERS = {
70
+ export const RUNTIME_STATUS_PARAMETERS = {
156
71
  type: "object",
157
- properties: {},
72
+ properties: {
73
+ runId: {
74
+ type: "string",
75
+ description: "可选:指定运行 ID;省略时列出最近运行",
76
+ },
77
+ },
158
78
  additionalProperties: false,
159
79
  };
160
- export const REQUEST_DESCRIPTION = "萧何 / Xiao He. Submit a request and WAIT for its terminal snapshot; the simplified call takes {requestId, handler, input} (messageId optional) and control calls take {requestId, type:\"status\"|\"result\"|\"cancel\"}. Use only when coordination helps: the same logical operation may be retried or deduplicated (reuse requestId), work must outlive a run_code budget end, or multiple steps must await one execution. Duration alone is not a reason — for a one-off long command use the original tool or its background Job. A PTC program end only stops waiting: accepted work continues and the same requestId can be retrieved with type:\"result\" or cancelled. Do not poll status inside the same run_code program as an unfinished request: end that program and retrieve in a later step. The snapshot may contain reused:true when this call attached to an existing execution. The full v1 envelope is still accepted. Diagnostic handler only: test {delayMs:0..30000,fail:boolean}; never use it for real work. Acceptance is volatile, not success. No polling or restart resume.";
161
- export const MODE_INSTRUCTIONS = "你正在萧何模式中。这不是角色扮演。保留宿主 PTC 的日常工具能力与权限规则。普通编辑、搜索、命令、安装和其他工作使用原工具,不要声称此模式只读。execution_bus 只在能获得协调收益时使用:① 同一逻辑操作可能重复提交或需要重试核对(沿用同一 requestId,只执行一次);② 任务需要在 run_code 时间预算结束后继续或跨步骤取回;③ 多个步骤/等待者需要同一执行结果。仅\"操作耗时长\"不是理由:一次性长命令用原工具或宿主后台 Job。其余普通任务直接用原工具,不要为简单调用增加包装。实际业务 Handler 为 host.read、host.glob、host.grep(只读,可复用终态结果)与 host.edit、host.write、host.pwsh(有副作用,终态结果不可复用);test 仅用于用户明确要求的诊断,不做真实工作。简化调用示例:await tools.execution_bus({requestId:\"<逻辑操作编号>\", handler:\"host.pwsh\", input:{description:\"运行测试\", command:\"pnpm test\", timeoutMs:120000}});预算中断后用 await tools.execution_bus({requestId:\"<同一编号>\", type:\"result\"}) 取回;同一逻辑操作重发沿用 requestId,messageId 可省略(自动生成)。返回快照出现 reused:true 表示本次调用复用了已有执行。注意:同一 run_code 程序内,未完成的 request 会阻塞后发工具调用的结果提交;跨步骤取回请让启动步骤尽早结束程序,下一步再用同一 requestId 的 status/result 或重发 request;不要在同一程序里对未完成请求轮询状态。通过 run_code 调用,受原有时间预算约束。每会话排队上限 " +
162
- ADMISSION_DEFAULTS.sessionQueueLimit +
163
- ",排队超过 " +
164
- ADMISSION_DEFAULTS.maxQueueWaitMs / 1000 +
165
- " 秒且未执行会返回 QUEUE_WAIT_TIMEOUT,不自动重试。execution_bus_state 只用于用户明确诊断本会话元数据,不循环查询。请求只提交一次并等待结果,同一逻辑操作重发沿用 requestId。PTC 程序结束或 run_code 时间预算到期时,已受理请求只停止等待、继续执行;用同一 requestId 的 status/result 取回,或用 cancel 取消;用户/会话中断仍会请求取消执行。cancelRequested 不等于已取消,unknown 不代表执行失败,不承诺跨重启恢复。后台命令返回 Job ID 不代表已经完成。";
80
+ export const RUNTIME_RUN_DESCRIPTION = "萧何声明式运行时:提交 DAG 计划(steps:id/tool/input/depends_on/retry/timeout_ms),由程序按依赖并行执行、对只读步骤重试、逐步落盘。默认等待完成返回步骤状态;async:true 立即返回 runId,用 runtime_status/runtime_result 取回,runtime_cancel 取消。仅允许 read/glob/grep/edit/write/pwsh;副作用步骤不重试、崩溃后在途步骤标记 unknown(不做 exactly-once 回滚)。简单一次调用请直接用原工具。";
81
+ export const RUNTIME_STATUS_DESCRIPTION = "读取本会话的运行元数据:不带 runId 列出最近运行,带 runId 返回指定运行与步骤状态(不含结果)。不要轮询正在等待的 runtime_run。";
82
+ export const RUNTIME_RESULT_DESCRIPTION = "读取指定运行已保存的步骤结果(单步结果超过 64KiB 时只返回 truncated/bytes 标记)。";
83
+ export const RUNTIME_CANCEL_DESCRIPTION = "请求取消指定运行:停止调度、向后发步骤传播取消;已开始的副作用步骤状态可能为 unknown。";
84
+ export const RUNTIME_RESUME_DESCRIPTION = "恢复 interrupted 运行:只读步骤重新排队,副作用在途步骤标记 failed(RUNTIME_INTERRUPTED);不自动执行,需显式调用。";
85
+ export const RUNTIME_INSTRUCTIONS = "萧何声明式运行时:当任务能写成稳定的 DAG(读取/搜索/编辑/写入/命令,步骤间有明确依赖)时,用 runtime_run 提交计划,而不是在 run_code 里写循环或反复等待。独立步骤会并行;只读步骤可 retry;运行记录落盘,可用 runtime_status/runtime_result/runtime_cancel/runtime_resume。副作用步骤不重试、崩溃后在途步骤可能 unknown,不保证 exactly-once;简单一次性操作仍用原工具或宿主后台 Job,不要为包装而包装。";
@@ -0,0 +1,18 @@
1
+ import type { Plan } from "./plan.js";
2
+ import type { RunRecord } from "./store.js";
3
+ import type { RunStore } from "./store.js";
4
+ export type Invoke = (tool: string, input: Record<string, unknown>, signal: AbortSignal) => Promise<unknown>;
5
+ /** 按 DAG 执行声明式计划:独立步骤并行、幂等步骤可重试、逐步落盘、可取消/恢复。 */
6
+ export declare class RuntimeExecutor {
7
+ private readonly store;
8
+ private readonly invoke;
9
+ constructor(store: RunStore, invoke: Invoke);
10
+ createRun(ownerSessionId: string, plan: Plan): RunRecord;
11
+ execute(run: RunRecord, signal?: AbortSignal): Promise<RunRecord>;
12
+ /** 只恢复 interrupted 运行:幂等步骤重排,副作用步骤保持 unknown/failed。 */
13
+ resume(run: RunRecord, signal?: AbortSignal): Promise<RunRecord>;
14
+ cancel(run: RunRecord, reason?: "RUNTIME_CANCELLED"): void;
15
+ private spec;
16
+ private runStep;
17
+ private touch;
18
+ }
@@ -0,0 +1,239 @@
1
+ import { ERRORS } from "../error-codes.js";
2
+ import { IDEMPOTENT_TOOLS, RUNTIME_LIMITS } from "./plan.js";
3
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
+ /** 按 DAG 执行声明式计划:独立步骤并行、幂等步骤可重试、逐步落盘、可取消/恢复。 */
5
+ export class RuntimeExecutor {
6
+ store;
7
+ invoke;
8
+ constructor(store, invoke) {
9
+ this.store = store;
10
+ this.invoke = invoke;
11
+ }
12
+ createRun(ownerSessionId, plan) {
13
+ const now = Date.now();
14
+ const runId = "run-" +
15
+ now.toString(36) +
16
+ "-" +
17
+ Math.random().toString(36).slice(2, 10);
18
+ const run = {
19
+ version: 1,
20
+ runId,
21
+ ownerSessionId,
22
+ ...(plan.goal === undefined ? {} : { goal: plan.goal }),
23
+ plan,
24
+ state: "running",
25
+ steps: plan.steps.map((step) => ({
26
+ id: step.id,
27
+ tool: step.tool,
28
+ state: "pending",
29
+ attempts: 0,
30
+ })),
31
+ results: {},
32
+ createdAt: now,
33
+ updatedAt: now,
34
+ };
35
+ this.store.save(run);
36
+ return run;
37
+ }
38
+ async execute(run, signal) {
39
+ if (run.state === "succeeded" || run.state === "failed" || run.state === "cancelled")
40
+ return run;
41
+ run.state = "running";
42
+ run.error = undefined;
43
+ run.finishedAt = undefined;
44
+ this.touch(run);
45
+ this.store.save(run);
46
+ while (true) {
47
+ if (signal?.aborted) {
48
+ this.cancel(run);
49
+ return run;
50
+ }
51
+ const pending = run.steps.filter((step) => step.state === "pending");
52
+ if (pending.length === 0)
53
+ break;
54
+ const done = new Set(run.steps.filter((step) => step.state === "succeeded").map((step) => step.id));
55
+ const ready = pending.filter((record) => {
56
+ const spec = this.spec(run, record.id);
57
+ return spec !== undefined && spec.dependsOn.every((dep) => done.has(dep));
58
+ });
59
+ if (ready.length === 0) {
60
+ run.state = "failed";
61
+ run.error = ERRORS.RUNTIME_INVALID_STATE;
62
+ break;
63
+ }
64
+ const batch = ready.slice(0, run.plan.concurrency);
65
+ for (const record of batch) {
66
+ record.state = "running";
67
+ record.startedAt = Date.now();
68
+ record.attempts++;
69
+ }
70
+ this.touch(run);
71
+ this.store.save(run);
72
+ await Promise.allSettled(batch.map((record) => this.runStep(run, record, signal)));
73
+ this.touch(run);
74
+ this.store.save(run);
75
+ const failedStep = batch.find((record) => record.state === "failed");
76
+ if (failedStep) {
77
+ run.state = "failed";
78
+ run.error = failedStep.error ?? ERRORS.RUNTIME_STEP_FAILED;
79
+ break;
80
+ }
81
+ if (batch.some((record) => record.state === "unknown" || record.state === "cancelled")) {
82
+ run.state = "cancelled";
83
+ break;
84
+ }
85
+ }
86
+ if (run.state === "running") {
87
+ const failed = run.steps.find((step) => step.state === "failed");
88
+ if (failed) {
89
+ run.state = "failed";
90
+ run.error = failed.error ?? ERRORS.RUNTIME_STEP_FAILED;
91
+ }
92
+ else if (run.steps.some((step) => step.state === "unknown")) {
93
+ run.state = "cancelled";
94
+ }
95
+ else {
96
+ run.state = "succeeded";
97
+ }
98
+ }
99
+ run.finishedAt = Date.now();
100
+ this.touch(run);
101
+ this.store.save(run);
102
+ return run;
103
+ }
104
+ /** 只恢复 interrupted 运行:幂等步骤重排,副作用步骤保持 unknown/failed。 */
105
+ async resume(run, signal) {
106
+ if (run.state !== "interrupted")
107
+ throw Error(ERRORS.RUNTIME_INVALID_STATE);
108
+ for (const record of run.steps) {
109
+ if (record.state !== "running" && record.state !== "unknown")
110
+ continue;
111
+ const spec = this.spec(run, record.id);
112
+ if (spec && IDEMPOTENT_TOOLS.has(spec.tool)) {
113
+ record.state = "pending";
114
+ record.error = undefined;
115
+ }
116
+ else {
117
+ record.state = "failed";
118
+ record.error = ERRORS.RUNTIME_INTERRUPTED;
119
+ }
120
+ }
121
+ run.state = "running";
122
+ run.error = undefined;
123
+ run.finishedAt = undefined;
124
+ this.touch(run);
125
+ this.store.save(run);
126
+ return this.execute(run, signal);
127
+ }
128
+ cancel(run, reason = ERRORS.RUNTIME_CANCELLED) {
129
+ if (run.state === "succeeded" ||
130
+ run.state === "failed" ||
131
+ run.state === "cancelled")
132
+ return;
133
+ run.state = "cancelled";
134
+ run.error = reason;
135
+ run.finishedAt = Date.now();
136
+ for (const record of run.steps) {
137
+ if (record.state === "pending") {
138
+ record.state = "cancelled";
139
+ record.error = reason;
140
+ record.finishedAt = Date.now();
141
+ }
142
+ else if (record.state === "running") {
143
+ record.state = "unknown";
144
+ record.error = reason;
145
+ record.finishedAt = Date.now();
146
+ }
147
+ }
148
+ this.touch(run);
149
+ this.store.save(run);
150
+ }
151
+ spec(run, id) {
152
+ return run.plan.steps.find((step) => step.id === id);
153
+ }
154
+ async runStep(run, record, runSignal) {
155
+ const spec = this.spec(run, record.id);
156
+ if (!spec) {
157
+ record.state = "failed";
158
+ record.error = ERRORS.RUNTIME_INVALID_STATE;
159
+ record.finishedAt = Date.now();
160
+ return;
161
+ }
162
+ const attemptsAllowed = spec.retry + 1;
163
+ for (let attempt = 1; attempt <= attemptsAllowed; attempt++) {
164
+ if (runSignal?.aborted) {
165
+ record.state = IDEMPOTENT_TOOLS.has(spec.tool) ? "cancelled" : "unknown";
166
+ record.error = ERRORS.RUNTIME_CANCELLED;
167
+ record.finishedAt = Date.now();
168
+ return;
169
+ }
170
+ const controller = new AbortController();
171
+ const onAbort = () => controller.abort();
172
+ runSignal?.addEventListener("abort", onAbort, { once: true });
173
+ let timer;
174
+ try {
175
+ const timeout = new Promise((_, reject) => {
176
+ timer = setTimeout(() => {
177
+ controller.abort();
178
+ reject(Error(ERRORS.RUNTIME_STEP_TIMEOUT));
179
+ }, spec.timeoutMs);
180
+ });
181
+ const value = await Promise.race([
182
+ this.invoke(spec.tool, spec.input, controller.signal),
183
+ timeout,
184
+ ]);
185
+ record.state = "succeeded";
186
+ record.error = undefined;
187
+ record.finishedAt = Date.now();
188
+ run.results[spec.id] = boundedResult(value);
189
+ return;
190
+ }
191
+ catch (error) {
192
+ const aborted = runSignal?.aborted === true;
193
+ const message = error instanceof Error ? error.message : "";
194
+ record.error = aborted
195
+ ? ERRORS.RUNTIME_CANCELLED
196
+ : message === ERRORS.RUNTIME_STEP_TIMEOUT
197
+ ? ERRORS.RUNTIME_STEP_TIMEOUT
198
+ : ERRORS.RUNTIME_STEP_FAILED;
199
+ if (aborted) {
200
+ record.state = IDEMPOTENT_TOOLS.has(spec.tool)
201
+ ? "cancelled"
202
+ : "unknown";
203
+ record.finishedAt = Date.now();
204
+ return;
205
+ }
206
+ if (attempt < attemptsAllowed) {
207
+ record.attempts++;
208
+ await delay(100 * attempt);
209
+ continue;
210
+ }
211
+ record.state = "failed";
212
+ record.finishedAt = Date.now();
213
+ return;
214
+ }
215
+ finally {
216
+ if (timer !== undefined)
217
+ clearTimeout(timer);
218
+ runSignal?.removeEventListener("abort", onAbort);
219
+ }
220
+ }
221
+ }
222
+ touch(run) {
223
+ run.updatedAt = Date.now();
224
+ }
225
+ }
226
+ function boundedResult(value) {
227
+ try {
228
+ const json = JSON.stringify(value);
229
+ if (json === undefined)
230
+ return { value: null, bytes: 0 };
231
+ const bytes = Buffer.byteLength(json);
232
+ if (bytes > RUNTIME_LIMITS.resultBytes)
233
+ return { truncated: true, bytes };
234
+ return { value: JSON.parse(json), bytes };
235
+ }
236
+ catch {
237
+ return { truncated: true };
238
+ }
239
+ }
@@ -0,0 +1,30 @@
1
+ /** 声明式运行时只允许调用这些宿主工具;新增工具需同时更新幂等性判断。 */
2
+ export declare const RUNTIME_TOOLS: readonly ["read", "glob", "grep", "edit", "write", "pwsh"];
3
+ export type RuntimeTool = (typeof RUNTIME_TOOLS)[number];
4
+ export declare const IDEMPOTENT_TOOLS: ReadonlySet<string>;
5
+ export interface PlanStep {
6
+ id: string;
7
+ tool: RuntimeTool;
8
+ input: Record<string, unknown>;
9
+ dependsOn: string[];
10
+ /** 仅幂等工具允许 >0;重试次数(不含首次)。 */
11
+ retry: number;
12
+ timeoutMs: number;
13
+ }
14
+ export interface Plan {
15
+ goal?: string;
16
+ steps: PlanStep[];
17
+ concurrency: number;
18
+ }
19
+ export declare const RUNTIME_LIMITS: Readonly<{
20
+ steps: 32;
21
+ planBytes: number;
22
+ idChars: 64;
23
+ retry: 3;
24
+ timeoutMinMs: 1000;
25
+ timeoutMaxMs: 600000;
26
+ concurrencyMax: 4;
27
+ goalChars: 500;
28
+ resultBytes: number;
29
+ }>;
30
+ export declare function parsePlan(raw: unknown): Plan;