@agwab/pi-workflow 0.2.1 → 0.4.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/README.md +3 -1
- package/dist/artifact-graph-runtime.d.ts +1 -1
- package/dist/artifact-graph-runtime.js +10 -5
- package/dist/artifact-graph-schema.js +127 -5
- package/dist/compiler.js +52 -19
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -1
- package/dist/engine-run-graph.d.ts +3 -0
- package/dist/engine-run-graph.js +194 -4
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +389 -41
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +30 -8
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/prompt-json.d.ts +7 -0
- package/dist/prompt-json.js +13 -0
- package/dist/roles.d.ts +1 -1
- package/dist/roles.js +5 -8
- package/dist/store.d.ts +20 -1
- package/dist/store.js +139 -35
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +710 -40
- package/dist/types.d.ts +107 -1
- package/dist/verification-ontology.d.ts +31 -0
- package/dist/verification-ontology.js +66 -0
- package/dist/workflow-artifact-tool.js +5 -6
- package/dist/workflow-artifacts.d.ts +7 -0
- package/dist/workflow-artifacts.js +55 -4
- package/dist/workflow-fetch-cache-extension.d.ts +1 -0
- package/dist/workflow-fetch-cache-extension.js +57 -9
- package/dist/workflow-metrics.d.ts +113 -0
- package/dist/workflow-metrics.js +272 -0
- package/dist/workflow-output-artifacts.js +5 -3
- package/dist/workflow-partial-output.d.ts +45 -0
- package/dist/workflow-partial-output.js +205 -0
- package/dist/workflow-progress-health.js +42 -10
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +194 -52
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +109 -30
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/skills/workflow-guide/SKILL.md +1 -0
- package/src/artifact-graph-runtime.ts +19 -13
- package/src/artifact-graph-schema.ts +143 -3
- package/src/cli.mjs +52 -0
- package/src/compiler.ts +63 -18
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +1 -1
- package/src/engine-run-graph.ts +246 -4
- package/src/engine.ts +545 -38
- package/src/extension.ts +36 -6
- package/src/index.ts +52 -1
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +194 -42
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +921 -62
- package/src/types.ts +116 -2
- package/src/verification-ontology.ts +88 -0
- package/src/workflow-artifact-tool.ts +5 -7
- package/src/workflow-artifacts.ts +83 -3
- package/src/workflow-fetch-cache-extension.ts +78 -13
- package/src/workflow-metrics.ts +478 -0
- package/src/workflow-output-artifacts.ts +5 -3
- package/src/workflow-partial-output.ts +299 -0
- package/src/workflow-progress-health.ts +47 -15
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +654 -232
- package/src/workflow-web-source.ts +153 -39
- package/workflows/README.md +7 -25
- package/workflows/deep-research/batched-verification.spec.json +253 -0
- package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
- package/workflows/deep-research/helpers/render-executive.mjs +40 -26
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
- package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
- package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
- package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
- package/workflows/deep-research/spec.json +32 -12
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { hashDynamicRequest } from "./dynamic-events.js";
|
|
5
|
+
import { readJson, writeJsonAtomic } from "./store.js";
|
|
6
|
+
|
|
7
|
+
export const WORKFLOW_PARTIAL_OUTPUT_PROTOCOL =
|
|
8
|
+
"workflow-partial-output-v1" as const;
|
|
9
|
+
export const WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA =
|
|
10
|
+
"workflow-partial-output-ledger-v1" as const;
|
|
11
|
+
export const WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE = "partial-control.json";
|
|
12
|
+
|
|
13
|
+
export type WorkflowPartialOutputIssueCode =
|
|
14
|
+
| "invalid_json"
|
|
15
|
+
| "invalid_type"
|
|
16
|
+
| "invalid_schema"
|
|
17
|
+
| "invalid_path"
|
|
18
|
+
| "disallowed_path"
|
|
19
|
+
| "missing_items"
|
|
20
|
+
| "missing_item_id"
|
|
21
|
+
| "duplicate_item_id";
|
|
22
|
+
|
|
23
|
+
export interface WorkflowPartialOutputIssue {
|
|
24
|
+
code: WorkflowPartialOutputIssueCode;
|
|
25
|
+
message: string;
|
|
26
|
+
sectionIndex?: number;
|
|
27
|
+
path?: string;
|
|
28
|
+
itemId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkflowPartialOutputItem {
|
|
32
|
+
path: string;
|
|
33
|
+
itemId: string;
|
|
34
|
+
itemHash: string;
|
|
35
|
+
item: unknown;
|
|
36
|
+
ordinal: number;
|
|
37
|
+
sectionIndex: number;
|
|
38
|
+
sectionItemIndex: number;
|
|
39
|
+
itemRef: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkflowPartialOutputLedger {
|
|
43
|
+
schema: typeof WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA;
|
|
44
|
+
protocol: typeof WORKFLOW_PARTIAL_OUTPUT_PROTOCOL;
|
|
45
|
+
items: WorkflowPartialOutputItem[];
|
|
46
|
+
issues: WorkflowPartialOutputIssue[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ParseWorkflowPartialOutputOptions {
|
|
50
|
+
allowedPaths?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PartialSectionMatch {
|
|
54
|
+
content: string;
|
|
55
|
+
start: number;
|
|
56
|
+
end: number;
|
|
57
|
+
index: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PARTIAL_CONTROL_OPEN = "partial-control";
|
|
61
|
+
|
|
62
|
+
export function partialOutputLedgerPath(taskDir: string): string {
|
|
63
|
+
return join(taskDir, WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readWorkflowPartialOutputLedger(
|
|
67
|
+
taskDir: string,
|
|
68
|
+
): Promise<WorkflowPartialOutputLedger | undefined> {
|
|
69
|
+
return await readJson<WorkflowPartialOutputLedger>(
|
|
70
|
+
partialOutputLedgerPath(taskDir),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function writeWorkflowPartialOutputLedger(options: {
|
|
75
|
+
taskDir: string;
|
|
76
|
+
rawOutput: string;
|
|
77
|
+
allowedPaths?: readonly string[];
|
|
78
|
+
}): Promise<WorkflowPartialOutputLedger> {
|
|
79
|
+
const ledger = parseWorkflowPartialOutput(options.rawOutput, {
|
|
80
|
+
allowedPaths: options.allowedPaths,
|
|
81
|
+
});
|
|
82
|
+
await writeJsonAtomic(partialOutputLedgerPath(options.taskDir), ledger);
|
|
83
|
+
return ledger;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function writeWorkflowPartialOutputLedgerFromFile(options: {
|
|
87
|
+
taskDir: string;
|
|
88
|
+
outputFile: string;
|
|
89
|
+
allowedPaths?: readonly string[];
|
|
90
|
+
}): Promise<WorkflowPartialOutputLedger | undefined> {
|
|
91
|
+
const rawOutput = await readFile(options.outputFile, "utf8").catch(
|
|
92
|
+
() => undefined,
|
|
93
|
+
);
|
|
94
|
+
if (rawOutput === undefined) return undefined;
|
|
95
|
+
return await writeWorkflowPartialOutputLedger({
|
|
96
|
+
taskDir: options.taskDir,
|
|
97
|
+
rawOutput,
|
|
98
|
+
allowedPaths: options.allowedPaths,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function stripWorkflowPartialOutputSections(raw: string): string {
|
|
103
|
+
if (!raw.includes(PARTIAL_CONTROL_OPEN)) return raw;
|
|
104
|
+
return raw.replace(partialControlSectionRegExp(), "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseWorkflowPartialOutput(
|
|
108
|
+
raw: string,
|
|
109
|
+
options: ParseWorkflowPartialOutputOptions = {},
|
|
110
|
+
): WorkflowPartialOutputLedger {
|
|
111
|
+
const allowedPaths = options.allowedPaths
|
|
112
|
+
? new Set(options.allowedPaths)
|
|
113
|
+
: undefined;
|
|
114
|
+
const items: WorkflowPartialOutputItem[] = [];
|
|
115
|
+
const issues: WorkflowPartialOutputIssue[] = [];
|
|
116
|
+
const byPathAndId = new Map<string, WorkflowPartialOutputItem>();
|
|
117
|
+
|
|
118
|
+
for (const section of collectPartialControlSections(raw)) {
|
|
119
|
+
const parsed = parsePartialSectionJson(section, issues);
|
|
120
|
+
if (!parsed) continue;
|
|
121
|
+
const path = parsePartialSectionPath(parsed, section, allowedPaths, issues);
|
|
122
|
+
if (!path) continue;
|
|
123
|
+
const rawItems = parsePartialSectionItems(parsed, section, path, issues);
|
|
124
|
+
if (!rawItems) continue;
|
|
125
|
+
for (const [sectionItemIndex, item] of rawItems.entries()) {
|
|
126
|
+
const itemId = stablePartialItemId(item);
|
|
127
|
+
if (!itemId) {
|
|
128
|
+
issues.push({
|
|
129
|
+
code: "missing_item_id",
|
|
130
|
+
sectionIndex: section.index,
|
|
131
|
+
path,
|
|
132
|
+
message:
|
|
133
|
+
"partial output items must be objects with a stable non-empty string id",
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const itemHash = hashDynamicRequest(item);
|
|
138
|
+
const key = `${path}\0${itemId}`;
|
|
139
|
+
const existing = byPathAndId.get(key);
|
|
140
|
+
if (existing) {
|
|
141
|
+
if (existing.itemHash !== itemHash) {
|
|
142
|
+
issues.push({
|
|
143
|
+
code: "duplicate_item_id",
|
|
144
|
+
sectionIndex: section.index,
|
|
145
|
+
path,
|
|
146
|
+
itemId,
|
|
147
|
+
message: `partial output item ${itemId} at ${path} changed after it was published`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const ordinal = items.length;
|
|
153
|
+
const partialItem: WorkflowPartialOutputItem = {
|
|
154
|
+
path,
|
|
155
|
+
itemId,
|
|
156
|
+
itemHash,
|
|
157
|
+
item,
|
|
158
|
+
ordinal,
|
|
159
|
+
sectionIndex: section.index,
|
|
160
|
+
sectionItemIndex,
|
|
161
|
+
itemRef: `${WORKFLOW_PARTIAL_OUTPUT_LEDGER_FILE}#/items/${ordinal}`,
|
|
162
|
+
};
|
|
163
|
+
items.push(partialItem);
|
|
164
|
+
byPathAndId.set(key, partialItem);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
schema: WORKFLOW_PARTIAL_OUTPUT_LEDGER_SCHEMA,
|
|
170
|
+
protocol: WORKFLOW_PARTIAL_OUTPUT_PROTOCOL,
|
|
171
|
+
items,
|
|
172
|
+
issues,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function hasFatalPartialOutputIssue(
|
|
177
|
+
ledger: Pick<WorkflowPartialOutputLedger, "issues"> | undefined,
|
|
178
|
+
): WorkflowPartialOutputIssue | undefined {
|
|
179
|
+
return ledger?.issues.find((issue) => issue.code === "duplicate_item_id");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function collectPartialControlSections(raw: string): PartialSectionMatch[] {
|
|
183
|
+
if (!raw.includes(PARTIAL_CONTROL_OPEN)) return [];
|
|
184
|
+
const matches: PartialSectionMatch[] = [];
|
|
185
|
+
const re = partialControlSectionRegExp();
|
|
186
|
+
let match: RegExpExecArray | null;
|
|
187
|
+
while ((match = re.exec(raw)) !== null) {
|
|
188
|
+
matches.push({
|
|
189
|
+
content: match[1] ?? "",
|
|
190
|
+
start: match.index,
|
|
191
|
+
end: re.lastIndex,
|
|
192
|
+
index: matches.length,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return matches;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function partialControlSectionRegExp(): RegExp {
|
|
199
|
+
return /[ \t]*<partial-control\s*>([\s\S]*?)<\/partial-control>[ \t]*(?:\r?\n)?/gi;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parsePartialSectionJson(
|
|
203
|
+
section: PartialSectionMatch,
|
|
204
|
+
issues: WorkflowPartialOutputIssue[],
|
|
205
|
+
): Record<string, unknown> | undefined {
|
|
206
|
+
let parsed: unknown;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(section.content.trim());
|
|
209
|
+
} catch (error) {
|
|
210
|
+
issues.push({
|
|
211
|
+
code: "invalid_json",
|
|
212
|
+
sectionIndex: section.index,
|
|
213
|
+
message: error instanceof Error ? error.message : String(error),
|
|
214
|
+
});
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
if (!isRecord(parsed)) {
|
|
218
|
+
issues.push({
|
|
219
|
+
code: "invalid_type",
|
|
220
|
+
sectionIndex: section.index,
|
|
221
|
+
message: "partial-control section must contain a JSON object",
|
|
222
|
+
});
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
if (parsed.schema !== WORKFLOW_PARTIAL_OUTPUT_PROTOCOL) {
|
|
226
|
+
issues.push({
|
|
227
|
+
code: "invalid_schema",
|
|
228
|
+
sectionIndex: section.index,
|
|
229
|
+
message: `partial-control schema must be ${WORKFLOW_PARTIAL_OUTPUT_PROTOCOL}`,
|
|
230
|
+
});
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
return parsed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function parsePartialSectionPath(
|
|
237
|
+
section: Record<string, unknown>,
|
|
238
|
+
match: PartialSectionMatch,
|
|
239
|
+
allowedPaths: Set<string> | undefined,
|
|
240
|
+
issues: WorkflowPartialOutputIssue[],
|
|
241
|
+
): string | undefined {
|
|
242
|
+
const path = section.path;
|
|
243
|
+
if (typeof path !== "string" || !path.startsWith("$.")) {
|
|
244
|
+
issues.push({
|
|
245
|
+
code: "invalid_path",
|
|
246
|
+
sectionIndex: match.index,
|
|
247
|
+
message: "partial-control path must be a control JSONPath starting with $.",
|
|
248
|
+
});
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
if (allowedPaths && !allowedPaths.has(path)) {
|
|
252
|
+
issues.push({
|
|
253
|
+
code: "disallowed_path",
|
|
254
|
+
sectionIndex: match.index,
|
|
255
|
+
path,
|
|
256
|
+
message: `partial-control path ${path} is not declared for this stage`,
|
|
257
|
+
});
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return path;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parsePartialSectionItems(
|
|
264
|
+
section: Record<string, unknown>,
|
|
265
|
+
match: PartialSectionMatch,
|
|
266
|
+
path: string,
|
|
267
|
+
issues: WorkflowPartialOutputIssue[],
|
|
268
|
+
): unknown[] | undefined {
|
|
269
|
+
const items = section.items;
|
|
270
|
+
if (!Array.isArray(items)) {
|
|
271
|
+
issues.push({
|
|
272
|
+
code: "missing_items",
|
|
273
|
+
sectionIndex: match.index,
|
|
274
|
+
path,
|
|
275
|
+
message: "partial-control items must be an array",
|
|
276
|
+
});
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return items;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function stablePartialItemId(item: unknown): string | undefined {
|
|
283
|
+
if (!isRecord(item) || typeof item.id !== "string") return undefined;
|
|
284
|
+
const sanitized = sanitizePartialItemId(item.id);
|
|
285
|
+
return sanitized || undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sanitizePartialItemId(value: string): string {
|
|
289
|
+
return value
|
|
290
|
+
.trim()
|
|
291
|
+
.toLowerCase()
|
|
292
|
+
.replace(/[^a-z0-9_.-]+/g, "-")
|
|
293
|
+
.replace(/^-+|-+$/g, "")
|
|
294
|
+
.slice(0, 64);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
299
|
+
}
|
|
@@ -104,16 +104,16 @@ export function diagnoseWorkflowRunHealth(
|
|
|
104
104
|
options: WorkflowHealthOptions = {},
|
|
105
105
|
): WorkflowProgressHealth {
|
|
106
106
|
const nowMs = options.nowMs ?? Date.now();
|
|
107
|
-
const runningTask = currentRunningTask(run.tasks ?? []);
|
|
108
|
-
if (runningTask)
|
|
109
|
-
return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
|
|
110
|
-
|
|
111
107
|
const problem = (run.tasks ?? []).find((task) =>
|
|
112
108
|
isProblemStatus(task.status),
|
|
113
109
|
);
|
|
114
110
|
if (problem) return problemRunHealth(problem, nowMs);
|
|
115
111
|
if (isProblemStatus(run.status)) return problemWorkflowHealth(run.status);
|
|
116
112
|
if (run.status === "completed") return completedWorkflowHealth();
|
|
113
|
+
|
|
114
|
+
const runningTask = currentRunningTask(run.tasks ?? []);
|
|
115
|
+
if (runningTask)
|
|
116
|
+
return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
|
|
117
117
|
return waitingWorkflowHealth(run, nowMs);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -143,19 +143,20 @@ export function classifyWorkflowTaskDuration(
|
|
|
143
143
|
.filter(Boolean)
|
|
144
144
|
.join(" ")
|
|
145
145
|
.toLowerCase();
|
|
146
|
-
if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
|
|
147
|
-
return "short";
|
|
148
|
-
if (
|
|
149
|
-
/\b(research|audit|synthesis|review|verify|verifier|normalize|plan|impact|spec)\b/.test(
|
|
150
|
-
text,
|
|
151
|
-
)
|
|
152
|
-
)
|
|
153
|
-
return "long";
|
|
154
146
|
const maxRuntimeMs = task.runtime?.maxRuntimeMs;
|
|
155
147
|
if (maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs)) {
|
|
156
148
|
if (maxRuntimeMs <= 5 * 60_000) return "short";
|
|
157
149
|
if (maxRuntimeMs >= 60 * 60_000) return "long";
|
|
158
150
|
}
|
|
151
|
+
if (task.kind === "support") return "short";
|
|
152
|
+
if (
|
|
153
|
+
/\b(research|audit|synthesi[sz]e?r?|review(?:er|ers|ing|s)?|verif(?:y|ier|iers|ication)|normaliz(?:e|er|ing|ation)?|plan(?:ning)?|impact|spec)\b/.test(
|
|
154
|
+
text,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return "long";
|
|
158
|
+
if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
|
|
159
|
+
return "short";
|
|
159
160
|
return "medium";
|
|
160
161
|
}
|
|
161
162
|
|
|
@@ -215,6 +216,33 @@ function waitingWorkflowHealth(
|
|
|
215
216
|
nowMs: number,
|
|
216
217
|
): WorkflowProgressHealth {
|
|
217
218
|
const hasPending = run.taskSummary.pending > 0;
|
|
219
|
+
const lastActivityAgeMs = ageMs(run.updatedAt, nowMs);
|
|
220
|
+
if (hasPending && lastActivityAgeMs !== undefined) {
|
|
221
|
+
if (lastActivityAgeMs >= STUCK_BY_DURATION.medium) {
|
|
222
|
+
return {
|
|
223
|
+
state: "likely-stuck",
|
|
224
|
+
label: "scheduler stuck",
|
|
225
|
+
summary: "pending tasks have not scheduled",
|
|
226
|
+
tone: "error",
|
|
227
|
+
suggestion: "resume",
|
|
228
|
+
reason: "no task is running and run activity is stale",
|
|
229
|
+
lastActivityAt: run.updatedAt,
|
|
230
|
+
lastActivityAgeMs,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (lastActivityAgeMs >= STALL_BY_DURATION.medium) {
|
|
234
|
+
return {
|
|
235
|
+
state: "stalled",
|
|
236
|
+
label: "scheduler quiet",
|
|
237
|
+
summary: "pending tasks are waiting without recent activity",
|
|
238
|
+
tone: "warning",
|
|
239
|
+
suggestion: "inspect",
|
|
240
|
+
reason: "no task is running and run activity is stale",
|
|
241
|
+
lastActivityAt: run.updatedAt,
|
|
242
|
+
lastActivityAgeMs,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
218
246
|
return {
|
|
219
247
|
state: hasPending ? "pending" : "active",
|
|
220
248
|
label: hasPending ? "pending" : "active",
|
|
@@ -227,7 +255,7 @@ function waitingWorkflowHealth(
|
|
|
227
255
|
? "no task is currently running"
|
|
228
256
|
: "workflow is still in progress",
|
|
229
257
|
lastActivityAt: run.updatedAt,
|
|
230
|
-
lastActivityAgeMs
|
|
258
|
+
lastActivityAgeMs,
|
|
231
259
|
};
|
|
232
260
|
}
|
|
233
261
|
|
|
@@ -335,8 +363,12 @@ function runningContext(
|
|
|
335
363
|
const durationClass = classifyWorkflowTaskDuration(task);
|
|
336
364
|
const startedAtMs = parseTime(task.startedAt);
|
|
337
365
|
const heartbeatAt = parseHeartbeatAt(task.lastMessage);
|
|
366
|
+
const heartbeatAgeMs = ageMs(heartbeatAt, nowMs);
|
|
338
367
|
const activityAt = latestIso([heartbeatAt, run?.updatedAt, task.startedAt]);
|
|
339
368
|
const lastActivityAgeMs = ageMs(activityAt, nowMs);
|
|
369
|
+
const hasFreshHeartbeat =
|
|
370
|
+
heartbeatAgeMs !== undefined &&
|
|
371
|
+
heartbeatAgeMs <= STALL_BY_DURATION[durationClass];
|
|
340
372
|
return {
|
|
341
373
|
task,
|
|
342
374
|
nowMs,
|
|
@@ -346,8 +378,8 @@ function runningContext(
|
|
|
346
378
|
activityAt,
|
|
347
379
|
lastActivityAgeMs,
|
|
348
380
|
heartbeatAt,
|
|
349
|
-
heartbeatAgeMs
|
|
350
|
-
hasBackendSignal: Boolean(task.
|
|
381
|
+
heartbeatAgeMs,
|
|
382
|
+
hasBackendSignal: Boolean(task.pid || hasFreshHeartbeat),
|
|
351
383
|
staleMs: lastActivityAgeMs ?? Number.POSITIVE_INFINITY,
|
|
352
384
|
};
|
|
353
385
|
}
|
package/src/workflow-runtime.ts
CHANGED
|
@@ -345,9 +345,25 @@ export function readSimpleJsonPath(value: unknown, path: string): unknown {
|
|
|
345
345
|
const parts = path.slice(2).split(".").filter(Boolean);
|
|
346
346
|
let current = value as any;
|
|
347
347
|
for (const part of parts) {
|
|
348
|
-
if (current
|
|
349
|
-
return undefined;
|
|
348
|
+
if (!canReadJsonPathPart(current, part)) return undefined;
|
|
350
349
|
current = current[part];
|
|
351
350
|
}
|
|
352
351
|
return current;
|
|
353
352
|
}
|
|
353
|
+
|
|
354
|
+
function canReadJsonPathPart(
|
|
355
|
+
value: unknown,
|
|
356
|
+
part: string,
|
|
357
|
+
): value is Record<string, unknown> {
|
|
358
|
+
return (
|
|
359
|
+
isSafeJsonPathPart(part) && isRecord(value) && Object.hasOwn(value, part)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isSafeJsonPathPart(part: string): boolean {
|
|
364
|
+
return part !== "__proto__" && part !== "prototype" && part !== "constructor";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
368
|
+
return typeof value === "object" && value !== null;
|
|
369
|
+
}
|
package/src/workflow-view.ts
CHANGED
|
@@ -1370,10 +1370,11 @@ function statusForSummary(
|
|
|
1370
1370
|
): WorkflowRunStatus | TaskRunStatus {
|
|
1371
1371
|
if (summary.running > 0) return "running";
|
|
1372
1372
|
if (summary.blocked > 0) return "blocked";
|
|
1373
|
-
if (summary.failed > 0
|
|
1373
|
+
if (summary.failed > 0) return "failed";
|
|
1374
1374
|
if (summary.pending > 0) return "pending";
|
|
1375
1375
|
if (summary.total > 0 && summary.completed === summary.total)
|
|
1376
1376
|
return "completed";
|
|
1377
|
+
if (summary.interrupted > 0) return "interrupted";
|
|
1377
1378
|
return "interrupted";
|
|
1378
1379
|
}
|
|
1379
1380
|
|