@ddse/acm-runtime 0.5.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +393 -0
  3. package/dist/src/checkpoint.d.ts +97 -0
  4. package/dist/src/checkpoint.d.ts.map +1 -0
  5. package/dist/src/checkpoint.js +200 -0
  6. package/dist/src/checkpoint.js.map +1 -0
  7. package/dist/src/execution-transcript.d.ts +30 -0
  8. package/dist/src/execution-transcript.d.ts.map +1 -0
  9. package/dist/src/execution-transcript.js +70 -0
  10. package/dist/src/execution-transcript.js.map +1 -0
  11. package/dist/src/executor.d.ts +49 -0
  12. package/dist/src/executor.d.ts.map +1 -0
  13. package/dist/src/executor.js +390 -0
  14. package/dist/src/executor.js.map +1 -0
  15. package/dist/src/guards.d.ts +7 -0
  16. package/dist/src/guards.d.ts.map +1 -0
  17. package/dist/src/guards.js +13 -0
  18. package/dist/src/guards.js.map +1 -0
  19. package/dist/src/index.d.ts +9 -0
  20. package/dist/src/index.d.ts.map +1 -0
  21. package/dist/src/index.js +10 -0
  22. package/dist/src/index.js.map +1 -0
  23. package/dist/src/ledger.d.ts +12 -0
  24. package/dist/src/ledger.d.ts.map +1 -0
  25. package/dist/src/ledger.js +53 -0
  26. package/dist/src/ledger.js.map +1 -0
  27. package/dist/src/resumable-executor.d.ts +39 -0
  28. package/dist/src/resumable-executor.d.ts.map +1 -0
  29. package/dist/src/resumable-executor.js +354 -0
  30. package/dist/src/resumable-executor.js.map +1 -0
  31. package/dist/src/retry.d.ts +7 -0
  32. package/dist/src/retry.d.ts.map +1 -0
  33. package/dist/src/retry.js +25 -0
  34. package/dist/src/retry.js.map +1 -0
  35. package/dist/src/tool-envelope.d.ts +14 -0
  36. package/dist/src/tool-envelope.d.ts.map +1 -0
  37. package/dist/src/tool-envelope.js +84 -0
  38. package/dist/src/tool-envelope.js.map +1 -0
  39. package/dist/tests/resumable.test.d.ts +2 -0
  40. package/dist/tests/resumable.test.d.ts.map +1 -0
  41. package/dist/tests/resumable.test.js +337 -0
  42. package/dist/tests/resumable.test.js.map +1 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +29 -0
  45. package/src/checkpoint.ts +311 -0
  46. package/src/execution-transcript.ts +108 -0
  47. package/src/executor.ts +540 -0
  48. package/src/guards.ts +21 -0
  49. package/src/index.ts +9 -0
  50. package/src/ledger.ts +63 -0
  51. package/src/resumable-executor.ts +471 -0
  52. package/src/retry.ts +37 -0
  53. package/src/tool-envelope.ts +113 -0
  54. package/tests/resumable.test.ts +421 -0
  55. package/tsconfig.json +11 -0
@@ -0,0 +1,311 @@
1
+ // Checkpoint and resume support for ACM runtime
2
+ import type {
3
+ Goal,
4
+ Context,
5
+ Plan,
6
+ LedgerEntry,
7
+ } from '@ddse/acm-sdk';
8
+ import type { TaskExecutionRecord } from './executor.js';
9
+
10
+ /**
11
+ * Schema version for checkpoint compatibility
12
+ */
13
+ export const CHECKPOINT_VERSION = '1.0.0';
14
+
15
+ /**
16
+ * Checkpoint represents a snapshot of execution state
17
+ */
18
+ export interface Checkpoint {
19
+ id: string;
20
+ runId: string;
21
+ ts: number;
22
+ version: string;
23
+ state: CheckpointState;
24
+ }
25
+
26
+ /**
27
+ * Execution state captured in a checkpoint
28
+ */
29
+ export interface CheckpointState {
30
+ goal: Goal;
31
+ context: Context;
32
+ plan: Plan;
33
+ outputs: Record<string, any>;
34
+ executionRecords?: Record<string, TaskExecutionRecord>;
35
+ executed: string[]; // Task IDs that have completed
36
+ ledger: LedgerEntry[];
37
+ metrics: {
38
+ costUsd: number;
39
+ elapsedSec: number;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Metadata for a checkpoint (lightweight listing)
45
+ */
46
+ export interface CheckpointMetadata {
47
+ id: string;
48
+ runId: string;
49
+ ts: number;
50
+ version: string;
51
+ tasksCompleted: number;
52
+ }
53
+
54
+ /**
55
+ * Storage interface for checkpoints
56
+ */
57
+ export interface CheckpointStore {
58
+ /**
59
+ * Store a checkpoint
60
+ */
61
+ put(runId: string, checkpoint: Checkpoint): Promise<void>;
62
+
63
+ /**
64
+ * Retrieve a checkpoint by ID (or latest if no ID provided)
65
+ */
66
+ get(runId: string, checkpointId?: string): Promise<Checkpoint | null>;
67
+
68
+ /**
69
+ * List all checkpoints for a run
70
+ */
71
+ list(runId: string): Promise<CheckpointMetadata[]>;
72
+
73
+ /**
74
+ * Prune old checkpoints, keeping only the last N
75
+ */
76
+ prune(runId: string, keepLast: number): Promise<void>;
77
+ }
78
+
79
+ /**
80
+ * Create a checkpoint from current execution state
81
+ */
82
+ export function createCheckpoint(
83
+ runId: string,
84
+ state: CheckpointState
85
+ ): Checkpoint {
86
+ const id = `checkpoint-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
87
+
88
+ return {
89
+ id,
90
+ runId,
91
+ ts: Date.now(),
92
+ version: CHECKPOINT_VERSION,
93
+ state: {
94
+ ...state,
95
+ // Ensure executed is a plain array (not Set)
96
+ executed: Array.isArray(state.executed) ? state.executed : Array.from(state.executed),
97
+ // Deep clone to prevent mutation
98
+ outputs: JSON.parse(JSON.stringify(state.outputs)),
99
+ executionRecords: state.executionRecords
100
+ ? JSON.parse(JSON.stringify(state.executionRecords))
101
+ : undefined,
102
+ ledger: JSON.parse(JSON.stringify(state.ledger)),
103
+ },
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Validate checkpoint compatibility
109
+ */
110
+ export function validateCheckpoint(checkpoint: Checkpoint): boolean {
111
+ if (!checkpoint.version) {
112
+ console.warn('Checkpoint missing version field');
113
+ return false;
114
+ }
115
+
116
+ const [major] = checkpoint.version.split('.');
117
+ const [currentMajor] = CHECKPOINT_VERSION.split('.');
118
+
119
+ if (major !== currentMajor) {
120
+ console.error(
121
+ `Checkpoint version ${checkpoint.version} incompatible with current version ${CHECKPOINT_VERSION}`
122
+ );
123
+ return false;
124
+ }
125
+
126
+ // Validate required fields
127
+ if (!checkpoint.id || !checkpoint.runId || !checkpoint.state) {
128
+ console.error('Checkpoint missing required fields');
129
+ return false;
130
+ }
131
+
132
+ const { state } = checkpoint;
133
+ if (!state.goal || !state.context || !state.plan || !state.ledger) {
134
+ console.error('Checkpoint state missing required fields');
135
+ return false;
136
+ }
137
+
138
+ return true;
139
+ }
140
+
141
+ /**
142
+ * In-memory checkpoint store (for testing and simple use cases)
143
+ */
144
+ export class MemoryCheckpointStore implements CheckpointStore {
145
+ private checkpoints: Map<string, Checkpoint[]> = new Map();
146
+
147
+ async put(runId: string, checkpoint: Checkpoint): Promise<void> {
148
+ if (!this.checkpoints.has(runId)) {
149
+ this.checkpoints.set(runId, []);
150
+ }
151
+ this.checkpoints.get(runId)!.push(checkpoint);
152
+ }
153
+
154
+ async get(runId: string, checkpointId?: string): Promise<Checkpoint | null> {
155
+ const checkpoints = this.checkpoints.get(runId);
156
+ if (!checkpoints || checkpoints.length === 0) {
157
+ return null;
158
+ }
159
+
160
+ if (checkpointId) {
161
+ return checkpoints.find(c => c.id === checkpointId) || null;
162
+ }
163
+
164
+ // Return latest checkpoint
165
+ return checkpoints[checkpoints.length - 1];
166
+ }
167
+
168
+ async list(runId: string): Promise<CheckpointMetadata[]> {
169
+ const checkpoints = this.checkpoints.get(runId) || [];
170
+ return checkpoints.map(c => ({
171
+ id: c.id,
172
+ runId: c.runId,
173
+ ts: c.ts,
174
+ version: c.version,
175
+ tasksCompleted: c.state.executed.length,
176
+ }));
177
+ }
178
+
179
+ async prune(runId: string, keepLast: number): Promise<void> {
180
+ const checkpoints = this.checkpoints.get(runId);
181
+ if (!checkpoints || checkpoints.length <= keepLast) {
182
+ return;
183
+ }
184
+
185
+ // Sort by timestamp and keep only the last N
186
+ checkpoints.sort((a, b) => a.ts - b.ts);
187
+ this.checkpoints.set(runId, checkpoints.slice(-keepLast));
188
+ }
189
+
190
+ /**
191
+ * Clear all checkpoints (for testing)
192
+ */
193
+ clear(): void {
194
+ this.checkpoints.clear();
195
+ }
196
+ }
197
+
198
+ /**
199
+ * File-based checkpoint store
200
+ */
201
+ export class FileCheckpointStore implements CheckpointStore {
202
+ constructor(private basePath: string) {}
203
+
204
+ async put(runId: string, checkpoint: Checkpoint): Promise<void> {
205
+ const fs = await import('fs/promises');
206
+ const path = await import('path');
207
+
208
+ const dirPath = path.join(this.basePath, runId);
209
+ await fs.mkdir(dirPath, { recursive: true });
210
+
211
+ const filePath = path.join(dirPath, `${checkpoint.id}.json`);
212
+ await fs.writeFile(filePath, JSON.stringify(checkpoint, null, 2), 'utf-8');
213
+ }
214
+
215
+ async get(runId: string, checkpointId?: string): Promise<Checkpoint | null> {
216
+ const fs = await import('fs/promises');
217
+ const path = await import('path');
218
+
219
+ const dirPath = path.join(this.basePath, runId);
220
+
221
+ try {
222
+ if (checkpointId) {
223
+ const filePath = path.join(dirPath, `${checkpointId}.json`);
224
+ const content = await fs.readFile(filePath, 'utf-8');
225
+ return JSON.parse(content);
226
+ }
227
+
228
+ // Get latest checkpoint
229
+ const files = await fs.readdir(dirPath);
230
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
231
+
232
+ if (jsonFiles.length === 0) {
233
+ return null;
234
+ }
235
+
236
+ // Sort by timestamp in filename
237
+ jsonFiles.sort();
238
+ const latestFile = jsonFiles[jsonFiles.length - 1];
239
+ const filePath = path.join(dirPath, latestFile);
240
+ const content = await fs.readFile(filePath, 'utf-8');
241
+ return JSON.parse(content);
242
+ } catch (err: any) {
243
+ if (err.code === 'ENOENT') {
244
+ return null;
245
+ }
246
+ throw err;
247
+ }
248
+ }
249
+
250
+ async list(runId: string): Promise<CheckpointMetadata[]> {
251
+ const fs = await import('fs/promises');
252
+ const path = await import('path');
253
+
254
+ const dirPath = path.join(this.basePath, runId);
255
+
256
+ try {
257
+ const files = await fs.readdir(dirPath);
258
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
259
+
260
+ const metadata: CheckpointMetadata[] = [];
261
+
262
+ for (const file of jsonFiles) {
263
+ const filePath = path.join(dirPath, file);
264
+ const content = await fs.readFile(filePath, 'utf-8');
265
+ const checkpoint: Checkpoint = JSON.parse(content);
266
+
267
+ metadata.push({
268
+ id: checkpoint.id,
269
+ runId: checkpoint.runId,
270
+ ts: checkpoint.ts,
271
+ version: checkpoint.version,
272
+ tasksCompleted: checkpoint.state.executed.length,
273
+ });
274
+ }
275
+
276
+ return metadata.sort((a, b) => a.ts - b.ts);
277
+ } catch (err: any) {
278
+ if (err.code === 'ENOENT') {
279
+ return [];
280
+ }
281
+ throw err;
282
+ }
283
+ }
284
+
285
+ async prune(runId: string, keepLast: number): Promise<void> {
286
+ const fs = await import('fs/promises');
287
+ const path = await import('path');
288
+
289
+ const dirPath = path.join(this.basePath, runId);
290
+
291
+ try {
292
+ const metadata = await this.list(runId);
293
+
294
+ if (metadata.length <= keepLast) {
295
+ return;
296
+ }
297
+
298
+ // Delete oldest checkpoints
299
+ const toDelete = metadata.slice(0, metadata.length - keepLast);
300
+
301
+ for (const meta of toDelete) {
302
+ const filePath = path.join(dirPath, `${meta.id}.json`);
303
+ await fs.unlink(filePath);
304
+ }
305
+ } catch (err: any) {
306
+ if (err.code !== 'ENOENT') {
307
+ throw err;
308
+ }
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,108 @@
1
+ // ExecutionTranscript helper observes ledger append events and emits structured narrative updates
2
+ import type { LedgerEntry, StreamSink } from '@ddse/acm-sdk';
3
+ import type { MemoryLedger } from './ledger.js';
4
+ import type { TaskNarrative } from './executor.js';
5
+
6
+ export type ExecutionTranscriptEvent =
7
+ | {
8
+ type: 'nucleus-reasoning';
9
+ taskId?: string;
10
+ planId?: string;
11
+ reasoning: string;
12
+ }
13
+ | {
14
+ type: 'task-completed';
15
+ taskId: string;
16
+ output: any;
17
+ narrative?: TaskNarrative;
18
+ }
19
+ | {
20
+ type: 'goal-summary';
21
+ goalId: string;
22
+ planId: string;
23
+ summary?: string;
24
+ };
25
+
26
+ type ExecutionTranscriptHandler = (entry: LedgerEntry) => void;
27
+
28
+ type LedgerWithTranscript = MemoryLedger & {
29
+ __executionTranscriptHandlers?: ExecutionTranscriptHandler[];
30
+ };
31
+
32
+ export class ExecutionTranscript {
33
+ private stream?: StreamSink;
34
+ private onEvent?: (event: ExecutionTranscriptEvent) => void;
35
+
36
+ constructor(options: { stream?: StreamSink; onEvent?: (event: ExecutionTranscriptEvent) => void } = {}) {
37
+ this.stream = options.stream;
38
+ this.onEvent = options.onEvent;
39
+ }
40
+
41
+ attach(ledger: MemoryLedger): void {
42
+ const target = ledger as LedgerWithTranscript;
43
+
44
+ if (!target.__executionTranscriptHandlers) {
45
+ target.__executionTranscriptHandlers = [];
46
+ const originalAppend = ledger.append.bind(ledger);
47
+
48
+ ledger.append = (type: any, details: Record<string, any>, computeDigest = true) => {
49
+ const entry = originalAppend(type, details, computeDigest);
50
+ for (const handler of target.__executionTranscriptHandlers!) {
51
+ handler(entry);
52
+ }
53
+ return entry;
54
+ };
55
+ }
56
+
57
+ target.__executionTranscriptHandlers.push(entry => this.handleEntry(entry));
58
+ }
59
+
60
+ private handleEntry(entry: LedgerEntry): void {
61
+ const event = mapLedgerEntry(entry);
62
+ if (!event) {
63
+ return;
64
+ }
65
+
66
+ this.stream?.emit('transcript', event);
67
+ this.onEvent?.(event);
68
+ }
69
+ }
70
+
71
+ function mapLedgerEntry(entry: LedgerEntry): ExecutionTranscriptEvent | undefined {
72
+ switch (entry.type) {
73
+ case 'NUCLEUS_INFERENCE': {
74
+ const reasoning = typeof entry.details?.reasoning === 'string' ? entry.details.reasoning.trim() : '';
75
+ if (!reasoning) {
76
+ return undefined;
77
+ }
78
+ return {
79
+ type: 'nucleus-reasoning',
80
+ taskId: entry.details?.nucleus?.taskId,
81
+ planId: entry.details?.nucleus?.planId,
82
+ reasoning,
83
+ };
84
+ }
85
+ case 'TASK_END': {
86
+ const taskId = entry.details?.taskId;
87
+ if (!taskId) {
88
+ return undefined;
89
+ }
90
+ return {
91
+ type: 'task-completed',
92
+ taskId,
93
+ output: entry.details?.output,
94
+ narrative: entry.details?.narrative,
95
+ };
96
+ }
97
+ case 'GOAL_SUMMARY': {
98
+ return {
99
+ type: 'goal-summary',
100
+ goalId: entry.details?.goalId,
101
+ planId: entry.details?.planId,
102
+ summary: entry.details?.summary,
103
+ };
104
+ }
105
+ default:
106
+ return undefined;
107
+ }
108
+ }