ralphwiggums 0.0.0 → 0.0.1
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/LICENSE +21 -0
- package/README.md +454 -2
- package/dist/src/checkpoint-do.d.ts +69 -0
- package/dist/src/checkpoint-do.js +232 -0
- package/dist/src/index.d.ts +137 -0
- package/dist/src/index.js +647 -0
- package/package.json +94 -8
- package/index.js +0 -2
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ralphwiggums - Durable Object Checkpoint Storage
|
|
3
|
+
* Production-ready checkpoint persistence using Cloudflare Durable Objects.
|
|
4
|
+
*/
|
|
5
|
+
import { Effect, Data, Layer } from "effect";
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Errors
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export class CheckpointError extends Data.TaggedError("CheckpointError") {
|
|
10
|
+
}
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// In-Memory Store (fallback for single-instance or testing)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const IN_MEMORY_TTL = 3600000; // 1 hour
|
|
15
|
+
/**
|
|
16
|
+
* Creates an in-memory checkpoint store (for development/testing).
|
|
17
|
+
*/
|
|
18
|
+
export function createInMemoryCheckpointStore() {
|
|
19
|
+
const store = new Map();
|
|
20
|
+
const taskIndex = new Map();
|
|
21
|
+
return {
|
|
22
|
+
async save(data) {
|
|
23
|
+
store.set(data.checkpointId, data);
|
|
24
|
+
const checkpoints = taskIndex.get(data.taskId) || [];
|
|
25
|
+
if (!checkpoints.includes(data.checkpointId)) {
|
|
26
|
+
checkpoints.push(data.checkpointId);
|
|
27
|
+
taskIndex.set(data.taskId, checkpoints);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async load(checkpointId) {
|
|
31
|
+
return store.get(checkpointId) || null;
|
|
32
|
+
},
|
|
33
|
+
async delete(checkpointId) {
|
|
34
|
+
const data = store.get(checkpointId);
|
|
35
|
+
if (data) {
|
|
36
|
+
store.delete(checkpointId);
|
|
37
|
+
const checkpoints = taskIndex.get(data.taskId);
|
|
38
|
+
if (checkpoints) {
|
|
39
|
+
const idx = checkpoints.indexOf(checkpointId);
|
|
40
|
+
if (idx >= 0)
|
|
41
|
+
checkpoints.splice(idx, 1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
async list(taskId) {
|
|
46
|
+
const checkpointIds = taskIndex.get(taskId) || [];
|
|
47
|
+
return checkpointIds
|
|
48
|
+
.map((id) => store.get(id))
|
|
49
|
+
.filter((c) => c !== undefined);
|
|
50
|
+
},
|
|
51
|
+
async gc() {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
for (const [id, data] of store.entries()) {
|
|
54
|
+
if (now > data.expiresAt) {
|
|
55
|
+
store.delete(id);
|
|
56
|
+
const checkpoints = taskIndex.get(data.taskId);
|
|
57
|
+
if (checkpoints) {
|
|
58
|
+
const idx = checkpoints.indexOf(id);
|
|
59
|
+
if (idx >= 0)
|
|
60
|
+
checkpoints.splice(idx, 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Checkpoint Durable Object utility class for Cloudflare Workers.
|
|
69
|
+
*
|
|
70
|
+
* Usage in worker:
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { CheckpointDO } from "ralphwiggums/checkpoint-do";
|
|
73
|
+
*
|
|
74
|
+
* export class RalphAgent extends DurableObject {
|
|
75
|
+
* async fetch(request) {
|
|
76
|
+
* return CheckpointDO.fetch(this.state, this.env, request);
|
|
77
|
+
* }
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export class CheckpointDO {
|
|
82
|
+
static async fetch(state, env, request) {
|
|
83
|
+
const url = new URL(request.url);
|
|
84
|
+
const path = url.pathname.slice(1);
|
|
85
|
+
try {
|
|
86
|
+
switch (request.method) {
|
|
87
|
+
case "GET": {
|
|
88
|
+
if (path.startsWith("list/")) {
|
|
89
|
+
const taskId = path.slice(5);
|
|
90
|
+
const checkpoints = await CheckpointDO.list(state, taskId);
|
|
91
|
+
return new Response(JSON.stringify(checkpoints), {
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const checkpoint = await CheckpointDO.load(state, path);
|
|
96
|
+
if (!checkpoint) {
|
|
97
|
+
return new Response(JSON.stringify({ error: "not found" }), {
|
|
98
|
+
status: 404,
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return new Response(JSON.stringify(checkpoint), {
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
case "POST": {
|
|
107
|
+
if (path === "save") {
|
|
108
|
+
const data = await request.json();
|
|
109
|
+
await CheckpointDO.save(state, data);
|
|
110
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (path === "gc") {
|
|
115
|
+
await CheckpointDO.gc(state);
|
|
116
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return new Response(JSON.stringify({ error: "unknown endpoint" }), {
|
|
121
|
+
status: 404,
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
case "DELETE": {
|
|
126
|
+
await CheckpointDO.delete(state, path);
|
|
127
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
return new Response(JSON.stringify({ error: "method not allowed" }), {
|
|
133
|
+
status: 405,
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return new Response(JSON.stringify({
|
|
140
|
+
error: error instanceof Error ? error.message : "unknown error",
|
|
141
|
+
}), {
|
|
142
|
+
status: 500,
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ========================================================================
|
|
148
|
+
// DO Storage Operations
|
|
149
|
+
// ========================================================================
|
|
150
|
+
static async save(state, data) {
|
|
151
|
+
await state.storage.put(`checkpoint:${data.checkpointId}`, data);
|
|
152
|
+
const taskIndexRaw = await state.storage.get(`task_index:${data.taskId}`);
|
|
153
|
+
const taskIndex = taskIndexRaw || [];
|
|
154
|
+
if (!taskIndex.includes(data.checkpointId)) {
|
|
155
|
+
taskIndex.push(data.checkpointId);
|
|
156
|
+
await state.storage.put(`task_index:${data.taskId}`, taskIndex);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
static async load(state, checkpointId) {
|
|
160
|
+
const data = await state.storage.get(`checkpoint:${checkpointId}`);
|
|
161
|
+
if (!data)
|
|
162
|
+
return null;
|
|
163
|
+
if (Date.now() > data.expiresAt) {
|
|
164
|
+
await CheckpointDO.delete(state, checkpointId);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return data;
|
|
168
|
+
}
|
|
169
|
+
static async delete(state, checkpointId) {
|
|
170
|
+
const data = await state.storage.get(`checkpoint:${checkpointId}`);
|
|
171
|
+
if (!data)
|
|
172
|
+
return;
|
|
173
|
+
await state.storage.delete(`checkpoint:${checkpointId}`);
|
|
174
|
+
const taskIndexRaw = await state.storage.get(`task_index:${data.taskId}`);
|
|
175
|
+
if (taskIndexRaw) {
|
|
176
|
+
const idx = taskIndexRaw.indexOf(checkpointId);
|
|
177
|
+
if (idx >= 0) {
|
|
178
|
+
taskIndexRaw.splice(idx, 1);
|
|
179
|
+
if (taskIndexRaw.length === 0) {
|
|
180
|
+
await state.storage.delete(`task_index:${data.taskId}`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
await state.storage.put(`task_index:${data.taskId}`, taskIndexRaw);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
static async list(state, taskId) {
|
|
189
|
+
const taskIndexRaw = await state.storage.get(`task_index:${taskId}`);
|
|
190
|
+
if (!taskIndexRaw)
|
|
191
|
+
return [];
|
|
192
|
+
const checkpoints = [];
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
for (const checkpointId of taskIndexRaw) {
|
|
195
|
+
const data = await state.storage.get(`checkpoint:${checkpointId}`);
|
|
196
|
+
if (data && now <= data.expiresAt) {
|
|
197
|
+
checkpoints.push(data);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return checkpoints;
|
|
201
|
+
}
|
|
202
|
+
static async gc(state) {
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const taskIndexEntries = await state.storage.list({
|
|
205
|
+
start: "task_index:",
|
|
206
|
+
end: "task_index:\xff",
|
|
207
|
+
});
|
|
208
|
+
for (const [key, value] of taskIndexEntries) {
|
|
209
|
+
const taskIndex = value;
|
|
210
|
+
const validCheckpoints = [];
|
|
211
|
+
for (const checkpointId of taskIndex) {
|
|
212
|
+
const data = await state.storage.get(`checkpoint:${checkpointId}`);
|
|
213
|
+
if (data && now <= data.expiresAt) {
|
|
214
|
+
validCheckpoints.push(checkpointId);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
await state.storage.delete(`checkpoint:${checkpointId}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (validCheckpoints.length === 0) {
|
|
221
|
+
await state.storage.delete(key);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await state.storage.put(key, validCheckpoints);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ============================================================================
|
|
230
|
+
// Effect-friendly API
|
|
231
|
+
// ============================================================================
|
|
232
|
+
//# sourceMappingURL=checkpoint-do.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ralphwiggums - Core
|
|
3
|
+
* Effect-first browser automation with OpenCode Zen.
|
|
4
|
+
* Production-ready with secure-by-default configuration.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
export interface RalphOptions {
|
|
8
|
+
maxIterations?: number;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
resumeFrom?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RalphResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
data?: unknown;
|
|
16
|
+
iterations: number;
|
|
17
|
+
checkpointId?: string;
|
|
18
|
+
requestId?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface RalphConfig {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
maxPromptLength: number;
|
|
23
|
+
maxConcurrent: number;
|
|
24
|
+
requestTimeout: number;
|
|
25
|
+
debug: boolean;
|
|
26
|
+
}
|
|
27
|
+
declare const ValidationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
28
|
+
readonly _tag: "ValidationError";
|
|
29
|
+
} & Readonly<A>;
|
|
30
|
+
export declare class ValidationError extends ValidationError_base<{
|
|
31
|
+
field: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}> {
|
|
34
|
+
}
|
|
35
|
+
declare const MaxIterationsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
36
|
+
readonly _tag: "MaxIterationsError";
|
|
37
|
+
} & Readonly<A>;
|
|
38
|
+
export declare class MaxIterationsError extends MaxIterationsError_base<{
|
|
39
|
+
maxIterations: number;
|
|
40
|
+
requestId: string;
|
|
41
|
+
}> {
|
|
42
|
+
}
|
|
43
|
+
declare const TimeoutError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
44
|
+
readonly _tag: "TimeoutError";
|
|
45
|
+
} & Readonly<A>;
|
|
46
|
+
export declare class TimeoutError extends TimeoutError_base<{
|
|
47
|
+
duration: number;
|
|
48
|
+
requestId: string;
|
|
49
|
+
}> {
|
|
50
|
+
}
|
|
51
|
+
declare const BrowserError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
52
|
+
readonly _tag: "BrowserError";
|
|
53
|
+
} & Readonly<A>;
|
|
54
|
+
export declare class BrowserError extends BrowserError_base<{
|
|
55
|
+
reason: string;
|
|
56
|
+
requestId: string;
|
|
57
|
+
}> {
|
|
58
|
+
}
|
|
59
|
+
declare const RateLimitError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
60
|
+
readonly _tag: "RateLimitError";
|
|
61
|
+
} & Readonly<A>;
|
|
62
|
+
export declare class RateLimitError extends RateLimitError_base<{
|
|
63
|
+
retryAfter: number;
|
|
64
|
+
}> {
|
|
65
|
+
}
|
|
66
|
+
declare const UnauthorizedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
67
|
+
readonly _tag: "UnauthorizedError";
|
|
68
|
+
} & Readonly<A>;
|
|
69
|
+
export declare class UnauthorizedError extends UnauthorizedError_base {
|
|
70
|
+
}
|
|
71
|
+
declare const InternalError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
72
|
+
readonly _tag: "InternalError";
|
|
73
|
+
} & Readonly<A>;
|
|
74
|
+
declare class InternalError extends InternalError_base {
|
|
75
|
+
readonly requestId: string;
|
|
76
|
+
readonly message: string;
|
|
77
|
+
constructor(requestId: string, message: string);
|
|
78
|
+
}
|
|
79
|
+
export declare function getConfig(): RalphConfig;
|
|
80
|
+
export declare function generateRequestId(): string;
|
|
81
|
+
export declare function log(requestId: string, level: string, message: string, meta?: Record<string, unknown>): void;
|
|
82
|
+
interface CircuitState {
|
|
83
|
+
state: "closed" | "open" | "half-open";
|
|
84
|
+
lastFailure: number;
|
|
85
|
+
failureCount: number;
|
|
86
|
+
}
|
|
87
|
+
export declare function getCircuitState(): CircuitState;
|
|
88
|
+
export interface CheckpointData {
|
|
89
|
+
checkpointId: string;
|
|
90
|
+
taskId: string;
|
|
91
|
+
iteration: number;
|
|
92
|
+
url?: string;
|
|
93
|
+
pageState?: string;
|
|
94
|
+
timestamp: number;
|
|
95
|
+
expiresAt: number;
|
|
96
|
+
}
|
|
97
|
+
export interface CheckpointStore {
|
|
98
|
+
save(data: CheckpointData): Promise<void>;
|
|
99
|
+
load(checkpointId: string): Promise<CheckpointData | null>;
|
|
100
|
+
delete(checkpointId: string): Promise<void>;
|
|
101
|
+
list(taskId: string): Promise<CheckpointData[]>;
|
|
102
|
+
gc(): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
export { CheckpointDO, createInMemoryCheckpointStore, type CheckpointDOState, } from "./checkpoint-do.js";
|
|
105
|
+
interface LocalDurableObjectStorage {
|
|
106
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
107
|
+
put(key: string, value: unknown): Promise<void>;
|
|
108
|
+
delete(key: string): Promise<void>;
|
|
109
|
+
list<T>(options?: {
|
|
110
|
+
start?: string;
|
|
111
|
+
end?: string;
|
|
112
|
+
limit?: number;
|
|
113
|
+
}): Promise<Map<string, T>>;
|
|
114
|
+
}
|
|
115
|
+
export declare function getCheckpointStore(): CheckpointStore;
|
|
116
|
+
/**
|
|
117
|
+
* Create a checkpoint store backed by Durable Objects.
|
|
118
|
+
* Use this for production deployments with multiple workers.
|
|
119
|
+
*
|
|
120
|
+
* @param state - DO state with storage
|
|
121
|
+
* @returns CheckpointStore implementation using DO storage
|
|
122
|
+
*/
|
|
123
|
+
export declare function createDOCheckpointStore(state: {
|
|
124
|
+
storage: LocalDurableObjectStorage;
|
|
125
|
+
}): CheckpointStore;
|
|
126
|
+
export declare function saveCheckpoint(requestId: string, taskId: string, iteration: number, url?: string, pageState?: string): Promise<string>;
|
|
127
|
+
export declare function loadCheckpoint(checkpointId: string): Promise<CheckpointData | null>;
|
|
128
|
+
export declare function acquireSemaphore(): Promise<void>;
|
|
129
|
+
export declare function releaseSemaphore(): void;
|
|
130
|
+
export declare function getActiveRequestCount(): number;
|
|
131
|
+
export declare function setContainerBinding(binding: any): void;
|
|
132
|
+
export declare function setContainerFetch(fetchFn: ((path: string, body?: object) => Promise<any>) | null): void;
|
|
133
|
+
export declare function setContainerUrl(url: string): void;
|
|
134
|
+
export declare function doThis(prompt: string, opts?: RalphOptions, requestId?: string): Effect.Effect<RalphResult, ValidationError | MaxIterationsError | TimeoutError | BrowserError | RateLimitError | InternalError, never>;
|
|
135
|
+
export declare function run(prompt: string, opts?: RalphOptions, requestId?: string): Promise<RalphResult>;
|
|
136
|
+
import { Hono } from "hono";
|
|
137
|
+
export declare function createHandlers(): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|