@orka-js/durable 1.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orka Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ // src/durable-agent.ts
4
+ function generateId() {
5
+ return `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
6
+ }
7
+ function now() {
8
+ return /* @__PURE__ */ new Date();
9
+ }
10
+ var DurableAgent = class {
11
+ agent;
12
+ store;
13
+ config;
14
+ schedulerHandle = null;
15
+ constructor(agent, store, config = {}) {
16
+ this.agent = agent;
17
+ this.store = store;
18
+ this.config = { maxRetries: 0, retryDelayMs: 1e3, ...config };
19
+ }
20
+ /**
21
+ * Run a job by ID.
22
+ * - Creates a new job if jobId doesn't exist yet.
23
+ * - If the job is already completed/cancelled, returns it as-is.
24
+ * - If paused, call resume() instead.
25
+ */
26
+ async run(jobId, input) {
27
+ let job = await this.store.load(jobId);
28
+ if (!job) {
29
+ job = {
30
+ id: jobId,
31
+ input,
32
+ status: "pending",
33
+ attempts: 0,
34
+ metadata: this.config.metadata,
35
+ createdAt: now(),
36
+ updatedAt: now()
37
+ };
38
+ await this.store.save(job);
39
+ }
40
+ if (job.status === "completed" || job.status === "cancelled") {
41
+ return job;
42
+ }
43
+ job.status = "running";
44
+ job.updatedAt = now();
45
+ job.attempts++;
46
+ await this.store.save(job);
47
+ try {
48
+ const result = await this.agent.run(input);
49
+ job.status = "completed";
50
+ job.result = result.output;
51
+ job.completedAt = now();
52
+ } catch (error) {
53
+ const err = error;
54
+ job.error = err.message;
55
+ if (job.attempts <= (this.config.maxRetries ?? 0)) {
56
+ job.status = "pending";
57
+ job.updatedAt = now();
58
+ await this.store.save(job);
59
+ if (this.config.retryDelayMs) {
60
+ await new Promise((r) => setTimeout(r, this.config.retryDelayMs));
61
+ }
62
+ return this.run(jobId, input);
63
+ }
64
+ job.status = "failed";
65
+ }
66
+ job.updatedAt = now();
67
+ await this.store.save(job);
68
+ return job;
69
+ }
70
+ /**
71
+ * Streaming run — yields LLM stream events plus job_status bookends.
72
+ */
73
+ async *runStream(jobId, input) {
74
+ let job = await this.store.load(jobId);
75
+ if (!job) {
76
+ job = {
77
+ id: jobId,
78
+ input,
79
+ status: "pending",
80
+ attempts: 0,
81
+ metadata: this.config.metadata,
82
+ createdAt: now(),
83
+ updatedAt: now()
84
+ };
85
+ }
86
+ if (job.status === "completed" || job.status === "cancelled") {
87
+ yield { type: "job_status", job };
88
+ return;
89
+ }
90
+ job.status = "running";
91
+ job.updatedAt = now();
92
+ job.attempts++;
93
+ await this.store.save(job);
94
+ yield { type: "job_status", job: { ...job } };
95
+ try {
96
+ if (typeof this.agent.runStream === "function") {
97
+ let finalContent = "";
98
+ for await (const event of this.agent.runStream(input)) {
99
+ yield event;
100
+ if (event.type === "done") finalContent = event.content;
101
+ }
102
+ job.status = "completed";
103
+ job.result = finalContent;
104
+ } else {
105
+ const result = await this.agent.run(input);
106
+ job.status = "completed";
107
+ job.result = result.output;
108
+ }
109
+ job.completedAt = now();
110
+ } catch (error) {
111
+ job.status = "failed";
112
+ job.error = error.message;
113
+ }
114
+ job.updatedAt = now();
115
+ await this.store.save(job);
116
+ yield { type: "job_status", job: { ...job } };
117
+ }
118
+ /** Pause a running job (marks as paused — the caller must stop feeding input). */
119
+ async pause(jobId) {
120
+ const job = await this.store.load(jobId);
121
+ if (!job) throw new Error(`Job "${jobId}" not found`);
122
+ if (job.status === "completed" || job.status === "cancelled") {
123
+ throw new Error(`Cannot pause a ${job.status} job`);
124
+ }
125
+ job.status = "paused";
126
+ job.updatedAt = now();
127
+ await this.store.save(job);
128
+ }
129
+ /** Resume a paused job with optional new input. */
130
+ async resume(jobId, newInput) {
131
+ const job = await this.store.load(jobId);
132
+ if (!job) throw new Error(`Job "${jobId}" not found`);
133
+ if (job.status !== "paused") {
134
+ throw new Error(`Job "${jobId}" is not paused (current status: ${job.status})`);
135
+ }
136
+ return this.run(jobId, newInput ?? job.input);
137
+ }
138
+ /** Cancel a job. */
139
+ async cancel(jobId) {
140
+ const job = await this.store.load(jobId);
141
+ if (!job) throw new Error(`Job "${jobId}" not found`);
142
+ job.status = "cancelled";
143
+ job.updatedAt = now();
144
+ await this.store.save(job);
145
+ }
146
+ /** Get job status. */
147
+ async status(jobId) {
148
+ return this.store.load(jobId);
149
+ }
150
+ /** List all jobs, optionally filtered by status. */
151
+ async list(filter) {
152
+ return this.store.list(filter);
153
+ }
154
+ /**
155
+ * Start a cron-scheduled agent (requires `node-cron` peer dep).
156
+ * @param schedule - Cron expression, e.g. '0 9 * * MON'
157
+ * @param inputFn - Function returning the input for each run
158
+ */
159
+ startScheduler() {
160
+ if (!this.config.schedule) throw new Error("DurableAgent: no schedule configured");
161
+ if (!this.config.onSchedule) throw new Error("DurableAgent: no onSchedule handler configured");
162
+ import('node-cron').then((cron) => {
163
+ this.schedulerHandle = cron.schedule(this.config.schedule, async () => {
164
+ const input = await this.config.onSchedule();
165
+ const jobId = generateId();
166
+ await this.run(jobId, input);
167
+ });
168
+ }).catch(() => {
169
+ throw new Error(
170
+ '@orka-js/durable: startScheduler() requires the "node-cron" package.\nInstall it with: npm install node-cron'
171
+ );
172
+ });
173
+ }
174
+ stopScheduler() {
175
+ if (this.schedulerHandle && typeof this.schedulerHandle.stop === "function") {
176
+ this.schedulerHandle.stop();
177
+ this.schedulerHandle = null;
178
+ }
179
+ }
180
+ };
181
+
182
+ // src/stores/memory-store.ts
183
+ var MemoryDurableStore = class {
184
+ jobs = /* @__PURE__ */ new Map();
185
+ async save(job) {
186
+ this.jobs.set(job.id, { ...job });
187
+ }
188
+ async load(jobId) {
189
+ const job = this.jobs.get(jobId);
190
+ return job ? { ...job } : null;
191
+ }
192
+ async list(filter) {
193
+ const all = [...this.jobs.values()].map((j) => ({ ...j }));
194
+ if (filter?.status) {
195
+ return all.filter((j) => j.status === filter.status);
196
+ }
197
+ return all.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
198
+ }
199
+ async delete(jobId) {
200
+ this.jobs.delete(jobId);
201
+ }
202
+ clear() {
203
+ this.jobs.clear();
204
+ }
205
+ size() {
206
+ return this.jobs.size;
207
+ }
208
+ };
209
+
210
+ // src/stores/redis-store.ts
211
+ var RedisDurableStore = class {
212
+ client = null;
213
+ prefix;
214
+ ttlSeconds;
215
+ url;
216
+ constructor(config = {}) {
217
+ this.url = config.url ?? "redis://localhost:6379";
218
+ this.prefix = config.prefix ?? "orka:durable:";
219
+ this.ttlSeconds = config.ttlSeconds ?? 60 * 60 * 24 * 7;
220
+ }
221
+ async getClient() {
222
+ if (this.client) return this.client;
223
+ let redis;
224
+ try {
225
+ redis = await import('redis');
226
+ } catch {
227
+ throw new Error(
228
+ '@orka-js/durable: RedisDurableStore requires the "redis" package.\nInstall it with: npm install redis'
229
+ );
230
+ }
231
+ const client = redis.createClient({ url: this.url });
232
+ await client.connect();
233
+ this.client = client;
234
+ return client;
235
+ }
236
+ key(jobId) {
237
+ return `${this.prefix}${jobId}`;
238
+ }
239
+ serialize(job) {
240
+ return JSON.stringify({
241
+ ...job,
242
+ createdAt: job.createdAt.toISOString(),
243
+ updatedAt: job.updatedAt.toISOString(),
244
+ completedAt: job.completedAt?.toISOString()
245
+ });
246
+ }
247
+ deserialize(raw) {
248
+ const data = JSON.parse(raw);
249
+ return {
250
+ ...data,
251
+ createdAt: new Date(data.createdAt),
252
+ updatedAt: new Date(data.updatedAt),
253
+ completedAt: data.completedAt ? new Date(data.completedAt) : void 0
254
+ };
255
+ }
256
+ async save(job) {
257
+ const client = await this.getClient();
258
+ await client.set(this.key(job.id), this.serialize(job));
259
+ if (["completed", "failed", "cancelled"].includes(job.status)) {
260
+ await client.expire(this.key(job.id), this.ttlSeconds);
261
+ }
262
+ }
263
+ async load(jobId) {
264
+ const client = await this.getClient();
265
+ const raw = await client.get(this.key(jobId));
266
+ return raw ? this.deserialize(raw) : null;
267
+ }
268
+ async list(filter) {
269
+ const client = await this.getClient();
270
+ const keys = await client.keys(`${this.prefix}*`);
271
+ if (keys.length === 0) return [];
272
+ const raws = await client.mGet(keys);
273
+ const jobs = raws.filter((r) => r !== null).map((r) => this.deserialize(r));
274
+ if (filter?.status) return jobs.filter((j) => j.status === filter.status);
275
+ return jobs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
276
+ }
277
+ async delete(jobId) {
278
+ const client = await this.getClient();
279
+ await client.del(this.key(jobId));
280
+ }
281
+ };
282
+
283
+ exports.DurableAgent = DurableAgent;
284
+ exports.MemoryDurableStore = MemoryDurableStore;
285
+ exports.RedisDurableStore = RedisDurableStore;
286
+ //# sourceMappingURL=index.cjs.map
287
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/durable-agent.ts","../src/stores/memory-store.ts","../src/stores/redis-store.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { LLMStreamEvent } from '@orka-js/core';\nimport type { BaseAgent } from '@orka-js/agent';\nimport type { DurableAgentConfig, DurableJob, DurableJobStatus, DurableStore } from './types.js';\n\nfunction generateId(): string {\n return `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction now(): Date {\n return new Date();\n}\n\ntype StreamableAgent = BaseAgent & {\n runStream?: (input: string) => AsyncIterable<LLMStreamEvent>;\n};\n\nexport class DurableAgent {\n private agent: StreamableAgent;\n private store: DurableStore;\n private config: DurableAgentConfig;\n private schedulerHandle: unknown = null;\n\n constructor(\n agent: BaseAgent,\n store: DurableStore,\n config: DurableAgentConfig = {},\n ) {\n this.agent = agent as StreamableAgent;\n this.store = store;\n this.config = { maxRetries: 0, retryDelayMs: 1000, ...config };\n }\n\n /**\n * Run a job by ID.\n * - Creates a new job if jobId doesn't exist yet.\n * - If the job is already completed/cancelled, returns it as-is.\n * - If paused, call resume() instead.\n */\n async run(jobId: string, input: string): Promise<DurableJob> {\n let job = await this.store.load(jobId);\n\n if (!job) {\n job = {\n id: jobId,\n input,\n status: 'pending',\n attempts: 0,\n metadata: this.config.metadata,\n createdAt: now(),\n updatedAt: now(),\n };\n await this.store.save(job);\n }\n\n if (job.status === 'completed' || job.status === 'cancelled') {\n return job;\n }\n\n job.status = 'running';\n job.updatedAt = now();\n job.attempts++;\n await this.store.save(job);\n\n try {\n const result = await this.agent.run(input);\n job.status = 'completed';\n job.result = result.output;\n job.completedAt = now();\n } catch (error) {\n const err = error as Error;\n job.error = err.message;\n\n if (job.attempts <= (this.config.maxRetries ?? 0)) {\n job.status = 'pending';\n job.updatedAt = now();\n await this.store.save(job);\n if (this.config.retryDelayMs) {\n await new Promise(r => setTimeout(r, this.config.retryDelayMs));\n }\n return this.run(jobId, input);\n }\n\n job.status = 'failed';\n }\n\n job.updatedAt = now();\n await this.store.save(job);\n return job;\n }\n\n /**\n * Streaming run — yields LLM stream events plus job_status bookends.\n */\n async *runStream(\n jobId: string,\n input: string,\n ): AsyncIterable<LLMStreamEvent | { type: 'job_status'; job: DurableJob }> {\n let job = await this.store.load(jobId);\n\n if (!job) {\n job = {\n id: jobId,\n input,\n status: 'pending',\n attempts: 0,\n metadata: this.config.metadata,\n createdAt: now(),\n updatedAt: now(),\n };\n }\n\n if (job.status === 'completed' || job.status === 'cancelled') {\n yield { type: 'job_status', job };\n return;\n }\n\n job.status = 'running';\n job.updatedAt = now();\n job.attempts++;\n await this.store.save(job);\n yield { type: 'job_status', job: { ...job } };\n\n try {\n if (typeof this.agent.runStream === 'function') {\n let finalContent = '';\n for await (const event of this.agent.runStream(input)) {\n yield event;\n if (event.type === 'done') finalContent = event.content;\n }\n job.status = 'completed';\n job.result = finalContent;\n } else {\n const result = await this.agent.run(input);\n job.status = 'completed';\n job.result = result.output;\n }\n job.completedAt = now();\n } catch (error) {\n job.status = 'failed';\n job.error = (error as Error).message;\n }\n\n job.updatedAt = now();\n await this.store.save(job);\n yield { type: 'job_status', job: { ...job } };\n }\n\n /** Pause a running job (marks as paused — the caller must stop feeding input). */\n async pause(jobId: string): Promise<void> {\n const job = await this.store.load(jobId);\n if (!job) throw new Error(`Job \"${jobId}\" not found`);\n if (job.status === 'completed' || job.status === 'cancelled') {\n throw new Error(`Cannot pause a ${job.status} job`);\n }\n job.status = 'paused';\n job.updatedAt = now();\n await this.store.save(job);\n }\n\n /** Resume a paused job with optional new input. */\n async resume(jobId: string, newInput?: string): Promise<DurableJob> {\n const job = await this.store.load(jobId);\n if (!job) throw new Error(`Job \"${jobId}\" not found`);\n if (job.status !== 'paused') {\n throw new Error(`Job \"${jobId}\" is not paused (current status: ${job.status})`);\n }\n return this.run(jobId, newInput ?? job.input);\n }\n\n /** Cancel a job. */\n async cancel(jobId: string): Promise<void> {\n const job = await this.store.load(jobId);\n if (!job) throw new Error(`Job \"${jobId}\" not found`);\n job.status = 'cancelled';\n job.updatedAt = now();\n await this.store.save(job);\n }\n\n /** Get job status. */\n async status(jobId: string): Promise<DurableJob | null> {\n return this.store.load(jobId);\n }\n\n /** List all jobs, optionally filtered by status. */\n async list(filter?: { status?: DurableJobStatus }): Promise<DurableJob[]> {\n return this.store.list(filter);\n }\n\n /**\n * Start a cron-scheduled agent (requires `node-cron` peer dep).\n * @param schedule - Cron expression, e.g. '0 9 * * MON'\n * @param inputFn - Function returning the input for each run\n */\n startScheduler(): void {\n if (!this.config.schedule) throw new Error('DurableAgent: no schedule configured');\n if (!this.config.onSchedule) throw new Error('DurableAgent: no onSchedule handler configured');\n\n import('node-cron').then(cron => {\n this.schedulerHandle = (cron as {\n schedule(expr: string, fn: () => void): unknown;\n }).schedule(this.config.schedule!, async () => {\n const input = await this.config.onSchedule!();\n const jobId = generateId();\n await this.run(jobId, input);\n });\n }).catch(() => {\n throw new Error(\n '@orka-js/durable: startScheduler() requires the \"node-cron\" package.\\n' +\n 'Install it with: npm install node-cron'\n );\n });\n }\n\n stopScheduler(): void {\n if (this.schedulerHandle && typeof (this.schedulerHandle as { stop?: () => void }).stop === 'function') {\n (this.schedulerHandle as { stop(): void }).stop();\n this.schedulerHandle = null;\n }\n }\n}\n","import type { DurableJob, DurableJobStatus, DurableStore } from '../types.js';\n\n/**\n * In-memory durable store — suitable for development and testing.\n * Data is lost on process restart.\n */\nexport class MemoryDurableStore implements DurableStore {\n private jobs: Map<string, DurableJob> = new Map();\n\n async save(job: DurableJob): Promise<void> {\n this.jobs.set(job.id, { ...job });\n }\n\n async load(jobId: string): Promise<DurableJob | null> {\n const job = this.jobs.get(jobId);\n return job ? { ...job } : null;\n }\n\n async list(filter?: { status?: DurableJobStatus }): Promise<DurableJob[]> {\n const all = [...this.jobs.values()].map(j => ({ ...j }));\n if (filter?.status) {\n return all.filter(j => j.status === filter.status);\n }\n return all.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n }\n\n async delete(jobId: string): Promise<void> {\n this.jobs.delete(jobId);\n }\n\n clear(): void {\n this.jobs.clear();\n }\n\n size(): number {\n return this.jobs.size;\n }\n}\n","import type { DurableJob, DurableJobStatus, DurableStore } from '../types.js';\n\nexport interface RedisDurableStoreConfig {\n /** Redis connection URL — default 'redis://localhost:6379' */\n url?: string;\n /** Key prefix — default 'orka:durable:' */\n prefix?: string;\n /** TTL for terminal jobs (completed/failed/cancelled) in seconds — default 7 days */\n ttlSeconds?: number;\n}\n\ntype RedisClient = {\n connect(): Promise<void>;\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n del(key: string): Promise<unknown>;\n keys(pattern: string): Promise<string[]>;\n mGet(keys: string[]): Promise<(string | null)[]>;\n};\n\n/**\n * Redis-backed durable store for production use.\n * Requires the `redis` package as a peer dependency.\n *\n * @example\n * ```typescript\n * import { RedisDurableStore } from '@orka-js/durable'\n * const store = new RedisDurableStore({ url: process.env.REDIS_URL })\n * ```\n */\nexport class RedisDurableStore implements DurableStore {\n private client: RedisClient | null = null;\n private prefix: string;\n private ttlSeconds: number;\n private url: string;\n\n constructor(config: RedisDurableStoreConfig = {}) {\n this.url = config.url ?? 'redis://localhost:6379';\n this.prefix = config.prefix ?? 'orka:durable:';\n this.ttlSeconds = config.ttlSeconds ?? 60 * 60 * 24 * 7;\n }\n\n private async getClient(): Promise<RedisClient> {\n if (this.client) return this.client;\n\n let redis: { createClient(config: { url: string }): RedisClient };\n try {\n redis = await import('redis') as unknown as typeof redis;\n } catch {\n throw new Error(\n '@orka-js/durable: RedisDurableStore requires the \"redis\" package.\\n' +\n 'Install it with: npm install redis'\n );\n }\n\n const client = redis.createClient({ url: this.url });\n await client.connect();\n this.client = client;\n return client;\n }\n\n private key(jobId: string): string {\n return `${this.prefix}${jobId}`;\n }\n\n private serialize(job: DurableJob): string {\n return JSON.stringify({\n ...job,\n createdAt: job.createdAt.toISOString(),\n updatedAt: job.updatedAt.toISOString(),\n completedAt: job.completedAt?.toISOString(),\n });\n }\n\n private deserialize(raw: string): DurableJob {\n const data = JSON.parse(raw) as unknown as DurableJob & { createdAt: string; updatedAt: string; completedAt?: string };\n return {\n ...data,\n createdAt: new Date(data.createdAt),\n updatedAt: new Date(data.updatedAt),\n completedAt: data.completedAt ? new Date(data.completedAt) : undefined,\n };\n }\n\n async save(job: DurableJob): Promise<void> {\n const client = await this.getClient();\n await client.set(this.key(job.id), this.serialize(job));\n if (['completed', 'failed', 'cancelled'].includes(job.status)) {\n await client.expire(this.key(job.id), this.ttlSeconds);\n }\n }\n\n async load(jobId: string): Promise<DurableJob | null> {\n const client = await this.getClient();\n const raw = await client.get(this.key(jobId));\n return raw ? this.deserialize(raw) : null;\n }\n\n async list(filter?: { status?: DurableJobStatus }): Promise<DurableJob[]> {\n const client = await this.getClient();\n const keys = await client.keys(`${this.prefix}*`);\n if (keys.length === 0) return [];\n const raws = await client.mGet(keys);\n const jobs = raws\n .filter((r): r is string => r !== null)\n .map(r => this.deserialize(r));\n if (filter?.status) return jobs.filter(j => j.status === filter.status);\n return jobs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n }\n\n async delete(jobId: string): Promise<void> {\n const client = await this.getClient();\n await client.del(this.key(jobId));\n }\n}\n"]}
@@ -0,0 +1,141 @@
1
+ import { LLMStreamEvent } from '@orka-js/core';
2
+ import { BaseAgent } from '@orka-js/agent';
3
+
4
+ type DurableJobStatus = 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
5
+ interface DurableJob {
6
+ id: string;
7
+ input: string;
8
+ status: DurableJobStatus;
9
+ result?: string;
10
+ error?: string;
11
+ /** Saved conversation messages for resuming */
12
+ messages?: Array<{
13
+ role: 'user' | 'assistant' | 'system';
14
+ content: string;
15
+ }>;
16
+ metadata?: Record<string, unknown>;
17
+ createdAt: Date;
18
+ updatedAt: Date;
19
+ completedAt?: Date;
20
+ /** Number of retry attempts so far */
21
+ attempts: number;
22
+ }
23
+ interface DurableStore {
24
+ save(job: DurableJob): Promise<void>;
25
+ load(jobId: string): Promise<DurableJob | null>;
26
+ list(filter?: {
27
+ status?: DurableJobStatus;
28
+ }): Promise<DurableJob[]>;
29
+ delete(jobId: string): Promise<void>;
30
+ }
31
+ interface DurableAgentConfig {
32
+ /** Max retries on failure — default 0 */
33
+ maxRetries?: number;
34
+ /** Delay between retries in ms — default 1000 */
35
+ retryDelayMs?: number;
36
+ /** Cron expression for scheduled runs (requires node-cron peer dep) */
37
+ schedule?: string;
38
+ /** Called on each scheduled run to produce the input string */
39
+ onSchedule?: () => Promise<string> | string;
40
+ /** Metadata to attach to every job */
41
+ metadata?: Record<string, unknown>;
42
+ }
43
+ type DurableStreamEvent = LLMStreamEvent | {
44
+ type: 'job_status';
45
+ job: DurableJob;
46
+ };
47
+
48
+ declare class DurableAgent {
49
+ private agent;
50
+ private store;
51
+ private config;
52
+ private schedulerHandle;
53
+ constructor(agent: BaseAgent, store: DurableStore, config?: DurableAgentConfig);
54
+ /**
55
+ * Run a job by ID.
56
+ * - Creates a new job if jobId doesn't exist yet.
57
+ * - If the job is already completed/cancelled, returns it as-is.
58
+ * - If paused, call resume() instead.
59
+ */
60
+ run(jobId: string, input: string): Promise<DurableJob>;
61
+ /**
62
+ * Streaming run — yields LLM stream events plus job_status bookends.
63
+ */
64
+ runStream(jobId: string, input: string): AsyncIterable<LLMStreamEvent | {
65
+ type: 'job_status';
66
+ job: DurableJob;
67
+ }>;
68
+ /** Pause a running job (marks as paused — the caller must stop feeding input). */
69
+ pause(jobId: string): Promise<void>;
70
+ /** Resume a paused job with optional new input. */
71
+ resume(jobId: string, newInput?: string): Promise<DurableJob>;
72
+ /** Cancel a job. */
73
+ cancel(jobId: string): Promise<void>;
74
+ /** Get job status. */
75
+ status(jobId: string): Promise<DurableJob | null>;
76
+ /** List all jobs, optionally filtered by status. */
77
+ list(filter?: {
78
+ status?: DurableJobStatus;
79
+ }): Promise<DurableJob[]>;
80
+ /**
81
+ * Start a cron-scheduled agent (requires `node-cron` peer dep).
82
+ * @param schedule - Cron expression, e.g. '0 9 * * MON'
83
+ * @param inputFn - Function returning the input for each run
84
+ */
85
+ startScheduler(): void;
86
+ stopScheduler(): void;
87
+ }
88
+
89
+ /**
90
+ * In-memory durable store — suitable for development and testing.
91
+ * Data is lost on process restart.
92
+ */
93
+ declare class MemoryDurableStore implements DurableStore {
94
+ private jobs;
95
+ save(job: DurableJob): Promise<void>;
96
+ load(jobId: string): Promise<DurableJob | null>;
97
+ list(filter?: {
98
+ status?: DurableJobStatus;
99
+ }): Promise<DurableJob[]>;
100
+ delete(jobId: string): Promise<void>;
101
+ clear(): void;
102
+ size(): number;
103
+ }
104
+
105
+ interface RedisDurableStoreConfig {
106
+ /** Redis connection URL — default 'redis://localhost:6379' */
107
+ url?: string;
108
+ /** Key prefix — default 'orka:durable:' */
109
+ prefix?: string;
110
+ /** TTL for terminal jobs (completed/failed/cancelled) in seconds — default 7 days */
111
+ ttlSeconds?: number;
112
+ }
113
+ /**
114
+ * Redis-backed durable store for production use.
115
+ * Requires the `redis` package as a peer dependency.
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * import { RedisDurableStore } from '@orka-js/durable'
120
+ * const store = new RedisDurableStore({ url: process.env.REDIS_URL })
121
+ * ```
122
+ */
123
+ declare class RedisDurableStore implements DurableStore {
124
+ private client;
125
+ private prefix;
126
+ private ttlSeconds;
127
+ private url;
128
+ constructor(config?: RedisDurableStoreConfig);
129
+ private getClient;
130
+ private key;
131
+ private serialize;
132
+ private deserialize;
133
+ save(job: DurableJob): Promise<void>;
134
+ load(jobId: string): Promise<DurableJob | null>;
135
+ list(filter?: {
136
+ status?: DurableJobStatus;
137
+ }): Promise<DurableJob[]>;
138
+ delete(jobId: string): Promise<void>;
139
+ }
140
+
141
+ export { DurableAgent, type DurableAgentConfig, type DurableJob, type DurableJobStatus, type DurableStore, type DurableStreamEvent, MemoryDurableStore, RedisDurableStore };
@@ -0,0 +1,141 @@
1
+ import { LLMStreamEvent } from '@orka-js/core';
2
+ import { BaseAgent } from '@orka-js/agent';
3
+
4
+ type DurableJobStatus = 'pending' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
5
+ interface DurableJob {
6
+ id: string;
7
+ input: string;
8
+ status: DurableJobStatus;
9
+ result?: string;
10
+ error?: string;
11
+ /** Saved conversation messages for resuming */
12
+ messages?: Array<{
13
+ role: 'user' | 'assistant' | 'system';
14
+ content: string;
15
+ }>;
16
+ metadata?: Record<string, unknown>;
17
+ createdAt: Date;
18
+ updatedAt: Date;
19
+ completedAt?: Date;
20
+ /** Number of retry attempts so far */
21
+ attempts: number;
22
+ }
23
+ interface DurableStore {
24
+ save(job: DurableJob): Promise<void>;
25
+ load(jobId: string): Promise<DurableJob | null>;
26
+ list(filter?: {
27
+ status?: DurableJobStatus;
28
+ }): Promise<DurableJob[]>;
29
+ delete(jobId: string): Promise<void>;
30
+ }
31
+ interface DurableAgentConfig {
32
+ /** Max retries on failure — default 0 */
33
+ maxRetries?: number;
34
+ /** Delay between retries in ms — default 1000 */
35
+ retryDelayMs?: number;
36
+ /** Cron expression for scheduled runs (requires node-cron peer dep) */
37
+ schedule?: string;
38
+ /** Called on each scheduled run to produce the input string */
39
+ onSchedule?: () => Promise<string> | string;
40
+ /** Metadata to attach to every job */
41
+ metadata?: Record<string, unknown>;
42
+ }
43
+ type DurableStreamEvent = LLMStreamEvent | {
44
+ type: 'job_status';
45
+ job: DurableJob;
46
+ };
47
+
48
+ declare class DurableAgent {
49
+ private agent;
50
+ private store;
51
+ private config;
52
+ private schedulerHandle;
53
+ constructor(agent: BaseAgent, store: DurableStore, config?: DurableAgentConfig);
54
+ /**
55
+ * Run a job by ID.
56
+ * - Creates a new job if jobId doesn't exist yet.
57
+ * - If the job is already completed/cancelled, returns it as-is.
58
+ * - If paused, call resume() instead.
59
+ */
60
+ run(jobId: string, input: string): Promise<DurableJob>;
61
+ /**
62
+ * Streaming run — yields LLM stream events plus job_status bookends.
63
+ */
64
+ runStream(jobId: string, input: string): AsyncIterable<LLMStreamEvent | {
65
+ type: 'job_status';
66
+ job: DurableJob;
67
+ }>;
68
+ /** Pause a running job (marks as paused — the caller must stop feeding input). */
69
+ pause(jobId: string): Promise<void>;
70
+ /** Resume a paused job with optional new input. */
71
+ resume(jobId: string, newInput?: string): Promise<DurableJob>;
72
+ /** Cancel a job. */
73
+ cancel(jobId: string): Promise<void>;
74
+ /** Get job status. */
75
+ status(jobId: string): Promise<DurableJob | null>;
76
+ /** List all jobs, optionally filtered by status. */
77
+ list(filter?: {
78
+ status?: DurableJobStatus;
79
+ }): Promise<DurableJob[]>;
80
+ /**
81
+ * Start a cron-scheduled agent (requires `node-cron` peer dep).
82
+ * @param schedule - Cron expression, e.g. '0 9 * * MON'
83
+ * @param inputFn - Function returning the input for each run
84
+ */
85
+ startScheduler(): void;
86
+ stopScheduler(): void;
87
+ }
88
+
89
+ /**
90
+ * In-memory durable store — suitable for development and testing.
91
+ * Data is lost on process restart.
92
+ */
93
+ declare class MemoryDurableStore implements DurableStore {
94
+ private jobs;
95
+ save(job: DurableJob): Promise<void>;
96
+ load(jobId: string): Promise<DurableJob | null>;
97
+ list(filter?: {
98
+ status?: DurableJobStatus;
99
+ }): Promise<DurableJob[]>;
100
+ delete(jobId: string): Promise<void>;
101
+ clear(): void;
102
+ size(): number;
103
+ }
104
+
105
+ interface RedisDurableStoreConfig {
106
+ /** Redis connection URL — default 'redis://localhost:6379' */
107
+ url?: string;
108
+ /** Key prefix — default 'orka:durable:' */
109
+ prefix?: string;
110
+ /** TTL for terminal jobs (completed/failed/cancelled) in seconds — default 7 days */
111
+ ttlSeconds?: number;
112
+ }
113
+ /**
114
+ * Redis-backed durable store for production use.
115
+ * Requires the `redis` package as a peer dependency.
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * import { RedisDurableStore } from '@orka-js/durable'
120
+ * const store = new RedisDurableStore({ url: process.env.REDIS_URL })
121
+ * ```
122
+ */
123
+ declare class RedisDurableStore implements DurableStore {
124
+ private client;
125
+ private prefix;
126
+ private ttlSeconds;
127
+ private url;
128
+ constructor(config?: RedisDurableStoreConfig);
129
+ private getClient;
130
+ private key;
131
+ private serialize;
132
+ private deserialize;
133
+ save(job: DurableJob): Promise<void>;
134
+ load(jobId: string): Promise<DurableJob | null>;
135
+ list(filter?: {
136
+ status?: DurableJobStatus;
137
+ }): Promise<DurableJob[]>;
138
+ delete(jobId: string): Promise<void>;
139
+ }
140
+
141
+ export { DurableAgent, type DurableAgentConfig, type DurableJob, type DurableJobStatus, type DurableStore, type DurableStreamEvent, MemoryDurableStore, RedisDurableStore };
package/dist/index.js ADDED
@@ -0,0 +1,283 @@
1
+ // src/durable-agent.ts
2
+ function generateId() {
3
+ return `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4
+ }
5
+ function now() {
6
+ return /* @__PURE__ */ new Date();
7
+ }
8
+ var DurableAgent = class {
9
+ agent;
10
+ store;
11
+ config;
12
+ schedulerHandle = null;
13
+ constructor(agent, store, config = {}) {
14
+ this.agent = agent;
15
+ this.store = store;
16
+ this.config = { maxRetries: 0, retryDelayMs: 1e3, ...config };
17
+ }
18
+ /**
19
+ * Run a job by ID.
20
+ * - Creates a new job if jobId doesn't exist yet.
21
+ * - If the job is already completed/cancelled, returns it as-is.
22
+ * - If paused, call resume() instead.
23
+ */
24
+ async run(jobId, input) {
25
+ let job = await this.store.load(jobId);
26
+ if (!job) {
27
+ job = {
28
+ id: jobId,
29
+ input,
30
+ status: "pending",
31
+ attempts: 0,
32
+ metadata: this.config.metadata,
33
+ createdAt: now(),
34
+ updatedAt: now()
35
+ };
36
+ await this.store.save(job);
37
+ }
38
+ if (job.status === "completed" || job.status === "cancelled") {
39
+ return job;
40
+ }
41
+ job.status = "running";
42
+ job.updatedAt = now();
43
+ job.attempts++;
44
+ await this.store.save(job);
45
+ try {
46
+ const result = await this.agent.run(input);
47
+ job.status = "completed";
48
+ job.result = result.output;
49
+ job.completedAt = now();
50
+ } catch (error) {
51
+ const err = error;
52
+ job.error = err.message;
53
+ if (job.attempts <= (this.config.maxRetries ?? 0)) {
54
+ job.status = "pending";
55
+ job.updatedAt = now();
56
+ await this.store.save(job);
57
+ if (this.config.retryDelayMs) {
58
+ await new Promise((r) => setTimeout(r, this.config.retryDelayMs));
59
+ }
60
+ return this.run(jobId, input);
61
+ }
62
+ job.status = "failed";
63
+ }
64
+ job.updatedAt = now();
65
+ await this.store.save(job);
66
+ return job;
67
+ }
68
+ /**
69
+ * Streaming run — yields LLM stream events plus job_status bookends.
70
+ */
71
+ async *runStream(jobId, input) {
72
+ let job = await this.store.load(jobId);
73
+ if (!job) {
74
+ job = {
75
+ id: jobId,
76
+ input,
77
+ status: "pending",
78
+ attempts: 0,
79
+ metadata: this.config.metadata,
80
+ createdAt: now(),
81
+ updatedAt: now()
82
+ };
83
+ }
84
+ if (job.status === "completed" || job.status === "cancelled") {
85
+ yield { type: "job_status", job };
86
+ return;
87
+ }
88
+ job.status = "running";
89
+ job.updatedAt = now();
90
+ job.attempts++;
91
+ await this.store.save(job);
92
+ yield { type: "job_status", job: { ...job } };
93
+ try {
94
+ if (typeof this.agent.runStream === "function") {
95
+ let finalContent = "";
96
+ for await (const event of this.agent.runStream(input)) {
97
+ yield event;
98
+ if (event.type === "done") finalContent = event.content;
99
+ }
100
+ job.status = "completed";
101
+ job.result = finalContent;
102
+ } else {
103
+ const result = await this.agent.run(input);
104
+ job.status = "completed";
105
+ job.result = result.output;
106
+ }
107
+ job.completedAt = now();
108
+ } catch (error) {
109
+ job.status = "failed";
110
+ job.error = error.message;
111
+ }
112
+ job.updatedAt = now();
113
+ await this.store.save(job);
114
+ yield { type: "job_status", job: { ...job } };
115
+ }
116
+ /** Pause a running job (marks as paused — the caller must stop feeding input). */
117
+ async pause(jobId) {
118
+ const job = await this.store.load(jobId);
119
+ if (!job) throw new Error(`Job "${jobId}" not found`);
120
+ if (job.status === "completed" || job.status === "cancelled") {
121
+ throw new Error(`Cannot pause a ${job.status} job`);
122
+ }
123
+ job.status = "paused";
124
+ job.updatedAt = now();
125
+ await this.store.save(job);
126
+ }
127
+ /** Resume a paused job with optional new input. */
128
+ async resume(jobId, newInput) {
129
+ const job = await this.store.load(jobId);
130
+ if (!job) throw new Error(`Job "${jobId}" not found`);
131
+ if (job.status !== "paused") {
132
+ throw new Error(`Job "${jobId}" is not paused (current status: ${job.status})`);
133
+ }
134
+ return this.run(jobId, newInput ?? job.input);
135
+ }
136
+ /** Cancel a job. */
137
+ async cancel(jobId) {
138
+ const job = await this.store.load(jobId);
139
+ if (!job) throw new Error(`Job "${jobId}" not found`);
140
+ job.status = "cancelled";
141
+ job.updatedAt = now();
142
+ await this.store.save(job);
143
+ }
144
+ /** Get job status. */
145
+ async status(jobId) {
146
+ return this.store.load(jobId);
147
+ }
148
+ /** List all jobs, optionally filtered by status. */
149
+ async list(filter) {
150
+ return this.store.list(filter);
151
+ }
152
+ /**
153
+ * Start a cron-scheduled agent (requires `node-cron` peer dep).
154
+ * @param schedule - Cron expression, e.g. '0 9 * * MON'
155
+ * @param inputFn - Function returning the input for each run
156
+ */
157
+ startScheduler() {
158
+ if (!this.config.schedule) throw new Error("DurableAgent: no schedule configured");
159
+ if (!this.config.onSchedule) throw new Error("DurableAgent: no onSchedule handler configured");
160
+ import('node-cron').then((cron) => {
161
+ this.schedulerHandle = cron.schedule(this.config.schedule, async () => {
162
+ const input = await this.config.onSchedule();
163
+ const jobId = generateId();
164
+ await this.run(jobId, input);
165
+ });
166
+ }).catch(() => {
167
+ throw new Error(
168
+ '@orka-js/durable: startScheduler() requires the "node-cron" package.\nInstall it with: npm install node-cron'
169
+ );
170
+ });
171
+ }
172
+ stopScheduler() {
173
+ if (this.schedulerHandle && typeof this.schedulerHandle.stop === "function") {
174
+ this.schedulerHandle.stop();
175
+ this.schedulerHandle = null;
176
+ }
177
+ }
178
+ };
179
+
180
+ // src/stores/memory-store.ts
181
+ var MemoryDurableStore = class {
182
+ jobs = /* @__PURE__ */ new Map();
183
+ async save(job) {
184
+ this.jobs.set(job.id, { ...job });
185
+ }
186
+ async load(jobId) {
187
+ const job = this.jobs.get(jobId);
188
+ return job ? { ...job } : null;
189
+ }
190
+ async list(filter) {
191
+ const all = [...this.jobs.values()].map((j) => ({ ...j }));
192
+ if (filter?.status) {
193
+ return all.filter((j) => j.status === filter.status);
194
+ }
195
+ return all.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
196
+ }
197
+ async delete(jobId) {
198
+ this.jobs.delete(jobId);
199
+ }
200
+ clear() {
201
+ this.jobs.clear();
202
+ }
203
+ size() {
204
+ return this.jobs.size;
205
+ }
206
+ };
207
+
208
+ // src/stores/redis-store.ts
209
+ var RedisDurableStore = class {
210
+ client = null;
211
+ prefix;
212
+ ttlSeconds;
213
+ url;
214
+ constructor(config = {}) {
215
+ this.url = config.url ?? "redis://localhost:6379";
216
+ this.prefix = config.prefix ?? "orka:durable:";
217
+ this.ttlSeconds = config.ttlSeconds ?? 60 * 60 * 24 * 7;
218
+ }
219
+ async getClient() {
220
+ if (this.client) return this.client;
221
+ let redis;
222
+ try {
223
+ redis = await import('redis');
224
+ } catch {
225
+ throw new Error(
226
+ '@orka-js/durable: RedisDurableStore requires the "redis" package.\nInstall it with: npm install redis'
227
+ );
228
+ }
229
+ const client = redis.createClient({ url: this.url });
230
+ await client.connect();
231
+ this.client = client;
232
+ return client;
233
+ }
234
+ key(jobId) {
235
+ return `${this.prefix}${jobId}`;
236
+ }
237
+ serialize(job) {
238
+ return JSON.stringify({
239
+ ...job,
240
+ createdAt: job.createdAt.toISOString(),
241
+ updatedAt: job.updatedAt.toISOString(),
242
+ completedAt: job.completedAt?.toISOString()
243
+ });
244
+ }
245
+ deserialize(raw) {
246
+ const data = JSON.parse(raw);
247
+ return {
248
+ ...data,
249
+ createdAt: new Date(data.createdAt),
250
+ updatedAt: new Date(data.updatedAt),
251
+ completedAt: data.completedAt ? new Date(data.completedAt) : void 0
252
+ };
253
+ }
254
+ async save(job) {
255
+ const client = await this.getClient();
256
+ await client.set(this.key(job.id), this.serialize(job));
257
+ if (["completed", "failed", "cancelled"].includes(job.status)) {
258
+ await client.expire(this.key(job.id), this.ttlSeconds);
259
+ }
260
+ }
261
+ async load(jobId) {
262
+ const client = await this.getClient();
263
+ const raw = await client.get(this.key(jobId));
264
+ return raw ? this.deserialize(raw) : null;
265
+ }
266
+ async list(filter) {
267
+ const client = await this.getClient();
268
+ const keys = await client.keys(`${this.prefix}*`);
269
+ if (keys.length === 0) return [];
270
+ const raws = await client.mGet(keys);
271
+ const jobs = raws.filter((r) => r !== null).map((r) => this.deserialize(r));
272
+ if (filter?.status) return jobs.filter((j) => j.status === filter.status);
273
+ return jobs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
274
+ }
275
+ async delete(jobId) {
276
+ const client = await this.getClient();
277
+ await client.del(this.key(jobId));
278
+ }
279
+ };
280
+
281
+ export { DurableAgent, MemoryDurableStore, RedisDurableStore };
282
+ //# sourceMappingURL=index.js.map
283
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/durable-agent.ts","../src/stores/memory-store.ts","../src/stores/redis-store.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { LLMStreamEvent } from '@orka-js/core';\nimport type { BaseAgent } from '@orka-js/agent';\nimport type { DurableAgentConfig, DurableJob, DurableJobStatus, DurableStore } from './types.js';\n\nfunction generateId(): string {\n return `job_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction now(): Date {\n return new Date();\n}\n\ntype StreamableAgent = BaseAgent & {\n runStream?: (input: string) => AsyncIterable<LLMStreamEvent>;\n};\n\nexport class DurableAgent {\n private agent: StreamableAgent;\n private store: DurableStore;\n private config: DurableAgentConfig;\n private schedulerHandle: unknown = null;\n\n constructor(\n agent: BaseAgent,\n store: DurableStore,\n config: DurableAgentConfig = {},\n ) {\n this.agent = agent as StreamableAgent;\n this.store = store;\n this.config = { maxRetries: 0, retryDelayMs: 1000, ...config };\n }\n\n /**\n * Run a job by ID.\n * - Creates a new job if jobId doesn't exist yet.\n * - If the job is already completed/cancelled, returns it as-is.\n * - If paused, call resume() instead.\n */\n async run(jobId: string, input: string): Promise<DurableJob> {\n let job = await this.store.load(jobId);\n\n if (!job) {\n job = {\n id: jobId,\n input,\n status: 'pending',\n attempts: 0,\n metadata: this.config.metadata,\n createdAt: now(),\n updatedAt: now(),\n };\n await this.store.save(job);\n }\n\n if (job.status === 'completed' || job.status === 'cancelled') {\n return job;\n }\n\n job.status = 'running';\n job.updatedAt = now();\n job.attempts++;\n await this.store.save(job);\n\n try {\n const result = await this.agent.run(input);\n job.status = 'completed';\n job.result = result.output;\n job.completedAt = now();\n } catch (error) {\n const err = error as Error;\n job.error = err.message;\n\n if (job.attempts <= (this.config.maxRetries ?? 0)) {\n job.status = 'pending';\n job.updatedAt = now();\n await this.store.save(job);\n if (this.config.retryDelayMs) {\n await new Promise(r => setTimeout(r, this.config.retryDelayMs));\n }\n return this.run(jobId, input);\n }\n\n job.status = 'failed';\n }\n\n job.updatedAt = now();\n await this.store.save(job);\n return job;\n }\n\n /**\n * Streaming run — yields LLM stream events plus job_status bookends.\n */\n async *runStream(\n jobId: string,\n input: string,\n ): AsyncIterable<LLMStreamEvent | { type: 'job_status'; job: DurableJob }> {\n let job = await this.store.load(jobId);\n\n if (!job) {\n job = {\n id: jobId,\n input,\n status: 'pending',\n attempts: 0,\n metadata: this.config.metadata,\n createdAt: now(),\n updatedAt: now(),\n };\n }\n\n if (job.status === 'completed' || job.status === 'cancelled') {\n yield { type: 'job_status', job };\n return;\n }\n\n job.status = 'running';\n job.updatedAt = now();\n job.attempts++;\n await this.store.save(job);\n yield { type: 'job_status', job: { ...job } };\n\n try {\n if (typeof this.agent.runStream === 'function') {\n let finalContent = '';\n for await (const event of this.agent.runStream(input)) {\n yield event;\n if (event.type === 'done') finalContent = event.content;\n }\n job.status = 'completed';\n job.result = finalContent;\n } else {\n const result = await this.agent.run(input);\n job.status = 'completed';\n job.result = result.output;\n }\n job.completedAt = now();\n } catch (error) {\n job.status = 'failed';\n job.error = (error as Error).message;\n }\n\n job.updatedAt = now();\n await this.store.save(job);\n yield { type: 'job_status', job: { ...job } };\n }\n\n /** Pause a running job (marks as paused — the caller must stop feeding input). */\n async pause(jobId: string): Promise<void> {\n const job = await this.store.load(jobId);\n if (!job) throw new Error(`Job \"${jobId}\" not found`);\n if (job.status === 'completed' || job.status === 'cancelled') {\n throw new Error(`Cannot pause a ${job.status} job`);\n }\n job.status = 'paused';\n job.updatedAt = now();\n await this.store.save(job);\n }\n\n /** Resume a paused job with optional new input. */\n async resume(jobId: string, newInput?: string): Promise<DurableJob> {\n const job = await this.store.load(jobId);\n if (!job) throw new Error(`Job \"${jobId}\" not found`);\n if (job.status !== 'paused') {\n throw new Error(`Job \"${jobId}\" is not paused (current status: ${job.status})`);\n }\n return this.run(jobId, newInput ?? job.input);\n }\n\n /** Cancel a job. */\n async cancel(jobId: string): Promise<void> {\n const job = await this.store.load(jobId);\n if (!job) throw new Error(`Job \"${jobId}\" not found`);\n job.status = 'cancelled';\n job.updatedAt = now();\n await this.store.save(job);\n }\n\n /** Get job status. */\n async status(jobId: string): Promise<DurableJob | null> {\n return this.store.load(jobId);\n }\n\n /** List all jobs, optionally filtered by status. */\n async list(filter?: { status?: DurableJobStatus }): Promise<DurableJob[]> {\n return this.store.list(filter);\n }\n\n /**\n * Start a cron-scheduled agent (requires `node-cron` peer dep).\n * @param schedule - Cron expression, e.g. '0 9 * * MON'\n * @param inputFn - Function returning the input for each run\n */\n startScheduler(): void {\n if (!this.config.schedule) throw new Error('DurableAgent: no schedule configured');\n if (!this.config.onSchedule) throw new Error('DurableAgent: no onSchedule handler configured');\n\n import('node-cron').then(cron => {\n this.schedulerHandle = (cron as {\n schedule(expr: string, fn: () => void): unknown;\n }).schedule(this.config.schedule!, async () => {\n const input = await this.config.onSchedule!();\n const jobId = generateId();\n await this.run(jobId, input);\n });\n }).catch(() => {\n throw new Error(\n '@orka-js/durable: startScheduler() requires the \"node-cron\" package.\\n' +\n 'Install it with: npm install node-cron'\n );\n });\n }\n\n stopScheduler(): void {\n if (this.schedulerHandle && typeof (this.schedulerHandle as { stop?: () => void }).stop === 'function') {\n (this.schedulerHandle as { stop(): void }).stop();\n this.schedulerHandle = null;\n }\n }\n}\n","import type { DurableJob, DurableJobStatus, DurableStore } from '../types.js';\n\n/**\n * In-memory durable store — suitable for development and testing.\n * Data is lost on process restart.\n */\nexport class MemoryDurableStore implements DurableStore {\n private jobs: Map<string, DurableJob> = new Map();\n\n async save(job: DurableJob): Promise<void> {\n this.jobs.set(job.id, { ...job });\n }\n\n async load(jobId: string): Promise<DurableJob | null> {\n const job = this.jobs.get(jobId);\n return job ? { ...job } : null;\n }\n\n async list(filter?: { status?: DurableJobStatus }): Promise<DurableJob[]> {\n const all = [...this.jobs.values()].map(j => ({ ...j }));\n if (filter?.status) {\n return all.filter(j => j.status === filter.status);\n }\n return all.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n }\n\n async delete(jobId: string): Promise<void> {\n this.jobs.delete(jobId);\n }\n\n clear(): void {\n this.jobs.clear();\n }\n\n size(): number {\n return this.jobs.size;\n }\n}\n","import type { DurableJob, DurableJobStatus, DurableStore } from '../types.js';\n\nexport interface RedisDurableStoreConfig {\n /** Redis connection URL — default 'redis://localhost:6379' */\n url?: string;\n /** Key prefix — default 'orka:durable:' */\n prefix?: string;\n /** TTL for terminal jobs (completed/failed/cancelled) in seconds — default 7 days */\n ttlSeconds?: number;\n}\n\ntype RedisClient = {\n connect(): Promise<void>;\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<unknown>;\n expire(key: string, seconds: number): Promise<unknown>;\n del(key: string): Promise<unknown>;\n keys(pattern: string): Promise<string[]>;\n mGet(keys: string[]): Promise<(string | null)[]>;\n};\n\n/**\n * Redis-backed durable store for production use.\n * Requires the `redis` package as a peer dependency.\n *\n * @example\n * ```typescript\n * import { RedisDurableStore } from '@orka-js/durable'\n * const store = new RedisDurableStore({ url: process.env.REDIS_URL })\n * ```\n */\nexport class RedisDurableStore implements DurableStore {\n private client: RedisClient | null = null;\n private prefix: string;\n private ttlSeconds: number;\n private url: string;\n\n constructor(config: RedisDurableStoreConfig = {}) {\n this.url = config.url ?? 'redis://localhost:6379';\n this.prefix = config.prefix ?? 'orka:durable:';\n this.ttlSeconds = config.ttlSeconds ?? 60 * 60 * 24 * 7;\n }\n\n private async getClient(): Promise<RedisClient> {\n if (this.client) return this.client;\n\n let redis: { createClient(config: { url: string }): RedisClient };\n try {\n redis = await import('redis') as unknown as typeof redis;\n } catch {\n throw new Error(\n '@orka-js/durable: RedisDurableStore requires the \"redis\" package.\\n' +\n 'Install it with: npm install redis'\n );\n }\n\n const client = redis.createClient({ url: this.url });\n await client.connect();\n this.client = client;\n return client;\n }\n\n private key(jobId: string): string {\n return `${this.prefix}${jobId}`;\n }\n\n private serialize(job: DurableJob): string {\n return JSON.stringify({\n ...job,\n createdAt: job.createdAt.toISOString(),\n updatedAt: job.updatedAt.toISOString(),\n completedAt: job.completedAt?.toISOString(),\n });\n }\n\n private deserialize(raw: string): DurableJob {\n const data = JSON.parse(raw) as unknown as DurableJob & { createdAt: string; updatedAt: string; completedAt?: string };\n return {\n ...data,\n createdAt: new Date(data.createdAt),\n updatedAt: new Date(data.updatedAt),\n completedAt: data.completedAt ? new Date(data.completedAt) : undefined,\n };\n }\n\n async save(job: DurableJob): Promise<void> {\n const client = await this.getClient();\n await client.set(this.key(job.id), this.serialize(job));\n if (['completed', 'failed', 'cancelled'].includes(job.status)) {\n await client.expire(this.key(job.id), this.ttlSeconds);\n }\n }\n\n async load(jobId: string): Promise<DurableJob | null> {\n const client = await this.getClient();\n const raw = await client.get(this.key(jobId));\n return raw ? this.deserialize(raw) : null;\n }\n\n async list(filter?: { status?: DurableJobStatus }): Promise<DurableJob[]> {\n const client = await this.getClient();\n const keys = await client.keys(`${this.prefix}*`);\n if (keys.length === 0) return [];\n const raws = await client.mGet(keys);\n const jobs = raws\n .filter((r): r is string => r !== null)\n .map(r => this.deserialize(r));\n if (filter?.status) return jobs.filter(j => j.status === filter.status);\n return jobs.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n }\n\n async delete(jobId: string): Promise<void> {\n const client = await this.getClient();\n await client.del(this.key(jobId));\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@orka-js/durable",
3
+ "version": "1.0.0",
4
+ "description": "Durable, resumable, and scheduled agents for OrkaJS",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "keywords": [
21
+ "orkajs",
22
+ "orka",
23
+ "durable",
24
+ "agent",
25
+ "persistent",
26
+ "scheduler",
27
+ "llm",
28
+ "ai"
29
+ ],
30
+ "author": "OrkaJS Team",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/orka-js/orkajs.git",
35
+ "directory": "packages/durable"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@orka-js/core": "1.5.0",
42
+ "@orka-js/agent": "1.5.2"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node-cron": "^3.0.11",
46
+ "tsup": "^8.0.1",
47
+ "typescript": "^5.3.3",
48
+ "vitest": "^1.6.0"
49
+ },
50
+ "peerDependencies": {
51
+ "node-cron": "^3.0.0",
52
+ "redis": "^4.6.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "redis": {
56
+ "optional": true
57
+ },
58
+ "node-cron": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "dev": "tsup --watch",
68
+ "typecheck": "tsc --noEmit",
69
+ "test": "vitest run"
70
+ }
71
+ }