@openclaw/lobster 2026.3.13 → 2026.5.2-beta.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/README.md +1 -2
- package/index.ts +22 -16
- package/openclaw.plugin.json +6 -0
- package/package.json +27 -3
- package/runtime-api.ts +12 -0
- package/src/lobster-ajv-cache.ts +142 -0
- package/src/lobster-core.d.ts +60 -0
- package/src/lobster-runner.test.ts +572 -0
- package/src/lobster-runner.ts +395 -0
- package/src/lobster-taskflow.test.ts +227 -0
- package/src/lobster-taskflow.ts +279 -0
- package/src/lobster-tool.test.ts +250 -208
- package/src/lobster-tool.ts +245 -191
- package/src/taskflow-test-helpers.ts +48 -0
- package/tsconfig.json +16 -0
- package/src/test-helpers.ts +0 -43
- package/src/windows-spawn.test.ts +0 -118
- package/src/windows-spawn.ts +0 -36
package/src/lobster-tool.ts
CHANGED
|
@@ -1,213 +1,216 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "../runtime-api.js";
|
|
3
|
+
import {
|
|
4
|
+
createEmbeddedLobsterRunner,
|
|
5
|
+
resolveLobsterCwd,
|
|
6
|
+
type LobsterRunner,
|
|
7
|
+
type LobsterRunnerParams,
|
|
8
|
+
} from "./lobster-runner.js";
|
|
9
|
+
import {
|
|
10
|
+
type ManagedLobsterFlowResult,
|
|
11
|
+
resumeManagedLobsterFlow,
|
|
12
|
+
runManagedLobsterFlow,
|
|
13
|
+
} from "./lobster-taskflow.js";
|
|
14
|
+
|
|
15
|
+
type BoundTaskFlow = ReturnType<
|
|
16
|
+
NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
type JsonLike =
|
|
20
|
+
| null
|
|
21
|
+
| boolean
|
|
22
|
+
| number
|
|
23
|
+
| string
|
|
24
|
+
| JsonLike[]
|
|
19
25
|
| {
|
|
20
|
-
|
|
21
|
-
error: { type?: string; message: string };
|
|
26
|
+
[key: string]: JsonLike;
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
type LobsterToolOptions = {
|
|
30
|
+
runner?: LobsterRunner;
|
|
31
|
+
taskFlow?: BoundTaskFlow;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ManagedFlowRunParams = {
|
|
35
|
+
controllerId: string;
|
|
36
|
+
goal: string;
|
|
37
|
+
currentStep?: string;
|
|
38
|
+
waitingStep?: string;
|
|
39
|
+
stateJson?: JsonLike;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ManagedFlowResumeParams = {
|
|
43
|
+
flowId: string;
|
|
44
|
+
expectedRevision: number;
|
|
45
|
+
currentStep?: string;
|
|
46
|
+
waitingStep?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type ManagedFlowSuccessResult = {
|
|
50
|
+
ok: true;
|
|
51
|
+
envelope: unknown;
|
|
52
|
+
flow: unknown;
|
|
53
|
+
mutation: unknown;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function readOptionalTrimmedString(value: unknown, fieldName: string): string | undefined {
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value !== "string") {
|
|
61
|
+
throw new Error(`${fieldName} must be a string`);
|
|
62
|
+
}
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
return trimmed ? trimmed : undefined;
|
|
27
65
|
}
|
|
28
66
|
|
|
29
|
-
function
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
67
|
+
function readOptionalNumber(value: unknown, fieldName: string): number | undefined {
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
return undefined;
|
|
32
70
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new Error("cwd must be a relative path");
|
|
71
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
72
|
+
throw new Error(`${fieldName} must be an integer`);
|
|
36
73
|
}
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
77
|
+
function readOptionalBoolean(value: unknown, fieldName: string): boolean | undefined {
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
return undefined;
|
|
43
80
|
}
|
|
44
|
-
if (
|
|
45
|
-
throw new Error(
|
|
81
|
+
if (typeof value !== "boolean") {
|
|
82
|
+
throw new Error(`${fieldName} must be a boolean`);
|
|
46
83
|
}
|
|
47
|
-
return
|
|
84
|
+
return value;
|
|
48
85
|
}
|
|
49
86
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const env = { ...process.env, LOBSTER_MODE: "tool" } as Record<string, string | undefined>;
|
|
62
|
-
const nodeOptions = env.NODE_OPTIONS ?? "";
|
|
63
|
-
if (nodeOptions.includes("--inspect")) {
|
|
64
|
-
delete env.NODE_OPTIONS;
|
|
87
|
+
function parseOptionalFlowStateJson(value: unknown): JsonLike | undefined {
|
|
88
|
+
if (value === undefined) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value !== "string") {
|
|
92
|
+
throw new Error("flowStateJson must be a JSON string");
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(value) as JsonLike;
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error("flowStateJson must be valid JSON");
|
|
65
98
|
}
|
|
66
|
-
const spawnTarget =
|
|
67
|
-
process.platform === "win32"
|
|
68
|
-
? resolveWindowsLobsterSpawn(execPath, argv, env)
|
|
69
|
-
: { command: execPath, argv };
|
|
70
|
-
|
|
71
|
-
return await new Promise<{ stdout: string }>((resolve, reject) => {
|
|
72
|
-
const child = spawn(spawnTarget.command, spawnTarget.argv, {
|
|
73
|
-
cwd,
|
|
74
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
-
env,
|
|
76
|
-
windowsHide: spawnTarget.windowsHide,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
let stdout = "";
|
|
80
|
-
let stdoutBytes = 0;
|
|
81
|
-
let stderr = "";
|
|
82
|
-
let settled = false;
|
|
83
|
-
|
|
84
|
-
const settle = (
|
|
85
|
-
result: { ok: true; value: { stdout: string } } | { ok: false; error: Error },
|
|
86
|
-
) => {
|
|
87
|
-
if (settled) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
settled = true;
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
if (result.ok) {
|
|
93
|
-
resolve(result.value);
|
|
94
|
-
} else {
|
|
95
|
-
reject(result.error);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const failAndTerminate = (message: string) => {
|
|
100
|
-
try {
|
|
101
|
-
child.kill("SIGKILL");
|
|
102
|
-
} finally {
|
|
103
|
-
settle({ ok: false, error: new Error(message) });
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
child.stdout?.setEncoding("utf8");
|
|
108
|
-
child.stderr?.setEncoding("utf8");
|
|
109
|
-
|
|
110
|
-
child.stdout?.on("data", (chunk) => {
|
|
111
|
-
const str = String(chunk);
|
|
112
|
-
stdoutBytes += Buffer.byteLength(str, "utf8");
|
|
113
|
-
if (stdoutBytes > maxStdoutBytes) {
|
|
114
|
-
failAndTerminate("lobster output exceeded maxStdoutBytes");
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
stdout += str;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
child.stderr?.on("data", (chunk) => {
|
|
121
|
-
stderr += String(chunk);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const timer = setTimeout(() => {
|
|
125
|
-
failAndTerminate("lobster subprocess timed out");
|
|
126
|
-
}, timeoutMs);
|
|
127
|
-
|
|
128
|
-
child.once("error", (err) => {
|
|
129
|
-
settle({ ok: false, error: err });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
child.once("exit", (code) => {
|
|
133
|
-
if (code !== 0) {
|
|
134
|
-
settle({
|
|
135
|
-
ok: false,
|
|
136
|
-
error: new Error(`lobster failed (${code ?? "?"}): ${stderr.trim() || stdout.trim()}`),
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
settle({ ok: true, value: { stdout } });
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
99
|
}
|
|
144
100
|
|
|
145
|
-
function
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
101
|
+
function parseRunFlowParams(params: Record<string, unknown>): ManagedFlowRunParams | null {
|
|
102
|
+
const controllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
|
|
103
|
+
const goal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
|
|
104
|
+
const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep");
|
|
105
|
+
const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep");
|
|
106
|
+
const stateJson = parseOptionalFlowStateJson(params.flowStateJson);
|
|
107
|
+
const resumeFlowId = readOptionalTrimmedString(params.flowId, "flowId");
|
|
108
|
+
const resumeRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
|
|
109
|
+
|
|
110
|
+
const hasRunFields =
|
|
111
|
+
controllerId !== undefined ||
|
|
112
|
+
goal !== undefined ||
|
|
113
|
+
currentStep !== undefined ||
|
|
114
|
+
waitingStep !== undefined ||
|
|
115
|
+
stateJson !== undefined;
|
|
116
|
+
|
|
117
|
+
if (!hasRunFields) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (resumeFlowId !== undefined || resumeRevision !== undefined) {
|
|
121
|
+
throw new Error("run action does not accept flowId or flowExpectedRevision");
|
|
122
|
+
}
|
|
123
|
+
if (!controllerId) {
|
|
124
|
+
throw new Error("flowControllerId required when using managed TaskFlow run mode");
|
|
125
|
+
}
|
|
126
|
+
if (!goal) {
|
|
127
|
+
throw new Error("flowGoal required when using managed TaskFlow run mode");
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
controllerId,
|
|
131
|
+
goal,
|
|
132
|
+
...(currentStep ? { currentStep } : {}),
|
|
133
|
+
...(waitingStep ? { waitingStep } : {}),
|
|
134
|
+
...(stateJson !== undefined ? { stateJson } : {}),
|
|
154
135
|
};
|
|
136
|
+
}
|
|
155
137
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
138
|
+
function parseResumeFlowParams(params: Record<string, unknown>): ManagedFlowResumeParams | null {
|
|
139
|
+
const flowId = readOptionalTrimmedString(params.flowId, "flowId");
|
|
140
|
+
const expectedRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
|
|
141
|
+
const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep");
|
|
142
|
+
const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep");
|
|
143
|
+
const token = readOptionalTrimmedString(params.token, "token");
|
|
144
|
+
const approvalId = readOptionalTrimmedString(params.approvalId, "approvalId");
|
|
145
|
+
const approve = readOptionalBoolean(params.approve, "approve");
|
|
146
|
+
const runControllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
|
|
147
|
+
const runGoal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
|
|
148
|
+
const stateJson = params.flowStateJson;
|
|
149
|
+
|
|
150
|
+
const hasResumeFields =
|
|
151
|
+
flowId !== undefined ||
|
|
152
|
+
expectedRevision !== undefined ||
|
|
153
|
+
currentStep !== undefined ||
|
|
154
|
+
waitingStep !== undefined;
|
|
155
|
+
|
|
156
|
+
if (!hasResumeFields) {
|
|
157
|
+
return null;
|
|
165
158
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
throw new Error("lobster returned invalid JSON");
|
|
159
|
+
if (runControllerId !== undefined || runGoal !== undefined || stateJson !== undefined) {
|
|
160
|
+
throw new Error("resume action does not accept flowControllerId, flowGoal, or flowStateJson");
|
|
169
161
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
throw new Error("lobster returned invalid JSON envelope");
|
|
162
|
+
if (!flowId) {
|
|
163
|
+
throw new Error("flowId required when using managed TaskFlow resume mode");
|
|
173
164
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (ok === true || ok === false) {
|
|
177
|
-
return parsed as LobsterEnvelope;
|
|
165
|
+
if (expectedRevision === undefined) {
|
|
166
|
+
throw new Error("flowExpectedRevision required when using managed TaskFlow resume mode");
|
|
178
167
|
}
|
|
168
|
+
if (!token && !approvalId) {
|
|
169
|
+
throw new Error("token or approvalId required when using managed TaskFlow resume mode");
|
|
170
|
+
}
|
|
171
|
+
if (approve === undefined) {
|
|
172
|
+
throw new Error("approve required when using managed TaskFlow resume mode");
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
flowId,
|
|
176
|
+
expectedRevision,
|
|
177
|
+
...(currentStep ? { currentStep } : {}),
|
|
178
|
+
...(waitingStep ? { waitingStep } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
function formatManagedFlowResult(result: ManagedFlowSuccessResult) {
|
|
183
|
+
const envelope =
|
|
184
|
+
result.envelope && typeof result.envelope === "object" && !Array.isArray(result.envelope)
|
|
185
|
+
? result.envelope
|
|
186
|
+
: { envelope: result.envelope };
|
|
187
|
+
const details = {
|
|
188
|
+
...envelope,
|
|
189
|
+
flow: result.flow,
|
|
190
|
+
mutation: result.mutation,
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
|
|
194
|
+
details,
|
|
195
|
+
};
|
|
181
196
|
}
|
|
182
197
|
|
|
183
|
-
function
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
if (!pipeline.trim()) {
|
|
187
|
-
throw new Error("pipeline required");
|
|
188
|
-
}
|
|
189
|
-
const argv = ["run", "--mode", "tool", pipeline];
|
|
190
|
-
const argsJson = typeof params.argsJson === "string" ? params.argsJson : "";
|
|
191
|
-
if (argsJson.trim()) {
|
|
192
|
-
argv.push("--args-json", argsJson);
|
|
193
|
-
}
|
|
194
|
-
return argv;
|
|
198
|
+
function requireTaskFlowRuntime(taskFlow: BoundTaskFlow | undefined, action: "run" | "resume") {
|
|
199
|
+
if (!taskFlow) {
|
|
200
|
+
throw new Error(`Managed TaskFlow ${action} mode requires a bound taskFlow runtime`);
|
|
195
201
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (typeof approve !== "boolean") {
|
|
203
|
-
throw new Error("approve required");
|
|
204
|
-
}
|
|
205
|
-
return ["resume", "--token", token, "--approve", approve ? "yes" : "no"];
|
|
202
|
+
return taskFlow;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveManagedFlowToolResult(result: ManagedLobsterFlowResult) {
|
|
206
|
+
if (!result.ok) {
|
|
207
|
+
throw result.error;
|
|
206
208
|
}
|
|
207
|
-
|
|
209
|
+
return formatManagedFlowResult(result);
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
export function createLobsterTool(api: OpenClawPluginApi) {
|
|
212
|
+
export function createLobsterTool(api: OpenClawPluginApi, options?: LobsterToolOptions) {
|
|
213
|
+
const runner = options?.runner ?? createEmbeddedLobsterRunner();
|
|
211
214
|
return {
|
|
212
215
|
name: "lobster",
|
|
213
216
|
label: "Lobster Workflow",
|
|
@@ -219,6 +222,7 @@ export function createLobsterTool(api: OpenClawPluginApi) {
|
|
|
219
222
|
pipeline: Type.Optional(Type.String()),
|
|
220
223
|
argsJson: Type.Optional(Type.String()),
|
|
221
224
|
token: Type.Optional(Type.String()),
|
|
225
|
+
approvalId: Type.Optional(Type.String()),
|
|
222
226
|
approve: Type.Optional(Type.Boolean()),
|
|
223
227
|
cwd: Type.Optional(
|
|
224
228
|
Type.String({
|
|
@@ -228,35 +232,85 @@ export function createLobsterTool(api: OpenClawPluginApi) {
|
|
|
228
232
|
),
|
|
229
233
|
timeoutMs: Type.Optional(Type.Number()),
|
|
230
234
|
maxStdoutBytes: Type.Optional(Type.Number()),
|
|
235
|
+
flowControllerId: Type.Optional(Type.String()),
|
|
236
|
+
flowGoal: Type.Optional(Type.String()),
|
|
237
|
+
flowStateJson: Type.Optional(Type.String()),
|
|
238
|
+
flowId: Type.Optional(Type.String()),
|
|
239
|
+
flowExpectedRevision: Type.Optional(Type.Number()),
|
|
240
|
+
flowCurrentStep: Type.Optional(Type.String()),
|
|
241
|
+
flowWaitingStep: Type.Optional(Type.String()),
|
|
231
242
|
}),
|
|
232
243
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
233
244
|
const action = typeof params.action === "string" ? params.action.trim() : "";
|
|
234
245
|
if (!action) {
|
|
235
246
|
throw new Error("action required");
|
|
236
247
|
}
|
|
248
|
+
if (action !== "run" && action !== "resume") {
|
|
249
|
+
throw new Error(`Unknown action: ${action}`);
|
|
250
|
+
}
|
|
237
251
|
|
|
238
|
-
const
|
|
239
|
-
const cwd = resolveCwd(params.cwd);
|
|
252
|
+
const cwd = resolveLobsterCwd(params.cwd);
|
|
240
253
|
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
|
|
241
254
|
const maxStdoutBytes =
|
|
242
255
|
typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
|
|
243
256
|
|
|
244
|
-
const argv = buildLobsterArgv(action, params);
|
|
245
|
-
|
|
246
257
|
if (api.runtime?.version && api.logger?.debug) {
|
|
247
258
|
api.logger.debug(`lobster plugin runtime=${api.runtime.version}`);
|
|
248
259
|
}
|
|
249
260
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
261
|
+
const runnerParams: LobsterRunnerParams = {
|
|
262
|
+
action,
|
|
263
|
+
...(typeof params.pipeline === "string" ? { pipeline: params.pipeline } : {}),
|
|
264
|
+
...(typeof params.argsJson === "string" ? { argsJson: params.argsJson } : {}),
|
|
265
|
+
...(typeof params.token === "string" ? { token: params.token } : {}),
|
|
266
|
+
...(typeof params.approvalId === "string" ? { approvalId: params.approvalId } : {}),
|
|
267
|
+
...(typeof params.approve === "boolean" ? { approve: params.approve } : {}),
|
|
253
268
|
cwd,
|
|
254
269
|
timeoutMs,
|
|
255
270
|
maxStdoutBytes,
|
|
256
|
-
}
|
|
271
|
+
};
|
|
257
272
|
|
|
258
|
-
const
|
|
273
|
+
const taskFlow = options?.taskFlow;
|
|
274
|
+
if (action === "run") {
|
|
275
|
+
const flowParams = parseRunFlowParams(params);
|
|
276
|
+
if (flowParams) {
|
|
277
|
+
return resolveManagedFlowToolResult(
|
|
278
|
+
await runManagedLobsterFlow({
|
|
279
|
+
taskFlow: requireTaskFlowRuntime(taskFlow, "run"),
|
|
280
|
+
runner,
|
|
281
|
+
runnerParams,
|
|
282
|
+
controllerId: flowParams.controllerId,
|
|
283
|
+
goal: flowParams.goal,
|
|
284
|
+
...(flowParams.stateJson !== undefined ? { stateJson: flowParams.stateJson } : {}),
|
|
285
|
+
...(flowParams.currentStep ? { currentStep: flowParams.currentStep } : {}),
|
|
286
|
+
...(flowParams.waitingStep ? { waitingStep: flowParams.waitingStep } : {}),
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
const flowParams = parseResumeFlowParams(params);
|
|
292
|
+
if (flowParams) {
|
|
293
|
+
return resolveManagedFlowToolResult(
|
|
294
|
+
await resumeManagedLobsterFlow({
|
|
295
|
+
taskFlow: requireTaskFlowRuntime(taskFlow, "resume"),
|
|
296
|
+
runner,
|
|
297
|
+
runnerParams: runnerParams as LobsterRunnerParams & {
|
|
298
|
+
action: "resume";
|
|
299
|
+
approve: boolean;
|
|
300
|
+
} & ({ token: string } | { approvalId: string }),
|
|
301
|
+
flowId: flowParams.flowId,
|
|
302
|
+
expectedRevision: flowParams.expectedRevision,
|
|
303
|
+
...(flowParams.currentStep ? { currentStep: flowParams.currentStep } : {}),
|
|
304
|
+
...(flowParams.waitingStep ? { waitingStep: flowParams.waitingStep } : {}),
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
259
309
|
|
|
310
|
+
const envelope = await runner.run(runnerParams);
|
|
311
|
+
if (!envelope.ok) {
|
|
312
|
+
throw new Error(envelope.error.message);
|
|
313
|
+
}
|
|
260
314
|
return {
|
|
261
315
|
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
|
|
262
316
|
details: envelope,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import type { OpenClawPluginApi } from "../runtime-api.js";
|
|
3
|
+
|
|
4
|
+
type BoundTaskFlow = ReturnType<
|
|
5
|
+
NonNullable<OpenClawPluginApi["runtime"]>["tasks"]["managedFlows"]["bindSession"]
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function createFakeTaskFlow(overrides?: Partial<BoundTaskFlow>): BoundTaskFlow {
|
|
9
|
+
const baseFlow = {
|
|
10
|
+
flowId: "flow-1",
|
|
11
|
+
revision: 1,
|
|
12
|
+
syncMode: "managed" as const,
|
|
13
|
+
controllerId: "tests/lobster",
|
|
14
|
+
ownerKey: "agent:main:main",
|
|
15
|
+
status: "running" as const,
|
|
16
|
+
goal: "Run Lobster workflow",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
sessionKey: "agent:main:main",
|
|
21
|
+
createManaged: vi.fn().mockReturnValue(baseFlow),
|
|
22
|
+
get: vi.fn(),
|
|
23
|
+
list: vi.fn().mockReturnValue([]),
|
|
24
|
+
findLatest: vi.fn(),
|
|
25
|
+
resolve: vi.fn(),
|
|
26
|
+
getTaskSummary: vi.fn(),
|
|
27
|
+
setWaiting: vi.fn().mockImplementation((input) => ({
|
|
28
|
+
applied: true,
|
|
29
|
+
flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "waiting" as const },
|
|
30
|
+
})),
|
|
31
|
+
resume: vi.fn().mockImplementation((input) => ({
|
|
32
|
+
applied: true,
|
|
33
|
+
flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "running" as const },
|
|
34
|
+
})),
|
|
35
|
+
finish: vi.fn().mockImplementation((input) => ({
|
|
36
|
+
applied: true,
|
|
37
|
+
flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "completed" as const },
|
|
38
|
+
})),
|
|
39
|
+
fail: vi.fn().mockImplementation((input) => ({
|
|
40
|
+
applied: true,
|
|
41
|
+
flow: { ...baseFlow, revision: input.expectedRevision + 1, status: "failed" as const },
|
|
42
|
+
})),
|
|
43
|
+
requestCancel: vi.fn(),
|
|
44
|
+
cancel: vi.fn(),
|
|
45
|
+
runTask: vi.fn(),
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.package-boundary.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "."
|
|
5
|
+
},
|
|
6
|
+
"include": ["./*.ts", "./src/**/*.ts"],
|
|
7
|
+
"exclude": [
|
|
8
|
+
"./**/*.test.ts",
|
|
9
|
+
"./dist/**",
|
|
10
|
+
"./node_modules/**",
|
|
11
|
+
"./src/test-support/**",
|
|
12
|
+
"./src/**/*test-helpers.ts",
|
|
13
|
+
"./src/**/*test-harness.ts",
|
|
14
|
+
"./src/**/*test-support.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
package/src/test-helpers.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
type PathEnvKey = "PATH" | "Path" | "PATHEXT" | "Pathext";
|
|
2
|
-
|
|
3
|
-
const PATH_ENV_KEYS = ["PATH", "Path", "PATHEXT", "Pathext"] as const;
|
|
4
|
-
|
|
5
|
-
export type PlatformPathEnvSnapshot = {
|
|
6
|
-
platformDescriptor: PropertyDescriptor | undefined;
|
|
7
|
-
env: Record<PathEnvKey, string | undefined>;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function setProcessPlatform(platform: NodeJS.Platform): void {
|
|
11
|
-
Object.defineProperty(process, "platform", {
|
|
12
|
-
value: platform,
|
|
13
|
-
configurable: true,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function snapshotPlatformPathEnv(): PlatformPathEnvSnapshot {
|
|
18
|
-
return {
|
|
19
|
-
platformDescriptor: Object.getOwnPropertyDescriptor(process, "platform"),
|
|
20
|
-
env: {
|
|
21
|
-
PATH: process.env.PATH,
|
|
22
|
-
Path: process.env.Path,
|
|
23
|
-
PATHEXT: process.env.PATHEXT,
|
|
24
|
-
Pathext: process.env.Pathext,
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function restorePlatformPathEnv(snapshot: PlatformPathEnvSnapshot): void {
|
|
30
|
-
if (snapshot.platformDescriptor) {
|
|
31
|
-
Object.defineProperty(process, "platform", snapshot.platformDescriptor);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
for (const key of PATH_ENV_KEYS) {
|
|
35
|
-
const value = snapshot.env[key];
|
|
36
|
-
if (value === undefined) {
|
|
37
|
-
delete process.env[key];
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
process.env[key] = value;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export { createWindowsCmdShimFixture } from "../../shared/windows-cmd-shim-test-fixtures.js";
|