@principles/core 1.85.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.
- package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js +52 -0
- package/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/telemetry-event.test.d.ts +2 -0
- package/dist/runtime-v2/__tests__/telemetry-event.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/telemetry-event.test.js +56 -0
- package/dist/runtime-v2/__tests__/telemetry-event.test.js.map +1 -0
- package/dist/runtime-v2/internalization/artificer-output.d.ts +3 -2
- package/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/artificer-output.js +40 -31
- package/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
- package/dist/runtime-v2/internalization/artificer-runner.d.ts +59 -38
- package/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/artificer-runner.js +121 -395
- package/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
- package/dist/telemetry-event.d.ts +2 -2
- package/dist/telemetry-event.d.ts.map +1 -1
- package/dist/telemetry-event.js +65 -0
- package/dist/telemetry-event.js.map +1 -1
- 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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
+
this.emitEvent('scribe_dep_selected', taskId, {
|
|
165
70
|
depTaskId: depId,
|
|
166
71
|
artifactId: firstArtifact.artifactId,
|
|
167
72
|
});
|
|
168
73
|
return {
|
|
169
|
-
contextHash:
|
|
74
|
+
contextHash: BasePeerRunner.hashContextRefs([artifactRef]),
|
|
170
75
|
scribeArtifact: firstArtifact.contentJson,
|
|
171
76
|
sourceScribeArtifactId: firstArtifact.artifactId,
|
|
172
77
|
};
|
|
173
78
|
}
|
|
174
79
|
}
|
|
175
|
-
this.
|
|
80
|
+
this.emitEvent('no_scribe_artifact', taskId, {});
|
|
176
81
|
return { contextHash: 'empty', scribeArtifact: null, sourceScribeArtifactId: null };
|
|
177
82
|
}
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
209
|
-
contextHash:
|
|
210
|
-
scribeArtifact:
|
|
211
|
-
sourceScribeArtifactId:
|
|
96
|
+
taskId,
|
|
97
|
+
contextHash: context.contextHash,
|
|
98
|
+
scribeArtifact: scribeArtifactInput,
|
|
99
|
+
sourceScribeArtifactId: context.sourceScribeArtifactId,
|
|
212
100
|
});
|
|
213
|
-
|
|
101
|
+
return this.runtimeAdapter.startRun({
|
|
214
102
|
agentSpec: { agentId: this.resolvedOptions.agentId, schemaVersion: 'v1' },
|
|
215
|
-
taskRef: { 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
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
await this.runtimeAdapter.cancelRun(runHandle.runId);
|
|
119
|
+
else if (isPDErrorCategory(rawCategory)) {
|
|
120
|
+
errorCategory = rawCategory;
|
|
236
121
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
130
|
+
return {
|
|
131
|
+
valid: result.valid,
|
|
132
|
+
errors: result.errors,
|
|
133
|
+
errorCategory,
|
|
134
|
+
};
|
|
246
135
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
}
|
|
254
|
-
async succeedTask(ctx) {
|
|
142
|
+
// Store output before marking succeeded
|
|
255
143
|
try {
|
|
256
|
-
await this.stateManager.updateRunOutput(
|
|
144
|
+
await this.stateManager.updateRunOutput(runId, JSON.stringify(output));
|
|
257
145
|
}
|
|
258
146
|
catch (updateErr) {
|
|
259
|
-
this.
|
|
260
|
-
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
|
-
|
|
266
|
-
const now = new Date().toISOString();
|
|
153
|
+
// Resolve lineage artifact IDs
|
|
267
154
|
let lineageArtifactIds = [];
|
|
155
|
+
let lineageHasRejected = false;
|
|
268
156
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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:
|
|
181
|
+
sourceTaskId: taskId,
|
|
286
182
|
lineageArtifactIds,
|
|
287
183
|
validationStatus: 'pending',
|
|
288
|
-
contentJson: JSON.stringify(
|
|
184
|
+
contentJson: JSON.stringify(output),
|
|
289
185
|
createdAt: now,
|
|
290
186
|
updatedAt: now,
|
|
291
187
|
});
|
|
292
188
|
}
|
|
293
189
|
catch (artifactErr) {
|
|
294
|
-
this.
|
|
295
|
-
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
|
|
300
|
-
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
|
-
|
|
201
|
+
// Mark task succeeded
|
|
202
|
+
const resultRef = `${this.config.resultRefPrefix}://${runId}`;
|
|
306
203
|
try {
|
|
307
|
-
await this.stateManager.markTaskSucceeded(
|
|
204
|
+
await this.stateManager.markTaskSucceeded(taskId, resultRef);
|
|
308
205
|
}
|
|
309
206
|
catch (stateErr) {
|
|
310
|
-
this.
|
|
311
|
-
taskId
|
|
312
|
-
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.
|
|
318
|
-
attemptCount:
|
|
214
|
+
this.emitEvent('task_succeeded', taskId, {
|
|
215
|
+
attemptCount: task.attemptCount,
|
|
319
216
|
resultRef,
|
|
320
|
-
implementationSummary:
|
|
217
|
+
implementationSummary: output.implementationPlan.summary,
|
|
321
218
|
});
|
|
322
|
-
this.phase = RunnerPhase.Completed;
|
|
323
219
|
return {
|
|
324
220
|
status: 'succeeded',
|
|
325
|
-
taskId
|
|
326
|
-
runId
|
|
221
|
+
taskId,
|
|
222
|
+
runId,
|
|
327
223
|
artifactId,
|
|
328
224
|
resultRef,
|
|
329
|
-
contextHash
|
|
330
|
-
output
|
|
331
|
-
attemptCount:
|
|
225
|
+
contextHash,
|
|
226
|
+
output,
|
|
227
|
+
attemptCount: task.attemptCount,
|
|
332
228
|
};
|
|
333
229
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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 };
|