pi-workflow-engine 0.11.0 → 0.13.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/.pi/extensions/pi-workflow-engine/index.ts +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +13 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +26 -4
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +82 -10
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +13 -6
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +127 -41
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +52 -15
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +151 -178
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +47 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +270 -17
- package/package.json +1 -1
|
@@ -1,55 +1,20 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
3
|
-
import type { WorkflowLaneItemStatus,
|
|
2
|
+
import type { WorkflowProgressEvent } from "./types.ts";
|
|
3
|
+
import type { AgentRowStatus, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
4
|
+
import { formatWorkflowUsageLine, type WorkflowUsageSnapshot } from "./usage.ts";
|
|
4
5
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
5
6
|
import { statusTextFromCounts, type WorkflowStatusCounts } from "./ui/workflow-format.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
export type
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
readonly lastTool?: string;
|
|
18
|
-
readonly error?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface PhaseSnapshot {
|
|
22
|
-
readonly title: string;
|
|
23
|
-
readonly agents: readonly AgentRowSnapshot[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface WorkflowCounterSnapshot {
|
|
27
|
-
readonly key: string;
|
|
28
|
-
readonly label: string;
|
|
29
|
-
readonly value: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface WorkflowLaneItemSnapshot {
|
|
33
|
-
readonly lane: string;
|
|
34
|
-
readonly title: string;
|
|
35
|
-
readonly subtitle?: string;
|
|
36
|
-
readonly status: WorkflowLaneItemStatus;
|
|
37
|
-
readonly details?: string;
|
|
38
|
-
readonly createdAt: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface WorkflowProgressSnapshot {
|
|
42
|
-
readonly title: string;
|
|
43
|
-
readonly startedAt: number;
|
|
44
|
-
readonly doneAt?: number;
|
|
45
|
-
readonly currentPhase: string;
|
|
46
|
-
readonly phases: readonly PhaseSnapshot[];
|
|
47
|
-
readonly counters: readonly WorkflowCounterSnapshot[];
|
|
48
|
-
readonly summary: readonly [string, string | number][];
|
|
49
|
-
readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
|
|
50
|
-
readonly laneOverflow: readonly [string, number][];
|
|
51
|
-
readonly logs: readonly string[];
|
|
52
|
-
}
|
|
7
|
+
import { renderWorkflowWidgetLines } from "./ui/workflow-widget.ts";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
AgentRowSnapshot,
|
|
11
|
+
AgentRowStatus,
|
|
12
|
+
PhaseSnapshot,
|
|
13
|
+
WorkflowCounterSnapshot,
|
|
14
|
+
WorkflowLaneItemSnapshot,
|
|
15
|
+
WorkflowLaneItemStatus,
|
|
16
|
+
WorkflowProgressSnapshot,
|
|
17
|
+
} from "./progress-types.ts";
|
|
53
18
|
|
|
54
19
|
interface AgentRow {
|
|
55
20
|
id: number;
|
|
@@ -83,6 +48,7 @@ interface WorkflowLaneItem {
|
|
|
83
48
|
}
|
|
84
49
|
|
|
85
50
|
const LOG_LIMIT = 24;
|
|
51
|
+
const WIDGET_REFRESH_INTERVAL_MS = 1_000;
|
|
86
52
|
export const DEFAULT_LANE_ITEM_LIMIT = 200;
|
|
87
53
|
|
|
88
54
|
/**
|
|
@@ -103,17 +69,18 @@ export class ProgressTracker {
|
|
|
103
69
|
private doneAt: number | undefined;
|
|
104
70
|
private currentPhase = "Workflow";
|
|
105
71
|
private nextAgentId = 1;
|
|
106
|
-
private widget: WorkflowWidget | undefined;
|
|
107
|
-
private widgetRegistered = false;
|
|
108
|
-
private tui: Pick<TUI, "requestRender"> | undefined;
|
|
109
|
-
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
110
72
|
private lastStatusText: string | undefined;
|
|
111
|
-
private
|
|
73
|
+
private usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
74
|
+
private widgetRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
75
|
+
private readonly surfaceKey: string;
|
|
112
76
|
|
|
113
77
|
constructor(
|
|
114
78
|
private readonly ctx: ExtensionContext,
|
|
115
79
|
private readonly title: string,
|
|
80
|
+
private readonly runId: string,
|
|
81
|
+
private readonly onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void,
|
|
116
82
|
) {
|
|
83
|
+
this.surfaceKey = `workflow:${runId}`;
|
|
117
84
|
this.ensurePhase(this.currentPhase);
|
|
118
85
|
}
|
|
119
86
|
|
|
@@ -130,14 +97,14 @@ export class ProgressTracker {
|
|
|
130
97
|
this.currentPhase = title;
|
|
131
98
|
this.ensurePhase(title);
|
|
132
99
|
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${title}\n`);
|
|
133
|
-
this.
|
|
100
|
+
this.publish();
|
|
134
101
|
}
|
|
135
102
|
|
|
136
103
|
log(message: string): void {
|
|
137
104
|
this.logs.push(message);
|
|
138
105
|
while (this.logs.length > LOG_LIMIT) this.logs.shift();
|
|
139
106
|
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${message}\n`);
|
|
140
|
-
this.
|
|
107
|
+
this.publish();
|
|
141
108
|
}
|
|
142
109
|
|
|
143
110
|
event(event: WorkflowProgressEvent): void {
|
|
@@ -172,7 +139,7 @@ export class ProgressTracker {
|
|
|
172
139
|
this.summary.set(event.key, event.value);
|
|
173
140
|
break;
|
|
174
141
|
}
|
|
175
|
-
this.
|
|
142
|
+
this.publish();
|
|
176
143
|
}
|
|
177
144
|
|
|
178
145
|
agentQueued(phase: string | undefined, label: string): number {
|
|
@@ -181,7 +148,7 @@ export class ProgressTracker {
|
|
|
181
148
|
this.ensurePhase(phase ?? this.currentPhase).agents.push(row);
|
|
182
149
|
this.rowsById.set(id, row);
|
|
183
150
|
this.agentCounts.queued++;
|
|
184
|
-
this.
|
|
151
|
+
this.publish();
|
|
185
152
|
return id;
|
|
186
153
|
}
|
|
187
154
|
|
|
@@ -203,7 +170,7 @@ export class ProgressTracker {
|
|
|
203
170
|
this.rowsById.set(nextRow.id, nextRow);
|
|
204
171
|
this.agentCounts.running++;
|
|
205
172
|
}
|
|
206
|
-
this.
|
|
173
|
+
this.publish();
|
|
207
174
|
}
|
|
208
175
|
|
|
209
176
|
agentTool(label: string, tool: string, id?: number): void {
|
|
@@ -212,7 +179,7 @@ export class ProgressTracker {
|
|
|
212
179
|
row.lastTool = tool;
|
|
213
180
|
row.toolUses += 1;
|
|
214
181
|
}
|
|
215
|
-
this.
|
|
182
|
+
this.publish();
|
|
216
183
|
}
|
|
217
184
|
|
|
218
185
|
agentDone(label: string, id?: number): void {
|
|
@@ -221,7 +188,7 @@ export class ProgressTracker {
|
|
|
221
188
|
this.transitionAgentStatus(row, "done");
|
|
222
189
|
row.doneAt = Date.now();
|
|
223
190
|
}
|
|
224
|
-
this.
|
|
191
|
+
this.publish();
|
|
225
192
|
}
|
|
226
193
|
|
|
227
194
|
agentFailed(label: string, error: unknown, id?: number): void {
|
|
@@ -231,11 +198,17 @@ export class ProgressTracker {
|
|
|
231
198
|
row.doneAt = Date.now();
|
|
232
199
|
row.error = unknownErrorMessage(error);
|
|
233
200
|
}
|
|
234
|
-
this.
|
|
201
|
+
this.publish();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
updateUsage(snapshot: WorkflowUsageSnapshot): void {
|
|
205
|
+
this.usageSnapshot = snapshot;
|
|
206
|
+
this.publish();
|
|
235
207
|
}
|
|
236
208
|
|
|
237
209
|
snapshot(): WorkflowProgressSnapshot {
|
|
238
210
|
return {
|
|
211
|
+
runId: this.runId,
|
|
239
212
|
title: this.title,
|
|
240
213
|
startedAt: this.startedAt,
|
|
241
214
|
doneAt: this.doneAt,
|
|
@@ -249,6 +222,7 @@ export class ProgressTracker {
|
|
|
249
222
|
lanes: [...this.lanes.entries()].map(([lane, items]) => [lane, items.map((item) => ({ ...item }))]),
|
|
250
223
|
laneOverflow: [...this.laneOverflow.entries()],
|
|
251
224
|
logs: [...this.logs],
|
|
225
|
+
usage: this.usageSnapshot,
|
|
252
226
|
};
|
|
253
227
|
}
|
|
254
228
|
|
|
@@ -300,25 +274,35 @@ export class ProgressTracker {
|
|
|
300
274
|
return this.rowsById.get(id);
|
|
301
275
|
}
|
|
302
276
|
|
|
303
|
-
private
|
|
277
|
+
private publish(): void {
|
|
278
|
+
this.publishSnapshot();
|
|
304
279
|
if (!this.ctx.hasUI) return;
|
|
305
|
-
this.
|
|
306
|
-
this.
|
|
307
|
-
this.requestRenderSoon();
|
|
280
|
+
this.publishWidget();
|
|
281
|
+
this.startWidgetRefresh();
|
|
308
282
|
this.publishStatus();
|
|
309
283
|
}
|
|
310
284
|
|
|
311
|
-
private
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
285
|
+
private publishWidget(): void {
|
|
286
|
+
this.ctx.ui.setWidget(
|
|
287
|
+
this.surfaceKey,
|
|
288
|
+
renderWorkflowWidgetLines(this.snapshot(), this.ctx.ui.theme),
|
|
289
|
+
{ placement: "aboveEditor" },
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private startWidgetRefresh(): void {
|
|
294
|
+
if (this.widgetRefreshInterval !== undefined) return;
|
|
295
|
+
this.widgetRefreshInterval = setInterval(() => this.publishWidget(), WIDGET_REFRESH_INTERVAL_MS);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private stopWidgetRefresh(): void {
|
|
299
|
+
if (this.widgetRefreshInterval === undefined) return;
|
|
300
|
+
clearInterval(this.widgetRefreshInterval);
|
|
301
|
+
this.widgetRefreshInterval = undefined;
|
|
318
302
|
}
|
|
319
303
|
|
|
320
304
|
private publishStatus(): void {
|
|
321
|
-
const
|
|
305
|
+
const status = statusTextFromCounts(
|
|
322
306
|
{
|
|
323
307
|
title: this.title,
|
|
324
308
|
doneAt: this.doneAt,
|
|
@@ -328,55 +312,26 @@ export class ProgressTracker {
|
|
|
328
312
|
this.statusCountsSnapshot(),
|
|
329
313
|
this.ctx.ui.theme,
|
|
330
314
|
);
|
|
315
|
+
const usage = formatWorkflowUsageLine(this.usageSnapshot);
|
|
316
|
+
const next = usage ? `${status} · ${usage}` : status;
|
|
331
317
|
if (next === this.lastStatusText) return;
|
|
332
|
-
this.ctx.ui.setStatus(
|
|
318
|
+
this.ctx.ui.setStatus(this.surfaceKey, next);
|
|
333
319
|
this.lastStatusText = next;
|
|
334
320
|
}
|
|
335
321
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
this.widget = createWorkflowWidget(() => this.snapshot());
|
|
339
|
-
this.ctx.ui.setWidget(
|
|
340
|
-
"workflow",
|
|
341
|
-
(tui, theme) => {
|
|
342
|
-
this.tui = tui;
|
|
343
|
-
return {
|
|
344
|
-
render: (width?: number) => this.widget?.render(width ?? tui.terminal.columns, theme) ?? [],
|
|
345
|
-
invalidate: () => this.widget?.invalidate(),
|
|
346
|
-
};
|
|
347
|
-
},
|
|
348
|
-
{ placement: "aboveEditor" },
|
|
349
|
-
);
|
|
350
|
-
this.widgetRegistered = true;
|
|
351
|
-
this.startWidgetTimer();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private startWidgetTimer(): void {
|
|
355
|
-
if (this.widgetInterval !== undefined) return;
|
|
356
|
-
this.widgetInterval = setInterval(() => {
|
|
357
|
-
this.widget?.nextFrame();
|
|
358
|
-
this.tui?.requestRender();
|
|
359
|
-
}, 100);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private stopWidgetTimer(): void {
|
|
363
|
-
if (this.widgetInterval === undefined) return;
|
|
364
|
-
clearInterval(this.widgetInterval);
|
|
365
|
-
this.widgetInterval = undefined;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Clear live workflow surfaces; optionally leave a one-line final status. */
|
|
369
|
-
done(status?: string): void {
|
|
322
|
+
/** Clear this run's live workflow surfaces. Final feedback is delivered by the result surface. */
|
|
323
|
+
done(): void {
|
|
370
324
|
this.doneAt = Date.now();
|
|
371
|
-
this.
|
|
325
|
+
this.stopWidgetRefresh();
|
|
326
|
+
this.publishSnapshot();
|
|
372
327
|
if (!this.ctx.hasUI) return;
|
|
373
|
-
this.ctx.ui.setWidget(
|
|
374
|
-
this.ctx.ui.setStatus(
|
|
375
|
-
this.lastStatusText =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
this.
|
|
328
|
+
this.ctx.ui.setWidget(this.surfaceKey, undefined);
|
|
329
|
+
this.ctx.ui.setStatus(this.surfaceKey, undefined);
|
|
330
|
+
this.lastStatusText = undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private publishSnapshot(): void {
|
|
334
|
+
this.onSnapshot?.(this.snapshot());
|
|
380
335
|
}
|
|
381
336
|
}
|
|
382
337
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { WorkflowPauseError } from "./cancellation.ts";
|
|
2
|
+
import {
|
|
3
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
4
|
+
type ResolvedWorkflowRunOptions,
|
|
5
|
+
} from "./options.ts";
|
|
6
|
+
import type { WorkflowProviderUsageLimitPause } from "./workflow-run-record.ts";
|
|
7
|
+
|
|
8
|
+
export const WORKFLOW_PROVIDER_USAGE_LIMIT_CODE = "WORKFLOW_PROVIDER_USAGE_LIMIT";
|
|
9
|
+
export const WORKFLOW_USAGE_LIMIT_FALLBACK_DELAY_MS = 60_000;
|
|
10
|
+
const MAX_RESET_HINT_LENGTH = 512;
|
|
11
|
+
|
|
12
|
+
export interface ProviderUsageLimitDetails {
|
|
13
|
+
readonly stopReason: "error";
|
|
14
|
+
readonly providerMessage: string;
|
|
15
|
+
readonly provider?: string;
|
|
16
|
+
readonly model?: string;
|
|
17
|
+
readonly api?: string;
|
|
18
|
+
readonly resetHint?: string;
|
|
19
|
+
readonly resetAt?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProviderUsageLimitPauseRecord {
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly pause: WorkflowProviderUsageLimitPause;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Fatal-to-the-current-run provider limit that remains resumable from its journal boundary. */
|
|
28
|
+
export class WorkflowProviderUsageLimitError extends WorkflowPauseError {
|
|
29
|
+
override readonly name = "WorkflowProviderUsageLimitError";
|
|
30
|
+
readonly code = WORKFLOW_PROVIDER_USAGE_LIMIT_CODE;
|
|
31
|
+
|
|
32
|
+
constructor(readonly details: ProviderUsageLimitDetails) {
|
|
33
|
+
super(details.providerMessage);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toJSON(): {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly message: string;
|
|
39
|
+
readonly code: string;
|
|
40
|
+
readonly details: ProviderUsageLimitDetails;
|
|
41
|
+
} {
|
|
42
|
+
return { name: this.name, message: this.message, code: this.code, details: this.details };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function providerUsageLimitFromMessages(
|
|
47
|
+
messages: readonly unknown[],
|
|
48
|
+
now = Date.now(),
|
|
49
|
+
): WorkflowProviderUsageLimitError | undefined {
|
|
50
|
+
const message = messages.findLast(isAssistantMessage);
|
|
51
|
+
if (!message || message.stopReason !== "error") return undefined;
|
|
52
|
+
const providerMessage = typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
53
|
+
if (!providerMessage || !isUsageLimitMessage(providerMessage)) return undefined;
|
|
54
|
+
const reset = parseProviderResetHint(providerMessage, now);
|
|
55
|
+
const provider = stringDetail(message.provider);
|
|
56
|
+
const model = stringDetail(message.model);
|
|
57
|
+
const api = stringDetail(message.api);
|
|
58
|
+
return new WorkflowProviderUsageLimitError({
|
|
59
|
+
stopReason: "error",
|
|
60
|
+
providerMessage,
|
|
61
|
+
...(provider ? { provider } : {}),
|
|
62
|
+
...(model ? { model } : {}),
|
|
63
|
+
...(api ? { api } : {}),
|
|
64
|
+
...reset,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createProviderUsageLimitPauseRecord(
|
|
69
|
+
error: WorkflowProviderUsageLimitError,
|
|
70
|
+
options: ResolvedWorkflowRunOptions,
|
|
71
|
+
replayableInvocation: boolean,
|
|
72
|
+
now = Date.now(),
|
|
73
|
+
): ProviderUsageLimitPauseRecord {
|
|
74
|
+
const attempt = options.usageLimitAttempt + 1;
|
|
75
|
+
const hintedDelay = error.details.resetAt === undefined
|
|
76
|
+
? WORKFLOW_USAGE_LIMIT_FALLBACK_DELAY_MS
|
|
77
|
+
: error.details.resetAt - now;
|
|
78
|
+
const delayMs = Math.min(
|
|
79
|
+
options.usageLimitMaxDelayMs,
|
|
80
|
+
Math.max(
|
|
81
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
82
|
+
hintedDelay > 0 ? hintedDelay : WORKFLOW_USAGE_LIMIT_FALLBACK_DELAY_MS,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
const autoResume = options.autoResumeOnUsageLimit
|
|
86
|
+
&& attempt < options.usageLimitMaxAttempts
|
|
87
|
+
&& replayableInvocation;
|
|
88
|
+
return {
|
|
89
|
+
message: autoResume
|
|
90
|
+
? `Provider usage limit paused this run; automatic attempt ${attempt + 1}/${options.usageLimitMaxAttempts} is scheduled.`
|
|
91
|
+
: `Provider usage limit paused this run at attempt ${attempt}/${options.usageLimitMaxAttempts}.`,
|
|
92
|
+
pause: {
|
|
93
|
+
kind: "provider_usage_limit",
|
|
94
|
+
reason: "provider_usage_limit",
|
|
95
|
+
providerMessage: error.details.providerMessage,
|
|
96
|
+
provider: error.details.provider,
|
|
97
|
+
model: error.details.model,
|
|
98
|
+
api: error.details.api,
|
|
99
|
+
resetHint: error.details.resetHint,
|
|
100
|
+
attempt,
|
|
101
|
+
nextEligibleAt: now + delayMs,
|
|
102
|
+
autoResume,
|
|
103
|
+
maxAttempts: options.usageLimitMaxAttempts,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function parseProviderResetHint(
|
|
109
|
+
message: string,
|
|
110
|
+
now = Date.now(),
|
|
111
|
+
): { readonly resetHint?: string; readonly resetAt?: number } {
|
|
112
|
+
const iso = message.match(
|
|
113
|
+
/(?:[a-z-]*reset[a-z-]*|resets?\s+at|available\s+again\s+at)\s*[:=]?\s*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))/i,
|
|
114
|
+
)?.[1];
|
|
115
|
+
if (iso) {
|
|
116
|
+
const parsed = Date.parse(iso);
|
|
117
|
+
return Number.isFinite(parsed) && parsed > now
|
|
118
|
+
? { resetHint: boundedHint(iso), resetAt: parsed }
|
|
119
|
+
: { resetHint: boundedHint(iso) };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const labelled = message.match(
|
|
123
|
+
/(?:retry[- ]after|try again in|resets? in|available again in|x-ratelimit-reset(?:-[a-z-]+)?)\s*[:=]?\s*((?:\d+(?:\.\d+)?\s*(?:milliseconds?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\s*)+)/i,
|
|
124
|
+
);
|
|
125
|
+
if (labelled?.[1]) {
|
|
126
|
+
const delayMs = parseDurationMs(labelled[1]);
|
|
127
|
+
return delayMs === undefined
|
|
128
|
+
? { resetHint: boundedHint(labelled[0]) }
|
|
129
|
+
: { resetHint: boundedHint(labelled[0]), resetAt: now + delayMs };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const retryAfter = message.match(/retry[- ]after\s*[:=]\s*([^\s,;]+)/i);
|
|
133
|
+
if (retryAfter?.[1]) {
|
|
134
|
+
const seconds = Number(retryAfter[1]);
|
|
135
|
+
return Number.isFinite(seconds) && seconds >= 0
|
|
136
|
+
? { resetHint: boundedHint(retryAfter[0]), resetAt: now + seconds * 1_000 }
|
|
137
|
+
: { resetHint: boundedHint(retryAfter[0]) };
|
|
138
|
+
}
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isUsageLimitMessage(message: string): boolean {
|
|
143
|
+
return /(?:GoUsageLimitError|FreeUsageLimitError|insufficient_quota|rate_limit_error|ResourceExhausted)/i.test(message)
|
|
144
|
+
|| /\btoo many requests\b/i.test(message)
|
|
145
|
+
|| /\b(?:http|status(?: code)?|error)\s*[:=]?\s*429\b/i.test(message)
|
|
146
|
+
|| /\b429\b[^\n]*(?:rate|requests?|quota|limit)/i.test(message)
|
|
147
|
+
|| /\b(?:usage|quota|monthly|weekly|daily)\s+(?:window\s+)?limit\b/i.test(message)
|
|
148
|
+
|| /\bquota\s+(?:has\s+been\s+)?exceeded\b/i.test(message)
|
|
149
|
+
|| /\bexceeded\b[^\n]*\bquota\b/i.test(message)
|
|
150
|
+
|| /\b(?:usage|rate)\s+limit\s+(?:(?:has\s+been|was)\s+)?(?:reached|exceeded)\b/i.test(message);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseDurationMs(value: string): number | undefined {
|
|
154
|
+
const unitPattern = /(\d+(?:\.\d+)?)\s*(milliseconds?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)/gi;
|
|
155
|
+
let total = 0;
|
|
156
|
+
let matched = "";
|
|
157
|
+
for (const part of value.matchAll(unitPattern)) {
|
|
158
|
+
const amount = Number(part[1]);
|
|
159
|
+
const unit = part[2]?.toLowerCase();
|
|
160
|
+
if (!Number.isFinite(amount) || !unit) return undefined;
|
|
161
|
+
matched += part[0];
|
|
162
|
+
if (unit === "ms" || unit.startsWith("millisecond")) total += amount;
|
|
163
|
+
else if (unit === "s" || unit.startsWith("sec")) total += amount * 1_000;
|
|
164
|
+
else if (unit === "m" || unit.startsWith("min")) total += amount * 60_000;
|
|
165
|
+
else if (unit === "h" || unit.startsWith("hr") || unit.startsWith("hour")) total += amount * 3_600_000;
|
|
166
|
+
else total += amount * 86_400_000;
|
|
167
|
+
}
|
|
168
|
+
if (!matched || value.replace(/\s+/g, "") !== matched.replace(/\s+/g, "")) return undefined;
|
|
169
|
+
return Number.isFinite(total) && total >= 0 ? total : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function boundedHint(value: string): string {
|
|
173
|
+
return value.length <= MAX_RESET_HINT_LENGTH ? value : `${value.slice(0, MAX_RESET_HINT_LENGTH - 1)}…`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isAssistantMessage(value: unknown): value is {
|
|
177
|
+
readonly role: "assistant";
|
|
178
|
+
readonly stopReason?: unknown;
|
|
179
|
+
readonly errorMessage?: unknown;
|
|
180
|
+
readonly provider?: unknown;
|
|
181
|
+
readonly model?: unknown;
|
|
182
|
+
readonly api?: unknown;
|
|
183
|
+
} {
|
|
184
|
+
return typeof value === "object" && value !== null && "role" in value && value.role === "assistant";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stringDetail(value: unknown): string | undefined {
|
|
188
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
189
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const MAX_RESEARCH_LANES = 4;
|
|
4
|
+
export const MAX_LANE_EVIDENCE = 6;
|
|
5
|
+
export const MAX_VERIFICATION_CLAIMS = 12;
|
|
6
|
+
|
|
7
|
+
export const ResearchSourceSchema = Type.Object({
|
|
8
|
+
title: Type.String({ minLength: 1, description: "Human-readable title of the supporting page." }),
|
|
9
|
+
url: Type.String({ minLength: 1, description: "Direct HTTP(S) URL of the supporting page, never a search-results URL." }),
|
|
10
|
+
publishedAt: Type.Optional(Type.String({ description: "Publication or update date when the page states one." })),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type ResearchSource = Static<typeof ResearchSourceSchema>;
|
|
14
|
+
|
|
15
|
+
export const ResearchPlanSchema = Type.Object({
|
|
16
|
+
scopeConstraints: Type.Array(Type.String()),
|
|
17
|
+
lanes: Type.Array(
|
|
18
|
+
Type.Object({
|
|
19
|
+
id: Type.String({ minLength: 1 }),
|
|
20
|
+
title: Type.String({ minLength: 1 }),
|
|
21
|
+
objective: Type.String({ minLength: 1 }),
|
|
22
|
+
queries: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 4 }),
|
|
23
|
+
}),
|
|
24
|
+
{ minItems: 1, maxItems: MAX_RESEARCH_LANES },
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type ResearchPlan = Static<typeof ResearchPlanSchema>;
|
|
29
|
+
export type ResearchLane = ResearchPlan["lanes"][number];
|
|
30
|
+
|
|
31
|
+
export const ResearchEvidenceSchema = Type.Object({
|
|
32
|
+
claim: Type.String({ minLength: 1 }),
|
|
33
|
+
importance: Type.Union([Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")]),
|
|
34
|
+
stance: Type.Union([Type.Literal("supports"), Type.Literal("conflicts")]),
|
|
35
|
+
evidence: Type.String({ minLength: 1, description: "A short supporting passage or precise paraphrase from the page." }),
|
|
36
|
+
source: ResearchSourceSchema,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type ResearchEvidence = Static<typeof ResearchEvidenceSchema>;
|
|
40
|
+
|
|
41
|
+
export const ResearchLaneResultSchema = Type.Object({
|
|
42
|
+
laneId: Type.String({ minLength: 1 }),
|
|
43
|
+
evidence: Type.Array(ResearchEvidenceSchema, { maxItems: MAX_LANE_EVIDENCE }),
|
|
44
|
+
gaps: Type.Array(Type.String()),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type ResearchLaneResult = Static<typeof ResearchLaneResultSchema>;
|
|
48
|
+
|
|
49
|
+
export interface ResearchClaimCandidate {
|
|
50
|
+
readonly claim: string;
|
|
51
|
+
readonly importance: ResearchEvidence["importance"];
|
|
52
|
+
readonly evidence: readonly ResearchEvidence[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const ResearchVerificationSchema = Type.Object({
|
|
56
|
+
claim: Type.String({ minLength: 1 }),
|
|
57
|
+
verdict: Type.Union([
|
|
58
|
+
Type.Literal("SUPPORTED"),
|
|
59
|
+
Type.Literal("CONFLICTED"),
|
|
60
|
+
Type.Literal("UNCERTAIN"),
|
|
61
|
+
Type.Literal("INFERENCE"),
|
|
62
|
+
Type.Literal("REJECTED"),
|
|
63
|
+
]),
|
|
64
|
+
explanation: Type.String({ minLength: 1 }),
|
|
65
|
+
sources: Type.Array(ResearchSourceSchema, { maxItems: 8 }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export type ResearchVerification = Static<typeof ResearchVerificationSchema>;
|
|
69
|
+
|
|
70
|
+
export const ResearchReportEntrySchema = Type.Object({
|
|
71
|
+
claim: Type.String({ minLength: 1 }),
|
|
72
|
+
explanation: Type.String({ minLength: 1 }),
|
|
73
|
+
citations: Type.Array(ResearchSourceSchema, { maxItems: 8 }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export type ResearchReportEntry = Static<typeof ResearchReportEntrySchema>;
|
|
77
|
+
|
|
78
|
+
export const ResearchReportSchema = Type.Object({
|
|
79
|
+
answer: Type.String(),
|
|
80
|
+
supportedClaims: Type.Array(ResearchReportEntrySchema),
|
|
81
|
+
conflictingEvidence: Type.Array(ResearchReportEntrySchema),
|
|
82
|
+
uncertainties: Type.Array(ResearchReportEntrySchema),
|
|
83
|
+
inferences: Type.Array(ResearchReportEntrySchema),
|
|
84
|
+
sources: Type.Array(ResearchSourceSchema),
|
|
85
|
+
limitations: Type.Array(Type.String()),
|
|
86
|
+
nextSteps: Type.Array(Type.String()),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type ResearchReport = Static<typeof ResearchReportSchema>;
|