@principles/core 1.84.0 → 1.86.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 (36) hide show
  1. package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js +52 -0
  2. package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js.map +1 -1
  3. package/dist/runtime-v2/__tests__/telemetry-event.test.d.ts +2 -0
  4. package/dist/runtime-v2/__tests__/telemetry-event.test.d.ts.map +1 -0
  5. package/dist/runtime-v2/__tests__/telemetry-event.test.js +56 -0
  6. package/dist/runtime-v2/__tests__/telemetry-event.test.js.map +1 -0
  7. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.d.ts +2 -0
  8. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.d.ts.map +1 -0
  9. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.js +398 -0
  10. package/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.js.map +1 -0
  11. package/dist/runtime-v2/internalization/artificer-output.d.ts +3 -2
  12. package/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
  13. package/dist/runtime-v2/internalization/artificer-output.js +40 -31
  14. package/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
  15. package/dist/runtime-v2/internalization/artificer-runner.d.ts +59 -38
  16. package/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  17. package/dist/runtime-v2/internalization/artificer-runner.js +121 -395
  18. package/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  19. package/dist/runtime-v2/internalization/philosopher-output.d.ts +8 -2
  20. package/dist/runtime-v2/internalization/philosopher-output.d.ts.map +1 -1
  21. package/dist/runtime-v2/internalization/philosopher-output.js +16 -11
  22. package/dist/runtime-v2/internalization/philosopher-output.js.map +1 -1
  23. package/dist/runtime-v2/internalization/philosopher-runner.d.ts +36 -55
  24. package/dist/runtime-v2/internalization/philosopher-runner.d.ts.map +1 -1
  25. package/dist/runtime-v2/internalization/philosopher-runner.js +117 -401
  26. package/dist/runtime-v2/internalization/philosopher-runner.js.map +1 -1
  27. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +22 -0
  28. package/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -1
  29. package/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -1
  30. package/dist/runtime-v2/runner/base-peer-runner.js +5 -1
  31. package/dist/runtime-v2/runner/base-peer-runner.js.map +1 -1
  32. package/dist/telemetry-event.d.ts +2 -2
  33. package/dist/telemetry-event.d.ts.map +1 -1
  34. package/dist/telemetry-event.js +88 -0
  35. package/dist/telemetry-event.js.map +1 -1
  36. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { PDRuntimeError } from '../error-categories.js';
1
+ import { PDRuntimeError, isPDErrorCategory } from '../error-categories.js';
2
2
  import { hydratePITaskRecord } from './pitask-metadata.js';
3
- import { RunnerPhase } from '../runner/runner-phase.js';
4
3
  import { ArtificerPromptBuilder } from './artificer-prompt-builder.js';
5
4
  import { injectRunnerLineageIfAbsent } from './peer-runner-contracts.js';
5
+ import { BasePeerRunner } from '../runner/base-peer-runner.js';
6
6
  const DEFAULT_ARTIFICER_RUNNER_OPTIONS = {
7
7
  pollIntervalMs: 5_000,
8
8
  timeoutMs: 300_000,
@@ -19,117 +19,22 @@ export function resolveArtificerRunnerOptions(options) {
19
19
  agentId: options.agentId ?? DEFAULT_ARTIFICER_RUNNER_OPTIONS.agentId,
20
20
  };
21
21
  }
22
- export class ArtificerRunner {
23
- phase = RunnerPhase.Idle;
24
- resolvedOptions;
25
- stateManager;
26
- runtimeAdapter;
27
- eventEmitter;
22
+ // ── ArtificerRunner ──────────────────────────────────────────────────────────
23
+ export class ArtificerRunner extends BasePeerRunner {
28
24
  validator;
29
- artifactStore;
30
25
  constructor(deps, options) {
31
- this.stateManager = deps.stateManager;
32
- this.runtimeAdapter = deps.runtimeAdapter;
33
- this.eventEmitter = deps.eventEmitter;
34
- this.validator = deps.validator;
35
- this.artifactStore = deps.artifactStore;
36
- this.resolvedOptions = resolveArtificerRunnerOptions(options);
37
- }
38
- get currentPhase() {
39
- return this.phase;
40
- }
41
- emitArtificerEvent(eventType, taskId, payload) {
42
- this.eventEmitter.emitTelemetry({
43
- eventType: eventType,
44
- traceId: taskId,
45
- timestamp: new Date().toISOString(),
46
- sessionId: this.resolvedOptions.owner,
47
- agentId: this.resolvedOptions.agentId,
48
- payload,
26
+ super(deps, options, {
27
+ runnerName: 'artificer',
28
+ expectedTaskKind: 'artificer',
29
+ defaultAgentId: 'artificer',
30
+ resultRefPrefix: 'artificer',
49
31
  });
32
+ this.validator = deps.validator;
50
33
  }
51
- async run(taskId) {
52
- this.phase = RunnerPhase.Idle;
53
- let leasedTask;
54
- try {
55
- leasedTask = await this.stateManager.acquireLease({
56
- taskId,
57
- owner: this.resolvedOptions.owner,
58
- runtimeKind: this.resolvedOptions.runtimeKind,
59
- });
60
- }
61
- catch (error) {
62
- return await this.handleLeaseOrPhaseError(taskId, error);
63
- }
64
- if (leasedTask.taskKind !== 'artificer') {
65
- this.emitArtificerEvent('artificer_wrong_task_kind', taskId, {
66
- expectedKind: 'artificer',
67
- actualKind: leasedTask.taskKind,
68
- });
69
- await this.stateManager.markTaskFailed(taskId, 'input_invalid');
70
- return {
71
- status: 'failed',
72
- taskId,
73
- errorCategory: 'input_invalid',
74
- failureReason: `Task kind must be 'artificer', got '${leasedTask.taskKind}'`,
75
- attemptCount: leasedTask.attemptCount,
76
- };
77
- }
78
- this.emitArtificerEvent('artificer_task_leased', taskId, {
79
- taskKind: 'artificer',
80
- attemptCount: leasedTask.attemptCount,
81
- });
82
- try {
83
- const storeRunId = await this.resolveStoreRunId(taskId);
84
- this.phase = RunnerPhase.BuildingContext;
85
- const { contextHash, scribeArtifact, sourceScribeArtifactId } = await this.buildContext(taskId);
86
- if (!scribeArtifact || !sourceScribeArtifactId) {
87
- return this.retryOrFail({
88
- taskId,
89
- task: leasedTask,
90
- errorCategory: 'input_invalid',
91
- failureReason: sourceScribeArtifactId ? 'Scribe dependency artifact not found' : 'Scribe dependency artifact ID not resolved',
92
- });
93
- }
94
- this.emitArtificerEvent('artificer_context_built', taskId, { contextHash });
95
- this.phase = RunnerPhase.Invoking;
96
- const runHandle = await this.invokeRuntime({ taskId, contextHash, scribeArtifact, sourceScribeArtifactId });
97
- this.emitArtificerEvent('artificer_run_started', taskId, {
98
- runtimeKind: this.resolvedOptions.runtimeKind,
99
- });
100
- this.phase = RunnerPhase.Polling;
101
- const finalStatus = await this.pollUntilTerminal(runHandle);
102
- if (finalStatus.status !== 'succeeded') {
103
- return await this.handleRuntimeFailure(taskId, leasedTask, finalStatus);
104
- }
105
- this.phase = RunnerPhase.FetchingOutput;
106
- const output = await this.fetchAndParseOutput(runHandle.runId);
107
- // Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
108
- injectRunnerLineageIfAbsent(output, 'taskId', taskId);
109
- this.phase = RunnerPhase.Validating;
110
- const validationResult = await this.validator.validate(output, taskId, sourceScribeArtifactId ?? undefined);
111
- if (!validationResult.valid) {
112
- return await this.handleValidationError({
113
- taskId,
114
- task: leasedTask,
115
- errors: validationResult.errors,
116
- errorCategory: validationResult.errorCategory,
117
- });
118
- }
119
- this.emitArtificerEvent('artificer_output_validated', taskId, {
120
- implementationSummary: output.implementationPlan.summary,
121
- });
122
- return await this.succeedTask({
123
- taskId,
124
- runId: storeRunId,
125
- output,
126
- task: leasedTask,
127
- contextHash,
128
- });
129
- }
130
- catch (error) {
131
- return await this.handlePostLeaseError(taskId, leasedTask, error);
132
- }
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', 'output_invalid']);
133
38
  }
134
39
  async buildContext(taskId) {
135
40
  const task = await this.stateManager.getTask(taskId);
@@ -139,7 +44,7 @@ export class ArtificerRunner {
139
44
  const piTask = hydratePITaskRecord(task);
140
45
  const deps = piTask?.dependencyTaskIds ?? [];
141
46
  if (deps.length === 0) {
142
- this.emitArtificerEvent('artificer_no_dependencies', taskId, {});
47
+ this.emitEvent('no_dependencies', taskId, {});
143
48
  return { contextHash: 'empty', scribeArtifact: null, sourceScribeArtifactId: null };
144
49
  }
145
50
  for (const depId of deps) {
@@ -149,7 +54,7 @@ export class ArtificerRunner {
149
54
  if (depTask.taskKind !== 'scribe')
150
55
  continue;
151
56
  if (depTask.status !== 'succeeded') {
152
- this.emitArtificerEvent('artificer_dependency_not_succeeded', taskId, {
57
+ this.emitEvent('dependency_not_succeeded', taskId, {
153
58
  depTaskId: depId,
154
59
  depStatus: depTask.status,
155
60
  });
@@ -161,362 +66,183 @@ export class ArtificerRunner {
161
66
  if (!firstArtifact)
162
67
  continue;
163
68
  const artifactRef = firstArtifact.artifactId;
164
- this.emitArtificerEvent('artificer_scribe_dep_selected', taskId, {
69
+ this.emitEvent('scribe_dep_selected', taskId, {
165
70
  depTaskId: depId,
166
71
  artifactId: firstArtifact.artifactId,
167
72
  });
168
73
  return {
169
- contextHash: ArtificerRunner.hashContextRefs([artifactRef]),
74
+ contextHash: BasePeerRunner.hashContextRefs([artifactRef]),
170
75
  scribeArtifact: firstArtifact.contentJson,
171
76
  sourceScribeArtifactId: firstArtifact.artifactId,
172
77
  };
173
78
  }
174
79
  }
175
- this.emitArtificerEvent('artificer_no_scribe_artifact', taskId, {});
80
+ this.emitEvent('no_scribe_artifact', taskId, {});
176
81
  return { contextHash: 'empty', scribeArtifact: null, sourceScribeArtifactId: null };
177
82
  }
178
- static hashContextRefs(refs) {
179
- if (refs.length === 0)
180
- return 'empty';
181
- const str = refs.join('|');
182
- let hash = 0;
183
- for (let i = 0; i < str.length; i++) {
184
- hash = (Math.imul(31, hash) + str.charCodeAt(i)) | 0;
83
+ async invokeRuntime(taskId, context) {
84
+ if (!context.scribeArtifact || !context.sourceScribeArtifactId) {
85
+ throw new PDRuntimeError('input_invalid', 'Scribe dependency artifact not resolved');
185
86
  }
186
- return `ctx-${Math.abs(hash).toString(16)}`;
187
- }
188
- async resolveStoreRunId(taskId) {
189
- const runs = await this.stateManager.getRunsByTask(taskId);
190
- const latestRun = runs[runs.length - 1];
191
- if (!latestRun) {
192
- throw new PDRuntimeError('execution_failed', `No run records found for task ${taskId} after lease acquisition`);
87
+ let scribeArtifactInput;
88
+ try {
89
+ scribeArtifactInput = JSON.parse(context.scribeArtifact);
193
90
  }
194
- return latestRun.runId;
195
- }
196
- async invokeRuntime(params) {
197
- let parsedScribeArtifact = null;
198
- if (params.scribeArtifact) {
199
- try {
200
- parsedScribeArtifact = JSON.parse(params.scribeArtifact);
201
- }
202
- catch {
203
- parsedScribeArtifact = params.scribeArtifact;
204
- }
91
+ catch {
92
+ scribeArtifactInput = context.scribeArtifact;
205
93
  }
206
94
  const builder = new ArtificerPromptBuilder();
207
95
  const { message } = builder.buildPrompt({
208
- taskId: params.taskId,
209
- contextHash: params.contextHash,
210
- scribeArtifact: parsedScribeArtifact,
211
- sourceScribeArtifactId: params.sourceScribeArtifactId,
96
+ taskId,
97
+ contextHash: context.contextHash,
98
+ scribeArtifact: scribeArtifactInput,
99
+ sourceScribeArtifactId: context.sourceScribeArtifactId,
212
100
  });
213
- const startInput = {
101
+ return this.runtimeAdapter.startRun({
214
102
  agentSpec: { agentId: this.resolvedOptions.agentId, schemaVersion: 'v1' },
215
- taskRef: { taskId: params.taskId },
103
+ taskRef: { taskId },
216
104
  inputPayload: message,
217
105
  contextItems: [],
218
106
  outputSchemaRef: 'artificer-output-v1',
219
107
  timeoutMs: this.resolvedOptions.timeoutMs,
220
- };
221
- return this.runtimeAdapter.startRun(startInput);
108
+ });
222
109
  }
223
- async pollUntilTerminal(runHandle) {
224
- const deadline = Date.now() + this.resolvedOptions.timeoutMs;
225
- const terminalStatuses = ['succeeded', 'failed', 'timed_out', 'cancelled'];
226
- while (Date.now() < deadline) {
227
- const status = await this.runtimeAdapter.pollRun(runHandle.runId);
228
- if (terminalStatuses.includes(status.status)) {
229
- return status;
230
- }
231
- await this.sleep(this.resolvedOptions.pollIntervalMs);
110
+ async validateOutput(output, taskId, context) {
111
+ const result = await this.validator.validate(output, taskId, context.sourceScribeArtifactId ?? undefined);
112
+ // Trust-boundary: validator returns `string | undefined` for errorCategory.
113
+ // Must not `as`-cast; validate at runtime (ERR-001, ERR-005).
114
+ const rawCategory = result.errorCategory;
115
+ let errorCategory;
116
+ if (rawCategory == null) {
117
+ errorCategory = undefined;
232
118
  }
233
- let cancelFailed = false;
234
- try {
235
- await this.runtimeAdapter.cancelRun(runHandle.runId);
119
+ else if (isPDErrorCategory(rawCategory)) {
120
+ errorCategory = rawCategory;
236
121
  }
237
- catch (cancelErr) {
238
- cancelFailed = true;
239
- this.emitArtificerEvent('artificer_cancel_run_failed', runHandle.runId, {
240
- runId: runHandle.runId,
241
- errorMessage: cancelErr instanceof Error ? cancelErr.message : String(cancelErr),
242
- });
122
+ else {
123
+ // Invalid errorCategory from validator — fail loud, do not pass through
124
+ return {
125
+ valid: false,
126
+ errors: [...result.errors, `invalid errorCategory: ${rawCategory}`],
127
+ errorCategory: 'output_invalid',
128
+ };
243
129
  }
244
- const cancelNote = cancelFailed ? ' (cancelRun also failed)' : '';
245
- throw new PDRuntimeError('timeout', `Run ${runHandle.runId} timed out after ${this.resolvedOptions.timeoutMs}ms${cancelNote}`);
130
+ return {
131
+ valid: result.valid,
132
+ errors: result.errors,
133
+ errorCategory,
134
+ };
246
135
  }
247
- async fetchAndParseOutput(runId) {
248
- const result = await this.runtimeAdapter.fetchOutput(runId);
249
- if (!result || !result.payload) {
250
- throw new PDRuntimeError('output_invalid', `No output available for run ${runId}`);
136
+ // eslint-disable-next-line @typescript-eslint/max-params
137
+ async succeedTask(taskId, runId, output, task, contextHash, context) {
138
+ // Lineage consistency: sourceScribeArtifactId must match buildContext result (ERR-004, ERR-008).
139
+ if (!context.sourceScribeArtifactId || output.sourceScribeArtifactId !== context.sourceScribeArtifactId) {
140
+ throw new PDRuntimeError('output_invalid', `sourceScribeArtifactId mismatch: expected ${context.sourceScribeArtifactId ?? '(none)'}, got ${output.sourceScribeArtifactId}`);
251
141
  }
252
- return result.payload;
253
- }
254
- async succeedTask(ctx) {
142
+ // Store output before marking succeeded
255
143
  try {
256
- await this.stateManager.updateRunOutput(ctx.runId, JSON.stringify(ctx.output));
144
+ await this.stateManager.updateRunOutput(runId, JSON.stringify(output));
257
145
  }
258
146
  catch (updateErr) {
259
- this.emitArtificerEvent('artificer_update_output_failed', ctx.taskId, {
260
- runId: ctx.runId,
147
+ this.emitEvent('update_output_failed', taskId, {
148
+ runId,
261
149
  errorMessage: updateErr instanceof Error ? updateErr.message : String(updateErr),
262
150
  });
263
151
  throw updateErr;
264
152
  }
265
- const artifactId = `pi-art-${ctx.taskId}-${ctx.runId}`;
266
- const now = new Date().toISOString();
153
+ // Resolve lineage artifact IDs
267
154
  let lineageArtifactIds = [];
155
+ let lineageHasRejected = false;
268
156
  try {
269
- const piTask = hydratePITaskRecord(ctx.task);
270
- const deps = piTask?.dependencyTaskIds ?? [];
271
- const results = await Promise.allSettled(deps.map((depId) => this.artifactStore.listBySourceTaskId(depId)));
272
- for (const result of results) {
273
- if (result.status === 'fulfilled') {
274
- for (const artifact of result.value) {
275
- lineageArtifactIds.push(artifact.artifactId);
276
- }
277
- }
278
- }
157
+ const lineageResult = await this.resolveLineageArtifactIds(taskId);
158
+ lineageArtifactIds = lineageResult.ids;
159
+ lineageHasRejected = lineageResult.hasRejected;
160
+ }
161
+ catch (lineageErr) {
162
+ this.emitEvent('lineage_resolve_failed', taskId, {
163
+ runId,
164
+ errorMessage: lineageErr instanceof Error ? lineageErr.message : String(lineageErr),
165
+ });
166
+ }
167
+ if (lineageHasRejected) {
168
+ this.emitEvent('lineage_partial', taskId, {
169
+ runId,
170
+ resolvedCount: lineageArtifactIds.length,
171
+ warning: 'Some dependency artifact queries were rejected; lineage may be incomplete',
172
+ });
279
173
  }
280
- catch { /* lineage resolution failure is non-fatal */ }
174
+ // Write PIArtifact via artifactStore (idempotent upsert)
175
+ const artifactId = `pi-art-${taskId}-${runId}`;
176
+ const now = new Date().toISOString();
281
177
  try {
282
178
  await this.artifactStore.upsertArtifact({
283
179
  artifactId,
284
180
  artifactKind: 'principle',
285
- sourceTaskId: ctx.taskId,
181
+ sourceTaskId: taskId,
286
182
  lineageArtifactIds,
287
183
  validationStatus: 'pending',
288
- contentJson: JSON.stringify(ctx.output),
184
+ contentJson: JSON.stringify(output),
289
185
  createdAt: now,
290
186
  updatedAt: now,
291
187
  });
292
188
  }
293
189
  catch (artifactErr) {
294
- this.emitArtificerEvent('artificer_artifact_write_failed', ctx.taskId, {
295
- runId: ctx.runId,
190
+ this.emitEvent('artifact_write_failed', taskId, {
191
+ runId,
296
192
  errorMessage: artifactErr instanceof Error ? artifactErr.message : String(artifactErr),
297
193
  });
298
194
  return this.retryOrFail({
299
- taskId: ctx.taskId,
300
- task: ctx.task,
195
+ taskId,
196
+ task,
301
197
  errorCategory: 'artifact_commit_failed',
302
198
  failureReason: `PIArtifact write failed: ${artifactErr instanceof Error ? artifactErr.message : String(artifactErr)}`,
303
199
  });
304
200
  }
305
- const resultRef = `artificer://${ctx.runId}`;
201
+ // Mark task succeeded
202
+ const resultRef = `${this.config.resultRefPrefix}://${runId}`;
306
203
  try {
307
- await this.stateManager.markTaskSucceeded(ctx.taskId, resultRef);
204
+ await this.stateManager.markTaskSucceeded(taskId, resultRef);
308
205
  }
309
206
  catch (stateErr) {
310
- this.emitArtificerEvent('artificer_mark_succeeded_failed', ctx.taskId, {
311
- taskId: ctx.taskId,
312
- runId: ctx.runId,
207
+ this.emitEvent('mark_succeeded_failed', taskId, {
208
+ taskId,
209
+ runId,
313
210
  errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
314
211
  });
315
212
  throw stateErr;
316
213
  }
317
- this.emitArtificerEvent('artificer_task_succeeded', ctx.taskId, {
318
- attemptCount: ctx.task.attemptCount,
214
+ this.emitEvent('task_succeeded', taskId, {
215
+ attemptCount: task.attemptCount,
319
216
  resultRef,
320
- implementationSummary: ctx.output.implementationPlan.summary,
217
+ implementationSummary: output.implementationPlan.summary,
321
218
  });
322
- this.phase = RunnerPhase.Completed;
323
219
  return {
324
220
  status: 'succeeded',
325
- taskId: ctx.taskId,
326
- runId: ctx.runId,
221
+ taskId,
222
+ runId,
327
223
  artifactId,
328
224
  resultRef,
329
- contextHash: ctx.contextHash,
330
- output: ctx.output,
331
- attemptCount: ctx.task.attemptCount,
225
+ contextHash,
226
+ output,
227
+ attemptCount: task.attemptCount,
332
228
  };
333
229
  }
334
- async handleRuntimeFailure(taskId, task, runStatus) {
335
- const errorCategory = this.mapRunStatusToErrorCategory(runStatus.status);
336
- this.emitArtificerEvent('artificer_run_failed', taskId, {
337
- runStatus: runStatus.status,
338
- errorCategory,
339
- });
340
- return this.retryOrFail({
341
- taskId,
342
- task,
343
- errorCategory,
344
- failureReason: `Runtime execution ended with status: ${runStatus.status}`,
345
- });
346
- }
347
- async handleValidationError(ctx) {
348
- const category = (ctx.errorCategory ?? 'output_invalid');
349
- this.emitArtificerEvent('artificer_output_invalid', ctx.taskId, {
350
- errorCount: ctx.errors.length,
351
- errorCategory: category,
352
- });
353
- return this.retryOrFail({
354
- taskId: ctx.taskId,
355
- task: ctx.task,
356
- errorCategory: category,
357
- failureReason: `Validation failed: ${ctx.errors.join('; ')}`,
358
- });
359
- }
360
- async handleLeaseOrPhaseError(taskId, error) {
361
- const classified = this.classifyError(error);
362
- if (classified.category === 'lease_conflict') {
363
- this.emitArtificerEvent('artificer_run_failed', taskId, {
364
- errorCategory: 'lease_conflict',
365
- errorMessage: classified.message,
366
- });
367
- return {
368
- status: 'failed',
369
- taskId,
370
- errorCategory: 'lease_conflict',
371
- failureReason: classified.message,
372
- attemptCount: 1,
373
- };
374
- }
375
- this.emitArtificerEvent('artificer_run_failed', taskId, {
376
- errorCategory: classified.category,
377
- errorMessage: classified.message,
378
- });
379
- const task = {
380
- taskId,
381
- taskKind: 'artificer',
382
- status: 'leased',
383
- createdAt: new Date().toISOString(),
384
- updatedAt: new Date().toISOString(),
385
- attemptCount: 1,
386
- maxAttempts: this.resolvedOptions.defaultMaxAttempts,
387
- };
388
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
389
- }
390
- async handlePostLeaseError(taskId, task, error) {
391
- const classified = this.classifyError(error);
392
- this.emitArtificerEvent('artificer_run_failed', taskId, {
393
- errorCategory: classified.category,
394
- errorMessage: classified.message,
395
- });
396
- return this.retryOrFail({ taskId, task, errorCategory: classified.category, failureReason: classified.message });
397
- }
398
- async retryOrFail(ctx) {
399
- if (this.isPermanentError(ctx.errorCategory)) {
400
- try {
401
- await this.stateManager.markTaskFailed(ctx.taskId, ctx.errorCategory);
402
- }
403
- catch (stateErr) {
404
- this.emitArtificerEvent('artificer_mark_failed_error', ctx.taskId, {
405
- errorCategory: 'storage_unavailable',
406
- attemptCount: ctx.task.attemptCount,
407
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
408
- });
409
- return {
410
- status: 'failed',
411
- taskId: ctx.taskId,
412
- errorCategory: 'storage_unavailable',
413
- failureReason: `State manager error: ${ctx.failureReason}`,
414
- attemptCount: ctx.task.attemptCount,
415
- };
416
- }
417
- this.emitArtificerEvent('artificer_task_failed', ctx.taskId, {
418
- errorCategory: ctx.errorCategory,
419
- attemptCount: ctx.task.attemptCount,
420
- failureReason: ctx.failureReason,
421
- });
422
- this.phase = RunnerPhase.Failed;
423
- return {
424
- status: 'failed',
425
- taskId: ctx.taskId,
426
- errorCategory: ctx.errorCategory,
427
- failureReason: ctx.failureReason,
428
- attemptCount: ctx.task.attemptCount,
429
- };
430
- }
431
- const shouldRetry = this.stateManager.getRetryPolicy().shouldRetry(ctx.task);
432
- if (shouldRetry) {
433
- try {
434
- await this.stateManager.markTaskRetryWait(ctx.taskId, ctx.errorCategory);
435
- }
436
- catch (stateErr) {
437
- this.emitArtificerEvent('artificer_mark_retry_error', ctx.taskId, {
438
- errorCategory: 'storage_unavailable',
439
- attemptCount: ctx.task.attemptCount,
440
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
441
- });
442
- return {
443
- status: 'failed',
444
- taskId: ctx.taskId,
445
- errorCategory: 'storage_unavailable',
446
- failureReason: `State manager error: ${ctx.failureReason}`,
447
- attemptCount: ctx.task.attemptCount,
448
- };
449
- }
450
- this.emitArtificerEvent('artificer_task_retried', ctx.taskId, {
451
- errorCategory: ctx.errorCategory,
452
- attemptCount: ctx.task.attemptCount,
453
- });
454
- this.phase = RunnerPhase.RetryWaiting;
455
- return {
456
- status: 'retried',
457
- taskId: ctx.taskId,
458
- errorCategory: ctx.errorCategory,
459
- failureReason: ctx.failureReason,
460
- attemptCount: ctx.task.attemptCount,
461
- };
462
- }
463
- try {
464
- await this.stateManager.markTaskFailed(ctx.taskId, 'max_attempts_exceeded');
465
- }
466
- catch (stateErr) {
467
- this.emitArtificerEvent('artificer_mark_failed_error', ctx.taskId, {
468
- errorCategory: 'storage_unavailable',
469
- attemptCount: ctx.task.attemptCount,
470
- errorMessage: stateErr instanceof Error ? stateErr.message : String(stateErr),
471
- });
472
- return {
473
- status: 'failed',
474
- taskId: ctx.taskId,
475
- errorCategory: 'storage_unavailable',
476
- failureReason: `State manager error: ${ctx.failureReason}`,
477
- attemptCount: ctx.task.attemptCount,
478
- };
479
- }
480
- this.emitArtificerEvent('artificer_task_failed', ctx.taskId, {
481
- errorCategory: 'max_attempts_exceeded',
482
- attemptCount: ctx.task.attemptCount,
483
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
484
- });
485
- this.phase = RunnerPhase.Failed;
486
- return {
487
- status: 'failed',
488
- taskId: ctx.taskId,
489
- errorCategory: 'max_attempts_exceeded',
490
- failureReason: `Max attempts exceeded: ${ctx.failureReason}`,
491
- attemptCount: ctx.task.attemptCount,
492
- };
493
- }
494
- PERMANENT_ERROR_CATEGORIES = new Set(Object.freeze(['storage_unavailable', 'workspace_invalid', 'capability_missing', 'cancelled', 'input_invalid', 'output_invalid']));
495
- isPermanentError(category) {
496
- return this.PERMANENT_ERROR_CATEGORIES.has(category);
497
- }
498
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
499
- classifyError(error) {
500
- if (error instanceof PDRuntimeError) {
501
- return { category: error.category, message: error.message };
502
- }
503
- if (error instanceof Error) {
504
- return { category: 'execution_failed', message: error.message };
505
- }
506
- return { category: 'execution_failed', message: String(error) };
507
- }
230
+ // ── Optional hooks ─────────────────────────────────────────────────────────
231
+ /**
232
+ * Re-inject taskId if stripped by stripLineageFields (PRI-272 / ERR-008).
233
+ * Only fill when absent via Object.hasOwn — present-but-falsy values
234
+ * must reach validation and fail loud (Runtime Contract Rule 3).
235
+ */
508
236
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
509
- mapRunStatusToErrorCategory(status) {
510
- switch (status) {
511
- case 'failed': return 'execution_failed';
512
- case 'timed_out': return 'timeout';
513
- case 'cancelled': return 'cancelled';
514
- default: return 'execution_failed';
515
- }
516
- }
517
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
518
- sleep(ms) {
519
- return new Promise((resolve) => setTimeout(resolve, ms));
237
+ postFetchTransform(taskId, untrustedOutput) {
238
+ injectRunnerLineageIfAbsent(untrustedOutput, 'taskId', taskId);
239
+ }
240
+ emitSuccessTelemetry(taskId, output) {
241
+ this.emitEvent('implementation_plan_generated', taskId, {
242
+ implementationSummary: output.implementationPlan.summary,
243
+ targetSurface: output.implementationPlan.targetSurface,
244
+ confidence: output.implementationPlan.confidence,
245
+ });
520
246
  }
521
247
  }
522
248
  export { DEFAULT_ARTIFICER_RUNNER_OPTIONS };