@agwab/pi-workflow 0.1.2 → 0.2.1
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 +9 -13
- package/dist/compiler.d.ts +5 -5
- package/dist/compiler.js +82 -24
- package/dist/dynamic-generated-task-runtime.d.ts +2 -0
- package/dist/dynamic-generated-task-runtime.js +21 -8
- package/dist/engine.d.ts +6 -5
- package/dist/engine.js +39 -54
- package/dist/extension.js +211 -24
- package/dist/store.d.ts +3 -1
- package/dist/store.js +135 -38
- package/dist/subagent-backend.d.ts +4 -0
- package/dist/subagent-backend.js +128 -4
- package/dist/types.d.ts +5 -0
- package/dist/workflow-progress-health.d.ts +37 -0
- package/dist/workflow-progress-health.js +296 -0
- package/dist/workflow-runtime.d.ts +8 -0
- package/dist/workflow-runtime.js +63 -10
- package/dist/workflow-view.d.ts +2 -0
- package/dist/workflow-view.js +97 -18
- package/dist/workflow-web-source.js +32 -14
- package/docs/usage.md +12 -1
- package/package.json +6 -6
- package/src/compiler.ts +136 -41
- package/src/dynamic-generated-task-runtime.ts +47 -12
- package/src/engine.ts +55 -100
- package/src/extension.ts +270 -34
- package/src/store.ts +180 -44
- package/src/subagent-backend.ts +170 -6
- package/src/types.ts +10 -0
- package/src/workflow-progress-health.ts +461 -0
- package/src/workflow-runtime.ts +85 -13
- package/src/workflow-view.ts +186 -41
- package/src/workflow-web-source.ts +192 -69
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +111 -37
- package/workflows/deep-research/helpers/final-audit-packet.mjs +191 -14
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +159 -50
- package/workflows/deep-research/helpers/render-executive.mjs +671 -37
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +2 -0
- package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
- package/workflows/deep-research/spec.json +41 -11
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TaskRunStatus,
|
|
3
|
+
WorkflowRunRecord,
|
|
4
|
+
WorkflowRunStatus,
|
|
5
|
+
WorkflowTaskRunRecord,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
|
|
8
|
+
export type WorkflowHealthState =
|
|
9
|
+
| "completed"
|
|
10
|
+
| "pending"
|
|
11
|
+
| "active"
|
|
12
|
+
| "long-tail"
|
|
13
|
+
| "stalled"
|
|
14
|
+
| "likely-stuck"
|
|
15
|
+
| "needs-action";
|
|
16
|
+
|
|
17
|
+
export type WorkflowHealthTone =
|
|
18
|
+
| "success"
|
|
19
|
+
| "accent"
|
|
20
|
+
| "warning"
|
|
21
|
+
| "error"
|
|
22
|
+
| "dim";
|
|
23
|
+
export type WorkflowDurationClass = "short" | "medium" | "long";
|
|
24
|
+
export type WorkflowHealthSuggestion = "wait" | "inspect" | "resume" | "review";
|
|
25
|
+
|
|
26
|
+
export interface WorkflowHealthTaskSummary {
|
|
27
|
+
taskId?: string;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
stageId?: string;
|
|
30
|
+
status?: TaskRunStatus;
|
|
31
|
+
elapsedMs?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WorkflowProgressHealth {
|
|
35
|
+
state: WorkflowHealthState;
|
|
36
|
+
label: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
tone: WorkflowHealthTone;
|
|
39
|
+
suggestion: WorkflowHealthSuggestion;
|
|
40
|
+
reason: string;
|
|
41
|
+
durationClass?: WorkflowDurationClass;
|
|
42
|
+
currentTask?: WorkflowHealthTaskSummary;
|
|
43
|
+
lastActivityAt?: string;
|
|
44
|
+
lastActivityAgeMs?: number;
|
|
45
|
+
heartbeatAt?: string;
|
|
46
|
+
heartbeatAgeMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type TaskHealthInput = Pick<
|
|
50
|
+
WorkflowTaskRunRecord,
|
|
51
|
+
| "taskId"
|
|
52
|
+
| "specId"
|
|
53
|
+
| "displayName"
|
|
54
|
+
| "status"
|
|
55
|
+
| "statusDetail"
|
|
56
|
+
| "stageId"
|
|
57
|
+
| "kind"
|
|
58
|
+
| "startedAt"
|
|
59
|
+
| "lastMessage"
|
|
60
|
+
| "runtime"
|
|
61
|
+
| "backendHandle"
|
|
62
|
+
| "pid"
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
type RunHealthInput = Pick<
|
|
66
|
+
WorkflowRunRecord,
|
|
67
|
+
"status" | "taskSummary" | "createdAt" | "updatedAt"
|
|
68
|
+
> & {
|
|
69
|
+
tasks?: TaskHealthInput[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type RunningTaskContext = {
|
|
73
|
+
task: TaskHealthInput;
|
|
74
|
+
nowMs: number;
|
|
75
|
+
durationClass: WorkflowDurationClass;
|
|
76
|
+
elapsedMs?: number;
|
|
77
|
+
activityAt?: string;
|
|
78
|
+
lastActivityAgeMs?: number;
|
|
79
|
+
heartbeatAt?: string;
|
|
80
|
+
heartbeatAgeMs?: number;
|
|
81
|
+
hasBackendSignal: boolean;
|
|
82
|
+
staleMs: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export interface WorkflowHealthOptions {
|
|
86
|
+
nowMs?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ACTIVE_ACTIVITY_MS = 2 * 60_000;
|
|
90
|
+
const LONG_TAIL_ELAPSED_MS = 8 * 60_000;
|
|
91
|
+
const STALL_BY_DURATION: Record<WorkflowDurationClass, number> = {
|
|
92
|
+
short: 5 * 60_000,
|
|
93
|
+
medium: 10 * 60_000,
|
|
94
|
+
long: 20 * 60_000,
|
|
95
|
+
};
|
|
96
|
+
const STUCK_BY_DURATION: Record<WorkflowDurationClass, number> = {
|
|
97
|
+
short: 15 * 60_000,
|
|
98
|
+
medium: 30 * 60_000,
|
|
99
|
+
long: 60 * 60_000,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function diagnoseWorkflowRunHealth(
|
|
103
|
+
run: RunHealthInput,
|
|
104
|
+
options: WorkflowHealthOptions = {},
|
|
105
|
+
): WorkflowProgressHealth {
|
|
106
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
107
|
+
const runningTask = currentRunningTask(run.tasks ?? []);
|
|
108
|
+
if (runningTask)
|
|
109
|
+
return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
|
|
110
|
+
|
|
111
|
+
const problem = (run.tasks ?? []).find((task) =>
|
|
112
|
+
isProblemStatus(task.status),
|
|
113
|
+
);
|
|
114
|
+
if (problem) return problemRunHealth(problem, nowMs);
|
|
115
|
+
if (isProblemStatus(run.status)) return problemWorkflowHealth(run.status);
|
|
116
|
+
if (run.status === "completed") return completedWorkflowHealth();
|
|
117
|
+
return waitingWorkflowHealth(run, nowMs);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function diagnoseWorkflowTaskHealth(
|
|
121
|
+
task: TaskHealthInput,
|
|
122
|
+
run?: Pick<WorkflowRunRecord, "updatedAt">,
|
|
123
|
+
options: WorkflowHealthOptions = {},
|
|
124
|
+
): WorkflowProgressHealth {
|
|
125
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
126
|
+
if (task.status !== "running") return terminalTaskHealth(task, nowMs);
|
|
127
|
+
return runningTaskHealth(runningContext(task, run, nowMs));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function classifyWorkflowTaskDuration(
|
|
131
|
+
task: Pick<
|
|
132
|
+
WorkflowTaskRunRecord,
|
|
133
|
+
"stageId" | "displayName" | "specId" | "kind" | "statusDetail" | "runtime"
|
|
134
|
+
>,
|
|
135
|
+
): WorkflowDurationClass {
|
|
136
|
+
const text = [
|
|
137
|
+
task.stageId,
|
|
138
|
+
task.displayName,
|
|
139
|
+
task.specId,
|
|
140
|
+
task.kind,
|
|
141
|
+
task.statusDetail,
|
|
142
|
+
]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" ")
|
|
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
|
+
const maxRuntimeMs = task.runtime?.maxRuntimeMs;
|
|
155
|
+
if (maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs)) {
|
|
156
|
+
if (maxRuntimeMs <= 5 * 60_000) return "short";
|
|
157
|
+
if (maxRuntimeMs >= 60 * 60_000) return "long";
|
|
158
|
+
}
|
|
159
|
+
return "medium";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function currentRunningTask(
|
|
163
|
+
tasks: TaskHealthInput[],
|
|
164
|
+
): TaskHealthInput | undefined {
|
|
165
|
+
return tasks
|
|
166
|
+
.filter((task) => task.status === "running")
|
|
167
|
+
.sort(
|
|
168
|
+
(left, right) =>
|
|
169
|
+
(parseTime(left.startedAt) ?? Number.POSITIVE_INFINITY) -
|
|
170
|
+
(parseTime(right.startedAt) ?? Number.POSITIVE_INFINITY),
|
|
171
|
+
)[0];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function completedWorkflowHealth(): WorkflowProgressHealth {
|
|
175
|
+
return {
|
|
176
|
+
state: "completed",
|
|
177
|
+
label: "completed",
|
|
178
|
+
summary: "run completed",
|
|
179
|
+
tone: "success",
|
|
180
|
+
suggestion: "review",
|
|
181
|
+
reason: "all tasks reached a terminal successful state",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function problemWorkflowHealth(
|
|
186
|
+
status: WorkflowRunStatus,
|
|
187
|
+
): WorkflowProgressHealth {
|
|
188
|
+
return {
|
|
189
|
+
state: "needs-action",
|
|
190
|
+
label: "needs action",
|
|
191
|
+
summary: `run ${status}`,
|
|
192
|
+
tone: "error",
|
|
193
|
+
suggestion: "inspect",
|
|
194
|
+
reason: `workflow status is ${status}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function problemRunHealth(
|
|
199
|
+
task: TaskHealthInput,
|
|
200
|
+
nowMs: number,
|
|
201
|
+
): WorkflowProgressHealth {
|
|
202
|
+
return {
|
|
203
|
+
state: "needs-action",
|
|
204
|
+
label: "needs action",
|
|
205
|
+
summary: `${task.displayName ?? task.taskId ?? "task"} needs attention`,
|
|
206
|
+
tone: "error",
|
|
207
|
+
suggestion: "inspect",
|
|
208
|
+
reason: task.lastMessage ?? task.statusDetail ?? "task did not complete",
|
|
209
|
+
currentTask: taskSummary(task, nowMs),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function waitingWorkflowHealth(
|
|
214
|
+
run: RunHealthInput,
|
|
215
|
+
nowMs: number,
|
|
216
|
+
): WorkflowProgressHealth {
|
|
217
|
+
const hasPending = run.taskSummary.pending > 0;
|
|
218
|
+
return {
|
|
219
|
+
state: hasPending ? "pending" : "active",
|
|
220
|
+
label: hasPending ? "pending" : "active",
|
|
221
|
+
summary: hasPending
|
|
222
|
+
? "waiting for the next schedulable task"
|
|
223
|
+
: "run is active",
|
|
224
|
+
tone: hasPending ? "dim" : "accent",
|
|
225
|
+
suggestion: "wait",
|
|
226
|
+
reason: hasPending
|
|
227
|
+
? "no task is currently running"
|
|
228
|
+
: "workflow is still in progress",
|
|
229
|
+
lastActivityAt: run.updatedAt,
|
|
230
|
+
lastActivityAgeMs: ageMs(run.updatedAt, nowMs),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function runningTaskHealth(
|
|
235
|
+
context: RunningTaskContext,
|
|
236
|
+
): WorkflowProgressHealth {
|
|
237
|
+
return (
|
|
238
|
+
runtimeExceededHealth(context) ??
|
|
239
|
+
staleRunningHealth(context) ??
|
|
240
|
+
longTailHealth(context) ??
|
|
241
|
+
activeRunningHealth(context)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function runtimeExceededHealth(
|
|
246
|
+
context: RunningTaskContext,
|
|
247
|
+
): WorkflowProgressHealth | undefined {
|
|
248
|
+
const maxRuntimeMs = context.task.runtime?.maxRuntimeMs;
|
|
249
|
+
const hasElapsed = context.elapsedMs !== undefined;
|
|
250
|
+
const hasBudget = maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs);
|
|
251
|
+
if (!hasElapsed || !hasBudget) return undefined;
|
|
252
|
+
if (maxRuntimeMs <= 0 || context.elapsedMs! <= maxRuntimeMs) return undefined;
|
|
253
|
+
return runningHealth(context, {
|
|
254
|
+
state: "likely-stuck",
|
|
255
|
+
label: "runtime exceeded",
|
|
256
|
+
summary: "task exceeded its runtime budget",
|
|
257
|
+
tone: "error",
|
|
258
|
+
suggestion: "resume",
|
|
259
|
+
reason: "elapsed time is past runtime.maxRuntimeMs",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function staleRunningHealth(
|
|
264
|
+
context: RunningTaskContext,
|
|
265
|
+
): WorkflowProgressHealth | undefined {
|
|
266
|
+
if (
|
|
267
|
+
context.staleMs >= STUCK_BY_DURATION[context.durationClass] &&
|
|
268
|
+
!context.hasBackendSignal
|
|
269
|
+
) {
|
|
270
|
+
return runningHealth(context, {
|
|
271
|
+
state: "likely-stuck",
|
|
272
|
+
label: "likely stuck",
|
|
273
|
+
summary: "no fresh backend or activity signal",
|
|
274
|
+
tone: "error",
|
|
275
|
+
suggestion: "resume",
|
|
276
|
+
reason: "running task has no backend signal and activity is stale",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (context.staleMs < STALL_BY_DURATION[context.durationClass])
|
|
280
|
+
return undefined;
|
|
281
|
+
return runningHealth(context, {
|
|
282
|
+
state: "stalled",
|
|
283
|
+
label: "possibly stalled",
|
|
284
|
+
summary: "no recent visible progress",
|
|
285
|
+
tone: "warning",
|
|
286
|
+
suggestion: "inspect",
|
|
287
|
+
reason: context.hasBackendSignal
|
|
288
|
+
? "backend signal exists, but activity is stale"
|
|
289
|
+
: "activity is stale",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function longTailHealth(
|
|
294
|
+
context: RunningTaskContext,
|
|
295
|
+
): WorkflowProgressHealth | undefined {
|
|
296
|
+
const isLongTail =
|
|
297
|
+
context.durationClass === "long" &&
|
|
298
|
+
context.elapsedMs !== undefined &&
|
|
299
|
+
context.elapsedMs >= LONG_TAIL_ELAPSED_MS;
|
|
300
|
+
if (!isLongTail) return undefined;
|
|
301
|
+
return runningHealth(context, {
|
|
302
|
+
state: "long-tail",
|
|
303
|
+
label: "long-tail active",
|
|
304
|
+
summary: "slow task with fresh liveness signals",
|
|
305
|
+
tone: "accent",
|
|
306
|
+
suggestion: "wait",
|
|
307
|
+
reason:
|
|
308
|
+
context.staleMs <= ACTIVE_ACTIVITY_MS
|
|
309
|
+
? "liveness signal is fresh"
|
|
310
|
+
: "long-running stage is still within the stale threshold",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function activeRunningHealth(
|
|
315
|
+
context: RunningTaskContext,
|
|
316
|
+
): WorkflowProgressHealth {
|
|
317
|
+
return runningHealth(context, {
|
|
318
|
+
state: "active",
|
|
319
|
+
label: "active",
|
|
320
|
+
summary: "task is running",
|
|
321
|
+
tone: "accent",
|
|
322
|
+
suggestion: "wait",
|
|
323
|
+
reason:
|
|
324
|
+
context.staleMs <= ACTIVE_ACTIVITY_MS
|
|
325
|
+
? "liveness signal is fresh"
|
|
326
|
+
: "activity remains within the expected window",
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function runningContext(
|
|
331
|
+
task: TaskHealthInput,
|
|
332
|
+
run: Pick<WorkflowRunRecord, "updatedAt"> | undefined,
|
|
333
|
+
nowMs: number,
|
|
334
|
+
): RunningTaskContext {
|
|
335
|
+
const durationClass = classifyWorkflowTaskDuration(task);
|
|
336
|
+
const startedAtMs = parseTime(task.startedAt);
|
|
337
|
+
const heartbeatAt = parseHeartbeatAt(task.lastMessage);
|
|
338
|
+
const activityAt = latestIso([heartbeatAt, run?.updatedAt, task.startedAt]);
|
|
339
|
+
const lastActivityAgeMs = ageMs(activityAt, nowMs);
|
|
340
|
+
return {
|
|
341
|
+
task,
|
|
342
|
+
nowMs,
|
|
343
|
+
durationClass,
|
|
344
|
+
elapsedMs:
|
|
345
|
+
startedAtMs === undefined ? undefined : Math.max(0, nowMs - startedAtMs),
|
|
346
|
+
activityAt,
|
|
347
|
+
lastActivityAgeMs,
|
|
348
|
+
heartbeatAt,
|
|
349
|
+
heartbeatAgeMs: ageMs(heartbeatAt, nowMs),
|
|
350
|
+
hasBackendSignal: Boolean(task.backendHandle || task.pid || heartbeatAt),
|
|
351
|
+
staleMs: lastActivityAgeMs ?? Number.POSITIVE_INFINITY,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function terminalTaskHealth(
|
|
356
|
+
task: TaskHealthInput,
|
|
357
|
+
nowMs: number,
|
|
358
|
+
): WorkflowProgressHealth {
|
|
359
|
+
if (task.status === "completed" || task.status === "skipped") {
|
|
360
|
+
return {
|
|
361
|
+
state: "completed",
|
|
362
|
+
label: task.status === "skipped" ? "skipped" : "completed",
|
|
363
|
+
summary: task.status === "skipped" ? "task skipped" : "task completed",
|
|
364
|
+
tone: "success",
|
|
365
|
+
suggestion: "review",
|
|
366
|
+
reason: task.statusDetail,
|
|
367
|
+
currentTask: taskSummary(task, nowMs),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (task.status === "pending") {
|
|
371
|
+
return {
|
|
372
|
+
state: "pending",
|
|
373
|
+
label: "pending",
|
|
374
|
+
summary: "waiting for dependencies or scheduler",
|
|
375
|
+
tone: "dim",
|
|
376
|
+
suggestion: "wait",
|
|
377
|
+
reason: task.statusDetail,
|
|
378
|
+
currentTask: taskSummary(task, nowMs),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
state: "needs-action",
|
|
383
|
+
label: "needs action",
|
|
384
|
+
summary: `${task.status} task needs attention`,
|
|
385
|
+
tone: "error",
|
|
386
|
+
suggestion: "inspect",
|
|
387
|
+
reason: task.lastMessage ?? task.statusDetail,
|
|
388
|
+
currentTask: taskSummary(task, nowMs),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function runningHealth(
|
|
393
|
+
context: RunningTaskContext,
|
|
394
|
+
health: Pick<
|
|
395
|
+
WorkflowProgressHealth,
|
|
396
|
+
"state" | "label" | "summary" | "tone" | "suggestion" | "reason"
|
|
397
|
+
>,
|
|
398
|
+
): WorkflowProgressHealth {
|
|
399
|
+
return {
|
|
400
|
+
...health,
|
|
401
|
+
durationClass: context.durationClass,
|
|
402
|
+
currentTask: taskSummary(context.task, context.nowMs),
|
|
403
|
+
lastActivityAt: context.activityAt,
|
|
404
|
+
lastActivityAgeMs: context.lastActivityAgeMs,
|
|
405
|
+
heartbeatAt: context.heartbeatAt,
|
|
406
|
+
heartbeatAgeMs: context.heartbeatAgeMs,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function taskSummary(
|
|
411
|
+
task: TaskHealthInput,
|
|
412
|
+
nowMs: number,
|
|
413
|
+
): WorkflowHealthTaskSummary {
|
|
414
|
+
const startedAtMs = parseTime(task.startedAt);
|
|
415
|
+
return {
|
|
416
|
+
taskId: task.taskId,
|
|
417
|
+
displayName: task.displayName,
|
|
418
|
+
stageId: task.stageId,
|
|
419
|
+
status: task.status,
|
|
420
|
+
...(startedAtMs === undefined
|
|
421
|
+
? {}
|
|
422
|
+
: { elapsedMs: Math.max(0, nowMs - startedAtMs) }),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isProblemStatus(status: TaskRunStatus | WorkflowRunStatus): boolean {
|
|
427
|
+
return (
|
|
428
|
+
status === "failed" || status === "blocked" || status === "interrupted"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseHeartbeatAt(message: string | undefined): string | undefined {
|
|
433
|
+
if (!message) return undefined;
|
|
434
|
+
const match = /heartbeat\s+(\d{4}-\d{2}-\d{2}T\S+?Z)/i.exec(message);
|
|
435
|
+
if (!match) return undefined;
|
|
436
|
+
const value = match[1];
|
|
437
|
+
return parseTime(value) === undefined ? undefined : value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function latestIso(values: Array<string | undefined>): string | undefined {
|
|
441
|
+
let latest: string | undefined;
|
|
442
|
+
let latestMs = Number.NEGATIVE_INFINITY;
|
|
443
|
+
for (const value of values) {
|
|
444
|
+
const time = parseTime(value);
|
|
445
|
+
if (time === undefined || time <= latestMs) continue;
|
|
446
|
+
latest = value;
|
|
447
|
+
latestMs = time;
|
|
448
|
+
}
|
|
449
|
+
return latest;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function ageMs(value: string | undefined, nowMs: number): number | undefined {
|
|
453
|
+
const time = parseTime(value);
|
|
454
|
+
return time === undefined ? undefined : Math.max(0, nowMs - time);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function parseTime(value: string | undefined): number | undefined {
|
|
458
|
+
if (!value) return undefined;
|
|
459
|
+
const parsed = Date.parse(value);
|
|
460
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
461
|
+
}
|
package/src/workflow-runtime.ts
CHANGED
|
@@ -17,9 +17,16 @@ export interface WorkflowRuntimeDefaults {
|
|
|
17
17
|
thinking?: ThinkingLevel;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface WorkflowRuntimeThinkingResolution {
|
|
21
|
+
requested?: ThinkingLevel;
|
|
22
|
+
resolved?: ThinkingLevel;
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export interface WorkflowRuntimeResolutionInput {
|
|
21
27
|
model?: string;
|
|
22
28
|
thinking?: ThinkingLevel;
|
|
29
|
+
thinkingResolution?: WorkflowRuntimeThinkingResolution;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export interface WorkflowRuntimeResolutionContext {
|
|
@@ -39,6 +46,41 @@ export interface ResolveWorkflowRuntimeOptions {
|
|
|
39
46
|
prompt?: WorkflowRuntimePrompt;
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
export type WorkflowRuntimeLayer = WorkflowRuntimeDefaults | undefined;
|
|
50
|
+
|
|
51
|
+
export function selectWorkflowRuntime(
|
|
52
|
+
...layers: WorkflowRuntimeLayer[]
|
|
53
|
+
): WorkflowRuntimeResolutionInput {
|
|
54
|
+
const modelLayer = layers.find((layer) => modelOf(layer));
|
|
55
|
+
const model = modelOf(modelLayer);
|
|
56
|
+
let thinking: ThinkingLevel | undefined;
|
|
57
|
+
for (const layer of layers) {
|
|
58
|
+
if (!layer) continue;
|
|
59
|
+
if (layer.thinking) {
|
|
60
|
+
thinking = layer.thinking;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const layerModel = modelOf(layer);
|
|
64
|
+
const modelThinking = layerModel
|
|
65
|
+
? splitKnownThinkingSuffix(layerModel).thinking
|
|
66
|
+
: undefined;
|
|
67
|
+
if (modelThinking) {
|
|
68
|
+
thinking = modelThinking;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...(model ? { model } : {}),
|
|
74
|
+
...(thinking ? { thinking } : {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modelOf(layer: WorkflowRuntimeLayer): string | undefined {
|
|
79
|
+
return typeof layer?.model === "string" && layer.model.trim()
|
|
80
|
+
? layer.model.trim()
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
export function toWorkflowModelInfo(model: {
|
|
43
85
|
provider: string;
|
|
44
86
|
id: string;
|
|
@@ -66,7 +108,7 @@ export async function resolveWorkflowRuntime(
|
|
|
66
108
|
const model = await resolveModel(baseModel, context, options);
|
|
67
109
|
const effectiveThinking =
|
|
68
110
|
runtime.thinking ?? thinking ?? options.defaults?.thinking;
|
|
69
|
-
const
|
|
111
|
+
const thinkingResolution = await resolveThinking(
|
|
70
112
|
model,
|
|
71
113
|
effectiveThinking,
|
|
72
114
|
context,
|
|
@@ -74,7 +116,10 @@ export async function resolveWorkflowRuntime(
|
|
|
74
116
|
);
|
|
75
117
|
return {
|
|
76
118
|
...(model ? { model } : {}),
|
|
77
|
-
...(
|
|
119
|
+
...(thinkingResolution?.resolved
|
|
120
|
+
? { thinking: thinkingResolution.resolved }
|
|
121
|
+
: {}),
|
|
122
|
+
...(thinkingResolution ? { thinkingResolution } : {}),
|
|
78
123
|
};
|
|
79
124
|
}
|
|
80
125
|
|
|
@@ -208,36 +253,63 @@ async function resolveThinking(
|
|
|
208
253
|
requested: ThinkingLevel | undefined,
|
|
209
254
|
context: WorkflowRuntimeResolutionContext,
|
|
210
255
|
options: ResolveWorkflowRuntimeOptions,
|
|
211
|
-
): Promise<
|
|
256
|
+
): Promise<WorkflowRuntimeThinkingResolution | undefined> {
|
|
212
257
|
if (!requested) return undefined;
|
|
213
258
|
const model = findModelInfo(modelId, options.availableModels ?? []);
|
|
214
259
|
const supported = getSupportedThinkingLevels(model);
|
|
215
|
-
if (supported.includes(requested))
|
|
260
|
+
if (supported.includes(requested)) {
|
|
261
|
+
return { requested, resolved: requested };
|
|
262
|
+
}
|
|
216
263
|
|
|
217
|
-
if (
|
|
218
|
-
const modelLabel = modelId ?? "selected model";
|
|
264
|
+
if (supported.length === 0) {
|
|
219
265
|
throw new Error(
|
|
220
|
-
`${
|
|
266
|
+
`${modelId ?? "selected model"} does not expose any supported reasoning levels for ${context.taskKey}`,
|
|
221
267
|
);
|
|
222
268
|
}
|
|
223
269
|
|
|
224
|
-
|
|
270
|
+
const downgradeOptions = lowerOrEqualSupportedThinking(requested, supported);
|
|
271
|
+
if (downgradeOptions.length === 0) {
|
|
272
|
+
const modelLabel = modelId ?? "selected model";
|
|
225
273
|
throw new Error(
|
|
226
|
-
`${
|
|
274
|
+
`${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}, and no lower-or-equal fallback is available. Supported: ${supported.join(", ") || "none"}`,
|
|
227
275
|
);
|
|
228
276
|
}
|
|
229
277
|
|
|
278
|
+
if (!options.prompt) {
|
|
279
|
+
const resolved = downgradeOptions[downgradeOptions.length - 1]!;
|
|
280
|
+
return {
|
|
281
|
+
requested,
|
|
282
|
+
resolved,
|
|
283
|
+
reason: `requested ${requested} is unsupported by ${modelId ?? "selected model"}; using ${resolved}`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
230
287
|
const selected = await options.prompt.select(
|
|
231
|
-
`${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported level.`,
|
|
232
|
-
|
|
288
|
+
`${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported lower-or-equal level.`,
|
|
289
|
+
downgradeOptions,
|
|
233
290
|
);
|
|
234
291
|
if (!selected)
|
|
235
292
|
throw new Error(`Reasoning selection cancelled for ${context.taskKey}`);
|
|
236
|
-
if (!isThinkingLevel(selected))
|
|
293
|
+
if (!isThinkingLevel(selected) || !downgradeOptions.includes(selected))
|
|
237
294
|
throw new Error(
|
|
238
295
|
`Invalid reasoning selection "${selected}" for ${context.taskKey}`,
|
|
239
296
|
);
|
|
240
|
-
return
|
|
297
|
+
return {
|
|
298
|
+
requested,
|
|
299
|
+
resolved: selected,
|
|
300
|
+
reason: `selected supported reasoning ${selected} for unsupported request ${requested}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function lowerOrEqualSupportedThinking(
|
|
305
|
+
requested: ThinkingLevel,
|
|
306
|
+
supported: ThinkingLevel[],
|
|
307
|
+
): ThinkingLevel[] {
|
|
308
|
+
const requestedIndex = THINKING_LEVELS.indexOf(requested);
|
|
309
|
+
if (requestedIndex < 0) return [];
|
|
310
|
+
return THINKING_LEVELS.slice(0, requestedIndex + 1).filter((level) =>
|
|
311
|
+
supported.includes(level),
|
|
312
|
+
);
|
|
241
313
|
}
|
|
242
314
|
|
|
243
315
|
function findModelInfo(
|