libretto 0.6.21 → 0.6.22
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 +5 -1
- package/README.template.md +5 -1
- package/dist/cli/commands/execution.js +8 -1
- package/dist/cli/core/browser.js +8 -3
- package/dist/cli/core/daemon/daemon.js +8 -6
- package/dist/cli/core/providers/kernel.js +107 -29
- package/dist/cli/core/providers/steel.js +10 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +15 -1
- package/dist/runtime/recovery/agent.d.ts +50 -2
- package/dist/runtime/recovery/agent.js +159 -45
- package/dist/runtime/recovery/index.d.ts +2 -1
- package/dist/runtime/recovery/index.js +16 -2
- package/dist/runtime/recovery/page-fallbacks.d.ts +45 -0
- package/dist/runtime/recovery/page-fallbacks.js +342 -0
- package/dist/shared/state/index.d.ts +1 -1
- package/dist/shared/state/session-state.d.ts +4 -1
- package/dist/shared/state/session-state.js +2 -1
- package/dist/shared/workflow/workflow.d.ts +19 -6
- package/dist/shared/workflow/workflow.js +38 -9
- package/docs/reference/runtime/page-fallbacks.mdx +85 -0
- package/docs/understand-libretto/error-handling-and-recovery.mdx +45 -0
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +8 -2
- package/skills/libretto/references/code-generation-rules.md +23 -6
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/execution.ts +8 -1
- package/src/cli/core/browser.ts +7 -2
- package/src/cli/core/daemon/daemon.ts +9 -4
- package/src/cli/core/daemon/ipc.ts +1 -0
- package/src/cli/core/providers/kernel.ts +153 -29
- package/src/cli/core/providers/steel.ts +11 -1
- package/src/cli/core/providers/types.ts +3 -0
- package/src/index.ts +22 -2
- package/src/runtime/recovery/agent.ts +227 -50
- package/src/runtime/recovery/index.ts +21 -1
- package/src/runtime/recovery/page-fallbacks.ts +476 -0
- package/src/shared/state/index.ts +1 -0
- package/src/shared/state/session-state.ts +2 -0
- package/src/shared/workflow/workflow.ts +90 -20
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import type { FrameLocator, Locator, Page } from "playwright";
|
|
2
|
+
import type { LanguageModel } from "ai";
|
|
3
|
+
import { executeRecoveryAgent, type RecoveryAgentResult } from "./agent.js";
|
|
4
|
+
|
|
5
|
+
export type RecoveryActionTargetType = "page" | "locator";
|
|
6
|
+
|
|
7
|
+
export type RecoveryActionContext = {
|
|
8
|
+
page: Page;
|
|
9
|
+
targetType: RecoveryActionTargetType;
|
|
10
|
+
method: string;
|
|
11
|
+
args: readonly unknown[];
|
|
12
|
+
error: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RecoveryActionResult = Record<string, unknown> | void;
|
|
16
|
+
|
|
17
|
+
export type RecoveryActionHandler = (
|
|
18
|
+
context: RecoveryActionContext,
|
|
19
|
+
) => Promise<RecoveryActionResult>;
|
|
20
|
+
|
|
21
|
+
export type RecoveryAction = RecoveryActionHandler;
|
|
22
|
+
|
|
23
|
+
export type RecoveryActionOptions = {
|
|
24
|
+
recoveryAction: RecoveryAction;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ComputerUseRecoveryModelOptions =
|
|
28
|
+
| {
|
|
29
|
+
languageModel: LanguageModel;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
provider: "openai";
|
|
33
|
+
apiKey: string;
|
|
34
|
+
model?: "gpt-5.5";
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
provider: "anthropic";
|
|
38
|
+
apiKey: string;
|
|
39
|
+
model?: "claude-sonnet-4-6";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ComputerUseRecoveryActionOptions = ComputerUseRecoveryModelOptions & {
|
|
43
|
+
instruction: string;
|
|
44
|
+
maxSteps?: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type PopupRecoveryActionOptions = ComputerUseRecoveryModelOptions & {
|
|
48
|
+
maxSteps?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const POPUP_RECOVERY_INSTRUCTION = [
|
|
52
|
+
"Look at the page for any popup, modal, cookie banner, overlay, dialog, or interstitial that blocks interaction.",
|
|
53
|
+
"If any blocking popup is visible, close it before returning done.",
|
|
54
|
+
"Prefer obvious close, dismiss, continue, accept, or X buttons.",
|
|
55
|
+
"Do not return done while a blocking overlay or dialog is still visible.",
|
|
56
|
+
].join(" ");
|
|
57
|
+
|
|
58
|
+
export const COMPUTER_USE_RECOVERY_MODELS = {
|
|
59
|
+
anthropic: "claude-sonnet-4-6",
|
|
60
|
+
openai: "gpt-5.5",
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
const PAGE_UI_METHODS = new Set([
|
|
64
|
+
"click",
|
|
65
|
+
"dblclick",
|
|
66
|
+
"tap",
|
|
67
|
+
"hover",
|
|
68
|
+
"fill",
|
|
69
|
+
"type",
|
|
70
|
+
"press",
|
|
71
|
+
"pressSequentially",
|
|
72
|
+
"check",
|
|
73
|
+
"uncheck",
|
|
74
|
+
"setChecked",
|
|
75
|
+
"selectOption",
|
|
76
|
+
"setInputFiles",
|
|
77
|
+
"selectText",
|
|
78
|
+
"dispatchEvent",
|
|
79
|
+
"focus",
|
|
80
|
+
"blur",
|
|
81
|
+
"dragAndDrop",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const PAGE_READ_METHODS = new Set([
|
|
85
|
+
"title",
|
|
86
|
+
"content",
|
|
87
|
+
"screenshot",
|
|
88
|
+
"waitForLoadState",
|
|
89
|
+
"waitForRequest",
|
|
90
|
+
"waitForResponse",
|
|
91
|
+
"waitForURL",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const LOCATOR_UI_METHODS = new Set([
|
|
95
|
+
"click",
|
|
96
|
+
"dblclick",
|
|
97
|
+
"tap",
|
|
98
|
+
"hover",
|
|
99
|
+
"fill",
|
|
100
|
+
"type",
|
|
101
|
+
"press",
|
|
102
|
+
"pressSequentially",
|
|
103
|
+
"check",
|
|
104
|
+
"uncheck",
|
|
105
|
+
"setChecked",
|
|
106
|
+
"selectOption",
|
|
107
|
+
"setInputFiles",
|
|
108
|
+
"selectText",
|
|
109
|
+
"dispatchEvent",
|
|
110
|
+
"focus",
|
|
111
|
+
"blur",
|
|
112
|
+
"clear",
|
|
113
|
+
"dragTo",
|
|
114
|
+
"scrollIntoViewIfNeeded",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const LOCATOR_READ_METHODS = new Set([
|
|
118
|
+
"textContent",
|
|
119
|
+
"innerText",
|
|
120
|
+
"innerHTML",
|
|
121
|
+
"allTextContents",
|
|
122
|
+
"allInnerTexts",
|
|
123
|
+
"ariaSnapshot",
|
|
124
|
+
"boundingBox",
|
|
125
|
+
"count",
|
|
126
|
+
"getAttribute",
|
|
127
|
+
"inputValue",
|
|
128
|
+
"isChecked",
|
|
129
|
+
"isDisabled",
|
|
130
|
+
"isEditable",
|
|
131
|
+
"isEnabled",
|
|
132
|
+
"isVisible",
|
|
133
|
+
"isHidden",
|
|
134
|
+
"screenshot",
|
|
135
|
+
"waitFor",
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const PAGE_LOCATOR_FACTORY_METHODS = new Set([
|
|
139
|
+
"locator",
|
|
140
|
+
"getByRole",
|
|
141
|
+
"getByText",
|
|
142
|
+
"getByLabel",
|
|
143
|
+
"getByPlaceholder",
|
|
144
|
+
"getByAltText",
|
|
145
|
+
"getByTitle",
|
|
146
|
+
"getByTestId",
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const LOCATOR_FACTORY_METHODS = new Set([
|
|
150
|
+
"locator",
|
|
151
|
+
"getByRole",
|
|
152
|
+
"getByText",
|
|
153
|
+
"getByLabel",
|
|
154
|
+
"getByPlaceholder",
|
|
155
|
+
"getByAltText",
|
|
156
|
+
"getByTitle",
|
|
157
|
+
"getByTestId",
|
|
158
|
+
"filter",
|
|
159
|
+
"and",
|
|
160
|
+
"or",
|
|
161
|
+
"first",
|
|
162
|
+
"last",
|
|
163
|
+
"nth",
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
const FRAME_LOCATOR_FACTORY_METHODS = new Set([
|
|
167
|
+
"locator",
|
|
168
|
+
"getByRole",
|
|
169
|
+
"getByText",
|
|
170
|
+
"getByLabel",
|
|
171
|
+
"getByPlaceholder",
|
|
172
|
+
"getByAltText",
|
|
173
|
+
"getByTitle",
|
|
174
|
+
"getByTestId",
|
|
175
|
+
"owner",
|
|
176
|
+
"first",
|
|
177
|
+
"last",
|
|
178
|
+
"nth",
|
|
179
|
+
"frameLocator",
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
function isUiMethod(
|
|
183
|
+
targetType: RecoveryActionTargetType,
|
|
184
|
+
method: string,
|
|
185
|
+
): boolean {
|
|
186
|
+
return targetType === "page"
|
|
187
|
+
? PAGE_UI_METHODS.has(method)
|
|
188
|
+
: LOCATOR_UI_METHODS.has(method);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isReadMethod(
|
|
192
|
+
targetType: RecoveryActionTargetType,
|
|
193
|
+
method: string,
|
|
194
|
+
): boolean {
|
|
195
|
+
return targetType === "page"
|
|
196
|
+
? PAGE_READ_METHODS.has(method)
|
|
197
|
+
: LOCATOR_READ_METHODS.has(method);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isSupportedMethod(
|
|
201
|
+
targetType: RecoveryActionTargetType,
|
|
202
|
+
method: string,
|
|
203
|
+
): boolean {
|
|
204
|
+
return isUiMethod(targetType, method) || isReadMethod(targetType, method);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runWithFallback<T>(args: {
|
|
208
|
+
page: Page;
|
|
209
|
+
targetType: RecoveryActionTargetType;
|
|
210
|
+
method: string;
|
|
211
|
+
methodArgs: readonly unknown[];
|
|
212
|
+
invoke: () => T | Promise<T>;
|
|
213
|
+
options: RecoveryActionOptions;
|
|
214
|
+
}): Promise<T> {
|
|
215
|
+
try {
|
|
216
|
+
return await args.invoke();
|
|
217
|
+
} catch (originalError) {
|
|
218
|
+
const baseContext = {
|
|
219
|
+
page: args.page,
|
|
220
|
+
targetType: args.targetType,
|
|
221
|
+
method: args.method,
|
|
222
|
+
args: args.methodArgs,
|
|
223
|
+
} as const;
|
|
224
|
+
if (!isSupportedMethod(baseContext.targetType, baseContext.method)) {
|
|
225
|
+
throw originalError;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await args.options.recoveryAction({
|
|
230
|
+
...baseContext,
|
|
231
|
+
error: originalError,
|
|
232
|
+
});
|
|
233
|
+
return await args.invoke();
|
|
234
|
+
} catch {
|
|
235
|
+
throw originalError;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type ProxyCaches = {
|
|
241
|
+
locators: WeakMap<Locator, Locator>;
|
|
242
|
+
frameLocators: WeakMap<FrameLocator, FrameLocator>;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function bindOrWrapLocatorMethod(
|
|
246
|
+
locator: Locator,
|
|
247
|
+
rawPage: Page,
|
|
248
|
+
method: string,
|
|
249
|
+
value: unknown,
|
|
250
|
+
options: RecoveryActionOptions,
|
|
251
|
+
caches: ProxyCaches,
|
|
252
|
+
): unknown {
|
|
253
|
+
if (typeof value !== "function") return value;
|
|
254
|
+
|
|
255
|
+
if (LOCATOR_FACTORY_METHODS.has(method)) {
|
|
256
|
+
return (...args: unknown[]) => {
|
|
257
|
+
const nextLocator = value.apply(locator, args) as Locator;
|
|
258
|
+
return createFallbackLocator(nextLocator, rawPage, options, caches);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (method === "all") {
|
|
263
|
+
return async (...args: unknown[]) => {
|
|
264
|
+
const locators = (await value.apply(locator, args)) as Locator[];
|
|
265
|
+
return locators.map((nextLocator) =>
|
|
266
|
+
createFallbackLocator(nextLocator, rawPage, options, caches),
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (method === "contentFrame") {
|
|
272
|
+
return (...args: unknown[]) => {
|
|
273
|
+
const frameLocator = value.apply(locator, args) as FrameLocator;
|
|
274
|
+
return createFallbackFrameLocator(frameLocator, rawPage, options, caches);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!isSupportedMethod("locator", method)) {
|
|
279
|
+
return value.bind(locator);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (...args: unknown[]) =>
|
|
283
|
+
runWithFallback({
|
|
284
|
+
page: rawPage,
|
|
285
|
+
targetType: "locator",
|
|
286
|
+
method,
|
|
287
|
+
methodArgs: args,
|
|
288
|
+
invoke: () => value.apply(locator, args),
|
|
289
|
+
options,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function createFallbackLocator(
|
|
294
|
+
locator: Locator,
|
|
295
|
+
rawPage: Page,
|
|
296
|
+
options: RecoveryActionOptions,
|
|
297
|
+
caches: ProxyCaches,
|
|
298
|
+
): Locator {
|
|
299
|
+
const cached = caches.locators.get(locator);
|
|
300
|
+
if (cached) return cached;
|
|
301
|
+
|
|
302
|
+
const proxy = new Proxy(locator, {
|
|
303
|
+
get(target, prop, receiver) {
|
|
304
|
+
if (typeof prop !== "string") {
|
|
305
|
+
return Reflect.get(target, prop, receiver);
|
|
306
|
+
}
|
|
307
|
+
return bindOrWrapLocatorMethod(
|
|
308
|
+
target,
|
|
309
|
+
rawPage,
|
|
310
|
+
prop,
|
|
311
|
+
Reflect.get(target, prop, target),
|
|
312
|
+
options,
|
|
313
|
+
caches,
|
|
314
|
+
);
|
|
315
|
+
},
|
|
316
|
+
}) as Locator;
|
|
317
|
+
|
|
318
|
+
caches.locators.set(locator, proxy);
|
|
319
|
+
return proxy;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function createFallbackFrameLocator(
|
|
323
|
+
frameLocator: FrameLocator,
|
|
324
|
+
rawPage: Page,
|
|
325
|
+
options: RecoveryActionOptions,
|
|
326
|
+
caches: ProxyCaches,
|
|
327
|
+
): FrameLocator {
|
|
328
|
+
const cached = caches.frameLocators.get(frameLocator);
|
|
329
|
+
if (cached) return cached;
|
|
330
|
+
|
|
331
|
+
const proxy = new Proxy(frameLocator, {
|
|
332
|
+
get(target, prop, receiver) {
|
|
333
|
+
if (typeof prop !== "string") {
|
|
334
|
+
return Reflect.get(target, prop, receiver);
|
|
335
|
+
}
|
|
336
|
+
const value = Reflect.get(target, prop, target);
|
|
337
|
+
if (typeof value !== "function") return value;
|
|
338
|
+
|
|
339
|
+
if (FRAME_LOCATOR_FACTORY_METHODS.has(prop)) {
|
|
340
|
+
return (...args: unknown[]) => {
|
|
341
|
+
const result = value.apply(target, args);
|
|
342
|
+
if (prop === "first" || prop === "last" || prop === "nth") {
|
|
343
|
+
return createFallbackFrameLocator(
|
|
344
|
+
result as FrameLocator,
|
|
345
|
+
rawPage,
|
|
346
|
+
options,
|
|
347
|
+
caches,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (prop === "frameLocator") {
|
|
351
|
+
return createFallbackFrameLocator(
|
|
352
|
+
result as FrameLocator,
|
|
353
|
+
rawPage,
|
|
354
|
+
options,
|
|
355
|
+
caches,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return createFallbackLocator(
|
|
359
|
+
result as Locator,
|
|
360
|
+
rawPage,
|
|
361
|
+
options,
|
|
362
|
+
caches,
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return value.bind(target);
|
|
368
|
+
},
|
|
369
|
+
}) as FrameLocator;
|
|
370
|
+
|
|
371
|
+
caches.frameLocators.set(frameLocator, proxy);
|
|
372
|
+
return proxy;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function createRecoveryPage(
|
|
376
|
+
page: Page,
|
|
377
|
+
options: RecoveryActionOptions,
|
|
378
|
+
): Page {
|
|
379
|
+
const caches: ProxyCaches = {
|
|
380
|
+
locators: new WeakMap(),
|
|
381
|
+
frameLocators: new WeakMap(),
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return new Proxy(page, {
|
|
385
|
+
get(target, prop, receiver) {
|
|
386
|
+
if (typeof prop !== "string") {
|
|
387
|
+
return Reflect.get(target, prop, receiver);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const value = Reflect.get(target, prop, target);
|
|
391
|
+
if (typeof value !== "function") return value;
|
|
392
|
+
|
|
393
|
+
if (PAGE_LOCATOR_FACTORY_METHODS.has(prop)) {
|
|
394
|
+
return (...args: unknown[]) => {
|
|
395
|
+
const locator = value.apply(target, args) as Locator;
|
|
396
|
+
return createFallbackLocator(locator, page, options, caches);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (prop === "frameLocator") {
|
|
401
|
+
return (...args: unknown[]) => {
|
|
402
|
+
const frameLocator = value.apply(target, args) as FrameLocator;
|
|
403
|
+
return createFallbackFrameLocator(frameLocator, page, options, caches);
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!isSupportedMethod("page", prop)) {
|
|
408
|
+
return value.bind(target);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return (...args: unknown[]) =>
|
|
412
|
+
runWithFallback({
|
|
413
|
+
page,
|
|
414
|
+
targetType: "page",
|
|
415
|
+
method: prop,
|
|
416
|
+
methodArgs: args,
|
|
417
|
+
invoke: () => value.apply(target, args),
|
|
418
|
+
options,
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
}) as Page;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function resolveComputerUseRecoveryModel(
|
|
425
|
+
options: ComputerUseRecoveryActionOptions,
|
|
426
|
+
): Promise<LanguageModel> {
|
|
427
|
+
if ("provider" in options) {
|
|
428
|
+
if (options.provider === "openai") {
|
|
429
|
+
const model: string = options.model ?? COMPUTER_USE_RECOVERY_MODELS.openai;
|
|
430
|
+
if (model !== COMPUTER_USE_RECOVERY_MODELS.openai) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Unsupported OpenAI computer use recovery model "${model}". Supported model: ${COMPUTER_USE_RECOVERY_MODELS.openai}.`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return import("@ai-sdk/openai").then(({ createOpenAI }) =>
|
|
436
|
+
createOpenAI({ apiKey: options.apiKey })(model),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const model: string =
|
|
440
|
+
options.model ?? COMPUTER_USE_RECOVERY_MODELS.anthropic;
|
|
441
|
+
if (model !== COMPUTER_USE_RECOVERY_MODELS.anthropic) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`Unsupported Anthropic computer use recovery model "${model}". Supported model: ${COMPUTER_USE_RECOVERY_MODELS.anthropic}.`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return import("@ai-sdk/anthropic").then(({ createAnthropic }) =>
|
|
447
|
+
createAnthropic({ apiKey: options.apiKey })(model),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return options.languageModel;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function computerUseRecoveryAction(
|
|
455
|
+
options: ComputerUseRecoveryActionOptions,
|
|
456
|
+
): RecoveryAction {
|
|
457
|
+
return async ({ page }): Promise<RecoveryAgentResult> => {
|
|
458
|
+
const model = await resolveComputerUseRecoveryModel(options);
|
|
459
|
+
return executeRecoveryAgent(
|
|
460
|
+
page,
|
|
461
|
+
options.instruction,
|
|
462
|
+
undefined,
|
|
463
|
+
model,
|
|
464
|
+
options.maxSteps,
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function popupRecoveryAction(
|
|
470
|
+
options: PopupRecoveryActionOptions,
|
|
471
|
+
): RecoveryAction {
|
|
472
|
+
return computerUseRecoveryAction({
|
|
473
|
+
...options,
|
|
474
|
+
instruction: POPUP_RECOVERY_INSTRUCTION,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
@@ -19,6 +19,7 @@ export const SessionViewportSchema = z.object({
|
|
|
19
19
|
export const ProviderStateSchema = z.object({
|
|
20
20
|
name: z.string(),
|
|
21
21
|
sessionId: z.string(),
|
|
22
|
+
recordingUrl: z.string().url().optional(),
|
|
22
23
|
});
|
|
23
24
|
|
|
24
25
|
export const SessionStateFileSchema = z.object({
|
|
@@ -38,6 +39,7 @@ export const SessionStateFileSchema = z.object({
|
|
|
38
39
|
|
|
39
40
|
export type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
40
41
|
export type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
|
|
42
|
+
export type ProviderState = z.infer<typeof ProviderStateSchema>;
|
|
41
43
|
export type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
|
|
42
44
|
export type SessionState = Omit<SessionStateFile, "version">;
|
|
43
45
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Page } from "playwright";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createRecoveryPage,
|
|
5
|
+
type RecoveryAction,
|
|
6
|
+
} from "../../runtime/recovery/page-fallbacks.js";
|
|
3
7
|
|
|
4
8
|
export const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
|
|
5
9
|
|
|
@@ -13,12 +17,23 @@ export type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (
|
|
|
13
17
|
input: Input,
|
|
14
18
|
) => Promise<Output>;
|
|
15
19
|
|
|
16
|
-
export type
|
|
17
|
-
InputSchema extends z.ZodType
|
|
18
|
-
OutputSchema extends z.ZodType
|
|
20
|
+
export type LibrettoWorkflowDefinition<
|
|
21
|
+
InputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
22
|
+
OutputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
19
23
|
> = {
|
|
20
|
-
input
|
|
21
|
-
output
|
|
24
|
+
input?: InputSchema;
|
|
25
|
+
output?: OutputSchema;
|
|
26
|
+
recoveryAction?: RecoveryAction;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type LibrettoWorkflowOptions<
|
|
30
|
+
InputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
31
|
+
OutputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
32
|
+
> = LibrettoWorkflowDefinition<InputSchema, OutputSchema> & {
|
|
33
|
+
handler: LibrettoWorkflowHandler<
|
|
34
|
+
z.infer<InputSchema>,
|
|
35
|
+
z.infer<OutputSchema>
|
|
36
|
+
>;
|
|
22
37
|
};
|
|
23
38
|
|
|
24
39
|
// Thrown when input fails Zod validation. The runner surfaces `.message`
|
|
@@ -89,6 +104,7 @@ export class LibrettoWorkflow<
|
|
|
89
104
|
// this schema to JSON Schema at build time and exposes it via
|
|
90
105
|
// /v1/workflows/get so API consumers know the workflow's output shape.
|
|
91
106
|
public readonly outputSchema?: OutputSchema;
|
|
107
|
+
public readonly recoveryAction?: RecoveryAction;
|
|
92
108
|
private readonly handler: LibrettoWorkflowHandler<
|
|
93
109
|
z.infer<InputSchema>,
|
|
94
110
|
z.infer<OutputSchema>
|
|
@@ -96,15 +112,22 @@ export class LibrettoWorkflow<
|
|
|
96
112
|
|
|
97
113
|
constructor(
|
|
98
114
|
name: string,
|
|
99
|
-
|
|
115
|
+
options:
|
|
116
|
+
| {
|
|
117
|
+
inputSchema?: InputSchema;
|
|
118
|
+
outputSchema?: OutputSchema;
|
|
119
|
+
recoveryAction?: RecoveryAction;
|
|
120
|
+
}
|
|
121
|
+
| undefined,
|
|
100
122
|
handler: LibrettoWorkflowHandler<
|
|
101
123
|
z.infer<InputSchema>,
|
|
102
124
|
z.infer<OutputSchema>
|
|
103
125
|
>,
|
|
104
126
|
) {
|
|
105
127
|
this.name = name;
|
|
106
|
-
this.inputSchema =
|
|
107
|
-
this.outputSchema =
|
|
128
|
+
this.inputSchema = options?.inputSchema;
|
|
129
|
+
this.outputSchema = options?.outputSchema;
|
|
130
|
+
this.recoveryAction = options?.recoveryAction;
|
|
108
131
|
this.handler = handler;
|
|
109
132
|
}
|
|
110
133
|
|
|
@@ -113,7 +136,16 @@ export class LibrettoWorkflow<
|
|
|
113
136
|
input: unknown,
|
|
114
137
|
): Promise<z.infer<OutputSchema>> {
|
|
115
138
|
const parsed = parseWorkflowInput(this.name, this.inputSchema, input);
|
|
116
|
-
|
|
139
|
+
const workflowContext =
|
|
140
|
+
!this.recoveryAction
|
|
141
|
+
? ctx
|
|
142
|
+
: {
|
|
143
|
+
...ctx,
|
|
144
|
+
page: createRecoveryPage(ctx.page, {
|
|
145
|
+
recoveryAction: this.recoveryAction,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
return this.handler(workflowContext, parsed);
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
151
|
|
|
@@ -122,6 +154,7 @@ export type ExportedLibrettoWorkflow = {
|
|
|
122
154
|
readonly name: string;
|
|
123
155
|
readonly inputSchema?: z.ZodType;
|
|
124
156
|
readonly outputSchema?: z.ZodType;
|
|
157
|
+
readonly recoveryAction?: RecoveryAction;
|
|
125
158
|
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
126
159
|
};
|
|
127
160
|
|
|
@@ -212,22 +245,47 @@ export function getWorkflowFromModuleExports(
|
|
|
212
245
|
return null;
|
|
213
246
|
}
|
|
214
247
|
|
|
215
|
-
|
|
216
|
-
// and the hosted platform can expose typed I/O metadata via /v1/workflows/get.
|
|
217
|
-
export function workflow<
|
|
248
|
+
function getWorkflowConstructorOptions<
|
|
218
249
|
InputSchema extends z.ZodType,
|
|
219
250
|
OutputSchema extends z.ZodType,
|
|
251
|
+
>(
|
|
252
|
+
options:
|
|
253
|
+
| LibrettoWorkflowDefinition<InputSchema, OutputSchema>
|
|
254
|
+
| LibrettoWorkflowOptions<InputSchema, OutputSchema>,
|
|
255
|
+
): {
|
|
256
|
+
inputSchema?: InputSchema;
|
|
257
|
+
outputSchema?: OutputSchema;
|
|
258
|
+
recoveryAction?: RecoveryAction;
|
|
259
|
+
} {
|
|
260
|
+
return {
|
|
261
|
+
inputSchema: options.input,
|
|
262
|
+
outputSchema: options.output,
|
|
263
|
+
recoveryAction: options.recoveryAction,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function workflow<
|
|
268
|
+
InputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
269
|
+
OutputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
220
270
|
>(
|
|
221
271
|
name: string,
|
|
222
|
-
|
|
272
|
+
definition: LibrettoWorkflowDefinition<InputSchema, OutputSchema>,
|
|
223
273
|
handler: LibrettoWorkflowHandler<
|
|
224
274
|
z.infer<InputSchema>,
|
|
225
275
|
z.infer<OutputSchema>
|
|
226
276
|
>,
|
|
227
277
|
): LibrettoWorkflow<InputSchema, OutputSchema>;
|
|
228
278
|
|
|
279
|
+
export function workflow<
|
|
280
|
+
InputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
281
|
+
OutputSchema extends z.ZodType = z.ZodType<unknown>,
|
|
282
|
+
>(
|
|
283
|
+
name: string,
|
|
284
|
+
options: LibrettoWorkflowOptions<InputSchema, OutputSchema>,
|
|
285
|
+
): LibrettoWorkflow<InputSchema, OutputSchema>;
|
|
286
|
+
|
|
229
287
|
// Legacy 2-arg form kept so deployments built before Zod schemas existed
|
|
230
|
-
// continue to load. New code should
|
|
288
|
+
// continue to load. New code should pass input/output schemas when possible.
|
|
231
289
|
export function workflow<Input = unknown, Output = unknown>(
|
|
232
290
|
name: string,
|
|
233
291
|
handler: LibrettoWorkflowHandler<Input, Output>,
|
|
@@ -235,18 +293,30 @@ export function workflow<Input = unknown, Output = unknown>(
|
|
|
235
293
|
|
|
236
294
|
export function workflow(
|
|
237
295
|
name: string,
|
|
238
|
-
|
|
239
|
-
|
|
|
296
|
+
definitionOrHandler:
|
|
297
|
+
| LibrettoWorkflowDefinition<z.ZodType, z.ZodType>
|
|
298
|
+
| LibrettoWorkflowOptions
|
|
240
299
|
| LibrettoWorkflowHandler,
|
|
241
300
|
maybeHandler?: LibrettoWorkflowHandler,
|
|
242
301
|
): LibrettoWorkflow {
|
|
243
|
-
if (typeof
|
|
244
|
-
return new LibrettoWorkflow(name, undefined,
|
|
302
|
+
if (typeof definitionOrHandler === "function") {
|
|
303
|
+
return new LibrettoWorkflow(name, undefined, definitionOrHandler);
|
|
304
|
+
}
|
|
305
|
+
if ("handler" in definitionOrHandler) {
|
|
306
|
+
return new LibrettoWorkflow(
|
|
307
|
+
name,
|
|
308
|
+
getWorkflowConstructorOptions(definitionOrHandler),
|
|
309
|
+
definitionOrHandler.handler,
|
|
310
|
+
);
|
|
245
311
|
}
|
|
246
312
|
if (!maybeHandler) {
|
|
247
313
|
throw new Error(
|
|
248
|
-
`workflow("${name}") called
|
|
314
|
+
`workflow("${name}") called without a handler. Pass the handler as the third argument or in the options object.`,
|
|
249
315
|
);
|
|
250
316
|
}
|
|
251
|
-
return new LibrettoWorkflow(
|
|
317
|
+
return new LibrettoWorkflow(
|
|
318
|
+
name,
|
|
319
|
+
getWorkflowConstructorOptions(definitionOrHandler),
|
|
320
|
+
maybeHandler,
|
|
321
|
+
);
|
|
252
322
|
}
|