@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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JOB_STATUS_SYNONYMS,
|
|
3
|
+
VALID_JOB_STATUSES,
|
|
4
|
+
deriveJobStatusFromTasks,
|
|
5
|
+
jobStateToStatus,
|
|
6
|
+
normalizeJobStatus,
|
|
7
|
+
normalizeTaskState,
|
|
8
|
+
} from "../config/statuses.ts";
|
|
9
|
+
import type { JobStatusValue, TaskStateValue } from "../config/statuses.ts";
|
|
10
|
+
import type { UsageSource } from "../providers/types.ts";
|
|
11
|
+
import type { GateInfo } from "./status-writer.ts";
|
|
12
|
+
|
|
13
|
+
export type RetryError = { message: string; name?: string; stack?: string } | string;
|
|
14
|
+
|
|
15
|
+
export interface JobViewError {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
stack?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface JobViewTask {
|
|
24
|
+
state: TaskStateValue;
|
|
25
|
+
name: string;
|
|
26
|
+
files: { artifacts: string[]; logs: string[]; tmp: string[] };
|
|
27
|
+
startedAt?: string | null;
|
|
28
|
+
endedAt?: string | null;
|
|
29
|
+
attempts?: number;
|
|
30
|
+
restartCount?: number;
|
|
31
|
+
retrying?: boolean;
|
|
32
|
+
nextRetryAt?: string;
|
|
33
|
+
lastRetryError?: RetryError | null;
|
|
34
|
+
executionTimeMs?: number;
|
|
35
|
+
refinementAttempts?: number;
|
|
36
|
+
stageLogPath?: string;
|
|
37
|
+
errorContext?: unknown;
|
|
38
|
+
currentStage?: string;
|
|
39
|
+
failedStage?: string;
|
|
40
|
+
artifacts?: unknown;
|
|
41
|
+
tokenUsage?: unknown[];
|
|
42
|
+
error?: { message: string; [key: string]: unknown } | null;
|
|
43
|
+
skipReason?: string;
|
|
44
|
+
skippedBy?: string;
|
|
45
|
+
controlApplied?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface JobView {
|
|
49
|
+
id: string;
|
|
50
|
+
jobId: string;
|
|
51
|
+
name: string;
|
|
52
|
+
title: string;
|
|
53
|
+
status: JobStatusValue;
|
|
54
|
+
progress: number;
|
|
55
|
+
createdAt: string | null;
|
|
56
|
+
updatedAt: string | null;
|
|
57
|
+
lastUpdated?: string;
|
|
58
|
+
location: string | null;
|
|
59
|
+
tasks: Record<string, JobViewTask>;
|
|
60
|
+
files: Record<string, unknown>;
|
|
61
|
+
costs: Record<string, unknown>;
|
|
62
|
+
gate?: GateInfo | null;
|
|
63
|
+
pipeline?: string;
|
|
64
|
+
pipelineLabel?: string;
|
|
65
|
+
pipelineConfig?: Record<string, unknown>;
|
|
66
|
+
current?: unknown;
|
|
67
|
+
currentStage?: unknown;
|
|
68
|
+
warnings?: string[];
|
|
69
|
+
readable?: boolean;
|
|
70
|
+
error?: JobViewError;
|
|
71
|
+
errorContext?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface TaskFilesView {
|
|
75
|
+
artifacts: string[];
|
|
76
|
+
logs: string[];
|
|
77
|
+
tmp: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const EMPTY_TASK_FILES: TaskFilesView = { artifacts: [], logs: [], tmp: [] };
|
|
81
|
+
const DEFAULT_JOB_ERROR_CODE = "JOB_ERROR";
|
|
82
|
+
|
|
83
|
+
export interface DerivedProgress {
|
|
84
|
+
taskCount: number;
|
|
85
|
+
doneCount: number;
|
|
86
|
+
completedCount: number;
|
|
87
|
+
progress: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function deriveProgress(
|
|
91
|
+
tasks: ReadonlyArray<{ state?: string }>,
|
|
92
|
+
configuredCount: number | null,
|
|
93
|
+
): DerivedProgress {
|
|
94
|
+
const actual = tasks.length;
|
|
95
|
+
const doneCount = tasks.filter((task) => task.state === "done").length;
|
|
96
|
+
const completedCount = tasks.filter(
|
|
97
|
+
(task) => task.state === "done" || task.state === "skipped",
|
|
98
|
+
).length;
|
|
99
|
+
const taskCount = Math.max(configuredCount ?? 0, actual);
|
|
100
|
+
const progress = taskCount === 0
|
|
101
|
+
? 0
|
|
102
|
+
: Math.min(100, Math.floor((completedCount / taskCount) * 100));
|
|
103
|
+
return { taskCount, doneCount, completedCount, progress };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const warnedFallbackKeys = new Set<string>();
|
|
107
|
+
|
|
108
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
109
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isRecognizedStatusValue(value: unknown): value is string {
|
|
113
|
+
if (typeof value !== "string") return false;
|
|
114
|
+
const normalized = value.toLowerCase().trim();
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(JOB_STATUS_SYNONYMS, normalized)) return true;
|
|
116
|
+
return VALID_JOB_STATUSES.has(normalized);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toStringOrNull(value: unknown): string | null {
|
|
120
|
+
return typeof value === "string" ? value : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toStringArray(value: unknown): string[] {
|
|
124
|
+
if (!Array.isArray(value)) return [];
|
|
125
|
+
return value.filter((entry): entry is string => typeof entry === "string");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toFiniteNumber(value: unknown): number | undefined {
|
|
129
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toTaskFiles(value: unknown): TaskFilesView {
|
|
133
|
+
if (!isRecord(value)) return { ...EMPTY_TASK_FILES };
|
|
134
|
+
return {
|
|
135
|
+
artifacts: toStringArray(value["artifacts"]),
|
|
136
|
+
logs: toStringArray(value["logs"]),
|
|
137
|
+
tmp: toStringArray(value["tmp"]),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toTaskError(value: unknown): JobViewTask["error"] {
|
|
142
|
+
if (value === null) return null;
|
|
143
|
+
if (typeof value === "string") return { message: value };
|
|
144
|
+
if (isRecord(value)) return value as { message: string; [key: string]: unknown };
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toGateInfo(value: unknown): GateInfo | null | undefined {
|
|
149
|
+
if (value === null) return null;
|
|
150
|
+
if (!isRecord(value)) return undefined;
|
|
151
|
+
const rawArtifacts = value["artifacts"];
|
|
152
|
+
const artifacts = Array.isArray(rawArtifacts)
|
|
153
|
+
? rawArtifacts.filter((entry): entry is string => typeof entry === "string")
|
|
154
|
+
: undefined;
|
|
155
|
+
const rawKind = value["kind"];
|
|
156
|
+
const kind = rawKind === "approval" || rawKind === "control_invalid" ? rawKind : undefined;
|
|
157
|
+
return {
|
|
158
|
+
afterTask: typeof value["afterTask"] === "string" ? value["afterTask"] : "",
|
|
159
|
+
message: typeof value["message"] === "string" ? value["message"] : "",
|
|
160
|
+
requestedAt: typeof value["requestedAt"] === "string" ? value["requestedAt"] : "",
|
|
161
|
+
...(kind ? { kind } : {}),
|
|
162
|
+
...(artifacts && artifacts.length > 0 ? { artifacts } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function toJobViewTask(name: string, value: unknown): JobViewTask {
|
|
167
|
+
const task = isRecord(value) ? value : {};
|
|
168
|
+
return {
|
|
169
|
+
name,
|
|
170
|
+
state: normalizeTaskState(task["state"]),
|
|
171
|
+
files: toTaskFiles(task["files"]),
|
|
172
|
+
startedAt: toStringOrNull(task["startedAt"]),
|
|
173
|
+
endedAt: toStringOrNull(task["endedAt"]),
|
|
174
|
+
attempts: typeof task["attempts"] === "number" ? task["attempts"] : undefined,
|
|
175
|
+
restartCount: typeof task["restartCount"] === "number" ? task["restartCount"] : undefined,
|
|
176
|
+
retrying: typeof task["retrying"] === "boolean" ? task["retrying"] : undefined,
|
|
177
|
+
nextRetryAt: typeof task["nextRetryAt"] === "string" ? task["nextRetryAt"] : undefined,
|
|
178
|
+
lastRetryError: Object.hasOwn(task, "lastRetryError")
|
|
179
|
+
? (task["lastRetryError"] as RetryError | null)
|
|
180
|
+
: undefined,
|
|
181
|
+
executionTimeMs: typeof task["executionTimeMs"] === "number"
|
|
182
|
+
? task["executionTimeMs"]
|
|
183
|
+
: undefined,
|
|
184
|
+
refinementAttempts: typeof task["refinementAttempts"] === "number"
|
|
185
|
+
? task["refinementAttempts"]
|
|
186
|
+
: undefined,
|
|
187
|
+
stageLogPath: typeof task["stageLogPath"] === "string" ? task["stageLogPath"] : undefined,
|
|
188
|
+
errorContext: task["errorContext"],
|
|
189
|
+
currentStage: typeof task["currentStage"] === "string" ? task["currentStage"] : undefined,
|
|
190
|
+
failedStage: typeof task["failedStage"] === "string" ? task["failedStage"] : undefined,
|
|
191
|
+
artifacts: task["artifacts"],
|
|
192
|
+
tokenUsage: Array.isArray(task["tokenUsage"]) ? task["tokenUsage"] : undefined,
|
|
193
|
+
error: toTaskError(task["error"]),
|
|
194
|
+
skipReason: typeof task["skipReason"] === "string" ? task["skipReason"] : undefined,
|
|
195
|
+
skippedBy: typeof task["skippedBy"] === "string" ? task["skippedBy"] : undefined,
|
|
196
|
+
controlApplied: typeof task["controlApplied"] === "boolean" ? task["controlApplied"] : undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function toJobViewTasks(value: unknown): Record<string, JobViewTask> {
|
|
201
|
+
if (Array.isArray(value)) {
|
|
202
|
+
return Object.fromEntries(
|
|
203
|
+
value.map((entry, index) => {
|
|
204
|
+
const record = isRecord(entry) ? entry : {};
|
|
205
|
+
const explicitName = record["name"];
|
|
206
|
+
const name = typeof explicitName === "string" && explicitName.length > 0
|
|
207
|
+
? explicitName
|
|
208
|
+
: `task-${index}`;
|
|
209
|
+
return [name, toJobViewTask(name, record)] as const;
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (isRecord(value)) {
|
|
214
|
+
return Object.fromEntries(
|
|
215
|
+
Object.entries(value).map(([name, entry]) => [name, toJobViewTask(name, entry)] as const),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return {};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getRawTaskNameValuePairs(value: unknown): Array<[string, unknown]> {
|
|
222
|
+
if (Array.isArray(value)) {
|
|
223
|
+
return value.map((entry, index) => {
|
|
224
|
+
const record = isRecord(entry) ? entry : {};
|
|
225
|
+
const explicitName = record["name"];
|
|
226
|
+
const name = typeof explicitName === "string" && explicitName.length > 0
|
|
227
|
+
? explicitName
|
|
228
|
+
: `task-${index}`;
|
|
229
|
+
return [name, entry] as [string, unknown];
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (isRecord(value)) {
|
|
233
|
+
return Object.entries(value);
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readUsageSource(value: unknown): UsageSource {
|
|
239
|
+
return value === "reported" ||
|
|
240
|
+
value === "estimated" ||
|
|
241
|
+
value === "unavailable" ||
|
|
242
|
+
value === "zero"
|
|
243
|
+
? value
|
|
244
|
+
: "reported";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const SOURCE_RANK: Record<UsageSource, number> = {
|
|
248
|
+
reported: 0,
|
|
249
|
+
zero: 0,
|
|
250
|
+
estimated: 1,
|
|
251
|
+
unavailable: 2,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
function combineSource(current: UsageSource | undefined, next: UsageSource): UsageSource {
|
|
255
|
+
if (current === undefined) return next;
|
|
256
|
+
return SOURCE_RANK[current] >= SOURCE_RANK[next] ? current : next;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readCostEstimated(entry: unknown[], source: UsageSource): boolean {
|
|
260
|
+
if (typeof entry[6] === "boolean") return entry[6];
|
|
261
|
+
if (source === "estimated" || source === "unavailable") return true;
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface TaskCostBreakdownEntry {
|
|
266
|
+
inputTokens: number;
|
|
267
|
+
outputTokens: number;
|
|
268
|
+
inputCost: number;
|
|
269
|
+
outputCost: number;
|
|
270
|
+
totalCost: number;
|
|
271
|
+
source?: UsageSource;
|
|
272
|
+
failed?: true;
|
|
273
|
+
costEstimated?: true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getCosts(record: Record<string, unknown>): Record<string, unknown> {
|
|
277
|
+
const explicitCosts = record["costs"];
|
|
278
|
+
if (isRecord(explicitCosts)) return explicitCosts;
|
|
279
|
+
|
|
280
|
+
let totalCost = 0;
|
|
281
|
+
let estimatedCost = 0;
|
|
282
|
+
let unavailableCost = 0;
|
|
283
|
+
let totalInputTokens = 0;
|
|
284
|
+
let totalOutputTokens = 0;
|
|
285
|
+
let hasEstimated = false;
|
|
286
|
+
const breakdown: Record<string, TaskCostBreakdownEntry> = {};
|
|
287
|
+
|
|
288
|
+
for (const [name, value] of getRawTaskNameValuePairs(record["tasks"])) {
|
|
289
|
+
if (!isRecord(value)) continue;
|
|
290
|
+
const usage = value["tokenUsage"];
|
|
291
|
+
if (!Array.isArray(usage)) continue;
|
|
292
|
+
|
|
293
|
+
let taskInputTokens = 0;
|
|
294
|
+
let taskOutputTokens = 0;
|
|
295
|
+
let taskReportedCost = 0;
|
|
296
|
+
let taskEstimatedCost = 0;
|
|
297
|
+
let taskUnavailableCost = 0;
|
|
298
|
+
let taskSource: UsageSource | undefined;
|
|
299
|
+
let taskFailed: true | undefined;
|
|
300
|
+
let taskCostEstimated = false;
|
|
301
|
+
let contributed = false;
|
|
302
|
+
|
|
303
|
+
for (const entry of usage) {
|
|
304
|
+
if (!Array.isArray(entry)) continue;
|
|
305
|
+
contributed = true;
|
|
306
|
+
const inputTokens = toFiniteNumber(entry[1]) ?? 0;
|
|
307
|
+
const outputTokens = toFiniteNumber(entry[2]) ?? 0;
|
|
308
|
+
const cost = toFiniteNumber(entry[3]) ?? 0;
|
|
309
|
+
const source = readUsageSource(entry[4]);
|
|
310
|
+
const failed = entry[5] === true;
|
|
311
|
+
const costEstimated = readCostEstimated(entry, source);
|
|
312
|
+
|
|
313
|
+
taskInputTokens += inputTokens;
|
|
314
|
+
taskOutputTokens += outputTokens;
|
|
315
|
+
totalInputTokens += inputTokens;
|
|
316
|
+
totalOutputTokens += outputTokens;
|
|
317
|
+
|
|
318
|
+
if (source === "unavailable") {
|
|
319
|
+
unavailableCost += cost;
|
|
320
|
+
taskUnavailableCost += cost;
|
|
321
|
+
hasEstimated = true;
|
|
322
|
+
} else if (costEstimated) {
|
|
323
|
+
estimatedCost += cost;
|
|
324
|
+
taskEstimatedCost += cost;
|
|
325
|
+
hasEstimated = true;
|
|
326
|
+
} else {
|
|
327
|
+
totalCost += cost;
|
|
328
|
+
taskReportedCost += cost;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
taskSource = combineSource(taskSource, source);
|
|
332
|
+
if (failed) taskFailed = true;
|
|
333
|
+
if (costEstimated) {
|
|
334
|
+
hasEstimated = true;
|
|
335
|
+
taskCostEstimated = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!contributed) continue;
|
|
340
|
+
|
|
341
|
+
const entry: TaskCostBreakdownEntry = {
|
|
342
|
+
inputTokens: taskInputTokens,
|
|
343
|
+
outputTokens: taskOutputTokens,
|
|
344
|
+
inputCost: 0,
|
|
345
|
+
outputCost: 0,
|
|
346
|
+
totalCost: taskReportedCost + taskEstimatedCost + taskUnavailableCost,
|
|
347
|
+
};
|
|
348
|
+
if (taskSource !== undefined) entry.source = taskSource;
|
|
349
|
+
if (taskFailed === true) entry.failed = true;
|
|
350
|
+
if (taskCostEstimated) entry.costEstimated = true;
|
|
351
|
+
breakdown[name] = entry;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
355
|
+
if (
|
|
356
|
+
totalTokens === 0 &&
|
|
357
|
+
totalCost === 0 &&
|
|
358
|
+
estimatedCost === 0 &&
|
|
359
|
+
unavailableCost === 0
|
|
360
|
+
) {
|
|
361
|
+
return {};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
totalCost,
|
|
365
|
+
totalTokens,
|
|
366
|
+
totalInputTokens,
|
|
367
|
+
totalOutputTokens,
|
|
368
|
+
estimatedCost,
|
|
369
|
+
unavailableCost,
|
|
370
|
+
hasEstimated,
|
|
371
|
+
...breakdown,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function resolveStateStatus(
|
|
376
|
+
fromState: JobStatusValue,
|
|
377
|
+
tasks: JobViewTask[],
|
|
378
|
+
location: string,
|
|
379
|
+
): JobStatusValue {
|
|
380
|
+
if (fromState !== "complete" || location === "complete" || tasks.length === 0) {
|
|
381
|
+
return fromState;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const taskStatus = deriveJobStatusFromTasks(tasks);
|
|
385
|
+
return taskStatus === "complete" ? fromState : taskStatus;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function toTitle(record: Record<string, unknown>, fallback: string): string {
|
|
389
|
+
const candidate = record["title"] ?? record["name"];
|
|
390
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : fallback;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function toErrorView(value: unknown): JobViewError | undefined {
|
|
394
|
+
if (!isRecord(value)) return undefined;
|
|
395
|
+
if (typeof value["message"] !== "string") return undefined;
|
|
396
|
+
const code = typeof value["code"] === "string"
|
|
397
|
+
? value["code"]
|
|
398
|
+
: typeof value["name"] === "string"
|
|
399
|
+
? value["name"]
|
|
400
|
+
: DEFAULT_JOB_ERROR_CODE;
|
|
401
|
+
const result: JobViewError = { code, message: value["message"] };
|
|
402
|
+
if (typeof value["path"] === "string") result.path = value["path"];
|
|
403
|
+
if (typeof value["name"] === "string") result.name = value["name"];
|
|
404
|
+
if (typeof value["stack"] === "string") result.stack = value["stack"];
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function normalizeJobView(raw: unknown, jobId: string, location: string): JobView {
|
|
409
|
+
const record = isRecord(raw) ? raw : {};
|
|
410
|
+
|
|
411
|
+
const tasks = toJobViewTasks(record["tasks"]);
|
|
412
|
+
const taskList = Object.values(tasks);
|
|
413
|
+
const pipelineConfig = isRecord(record["pipelineConfig"]) ? record["pipelineConfig"] : null;
|
|
414
|
+
const configuredTaskCount = pipelineConfig && Array.isArray(pipelineConfig["tasks"])
|
|
415
|
+
? pipelineConfig["tasks"].length
|
|
416
|
+
: null;
|
|
417
|
+
|
|
418
|
+
const fromState = jobStateToStatus(record["state"]);
|
|
419
|
+
let status: JobStatusValue;
|
|
420
|
+
if (fromState !== null) {
|
|
421
|
+
status = resolveStateStatus(fromState, taskList, location);
|
|
422
|
+
} else if (isRecognizedStatusValue(record["status"])) {
|
|
423
|
+
status = normalizeJobStatus(record["status"]);
|
|
424
|
+
} else {
|
|
425
|
+
const warnKey = `${location}:${jobId}`;
|
|
426
|
+
if (!warnedFallbackKeys.has(warnKey)) {
|
|
427
|
+
warnedFallbackKeys.add(warnKey);
|
|
428
|
+
console.warn(
|
|
429
|
+
`normalizeJobView: no valid state or status for ${warnKey}; deriving job status from tasks.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
status = deriveJobStatusFromTasks(taskList);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const title = toTitle(record, jobId);
|
|
436
|
+
const lastUpdated = toStringOrNull(record["lastUpdated"]);
|
|
437
|
+
|
|
438
|
+
const result: JobView = {
|
|
439
|
+
id: jobId,
|
|
440
|
+
jobId,
|
|
441
|
+
name: title,
|
|
442
|
+
title,
|
|
443
|
+
status,
|
|
444
|
+
progress: deriveProgress(taskList, configuredTaskCount).progress,
|
|
445
|
+
createdAt: toStringOrNull(record["createdAt"]),
|
|
446
|
+
updatedAt: toStringOrNull(record["updatedAt"]) ?? lastUpdated,
|
|
447
|
+
location,
|
|
448
|
+
tasks,
|
|
449
|
+
files: isRecord(record["files"]) ? record["files"] : {},
|
|
450
|
+
costs: getCosts(record),
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
if (lastUpdated !== null) result.lastUpdated = lastUpdated;
|
|
454
|
+
if ("gate" in record) result.gate = toGateInfo(record["gate"]);
|
|
455
|
+
if (typeof record["pipeline"] === "string") result.pipeline = record["pipeline"];
|
|
456
|
+
if (typeof record["pipelineLabel"] === "string") result.pipelineLabel = record["pipelineLabel"];
|
|
457
|
+
if (pipelineConfig) result.pipelineConfig = pipelineConfig;
|
|
458
|
+
if ("current" in record) result.current = record["current"];
|
|
459
|
+
if ("currentStage" in record) result.currentStage = record["currentStage"];
|
|
460
|
+
if (Array.isArray(record["warnings"])) {
|
|
461
|
+
result.warnings = record["warnings"].filter((entry): entry is string => typeof entry === "string");
|
|
462
|
+
}
|
|
463
|
+
if (typeof record["readable"] === "boolean") result.readable = record["readable"];
|
|
464
|
+
const errorView = toErrorView(record["error"]);
|
|
465
|
+
if (errorView) result.error = errorView;
|
|
466
|
+
if ("errorContext" in record) result.errorContext = record["errorContext"];
|
|
467
|
+
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function __resetWarnedFallbackKeys(): void {
|
|
472
|
+
warnedFallbackKeys.clear();
|
|
473
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { atomicWrite } from "./status-writer";
|
|
2
|
+
|
|
3
|
+
function causeDetail(cause: unknown): string {
|
|
4
|
+
if (cause instanceof Error) {
|
|
5
|
+
const code = (cause as NodeJS.ErrnoException).code;
|
|
6
|
+
return code ? `${code}: ${cause.message}` : cause.message;
|
|
7
|
+
}
|
|
8
|
+
return String(cause);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class JsonFileError extends Error {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly reason: "missing" | "parse";
|
|
14
|
+
constructor(path: string, reason: "missing" | "parse", cause?: unknown) {
|
|
15
|
+
super(`JSON file ${reason} at ${path}: ${causeDetail(cause)}`, { cause });
|
|
16
|
+
this.name = "JsonFileError";
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.reason = reason;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function readJsonFile<T>(filePath: string): Promise<T> {
|
|
23
|
+
let text: string;
|
|
24
|
+
try {
|
|
25
|
+
text = await Bun.file(filePath).text();
|
|
26
|
+
} catch (err: unknown) {
|
|
27
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
28
|
+
throw new JsonFileError(filePath, "missing", err);
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(text) as T;
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
if (err instanceof SyntaxError) {
|
|
37
|
+
throw new JsonFileError(filePath, "parse", err);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
44
|
+
await atomicWrite(filePath, JSON.stringify(value, null, 2) + "\n");
|
|
45
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeTaskState } from "../config/statuses";
|
|
2
|
+
|
|
1
3
|
type LifecycleOp = "start" | "restart";
|
|
2
4
|
|
|
3
5
|
interface TransitionInput {
|
|
@@ -13,7 +15,7 @@ interface TransitionAllowed {
|
|
|
13
15
|
interface TransitionBlocked {
|
|
14
16
|
ok: false;
|
|
15
17
|
code: "unsupported_lifecycle";
|
|
16
|
-
reason: "dependencies"
|
|
18
|
+
reason: "dependencies";
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
type TransitionDecision = TransitionAllowed | TransitionBlocked;
|
|
@@ -24,11 +26,24 @@ const BLOCKED_DEPS: Readonly<TransitionBlocked> = Object.freeze({
|
|
|
24
26
|
code: "unsupported_lifecycle" as const,
|
|
25
27
|
reason: "dependencies" as const,
|
|
26
28
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
|
|
30
|
+
export function areDependenciesSatisfied(predecessorStates: Iterable<string>): boolean {
|
|
31
|
+
for (const state of predecessorStates) {
|
|
32
|
+
const normalized = normalizeTaskState(state);
|
|
33
|
+
if (normalized !== "done" && normalized !== "skipped") return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function firstUnsatisfiedDependency(
|
|
39
|
+
predecessors: ReadonlyArray<{ name: string; state: string }>,
|
|
40
|
+
): { name: string; state: string } | null {
|
|
41
|
+
for (const predecessor of predecessors) {
|
|
42
|
+
const normalized = normalizeTaskState(predecessor.state);
|
|
43
|
+
if (normalized !== "done" && normalized !== "skipped") return predecessor;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
32
47
|
|
|
33
48
|
export function decideTransition(input: TransitionInput): Readonly<TransitionDecision> {
|
|
34
49
|
const { op, taskState, dependenciesReady } = input;
|
|
@@ -47,6 +62,6 @@ export function decideTransition(input: TransitionInput): Readonly<TransitionDec
|
|
|
47
62
|
return dependenciesReady ? ALLOWED : BLOCKED_DEPS;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
// op === "restart":
|
|
51
|
-
return
|
|
65
|
+
// op === "restart": gated on dependenciesReady (same rule as "start")
|
|
66
|
+
return dependenciesReady ? ALLOWED : BLOCKED_DEPS;
|
|
52
67
|
}
|
package/src/core/logger.ts
CHANGED
|
@@ -17,25 +17,6 @@ export interface Logger {
|
|
|
17
17
|
sse(eventType: string, eventData: unknown): void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// Lazy SSE registry cache
|
|
21
|
-
let sseRegistry: { broadcast: (eventType: string, data: unknown) => void } | null = null;
|
|
22
|
-
|
|
23
|
-
async function getSSERegistry(): Promise<{ broadcast: (eventType: string, data: unknown) => void } | null> {
|
|
24
|
-
if (sseRegistry !== null) return sseRegistry;
|
|
25
|
-
try {
|
|
26
|
-
// Dynamic import path kept as a variable to avoid static resolution errors
|
|
27
|
-
// when the module does not yet exist. Fails gracefully at runtime.
|
|
28
|
-
const ssePath = "../ui/sse.ts";
|
|
29
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
30
|
-
const mod = await import(/* @vite-ignore */ ssePath);
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
32
|
-
sseRegistry = mod as { broadcast: (eventType: string, data: unknown) => void };
|
|
33
|
-
return sseRegistry;
|
|
34
|
-
} catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
20
|
function formatPrefix(componentName: string, context?: LogContext): string {
|
|
40
21
|
const parts: string[] = [componentName];
|
|
41
22
|
if (context?.jobId) parts.push(context.jobId);
|
|
@@ -124,16 +105,6 @@ export function createLogger(componentName: string, context?: LogContext): Logge
|
|
|
124
105
|
|
|
125
106
|
sse(eventType, eventData) {
|
|
126
107
|
console.log(prefix, `[SSE:${eventType}]`, stringify(eventData));
|
|
127
|
-
void getSSERegistry().then((registry) => {
|
|
128
|
-
if (!registry) return;
|
|
129
|
-
try {
|
|
130
|
-
registry.broadcast(eventType, formatData(eventData));
|
|
131
|
-
} catch (err) {
|
|
132
|
-
console.warn(prefix, "SSE broadcast failed", err);
|
|
133
|
-
}
|
|
134
|
-
}).catch((err) => {
|
|
135
|
-
console.warn(prefix, "SSE registry unavailable", err);
|
|
136
|
-
});
|
|
137
108
|
},
|
|
138
109
|
};
|
|
139
110
|
}
|