@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4
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/docs/http-api.md +80 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +443 -32
- package/src/api/index.ts +108 -127
- package/src/cli/__tests__/analyze-task.test.ts +40 -7
- package/src/cli/__tests__/index.test.ts +337 -17
- package/src/cli/__tests__/types.test.ts +0 -18
- package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
- package/src/cli/analyze-task.ts +3 -14
- package/src/cli/index.ts +73 -61
- package/src/cli/types.ts +0 -13
- package/src/cli/update-pipeline-json.ts +3 -14
- package/src/config/__tests__/paths.test.ts +46 -1
- package/src/config/__tests__/pipeline-registry.test.ts +767 -0
- package/src/config/__tests__/sse-events.test.ts +470 -0
- package/src/config/__tests__/statuses.test.ts +41 -0
- package/src/config/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/config/statuses.ts +20 -1
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +545 -105
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +260 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +1341 -0
- package/src/core/__tests__/json-file.test.ts +111 -0
- package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
- package/src/core/__tests__/logger.test.ts +22 -0
- package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
- package/src/core/__tests__/orchestrator.test.ts +615 -2
- package/src/core/__tests__/pipeline-runner.test.ts +946 -9
- package/src/core/__tests__/redact.test.ts +153 -0
- package/src/core/__tests__/runner-liveness.test.ts +910 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- package/src/core/__tests__/task-runner.test.ts +594 -3
- package/src/core/__tests__/task-telemetry.test.ts +241 -0
- package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
- package/src/core/agent-step.ts +4 -1
- package/src/core/agent-types.ts +5 -4
- package/src/core/config.ts +134 -222
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +56 -20
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +473 -0
- package/src/core/json-file.ts +45 -0
- package/src/core/lifecycle-policy.ts +23 -8
- package/src/core/logger.ts +0 -29
- package/src/core/orchestrator.ts +200 -74
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +280 -0
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +27 -33
- package/src/core/task-runner.ts +356 -319
- package/src/core/task-telemetry.ts +107 -0
- package/src/harness/__tests__/subprocess.test.ts +112 -1
- package/src/harness/subprocess.ts +55 -16
- package/src/llm/__tests__/index.test.ts +684 -33
- package/src/llm/index.ts +310 -67
- package/src/providers/__tests__/alibaba.test.ts +67 -6
- package/src/providers/__tests__/anthropic.test.ts +35 -14
- package/src/providers/__tests__/base.test.ts +62 -0
- package/src/providers/__tests__/claude-code.test.ts +99 -14
- package/src/providers/__tests__/deepseek.test.ts +16 -6
- package/src/providers/__tests__/gemini.test.ts +47 -25
- package/src/providers/__tests__/moonshot.test.ts +27 -0
- package/src/providers/__tests__/openai.test.ts +262 -74
- package/src/providers/__tests__/opencode.test.ts +77 -0
- package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
- package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
- package/src/providers/__tests__/types.test.ts +85 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +27 -14
- package/src/providers/alibaba.ts +20 -39
- package/src/providers/anthropic.ts +23 -11
- package/src/providers/base.ts +19 -0
- package/src/providers/claude-code.ts +27 -18
- package/src/providers/deepseek.ts +9 -28
- package/src/providers/gemini.ts +20 -58
- package/src/providers/moonshot.ts +15 -11
- package/src/providers/openai.ts +79 -61
- package/src/providers/opencode.ts +16 -33
- package/src/providers/stream-accumulator.ts +27 -21
- package/src/providers/types.ts +29 -4
- package/src/providers/zhipu.ts +15 -44
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
- package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
- package/src/task-analysis/__tests__/repository.test.ts +65 -0
- package/src/task-analysis/__tests__/types.test.ts +63 -130
- package/src/task-analysis/analyzer.ts +91 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
- package/src/task-analysis/index.ts +2 -36
- package/src/task-analysis/repository.ts +45 -0
- package/src/task-analysis/types.ts +42 -58
- package/src/ui/client/__tests__/api.test.ts +143 -1
- package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
- package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
- package/src/ui/client/__tests__/load-state.test.ts +70 -0
- package/src/ui/client/__tests__/types.test.ts +66 -3
- package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
- package/src/ui/client/__tests__/useJobList.test.ts +198 -23
- package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
- package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
- package/src/ui/client/adapters/job-adapter.ts +41 -16
- package/src/ui/client/api.ts +38 -15
- package/src/ui/client/bootstrap.ts +19 -14
- package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
- package/src/ui/client/hooks/useJobList.ts +26 -31
- package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
- package/src/ui/client/load-state.ts +20 -0
- package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
- package/src/ui/client/reducers/job-events.ts +137 -0
- package/src/ui/client/types.ts +16 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +41 -13
- package/src/ui/components/StageTimeline.tsx +8 -20
- package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
- package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
- package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
- package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
- package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
- package/src/ui/components/types.ts +35 -15
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
- package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
- package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/pages/PipelineDetail.tsx +60 -4
- package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
- package/src/ui/pages/__tests__/pages.test.tsx +236 -42
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
- package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
- package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
- package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
- package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
- package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
- package/src/ui/server/__tests__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
- package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
- package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
- package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
- package/src/ui/server/__tests__/router-threading.test.ts +148 -0
- package/src/ui/server/__tests__/router.test.ts +104 -0
- package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
- package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/config-bridge.ts +3 -4
- package/src/ui/server/embedded-assets-imports.d.ts +44 -0
- package/src/ui/server/embedded-assets.ts +13 -2
- package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
- package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
- package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
- package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
- package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
- package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
- package/src/ui/server/endpoints/file-endpoints.ts +15 -10
- package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
- package/src/ui/server/endpoints/job-endpoints.ts +7 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
- package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
- package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
- package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
- package/src/ui/server/index.ts +19 -14
- package/src/ui/server/job-reader.ts +14 -2
- package/src/ui/server/router.ts +33 -10
- package/src/ui/server/sse-broadcast.ts +14 -3
- package/src/ui/server/sse-enhancer.ts +12 -2
- package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
- package/src/ui/state/__tests__/snapshot.test.ts +120 -14
- package/src/ui/state/__tests__/types.test.ts +104 -5
- package/src/ui/state/snapshot.ts +2 -2
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
- package/src/ui/state/transformers/list-transformer.ts +13 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +15 -48
- package/src/utils/__tests__/path-containment.test.ts +160 -0
- package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
- package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
- package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
- package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
- package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
- package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
- package/src/task-analysis/__tests__/index.test.ts +0 -143
- package/src/task-analysis/__tests__/parser.test.ts +0 -41
- package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
- package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
- package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
- package/src/task-analysis/extractors/artifacts.ts +0 -143
- package/src/task-analysis/extractors/llm-calls.ts +0 -117
- package/src/task-analysis/extractors/stages.ts +0 -45
- package/src/task-analysis/parser.ts +0 -20
- package/src/task-analysis/utils/ast.ts +0 -45
- package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
- package/src/ui/embedded-assets.js +0 -12
- package/src/ui/server/__tests__/path-containment.test.ts +0 -54
package/src/core/task-runner.ts
CHANGED
|
@@ -15,10 +15,11 @@ import { loadEnvironment } from "./environment";
|
|
|
15
15
|
import { createJobLogger } from "./logger";
|
|
16
16
|
import { createHighLevelLLM, createLLMWithOverride, getLLMEvents } from "../llm/index";
|
|
17
17
|
import { executeAgent } from "./agent-step";
|
|
18
|
+
import { withConsoleCapture, withTaskTelemetry } from "./task-telemetry";
|
|
18
19
|
import { TaskState } from "../config/statuses";
|
|
19
20
|
import { LogEvent, LogFileExtension } from "../config/log-events";
|
|
20
21
|
// LLMClient: src/llm/index.ts exports HighLevelLLM (no LLMClient type exists there).
|
|
21
|
-
import type { HighLevelLLM } from "../providers/types";
|
|
22
|
+
import type { HighLevelLLM, UsageSource } from "../providers/types";
|
|
22
23
|
import type { AgentStepResult, TaskAgentOptions, TaskAgentRunner } from "./agent-types";
|
|
23
24
|
|
|
24
25
|
/** Opaque alias — the LLM client passed into execution contexts. */
|
|
@@ -165,7 +166,15 @@ export interface LLMMetricRecord {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
/** Token usage tuple written to the job status file. */
|
|
168
|
-
export type TokenUsageTuple = [
|
|
169
|
+
export type TokenUsageTuple = [
|
|
170
|
+
modelKey: string,
|
|
171
|
+
inputTokens: number,
|
|
172
|
+
outputTokens: number,
|
|
173
|
+
cost: number,
|
|
174
|
+
source: UsageSource,
|
|
175
|
+
failed?: boolean,
|
|
176
|
+
costEstimated?: boolean,
|
|
177
|
+
];
|
|
169
178
|
|
|
170
179
|
// ─── Initial context ─────────────────────────────────────────────────────────
|
|
171
180
|
|
|
@@ -251,22 +260,84 @@ export function deriveModelKeyAndTokens(metric: Record<string, unknown>): TokenU
|
|
|
251
260
|
const rawCost = metric["cost"];
|
|
252
261
|
const cost = typeof rawCost === "number" && Number.isFinite(rawCost) ? rawCost : 0;
|
|
253
262
|
|
|
254
|
-
|
|
263
|
+
const rawSource = metric["usageSource"];
|
|
264
|
+
const source: UsageSource =
|
|
265
|
+
rawSource === "reported" ||
|
|
266
|
+
rawSource === "estimated" ||
|
|
267
|
+
rawSource === "unavailable" ||
|
|
268
|
+
rawSource === "zero"
|
|
269
|
+
? rawSource
|
|
270
|
+
: "reported";
|
|
271
|
+
|
|
272
|
+
const rawCostEstimated = metric["costEstimated"];
|
|
273
|
+
const costEstimated = typeof rawCostEstimated === "boolean"
|
|
274
|
+
? rawCostEstimated
|
|
275
|
+
: source !== "reported";
|
|
276
|
+
|
|
277
|
+
return [modelKey, inputTokens, outputTokens, cost, source, false, costEstimated];
|
|
255
278
|
}
|
|
256
279
|
|
|
257
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Builds a source-aware {@link TokenUsageTuple} from an agent step result.
|
|
282
|
+
*
|
|
283
|
+
* Classification (#18 `UsageSource` model — the agent lane never estimates
|
|
284
|
+
* tokens, so it keys on the result shape rather than a provider/harness table):
|
|
285
|
+
* - `result.usage` object present -> `"reported"` (even when token counts are
|
|
286
|
+
* zero); `costUsd` (when finite) is preserved as the cost, `costEstimated`
|
|
287
|
+
* stays `false`.
|
|
288
|
+
* - no `result.usage` but a finite `costUsd` -> `"unavailable"`: tokens are 0,
|
|
289
|
+
* the harness-reported `costUsd` is preserved as the cost, `costEstimated` is
|
|
290
|
+
* `true` (agent spend stops lying as measured).
|
|
291
|
+
* - neither `usage` nor `costUsd` -> `null` (nothing to record).
|
|
292
|
+
*
|
|
293
|
+
* `opts.failed` sets the tuple's `failed` flag (index 5).
|
|
294
|
+
*/
|
|
295
|
+
export function deriveAgentUsageTuple(
|
|
296
|
+
options: { harness: string; model?: TaskAgentOptions["model"] },
|
|
297
|
+
result: AgentStepResult,
|
|
298
|
+
opts?: { failed?: boolean },
|
|
299
|
+
): TokenUsageTuple | null {
|
|
258
300
|
const costUsd =
|
|
259
301
|
typeof result.costUsd === "number" && Number.isFinite(result.costUsd)
|
|
260
302
|
? result.costUsd
|
|
261
303
|
: undefined;
|
|
262
304
|
if (!result.usage && costUsd === undefined) return null;
|
|
263
305
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
306
|
+
const modelId =
|
|
307
|
+
typeof options.model === "string" ? options.model : options.model?.model;
|
|
308
|
+
const modelKey = `${options.harness}:${modelId ?? "default"}`;
|
|
309
|
+
const failed = opts?.failed === true;
|
|
310
|
+
|
|
311
|
+
if (result.usage) {
|
|
312
|
+
return [
|
|
313
|
+
modelKey,
|
|
314
|
+
result.usage.inputTokens ?? 0,
|
|
315
|
+
result.usage.outputTokens ?? 0,
|
|
316
|
+
costUsd ?? 0,
|
|
317
|
+
"reported",
|
|
318
|
+
failed,
|
|
319
|
+
false,
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return [modelKey, 0, 0, costUsd ?? 0, "unavailable", failed, true];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Records agent usage by building a {@link TokenUsageTuple} via
|
|
328
|
+
* {@link deriveAgentUsageTuple} and pushing it through the injected `push`
|
|
329
|
+
* sink only when a tuple exists. The skip-on-null decision lives here, so
|
|
330
|
+
* callers never branch on null. Owns the recording decision, not the storage
|
|
331
|
+
* mechanism (the persistence primitive is injected as `push`).
|
|
332
|
+
*/
|
|
333
|
+
export function recordAgentUsage(
|
|
334
|
+
options: { harness: string; model?: TaskAgentOptions["model"] },
|
|
335
|
+
result: AgentStepResult,
|
|
336
|
+
push: (tuple: TokenUsageTuple) => void,
|
|
337
|
+
opts?: { failed?: boolean },
|
|
338
|
+
): void {
|
|
339
|
+
const tuple = deriveAgentUsageTuple(options, result, opts);
|
|
340
|
+
if (tuple) push(tuple);
|
|
270
341
|
}
|
|
271
342
|
|
|
272
343
|
// ─── Safe clone ─────────────────────────────────────────────────────────────
|
|
@@ -417,47 +488,8 @@ export function checkFlagTypeConflicts(
|
|
|
417
488
|
|
|
418
489
|
// ─── Console capture ──────────────────────────────────────────────────────────
|
|
419
490
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
* string buffer with appropriate prefixes. Returns a restore function that
|
|
423
|
-
* flushes the buffer to `logPath` via Bun.write() and restores originals.
|
|
424
|
-
* Creates the log directory before capturing.
|
|
425
|
-
*/
|
|
426
|
-
export function captureConsoleOutput(logPath: string): () => Promise<void> {
|
|
427
|
-
const origLog = console.log;
|
|
428
|
-
const origError = console.error;
|
|
429
|
-
const origWarn = console.warn;
|
|
430
|
-
const origInfo = console.info;
|
|
431
|
-
const origDebug = console.debug;
|
|
432
|
-
|
|
433
|
-
let buffer = "";
|
|
434
|
-
|
|
435
|
-
const append = (prefix: string, args: unknown[]) => {
|
|
436
|
-
buffer += prefix + args.map(String).join(" ") + "\n";
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
440
|
-
console.log = (...args: any[]) => append("", args);
|
|
441
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
442
|
-
console.error = (...args: any[]) => append("[ERROR] ", args);
|
|
443
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
444
|
-
console.warn = (...args: any[]) => append("[WARN] ", args);
|
|
445
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
446
|
-
console.info = (...args: any[]) => append("[INFO] ", args);
|
|
447
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
448
|
-
console.debug = (...args: any[]) => append("[DEBUG] ", args);
|
|
449
|
-
|
|
450
|
-
return async () => {
|
|
451
|
-
console.log = origLog;
|
|
452
|
-
console.error = origError;
|
|
453
|
-
console.warn = origWarn;
|
|
454
|
-
console.info = origInfo;
|
|
455
|
-
console.debug = origDebug;
|
|
456
|
-
|
|
457
|
-
await mkdir(dirname(logPath), { recursive: true });
|
|
458
|
-
await Bun.write(logPath, buffer);
|
|
459
|
-
};
|
|
460
|
-
}
|
|
491
|
+
// `captureConsoleOutput` moved to ./task-telemetry; re-exported below so existing
|
|
492
|
+
// importers keep working. The per-stage usage at line ~733 is replaced in step 4.
|
|
461
493
|
|
|
462
494
|
// ─── Pipeline stage factory ───────────────────────────────────────────────────
|
|
463
495
|
|
|
@@ -532,6 +564,11 @@ export async function runPipeline(
|
|
|
532
564
|
let tokenWriteQueue: Promise<void> = Promise.resolve();
|
|
533
565
|
const events = getLLMEvents();
|
|
534
566
|
|
|
567
|
+
// `context` is built inside the withTaskTelemetry body below, but the LLM
|
|
568
|
+
// handlers (defined here so they can be passed into the wrapper) read
|
|
569
|
+
// `context.currentStage`. Declare it at this scope so both see the same value.
|
|
570
|
+
let context: ExecutionContext;
|
|
571
|
+
|
|
535
572
|
const onComplete = (metric: Record<string, unknown>) => {
|
|
536
573
|
const record: LLMMetricRecord = {
|
|
537
574
|
...metric,
|
|
@@ -564,304 +601,303 @@ export async function runPipeline(
|
|
|
564
601
|
stage: context.currentStage ?? undefined,
|
|
565
602
|
failed: true,
|
|
566
603
|
});
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
events.on("llm:request:complete", onComplete);
|
|
570
|
-
events.on("llm:request:error", onError);
|
|
571
|
-
|
|
572
|
-
// 7. Load task module
|
|
573
|
-
const taskModule = await loadFreshModule(pathToFileURL(modulePath));
|
|
574
604
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
// 9. Create file I/O adapter
|
|
579
|
-
let currentStageRef: StageName | null = null;
|
|
580
|
-
const io = createTaskFileIO({
|
|
581
|
-
workDir,
|
|
582
|
-
taskName,
|
|
583
|
-
getStage: () => currentStageRef ?? "",
|
|
584
|
-
statusPath,
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// 9b. Build the CLI-agent harness runner injected into stages. It reuses the
|
|
588
|
-
// task's own `io`, so an agent reads and writes the same artifacts the task
|
|
589
|
-
// sees. A caller may override it (tests/custom hosts).
|
|
590
|
-
const runAgentCore: TaskAgentRunner =
|
|
591
|
-
initialContext.runAgent ??
|
|
592
|
-
((options: TaskAgentOptions) =>
|
|
593
|
-
executeAgent({ io, entry: { ...options, name: taskName } }));
|
|
594
|
-
const runAgent: TaskAgentRunner = async (options: TaskAgentOptions) => {
|
|
595
|
-
const result = await runAgentCore(options);
|
|
596
|
-
const tuple = deriveAgentUsageTuple(options, result);
|
|
597
|
-
if (tuple) enqueueTokenUsage(tuple);
|
|
598
|
-
return result;
|
|
605
|
+
const tuple = deriveModelKeyAndTokens(metric);
|
|
606
|
+
tuple[5] = true;
|
|
607
|
+
enqueueTokenUsage(tuple);
|
|
599
608
|
};
|
|
600
609
|
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
610
|
+
// INVARIANT: ONE runPipeline at a time per process (sequential stages, one
|
|
611
|
+
// runner per job; see task-telemetry.ts). withTaskTelemetry attaches the two
|
|
612
|
+
// LLM listeners, runs the body below, and in `finally` removes both listeners
|
|
613
|
+
// and flushes the latest tokenWriteQueue (thunked) on every exit — return or
|
|
614
|
+
// throw. The body owns module-load through the final return.
|
|
615
|
+
return withTaskTelemetry(events, { onComplete, onError }, () => tokenWriteQueue, async () => {
|
|
616
|
+
// 7. Load task module
|
|
617
|
+
const taskModule = await loadFreshModule(pathToFileURL(modulePath));
|
|
618
|
+
|
|
619
|
+
// 8. Create per-invocation stage array
|
|
620
|
+
const stages = createPipelineStages(taskModule, initialContext.tasksOverride);
|
|
621
|
+
|
|
622
|
+
// 9. Create file I/O adapter
|
|
623
|
+
let currentStageRef: StageName | null = null;
|
|
624
|
+
const io = createTaskFileIO({
|
|
615
625
|
workDir,
|
|
626
|
+
taskName,
|
|
627
|
+
getStage: () => currentStageRef ?? "",
|
|
616
628
|
statusPath,
|
|
617
|
-
|
|
618
|
-
envLoaded: true,
|
|
619
|
-
modelConfig: initialContext.modelConfig,
|
|
620
|
-
pipelineTasks,
|
|
621
|
-
},
|
|
622
|
-
data: {
|
|
623
|
-
seed: seedData,
|
|
624
|
-
},
|
|
625
|
-
flags: {},
|
|
626
|
-
logs: [],
|
|
627
|
-
currentStage: null,
|
|
628
|
-
validators: { validateWithSchema },
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
// 11. Ensure log directory exists
|
|
632
|
-
const logsDir = join(workDir, "files", "logs");
|
|
633
|
-
await mkdir(logsDir, { recursive: true });
|
|
634
|
-
const registerStageLog = async (fileName: string): Promise<void> => {
|
|
635
|
-
await trackFile(jobDir, "logs", fileName, taskName, true);
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
// Track returned logs (separate from context.logs)
|
|
639
|
-
const returnedLogs: AuditLogEntry[] = [];
|
|
640
|
-
let lastStageOutput: unknown = context.data["seed"];
|
|
641
|
-
let previousStage = "seed";
|
|
642
|
-
|
|
643
|
-
// 12. Execute each stage sequentially
|
|
644
|
-
for (const stage of stages) {
|
|
645
|
-
const stageName = stage.name;
|
|
646
|
-
|
|
647
|
-
// Check skipIf -> skip if true (log to context.logs only)
|
|
648
|
-
if (stage.skipIf && stage.skipIf(context.flags)) {
|
|
649
|
-
context.logs.push({ stage: stageName, skipped: true });
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Check handler -> skip if null (log to returned logs)
|
|
654
|
-
if (stage.handler === null) {
|
|
655
|
-
returnedLogs.push({ stage: stageName, skipped: true });
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Capture console output
|
|
660
|
-
const logFileName = generateLogName(taskName, stageName, LogEvent.START, LogFileExtension.TEXT);
|
|
661
|
-
const logPath = join(logsDir, logFileName);
|
|
662
|
-
const restoreConsole = captureConsoleOutput(logPath);
|
|
629
|
+
});
|
|
663
630
|
|
|
664
|
-
|
|
631
|
+
// 9b. Build the CLI-agent harness runner injected into stages. It reuses the
|
|
632
|
+
// task's own `io`, so an agent reads and writes the same artifacts the task
|
|
633
|
+
// sees. A caller may override it (tests/custom hosts).
|
|
634
|
+
const runAgentCore: TaskAgentRunner =
|
|
635
|
+
initialContext.runAgent ??
|
|
636
|
+
((options: TaskAgentOptions) =>
|
|
637
|
+
executeAgent({ io, entry: { ...options, name: taskName } }));
|
|
638
|
+
const runAgent: TaskAgentRunner = async (options: TaskAgentOptions) => {
|
|
639
|
+
const result = await runAgentCore(options);
|
|
640
|
+
recordAgentUsage(options, result, enqueueTokenUsage);
|
|
641
|
+
return result;
|
|
642
|
+
};
|
|
665
643
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
context.currentStage = stageName;
|
|
669
|
-
currentStageRef = stageName;
|
|
644
|
+
// 10. Build execution context
|
|
645
|
+
const pipelineTasks = initialContext.pipelineTasks ?? initialContext.meta?.pipelineTasks;
|
|
670
646
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
snapshot.tasks[taskName]!.state = TaskState.RUNNING;
|
|
676
|
-
snapshot.tasks[taskName]!.currentStage = stageName;
|
|
677
|
-
});
|
|
678
|
-
} catch {
|
|
679
|
-
// Best-effort: swallow status write errors
|
|
680
|
-
}
|
|
647
|
+
// Seed handling: if initialContext.seed is falsy, use initialContext itself.
|
|
648
|
+
// JSON round-trip strips non-cloneable fields (functions, proxies) which matches
|
|
649
|
+
// the original JS behavior and ensures structuredClone works later.
|
|
650
|
+
const seedData = initialContext.seed || JSON.parse(JSON.stringify(initialContext));
|
|
681
651
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
io,
|
|
688
|
-
llm,
|
|
689
|
-
runAgent,
|
|
690
|
-
meta: context.meta,
|
|
691
|
-
data: safeClone(context.data),
|
|
692
|
-
flags: safeClone(context.flags),
|
|
693
|
-
currentStage: stageName,
|
|
694
|
-
output: safeClone(lastStageOutput),
|
|
695
|
-
previousStage,
|
|
696
|
-
validators: context.validators,
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
// Write context snapshot
|
|
700
|
-
const snapshotFileName = generateLogName(
|
|
652
|
+
context = {
|
|
653
|
+
io,
|
|
654
|
+
llm,
|
|
655
|
+
runAgent,
|
|
656
|
+
meta: {
|
|
701
657
|
taskName,
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// Execute handler, time with performance.now()
|
|
718
|
-
const t0 = performance.now();
|
|
719
|
-
const result = await stage.handler(stageContext);
|
|
720
|
-
const elapsed = performance.now() - t0;
|
|
658
|
+
workDir,
|
|
659
|
+
statusPath,
|
|
660
|
+
jobId: initialContext.jobId,
|
|
661
|
+
envLoaded: true,
|
|
662
|
+
modelConfig: initialContext.modelConfig,
|
|
663
|
+
pipelineTasks,
|
|
664
|
+
},
|
|
665
|
+
data: {
|
|
666
|
+
seed: seedData,
|
|
667
|
+
},
|
|
668
|
+
flags: {},
|
|
669
|
+
logs: [],
|
|
670
|
+
currentStage: null,
|
|
671
|
+
validators: { validateWithSchema },
|
|
672
|
+
};
|
|
721
673
|
|
|
722
|
-
|
|
723
|
-
|
|
674
|
+
// 11. Ensure log directory exists
|
|
675
|
+
const logsDir = join(workDir, "files", "logs");
|
|
676
|
+
await mkdir(logsDir, { recursive: true });
|
|
677
|
+
const registerStageLog = async (fileName: string): Promise<void> => {
|
|
678
|
+
await trackFile(jobDir, "logs", fileName, taskName, true);
|
|
679
|
+
};
|
|
724
680
|
|
|
725
|
-
|
|
726
|
-
|
|
681
|
+
// Track returned logs (separate from context.logs)
|
|
682
|
+
const returnedLogs: AuditLogEntry[] = [];
|
|
683
|
+
let lastStageOutput: unknown = context.data["seed"];
|
|
684
|
+
let previousStage = "seed";
|
|
727
685
|
|
|
728
|
-
|
|
729
|
-
|
|
686
|
+
// 12. Execute each stage sequentially
|
|
687
|
+
for (const stage of stages) {
|
|
688
|
+
const stageName = stage.name;
|
|
730
689
|
|
|
731
|
-
//
|
|
732
|
-
|
|
690
|
+
// Check skipIf -> skip if true (log to context.logs only)
|
|
691
|
+
if (stage.skipIf && stage.skipIf(context.flags)) {
|
|
692
|
+
context.logs.push({ stage: stageName, skipped: true });
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
733
695
|
|
|
734
|
-
//
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
696
|
+
// Check handler -> skip if null (log to returned logs)
|
|
697
|
+
if (stage.handler === null) {
|
|
698
|
+
returnedLogs.push({ stage: stageName, skipped: true });
|
|
699
|
+
continue;
|
|
738
700
|
}
|
|
739
701
|
|
|
740
|
-
//
|
|
741
|
-
|
|
702
|
+
// Capture console output
|
|
703
|
+
const logFileName = generateLogName(taskName, stageName, LogEvent.START, LogFileExtension.TEXT);
|
|
704
|
+
const logPath = join(logsDir, logFileName);
|
|
742
705
|
|
|
743
|
-
|
|
744
|
-
const auditEntry: AuditLogEntry = { stage: stageName, ok: true, ms: elapsed };
|
|
745
|
-
context.logs.push(auditEntry);
|
|
746
|
-
returnedLogs.push(auditEntry);
|
|
706
|
+
let stageError: { err: unknown } | null = null;
|
|
747
707
|
|
|
748
|
-
//
|
|
708
|
+
// withConsoleCapture restores every captured console method (and writes the
|
|
709
|
+
// buffered log file) in its own finally on both success and throw, then
|
|
710
|
+
// re-throws — so a stage throw is captured here as stageError. registerStageLog
|
|
711
|
+
// then runs once on every path, after console restore, matching the original
|
|
712
|
+
// "finally restores console then registers log" ordering.
|
|
749
713
|
try {
|
|
750
|
-
await
|
|
751
|
-
|
|
752
|
-
|
|
714
|
+
await withConsoleCapture(logPath, async () => {
|
|
715
|
+
// Set currentStage
|
|
716
|
+
context.currentStage = stageName;
|
|
717
|
+
currentStageRef = stageName;
|
|
718
|
+
|
|
719
|
+
// Write stage-start status (swallow errors)
|
|
720
|
+
try {
|
|
721
|
+
await writeJobStatus(jobDir, (snapshot: StatusSnapshot) => {
|
|
722
|
+
if (!snapshot.tasks[taskName]) snapshot.tasks[taskName] = {};
|
|
723
|
+
snapshot.tasks[taskName]!.state = TaskState.RUNNING;
|
|
724
|
+
snapshot.tasks[taskName]!.currentStage = stageName;
|
|
725
|
+
});
|
|
726
|
+
} catch {
|
|
727
|
+
// Best-effort: swallow status write errors
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Clone data, flags, output into StageContext.
|
|
731
|
+
// structuredClone throws on non-cloneable values (functions, streams,
|
|
732
|
+
// class instances with internal slots). Fall back to JSON round-trip
|
|
733
|
+
// which silently strips those fields.
|
|
734
|
+
const stageContext: StageContext = {
|
|
735
|
+
io,
|
|
736
|
+
llm,
|
|
737
|
+
runAgent,
|
|
738
|
+
meta: context.meta,
|
|
739
|
+
data: safeClone(context.data),
|
|
740
|
+
flags: safeClone(context.flags),
|
|
741
|
+
currentStage: stageName,
|
|
742
|
+
output: safeClone(lastStageOutput),
|
|
743
|
+
previousStage,
|
|
744
|
+
validators: context.validators,
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// Write context snapshot
|
|
748
|
+
const snapshotFileName = generateLogName(
|
|
749
|
+
taskName,
|
|
750
|
+
stageName,
|
|
751
|
+
LogEvent.CONTEXT,
|
|
752
|
+
LogFileExtension.JSON,
|
|
753
|
+
);
|
|
754
|
+
const snapshotPath = join(logsDir, snapshotFileName);
|
|
755
|
+
try {
|
|
756
|
+
await Bun.write(snapshotPath, JSON.stringify(stageContext, null, 2));
|
|
757
|
+
await registerStageLog(snapshotFileName);
|
|
758
|
+
} catch {
|
|
759
|
+
// Best-effort
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Validate prerequisite flags
|
|
763
|
+
validateFlagTypes(stageName, context.flags, "requires");
|
|
764
|
+
|
|
765
|
+
// Execute handler, time with performance.now()
|
|
766
|
+
const t0 = performance.now();
|
|
767
|
+
const result = await stage.handler!(stageContext);
|
|
768
|
+
const elapsed = performance.now() - t0;
|
|
769
|
+
|
|
770
|
+
// Validate result shape
|
|
771
|
+
assertStageResult(result, stageName);
|
|
772
|
+
|
|
773
|
+
// Validate produced flag types
|
|
774
|
+
validateFlagTypes(stageName, result.flags, "produces");
|
|
775
|
+
|
|
776
|
+
// Check flag type conflicts
|
|
777
|
+
checkFlagTypeConflicts(context.flags, result.flags);
|
|
778
|
+
|
|
779
|
+
// Store output in context.data[stageName]
|
|
780
|
+
context.data[stageName] = result.output;
|
|
781
|
+
|
|
782
|
+
// Update lastStageOutput if not a validation stage
|
|
783
|
+
if (!VALIDATION_STAGES.has(stageName)) {
|
|
784
|
+
lastStageOutput = result.output;
|
|
785
|
+
previousStage = stageName;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Merge flags
|
|
789
|
+
Object.assign(context.flags, result.flags);
|
|
790
|
+
|
|
791
|
+
// Log completion audit entry
|
|
792
|
+
const auditEntry: AuditLogEntry = { stage: stageName, ok: true, ms: elapsed };
|
|
793
|
+
context.logs.push(auditEntry);
|
|
794
|
+
returnedLogs.push(auditEntry);
|
|
795
|
+
|
|
796
|
+
// Write stage-completion status (swallow errors)
|
|
797
|
+
try {
|
|
798
|
+
await writeJobStatus(jobDir, (snapshot: StatusSnapshot) => {
|
|
799
|
+
if (!snapshot.tasks[taskName]) snapshot.tasks[taskName] = {};
|
|
800
|
+
snapshot.tasks[taskName]!.currentStage = stageName;
|
|
801
|
+
});
|
|
802
|
+
} catch {
|
|
803
|
+
// Best-effort
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Write completion log marker
|
|
807
|
+
const completeFileName = generateLogName(
|
|
808
|
+
taskName,
|
|
809
|
+
stageName,
|
|
810
|
+
LogEvent.COMPLETE,
|
|
811
|
+
LogFileExtension.TEXT,
|
|
812
|
+
);
|
|
813
|
+
try {
|
|
814
|
+
await Bun.write(join(logsDir, completeFileName), `Stage ${stageName} completed in ${elapsed.toFixed(1)}ms\n`);
|
|
815
|
+
await registerStageLog(completeFileName);
|
|
816
|
+
} catch {
|
|
817
|
+
// Best-effort
|
|
818
|
+
}
|
|
753
819
|
});
|
|
754
|
-
} catch {
|
|
755
|
-
//
|
|
820
|
+
} catch (err: unknown) {
|
|
821
|
+
// 13. On stage error — capture error; withConsoleCapture already restored console
|
|
822
|
+
stageError = { err };
|
|
756
823
|
}
|
|
757
824
|
|
|
758
|
-
//
|
|
759
|
-
const completeFileName = generateLogName(
|
|
760
|
-
taskName,
|
|
761
|
-
stageName,
|
|
762
|
-
LogEvent.COMPLETE,
|
|
763
|
-
LogFileExtension.TEXT,
|
|
764
|
-
);
|
|
765
|
-
try {
|
|
766
|
-
await Bun.write(join(logsDir, completeFileName), `Stage ${stageName} completed in ${elapsed.toFixed(1)}ms\n`);
|
|
767
|
-
await registerStageLog(completeFileName);
|
|
768
|
-
} catch {
|
|
769
|
-
// Best-effort
|
|
770
|
-
}
|
|
771
|
-
} catch (err: unknown) {
|
|
772
|
-
// 13. On stage error — capture error, let finally restore console
|
|
773
|
-
stageError = { err };
|
|
774
|
-
} finally {
|
|
775
|
-
// Always restore console, regardless of success or failure
|
|
776
|
-
await restoreConsole();
|
|
825
|
+
// Register the stage start log on every path, after console restore.
|
|
777
826
|
await registerStageLog(logFileName).catch(() => {});
|
|
778
|
-
}
|
|
779
827
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
828
|
+
if (stageError !== null) {
|
|
829
|
+
const { err } = stageError;
|
|
830
|
+
const base = normalizeError(err);
|
|
831
|
+
const logFileName2 = generateLogName(taskName, stageName, LogEvent.START, LogFileExtension.TEXT);
|
|
832
|
+
const snapshotFileName2 = generateLogName(taskName, stageName, LogEvent.CONTEXT, LogFileExtension.JSON);
|
|
833
|
+
|
|
834
|
+
const normalized: NormalizedError = {
|
|
835
|
+
...base,
|
|
836
|
+
debug: {
|
|
837
|
+
stage: stageName,
|
|
838
|
+
previousStage,
|
|
839
|
+
logPath: join(logsDir, logFileName2),
|
|
840
|
+
snapshotPath: join(logsDir, snapshotFileName2),
|
|
841
|
+
dataHasSeed: "seed" in context.data,
|
|
842
|
+
seedHasData: context.data["seed"] != null && typeof context.data["seed"] === "object" && "data" in (context.data["seed"] as Record<string, unknown>),
|
|
843
|
+
flagsKeys: Object.keys(context.flags),
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const errorEntry: AuditLogEntry = { stage: stageName, ok: false, ms: 0, error: normalized };
|
|
848
|
+
context.logs.push(errorEntry);
|
|
849
|
+
returnedLogs.push(errorEntry);
|
|
850
|
+
|
|
851
|
+
// Write failure status (swallow errors)
|
|
852
|
+
try {
|
|
853
|
+
await writeJobStatus(jobDir, (snapshot: StatusSnapshot) => {
|
|
854
|
+
if (!snapshot.tasks[taskName]) snapshot.tasks[taskName] = {};
|
|
855
|
+
snapshot.tasks[taskName]!.state = TaskState.FAILED;
|
|
856
|
+
snapshot.tasks[taskName]!.failedStage = stageName;
|
|
857
|
+
snapshot.tasks[taskName]!.error = normalized.message;
|
|
858
|
+
});
|
|
859
|
+
} catch {
|
|
860
|
+
// Best-effort
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
ok: false,
|
|
865
|
+
failedStage: stageName,
|
|
866
|
+
error: normalized,
|
|
867
|
+
logs: returnedLogs,
|
|
868
|
+
context,
|
|
869
|
+
};
|
|
813
870
|
}
|
|
814
|
-
|
|
815
|
-
// Flush token write queue
|
|
816
|
-
await tokenWriteQueue.catch(() => {});
|
|
817
|
-
|
|
818
|
-
// Remove LLM listeners
|
|
819
|
-
events.removeListener("llm:request:complete", onComplete);
|
|
820
|
-
events.removeListener("llm:request:error", onError);
|
|
821
|
-
|
|
822
|
-
return {
|
|
823
|
-
ok: false,
|
|
824
|
-
failedStage: stageName,
|
|
825
|
-
error: normalized,
|
|
826
|
-
logs: returnedLogs,
|
|
827
|
-
context,
|
|
828
|
-
};
|
|
829
871
|
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// 14. After all stages: flush token write queue
|
|
833
|
-
await tokenWriteQueue.catch(() => {});
|
|
834
872
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
} catch {
|
|
854
|
-
// Best-effort
|
|
855
|
-
}
|
|
873
|
+
// 14. After all stages: write done status (best-effort).
|
|
874
|
+
// Token-queue flush + listener removal are handled by withTaskTelemetry's finally.
|
|
875
|
+
try {
|
|
876
|
+
const lastStage = KNOWN_STAGES.at(-1)!;
|
|
877
|
+
const doneProgress = computeDeterministicProgress(
|
|
878
|
+
pipelineTasks ?? [taskName],
|
|
879
|
+
taskName,
|
|
880
|
+
lastStage,
|
|
881
|
+
);
|
|
882
|
+
await writeJobStatus(jobDir, (snapshot: StatusSnapshot) => {
|
|
883
|
+
snapshot.progress = doneProgress;
|
|
884
|
+
if (!snapshot.tasks[taskName]) snapshot.tasks[taskName] = {};
|
|
885
|
+
snapshot.tasks[taskName]!.state = TaskState.DONE;
|
|
886
|
+
snapshot.tasks[taskName]!.currentStage = null;
|
|
887
|
+
});
|
|
888
|
+
} catch {
|
|
889
|
+
// Best-effort
|
|
890
|
+
}
|
|
856
891
|
|
|
857
|
-
|
|
892
|
+
context.currentStage = null;
|
|
858
893
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
894
|
+
return {
|
|
895
|
+
ok: true,
|
|
896
|
+
logs: returnedLogs,
|
|
897
|
+
context,
|
|
898
|
+
llmMetrics,
|
|
899
|
+
};
|
|
900
|
+
});
|
|
865
901
|
}
|
|
866
902
|
|
|
867
903
|
// ─── runPipelineWithModelRouting ──────────────────────────────────────────────
|
|
@@ -884,5 +920,6 @@ export async function runPipelineWithModelRouting(
|
|
|
884
920
|
|
|
885
921
|
// ─── Re-exports ───────────────────────────────────────────────────────────────
|
|
886
922
|
|
|
923
|
+
export { captureConsoleOutput } from "./task-telemetry";
|
|
887
924
|
export { decideTransition } from "./lifecycle-policy";
|
|
888
925
|
export { computeDeterministicProgress, KNOWN_STAGES } from "./progress";
|