pi-crew 0.9.22 → 0.9.23
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/package.json +1 -1
- package/src/runtime/pipeline-runner.ts +116 -107
- package/src/state/event-log.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
2
|
import { errors } from "../errors.ts";
|
|
3
|
-
import { appendEventAsync } from "../state/event-log.ts";
|
|
3
|
+
import { appendEventAsync, flushEventLogBuffer } from "../state/event-log.ts";
|
|
4
4
|
import type { TeamTaskState } from "../state/types.ts";
|
|
5
5
|
import type { TeamConfig } from "../teams/team-config.ts";
|
|
6
6
|
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
@@ -107,142 +107,151 @@ export class PipelineRunner {
|
|
|
107
107
|
let previousResults: unknown[] = [];
|
|
108
108
|
const startTime = Date.now();
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
type: "pipeline:started",
|
|
112
|
-
runId,
|
|
113
|
-
message: `Pipeline '${workflow.name}' started`,
|
|
114
|
-
data: { stages: workflow.stages.map((s) => s.name) },
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
for (let i = 0; i < workflow.stages.length; i++) {
|
|
118
|
-
const stage = workflow.stages[i];
|
|
119
|
-
const stageStartTime = Date.now();
|
|
120
|
-
|
|
121
|
-
// Determine stop behavior for this stage
|
|
122
|
-
const effectiveStopOnError = stage.stopOnError ?? workflow.stopOnError ?? this.stopOnError;
|
|
123
|
-
|
|
110
|
+
try {
|
|
124
111
|
await appendEventAsync(eventsPath, {
|
|
125
|
-
type: "pipeline:
|
|
112
|
+
type: "pipeline:started",
|
|
126
113
|
runId,
|
|
127
|
-
message: `
|
|
128
|
-
data: {
|
|
114
|
+
message: `Pipeline '${workflow.name}' started`,
|
|
115
|
+
data: { stages: workflow.stages.map((s) => s.name) },
|
|
129
116
|
});
|
|
130
117
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
stageIndex: i,
|
|
135
|
-
stageName: stage.name,
|
|
136
|
-
previousResults,
|
|
137
|
-
totalStages: workflow.stages.length,
|
|
138
|
-
runId,
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Resolve inputs
|
|
142
|
-
const inputs = this.resolveInputs(stage.inputs, previousResults, context);
|
|
143
|
-
|
|
144
|
-
// Execute stage (handle fan-out if enabled)
|
|
145
|
-
const results = await this.executeStageInternal(stage, inputs, stageContext, executeStage);
|
|
118
|
+
for (let i = 0; i < workflow.stages.length; i++) {
|
|
119
|
+
const stage = workflow.stages[i];
|
|
120
|
+
const stageStartTime = Date.now();
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
name: stage.name,
|
|
150
|
-
status: "completed",
|
|
151
|
-
results,
|
|
152
|
-
duration,
|
|
153
|
-
fanOutItems: Array.isArray(inputs) ? inputs.length : undefined,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
previousResults = results;
|
|
122
|
+
// Determine stop behavior for this stage
|
|
123
|
+
const effectiveStopOnError = stage.stopOnError ?? workflow.stopOnError ?? this.stopOnError;
|
|
157
124
|
|
|
158
125
|
await appendEventAsync(eventsPath, {
|
|
159
|
-
type: "pipeline:
|
|
126
|
+
type: "pipeline:stage_started",
|
|
160
127
|
runId,
|
|
161
|
-
message: `Stage '${stage.name}'
|
|
162
|
-
data: {
|
|
163
|
-
stageIndex: i,
|
|
164
|
-
stageName: stage.name,
|
|
165
|
-
duration,
|
|
166
|
-
resultCount: results.length,
|
|
167
|
-
},
|
|
128
|
+
message: `Stage '${stage.name}' started`,
|
|
129
|
+
data: { stageIndex: i, stageName: stage.name },
|
|
168
130
|
});
|
|
169
|
-
} catch (error) {
|
|
170
|
-
const duration = Date.now() - stageStartTime;
|
|
171
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
172
131
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
await appendEventAsync(eventsPath, {
|
|
183
|
-
type: "pipeline:stage_failed",
|
|
132
|
+
try {
|
|
133
|
+
// Build stage context
|
|
134
|
+
const stageContext: PipelineContext = {
|
|
135
|
+
stageIndex: i,
|
|
136
|
+
stageName: stage.name,
|
|
137
|
+
previousResults,
|
|
138
|
+
totalStages: workflow.stages.length,
|
|
184
139
|
runId,
|
|
185
|
-
|
|
186
|
-
data: {
|
|
187
|
-
stageIndex: i,
|
|
188
|
-
stageName: stage.name,
|
|
189
|
-
duration,
|
|
190
|
-
error: errorMessage,
|
|
191
|
-
},
|
|
192
|
-
});
|
|
140
|
+
};
|
|
193
141
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
runId,
|
|
197
|
-
message: `Pipeline '${workflow.name}' failed at stage '${stage.name}'`,
|
|
198
|
-
data: { failedStage: stage.name, error: errorMessage },
|
|
199
|
-
});
|
|
142
|
+
// Resolve inputs
|
|
143
|
+
const inputs = this.resolveInputs(stage.inputs, previousResults, context);
|
|
200
144
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
status: "failed",
|
|
206
|
-
};
|
|
207
|
-
} else {
|
|
145
|
+
// Execute stage (handle fan-out if enabled)
|
|
146
|
+
const results = await this.executeStageInternal(stage, inputs, stageContext, executeStage);
|
|
147
|
+
|
|
148
|
+
const duration = Date.now() - stageStartTime;
|
|
208
149
|
stages.push({
|
|
209
150
|
name: stage.name,
|
|
210
|
-
status: "
|
|
211
|
-
results
|
|
212
|
-
error: errorMessage,
|
|
151
|
+
status: "completed",
|
|
152
|
+
results,
|
|
213
153
|
duration,
|
|
154
|
+
fanOutItems: Array.isArray(inputs) ? inputs.length : undefined,
|
|
214
155
|
});
|
|
215
156
|
|
|
157
|
+
previousResults = results;
|
|
158
|
+
|
|
216
159
|
await appendEventAsync(eventsPath, {
|
|
217
|
-
type: "pipeline:
|
|
160
|
+
type: "pipeline:stage_completed",
|
|
218
161
|
runId,
|
|
219
|
-
message: `Stage '${stage.name}'
|
|
162
|
+
message: `Stage '${stage.name}' completed`,
|
|
220
163
|
data: {
|
|
221
164
|
stageIndex: i,
|
|
222
165
|
stageName: stage.name,
|
|
223
166
|
duration,
|
|
224
|
-
|
|
167
|
+
resultCount: results.length,
|
|
225
168
|
},
|
|
226
169
|
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const duration = Date.now() - stageStartTime;
|
|
172
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
173
|
+
|
|
174
|
+
if (effectiveStopOnError) {
|
|
175
|
+
stages.push({
|
|
176
|
+
name: stage.name,
|
|
177
|
+
status: "failed",
|
|
178
|
+
results: [],
|
|
179
|
+
error: errorMessage,
|
|
180
|
+
duration,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await appendEventAsync(eventsPath, {
|
|
184
|
+
type: "pipeline:stage_failed",
|
|
185
|
+
runId,
|
|
186
|
+
message: `Stage '${stage.name}' failed: ${errorMessage}`,
|
|
187
|
+
data: {
|
|
188
|
+
stageIndex: i,
|
|
189
|
+
stageName: stage.name,
|
|
190
|
+
duration,
|
|
191
|
+
error: errorMessage,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await appendEventAsync(eventsPath, {
|
|
196
|
+
type: "pipeline:failed",
|
|
197
|
+
runId,
|
|
198
|
+
message: `Pipeline '${workflow.name}' failed at stage '${stage.name}'`,
|
|
199
|
+
data: { failedStage: stage.name, error: errorMessage },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
stages,
|
|
204
|
+
totalDuration: Date.now() - startTime,
|
|
205
|
+
finalResults: previousResults,
|
|
206
|
+
status: "failed",
|
|
207
|
+
};
|
|
208
|
+
} else {
|
|
209
|
+
stages.push({
|
|
210
|
+
name: stage.name,
|
|
211
|
+
status: "failed",
|
|
212
|
+
results: [],
|
|
213
|
+
error: errorMessage,
|
|
214
|
+
duration,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await appendEventAsync(eventsPath, {
|
|
218
|
+
type: "pipeline:stage_skipped",
|
|
219
|
+
runId,
|
|
220
|
+
message: `Stage '${stage.name}' skipped due to error`,
|
|
221
|
+
data: {
|
|
222
|
+
stageIndex: i,
|
|
223
|
+
stageName: stage.name,
|
|
224
|
+
duration,
|
|
225
|
+
error: errorMessage,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
227
229
|
}
|
|
228
230
|
}
|
|
229
|
-
}
|
|
230
231
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
232
|
+
await appendEventAsync(eventsPath, {
|
|
233
|
+
type: "pipeline:completed",
|
|
234
|
+
runId,
|
|
235
|
+
message: `Pipeline '${workflow.name}' completed`,
|
|
236
|
+
data: {
|
|
237
|
+
stages: stages.map((s) => ({ name: s.name, status: s.status })),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
return {
|
|
242
|
+
stages,
|
|
243
|
+
totalDuration: Date.now() - startTime,
|
|
244
|
+
finalResults: previousResults,
|
|
245
|
+
status: stages.some((s) => s.status === "failed") ? "partial" : "completed",
|
|
246
|
+
};
|
|
247
|
+
} finally {
|
|
248
|
+
// FIX (P0 follow-up): Drain the event-log buffer before returning so
|
|
249
|
+
// --test-force-exit does not detect "Promise resolution is still
|
|
250
|
+
// pending". The buffered flush uses a 20ms ref'd timer that fires
|
|
251
|
+
// asynchronously; without this explicit drain, the test runner sees
|
|
252
|
+
// pending appendEventAsync promises and cancels the test file.
|
|
253
|
+
await flushEventLogBuffer(eventsPath);
|
|
254
|
+
}
|
|
246
255
|
}
|
|
247
256
|
|
|
248
257
|
/**
|
package/src/state/event-log.ts
CHANGED
|
@@ -1013,6 +1013,30 @@ process.on("exit", () => {
|
|
|
1013
1013
|
}
|
|
1014
1014
|
asyncQueues.clear();
|
|
1015
1015
|
});
|
|
1016
|
+
|
|
1017
|
+
// FIX (P0 follow-up): Drain buffered events on `beforeExit` (async-aware).
|
|
1018
|
+
// The `exit` handler above is sync-only and cannot await the flush, leaving
|
|
1019
|
+
// pending promises that the test runner detects under --test-force-exit as
|
|
1020
|
+
// "Promise resolution is still pending but the event loop has already
|
|
1021
|
+
// resolved". `beforeExit` fires when the event loop drains naturally and
|
|
1022
|
+
// supports async handlers, so we can await the flush here and let the loop
|
|
1023
|
+
// drain cleanly before process.exit() is called.
|
|
1024
|
+
process.on("beforeExit", async () => {
|
|
1025
|
+
if (bufferedQueues.size > 0) {
|
|
1026
|
+
try {
|
|
1027
|
+
await flushEventLogBuffer();
|
|
1028
|
+
} catch {
|
|
1029
|
+
/* best-effort */
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (asyncQueues.size > 0) {
|
|
1033
|
+
try {
|
|
1034
|
+
await drainAsyncQueues();
|
|
1035
|
+
} catch {
|
|
1036
|
+
/* best-effort */
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1016
1040
|
process.on("SIGTERM", () => setImmediate(() => flushEventLogBuffer()));
|
|
1017
1041
|
process.on("SIGINT", () => setImmediate(() => flushEventLogBuffer()));
|
|
1018
1042
|
// FIX (Issue 1): Handle uncaught exceptions to flush buffered events before
|