@feng0531/dsh-execution-bus 0.12.1 → 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.
- package/README.md +47 -264
- package/cordis.patch.yml +1 -1
- package/dist/error-codes.d.ts +14 -0
- package/dist/error-codes.js +15 -0
- package/dist/mode.d.ts +2 -2
- package/dist/mode.js +7 -7
- package/dist/plugin.d.ts +2 -6
- package/dist/plugin.js +28 -232
- package/dist/preset.d.ts +1 -1
- package/dist/preset.js +4 -6
- package/dist/product.d.ts +91 -195
- package/dist/product.js +66 -146
- package/dist/runtime/executor.d.ts +18 -0
- package/dist/runtime/executor.js +239 -0
- package/dist/runtime/plan.d.ts +30 -0
- package/dist/runtime/plan.js +185 -0
- package/dist/runtime/service.d.ts +66 -0
- package/dist/runtime/service.js +185 -0
- package/dist/runtime/store.d.ts +45 -0
- package/dist/runtime/store.js +102 -0
- package/docs/DECLARATIVE-RUNTIME.md +44 -0
- package/docs/FLOW-TEST.md +7 -54
- package/docs/INTEGRATION.md +3 -1
- package/docs/PRODUCT.md +15 -42
- package/docs/RETROSPECTIVE.md +51 -0
- package/docs/host-compatibility.md +11 -74
- package/docs/protocol.md +27 -135
- package/docs/releases/1.0.0.md +33 -0
- package/docs/validation.md +23 -62
- package/package.json +4 -6
- package/dist/bus.d.ts +0 -153
- package/dist/bus.js +0 -833
- package/dist/diagnostics.d.ts +0 -41
- package/dist/diagnostics.js +0 -32
- package/dist/execution.d.ts +0 -70
- package/dist/execution.js +0 -193
- package/dist/handlers.d.ts +0 -27
- package/dist/handlers.js +0 -194
- package/dist/json.d.ts +0 -4
- package/dist/json.js +0 -46
- package/dist/limits.d.ts +0 -20
- package/dist/limits.js +0 -23
- package/dist/protocol.d.ts +0 -62
- package/dist/protocol.js +0 -54
package/dist/product.js
CHANGED
|
@@ -1,165 +1,85 @@
|
|
|
1
|
-
/** Stable product identities and
|
|
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: "
|
|
6
|
-
presetRowId: "preset-
|
|
5
|
+
presetId: "xiao-he-runtime",
|
|
6
|
+
presetRowId: "preset-xiao-he-runtime",
|
|
7
7
|
modeEntry: "@feng0531/dsh-execution-bus/mode",
|
|
8
|
-
modePluginName: "
|
|
8
|
+
modePluginName: "xiao-he-runtime-mode",
|
|
9
9
|
});
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
type: "
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
|
161
|
-
export const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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;
|