@agwab/pi-workflow 0.1.1 → 0.2.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 +20 -15
- package/agents/researcher.md +17 -7
- package/dist/artifact-graph-runtime.js +1 -0
- package/dist/compiler.d.ts +2 -0
- package/dist/compiler.js +29 -4
- package/dist/dynamic-generated-task-runtime.js +4 -3
- package/dist/dynamic-runtime-bundle.js +3 -2
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +3 -2
- package/dist/extension.js +240 -16
- package/dist/store.js +1 -0
- package/dist/subagent-backend.js +82 -27
- package/dist/tool-metadata.d.ts +1 -0
- package/dist/tool-metadata.js +13 -1
- package/dist/types.d.ts +3 -0
- package/dist/workflow-artifact-extension.js +3 -2
- package/dist/workflow-artifact-tool.js +84 -4
- package/dist/workflow-progress-health.d.ts +37 -0
- package/dist/workflow-progress-health.js +296 -0
- package/dist/workflow-runtime.d.ts +6 -0
- package/dist/workflow-runtime.js +33 -10
- package/dist/workflow-view.d.ts +2 -0
- package/dist/workflow-view.js +97 -18
- package/dist/workflow-web-source-extension.d.ts +43 -0
- package/dist/workflow-web-source-extension.js +1194 -0
- package/dist/workflow-web-source.d.ts +171 -0
- package/dist/workflow-web-source.js +915 -0
- package/docs/usage.md +32 -18
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
- package/package.json +7 -7
- package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
- package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
- package/src/artifact-graph-runtime.ts +1 -0
- package/src/compiler.ts +43 -3
- package/src/dynamic-generated-task-runtime.ts +4 -2
- package/src/dynamic-runtime-bundle.ts +3 -2
- package/src/engine.ts +7 -16
- package/src/extension.ts +299 -22
- package/src/store.ts +1 -0
- package/src/subagent-backend.ts +121 -37
- package/src/tool-metadata.ts +22 -1
- package/src/types.ts +4 -0
- package/src/workflow-artifact-extension.ts +3 -2
- package/src/workflow-artifact-tool.ts +96 -4
- package/src/workflow-progress-health.ts +461 -0
- package/src/workflow-runtime.ts +50 -13
- package/src/workflow-view.ts +186 -41
- package/src/workflow-web-source-extension.ts +1411 -0
- package/src/workflow-web-source.ts +1294 -0
- package/workflows/README.md +1 -1
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +552 -44
- package/workflows/deep-research/helpers/final-audit-packet.mjs +396 -0
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +545 -0
- package/workflows/deep-research/helpers/render-executive.mjs +1199 -192
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +37 -8
- package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
- package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
- package/workflows/deep-research/spec.json +71 -26
- package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
- package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
- package/workflows/deep-review/spec.json +22 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
const ACTIVE_ACTIVITY_MS = 2 * 60_000;
|
|
2
|
+
const LONG_TAIL_ELAPSED_MS = 8 * 60_000;
|
|
3
|
+
const STALL_BY_DURATION = {
|
|
4
|
+
short: 5 * 60_000,
|
|
5
|
+
medium: 10 * 60_000,
|
|
6
|
+
long: 20 * 60_000,
|
|
7
|
+
};
|
|
8
|
+
const STUCK_BY_DURATION = {
|
|
9
|
+
short: 15 * 60_000,
|
|
10
|
+
medium: 30 * 60_000,
|
|
11
|
+
long: 60 * 60_000,
|
|
12
|
+
};
|
|
13
|
+
export function diagnoseWorkflowRunHealth(run, options = {}) {
|
|
14
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
15
|
+
const runningTask = currentRunningTask(run.tasks ?? []);
|
|
16
|
+
if (runningTask)
|
|
17
|
+
return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
|
|
18
|
+
const problem = (run.tasks ?? []).find((task) => isProblemStatus(task.status));
|
|
19
|
+
if (problem)
|
|
20
|
+
return problemRunHealth(problem, nowMs);
|
|
21
|
+
if (isProblemStatus(run.status))
|
|
22
|
+
return problemWorkflowHealth(run.status);
|
|
23
|
+
if (run.status === "completed")
|
|
24
|
+
return completedWorkflowHealth();
|
|
25
|
+
return waitingWorkflowHealth(run, nowMs);
|
|
26
|
+
}
|
|
27
|
+
export function diagnoseWorkflowTaskHealth(task, run, options = {}) {
|
|
28
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
29
|
+
if (task.status !== "running")
|
|
30
|
+
return terminalTaskHealth(task, nowMs);
|
|
31
|
+
return runningTaskHealth(runningContext(task, run, nowMs));
|
|
32
|
+
}
|
|
33
|
+
export function classifyWorkflowTaskDuration(task) {
|
|
34
|
+
const text = [
|
|
35
|
+
task.stageId,
|
|
36
|
+
task.displayName,
|
|
37
|
+
task.specId,
|
|
38
|
+
task.kind,
|
|
39
|
+
task.statusDetail,
|
|
40
|
+
]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join(" ")
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
|
|
45
|
+
return "short";
|
|
46
|
+
if (/\b(research|audit|synthesis|review|verify|verifier|normalize|plan|impact|spec)\b/.test(text))
|
|
47
|
+
return "long";
|
|
48
|
+
const maxRuntimeMs = task.runtime?.maxRuntimeMs;
|
|
49
|
+
if (maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs)) {
|
|
50
|
+
if (maxRuntimeMs <= 5 * 60_000)
|
|
51
|
+
return "short";
|
|
52
|
+
if (maxRuntimeMs >= 60 * 60_000)
|
|
53
|
+
return "long";
|
|
54
|
+
}
|
|
55
|
+
return "medium";
|
|
56
|
+
}
|
|
57
|
+
function currentRunningTask(tasks) {
|
|
58
|
+
return tasks
|
|
59
|
+
.filter((task) => task.status === "running")
|
|
60
|
+
.sort((left, right) => (parseTime(left.startedAt) ?? Number.POSITIVE_INFINITY) -
|
|
61
|
+
(parseTime(right.startedAt) ?? Number.POSITIVE_INFINITY))[0];
|
|
62
|
+
}
|
|
63
|
+
function completedWorkflowHealth() {
|
|
64
|
+
return {
|
|
65
|
+
state: "completed",
|
|
66
|
+
label: "completed",
|
|
67
|
+
summary: "run completed",
|
|
68
|
+
tone: "success",
|
|
69
|
+
suggestion: "review",
|
|
70
|
+
reason: "all tasks reached a terminal successful state",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function problemWorkflowHealth(status) {
|
|
74
|
+
return {
|
|
75
|
+
state: "needs-action",
|
|
76
|
+
label: "needs action",
|
|
77
|
+
summary: `run ${status}`,
|
|
78
|
+
tone: "error",
|
|
79
|
+
suggestion: "inspect",
|
|
80
|
+
reason: `workflow status is ${status}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function problemRunHealth(task, nowMs) {
|
|
84
|
+
return {
|
|
85
|
+
state: "needs-action",
|
|
86
|
+
label: "needs action",
|
|
87
|
+
summary: `${task.displayName ?? task.taskId ?? "task"} needs attention`,
|
|
88
|
+
tone: "error",
|
|
89
|
+
suggestion: "inspect",
|
|
90
|
+
reason: task.lastMessage ?? task.statusDetail ?? "task did not complete",
|
|
91
|
+
currentTask: taskSummary(task, nowMs),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function waitingWorkflowHealth(run, nowMs) {
|
|
95
|
+
const hasPending = run.taskSummary.pending > 0;
|
|
96
|
+
return {
|
|
97
|
+
state: hasPending ? "pending" : "active",
|
|
98
|
+
label: hasPending ? "pending" : "active",
|
|
99
|
+
summary: hasPending
|
|
100
|
+
? "waiting for the next schedulable task"
|
|
101
|
+
: "run is active",
|
|
102
|
+
tone: hasPending ? "dim" : "accent",
|
|
103
|
+
suggestion: "wait",
|
|
104
|
+
reason: hasPending
|
|
105
|
+
? "no task is currently running"
|
|
106
|
+
: "workflow is still in progress",
|
|
107
|
+
lastActivityAt: run.updatedAt,
|
|
108
|
+
lastActivityAgeMs: ageMs(run.updatedAt, nowMs),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function runningTaskHealth(context) {
|
|
112
|
+
return (runtimeExceededHealth(context) ??
|
|
113
|
+
staleRunningHealth(context) ??
|
|
114
|
+
longTailHealth(context) ??
|
|
115
|
+
activeRunningHealth(context));
|
|
116
|
+
}
|
|
117
|
+
function runtimeExceededHealth(context) {
|
|
118
|
+
const maxRuntimeMs = context.task.runtime?.maxRuntimeMs;
|
|
119
|
+
const hasElapsed = context.elapsedMs !== undefined;
|
|
120
|
+
const hasBudget = maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs);
|
|
121
|
+
if (!hasElapsed || !hasBudget)
|
|
122
|
+
return undefined;
|
|
123
|
+
if (maxRuntimeMs <= 0 || context.elapsedMs <= maxRuntimeMs)
|
|
124
|
+
return undefined;
|
|
125
|
+
return runningHealth(context, {
|
|
126
|
+
state: "likely-stuck",
|
|
127
|
+
label: "runtime exceeded",
|
|
128
|
+
summary: "task exceeded its runtime budget",
|
|
129
|
+
tone: "error",
|
|
130
|
+
suggestion: "resume",
|
|
131
|
+
reason: "elapsed time is past runtime.maxRuntimeMs",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function staleRunningHealth(context) {
|
|
135
|
+
if (context.staleMs >= STUCK_BY_DURATION[context.durationClass] &&
|
|
136
|
+
!context.hasBackendSignal) {
|
|
137
|
+
return runningHealth(context, {
|
|
138
|
+
state: "likely-stuck",
|
|
139
|
+
label: "likely stuck",
|
|
140
|
+
summary: "no fresh backend or activity signal",
|
|
141
|
+
tone: "error",
|
|
142
|
+
suggestion: "resume",
|
|
143
|
+
reason: "running task has no backend signal and activity is stale",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (context.staleMs < STALL_BY_DURATION[context.durationClass])
|
|
147
|
+
return undefined;
|
|
148
|
+
return runningHealth(context, {
|
|
149
|
+
state: "stalled",
|
|
150
|
+
label: "possibly stalled",
|
|
151
|
+
summary: "no recent visible progress",
|
|
152
|
+
tone: "warning",
|
|
153
|
+
suggestion: "inspect",
|
|
154
|
+
reason: context.hasBackendSignal
|
|
155
|
+
? "backend signal exists, but activity is stale"
|
|
156
|
+
: "activity is stale",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function longTailHealth(context) {
|
|
160
|
+
const isLongTail = context.durationClass === "long" &&
|
|
161
|
+
context.elapsedMs !== undefined &&
|
|
162
|
+
context.elapsedMs >= LONG_TAIL_ELAPSED_MS;
|
|
163
|
+
if (!isLongTail)
|
|
164
|
+
return undefined;
|
|
165
|
+
return runningHealth(context, {
|
|
166
|
+
state: "long-tail",
|
|
167
|
+
label: "long-tail active",
|
|
168
|
+
summary: "slow task with fresh liveness signals",
|
|
169
|
+
tone: "accent",
|
|
170
|
+
suggestion: "wait",
|
|
171
|
+
reason: context.staleMs <= ACTIVE_ACTIVITY_MS
|
|
172
|
+
? "liveness signal is fresh"
|
|
173
|
+
: "long-running stage is still within the stale threshold",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function activeRunningHealth(context) {
|
|
177
|
+
return runningHealth(context, {
|
|
178
|
+
state: "active",
|
|
179
|
+
label: "active",
|
|
180
|
+
summary: "task is running",
|
|
181
|
+
tone: "accent",
|
|
182
|
+
suggestion: "wait",
|
|
183
|
+
reason: context.staleMs <= ACTIVE_ACTIVITY_MS
|
|
184
|
+
? "liveness signal is fresh"
|
|
185
|
+
: "activity remains within the expected window",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function runningContext(task, run, nowMs) {
|
|
189
|
+
const durationClass = classifyWorkflowTaskDuration(task);
|
|
190
|
+
const startedAtMs = parseTime(task.startedAt);
|
|
191
|
+
const heartbeatAt = parseHeartbeatAt(task.lastMessage);
|
|
192
|
+
const activityAt = latestIso([heartbeatAt, run?.updatedAt, task.startedAt]);
|
|
193
|
+
const lastActivityAgeMs = ageMs(activityAt, nowMs);
|
|
194
|
+
return {
|
|
195
|
+
task,
|
|
196
|
+
nowMs,
|
|
197
|
+
durationClass,
|
|
198
|
+
elapsedMs: startedAtMs === undefined ? undefined : Math.max(0, nowMs - startedAtMs),
|
|
199
|
+
activityAt,
|
|
200
|
+
lastActivityAgeMs,
|
|
201
|
+
heartbeatAt,
|
|
202
|
+
heartbeatAgeMs: ageMs(heartbeatAt, nowMs),
|
|
203
|
+
hasBackendSignal: Boolean(task.backendHandle || task.pid || heartbeatAt),
|
|
204
|
+
staleMs: lastActivityAgeMs ?? Number.POSITIVE_INFINITY,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function terminalTaskHealth(task, nowMs) {
|
|
208
|
+
if (task.status === "completed" || task.status === "skipped") {
|
|
209
|
+
return {
|
|
210
|
+
state: "completed",
|
|
211
|
+
label: task.status === "skipped" ? "skipped" : "completed",
|
|
212
|
+
summary: task.status === "skipped" ? "task skipped" : "task completed",
|
|
213
|
+
tone: "success",
|
|
214
|
+
suggestion: "review",
|
|
215
|
+
reason: task.statusDetail,
|
|
216
|
+
currentTask: taskSummary(task, nowMs),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (task.status === "pending") {
|
|
220
|
+
return {
|
|
221
|
+
state: "pending",
|
|
222
|
+
label: "pending",
|
|
223
|
+
summary: "waiting for dependencies or scheduler",
|
|
224
|
+
tone: "dim",
|
|
225
|
+
suggestion: "wait",
|
|
226
|
+
reason: task.statusDetail,
|
|
227
|
+
currentTask: taskSummary(task, nowMs),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
state: "needs-action",
|
|
232
|
+
label: "needs action",
|
|
233
|
+
summary: `${task.status} task needs attention`,
|
|
234
|
+
tone: "error",
|
|
235
|
+
suggestion: "inspect",
|
|
236
|
+
reason: task.lastMessage ?? task.statusDetail,
|
|
237
|
+
currentTask: taskSummary(task, nowMs),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function runningHealth(context, health) {
|
|
241
|
+
return {
|
|
242
|
+
...health,
|
|
243
|
+
durationClass: context.durationClass,
|
|
244
|
+
currentTask: taskSummary(context.task, context.nowMs),
|
|
245
|
+
lastActivityAt: context.activityAt,
|
|
246
|
+
lastActivityAgeMs: context.lastActivityAgeMs,
|
|
247
|
+
heartbeatAt: context.heartbeatAt,
|
|
248
|
+
heartbeatAgeMs: context.heartbeatAgeMs,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function taskSummary(task, nowMs) {
|
|
252
|
+
const startedAtMs = parseTime(task.startedAt);
|
|
253
|
+
return {
|
|
254
|
+
taskId: task.taskId,
|
|
255
|
+
displayName: task.displayName,
|
|
256
|
+
stageId: task.stageId,
|
|
257
|
+
status: task.status,
|
|
258
|
+
...(startedAtMs === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: { elapsedMs: Math.max(0, nowMs - startedAtMs) }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function isProblemStatus(status) {
|
|
264
|
+
return (status === "failed" || status === "blocked" || status === "interrupted");
|
|
265
|
+
}
|
|
266
|
+
function parseHeartbeatAt(message) {
|
|
267
|
+
if (!message)
|
|
268
|
+
return undefined;
|
|
269
|
+
const match = /heartbeat\s+(\d{4}-\d{2}-\d{2}T\S+?Z)/i.exec(message);
|
|
270
|
+
if (!match)
|
|
271
|
+
return undefined;
|
|
272
|
+
const value = match[1];
|
|
273
|
+
return parseTime(value) === undefined ? undefined : value;
|
|
274
|
+
}
|
|
275
|
+
function latestIso(values) {
|
|
276
|
+
let latest;
|
|
277
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
278
|
+
for (const value of values) {
|
|
279
|
+
const time = parseTime(value);
|
|
280
|
+
if (time === undefined || time <= latestMs)
|
|
281
|
+
continue;
|
|
282
|
+
latest = value;
|
|
283
|
+
latestMs = time;
|
|
284
|
+
}
|
|
285
|
+
return latest;
|
|
286
|
+
}
|
|
287
|
+
function ageMs(value, nowMs) {
|
|
288
|
+
const time = parseTime(value);
|
|
289
|
+
return time === undefined ? undefined : Math.max(0, nowMs - time);
|
|
290
|
+
}
|
|
291
|
+
function parseTime(value) {
|
|
292
|
+
if (!value)
|
|
293
|
+
return undefined;
|
|
294
|
+
const parsed = Date.parse(value);
|
|
295
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
296
|
+
}
|
|
@@ -11,9 +11,15 @@ export interface WorkflowRuntimeDefaults {
|
|
|
11
11
|
model?: string;
|
|
12
12
|
thinking?: ThinkingLevel;
|
|
13
13
|
}
|
|
14
|
+
export interface WorkflowRuntimeThinkingResolution {
|
|
15
|
+
requested?: ThinkingLevel;
|
|
16
|
+
resolved?: ThinkingLevel;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
14
19
|
export interface WorkflowRuntimeResolutionInput {
|
|
15
20
|
model?: string;
|
|
16
21
|
thinking?: ThinkingLevel;
|
|
22
|
+
thinkingResolution?: WorkflowRuntimeThinkingResolution;
|
|
17
23
|
}
|
|
18
24
|
export interface WorkflowRuntimeResolutionContext {
|
|
19
25
|
taskKey: string;
|
package/dist/workflow-runtime.js
CHANGED
|
@@ -15,10 +15,13 @@ export async function resolveWorkflowRuntime(runtime, context, options) {
|
|
|
15
15
|
: { baseModel: undefined, thinking: undefined };
|
|
16
16
|
const model = await resolveModel(baseModel, context, options);
|
|
17
17
|
const effectiveThinking = runtime.thinking ?? thinking ?? options.defaults?.thinking;
|
|
18
|
-
const
|
|
18
|
+
const thinkingResolution = await resolveThinking(model, effectiveThinking, context, options);
|
|
19
19
|
return {
|
|
20
20
|
...(model ? { model } : {}),
|
|
21
|
-
...(
|
|
21
|
+
...(thinkingResolution?.resolved
|
|
22
|
+
? { thinking: thinkingResolution.resolved }
|
|
23
|
+
: {}),
|
|
24
|
+
...(thinkingResolution ? { thinkingResolution } : {}),
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
export function splitKnownThinkingSuffix(model) {
|
|
@@ -101,21 +104,41 @@ async function resolveThinking(modelId, requested, context, options) {
|
|
|
101
104
|
return undefined;
|
|
102
105
|
const model = findModelInfo(modelId, options.availableModels ?? []);
|
|
103
106
|
const supported = getSupportedThinkingLevels(model);
|
|
104
|
-
if (supported.includes(requested))
|
|
105
|
-
return requested;
|
|
106
|
-
if (!options.prompt) {
|
|
107
|
-
const modelLabel = modelId ?? "selected model";
|
|
108
|
-
throw new Error(`${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}. Supported: ${supported.join(", ") || "none"}`);
|
|
107
|
+
if (supported.includes(requested)) {
|
|
108
|
+
return { requested, resolved: requested };
|
|
109
109
|
}
|
|
110
110
|
if (supported.length === 0) {
|
|
111
111
|
throw new Error(`${modelId ?? "selected model"} does not expose any supported reasoning levels for ${context.taskKey}`);
|
|
112
112
|
}
|
|
113
|
-
const
|
|
113
|
+
const downgradeOptions = lowerOrEqualSupportedThinking(requested, supported);
|
|
114
|
+
if (downgradeOptions.length === 0) {
|
|
115
|
+
const modelLabel = modelId ?? "selected model";
|
|
116
|
+
throw new Error(`${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}, and no lower-or-equal fallback is available. Supported: ${supported.join(", ") || "none"}`);
|
|
117
|
+
}
|
|
118
|
+
if (!options.prompt) {
|
|
119
|
+
const resolved = downgradeOptions[downgradeOptions.length - 1];
|
|
120
|
+
return {
|
|
121
|
+
requested,
|
|
122
|
+
resolved,
|
|
123
|
+
reason: `requested ${requested} is unsupported by ${modelId ?? "selected model"}; using ${resolved}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const selected = await options.prompt.select(`${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported lower-or-equal level.`, downgradeOptions);
|
|
114
127
|
if (!selected)
|
|
115
128
|
throw new Error(`Reasoning selection cancelled for ${context.taskKey}`);
|
|
116
|
-
if (!isThinkingLevel(selected))
|
|
129
|
+
if (!isThinkingLevel(selected) || !downgradeOptions.includes(selected))
|
|
117
130
|
throw new Error(`Invalid reasoning selection "${selected}" for ${context.taskKey}`);
|
|
118
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
requested,
|
|
133
|
+
resolved: selected,
|
|
134
|
+
reason: `selected supported reasoning ${selected} for unsupported request ${requested}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function lowerOrEqualSupportedThinking(requested, supported) {
|
|
138
|
+
const requestedIndex = THINKING_LEVELS.indexOf(requested);
|
|
139
|
+
if (requestedIndex < 0)
|
|
140
|
+
return [];
|
|
141
|
+
return THINKING_LEVELS.slice(0, requestedIndex + 1).filter((level) => supported.includes(level));
|
|
119
142
|
}
|
|
120
143
|
function findModelInfo(modelId, available) {
|
|
121
144
|
if (!modelId)
|
package/dist/workflow-view.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare class WorkflowView implements Component {
|
|
|
62
62
|
private taskIdentityLines;
|
|
63
63
|
private taskOverviewLines;
|
|
64
64
|
private taskTimelineLines;
|
|
65
|
+
private taskHealthLines;
|
|
65
66
|
private taskValidationStripLines;
|
|
66
67
|
private taskArtifactViewerLines;
|
|
67
68
|
private currentArtifactSourceLines;
|
|
@@ -83,6 +84,7 @@ export declare class WorkflowView implements Component {
|
|
|
83
84
|
private syncSelectedTaskId;
|
|
84
85
|
private breadcrumbText;
|
|
85
86
|
private runSummaryLines;
|
|
87
|
+
private runHealthLines;
|
|
86
88
|
private runDetailSummaryLines;
|
|
87
89
|
private stageContextLines;
|
|
88
90
|
private footerText;
|
package/dist/workflow-view.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { workflowRunPath, fromProjectPath, listRunRecords, readIndex, readRunRecord, } from "./store.js";
|
|
4
|
+
import { diagnoseWorkflowRunHealth, diagnoseWorkflowTaskHealth, } from "./workflow-progress-health.js";
|
|
4
5
|
import { WORKFLOW_RUN_TYPE, } from "./types.js";
|
|
5
6
|
const REFRESH_INTERVAL_MS = 1_000;
|
|
6
7
|
const MAX_LIST_ROWS = 18;
|
|
@@ -277,14 +278,18 @@ export class WorkflowView {
|
|
|
277
278
|
}
|
|
278
279
|
renderRunsScreen(width) {
|
|
279
280
|
const selected = this.flows[this.selectedFlow];
|
|
281
|
+
const selectedDetail = selected && this.detailRun?.runId === selected.runId
|
|
282
|
+
? this.detailRun
|
|
283
|
+
: undefined;
|
|
280
284
|
const sideLines = [
|
|
281
285
|
accent(this.theme, "All runs"),
|
|
282
286
|
kvRow(this.theme, "total", String(this.flows.length)),
|
|
283
287
|
kvRow(this.theme, "running", String(this.flows.filter((flow) => flow.status === "running").length)),
|
|
288
|
+
kvRow(this.theme, "needs action", String(this.flows.filter((flow) => ["failed", "blocked", "interrupted"].includes(flow.status)).length)),
|
|
284
289
|
"",
|
|
285
290
|
accent(this.theme, "Selected"),
|
|
286
291
|
...(selected
|
|
287
|
-
? this.runSummaryLines(selected)
|
|
292
|
+
? this.runSummaryLines(selected, selectedDetail)
|
|
288
293
|
: [placeholder(this.theme, "none")]),
|
|
289
294
|
];
|
|
290
295
|
return this.renderTwoPane(width, "Filters / Summary", sideLines, "Runs", this.runLines(Math.max(1, this.mainPaneBodyWidth(width))), 32);
|
|
@@ -331,9 +336,10 @@ export class WorkflowView {
|
|
|
331
336
|
return width - leftWidth - 5;
|
|
332
337
|
}
|
|
333
338
|
renderTaskDetail(width, run, task) {
|
|
339
|
+
const taskHealth = diagnoseWorkflowTaskHealth(task, run);
|
|
334
340
|
const lines = [
|
|
335
341
|
...boxed(this.theme, "Task Detail", width, [
|
|
336
|
-
`${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${muted(this.theme, this.breadcrumbText())}`,
|
|
342
|
+
`${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${healthInline(this.theme, taskHealth)} ${muted(this.theme, this.breadcrumbText())}`,
|
|
337
343
|
taskMetaLine(this.theme, [
|
|
338
344
|
["agent", task.agent],
|
|
339
345
|
["stage", task.stageId ?? "(none)"],
|
|
@@ -343,6 +349,10 @@ export class WorkflowView {
|
|
|
343
349
|
], statusColor(task.status)),
|
|
344
350
|
"",
|
|
345
351
|
];
|
|
352
|
+
const healthLines = this.taskHealthLines(taskHealth, width - 4);
|
|
353
|
+
if (healthLines.length > 0) {
|
|
354
|
+
lines.push(...boxed(this.theme, "Health", width, healthLines, healthColor(taskHealth)), "");
|
|
355
|
+
}
|
|
346
356
|
const validationLines = this.taskValidationStripLines(task, width - 4);
|
|
347
357
|
if (validationLines.length > 0) {
|
|
348
358
|
lines.push(...boxed(this.theme, "Validation", width, validationLines, task.outputValidation?.status === "invalid" ||
|
|
@@ -388,13 +398,15 @@ export class WorkflowView {
|
|
|
388
398
|
const name = flow.name ?? flow.type;
|
|
389
399
|
const left = `${prefix}${marker} ${selected ? strong(this.theme, name) : name}`;
|
|
390
400
|
const runIdText = shortId(flow.runId).slice(0, 16).padEnd(16, " ");
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
401
|
+
const detailRun = this.detailRun?.runId === flow.runId ? this.detailRun : undefined;
|
|
402
|
+
const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
|
|
403
|
+
const healthText = health.state === "completed"
|
|
404
|
+
? ""
|
|
405
|
+
: ` ${muted(this.theme, "·")} ${healthLabel(this.theme, health)}`;
|
|
394
406
|
const baseRight = `${statusColumn(this.theme, flow.status, runStatusLabel(flow), statusWidth)} ${progressBar(this.theme, flow.taskSummary, 5)} ${metaValue(this.theme, runIdText)}`;
|
|
395
407
|
const right = width >= 90
|
|
396
|
-
? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${
|
|
397
|
-
: `${baseRight}${
|
|
408
|
+
? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${healthText}`
|
|
409
|
+
: `${baseRight}${healthText}`;
|
|
398
410
|
const line = joinColumns(left, right, width, 17);
|
|
399
411
|
lines.push(selectedLine(this.theme, line, width, selected, true));
|
|
400
412
|
}
|
|
@@ -437,7 +449,7 @@ export class WorkflowView {
|
|
|
437
449
|
const selected = index === this.selectedTask;
|
|
438
450
|
const prefix = selected ? accent(this.theme, "› ") : " ";
|
|
439
451
|
const left = `${prefix}${statusGlyph(this.theme, task.status)} ${selected ? strong(this.theme, task.displayName) : task.displayName}`;
|
|
440
|
-
const right = taskListStatusLabel(this.theme, task);
|
|
452
|
+
const right = taskListStatusLabel(this.theme, task, diagnoseWorkflowTaskHealth(task, run));
|
|
441
453
|
const line = joinColumns(left, metaByStatus(this.theme, task.status, right), width, Math.max(22, Math.floor(width * 0.45)));
|
|
442
454
|
lines.push(selectedLine(this.theme, line, width, selected, true));
|
|
443
455
|
}
|
|
@@ -487,6 +499,24 @@ export class WorkflowView {
|
|
|
487
499
|
: "warning"));
|
|
488
500
|
return lines.map((line) => fit(line, width));
|
|
489
501
|
}
|
|
502
|
+
taskHealthLines(health, width) {
|
|
503
|
+
if (health.state === "completed" || health.state === "pending")
|
|
504
|
+
return [];
|
|
505
|
+
const lines = [
|
|
506
|
+
`${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
|
|
507
|
+
kvRow(this.theme, "suggested", health.suggestion, healthColor(health)),
|
|
508
|
+
kvRow(this.theme, "why", health.reason),
|
|
509
|
+
];
|
|
510
|
+
if (health.currentTask?.elapsedMs !== undefined)
|
|
511
|
+
lines.splice(1, 0, kvRow(this.theme, "elapsed", formatDuration(health.currentTask.elapsedMs)));
|
|
512
|
+
if (health.durationClass)
|
|
513
|
+
lines.push(kvRow(this.theme, "duration", `${health.durationClass} expected`));
|
|
514
|
+
if (health.heartbeatAgeMs !== undefined)
|
|
515
|
+
lines.push(kvRow(this.theme, "heartbeat", `${formatDuration(health.heartbeatAgeMs)} ago`));
|
|
516
|
+
if (health.lastActivityAgeMs !== undefined)
|
|
517
|
+
lines.push(kvRow(this.theme, "activity", `${formatDuration(health.lastActivityAgeMs)} ago`));
|
|
518
|
+
return lines.map((line) => fit(line, width));
|
|
519
|
+
}
|
|
490
520
|
taskValidationStripLines(task, width) {
|
|
491
521
|
const summary = taskValidationSummary(task);
|
|
492
522
|
if (!summary)
|
|
@@ -502,9 +532,7 @@ export class WorkflowView {
|
|
|
502
532
|
const total = sourceLines.length;
|
|
503
533
|
const maxStart = Math.max(0, total - TASK_ARTIFACT_VIEW_LINES);
|
|
504
534
|
const start = Math.min(this.artifactScrollLine, maxStart);
|
|
505
|
-
const end = total === 0
|
|
506
|
-
? 0
|
|
507
|
-
: Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
|
|
535
|
+
const end = total === 0 ? 0 : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
|
|
508
536
|
const visible = total === 0
|
|
509
537
|
? [
|
|
510
538
|
this.taskArtifactView === "output"
|
|
@@ -686,7 +714,8 @@ export class WorkflowView {
|
|
|
686
714
|
return this.tasksForSelectedStage(this.detailRun)[this.selectedTask];
|
|
687
715
|
}
|
|
688
716
|
syncSelectedTaskId(tasks) {
|
|
689
|
-
const stageTasks = tasks ??
|
|
717
|
+
const stageTasks = tasks ??
|
|
718
|
+
(this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
|
|
690
719
|
this.selectedTaskId = stageTasks[this.selectedTask]?.taskId ?? "";
|
|
691
720
|
}
|
|
692
721
|
breadcrumbText() {
|
|
@@ -704,7 +733,8 @@ export class WorkflowView {
|
|
|
704
733
|
parts.push(task.displayName);
|
|
705
734
|
return parts.join(" › ");
|
|
706
735
|
}
|
|
707
|
-
runSummaryLines(flow) {
|
|
736
|
+
runSummaryLines(flow, detailRun) {
|
|
737
|
+
const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
|
|
708
738
|
return [
|
|
709
739
|
`${statusGlyph(this.theme, flow.status)} ${strong(this.theme, flow.name ?? flow.type)} ${statusBadge(this.theme, flow.status, runStatusLabel(flow))}`,
|
|
710
740
|
progressBar(this.theme, flow.taskSummary, 8),
|
|
@@ -723,9 +753,28 @@ export class WorkflowView {
|
|
|
723
753
|
elapsedText(flow.createdAt, flow.updatedAt, flow.status === "running"),
|
|
724
754
|
],
|
|
725
755
|
]),
|
|
756
|
+
...this.runHealthLines(health),
|
|
757
|
+
];
|
|
758
|
+
}
|
|
759
|
+
runHealthLines(health) {
|
|
760
|
+
if (health.state === "completed")
|
|
761
|
+
return [];
|
|
762
|
+
const lines = [
|
|
763
|
+
"",
|
|
764
|
+
accent(this.theme, "Health"),
|
|
765
|
+
`${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
|
|
726
766
|
];
|
|
767
|
+
if (health.currentTask?.displayName)
|
|
768
|
+
lines.push(kvRow(this.theme, "current", health.currentTask.displayName));
|
|
769
|
+
if (health.lastActivityAgeMs !== undefined)
|
|
770
|
+
lines.push(kvRow(this.theme, "activity", `${formatDuration(health.lastActivityAgeMs)} ago`));
|
|
771
|
+
if (health.heartbeatAgeMs !== undefined)
|
|
772
|
+
lines.push(kvRow(this.theme, "heartbeat", `${formatDuration(health.heartbeatAgeMs)} ago`));
|
|
773
|
+
lines.push(kvRow(this.theme, "suggested", health.suggestion, healthColor(health)));
|
|
774
|
+
return lines;
|
|
727
775
|
}
|
|
728
776
|
runDetailSummaryLines(run) {
|
|
777
|
+
const health = diagnoseWorkflowRunHealth(run);
|
|
729
778
|
const lines = [
|
|
730
779
|
`${statusGlyph(this.theme, run.status)} ${strong(this.theme, run.name ?? run.type)} ${statusBadge(this.theme, run.status)}`,
|
|
731
780
|
progressBar(this.theme, run.taskSummary, 10),
|
|
@@ -745,6 +794,7 @@ export class WorkflowView {
|
|
|
745
794
|
["updated", timestampText(run.updatedAt)],
|
|
746
795
|
]),
|
|
747
796
|
kvRow(this.theme, "run", shortId(run.runId)),
|
|
797
|
+
...this.runHealthLines(health),
|
|
748
798
|
];
|
|
749
799
|
if (run.fanout && run.fanout.length > 0) {
|
|
750
800
|
lines.push("", accent(this.theme, "Fanout"));
|
|
@@ -1034,7 +1084,29 @@ function statusColor(status) {
|
|
|
1034
1084
|
function statusText(status) {
|
|
1035
1085
|
return status;
|
|
1036
1086
|
}
|
|
1037
|
-
function
|
|
1087
|
+
function healthColor(health) {
|
|
1088
|
+
return health.tone;
|
|
1089
|
+
}
|
|
1090
|
+
function healthGlyph(theme, health) {
|
|
1091
|
+
if (health.tone === "success")
|
|
1092
|
+
return success(theme, "✓");
|
|
1093
|
+
if (health.tone === "warning")
|
|
1094
|
+
return warning(theme, "●");
|
|
1095
|
+
if (health.tone === "error")
|
|
1096
|
+
return errorText(theme, "●");
|
|
1097
|
+
if (health.tone === "dim")
|
|
1098
|
+
return muted(theme, "•");
|
|
1099
|
+
return accent(theme, "●");
|
|
1100
|
+
}
|
|
1101
|
+
function healthLabel(theme, health) {
|
|
1102
|
+
return fg(theme, healthColor(health), strong(theme, health.label));
|
|
1103
|
+
}
|
|
1104
|
+
function healthInline(theme, health) {
|
|
1105
|
+
if (health.state === "completed" || health.state === "pending")
|
|
1106
|
+
return "";
|
|
1107
|
+
return `${healthGlyph(theme, health)} ${healthLabel(theme, health)}`;
|
|
1108
|
+
}
|
|
1109
|
+
function taskListStatusLabel(theme, task, health) {
|
|
1038
1110
|
const validation = taskValidationSummary(task);
|
|
1039
1111
|
const label = validation?.status === "invalid"
|
|
1040
1112
|
? "invalid output"
|
|
@@ -1042,8 +1114,13 @@ function taskListStatusLabel(theme, task) {
|
|
|
1042
1114
|
? "valid"
|
|
1043
1115
|
: task.status === "completed"
|
|
1044
1116
|
? "done"
|
|
1045
|
-
:
|
|
1046
|
-
|
|
1117
|
+
: task.status === "running"
|
|
1118
|
+
? health.label
|
|
1119
|
+
: statusText(task.status);
|
|
1120
|
+
const suffix = task.status === "running" && health.currentTask?.elapsedMs !== undefined
|
|
1121
|
+
? ` ${muted(theme, "·")} ${metaValue(theme, formatDuration(health.currentTask.elapsedMs))}`
|
|
1122
|
+
: "";
|
|
1123
|
+
return `${fg(theme, task.status === "running" ? healthColor(health) : statusColor(task.status), strong(theme, label))}${suffix}`;
|
|
1047
1124
|
}
|
|
1048
1125
|
function compactStatusLabel(theme, status) {
|
|
1049
1126
|
const label = status === "completed" ? "done" : statusText(status);
|
|
@@ -1107,7 +1184,7 @@ function taskValidationSummary(task) {
|
|
|
1107
1184
|
: undefined;
|
|
1108
1185
|
const issueMessage = typeof issue === "string"
|
|
1109
1186
|
? issue
|
|
1110
|
-
: issue?.message ?? issue?.path ?? issue?.code ?? "";
|
|
1187
|
+
: (issue?.message ?? issue?.path ?? issue?.code ?? "");
|
|
1111
1188
|
const message = validation.message ?? validation.reason ?? issueMessage;
|
|
1112
1189
|
if (status === "valid" && !message)
|
|
1113
1190
|
return undefined;
|
|
@@ -1263,7 +1340,9 @@ function truncateToWidth(text, width) {
|
|
|
1263
1340
|
return "";
|
|
1264
1341
|
if (visibleWidth(text) <= safeWidth)
|
|
1265
1342
|
return text;
|
|
1266
|
-
const hasAnsi = text.includes("\u001b[") ||
|
|
1343
|
+
const hasAnsi = text.includes("\u001b[") ||
|
|
1344
|
+
text.includes("\u001b]") ||
|
|
1345
|
+
text.includes("\u001b_");
|
|
1267
1346
|
const ellipsis = "…";
|
|
1268
1347
|
const ellipsisWidth = visibleWidth(ellipsis);
|
|
1269
1348
|
const limit = Math.max(0, safeWidth - ellipsisWidth);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type WorkflowWebSecurityPolicy, type WorkflowWebSourceCacheConfig, type WorkflowWebSourcePolicy } from "./workflow-web-source.js";
|
|
2
|
+
export declare const WORKFLOW_WEB_SOURCE_LAUNCH_CONFIG_SCHEMA: "workflow-web-source-launch-config-v1";
|
|
3
|
+
export interface WorkflowWebProviderLaunchConfig {
|
|
4
|
+
kind: "pi-web-access" | "extension" | "none";
|
|
5
|
+
extensionPath?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface WorkflowWebSourceLaunchConfig extends WorkflowWebSourceCacheConfig {
|
|
8
|
+
schema: typeof WORKFLOW_WEB_SOURCE_LAUNCH_CONFIG_SCHEMA;
|
|
9
|
+
workflowName?: string;
|
|
10
|
+
stageId?: string;
|
|
11
|
+
taskKey?: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
provider: WorkflowWebProviderLaunchConfig;
|
|
14
|
+
webSourcePolicy?: Partial<WorkflowWebSourcePolicy>;
|
|
15
|
+
securityPolicy?: Partial<WorkflowWebSecurityPolicy>;
|
|
16
|
+
exposeLegacyTools?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkflowWebSourceExtensionWrapperOptions {
|
|
19
|
+
wrapperPath: string;
|
|
20
|
+
importPath: string;
|
|
21
|
+
providerExtensionPath?: string;
|
|
22
|
+
config: WorkflowWebSourceLaunchConfig;
|
|
23
|
+
}
|
|
24
|
+
type ToolResult = {
|
|
25
|
+
content?: Array<Record<string, unknown>>;
|
|
26
|
+
details?: Record<string, unknown>;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
type ToolSpec = {
|
|
30
|
+
name?: string;
|
|
31
|
+
execute?: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: unknown) => Promise<ToolResult>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
type PiLike = Record<string | symbol, unknown> & {
|
|
35
|
+
registerTool(tool: ToolSpec): void;
|
|
36
|
+
appendEntry?(type: string, data: unknown): void;
|
|
37
|
+
};
|
|
38
|
+
type ProviderExtension = (pi: PiLike) => void;
|
|
39
|
+
export declare function registerWorkflowWebSourceExtension(pi: PiLike, config: WorkflowWebSourceLaunchConfig, providerExtension?: ProviderExtension): void;
|
|
40
|
+
export declare function buildWorkflowWebSourceExtensionWrapper(options: Omit<WorkflowWebSourceExtensionWrapperOptions, "wrapperPath">): string;
|
|
41
|
+
export declare function writeWorkflowWebSourceExtensionWrapper(options: WorkflowWebSourceExtensionWrapperOptions): Promise<string>;
|
|
42
|
+
export declare function workflowWebSourceModuleImportPath(modulePath: string): string;
|
|
43
|
+
export {};
|