libretto 0.6.21 → 0.6.23

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 (40) 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/steel.js +10 -1
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.js +15 -1
  10. package/dist/runtime/recovery/agent.d.ts +50 -2
  11. package/dist/runtime/recovery/agent.js +159 -45
  12. package/dist/runtime/recovery/index.d.ts +2 -1
  13. package/dist/runtime/recovery/index.js +16 -2
  14. package/dist/runtime/recovery/page-fallbacks.d.ts +45 -0
  15. package/dist/runtime/recovery/page-fallbacks.js +389 -0
  16. package/dist/shared/state/index.d.ts +1 -1
  17. package/dist/shared/state/session-state.d.ts +4 -1
  18. package/dist/shared/state/session-state.js +2 -1
  19. package/dist/shared/workflow/workflow.d.ts +19 -6
  20. package/dist/shared/workflow/workflow.js +38 -9
  21. package/docs/reference/runtime/page-fallbacks.mdx +85 -0
  22. package/docs/understand-libretto/error-handling-and-recovery.mdx +45 -0
  23. package/package.json +4 -12
  24. package/skills/libretto/SKILL.md +8 -2
  25. package/skills/libretto/references/code-generation-rules.md +23 -6
  26. package/skills/libretto-readonly/SKILL.md +1 -1
  27. package/src/cli/commands/execution.ts +8 -1
  28. package/src/cli/core/browser.ts +7 -2
  29. package/src/cli/core/daemon/daemon.ts +9 -4
  30. package/src/cli/core/daemon/ipc.ts +1 -0
  31. package/src/cli/core/providers/kernel.ts +153 -29
  32. package/src/cli/core/providers/steel.ts +11 -1
  33. package/src/cli/core/providers/types.ts +3 -0
  34. package/src/index.ts +22 -2
  35. package/src/runtime/recovery/agent.ts +227 -50
  36. package/src/runtime/recovery/index.ts +21 -1
  37. package/src/runtime/recovery/page-fallbacks.ts +527 -0
  38. package/src/shared/state/index.ts +1 -0
  39. package/src/shared/state/session-state.ts +2 -0
  40. package/src/shared/workflow/workflow.ts +90 -20
@@ -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.21",
3
+ "version": "0.6.23",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -31,30 +31,20 @@
31
31
  }
32
32
  },
33
33
  "peerDependencies": {
34
- "@ai-sdk/anthropic": "^3.0.58",
35
34
  "@ai-sdk/google": "^3.0.51",
36
- "@ai-sdk/google-vertex": "^4.0.80",
37
- "@ai-sdk/openai": "^3.0.41"
35
+ "@ai-sdk/google-vertex": "^4.0.80"
38
36
  },
39
37
  "peerDependenciesMeta": {
40
- "@ai-sdk/anthropic": {
41
- "optional": true
42
- },
43
38
  "@ai-sdk/google": {
44
39
  "optional": true
45
40
  },
46
41
  "@ai-sdk/google-vertex": {
47
42
  "optional": true
48
- },
49
- "@ai-sdk/openai": {
50
- "optional": true
51
43
  }
52
44
  },
53
45
  "devDependencies": {
54
- "@ai-sdk/anthropic": "^3.0.58",
55
46
  "@ai-sdk/google": "^3.0.51",
56
47
  "@ai-sdk/google-vertex": "^4.0.80",
57
- "@ai-sdk/openai": "^3.0.41",
58
48
  "@anthropic-ai/claude-agent-sdk": "^0.2.75",
59
49
  "@mariozechner/pi-agent-core": "^0.62.0",
60
50
  "@mariozechner/pi-ai": "^0.62.0",
@@ -70,6 +60,8 @@
70
60
  "vitest": "^4.1.5"
71
61
  },
72
62
  "dependencies": {
63
+ "@ai-sdk/anthropic": "^3.0.66",
64
+ "@ai-sdk/openai": "^3.0.66",
73
65
  "ai": "^6.0.116",
74
66
  "esbuild": "^0.27.0",
75
67
  "playwright": "^1.58.2",
@@ -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.21"
7
+ version: "0.6.23"
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.21"
7
+ version: "0.6.23"
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
 
@@ -1,51 +1,175 @@
1
1
  import type { ProviderApi } from "./types.js";
2
2
 
3
- const KERNEL_API_ENDPOINT = "https://api.onkernel.com";
3
+ export type KernelProviderOptions = {
4
+ apiKey?: string;
5
+ headless?: boolean;
6
+ stealth?: boolean;
7
+ timeoutSeconds?: number;
8
+ enableRecording?: boolean;
9
+ };
4
10
 
5
- export function createKernelProvider(): ProviderApi {
6
- const apiKey = process.env.KERNEL_API_KEY;
11
+ type KernelBrowserResponse = {
12
+ session_id: string;
13
+ cdp_ws_url: string;
14
+ browser_live_view_url?: string | null;
15
+ };
16
+
17
+ type KernelReplayResponse = {
18
+ replay_id: string;
19
+ replay_view_url?: string | null;
20
+ };
21
+
22
+ function readBooleanEnv(name: string, defaultValue: boolean): boolean {
23
+ const value = process.env[name]?.trim().toLowerCase();
24
+ if (!value) return defaultValue;
25
+ return value === "1" || value === "true" || value === "yes";
26
+ }
27
+
28
+ function readTimeoutSeconds(options: KernelProviderOptions): number {
29
+ if (options.timeoutSeconds !== undefined) return options.timeoutSeconds;
30
+ return Number(process.env.KERNEL_TIMEOUT_SECONDS ?? 300);
31
+ }
32
+
33
+ async function kernelFetchJson<T>(
34
+ endpoint: string,
35
+ apiKey: string,
36
+ path: string,
37
+ init: RequestInit,
38
+ ): Promise<T> {
39
+ const resp = await fetch(`${endpoint}${path}`, {
40
+ ...init,
41
+ headers: {
42
+ Authorization: `Bearer ${apiKey}`,
43
+ "Content-Type": "application/json",
44
+ ...init.headers,
45
+ },
46
+ });
47
+ if (!resp.ok) {
48
+ const body = await resp.text();
49
+ throw new Error(`Kernel API error (${resp.status}): ${body}`);
50
+ }
51
+ return (await resp.json()) as T;
52
+ }
53
+
54
+ async function kernelFetchNoBody(
55
+ endpoint: string,
56
+ apiKey: string,
57
+ path: string,
58
+ init: RequestInit,
59
+ ): Promise<void> {
60
+ const resp = await fetch(`${endpoint}${path}`, {
61
+ ...init,
62
+ headers: {
63
+ Authorization: `Bearer ${apiKey}`,
64
+ ...init.headers,
65
+ },
66
+ });
67
+ if (!resp.ok) {
68
+ const body = await resp.text();
69
+ throw new Error(`Kernel API error (${resp.status}): ${body}`);
70
+ }
71
+ }
72
+
73
+ function readEndpoint(): string {
74
+ return (
75
+ process.env.KERNEL_API_ENDPOINT?.trim() ||
76
+ process.env.KERNEL_ENDPOINT?.trim() ||
77
+ "https://api.onkernel.com"
78
+ );
79
+ }
80
+
81
+ export function createKernelProvider(
82
+ options: KernelProviderOptions = {},
83
+ ): ProviderApi {
84
+ const apiKey = options.apiKey ?? process.env.KERNEL_API_KEY;
7
85
  if (!apiKey)
8
86
  throw new Error("KERNEL_API_KEY is required for Kernel provider.");
87
+ const endpoint = readEndpoint();
88
+ const headless = options.headless ?? process.env.KERNEL_HEADLESS !== "false";
89
+ const stealth = options.stealth ?? readBooleanEnv("KERNEL_STEALTH", false);
90
+ const timeoutSeconds = readTimeoutSeconds(options);
91
+ const enableRecording =
92
+ options.enableRecording ?? readBooleanEnv("KERNEL_ENABLE_RECORDING", false);
93
+ const replays = new Map<
94
+ string,
95
+ {
96
+ replayId: string;
97
+ replayViewUrl?: string;
98
+ }
99
+ >();
9
100
 
10
101
  return {
11
102
  async createSession() {
12
- const resp = await fetch(`${KERNEL_API_ENDPOINT}/browsers`, {
13
- method: "POST",
14
- headers: {
15
- Authorization: `Bearer ${apiKey}`,
16
- "Content-Type": "application/json",
103
+ const json = await kernelFetchJson<KernelBrowserResponse>(
104
+ endpoint,
105
+ apiKey,
106
+ "/browsers",
107
+ {
108
+ method: "POST",
109
+ body: JSON.stringify({
110
+ headless,
111
+ stealth,
112
+ timeout_seconds: timeoutSeconds,
113
+ }),
17
114
  },
18
- body: JSON.stringify({
19
- headless: process.env.KERNEL_HEADLESS !== "false",
20
- stealth: process.env.KERNEL_STEALTH === "true",
21
- timeout_seconds: Number(process.env.KERNEL_TIMEOUT_SECONDS ?? 300),
22
- }),
23
- });
24
- if (!resp.ok) {
25
- const body = await resp.text();
26
- throw new Error(`Kernel API error (${resp.status}): ${body}`);
115
+ );
116
+
117
+ let replay: KernelReplayResponse | undefined;
118
+ if (enableRecording) {
119
+ try {
120
+ replay = await kernelFetchJson<KernelReplayResponse>(
121
+ endpoint,
122
+ apiKey,
123
+ `/browsers/${json.session_id}/replays`,
124
+ { method: "POST", body: JSON.stringify({}) },
125
+ );
126
+ replays.set(json.session_id, {
127
+ replayId: replay.replay_id,
128
+ replayViewUrl: replay.replay_view_url ?? undefined,
129
+ });
130
+ } catch (error) {
131
+ await kernelFetchNoBody(
132
+ endpoint,
133
+ apiKey,
134
+ `/browsers/${json.session_id}`,
135
+ { method: "DELETE" },
136
+ ).catch(() => {});
137
+ throw error;
138
+ }
27
139
  }
28
- const json = (await resp.json()) as {
29
- session_id: string;
30
- cdp_ws_url: string;
31
- };
140
+
32
141
  return {
33
142
  sessionId: json.session_id,
34
143
  cdpEndpoint: json.cdp_ws_url,
144
+ liveViewUrl: json.browser_live_view_url ?? undefined,
145
+ recordingUrl: replay?.replay_view_url ?? undefined,
35
146
  };
36
147
  },
37
148
  async closeSession(sessionId) {
38
- const resp = await fetch(`${KERNEL_API_ENDPOINT}/browsers/${sessionId}`, {
149
+ const replay = replays.get(sessionId);
150
+ let replayStopError: unknown;
151
+ if (replay) {
152
+ try {
153
+ await kernelFetchNoBody(
154
+ endpoint,
155
+ apiKey,
156
+ `/browsers/${sessionId}/replays/${replay.replayId}/stop`,
157
+ { method: "POST" },
158
+ );
159
+ } catch (error) {
160
+ replayStopError = error;
161
+ }
162
+ }
163
+
164
+ await kernelFetchNoBody(endpoint, apiKey, `/browsers/${sessionId}`, {
39
165
  method: "DELETE",
40
- headers: { Authorization: `Bearer ${apiKey}` },
41
166
  });
42
- if (!resp.ok) {
43
- const body = await resp.text();
44
- throw new Error(
45
- `Kernel API error closing session ${sessionId} (${resp.status}): ${body}`,
46
- );
167
+ replays.delete(sessionId);
168
+
169
+ if (replayStopError) {
170
+ throw replayStopError;
47
171
  }
48
- return {};
172
+ return { replayUrl: replay?.replayViewUrl };
49
173
  },
50
174
  };
51
175
  }
@@ -8,6 +8,16 @@ type SteelSessionResponse = {
8
8
  sessionViewerUrl?: string;
9
9
  };
10
10
 
11
+ const STEEL_STEALTH_SESSION_OPTIONS = {
12
+ solveCaptcha: true,
13
+ useProxy: true,
14
+ stealthConfig: {
15
+ humanizeInteractions: true,
16
+ autoCaptchaSolving: true,
17
+ skipFingerprintInjection: false,
18
+ },
19
+ };
20
+
11
21
  export type SteelProviderOptions = {
12
22
  apiKey?: string;
13
23
  };
@@ -30,7 +40,7 @@ export function createSteelProvider(
30
40
  "steel-api-key": apiKey,
31
41
  "Content-Type": "application/json",
32
42
  },
33
- body: JSON.stringify({}),
43
+ body: JSON.stringify(STEEL_STEALTH_SESSION_OPTIONS),
34
44
  });
35
45
  if (!resp.ok) {
36
46
  const body = await resp.text();
@@ -5,6 +5,9 @@ export type ProviderSession = {
5
5
  // Only libretto-cloud surfaces this today; direct-SDK providers leave it
6
6
  // undefined.
7
7
  liveViewUrl?: string;
8
+ // Provider-hosted URL for watching the recording for this session. It may be
9
+ // available as soon as recording starts, before the provider has finalized it.
10
+ recordingUrl?: string;
8
11
  };
9
12
 
10
13
  export type ProviderCloseResult = {
package/src/index.ts CHANGED
@@ -29,13 +29,33 @@ export {
29
29
  } from "./shared/state/index.js";
30
30
 
31
31
  // Recovery
32
- export { executeRecoveryAgent } from "./runtime/recovery/agent.js";
32
+ export {
33
+ executeRecoveryAgent,
34
+ type BrowserAction,
35
+ type RecoveryAgentResult,
36
+ type RecoveryAgentStep,
37
+ } from "./runtime/recovery/agent.js";
33
38
  export { attemptWithRecovery } from "./runtime/recovery/recovery.js";
34
39
  export {
35
40
  detectSubmissionError,
36
41
  type KnownSubmissionError,
37
42
  type DetectedSubmissionError,
38
43
  } from "./runtime/recovery/errors.js";
44
+ export {
45
+ COMPUTER_USE_RECOVERY_MODELS,
46
+ POPUP_RECOVERY_INSTRUCTION,
47
+ computerUseRecoveryAction,
48
+ createRecoveryPage,
49
+ popupRecoveryAction,
50
+ type ComputerUseRecoveryActionOptions,
51
+ type PopupRecoveryActionOptions,
52
+ type RecoveryActionContext,
53
+ type RecoveryAction,
54
+ type RecoveryActionHandler,
55
+ type RecoveryActionOptions,
56
+ type RecoveryActionResult,
57
+ type RecoveryActionTargetType,
58
+ } from "./runtime/recovery/page-fallbacks.js";
39
59
 
40
60
  // AI extraction
41
61
  export {
@@ -105,7 +125,7 @@ export {
105
125
  type ExportedLibrettoWorkflow,
106
126
  type LibrettoWorkflowContext,
107
127
  type LibrettoWorkflowHandler,
108
- type LibrettoWorkflowSchemas,
128
+ type LibrettoWorkflowOptions,
109
129
  type WorkflowInputValidator,
110
130
  } from "./shared/workflow/workflow.js";
111
131
  const isDirectExecution = (): boolean => {