@openclaw/lobster 2026.5.2 → 2026.5.3-beta.2
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/dist/index.js +654 -0
- package/dist/runtime-api.js +3 -0
- package/package.json +20 -3
- package/index.ts +0 -24
- package/runtime-api.ts +0 -12
- package/src/lobster-ajv-cache.ts +0 -142
- package/src/lobster-core.d.ts +0 -60
- package/src/lobster-runner.test.ts +0 -572
- package/src/lobster-runner.ts +0 -395
- package/src/lobster-taskflow.test.ts +0 -227
- package/src/lobster-taskflow.ts +0 -279
- package/src/lobster-tool.test.ts +0 -353
- package/src/lobster-tool.ts +0 -320
- package/src/taskflow-test-helpers.ts +0 -48
- package/tsconfig.json +0 -16
package/src/lobster-tool.ts
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
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[]
|
|
25
|
-
| {
|
|
26
|
-
[key: string]: JsonLike;
|
|
27
|
-
};
|
|
28
|
-
|
|
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;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function readOptionalNumber(value: unknown, fieldName: string): number | undefined {
|
|
68
|
-
if (value === undefined) {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
72
|
-
throw new Error(`${fieldName} must be an integer`);
|
|
73
|
-
}
|
|
74
|
-
return value;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readOptionalBoolean(value: unknown, fieldName: string): boolean | undefined {
|
|
78
|
-
if (value === undefined) {
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
if (typeof value !== "boolean") {
|
|
82
|
-
throw new Error(`${fieldName} must be a boolean`);
|
|
83
|
-
}
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
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");
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
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 } : {}),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
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;
|
|
158
|
-
}
|
|
159
|
-
if (runControllerId !== undefined || runGoal !== undefined || stateJson !== undefined) {
|
|
160
|
-
throw new Error("resume action does not accept flowControllerId, flowGoal, or flowStateJson");
|
|
161
|
-
}
|
|
162
|
-
if (!flowId) {
|
|
163
|
-
throw new Error("flowId required when using managed TaskFlow resume mode");
|
|
164
|
-
}
|
|
165
|
-
if (expectedRevision === undefined) {
|
|
166
|
-
throw new Error("flowExpectedRevision required when using managed TaskFlow resume mode");
|
|
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
|
-
}
|
|
181
|
-
|
|
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
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
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`);
|
|
201
|
-
}
|
|
202
|
-
return taskFlow;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function resolveManagedFlowToolResult(result: ManagedLobsterFlowResult) {
|
|
206
|
-
if (!result.ok) {
|
|
207
|
-
throw result.error;
|
|
208
|
-
}
|
|
209
|
-
return formatManagedFlowResult(result);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function createLobsterTool(api: OpenClawPluginApi, options?: LobsterToolOptions) {
|
|
213
|
-
const runner = options?.runner ?? createEmbeddedLobsterRunner();
|
|
214
|
-
return {
|
|
215
|
-
name: "lobster",
|
|
216
|
-
label: "Lobster Workflow",
|
|
217
|
-
description:
|
|
218
|
-
"Run Lobster pipelines as a local-first workflow runtime (typed JSON envelope + resumable approvals).",
|
|
219
|
-
parameters: Type.Object({
|
|
220
|
-
// NOTE: Prefer string enums in tool schemas; some providers reject unions/anyOf.
|
|
221
|
-
action: Type.Unsafe<"run" | "resume">({ type: "string", enum: ["run", "resume"] }),
|
|
222
|
-
pipeline: Type.Optional(Type.String()),
|
|
223
|
-
argsJson: Type.Optional(Type.String()),
|
|
224
|
-
token: Type.Optional(Type.String()),
|
|
225
|
-
approvalId: Type.Optional(Type.String()),
|
|
226
|
-
approve: Type.Optional(Type.Boolean()),
|
|
227
|
-
cwd: Type.Optional(
|
|
228
|
-
Type.String({
|
|
229
|
-
description:
|
|
230
|
-
"Relative working directory (optional). Must stay within the gateway working directory.",
|
|
231
|
-
}),
|
|
232
|
-
),
|
|
233
|
-
timeoutMs: Type.Optional(Type.Number()),
|
|
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()),
|
|
242
|
-
}),
|
|
243
|
-
async execute(_id: string, params: Record<string, unknown>) {
|
|
244
|
-
const action = typeof params.action === "string" ? params.action.trim() : "";
|
|
245
|
-
if (!action) {
|
|
246
|
-
throw new Error("action required");
|
|
247
|
-
}
|
|
248
|
-
if (action !== "run" && action !== "resume") {
|
|
249
|
-
throw new Error(`Unknown action: ${action}`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const cwd = resolveLobsterCwd(params.cwd);
|
|
253
|
-
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 20_000;
|
|
254
|
-
const maxStdoutBytes =
|
|
255
|
-
typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512_000;
|
|
256
|
-
|
|
257
|
-
if (api.runtime?.version && api.logger?.debug) {
|
|
258
|
-
api.logger.debug(`lobster plugin runtime=${api.runtime.version}`);
|
|
259
|
-
}
|
|
260
|
-
|
|
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 } : {}),
|
|
268
|
-
cwd,
|
|
269
|
-
timeoutMs,
|
|
270
|
-
maxStdoutBytes,
|
|
271
|
-
};
|
|
272
|
-
|
|
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
|
-
}
|
|
309
|
-
|
|
310
|
-
const envelope = await runner.run(runnerParams);
|
|
311
|
-
if (!envelope.ok) {
|
|
312
|
-
throw new Error(envelope.error.message);
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
|
|
316
|
-
details: envelope,
|
|
317
|
-
};
|
|
318
|
-
},
|
|
319
|
-
};
|
|
320
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
}
|