@ai2070/memex 0.9.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.
Files changed (46) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/.github/workflows/release.yml +35 -0
  3. package/API.md +1078 -0
  4. package/LICENSE +190 -0
  5. package/README.md +574 -0
  6. package/package.json +30 -0
  7. package/src/bulk.ts +128 -0
  8. package/src/envelope.ts +52 -0
  9. package/src/errors.ts +27 -0
  10. package/src/graph.ts +15 -0
  11. package/src/helpers.ts +51 -0
  12. package/src/index.ts +142 -0
  13. package/src/integrity.ts +378 -0
  14. package/src/intent.ts +311 -0
  15. package/src/query.ts +357 -0
  16. package/src/reducer.ts +177 -0
  17. package/src/replay.ts +32 -0
  18. package/src/retrieval.ts +306 -0
  19. package/src/serialization.ts +34 -0
  20. package/src/stats.ts +62 -0
  21. package/src/task.ts +373 -0
  22. package/src/transplant.ts +488 -0
  23. package/src/types.ts +248 -0
  24. package/tests/bugfix-and-coverage.test.ts +958 -0
  25. package/tests/bugfix-holes.test.ts +856 -0
  26. package/tests/bulk.test.ts +256 -0
  27. package/tests/edge-cases-v2.test.ts +355 -0
  28. package/tests/edge-cases.test.ts +661 -0
  29. package/tests/envelope.test.ts +92 -0
  30. package/tests/graph.test.ts +41 -0
  31. package/tests/helpers.test.ts +120 -0
  32. package/tests/integrity.test.ts +371 -0
  33. package/tests/intent.test.ts +276 -0
  34. package/tests/query-advanced.test.ts +252 -0
  35. package/tests/query.test.ts +623 -0
  36. package/tests/reducer.test.ts +342 -0
  37. package/tests/replay.test.ts +145 -0
  38. package/tests/retrieval.test.ts +691 -0
  39. package/tests/serialization.test.ts +118 -0
  40. package/tests/setup.test.ts +7 -0
  41. package/tests/stats.test.ts +163 -0
  42. package/tests/task.test.ts +322 -0
  43. package/tests/transplant.test.ts +385 -0
  44. package/tests/types.test.ts +231 -0
  45. package/tsconfig.json +18 -0
  46. package/vitest.config.ts +7 -0
package/src/task.ts ADDED
@@ -0,0 +1,373 @@
1
+ import { uuidv7 } from "uuidv7";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type TaskStatus =
8
+ | "pending"
9
+ | "running"
10
+ | "completed"
11
+ | "failed"
12
+ | "cancelled";
13
+
14
+ export interface Task {
15
+ id: string;
16
+ intent_id: string;
17
+
18
+ action: string; // "search_linkedin", "summarize_case"
19
+ label?: string;
20
+
21
+ status: TaskStatus;
22
+ priority: number; // 0..1
23
+
24
+ context?: Record<string, unknown>;
25
+ result?: Record<string, unknown>;
26
+ error?: string;
27
+
28
+ input_memory_ids?: string[];
29
+ output_memory_ids?: string[];
30
+
31
+ agent_id?: string;
32
+ attempt?: number;
33
+
34
+ meta?: Record<string, unknown>;
35
+ }
36
+
37
+ export interface TaskState {
38
+ tasks: Map<string, Task>;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // State
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export function createTaskState(): TaskState {
46
+ return { tasks: new Map() };
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Factory
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export function createTask(
54
+ input: Omit<Task, "id" | "status" | "attempt"> & {
55
+ id?: string;
56
+ status?: TaskStatus;
57
+ attempt?: number;
58
+ },
59
+ ): Task {
60
+ return {
61
+ ...input,
62
+ id: input.id ?? uuidv7(),
63
+ status: input.status ?? "pending",
64
+ attempt: input.attempt ?? 0,
65
+ };
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Commands
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export type TaskCommand =
73
+ | { type: "task.create"; task: Task }
74
+ | {
75
+ type: "task.update";
76
+ task_id: string;
77
+ partial: Partial<Task>;
78
+ author: string;
79
+ }
80
+ | { type: "task.start"; task_id: string; agent_id?: string }
81
+ | {
82
+ type: "task.complete";
83
+ task_id: string;
84
+ result?: Record<string, unknown>;
85
+ output_memory_ids?: string[];
86
+ }
87
+ | { type: "task.fail"; task_id: string; error: string; retryable?: boolean }
88
+ | { type: "task.cancel"; task_id: string; reason?: string };
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Lifecycle events
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export interface TaskLifecycleEvent {
95
+ namespace: "task";
96
+ type:
97
+ | "task.created"
98
+ | "task.updated"
99
+ | "task.started"
100
+ | "task.completed"
101
+ | "task.failed"
102
+ | "task.cancelled";
103
+ task: Task;
104
+ cause_type: string;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Errors
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export class TaskNotFoundError extends Error {
112
+ constructor(id: string) {
113
+ super(`Task not found: ${id}`);
114
+ this.name = "TaskNotFoundError";
115
+ }
116
+ }
117
+
118
+ export class DuplicateTaskError extends Error {
119
+ constructor(id: string) {
120
+ super(`Task already exists: ${id}`);
121
+ this.name = "DuplicateTaskError";
122
+ }
123
+ }
124
+
125
+ export class InvalidTaskTransitionError extends Error {
126
+ constructor(id: string, from: TaskStatus, to: string) {
127
+ super(`Invalid task transition: ${id} from ${from} to ${to}`);
128
+ this.name = "InvalidTaskTransitionError";
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Helpers
134
+ // ---------------------------------------------------------------------------
135
+
136
+ function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
137
+ const result: Record<string, unknown> = {};
138
+ for (const [key, value] of Object.entries(obj)) {
139
+ if (value !== undefined) result[key] = value;
140
+ }
141
+ return result as Partial<T>;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Reducer
146
+ // ---------------------------------------------------------------------------
147
+
148
+ export function applyTaskCommand(
149
+ state: TaskState,
150
+ cmd: TaskCommand,
151
+ ): { state: TaskState; events: TaskLifecycleEvent[] } {
152
+ switch (cmd.type) {
153
+ case "task.create": {
154
+ if (state.tasks.has(cmd.task.id)) {
155
+ throw new DuplicateTaskError(cmd.task.id);
156
+ }
157
+ const tasks = new Map(state.tasks);
158
+ tasks.set(cmd.task.id, cmd.task);
159
+ return {
160
+ state: { tasks },
161
+ events: [
162
+ {
163
+ namespace: "task",
164
+ type: "task.created",
165
+ task: cmd.task,
166
+ cause_type: cmd.type,
167
+ },
168
+ ],
169
+ };
170
+ }
171
+
172
+ case "task.update": {
173
+ const existing = state.tasks.get(cmd.task_id);
174
+ if (!existing) throw new TaskNotFoundError(cmd.task_id);
175
+ const { id: _id, status: _status, ...rest } = cmd.partial;
176
+ const updated: Task = { ...existing, ...stripUndefined(rest) };
177
+ const tasks = new Map(state.tasks);
178
+ tasks.set(cmd.task_id, updated);
179
+ return {
180
+ state: { tasks },
181
+ events: [
182
+ {
183
+ namespace: "task",
184
+ type: "task.updated",
185
+ task: updated,
186
+ cause_type: cmd.type,
187
+ },
188
+ ],
189
+ };
190
+ }
191
+
192
+ case "task.start": {
193
+ const existing = state.tasks.get(cmd.task_id);
194
+ if (!existing) throw new TaskNotFoundError(cmd.task_id);
195
+ if (existing.status !== "pending" && existing.status !== "failed") {
196
+ throw new InvalidTaskTransitionError(
197
+ cmd.task_id,
198
+ existing.status,
199
+ "running",
200
+ );
201
+ }
202
+ const updated: Task = {
203
+ ...existing,
204
+ status: "running",
205
+ agent_id: cmd.agent_id ?? existing.agent_id,
206
+ attempt: (existing.attempt ?? 0) + 1,
207
+ };
208
+ const tasks = new Map(state.tasks);
209
+ tasks.set(cmd.task_id, updated);
210
+ return {
211
+ state: { tasks },
212
+ events: [
213
+ {
214
+ namespace: "task",
215
+ type: "task.started",
216
+ task: updated,
217
+ cause_type: cmd.type,
218
+ },
219
+ ],
220
+ };
221
+ }
222
+
223
+ case "task.complete": {
224
+ const existing = state.tasks.get(cmd.task_id);
225
+ if (!existing) throw new TaskNotFoundError(cmd.task_id);
226
+ if (existing.status !== "running") {
227
+ throw new InvalidTaskTransitionError(
228
+ cmd.task_id,
229
+ existing.status,
230
+ "completed",
231
+ );
232
+ }
233
+ const updated: Task = {
234
+ ...existing,
235
+ status: "completed",
236
+ result: cmd.result ?? existing.result,
237
+ output_memory_ids: cmd.output_memory_ids ?? existing.output_memory_ids,
238
+ };
239
+ const tasks = new Map(state.tasks);
240
+ tasks.set(cmd.task_id, updated);
241
+ return {
242
+ state: { tasks },
243
+ events: [
244
+ {
245
+ namespace: "task",
246
+ type: "task.completed",
247
+ task: updated,
248
+ cause_type: cmd.type,
249
+ },
250
+ ],
251
+ };
252
+ }
253
+
254
+ case "task.fail": {
255
+ const existing = state.tasks.get(cmd.task_id);
256
+ if (!existing) throw new TaskNotFoundError(cmd.task_id);
257
+ if (existing.status !== "running") {
258
+ throw new InvalidTaskTransitionError(
259
+ cmd.task_id,
260
+ existing.status,
261
+ "failed",
262
+ );
263
+ }
264
+ const updated: Task = {
265
+ ...existing,
266
+ status: "failed",
267
+ error: cmd.error,
268
+ };
269
+ const tasks = new Map(state.tasks);
270
+ tasks.set(cmd.task_id, updated);
271
+ return {
272
+ state: { tasks },
273
+ events: [
274
+ {
275
+ namespace: "task",
276
+ type: "task.failed",
277
+ task: updated,
278
+ cause_type: cmd.type,
279
+ },
280
+ ],
281
+ };
282
+ }
283
+
284
+ case "task.cancel": {
285
+ const existing = state.tasks.get(cmd.task_id);
286
+ if (!existing) throw new TaskNotFoundError(cmd.task_id);
287
+ if (existing.status === "completed" || existing.status === "cancelled") {
288
+ throw new InvalidTaskTransitionError(
289
+ cmd.task_id,
290
+ existing.status,
291
+ "cancelled",
292
+ );
293
+ }
294
+ const updated: Task = {
295
+ ...existing,
296
+ status: "cancelled",
297
+ };
298
+ const tasks = new Map(state.tasks);
299
+ tasks.set(cmd.task_id, updated);
300
+ return {
301
+ state: { tasks },
302
+ events: [
303
+ {
304
+ namespace: "task",
305
+ type: "task.cancelled",
306
+ task: updated,
307
+ cause_type: cmd.type,
308
+ },
309
+ ],
310
+ };
311
+ }
312
+ }
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Query
317
+ // ---------------------------------------------------------------------------
318
+
319
+ export interface TaskFilter {
320
+ intent_id?: string;
321
+ action?: string;
322
+ status?: TaskStatus;
323
+ statuses?: TaskStatus[];
324
+ agent_id?: string;
325
+ min_priority?: number;
326
+ has_input_memory_id?: string;
327
+ has_output_memory_id?: string;
328
+ }
329
+
330
+ export function getTasks(state: TaskState, filter?: TaskFilter): Task[] {
331
+ if (!filter) return [...state.tasks.values()];
332
+
333
+ const results: Task[] = [];
334
+ for (const task of state.tasks.values()) {
335
+ if (filter.intent_id !== undefined && task.intent_id !== filter.intent_id)
336
+ continue;
337
+ if (filter.action !== undefined && task.action !== filter.action) continue;
338
+ if (filter.status !== undefined && task.status !== filter.status) continue;
339
+ if (filter.statuses !== undefined && !filter.statuses.includes(task.status))
340
+ continue;
341
+ if (filter.agent_id !== undefined && task.agent_id !== filter.agent_id)
342
+ continue;
343
+ if (
344
+ filter.min_priority !== undefined &&
345
+ task.priority < filter.min_priority
346
+ )
347
+ continue;
348
+ if (filter.has_input_memory_id !== undefined) {
349
+ if (
350
+ !task.input_memory_ids ||
351
+ !task.input_memory_ids.includes(filter.has_input_memory_id)
352
+ )
353
+ continue;
354
+ }
355
+ if (filter.has_output_memory_id !== undefined) {
356
+ if (
357
+ !task.output_memory_ids ||
358
+ !task.output_memory_ids.includes(filter.has_output_memory_id)
359
+ )
360
+ continue;
361
+ }
362
+ results.push(task);
363
+ }
364
+ return results;
365
+ }
366
+
367
+ export function getTaskById(state: TaskState, id: string): Task | undefined {
368
+ return state.tasks.get(id);
369
+ }
370
+
371
+ export function getTasksByIntent(state: TaskState, intentId: string): Task[] {
372
+ return getTasks(state, { intent_id: intentId });
373
+ }