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.
- 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 +389 -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 +4 -12
- 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 +527 -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,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.
|
|
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",
|
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.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
|
-
|
|
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,
|
|
@@ -1,51 +1,175 @@
|
|
|
1
1
|
import type { ProviderApi } from "./types.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export type KernelProviderOptions = {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
headless?: boolean;
|
|
6
|
+
stealth?: boolean;
|
|
7
|
+
timeoutSeconds?: number;
|
|
8
|
+
enableRecording?: boolean;
|
|
9
|
+
};
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 {
|
|
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
|
|
128
|
+
type LibrettoWorkflowOptions,
|
|
109
129
|
type WorkflowInputValidator,
|
|
110
130
|
} from "./shared/workflow/workflow.js";
|
|
111
131
|
const isDirectExecution = (): boolean => {
|