@principles/core 1.83.0 → 1.84.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 (25) hide show
  1. package/dist/runtime-v2/index.d.ts +2 -0
  2. package/dist/runtime-v2/index.d.ts.map +1 -1
  3. package/dist/runtime-v2/index.js +2 -0
  4. package/dist/runtime-v2/index.js.map +1 -1
  5. package/dist/runtime-v2/internalization/dreamer-output.d.ts +4 -3
  6. package/dist/runtime-v2/internalization/dreamer-output.d.ts.map +1 -1
  7. package/dist/runtime-v2/internalization/dreamer-output.js +11 -9
  8. package/dist/runtime-v2/internalization/dreamer-output.js.map +1 -1
  9. package/dist/runtime-v2/internalization/dreamer-runner.d.ts +31 -92
  10. package/dist/runtime-v2/internalization/dreamer-runner.d.ts.map +1 -1
  11. package/dist/runtime-v2/internalization/dreamer-runner.js +79 -495
  12. package/dist/runtime-v2/internalization/dreamer-runner.js.map +1 -1
  13. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.d.ts +2 -0
  14. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.d.ts.map +1 -0
  15. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +238 -0
  16. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -0
  17. package/dist/runtime-v2/runner/base-peer-runner.d.ts +122 -0
  18. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -0
  19. package/dist/runtime-v2/runner/base-peer-runner.js +516 -0
  20. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -0
  21. package/dist/runtime-v2/runner/peer-runner-types.d.ts +107 -0
  22. package/dist/runtime-v2/runner/peer-runner-types.d.ts.map +1 -0
  23. package/dist/runtime-v2/runner/peer-runner-types.js +13 -0
  24. package/dist/runtime-v2/runner/peer-runner-types.js.map +1 -0
  25. package/package.json +1 -1
@@ -1,15 +1,15 @@
1
1
  import { PDRuntimeError } from '../error-categories.js';
2
2
  import { hydratePITaskRecord } from './pitask-metadata.js';
3
- import { RunnerPhase } from '../runner/runner-phase.js';
4
3
  import { DreamerPromptBuilder } from './dreamer-prompt-builder.js';
5
4
  import { injectRunnerLineageIfAbsent } from './peer-runner-contracts.js';
6
- const DEFAULT_DREAMER_RUNNER_OPTIONS = {
5
+ import { BasePeerRunner } from '../runner/base-peer-runner.js';
6
+ export const DEFAULT_DREAMER_RUNNER_OPTIONS = {
7
7
  pollIntervalMs: 5_000,
8
8
  timeoutMs: 300_000,
9
9
  defaultMaxAttempts: 3,
10
10
  agentId: 'dreamer',
11
11
  };
12
- function resolveDreamerRunnerOptions(options) {
12
+ export function resolveDreamerRunnerOptions(options) {
13
13
  return {
14
14
  pollIntervalMs: options.pollIntervalMs ?? DEFAULT_DREAMER_RUNNER_OPTIONS.pollIntervalMs,
15
15
  timeoutMs: options.timeoutMs ?? DEFAULT_DREAMER_RUNNER_OPTIONS.timeoutMs,
@@ -19,151 +19,23 @@ function resolveDreamerRunnerOptions(options) {
19
19
  agentId: options.agentId ?? DEFAULT_DREAMER_RUNNER_OPTIONS.agentId,
20
20
  };
21
21
  }
22
- // ── DreamerRunner ─────────────────────────────────────────────────────────────
23
- export class DreamerRunner {
24
- phase = RunnerPhase.Idle;
25
- resolvedOptions;
26
- stateManager;
27
- runtimeAdapter;
28
- eventEmitter;
22
+ // ── DreamerRunner ────────────────────────────────────────────────────────────
23
+ export class DreamerRunner extends BasePeerRunner {
29
24
  validator;
30
- artifactStore;
31
25
  constructor(deps, options) {
32
- this.stateManager = deps.stateManager;
33
- this.runtimeAdapter = deps.runtimeAdapter;
34
- this.eventEmitter = deps.eventEmitter;
35
- this.validator = deps.validator;
36
- this.artifactStore = deps.artifactStore;
37
- this.resolvedOptions = resolveDreamerRunnerOptions(options);
38
- }
39
- /** Get the current internal phase. For testing/observability only. */
40
- get currentPhase() {
41
- return this.phase;
42
- }
43
- emitDreamerEvent(eventType, taskId, payload) {
44
- this.eventEmitter.emitTelemetry({
45
- eventType: eventType,
46
- traceId: taskId,
47
- timestamp: new Date().toISOString(),
48
- sessionId: this.resolvedOptions.owner,
49
- agentId: this.resolvedOptions.agentId,
50
- payload,
26
+ super(deps, options, {
27
+ runnerName: 'dreamer',
28
+ expectedTaskKind: 'dreamer',
29
+ defaultAgentId: 'dreamer',
30
+ resultRefPrefix: 'dreamer',
51
31
  });
32
+ this.validator = deps.validator;
52
33
  }
53
- /**
54
- * Execute the full Dreamer lifecycle for a task.
55
- *
56
- * The runner does NOT hold mutable state between run() calls.
57
- * Each invocation is independent.
58
- */
59
- async run(taskId) {
60
- this.phase = RunnerPhase.Idle;
61
- // 1. Acquire lease — isolated try/catch so lease_conflict never uses synthetic TaskRecord
62
- let leasedTask;
63
- try {
64
- leasedTask = await this.stateManager.acquireLease({
65
- taskId,
66
- owner: this.resolvedOptions.owner,
67
- runtimeKind: this.resolvedOptions.runtimeKind,
68
- });
69
- }
70
- catch (error) {
71
- return await this.handleLeaseOrPhaseError(taskId, error);
72
- }
73
- this.emitDreamerEvent('dreamer_task_leased', taskId, {
74
- taskKind: 'dreamer',
75
- attemptCount: leasedTask.attemptCount,
76
- });
77
- if (leasedTask.taskKind !== 'dreamer') {
78
- this.emitDreamerEvent('dreamer_wrong_task_kind', taskId, {
79
- expectedKind: 'dreamer',
80
- actualKind: leasedTask.taskKind,
81
- });
82
- await this.stateManager.markTaskFailed(taskId, 'input_invalid');
83
- return {
84
- status: 'failed',
85
- taskId,
86
- errorCategory: 'input_invalid',
87
- failureReason: `Task kind must be 'dreamer', got '${leasedTask.taskKind}'`,
88
- attemptCount: leasedTask.attemptCount,
89
- };
90
- }
91
- // All subsequent errors use the real leasedTask — no synthetic TaskRecord allowed
92
- try {
93
- // acquireLease creates a RunRecord; resolve its runId for store operations
94
- const storeRunId = await this.resolveStoreRunId(taskId);
95
- // 2. Build context from predecessor task outputs (no ContextAssembler)
96
- this.phase = RunnerPhase.BuildingContext;
97
- const { contextHash, contextRefs, predecessorOutput } = await this.buildContext(taskId);
98
- this.emitDreamerEvent('dreamer_context_built', taskId, { contextHash });
99
- // 3. Invoke runtime
100
- this.phase = RunnerPhase.Invoking;
101
- const runHandle = await this.invokeRuntime({ taskId, contextHash, contextRefs, predecessorOutput });
102
- this.emitDreamerEvent('dreamer_run_started', taskId, {
103
- runtimeKind: this.resolvedOptions.runtimeKind,
104
- });
105
- // 4. Poll until terminal
106
- this.phase = RunnerPhase.Polling;
107
- const finalStatus = await this.pollUntilTerminal(runHandle);
108
- // 5. Handle non-success terminal states
109
- if (finalStatus.status !== 'succeeded') {
110
- return await this.handleRuntimeFailure(taskId, leasedTask, finalStatus);
111
- }
112
- // 6. Fetch output
113
- this.phase = RunnerPhase.FetchingOutput;
114
- const output = await this.fetchAndParseOutput(runHandle.runId, taskId);
115
- // Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
116
- // Only fill when absent via Object.hasOwn — present-but-falsy values
117
- // must reach validation and fail loud (Runtime Contract Rule 3).
118
- injectRunnerLineageIfAbsent(output, 'taskId', taskId);
119
- // 7. Validate
120
- this.phase = RunnerPhase.Validating;
121
- const validationResult = await this.validator.validate(output, taskId);
122
- if (!validationResult.valid) {
123
- return await this.handleValidationError({
124
- taskId,
125
- task: leasedTask,
126
- errors: validationResult.errors,
127
- errorCategory: validationResult.errorCategory,
128
- });
129
- }
130
- this.emitDreamerEvent('dreamer_output_validated', taskId, {
131
- candidateCount: output.candidates.length,
132
- });
133
- // 8. Succeed task
134
- return await this.succeedTask({
135
- taskId,
136
- runId: storeRunId,
137
- output,
138
- task: leasedTask,
139
- contextHash,
140
- });
141
- }
142
- catch (error) {
143
- // handleLeaseOrPhaseError is only for lease errors (before lease).
144
- // Post-lease errors use retryOrFail with the real leasedTask.
145
- return await this.handlePostLeaseError(taskId, leasedTask, error);
146
- }
34
+ // ── Abstract implementations ───────────────────────────────────────────────
35
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this
36
+ get permanentErrorCategories() {
37
+ return new Set(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid']);
147
38
  }
148
- // ── Phase methods ─────────────────────────────────────────────────────────
149
- /**
150
- * Build context from predecessor task outputs.
151
- *
152
- * Dreamer doesn't use ContextAssembler — it resolves predecessor context
153
- * from dependencyTaskIds via stateManager.getTask(). The predecessor's
154
- * resultRef/outputArtifactRefs become the Dreamer's input context.
155
- *
156
- * Degradation semantics (intentional partial failure):
157
- * - If ALL dependencies fail → builds empty context (continues, not fail-closed)
158
- * - If SOME dependencies fail → builds partial context + emits dreamer_context_partial
159
- * - Only fulfilled results contribute contextRefs
160
- *
161
- * This is a deliberate design choice: Dreamer can produce output with partial
162
- * context, whereas the host layer's task-ready validation is fail-closed.
163
- * Partial context is reported via telemetry.
164
- *
165
- * @see hydratePITaskRecord (PRI-65) for fail-closed PI metadata access
166
- */
167
39
  async buildContext(taskId) {
168
40
  const task = await this.stateManager.getTask(taskId);
169
41
  if (!task) {
@@ -207,434 +79,146 @@ export class DreamerRunner {
207
79
  }
208
80
  }
209
81
  if (rejectedDeps.length > 0) {
210
- this.emitDreamerEvent('dreamer_context_partial', taskId, {
82
+ this.emitEvent('context_partial', taskId, {
211
83
  rejectedCount: rejectedDeps.length,
212
84
  rejectedDeps,
213
85
  });
214
86
  }
215
- const contextHash = DreamerRunner.hashContextRefs(contextRefs);
87
+ const contextHash = BasePeerRunner.hashContextRefs(contextRefs);
216
88
  return { contextHash, contextRefs, predecessorOutput };
217
89
  }
218
- /**
219
- * Compute a deterministic hash from context references.
220
- * Used for telemetry and result tracking, not for caching.
221
- */
222
- static hashContextRefs(refs) {
223
- if (refs.length === 0)
224
- return 'empty';
225
- // Simple deterministic hash via DJB2-style accumulator
226
- // Not cryptographic — only used for observability
227
- const str = refs.join('|');
228
- let hash = 0;
229
- for (let i = 0; i < str.length; i++) {
230
- hash = (Math.imul(31, hash) + str.charCodeAt(i)) | 0;
231
- }
232
- return `ctx-${Math.abs(hash).toString(16)}`;
233
- }
234
- async resolveStoreRunId(taskId) {
235
- const runs = await this.stateManager.getRunsByTask(taskId);
236
- const latestRun = runs[runs.length - 1];
237
- if (!latestRun) {
238
- throw new PDRuntimeError('execution_failed', `No run records found for task ${taskId} after lease acquisition`);
239
- }
240
- return latestRun.runId;
241
- }
242
- async resolveLineageArtifactIds(taskId) {
243
- const task = await this.stateManager.getTask(taskId);
244
- if (!task)
245
- return { ids: [], hasRejected: false };
246
- const piTask = hydratePITaskRecord(task);
247
- const deps = piTask?.dependencyTaskIds ?? [];
248
- if (deps.length === 0)
249
- return { ids: [], hasRejected: false };
250
- const lineageIds = [];
251
- let hasRejected = false;
252
- const results = await Promise.allSettled(deps.map((depId) => this.artifactStore.listBySourceTaskId(depId)));
253
- for (const result of results) {
254
- if (result.status === 'fulfilled') {
255
- for (const artifact of result.value) {
256
- lineageIds.push(artifact.artifactId);
257
- }
258
- }
259
- else {
260
- hasRejected = true;
261
- }
262
- }
263
- return { ids: lineageIds, hasRejected };
264
- }
265
- async invokeRuntime(params) {
90
+ async invokeRuntime(taskId, context) {
266
91
  const builder = new DreamerPromptBuilder();
267
92
  const { message } = builder.buildPrompt({
268
- taskId: params.taskId,
269
- contextHash: params.contextHash,
270
- contextRefs: params.contextRefs,
271
- predecessorOutput: params.predecessorOutput,
93
+ taskId,
94
+ contextHash: context.contextHash,
95
+ contextRefs: context.contextRefs,
96
+ predecessorOutput: context.predecessorOutput,
272
97
  });
273
- const startInput = {
98
+ return this.runtimeAdapter.startRun({
274
99
  agentSpec: { agentId: this.resolvedOptions.agentId, schemaVersion: 'v1' },
275
- taskRef: { taskId: params.taskId },
100
+ taskRef: { taskId },
276
101
  inputPayload: message,
277
102
  contextItems: [],
278
103
  outputSchemaRef: 'dreamer-output-v1',
279
104
  timeoutMs: this.resolvedOptions.timeoutMs,
280
- };
281
- return this.runtimeAdapter.startRun(startInput);
282
- }
283
- async pollUntilTerminal(runHandle) {
284
- const deadline = Date.now() + this.resolvedOptions.timeoutMs;
285
- const terminalStatuses = ['succeeded', 'failed', 'timed_out', 'cancelled'];
286
- while (Date.now() < deadline) {
287
- const status = await this.runtimeAdapter.pollRun(runHandle.runId);
288
- if (terminalStatuses.includes(status.status)) {
289
- return status;
290
- }
291
- await this.sleep(this.resolvedOptions.pollIntervalMs);
292
- }
293
- // Timeout — cancel gracefully, preserve timeout error with cancel status
294
- let cancelFailed = false;
295
- try {
296
- await this.runtimeAdapter.cancelRun(runHandle.runId);
297
- }
298
- catch (cancelErr) {
299
- cancelFailed = true;
300
- this.emitDreamerEvent('dreamer_cancel_run_failed', runHandle.runId, {
301
- runId: runHandle.runId,
302
- errorMessage: cancelErr instanceof Error ? cancelErr.message : String(cancelErr),
303
- });
304
- }
305
- const cancelNote = cancelFailed ? ' (cancelRun also failed)' : '';
306
- throw new PDRuntimeError('timeout', `Run ${runHandle.runId} timed out after ${this.resolvedOptions.timeoutMs}ms${cancelNote}`);
307
- }
308
- async fetchAndParseOutput(runId, taskId) {
309
- const result = await this.fetchOutputWithTelemetry(runId, taskId);
310
- if (!result || !result.payload) {
311
- this.emitDreamerEvent('dreamer_output_extraction_failed', taskId, {
312
- runId,
313
- stage: 'payload_missing',
314
- errorMessage: `No output available for run ${runId}`,
315
- });
316
- throw new PDRuntimeError('output_invalid', `No output available for run ${runId}`);
317
- }
318
- const payload = result.payload;
319
- if (typeof payload !== 'object' || payload === null) {
320
- this.emitDreamerEvent('dreamer_output_extraction_failed', taskId, {
321
- runId,
322
- stage: 'payload_not_object',
323
- errorMessage: `Output payload is not an object for run ${runId}`,
324
- });
325
- throw new PDRuntimeError('output_invalid', `Output payload is not an object for run ${runId}`);
326
- }
327
- return result.payload;
105
+ });
328
106
  }
329
- async fetchOutputWithTelemetry(runId, taskId) {
330
- try {
331
- return await this.runtimeAdapter.fetchOutput(runId);
332
- }
333
- catch (fetchErr) {
334
- this.emitDreamerEvent('dreamer_output_extraction_failed', taskId, {
335
- runId,
336
- stage: 'fetchOutput',
337
- errorMessage: fetchErr instanceof Error ? fetchErr.message : String(fetchErr),
338
- });
339
- throw fetchErr;
340
- }
107
+ async validateOutput(output, taskId) {
108
+ const result = await this.validator.validate(output, taskId);
109
+ return {
110
+ valid: result.valid,
111
+ errors: result.errors,
112
+ errorCategory: result.errorCategory,
113
+ };
341
114
  }
342
- async succeedTask(ctx) {
343
- // Store output before marking succeeded so run record reflects output
115
+ // eslint-disable-next-line @typescript-eslint/max-params
116
+ async succeedTask(taskId, runId, output, task, contextHash, _context) {
117
+ // Store output before marking succeeded
344
118
  try {
345
- await this.stateManager.updateRunOutput(ctx.runId, JSON.stringify(ctx.output));
119
+ await this.stateManager.updateRunOutput(runId, JSON.stringify(output));
346
120
  }
347
121
  catch (updateErr) {
348
- this.emitDreamerEvent('dreamer_update_output_failed', ctx.taskId, {
349
- runId: ctx.runId,
122
+ this.emitEvent('update_output_failed', taskId, {
123
+ runId,
350
124
  errorMessage: updateErr instanceof Error ? updateErr.message : String(updateErr),
351
125
  });
352
126
  throw updateErr;
353
127
  }
354
- // Emit per-candidate telemetry
355
- for (const candidate of ctx.output.candidates) {
356
- this.emitDreamerEvent('dreamer_candidate_generated', ctx.taskId, {
357
- candidateIndex: candidate.candidateIndex,
358
- confidence: candidate.confidence,
359
- riskLevel: candidate.riskLevel,
360
- });
361
- }
362
128
  // Write PIArtifact via artifactStore (idempotent upsert)
363
- // PIArtifact is the core durable output of DreamerRunner.
364
- // If artifact write fails, the task must NOT be marked succeeded —
365
- // downstream runners (Philosopher/Scribe) require durable artifact input.
366
129
  let lineageArtifactIds = [];
367
130
  let lineageHasRejected = false;
368
131
  try {
369
- const lineageResult = await this.resolveLineageArtifactIds(ctx.taskId);
132
+ const lineageResult = await this.resolveLineageArtifactIds(taskId);
370
133
  lineageArtifactIds = lineageResult.ids;
371
134
  lineageHasRejected = lineageResult.hasRejected;
372
135
  }
373
136
  catch (lineageErr) {
374
- this.emitDreamerEvent('dreamer_lineage_resolve_failed', ctx.taskId, {
375
- runId: ctx.runId,
137
+ this.emitEvent('lineage_resolve_failed', taskId, {
138
+ runId,
376
139
  errorMessage: lineageErr instanceof Error ? lineageErr.message : String(lineageErr),
377
140
  });
378
141
  }
379
142
  if (lineageHasRejected) {
380
- this.emitDreamerEvent('dreamer_lineage_partial', ctx.taskId, {
381
- runId: ctx.runId,
143
+ this.emitEvent('lineage_partial', taskId, {
144
+ runId,
382
145
  resolvedCount: lineageArtifactIds.length,
383
146
  warning: 'Some dependency artifact queries were rejected; lineage may be incomplete',
384
147
  });
385
148
  }
386
- const artifactId = `pi-art-${ctx.taskId}-${ctx.runId}`;
149
+ const artifactId = `pi-art-${taskId}-${runId}`;
387
150
  const now = new Date().toISOString();
388
151
  try {
389
152
  await this.artifactStore.upsertArtifact({
390
153
  artifactId,
391
154
  artifactKind: 'principle',
392
- sourceTaskId: ctx.taskId,
155
+ sourceTaskId: taskId,
393
156
  lineageArtifactIds,
394
157
  validationStatus: 'pending',
395
- contentJson: JSON.stringify(ctx.output),
158
+ contentJson: JSON.stringify(output),
396
159
  createdAt: now,
397
160
  updatedAt: now,
398
161
  });
399
162
  }
400
163
  catch (artifactErr) {
401
- this.emitDreamerEvent('dreamer_artifact_write_failed', ctx.taskId, {
402
- runId: ctx.runId,
164
+ this.emitEvent('artifact_write_failed', taskId, {
165
+ runId,
403
166
  errorMessage: artifactErr instanceof Error ? artifactErr.message : String(artifactErr),
404
167
  });
405
168
  return this.retryOrFail({
406
- taskId: ctx.taskId,
407
- task: ctx.task,
169
+ taskId,
170
+ task,
408
171
  errorCategory: 'artifact_commit_failed',
409
172
  failureReason: `PIArtifact write failed: ${artifactErr instanceof Error ? artifactErr.message : String(artifactErr)}`,
410
173
  });
411
174
  }
412
- // Mark task succeeded with dreamer:// resultRef
413
- const resultRef = `dreamer://${ctx.runId}`;
175
+ // Mark task succeeded
176
+ const resultRef = `${this.config.resultRefPrefix}://${runId}`;
414
177
  try {
415
- await this.stateManager.markTaskSucceeded(ctx.taskId, resultRef);
178
+ await this.stateManager.markTaskSucceeded(taskId, resultRef);
416
179
  }
417
180
  catch (stateErr) {
418
- this.emitDreamerEvent('dreamer_mark_succeeded_failed', ctx.taskId, {
419
- taskId: ctx.taskId,
420
- runId: ctx.runId,
181
+ this.emitEvent('mark_succeeded_failed', taskId, {
182
+ taskId,
183
+ runId,
421
184
  errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
422
185
  });
423
186
  throw stateErr;
424
187
  }
425
- this.emitDreamerEvent('dreamer_task_succeeded', ctx.taskId, {
426
- attemptCount: ctx.task.attemptCount,
188
+ this.emitEvent('task_succeeded', taskId, {
189
+ attemptCount: task.attemptCount,
427
190
  resultRef,
428
- candidateCount: ctx.output.candidates.length,
191
+ candidateCount: output.candidates.length,
429
192
  });
430
- this.phase = RunnerPhase.Completed;
431
193
  return {
432
194
  status: 'succeeded',
433
- taskId: ctx.taskId,
434
- runId: ctx.runId,
195
+ taskId,
196
+ runId,
435
197
  artifactId,
436
198
  resultRef,
437
- contextHash: ctx.contextHash,
438
- output: ctx.output,
439
- attemptCount: ctx.task.attemptCount,
440
- };
441
- }
442
- async handleRuntimeFailure(taskId, task, runStatus) {
443
- const errorCategory = this.mapRunStatusToErrorCategory(runStatus.status, runStatus.reason);
444
- this.emitDreamerEvent('dreamer_run_failed', taskId, {
445
- runStatus: runStatus.status,
446
- errorCategory,
447
- });
448
- return this.retryOrFail({
449
- taskId,
450
- task,
451
- errorCategory,
452
- failureReason: `Runtime execution ended with status: ${runStatus.status}`,
453
- });
454
- }
455
- async handleValidationError(ctx) {
456
- const category = ctx.errorCategory ?? 'output_invalid';
457
- this.emitDreamerEvent('dreamer_output_invalid', ctx.taskId, {
458
- errorCount: ctx.errors.length,
459
- errorCategory: category,
460
- });
461
- return this.retryOrFail({
462
- taskId: ctx.taskId,
463
- task: ctx.task,
464
- errorCategory: category,
465
- failureReason: `Validation failed: ${ctx.errors.join('; ')}`,
466
- });
467
- }
468
- async handleLeaseOrPhaseError(taskId, error) {
469
- const classified = this.classifyError(error);
470
- // lease_conflict is concurrent-access conflict, not a state change.
471
- // No mutation methods (markTaskFailed/markTaskRetryWait) are called.
472
- if (classified.category === 'lease_conflict') {
473
- this.emitDreamerEvent('dreamer_run_failed', taskId, {
474
- errorCategory: 'lease_conflict',
475
- errorMessage: classified.message,
476
- });
477
- return {
478
- status: 'failed',
479
- taskId,
480
- errorCategory: 'lease_conflict',
481
- failureReason: classified.message,
482
- attemptCount: 1,
483
- };
484
- }
485
- // Non-lease errors (e.g., storage_unavailable before lease) must not
486
- // use synthetic TaskRecord. Build one with real defaults from options.
487
- this.emitDreamerEvent('dreamer_run_failed', taskId, {
488
- errorCategory: classified.category,
489
- errorMessage: classified.message,
490
- });
491
- const task = {
492
- taskId,
493
- taskKind: 'dreamer',
494
- status: 'leased',
495
- createdAt: new Date().toISOString(),
496
- updatedAt: new Date().toISOString(),
497
- attemptCount: 1,
498
- maxAttempts: this.resolvedOptions.defaultMaxAttempts,
499
- };
500
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
501
- }
502
- async handlePostLeaseError(taskId, task, error) {
503
- const classified = this.classifyError(error);
504
- this.emitDreamerEvent('dreamer_run_failed', taskId, {
505
- errorCategory: classified.category,
506
- errorMessage: classified.message,
507
- });
508
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
509
- }
510
- async retryOrFail(ctx) {
511
- // Check if error is permanent (never retry)
512
- if (this.isPermanentError(ctx.errorCategory)) {
513
- try {
514
- await this.stateManager.markTaskFailed(ctx.taskId, ctx.errorCategory);
515
- }
516
- catch (stateErr) {
517
- this.emitDreamerEvent('dreamer_mark_failed_error', ctx.taskId, {
518
- errorCategory: 'storage_unavailable',
519
- attemptCount: ctx.task.attemptCount,
520
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
521
- });
522
- return {
523
- status: 'failed',
524
- taskId: ctx.taskId,
525
- errorCategory: 'storage_unavailable',
526
- failureReason: `State manager error: ${ctx.failureReason}`,
527
- attemptCount: ctx.task.attemptCount,
528
- };
529
- }
530
- this.emitDreamerEvent('dreamer_task_failed', ctx.taskId, {
531
- errorCategory: ctx.errorCategory,
532
- attemptCount: ctx.task.attemptCount,
533
- failureReason: ctx.failureReason,
534
- });
535
- this.phase = RunnerPhase.Failed;
536
- return {
537
- status: 'failed',
538
- taskId: ctx.taskId,
539
- errorCategory: ctx.errorCategory,
540
- failureReason: ctx.failureReason,
541
- attemptCount: ctx.task.attemptCount,
542
- };
543
- }
544
- // Check retry policy
545
- const shouldRetry = this.stateManager.getRetryPolicy().shouldRetry(ctx.task);
546
- if (shouldRetry) {
547
- try {
548
- await this.stateManager.markTaskRetryWait(ctx.taskId, ctx.errorCategory);
549
- }
550
- catch (stateErr) {
551
- this.emitDreamerEvent('dreamer_mark_retry_error', ctx.taskId, {
552
- errorCategory: 'storage_unavailable',
553
- attemptCount: ctx.task.attemptCount,
554
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
555
- });
556
- return {
557
- status: 'failed',
558
- taskId: ctx.taskId,
559
- errorCategory: 'storage_unavailable',
560
- failureReason: `State manager error: ${ctx.failureReason}`,
561
- attemptCount: ctx.task.attemptCount,
562
- };
563
- }
564
- this.emitDreamerEvent('dreamer_task_retried', ctx.taskId, {
565
- errorCategory: ctx.errorCategory,
566
- attemptCount: ctx.task.attemptCount,
567
- });
568
- this.phase = RunnerPhase.RetryWaiting;
569
- return {
570
- status: 'retried',
571
- taskId: ctx.taskId,
572
- errorCategory: ctx.errorCategory,
573
- failureReason: ctx.failureReason,
574
- attemptCount: ctx.task.attemptCount,
575
- };
576
- }
577
- // Max attempts exceeded
578
- try {
579
- await this.stateManager.markTaskFailed(ctx.taskId, 'max_attempts_exceeded');
580
- }
581
- catch (stateErr) {
582
- this.emitDreamerEvent('dreamer_mark_failed_error', ctx.taskId, {
583
- errorCategory: 'storage_unavailable',
584
- attemptCount: ctx.task.attemptCount,
585
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
586
- });
587
- return {
588
- status: 'failed',
589
- taskId: ctx.taskId,
590
- errorCategory: 'storage_unavailable',
591
- failureReason: `State manager error: ${ctx.failureReason}`,
592
- attemptCount: ctx.task.attemptCount,
593
- };
594
- }
595
- this.emitDreamerEvent('dreamer_task_failed', ctx.taskId, {
596
- errorCategory: 'max_attempts_exceeded',
597
- attemptCount: ctx.task.attemptCount,
598
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
599
- });
600
- this.phase = RunnerPhase.Failed;
601
- return {
602
- status: 'failed',
603
- taskId: ctx.taskId,
604
- errorCategory: 'max_attempts_exceeded',
605
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
606
- attemptCount: ctx.task.attemptCount,
199
+ contextHash,
200
+ output,
201
+ attemptCount: task.attemptCount,
607
202
  };
608
203
  }
609
- // ── Error classification ──────────────────────────────────────────────────
610
- PERMANENT_ERROR_CATEGORIES = new Set(Object.freeze(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid']));
611
- isPermanentError(category) {
612
- return this.PERMANENT_ERROR_CATEGORIES.has(category);
613
- }
204
+ // ── Optional hooks ─────────────────────────────────────────────────────────
205
+ /**
206
+ * Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
207
+ * Only fill when absent via Object.hasOwn — present-but-falsy values
208
+ * must reach validation and fail loud (Runtime Contract Rule 3).
209
+ */
614
210
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
615
- classifyError(error) {
616
- if (error instanceof PDRuntimeError) {
617
- return { category: error.category, message: error.message };
618
- }
619
- if (error instanceof Error) {
620
- return { category: 'execution_failed', message: error.message };
621
- }
622
- return { category: 'execution_failed', message: String(error) };
211
+ postFetchTransform(taskId, untrustedOutput) {
212
+ injectRunnerLineageIfAbsent(untrustedOutput, 'taskId', taskId);
623
213
  }
624
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
625
- mapRunStatusToErrorCategory(status, _reason) {
626
- switch (status) {
627
- case 'failed': return 'execution_failed';
628
- case 'timed_out': return 'timeout';
629
- case 'cancelled': return 'cancelled';
630
- default: return 'execution_failed';
214
+ emitSuccessTelemetry(taskId, output) {
215
+ for (const candidate of output.candidates) {
216
+ this.emitEvent('candidate_generated', taskId, {
217
+ candidateIndex: candidate.candidateIndex,
218
+ confidence: candidate.confidence,
219
+ riskLevel: candidate.riskLevel,
220
+ });
631
221
  }
632
222
  }
633
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
634
- sleep(ms) {
635
- return new Promise((resolve) => setTimeout(resolve, ms));
636
- }
637
223
  }
638
- // ── Exports ───────────────────────────────────────────────────────────────────
639
- export { resolveDreamerRunnerOptions, DEFAULT_DREAMER_RUNNER_OPTIONS };
640
224
  //# sourceMappingURL=dreamer-runner.js.map