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.
Files changed (42) hide show
  1. package/README.md +5 -1
  2. package/README.template.md +5 -1
  3. package/dist/cli/commands/execution.js +8 -1
  4. package/dist/cli/core/browser.js +8 -3
  5. package/dist/cli/core/daemon/daemon.js +8 -6
  6. package/dist/cli/core/providers/kernel.js +107 -29
  7. package/dist/cli/core/providers/libretto-cloud.js +22 -3
  8. package/dist/cli/core/providers/steel.js +10 -1
  9. package/dist/index.d.ts +3 -2
  10. package/dist/index.js +15 -1
  11. package/dist/runtime/recovery/agent.d.ts +50 -2
  12. package/dist/runtime/recovery/agent.js +159 -45
  13. package/dist/runtime/recovery/index.d.ts +2 -1
  14. package/dist/runtime/recovery/index.js +16 -2
  15. package/dist/runtime/recovery/page-fallbacks.d.ts +45 -0
  16. package/dist/runtime/recovery/page-fallbacks.js +342 -0
  17. package/dist/shared/state/index.d.ts +1 -1
  18. package/dist/shared/state/session-state.d.ts +4 -1
  19. package/dist/shared/state/session-state.js +2 -1
  20. package/dist/shared/workflow/workflow.d.ts +19 -6
  21. package/dist/shared/workflow/workflow.js +38 -9
  22. package/docs/reference/runtime/page-fallbacks.mdx +85 -0
  23. package/docs/understand-libretto/error-handling-and-recovery.mdx +45 -0
  24. package/package.json +1 -1
  25. package/skills/libretto/SKILL.md +8 -2
  26. package/skills/libretto/references/code-generation-rules.md +23 -6
  27. package/skills/libretto-readonly/SKILL.md +1 -1
  28. package/src/cli/commands/execution.ts +8 -1
  29. package/src/cli/core/browser.ts +7 -2
  30. package/src/cli/core/daemon/daemon.ts +9 -4
  31. package/src/cli/core/daemon/ipc.ts +1 -0
  32. package/src/cli/core/providers/kernel.ts +153 -29
  33. package/src/cli/core/providers/libretto-cloud.ts +29 -6
  34. package/src/cli/core/providers/steel.ts +11 -1
  35. package/src/cli/core/providers/types.ts +3 -0
  36. package/src/index.ts +22 -2
  37. package/src/runtime/recovery/agent.ts +227 -50
  38. package/src/runtime/recovery/index.ts +21 -1
  39. package/src/runtime/recovery/page-fallbacks.ts +476 -0
  40. package/src/shared/state/index.ts +1 -0
  41. package/src/shared/state/session-state.ts +2 -0
  42. 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 LibrettoWorkflowSchemas<InputSchema extends z.ZodType, OutputSchema extends z.ZodType> = {
11
- input: InputSchema;
12
- output: OutputSchema;
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, schemas: LibrettoWorkflowSchemas<InputSchema, OutputSchema> | undefined, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>);
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, OutputSchema extends z.ZodType>(name: string, schemas: LibrettoWorkflowSchemas<InputSchema, OutputSchema>, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>): LibrettoWorkflow<InputSchema, OutputSchema>;
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 LibrettoWorkflowSchemas, type WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow };
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, schemas, handler) {
49
+ constructor(name, options, handler) {
46
50
  this.name = name;
47
- this.inputSchema = schemas?.input;
48
- this.outputSchema = schemas?.output;
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
- return this.handler(ctx, parsed);
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 workflow(name, schemasOrHandler, maybeHandler) {
105
- if (typeof schemasOrHandler === "function") {
106
- return new LibrettoWorkflow(name, void 0, schemasOrHandler);
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 with schemas but no handler. Pass the handler as the third argument.`
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(name, schemasOrHandler, maybeHandler);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.20",
3
+ "version": "0.6.22",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -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.20"
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
- "myWorkflow",
28
- { input: inputSchema, output: outputSchema },
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 }, handler)` takes a unique workflow name, a pair of Zod schemas describing input and output, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
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:
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.20"
7
+ version: "0.6.22"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
@@ -628,7 +628,11 @@ async function runIntegrationFromFile(
628
628
  stayOpenOnSuccess: args.stayOpenOnSuccess,
629
629
  daemonSocketPath,
630
630
  provider: provider
631
- ? { name: provider.name, sessionId: provider.sessionId }
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 {
@@ -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 (result.replayUrl) {
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: result.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: result.replayUrl,
616
+ replayUrl,
612
617
  },
613
618
  null,
614
619
  2,
@@ -98,6 +98,7 @@ export type DaemonReadyMessage = {
98
98
  sessionId: string;
99
99
  cdpEndpoint: string;
100
100
  liveViewUrl?: string;
101
+ recordingUrl?: string;
101
102
  };
102
103
  };
103
104