@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.
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +35 -0
- package/API.md +1078 -0
- package/LICENSE +190 -0
- package/README.md +574 -0
- package/package.json +30 -0
- package/src/bulk.ts +128 -0
- package/src/envelope.ts +52 -0
- package/src/errors.ts +27 -0
- package/src/graph.ts +15 -0
- package/src/helpers.ts +51 -0
- package/src/index.ts +142 -0
- package/src/integrity.ts +378 -0
- package/src/intent.ts +311 -0
- package/src/query.ts +357 -0
- package/src/reducer.ts +177 -0
- package/src/replay.ts +32 -0
- package/src/retrieval.ts +306 -0
- package/src/serialization.ts +34 -0
- package/src/stats.ts +62 -0
- package/src/task.ts +373 -0
- package/src/transplant.ts +488 -0
- package/src/types.ts +248 -0
- package/tests/bugfix-and-coverage.test.ts +958 -0
- package/tests/bugfix-holes.test.ts +856 -0
- package/tests/bulk.test.ts +256 -0
- package/tests/edge-cases-v2.test.ts +355 -0
- package/tests/edge-cases.test.ts +661 -0
- package/tests/envelope.test.ts +92 -0
- package/tests/graph.test.ts +41 -0
- package/tests/helpers.test.ts +120 -0
- package/tests/integrity.test.ts +371 -0
- package/tests/intent.test.ts +276 -0
- package/tests/query-advanced.test.ts +252 -0
- package/tests/query.test.ts +623 -0
- package/tests/reducer.test.ts +342 -0
- package/tests/replay.test.ts +145 -0
- package/tests/retrieval.test.ts +691 -0
- package/tests/serialization.test.ts +118 -0
- package/tests/setup.test.ts +7 -0
- package/tests/stats.test.ts +163 -0
- package/tests/task.test.ts +322 -0
- package/tests/transplant.test.ts +385 -0
- package/tests/types.test.ts +231 -0
- package/tsconfig.json +18 -0
- 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
|
+
}
|