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
@@ -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
  }
@@ -6,7 +6,6 @@ type CloudSessionResponse = {
6
6
  status: string;
7
7
  cdp_url: string | null;
8
8
  live_view_url: string | null;
9
- recording_url: string | null;
10
9
  };
11
10
 
12
11
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
@@ -77,8 +76,11 @@ export function createLibrettoCloudProvider(): ProviderApi {
77
76
  };
78
77
  },
79
78
  async closeSession(sessionId) {
80
- const json = await closeCloudSession(endpoint, apiKey, sessionId);
81
- return { replayUrl: json.replay_url ?? undefined };
79
+ await closeCloudSession(endpoint, apiKey, sessionId);
80
+ const replayUrl = await getCloudRecordingUrl(endpoint, apiKey, sessionId).catch(
81
+ () => undefined,
82
+ );
83
+ return { replayUrl };
82
84
  },
83
85
  };
84
86
  }
@@ -172,7 +174,7 @@ async function closeCloudSession(
172
174
  endpoint: string,
173
175
  apiKey: string,
174
176
  sessionId: string,
175
- ): Promise<{ replay_url: string | null }> {
177
+ ): Promise<void> {
176
178
  const resp = await fetch(`${endpoint}/v1/sessions/close`, {
177
179
  method: "POST",
178
180
  headers: {
@@ -187,10 +189,31 @@ async function closeCloudSession(
187
189
  `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`,
188
190
  );
189
191
  }
192
+ }
193
+
194
+ async function getCloudRecordingUrl(
195
+ endpoint: string,
196
+ apiKey: string,
197
+ sessionId: string,
198
+ ): Promise<string | undefined> {
199
+ const resp = await fetch(`${endpoint}/v1/recordings/get`, {
200
+ method: "POST",
201
+ headers: {
202
+ "x-api-key": apiKey,
203
+ "Content-Type": "application/json",
204
+ },
205
+ body: JSON.stringify({ json: { session_id: sessionId } }),
206
+ });
207
+ if (!resp.ok) {
208
+ const body = await resp.text();
209
+ throw new Error(
210
+ `Libretto Cloud API error reading recording for session ${sessionId} (${resp.status}): ${body}`,
211
+ );
212
+ }
190
213
  const { json } = (await resp.json()) as {
191
- json: { replay_url: string | null };
214
+ json: { recording_url: string | null };
192
215
  };
193
- return json;
216
+ return json.recording_url ?? undefined;
194
217
  }
195
218
 
196
219
  function createStartupSessionCleanup(
@@ -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 => {