libretto 0.6.20 → 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/libretto-cloud.js +22 -3
- 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/libretto-cloud.ts +29 -6
- 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
|
@@ -20,6 +20,7 @@ declare const SessionViewportSchema: z.ZodObject<{
|
|
|
20
20
|
declare const ProviderStateSchema: z.ZodObject<{
|
|
21
21
|
name: z.ZodString;
|
|
22
22
|
sessionId: z.ZodString;
|
|
23
|
+
recordingUrl: z.ZodOptional<z.ZodString>;
|
|
23
24
|
}, z.core.$strip>;
|
|
24
25
|
declare const SessionStateFileSchema: z.ZodObject<{
|
|
25
26
|
version: z.ZodLiteral<1>;
|
|
@@ -48,15 +49,17 @@ declare const SessionStateFileSchema: z.ZodObject<{
|
|
|
48
49
|
provider: z.ZodOptional<z.ZodObject<{
|
|
49
50
|
name: z.ZodString;
|
|
50
51
|
sessionId: z.ZodString;
|
|
52
|
+
recordingUrl: z.ZodOptional<z.ZodString>;
|
|
51
53
|
}, z.core.$strip>>;
|
|
52
54
|
daemonSocketPath: z.ZodOptional<z.ZodString>;
|
|
53
55
|
}, z.core.$strip>;
|
|
54
56
|
type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
55
57
|
type SessionAccessMode = z.infer<typeof SessionAccessModeSchema>;
|
|
58
|
+
type ProviderState = z.infer<typeof ProviderStateSchema>;
|
|
56
59
|
type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
|
|
57
60
|
type SessionState = Omit<SessionStateFile, "version">;
|
|
58
61
|
declare function parseSessionStateData(rawState: unknown, source: string): SessionState;
|
|
59
62
|
declare function parseSessionStateContent(content: string, source: string): SessionState;
|
|
60
63
|
declare function serializeSessionState(state: SessionState): SessionStateFile;
|
|
61
64
|
|
|
62
|
-
export { ProviderStateSchema, SESSION_STATE_VERSION, type SessionAccessMode, SessionAccessModeSchema, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
|
|
65
|
+
export { type ProviderState, ProviderStateSchema, SESSION_STATE_VERSION, type SessionAccessMode, SessionAccessModeSchema, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
|
|
@@ -15,7 +15,8 @@ const SessionViewportSchema = z.object({
|
|
|
15
15
|
});
|
|
16
16
|
const ProviderStateSchema = z.object({
|
|
17
17
|
name: z.string(),
|
|
18
|
-
sessionId: z.string()
|
|
18
|
+
sessionId: z.string(),
|
|
19
|
+
recordingUrl: z.string().url().optional()
|
|
19
20
|
});
|
|
20
21
|
const SessionStateFileSchema = z.object({
|
|
21
22
|
version: z.literal(SESSION_STATE_VERSION),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Page } from 'playwright';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { RecoveryAction } from '../../runtime/recovery/page-fallbacks.js';
|
|
4
|
+
import 'ai';
|
|
3
5
|
|
|
4
6
|
declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
|
|
5
7
|
type LibrettoWorkflowContext = {
|
|
@@ -7,9 +9,13 @@ type LibrettoWorkflowContext = {
|
|
|
7
9
|
page: Page;
|
|
8
10
|
};
|
|
9
11
|
type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
|
|
10
|
-
type
|
|
11
|
-
input
|
|
12
|
-
output
|
|
12
|
+
type LibrettoWorkflowDefinition<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> = {
|
|
13
|
+
input?: InputSchema;
|
|
14
|
+
output?: OutputSchema;
|
|
15
|
+
recoveryAction?: RecoveryAction;
|
|
16
|
+
};
|
|
17
|
+
type LibrettoWorkflowOptions<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> = LibrettoWorkflowDefinition<InputSchema, OutputSchema> & {
|
|
18
|
+
handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>;
|
|
13
19
|
};
|
|
14
20
|
declare class LibrettoWorkflowInputError extends Error {
|
|
15
21
|
readonly workflowName: string;
|
|
@@ -26,8 +32,13 @@ declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown
|
|
|
26
32
|
readonly name: string;
|
|
27
33
|
readonly inputSchema?: InputSchema;
|
|
28
34
|
readonly outputSchema?: OutputSchema;
|
|
35
|
+
readonly recoveryAction?: RecoveryAction;
|
|
29
36
|
private readonly handler;
|
|
30
|
-
constructor(name: string,
|
|
37
|
+
constructor(name: string, options: {
|
|
38
|
+
inputSchema?: InputSchema;
|
|
39
|
+
outputSchema?: OutputSchema;
|
|
40
|
+
recoveryAction?: RecoveryAction;
|
|
41
|
+
} | undefined, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>);
|
|
31
42
|
run(ctx: LibrettoWorkflowContext, input: unknown): Promise<z.infer<OutputSchema>>;
|
|
32
43
|
}
|
|
33
44
|
type ExportedLibrettoWorkflow = {
|
|
@@ -35,6 +46,7 @@ type ExportedLibrettoWorkflow = {
|
|
|
35
46
|
readonly name: string;
|
|
36
47
|
readonly inputSchema?: z.ZodType;
|
|
37
48
|
readonly outputSchema?: z.ZodType;
|
|
49
|
+
readonly recoveryAction?: RecoveryAction;
|
|
38
50
|
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
39
51
|
};
|
|
40
52
|
type WorkflowModuleExports = Record<string, unknown>;
|
|
@@ -42,7 +54,8 @@ declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWo
|
|
|
42
54
|
declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
|
|
43
55
|
declare function getDefaultWorkflowFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow | null;
|
|
44
56
|
declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
|
|
45
|
-
declare function workflow<InputSchema extends z.ZodType
|
|
57
|
+
declare function workflow<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>>(name: string, definition: LibrettoWorkflowDefinition<InputSchema, OutputSchema>, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>): LibrettoWorkflow<InputSchema, OutputSchema>;
|
|
58
|
+
declare function workflow<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>>(name: string, options: LibrettoWorkflowOptions<InputSchema, OutputSchema>): LibrettoWorkflow<InputSchema, OutputSchema>;
|
|
46
59
|
declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<z.ZodType<Input>, z.ZodType<Output>>;
|
|
47
60
|
|
|
48
|
-
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, LibrettoWorkflowInputError, type
|
|
61
|
+
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowDefinition, type LibrettoWorkflowHandler, LibrettoWorkflowInputError, type LibrettoWorkflowOptions, type WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow };
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRecoveryPage
|
|
3
|
+
} from "../../runtime/recovery/page-fallbacks.js";
|
|
1
4
|
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
2
5
|
class LibrettoWorkflowInputError extends Error {
|
|
3
6
|
workflowName;
|
|
@@ -41,16 +44,24 @@ class LibrettoWorkflow {
|
|
|
41
44
|
// this schema to JSON Schema at build time and exposes it via
|
|
42
45
|
// /v1/workflows/get so API consumers know the workflow's output shape.
|
|
43
46
|
outputSchema;
|
|
47
|
+
recoveryAction;
|
|
44
48
|
handler;
|
|
45
|
-
constructor(name,
|
|
49
|
+
constructor(name, options, handler) {
|
|
46
50
|
this.name = name;
|
|
47
|
-
this.inputSchema =
|
|
48
|
-
this.outputSchema =
|
|
51
|
+
this.inputSchema = options?.inputSchema;
|
|
52
|
+
this.outputSchema = options?.outputSchema;
|
|
53
|
+
this.recoveryAction = options?.recoveryAction;
|
|
49
54
|
this.handler = handler;
|
|
50
55
|
}
|
|
51
56
|
async run(ctx, input) {
|
|
52
57
|
const parsed = parseWorkflowInput(this.name, this.inputSchema, input);
|
|
53
|
-
|
|
58
|
+
const workflowContext = !this.recoveryAction ? ctx : {
|
|
59
|
+
...ctx,
|
|
60
|
+
page: createRecoveryPage(ctx.page, {
|
|
61
|
+
recoveryAction: this.recoveryAction
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
return this.handler(workflowContext, parsed);
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
function isLibrettoWorkflow(value) {
|
|
@@ -101,16 +112,34 @@ function getWorkflowFromModuleExports(moduleExports, workflowName) {
|
|
|
101
112
|
}
|
|
102
113
|
return null;
|
|
103
114
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
function getWorkflowConstructorOptions(options) {
|
|
116
|
+
return {
|
|
117
|
+
inputSchema: options.input,
|
|
118
|
+
outputSchema: options.output,
|
|
119
|
+
recoveryAction: options.recoveryAction
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function workflow(name, definitionOrHandler, maybeHandler) {
|
|
123
|
+
if (typeof definitionOrHandler === "function") {
|
|
124
|
+
return new LibrettoWorkflow(name, void 0, definitionOrHandler);
|
|
125
|
+
}
|
|
126
|
+
if ("handler" in definitionOrHandler) {
|
|
127
|
+
return new LibrettoWorkflow(
|
|
128
|
+
name,
|
|
129
|
+
getWorkflowConstructorOptions(definitionOrHandler),
|
|
130
|
+
definitionOrHandler.handler
|
|
131
|
+
);
|
|
107
132
|
}
|
|
108
133
|
if (!maybeHandler) {
|
|
109
134
|
throw new Error(
|
|
110
|
-
`workflow("${name}") called
|
|
135
|
+
`workflow("${name}") called without a handler. Pass the handler as the third argument or in the options object.`
|
|
111
136
|
);
|
|
112
137
|
}
|
|
113
|
-
return new LibrettoWorkflow(
|
|
138
|
+
return new LibrettoWorkflow(
|
|
139
|
+
name,
|
|
140
|
+
getWorkflowConstructorOptions(definitionOrHandler),
|
|
141
|
+
maybeHandler
|
|
142
|
+
);
|
|
114
143
|
}
|
|
115
144
|
export {
|
|
116
145
|
LIBRETTO_WORKFLOW_BRAND,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Runtime recovery actions
|
|
2
|
+
|
|
3
|
+
`recoveryAction` lets a workflow recover from a supported Playwright Page or Locator failure. When a supported action fails, Libretto runs the recovery action and retries the original action once.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
export default workflow("reviewOrder", {
|
|
7
|
+
input,
|
|
8
|
+
output,
|
|
9
|
+
recoveryAction,
|
|
10
|
+
handler: async ({ page }, params) => {
|
|
11
|
+
await page.goto(params.url);
|
|
12
|
+
await page.locator("#review").click();
|
|
13
|
+
return { complete: true };
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## computerUseRecoveryAction
|
|
19
|
+
|
|
20
|
+
`computerUseRecoveryAction()` runs a small vision agent with your instruction. It screenshots the viewport, asks the model for a browser action, executes it with Playwright coordinates, and stops when the model returns `done` or `maxSteps` is reached.
|
|
21
|
+
|
|
22
|
+
One step is one screenshot, one model decision, and one browser action. The default `maxSteps` is `3`, which covers the common popup flow of close, confirm, then done.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { computerUseRecoveryAction } from "libretto";
|
|
26
|
+
|
|
27
|
+
const recoveryAction = computerUseRecoveryAction({
|
|
28
|
+
provider: "openai",
|
|
29
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
30
|
+
model: "gpt-5.5",
|
|
31
|
+
instruction: "Close any visible blocker that prevents submitting the form.",
|
|
32
|
+
maxSteps: 3,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Supported provider shortcuts:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
{ provider: "openai", apiKey, model?: "gpt-5.5" }
|
|
40
|
+
{ provider: "anthropic", apiKey, model?: "claude-sonnet-4-6" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Advanced callers can pass a preconfigured AI SDK `LanguageModel` with `languageModel`.
|
|
44
|
+
|
|
45
|
+
## popupRecoveryAction
|
|
46
|
+
|
|
47
|
+
`popupRecoveryAction()` is a preset for the common popup case. It uses Libretto's default instruction for closing popups, cookie banners, modals, overlays, and similar blockers.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { popupRecoveryAction } from "libretto";
|
|
51
|
+
|
|
52
|
+
const recoveryAction = popupRecoveryAction({
|
|
53
|
+
provider: "anthropic",
|
|
54
|
+
apiKey: process.env.ANTHROPIC_API_KEY!,
|
|
55
|
+
model: "claude-sonnet-4-6",
|
|
56
|
+
maxSteps: 3,
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Custom recovery logic
|
|
61
|
+
|
|
62
|
+
Use a custom `RecoveryAction` when the site needs deterministic recovery logic, or when it needs to combine `popupRecoveryAction()` with other steps such as reloading the page.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import {
|
|
66
|
+
popupRecoveryAction,
|
|
67
|
+
type RecoveryAction,
|
|
68
|
+
} from "libretto";
|
|
69
|
+
|
|
70
|
+
const popupRecoveryAgent = popupRecoveryAction({
|
|
71
|
+
provider: "openai",
|
|
72
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
73
|
+
model: "gpt-5.5",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const recoveryAction: RecoveryAction = async (context) => {
|
|
77
|
+
const result = await popupRecoveryAgent(context);
|
|
78
|
+
if (result.status === "incomplete") {
|
|
79
|
+
await context.page.reload();
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If the recovery action returns without throwing, Libretto retries the original failed method once.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Error recovery
|
|
2
|
+
|
|
3
|
+
Libretto helps your agent handle failures in two ways: inspect failed runs so it can fix the deterministic workflow, or add a narrow runtime recovery action for expected nondeterminism.
|
|
4
|
+
|
|
5
|
+
Most workflow failures should be fixed with better selectors, waits, or assertions. Runtime recovery is for cases where the workflow is on the right path but the site sometimes shows unexpected UI that blocks the next action.
|
|
6
|
+
|
|
7
|
+
## Debugging failed runs
|
|
8
|
+
|
|
9
|
+
When a workflow fails during development, the Libretto CLI helps your agent debug the code by preserving the browser session. The agent can run headed, add `pause(ctx.session)` at useful checkpoints, inspect the page state, iterate on code, and use `resume` to continue from the pause before redeploying.
|
|
10
|
+
|
|
11
|
+
For deployed workflows on the hosted platform, a failed run automatically starts an analysis agent. It reviews the error, logs, recording, screenshots, and browser state so it can explain what went wrong and suggest the code change.
|
|
12
|
+
|
|
13
|
+
## Runtime recovery actions
|
|
14
|
+
|
|
15
|
+
Add `recoveryAction` to a workflow when you want Libretto to recover after a supported Page or Locator action fails. Libretto calls the recovery action, then retries the original action once.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export default workflow("reviewOrder", {
|
|
19
|
+
input,
|
|
20
|
+
output,
|
|
21
|
+
recoveryAction: popupRecoveryAction({
|
|
22
|
+
provider: "openai",
|
|
23
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
24
|
+
model: "gpt-5.5",
|
|
25
|
+
maxSteps: 3,
|
|
26
|
+
}),
|
|
27
|
+
handler: async ({ page }, params) => {
|
|
28
|
+
await page.goto(params.url);
|
|
29
|
+
await page.locator("#review").click();
|
|
30
|
+
return { complete: true };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## AI vision recovery
|
|
36
|
+
|
|
37
|
+
The computer use recovery action looks at the current screen and takes one or more browser actions to get back to the expected path. It is useful for nondeterministic blockers such as cookie banners, popups, modals, interstitials, and overlays that intercept clicks.
|
|
38
|
+
|
|
39
|
+
It uses a viewport screenshot, asks the model for an action, scales the returned screenshot coordinates to Playwright coordinates, and executes the action.
|
|
40
|
+
|
|
41
|
+
`popupRecoveryAction()` is the built-in preset for closing popups and other blockers. Use `computerUseRecoveryAction()` when you want to provide your own instruction.
|
|
42
|
+
|
|
43
|
+
Simple sites without nondeterministic UI usually do not need AI vision recovery. For complicated sites automated through the DOM, add it when exploration shows blockers across sessions, accounts, or time. Do not use it to make business decisions that belong in the workflow handler.
|
|
44
|
+
|
|
45
|
+
Supported provider shortcut models are `gpt-5.5` and `claude-sonnet-4-6`.
|
package/package.json
CHANGED
package/skills/libretto/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: saffron-health
|
|
7
|
-
version: "0.6.
|
|
7
|
+
version: "0.6.22"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## How Libretto Works
|
|
@@ -24,7 +24,7 @@ Full documentation is published at [libretto.sh](https://libretto.sh). Available
|
|
|
24
24
|
- Workflow guides: [one-shot generation](https://libretto.sh/docs/guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/docs/guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/docs/guides/debugging-workflows), [convert to network requests](https://libretto.sh/docs/guides/convert-to-network-requests)
|
|
25
25
|
- CLI reference: [open and connect](https://libretto.sh/docs/reference/cli/open-and-connect), [sessions](https://libretto.sh/docs/reference/cli/sessions), [profiles](https://libretto.sh/docs/reference/cli/profiles), [snapshot](https://libretto.sh/docs/reference/cli/snapshot), [exec](https://libretto.sh/docs/reference/cli/exec), [run and resume](https://libretto.sh/docs/reference/cli/run-and-resume), [session logs](https://libretto.sh/docs/reference/cli/session-logs), [pages](https://libretto.sh/docs/reference/cli/pages)
|
|
26
26
|
- Library API: [workflow](https://libretto.sh/docs/reference/runtime/workflow), [AI extraction](https://libretto.sh/docs/reference/runtime/ai-extraction), [network requests](https://libretto.sh/docs/reference/runtime/network-requests), [file downloads](https://libretto.sh/docs/reference/runtime/file-downloads)
|
|
27
|
-
- Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments)
|
|
27
|
+
- Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments), [stealth](https://libretto.sh/docs/libretto-cloud-hosting/stealth)
|
|
28
28
|
- Alternative providers: [overview](https://libretto.sh/docs/alternative-providers/overview), [Kernel](https://libretto.sh/docs/alternative-providers/kernel), [Browserbase](https://libretto.sh/docs/alternative-providers/browserbase), [Steel](https://libretto.sh/docs/alternative-providers/steel), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
|
|
29
29
|
|
|
30
30
|
## Default Integration Approach
|
|
@@ -37,6 +37,12 @@ Mix strategies freely across steps on a site.
|
|
|
37
37
|
|
|
38
38
|
Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first navigation — deep URLs on a cold session are commonly blocked by edge bot protection.
|
|
39
39
|
|
|
40
|
+
## CAPTCHA Handling
|
|
41
|
+
|
|
42
|
+
- If a CAPTCHA, Cloudflare challenge, or similar bot check appears in a local Chromium session, try to solve it in the visible browser and continue from the solved state.
|
|
43
|
+
- If the same challenge appears while the session is using a hosted browser provider, wait up to 2 minutes for automatic CAPTCHA handling before deciding the workflow is blocked.
|
|
44
|
+
- When a CAPTCHA was observed during exploration, generated workflow code should include an explicit timeout wait at that point with a short comment linking to the hosted platform stealth docs. Follow `references/code-generation-rules.md` for the code shape.
|
|
45
|
+
|
|
40
46
|
## Setup
|
|
41
47
|
|
|
42
48
|
- Use the package manager convention for the target project. The examples use `npx libretto`; pnpm, yarn, and bun projects should use their equivalent package-manager execution form.
|
|
@@ -23,10 +23,10 @@ const outputSchema = z.object({
|
|
|
23
23
|
results: z.array(z.object({ name: z.string(), value: z.string() })),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
export default workflow(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
async (ctx: LibrettoWorkflowContext, input) => {
|
|
26
|
+
export default workflow("myWorkflow", {
|
|
27
|
+
input: inputSchema,
|
|
28
|
+
output: outputSchema,
|
|
29
|
+
handler: async (ctx: LibrettoWorkflowContext, input) => {
|
|
30
30
|
const { session, page } = ctx;
|
|
31
31
|
|
|
32
32
|
console.log("workflow-start", { session, query: input.query });
|
|
@@ -35,12 +35,12 @@ export default workflow(
|
|
|
35
35
|
|
|
36
36
|
return { results: [] };
|
|
37
37
|
},
|
|
38
|
-
);
|
|
38
|
+
});
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Key points:
|
|
42
42
|
|
|
43
|
-
- `workflow(name, { input, output
|
|
43
|
+
- `workflow(name, { input, output, recoveryAction, handler })` takes a unique workflow name, optional Zod input/output schemas, an optional recovery action, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
|
|
44
44
|
- At run time, Libretto validates `input` against `inputSchema` before calling the handler. Invalid input throws a clear error listing each failing field; the workflow handler never sees malformed input.
|
|
45
45
|
- `npx libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
|
|
46
46
|
- `ctx` provides `session` and `page`. Use `console.log`/`console.warn`/`console.error` for logging — the runtime wraps these with structured metadata automatically.
|
|
@@ -49,6 +49,12 @@ Key points:
|
|
|
49
49
|
- After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
|
|
50
50
|
- The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
|
|
51
51
|
|
|
52
|
+
## Workflow Error Handling
|
|
53
|
+
|
|
54
|
+
Do not add runtime recovery by default. Add `recoveryAction` only when exploration shows nondeterministic blockers such as popups, cookie banners, modals, or overlays.
|
|
55
|
+
|
|
56
|
+
Use `popupRecoveryAction()` for generic popup and modal dismissal. Use a custom `RecoveryAction` function when the site needs specific recovery steps around the popup recovery agent. Tell the user when you add runtime recovery and keep primary workflow logic in the handler.
|
|
57
|
+
|
|
52
58
|
## Playwright DOM Interaction Rules
|
|
53
59
|
|
|
54
60
|
Generated code must use Playwright locator APIs for all DOM interactions. Do not use `page.evaluate()` with `document.querySelector`, `querySelectorAll`, `textContent`, `click()`, or other DOM APIs when a Playwright locator can do the same thing.
|
|
@@ -57,6 +63,17 @@ During the interactive `exec` phase, `page.evaluate` is fine for quick prototypi
|
|
|
57
63
|
|
|
58
64
|
Before extracting data (for example text, rows, or field values), wait for the target content itself to be ready, not just its container.
|
|
59
65
|
|
|
66
|
+
## CAPTCHA Waits
|
|
67
|
+
|
|
68
|
+
If a CAPTCHA, Cloudflare challenge, or similar bot check appeared during exploration, preserve that recovery point in generated workflow code with an explicit 2 minute timeout wait. Put a short comment immediately above the wait linking to the hosted platform stealth docs:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Wait for hosted CAPTCHA solving; see https://libretto.sh/docs/libretto-cloud-hosting/stealth.
|
|
72
|
+
await page.waitForTimeout(120_000);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use the wait only where a challenge was actually observed or is expected from the site's normal flow. After the wait, continue with a concrete locator or URL assertion for the real page state so the workflow fails clearly if the challenge was not solved.
|
|
76
|
+
|
|
60
77
|
### Anti-Patterns
|
|
61
78
|
|
|
62
79
|
These patterns come up frequently during interactive sessions and should not carry over into production code:
|
|
@@ -628,7 +628,11 @@ async function runIntegrationFromFile(
|
|
|
628
628
|
stayOpenOnSuccess: args.stayOpenOnSuccess,
|
|
629
629
|
daemonSocketPath,
|
|
630
630
|
provider: provider
|
|
631
|
-
? {
|
|
631
|
+
? {
|
|
632
|
+
name: provider.name,
|
|
633
|
+
sessionId: provider.sessionId,
|
|
634
|
+
recordingUrl: provider.recordingUrl,
|
|
635
|
+
}
|
|
632
636
|
: undefined,
|
|
633
637
|
},
|
|
634
638
|
logger,
|
|
@@ -636,6 +640,9 @@ async function runIntegrationFromFile(
|
|
|
636
640
|
if (provider?.liveViewUrl) {
|
|
637
641
|
console.log(`View live session: ${provider.liveViewUrl}`);
|
|
638
642
|
}
|
|
643
|
+
if (provider?.recordingUrl) {
|
|
644
|
+
console.log(`View recording: ${provider.recordingUrl}`);
|
|
645
|
+
}
|
|
639
646
|
|
|
640
647
|
let outcome: WorkflowOutcome;
|
|
641
648
|
try {
|
package/src/cli/core/browser.ts
CHANGED
|
@@ -568,11 +568,15 @@ export async function runOpenWithProvider(
|
|
|
568
568
|
sessionId: providerSession.sessionId,
|
|
569
569
|
cdpEndpoint: providerSession.cdpEndpoint,
|
|
570
570
|
liveViewUrl: providerSession.liveViewUrl,
|
|
571
|
+
recordingUrl: providerSession.recordingUrl,
|
|
571
572
|
});
|
|
572
573
|
|
|
573
574
|
if (providerSession.liveViewUrl) {
|
|
574
575
|
console.log(`View live session: ${providerSession.liveViewUrl}`);
|
|
575
576
|
}
|
|
577
|
+
if (providerSession.recordingUrl) {
|
|
578
|
+
console.log(`View recording: ${providerSession.recordingUrl}`);
|
|
579
|
+
}
|
|
576
580
|
|
|
577
581
|
writeSessionState(
|
|
578
582
|
{
|
|
@@ -587,6 +591,7 @@ export async function runOpenWithProvider(
|
|
|
587
591
|
provider: {
|
|
588
592
|
name: providerName,
|
|
589
593
|
sessionId: providerSession.sessionId,
|
|
594
|
+
recordingUrl: providerSession.recordingUrl,
|
|
590
595
|
},
|
|
591
596
|
},
|
|
592
597
|
logger,
|
|
@@ -867,7 +872,7 @@ async function waitForCloseAllTargets(
|
|
|
867
872
|
|
|
868
873
|
async function closeProviderSessionDirectly(
|
|
869
874
|
session: string,
|
|
870
|
-
providerState: { name: string; sessionId: string },
|
|
875
|
+
providerState: { name: string; sessionId: string; recordingUrl?: string },
|
|
871
876
|
logger: LoggerApi,
|
|
872
877
|
): Promise<string | undefined> {
|
|
873
878
|
try {
|
|
@@ -879,7 +884,7 @@ async function closeProviderSessionDirectly(
|
|
|
879
884
|
sessionId: providerState.sessionId,
|
|
880
885
|
replayUrl: result.replayUrl,
|
|
881
886
|
});
|
|
882
|
-
return result.replayUrl;
|
|
887
|
+
return result.replayUrl ?? providerState.recordingUrl;
|
|
883
888
|
} catch (error) {
|
|
884
889
|
logger.warn("close-provider-direct-fallback-failed", {
|
|
885
890
|
session,
|
|
@@ -182,6 +182,7 @@ class BrowserDaemon {
|
|
|
182
182
|
provider: ProviderApi;
|
|
183
183
|
name: string;
|
|
184
184
|
sessionId: string;
|
|
185
|
+
recordingUrl?: string;
|
|
185
186
|
},
|
|
186
187
|
) {
|
|
187
188
|
this.logger = logger.withScope("child");
|
|
@@ -230,11 +231,13 @@ class BrowserDaemon {
|
|
|
230
231
|
sessionId: string;
|
|
231
232
|
cdpEndpoint: string;
|
|
232
233
|
liveViewUrl?: string;
|
|
234
|
+
recordingUrl?: string;
|
|
233
235
|
};
|
|
234
236
|
providerSession?: {
|
|
235
237
|
provider: ProviderApi;
|
|
236
238
|
name: string;
|
|
237
239
|
sessionId: string;
|
|
240
|
+
recordingUrl?: string;
|
|
238
241
|
};
|
|
239
242
|
beforeReady?: () => void;
|
|
240
243
|
}): Promise<BrowserDaemon> {
|
|
@@ -516,11 +519,13 @@ class BrowserDaemon {
|
|
|
516
519
|
sessionId: providerSession.sessionId,
|
|
517
520
|
cdpEndpoint: providerSession.cdpEndpoint,
|
|
518
521
|
liveViewUrl: providerSession.liveViewUrl,
|
|
522
|
+
recordingUrl: providerSession.recordingUrl,
|
|
519
523
|
},
|
|
520
524
|
providerSession: {
|
|
521
525
|
provider,
|
|
522
526
|
name: config.providerName,
|
|
523
527
|
sessionId: providerSession.sessionId,
|
|
528
|
+
recordingUrl: providerSession.recordingUrl,
|
|
524
529
|
},
|
|
525
530
|
beforeReady: startupCleanup.dispose,
|
|
526
531
|
});
|
|
@@ -593,13 +598,13 @@ class BrowserDaemon {
|
|
|
593
598
|
const result = await this.providerSession.provider.closeSession(
|
|
594
599
|
this.providerSession.sessionId,
|
|
595
600
|
);
|
|
596
|
-
replayUrl = result.replayUrl;
|
|
597
|
-
if (
|
|
601
|
+
replayUrl = result.replayUrl ?? this.providerSession.recordingUrl;
|
|
602
|
+
if (replayUrl) {
|
|
598
603
|
this.logger.info("provider-recording", {
|
|
599
604
|
session: this.session,
|
|
600
605
|
provider: this.providerSession.name,
|
|
601
606
|
sessionId: this.providerSession.sessionId,
|
|
602
|
-
replayUrl
|
|
607
|
+
replayUrl,
|
|
603
608
|
});
|
|
604
609
|
}
|
|
605
610
|
writeFileSync(
|
|
@@ -608,7 +613,7 @@ class BrowserDaemon {
|
|
|
608
613
|
{
|
|
609
614
|
provider: this.providerSession.name,
|
|
610
615
|
sessionId: this.providerSession.sessionId,
|
|
611
|
-
replayUrl
|
|
616
|
+
replayUrl,
|
|
612
617
|
},
|
|
613
618
|
null,
|
|
614
619
|
2,
|