libretto 0.6.20 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/README.template.md +5 -1
- package/dist/cli/commands/execution.js +8 -1
- package/dist/cli/core/browser.js +8 -3
- package/dist/cli/core/daemon/daemon.js +8 -6
- package/dist/cli/core/providers/kernel.js +107 -29
- package/dist/cli/core/providers/libretto-cloud.js +22 -3
- package/dist/cli/core/providers/steel.js +10 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +15 -1
- package/dist/runtime/recovery/agent.d.ts +50 -2
- package/dist/runtime/recovery/agent.js +159 -45
- package/dist/runtime/recovery/index.d.ts +2 -1
- package/dist/runtime/recovery/index.js +16 -2
- package/dist/runtime/recovery/page-fallbacks.d.ts +45 -0
- package/dist/runtime/recovery/page-fallbacks.js +342 -0
- package/dist/shared/state/index.d.ts +1 -1
- package/dist/shared/state/session-state.d.ts +4 -1
- package/dist/shared/state/session-state.js +2 -1
- package/dist/shared/workflow/workflow.d.ts +19 -6
- package/dist/shared/workflow/workflow.js +38 -9
- package/docs/reference/runtime/page-fallbacks.mdx +85 -0
- package/docs/understand-libretto/error-handling-and-recovery.mdx +45 -0
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +8 -2
- package/skills/libretto/references/code-generation-rules.md +23 -6
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/execution.ts +8 -1
- package/src/cli/core/browser.ts +7 -2
- package/src/cli/core/daemon/daemon.ts +9 -4
- package/src/cli/core/daemon/ipc.ts +1 -0
- package/src/cli/core/providers/kernel.ts +153 -29
- package/src/cli/core/providers/libretto-cloud.ts +29 -6
- package/src/cli/core/providers/steel.ts +11 -1
- package/src/cli/core/providers/types.ts +3 -0
- package/src/index.ts +22 -2
- package/src/runtime/recovery/agent.ts +227 -50
- package/src/runtime/recovery/index.ts +21 -1
- package/src/runtime/recovery/page-fallbacks.ts +476 -0
- package/src/shared/state/index.ts +1 -0
- package/src/shared/state/session-state.ts +2 -0
- package/src/shared/workflow/workflow.ts +90 -20
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
81
|
-
|
|
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<
|
|
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: {
|
|
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 {
|
|
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 => {
|