@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.
- package/LICENSE +21 -0
- package/README.md +393 -0
- package/dist/src/checkpoint.d.ts +97 -0
- package/dist/src/checkpoint.d.ts.map +1 -0
- package/dist/src/checkpoint.js +200 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/execution-transcript.d.ts +30 -0
- package/dist/src/execution-transcript.d.ts.map +1 -0
- package/dist/src/execution-transcript.js +70 -0
- package/dist/src/execution-transcript.js.map +1 -0
- package/dist/src/executor.d.ts +49 -0
- package/dist/src/executor.d.ts.map +1 -0
- package/dist/src/executor.js +390 -0
- package/dist/src/executor.js.map +1 -0
- package/dist/src/guards.d.ts +7 -0
- package/dist/src/guards.d.ts.map +1 -0
- package/dist/src/guards.js +13 -0
- package/dist/src/guards.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/ledger.d.ts +12 -0
- package/dist/src/ledger.d.ts.map +1 -0
- package/dist/src/ledger.js +53 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/resumable-executor.d.ts +39 -0
- package/dist/src/resumable-executor.d.ts.map +1 -0
- package/dist/src/resumable-executor.js +354 -0
- package/dist/src/resumable-executor.js.map +1 -0
- package/dist/src/retry.d.ts +7 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/retry.js +25 -0
- package/dist/src/retry.js.map +1 -0
- package/dist/src/tool-envelope.d.ts +14 -0
- package/dist/src/tool-envelope.d.ts.map +1 -0
- package/dist/src/tool-envelope.js +84 -0
- package/dist/src/tool-envelope.js.map +1 -0
- package/dist/tests/resumable.test.d.ts +2 -0
- package/dist/tests/resumable.test.d.ts.map +1 -0
- package/dist/tests/resumable.test.js +337 -0
- package/dist/tests/resumable.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +29 -0
- package/src/checkpoint.ts +311 -0
- package/src/execution-transcript.ts +108 -0
- package/src/executor.ts +540 -0
- package/src/guards.ts +21 -0
- package/src/index.ts +9 -0
- package/src/ledger.ts +63 -0
- package/src/resumable-executor.ts +471 -0
- package/src/retry.ts +37 -0
- package/src/tool-envelope.ts +113 -0
- package/tests/resumable.test.ts +421 -0
- package/tsconfig.json +11 -0
package/src/executor.ts
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// Main execution engine
|
|
2
|
+
import {
|
|
3
|
+
InternalContextScopeImpl,
|
|
4
|
+
type CapabilityRegistry,
|
|
5
|
+
type Context,
|
|
6
|
+
type Goal,
|
|
7
|
+
type LedgerEntry,
|
|
8
|
+
ExternalContextProviderAdapter,
|
|
9
|
+
type NucleusConfig,
|
|
10
|
+
type NucleusFactory,
|
|
11
|
+
type PostcheckResult,
|
|
12
|
+
type Plan,
|
|
13
|
+
type PolicyEngine,
|
|
14
|
+
type RunContext,
|
|
15
|
+
type StreamSink,
|
|
16
|
+
type ToolRegistry,
|
|
17
|
+
} from '@ddse/acm-sdk';
|
|
18
|
+
import { evaluateGuard } from './guards.js';
|
|
19
|
+
import { MemoryLedger } from './ledger.js';
|
|
20
|
+
import { withRetry } from './retry.js';
|
|
21
|
+
import { createInstrumentedToolGetter } from './tool-envelope.js';
|
|
22
|
+
|
|
23
|
+
export type TaskNarrative = {
|
|
24
|
+
reasoning?: string[];
|
|
25
|
+
postcheck?: {
|
|
26
|
+
status: PostcheckResult['status'];
|
|
27
|
+
reason?: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TaskExecutionRecord = {
|
|
32
|
+
output: any;
|
|
33
|
+
narrative?: TaskNarrative;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ExecutePlanOptions = {
|
|
37
|
+
goal: Goal;
|
|
38
|
+
context: Context;
|
|
39
|
+
plan: Plan;
|
|
40
|
+
capabilityRegistry: CapabilityRegistry;
|
|
41
|
+
toolRegistry: ToolRegistry;
|
|
42
|
+
policy?: PolicyEngine;
|
|
43
|
+
verify?: (taskId: string, output: any, expressions: string[]) => Promise<boolean>;
|
|
44
|
+
stream?: StreamSink;
|
|
45
|
+
ledger?: MemoryLedger;
|
|
46
|
+
nucleusFactory: NucleusFactory;
|
|
47
|
+
nucleusConfig: {
|
|
48
|
+
llmCall: NucleusConfig['llmCall'];
|
|
49
|
+
hooks?: NucleusConfig['hooks'];
|
|
50
|
+
allowedTools?: string[];
|
|
51
|
+
};
|
|
52
|
+
contextProvider?: ExternalContextProviderAdapter;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ExecutePlanResult = {
|
|
56
|
+
outputsByTask: Record<string, TaskExecutionRecord>;
|
|
57
|
+
ledger: readonly LedgerEntry[];
|
|
58
|
+
goalSummary?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export async function executePlan(options: ExecutePlanOptions): Promise<ExecutePlanResult> {
|
|
62
|
+
const {
|
|
63
|
+
goal,
|
|
64
|
+
context,
|
|
65
|
+
plan,
|
|
66
|
+
capabilityRegistry,
|
|
67
|
+
toolRegistry,
|
|
68
|
+
policy,
|
|
69
|
+
verify,
|
|
70
|
+
stream,
|
|
71
|
+
nucleusFactory,
|
|
72
|
+
nucleusConfig,
|
|
73
|
+
contextProvider,
|
|
74
|
+
} = options;
|
|
75
|
+
|
|
76
|
+
const ledger = options.ledger ?? new MemoryLedger();
|
|
77
|
+
const outputs: Record<string, any> = {};
|
|
78
|
+
const executionRecords: Record<string, TaskExecutionRecord> = {};
|
|
79
|
+
const policyContext: Record<string, any> = {};
|
|
80
|
+
const metrics = { costUsd: 0, elapsedSec: 0 };
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
|
|
83
|
+
// Log plan selection
|
|
84
|
+
ledger.append('PLAN_SELECTED', {
|
|
85
|
+
planId: plan.id,
|
|
86
|
+
contextRef: plan.contextRef,
|
|
87
|
+
capabilityMapVersion: plan.capabilityMapVersion,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Build execution order based on edges
|
|
91
|
+
const executed = new Set<string>();
|
|
92
|
+
const pending = [...plan.tasks];
|
|
93
|
+
|
|
94
|
+
while (pending.length > 0) {
|
|
95
|
+
const readyTasks = pending.filter(taskSpec => {
|
|
96
|
+
// Check if all dependencies are satisfied
|
|
97
|
+
const incomingEdges = plan.edges.filter(e => e.to === taskSpec.id);
|
|
98
|
+
if (incomingEdges.length === 0) {
|
|
99
|
+
return true; // No dependencies
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return incomingEdges.every(edge => {
|
|
103
|
+
if (!executed.has(edge.from)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Evaluate guard if present
|
|
108
|
+
if (edge.guard) {
|
|
109
|
+
const guardResult = evaluateGuard(edge.guard, {
|
|
110
|
+
context,
|
|
111
|
+
outputs,
|
|
112
|
+
policy: policyContext,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ledger.append('GUARD_EVAL', {
|
|
116
|
+
edge: `${edge.from}->${edge.to}`,
|
|
117
|
+
guard: edge.guard,
|
|
118
|
+
result: guardResult,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return guardResult;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return true;
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (readyTasks.length === 0) {
|
|
129
|
+
break; // No more tasks can be executed
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Execute ready tasks
|
|
133
|
+
for (const taskSpec of readyTasks) {
|
|
134
|
+
pending.splice(pending.indexOf(taskSpec), 1);
|
|
135
|
+
|
|
136
|
+
const capabilityName = taskSpec.capabilityRef || taskSpec.capability;
|
|
137
|
+
if (!capabilityName) {
|
|
138
|
+
throw new Error(`Task ${taskSpec.id} missing capability reference`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const task = capabilityRegistry.resolve(capabilityName);
|
|
142
|
+
if (!task) {
|
|
143
|
+
throw new Error(`Task not found for capability: ${capabilityName}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const nucleusAllowedTools = new Set<string>(nucleusConfig.allowedTools ?? []);
|
|
147
|
+
for (const tool of taskSpec.tools ?? []) {
|
|
148
|
+
nucleusAllowedTools.add(tool.name);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const nucleus = nucleusFactory({
|
|
152
|
+
goalId: goal.id,
|
|
153
|
+
goalIntent: goal.intent,
|
|
154
|
+
planId: plan.id,
|
|
155
|
+
taskId: taskSpec.id,
|
|
156
|
+
contextRef: plan.contextRef,
|
|
157
|
+
context,
|
|
158
|
+
llmCall: nucleusConfig.llmCall,
|
|
159
|
+
hooks: nucleusConfig.hooks,
|
|
160
|
+
allowedTools: Array.from(nucleusAllowedTools),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const internalScope = new InternalContextScopeImpl(entry => {
|
|
164
|
+
ledger.append(entry.type, entry.details);
|
|
165
|
+
});
|
|
166
|
+
nucleus.setInternalContext(internalScope);
|
|
167
|
+
|
|
168
|
+
const getTool = createInstrumentedToolGetter({
|
|
169
|
+
taskId: taskSpec.id,
|
|
170
|
+
capability: capabilityName,
|
|
171
|
+
toolRegistry,
|
|
172
|
+
ledger,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Build run context
|
|
176
|
+
const runContext: RunContext = {
|
|
177
|
+
goal,
|
|
178
|
+
context,
|
|
179
|
+
outputs,
|
|
180
|
+
metrics,
|
|
181
|
+
getTool,
|
|
182
|
+
getCapabilityRegistry: () => capabilityRegistry,
|
|
183
|
+
stream,
|
|
184
|
+
nucleus,
|
|
185
|
+
internalContext: internalScope,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
let preflight = await nucleus.preflight();
|
|
189
|
+
if (preflight.status === 'NEEDS_CONTEXT') {
|
|
190
|
+
const requestedDirectives = preflight.retrievalDirectives;
|
|
191
|
+
|
|
192
|
+
ledger.append('CONTEXT_INTERNALIZED', {
|
|
193
|
+
taskId: taskSpec.id,
|
|
194
|
+
directives: requestedDirectives,
|
|
195
|
+
status: contextProvider ? 'requested' : 'unhandled',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!contextProvider) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Task ${taskSpec.id} requires additional context retrieval: ${preflight.retrievalDirectives.join(', ')}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await contextProvider.fulfill({
|
|
205
|
+
directives: requestedDirectives,
|
|
206
|
+
scope: internalScope,
|
|
207
|
+
runContext,
|
|
208
|
+
nucleus,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
preflight = await nucleus.preflight();
|
|
212
|
+
if (preflight.status === 'NEEDS_CONTEXT') {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Task ${taskSpec.id} still requires additional context after adapter execution: ${preflight.retrievalDirectives.join(', ')}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
ledger.append('CONTEXT_INTERNALIZED', {
|
|
219
|
+
taskId: taskSpec.id,
|
|
220
|
+
directives: requestedDirectives,
|
|
221
|
+
status: 'resolved',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Policy pre-check
|
|
226
|
+
if (policy) {
|
|
227
|
+
const policyInput = task.policyInput?.(runContext, taskSpec.input) ?? {};
|
|
228
|
+
const decision = await policy.evaluate('task.pre', {
|
|
229
|
+
taskId: taskSpec.id,
|
|
230
|
+
capability: taskSpec.capability,
|
|
231
|
+
...policyInput,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
ledger.append('POLICY_PRE', {
|
|
235
|
+
taskId: taskSpec.id,
|
|
236
|
+
decision,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!decision.allow) {
|
|
240
|
+
throw new Error(`Policy denied task ${taskSpec.id}: ${decision.reason}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
policyContext[taskSpec.id] = decision;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Execute task with retry
|
|
247
|
+
const ledgerBaseline = ledger.getEntries().length;
|
|
248
|
+
|
|
249
|
+
ledger.append('TASK_START', {
|
|
250
|
+
taskId: taskSpec.id,
|
|
251
|
+
capability: capabilityName,
|
|
252
|
+
input: taskSpec.input,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
stream?.emit('task', { taskId: taskSpec.id, status: 'running' });
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const executeTask = async () => task.execute(runContext, taskSpec.input);
|
|
259
|
+
const retryConfig = taskSpec.retry || (taskSpec.retryPolicy
|
|
260
|
+
? {
|
|
261
|
+
attempts: taskSpec.retryPolicy.maxAttempts || 3,
|
|
262
|
+
backoff: 'exp' as const,
|
|
263
|
+
}
|
|
264
|
+
: undefined);
|
|
265
|
+
|
|
266
|
+
const output = retryConfig ? await withRetry(executeTask, retryConfig) : await executeTask();
|
|
267
|
+
outputs[taskSpec.id] = output;
|
|
268
|
+
|
|
269
|
+
// Policy post-check
|
|
270
|
+
if (policy) {
|
|
271
|
+
const decision = await policy.evaluate('task.post', {
|
|
272
|
+
taskId: taskSpec.id,
|
|
273
|
+
output,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
ledger.append('POLICY_POST', {
|
|
277
|
+
taskId: taskSpec.id,
|
|
278
|
+
decision,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Verification
|
|
283
|
+
if (verify && taskSpec.verification && taskSpec.verification.length > 0) {
|
|
284
|
+
const verified = await verify(taskSpec.id, output, taskSpec.verification);
|
|
285
|
+
|
|
286
|
+
ledger.append('VERIFICATION', {
|
|
287
|
+
taskId: taskSpec.id,
|
|
288
|
+
expressions: taskSpec.verification,
|
|
289
|
+
result: verified,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!verified) {
|
|
293
|
+
throw new Error(`Verification failed for task ${taskSpec.id}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const postcheck = await nucleus.postcheck(output);
|
|
298
|
+
if (postcheck.status === 'NEEDS_COMPENSATION') {
|
|
299
|
+
ledger.append('ERROR', {
|
|
300
|
+
taskId: taskSpec.id,
|
|
301
|
+
stage: 'NUCLEUS_POSTCHECK',
|
|
302
|
+
message: postcheck.reason,
|
|
303
|
+
});
|
|
304
|
+
throw new Error(`Task ${taskSpec.id} requires compensation: ${postcheck.reason}`);
|
|
305
|
+
}
|
|
306
|
+
if (postcheck.status === 'ESCALATE') {
|
|
307
|
+
ledger.append('ERROR', {
|
|
308
|
+
taskId: taskSpec.id,
|
|
309
|
+
stage: 'NUCLEUS_POSTCHECK',
|
|
310
|
+
message: postcheck.reason,
|
|
311
|
+
});
|
|
312
|
+
throw new Error(`Task ${taskSpec.id} escalated: ${postcheck.reason}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const narrative = buildTaskNarrative(ledger, ledgerBaseline, taskSpec.id, postcheck);
|
|
316
|
+
executionRecords[taskSpec.id] = {
|
|
317
|
+
output,
|
|
318
|
+
narrative,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
ledger.append('TASK_END', {
|
|
322
|
+
taskId: taskSpec.id,
|
|
323
|
+
output,
|
|
324
|
+
narrative,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
stream?.emit('task', { taskId: taskSpec.id, status: 'completed', output, narrative });
|
|
328
|
+
|
|
329
|
+
executed.add(taskSpec.id);
|
|
330
|
+
} catch (error: any) {
|
|
331
|
+
ledger.append('ERROR', {
|
|
332
|
+
taskId: taskSpec.id,
|
|
333
|
+
capability: capabilityName,
|
|
334
|
+
message: error.message || 'Unknown error',
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
stream?.emit('task', {
|
|
338
|
+
taskId: taskSpec.id,
|
|
339
|
+
status: 'failed',
|
|
340
|
+
error: error.message || error.toString(),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const goalSummary = await synthesizeGoalSummary({
|
|
349
|
+
goal,
|
|
350
|
+
plan,
|
|
351
|
+
executionRecords,
|
|
352
|
+
context,
|
|
353
|
+
nucleusFactory,
|
|
354
|
+
nucleusConfig,
|
|
355
|
+
ledger,
|
|
356
|
+
stream,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
metrics.elapsedSec = (Date.now() - startTime) / 1000;
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
outputsByTask: executionRecords,
|
|
363
|
+
ledger: ledger.getEntries(),
|
|
364
|
+
goalSummary,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function buildTaskNarrative(
|
|
369
|
+
ledger: MemoryLedger,
|
|
370
|
+
baselineIndex: number,
|
|
371
|
+
taskId: string,
|
|
372
|
+
postcheck: PostcheckResult
|
|
373
|
+
): TaskNarrative | undefined {
|
|
374
|
+
const entries = ledger.getEntries().slice(baselineIndex);
|
|
375
|
+
const reasonings = entries
|
|
376
|
+
.filter(entry => entry.type === 'NUCLEUS_INFERENCE')
|
|
377
|
+
.filter(entry => entry.details?.nucleus?.taskId === taskId)
|
|
378
|
+
.map(entry => (typeof entry.details?.reasoning === 'string' ? entry.details.reasoning.trim() : undefined))
|
|
379
|
+
.filter((text): text is string => Boolean(text && text.length > 0));
|
|
380
|
+
|
|
381
|
+
const narrative: TaskNarrative = {};
|
|
382
|
+
if (reasonings.length > 0) {
|
|
383
|
+
narrative.reasoning = reasonings;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
narrative.postcheck = {
|
|
387
|
+
status: postcheck.status,
|
|
388
|
+
...(postcheck.status !== 'COMPLETE' && 'reason' in postcheck
|
|
389
|
+
? { reason: (postcheck as Extract<PostcheckResult, { reason: string }>).reason }
|
|
390
|
+
: {}),
|
|
391
|
+
} as TaskNarrative['postcheck'];
|
|
392
|
+
|
|
393
|
+
if (!narrative.reasoning && !narrative.postcheck?.reason && narrative.postcheck?.status === 'COMPLETE') {
|
|
394
|
+
// Return undefined if narrative is completely empty beyond a routine COMPLETE status
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return narrative;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function synthesizeGoalSummary(options: {
|
|
402
|
+
goal: Goal;
|
|
403
|
+
plan: Plan;
|
|
404
|
+
executionRecords: Record<string, TaskExecutionRecord>;
|
|
405
|
+
context: Context;
|
|
406
|
+
nucleusFactory: NucleusFactory;
|
|
407
|
+
nucleusConfig: ExecutePlanOptions['nucleusConfig'];
|
|
408
|
+
ledger: MemoryLedger;
|
|
409
|
+
stream?: StreamSink;
|
|
410
|
+
}): Promise<string | undefined> {
|
|
411
|
+
const { goal, plan, executionRecords, context, nucleusFactory, nucleusConfig, ledger, stream } = options;
|
|
412
|
+
|
|
413
|
+
if (plan.tasks.length === 0 || Object.keys(executionRecords).length === 0) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const allowedTools = Array.from(new Set(nucleusConfig.allowedTools ?? []));
|
|
419
|
+
const nucleus = nucleusFactory({
|
|
420
|
+
goalId: goal.id,
|
|
421
|
+
goalIntent: goal.intent,
|
|
422
|
+
planId: plan.id,
|
|
423
|
+
taskId: 'goal-summary',
|
|
424
|
+
contextRef: plan.contextRef,
|
|
425
|
+
context,
|
|
426
|
+
llmCall: nucleusConfig.llmCall,
|
|
427
|
+
hooks: nucleusConfig.hooks,
|
|
428
|
+
allowedTools,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const prompt = buildGoalSummaryPrompt(goal, plan, executionRecords, context);
|
|
432
|
+
const result = await nucleus.invoke({ prompt, tools: [] });
|
|
433
|
+
const summary = result.reasoning?.trim() ?? '';
|
|
434
|
+
const normalizedSummary = summary.length > 0 ? summary : undefined;
|
|
435
|
+
|
|
436
|
+
ledger.append('GOAL_SUMMARY', {
|
|
437
|
+
goalId: goal.id,
|
|
438
|
+
planId: plan.id,
|
|
439
|
+
summary: normalizedSummary,
|
|
440
|
+
tasks: plan.tasks.map(task => {
|
|
441
|
+
const record = executionRecords[task.id];
|
|
442
|
+
return {
|
|
443
|
+
id: task.id,
|
|
444
|
+
title: task.title,
|
|
445
|
+
objective: task.objective,
|
|
446
|
+
successCriteria: task.successCriteria,
|
|
447
|
+
outputPreview: record ? previewForSummary(record.output) : undefined,
|
|
448
|
+
postcheck: record?.narrative?.postcheck,
|
|
449
|
+
};
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (normalizedSummary) {
|
|
454
|
+
stream?.emit('summary', {
|
|
455
|
+
goalId: goal.id,
|
|
456
|
+
planId: plan.id,
|
|
457
|
+
summary: normalizedSummary,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return normalizedSummary;
|
|
462
|
+
} catch (error: any) {
|
|
463
|
+
ledger.append('ERROR', {
|
|
464
|
+
stage: 'GOAL_SUMMARY',
|
|
465
|
+
message: error?.message ?? 'Failed to synthesize goal summary',
|
|
466
|
+
});
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function buildGoalSummaryPrompt(
|
|
472
|
+
goal: Goal,
|
|
473
|
+
plan: Plan,
|
|
474
|
+
executionRecords: Record<string, TaskExecutionRecord>,
|
|
475
|
+
context: Context
|
|
476
|
+
): string {
|
|
477
|
+
const goalIntent = goal.intent ?? goal.id;
|
|
478
|
+
const contextFacts = context?.facts ? JSON.stringify(context.facts, null, 2) : '{}';
|
|
479
|
+
const contextAssumptions = context?.assumptions?.length
|
|
480
|
+
? `Assumptions:\n- ${context.assumptions.join('\n- ')}`
|
|
481
|
+
: 'Assumptions: none provided';
|
|
482
|
+
|
|
483
|
+
const taskSections = plan.tasks.map(task => {
|
|
484
|
+
const record = executionRecords[task.id];
|
|
485
|
+
const label = task.title || task.capabilityRef || task.capability || task.id;
|
|
486
|
+
const pieces = [
|
|
487
|
+
`Task ${task.id}: ${label}`,
|
|
488
|
+
task.objective ? `Objective: ${task.objective}` : undefined,
|
|
489
|
+
task.successCriteria && task.successCriteria.length > 0
|
|
490
|
+
? `Success Criteria: ${task.successCriteria.join('; ')}`
|
|
491
|
+
: undefined,
|
|
492
|
+
record ? `Outcome: ${previewForSummary(record.output)}` : 'Outcome: not captured.',
|
|
493
|
+
record?.narrative?.reasoning?.length
|
|
494
|
+
? `Narrative: ${record.narrative.reasoning.join(' ')}`
|
|
495
|
+
: undefined,
|
|
496
|
+
record?.narrative?.postcheck
|
|
497
|
+
? `Postcheck: ${record.narrative.postcheck.status}${record.narrative.postcheck.reason ? ` (${record.narrative.postcheck.reason})` : ''}`
|
|
498
|
+
: undefined,
|
|
499
|
+
].filter(Boolean);
|
|
500
|
+
|
|
501
|
+
return pieces.join('\n');
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return `You are composing the wrap-up for an ACM execution run.
|
|
505
|
+
Goal intent: ${goalIntent}
|
|
506
|
+
Context reference: ${plan.contextRef}
|
|
507
|
+
|
|
508
|
+
Context facts:
|
|
509
|
+
${contextFacts}
|
|
510
|
+
${contextAssumptions}
|
|
511
|
+
|
|
512
|
+
Summarize the outcome in 2-3 sentences for the operator. Highlight what happened, any remaining risks or follow-up, and reference task achievements when relevant.
|
|
513
|
+
|
|
514
|
+
Task outcomes:
|
|
515
|
+
${taskSections.join('\n\n')}`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function previewForSummary(value: any, maxLength = 240): string {
|
|
519
|
+
if (value === null || value === undefined) {
|
|
520
|
+
return 'No output provided.';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let text: string;
|
|
524
|
+
if (typeof value === 'string') {
|
|
525
|
+
text = value.trim();
|
|
526
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
527
|
+
text = String(value);
|
|
528
|
+
} else {
|
|
529
|
+
try {
|
|
530
|
+
text = JSON.stringify(value);
|
|
531
|
+
} catch {
|
|
532
|
+
text = String(value);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (text.length <= maxLength) {
|
|
537
|
+
return text;
|
|
538
|
+
}
|
|
539
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
540
|
+
}
|
package/src/guards.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Guard evaluator
|
|
2
|
+
import type { GuardExpr, Context } from '@ddse/acm-sdk';
|
|
3
|
+
|
|
4
|
+
export function evaluateGuard(
|
|
5
|
+
expr: GuardExpr,
|
|
6
|
+
context: {
|
|
7
|
+
context: Context;
|
|
8
|
+
outputs: Record<string, any>;
|
|
9
|
+
policy: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
): boolean {
|
|
12
|
+
try {
|
|
13
|
+
// Simple expression evaluation
|
|
14
|
+
// Support basic comparisons and logical operators
|
|
15
|
+
const func = new Function('context', 'outputs', 'policy', `return ${expr};`);
|
|
16
|
+
return func(context.context, context.outputs, context.policy);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error(`Guard evaluation failed: ${expr}`, err);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Runtime exports
|
|
2
|
+
export * from './executor.js';
|
|
3
|
+
export * from './guards.js';
|
|
4
|
+
export * from './ledger.js';
|
|
5
|
+
export * from './retry.js';
|
|
6
|
+
export * from './checkpoint.js';
|
|
7
|
+
export * from './resumable-executor.js';
|
|
8
|
+
export * from './tool-envelope.js';
|
|
9
|
+
export * from './execution-transcript.js';
|
package/src/ledger.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Memory ledger implementation
|
|
2
|
+
import type { LedgerEntry } from '@ddse/acm-sdk';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
export class MemoryLedger {
|
|
6
|
+
private entries: LedgerEntry[] = [];
|
|
7
|
+
private nextId = 1;
|
|
8
|
+
|
|
9
|
+
append(type: LedgerEntry['type'], details: Record<string, any>, computeDigest = true): LedgerEntry {
|
|
10
|
+
const entry: LedgerEntry = {
|
|
11
|
+
id: `ledger-${this.nextId++}`,
|
|
12
|
+
ts: Date.now(),
|
|
13
|
+
type,
|
|
14
|
+
details,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Compute digest for tamper detection if requested
|
|
18
|
+
if (computeDigest) {
|
|
19
|
+
entry.digest = this.computeDigest(entry);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.entries.push(entry);
|
|
23
|
+
return entry;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getEntries(): readonly LedgerEntry[] {
|
|
27
|
+
return [...this.entries];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getEntriesByType(type: LedgerEntry['type']): readonly LedgerEntry[] {
|
|
31
|
+
return this.entries.filter(e => e.type === type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clear(): void {
|
|
35
|
+
this.entries = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate ledger integrity
|
|
39
|
+
validate(): boolean {
|
|
40
|
+
for (const entry of this.entries) {
|
|
41
|
+
if (entry.digest) {
|
|
42
|
+
const computed = this.computeDigest(entry);
|
|
43
|
+
if (computed !== entry.digest) {
|
|
44
|
+
console.error(`Ledger entry ${entry.id} failed integrity check`);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private computeDigest(entry: LedgerEntry): string {
|
|
53
|
+
const normalized = JSON.stringify({
|
|
54
|
+
id: entry.id,
|
|
55
|
+
ts: entry.ts,
|
|
56
|
+
type: entry.type,
|
|
57
|
+
details: entry.details,
|
|
58
|
+
});
|
|
59
|
+
const hash = createHash('sha256');
|
|
60
|
+
hash.update(normalized);
|
|
61
|
+
return hash.digest('hex').substring(0, 32);
|
|
62
|
+
}
|
|
63
|
+
}
|