@jameslovespancakes/pi-plus 1.0.11 → 1.0.12
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 +44 -20
- package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
- package/package.json +6 -3
- package/src/core/accounts/oauth-pool.ts +102 -0
- package/src/core/accounts/registry.ts +8 -25
- package/src/core/accounts/routing.ts +122 -0
- package/src/core/anthropic/client-identity.ts +9 -64
- package/src/core/anthropic/quota.ts +8 -53
- package/src/core/anthropic/routing.ts +40 -115
- package/src/core/anthropic/store.ts +7 -9
- package/src/core/catalog/quality.ts +33 -15
- package/src/core/codex/quota.ts +8 -2
- package/src/core/codex/store.ts +12 -9
- package/src/core/config.ts +4 -17
- package/src/core/store.ts +13 -12
- package/src/domains/models/provider-picker.ts +1 -1
- package/src/domains/setup/index.ts +23 -23
- package/src/domains/subscriptions/accounts-picker.ts +18 -37
- package/src/domains/subscriptions/accounts.ts +23 -29
- package/src/domains/subscriptions/footer.ts +48 -29
- package/src/domains/subscriptions/index.ts +11 -27
- package/src/domains/subscriptions/provider.ts +38 -103
- package/src/domains/subscriptions/providers/anthropic.ts +5 -17
- package/src/domains/subscriptions/providers/codex.ts +171 -4
- package/src/domains/subscriptions/providers/hosted.ts +18 -0
- package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
- package/src/domains/subscriptions/routing.ts +19 -11
- package/src/domains/workflows/LICENSE.md +21 -0
- package/src/domains/workflows/index.ts +836 -0
- package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
- package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
- package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
- package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
- package/src/domains/workflows/runtime/agent-limits.ts +43 -0
- package/src/domains/workflows/runtime/agent-replay.ts +399 -0
- package/src/domains/workflows/runtime/agent-retry.ts +116 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
- package/src/domains/workflows/runtime/agent-runner.ts +203 -0
- package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
- package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
- package/src/domains/workflows/runtime/agent-session.ts +382 -0
- package/src/domains/workflows/runtime/agent-skills.ts +270 -0
- package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
- package/src/domains/workflows/runtime/background-workflows.ts +492 -0
- package/src/domains/workflows/runtime/budget.ts +53 -0
- package/src/domains/workflows/runtime/cancellation.ts +87 -0
- package/src/domains/workflows/runtime/command-completions.ts +36 -0
- package/src/domains/workflows/runtime/concurrency.ts +403 -0
- package/src/domains/workflows/runtime/debug.ts +3 -0
- package/src/domains/workflows/runtime/diff-capture.ts +81 -0
- package/src/domains/workflows/runtime/discovery.ts +137 -0
- package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
- package/src/domains/workflows/runtime/dynamax.ts +330 -0
- package/src/domains/workflows/runtime/engine.ts +543 -0
- package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
- package/src/domains/workflows/runtime/finalizers.ts +66 -0
- package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
- package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
- package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
- package/src/domains/workflows/runtime/journal.ts +313 -0
- package/src/domains/workflows/runtime/model-profiles.ts +310 -0
- package/src/domains/workflows/runtime/options.ts +157 -0
- package/src/domains/workflows/runtime/perf.ts +146 -0
- package/src/domains/workflows/runtime/pi-compat.ts +32 -0
- package/src/domains/workflows/runtime/process-runner.ts +260 -0
- package/src/domains/workflows/runtime/progress-types.ts +59 -0
- package/src/domains/workflows/runtime/progress.ts +400 -0
- package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
- package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
- package/src/domains/workflows/runtime/research-contract.ts +89 -0
- package/src/domains/workflows/runtime/research-evidence.ts +289 -0
- package/src/domains/workflows/runtime/resume-context.ts +751 -0
- package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
- package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
- package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
- package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
- package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
- package/src/domains/workflows/runtime/review/review-format.ts +133 -0
- package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
- package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
- package/src/domains/workflows/runtime/review/review-report.ts +42 -0
- package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
- package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
- package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
- package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
- package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
- package/src/domains/workflows/runtime/session-identity.ts +6 -0
- package/src/domains/workflows/runtime/structured-output.ts +32 -0
- package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
- package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
- package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
- package/src/domains/workflows/runtime/types.ts +255 -0
- package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
- package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
- package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
- package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
- package/src/domains/workflows/runtime/unknown-error.ts +8 -0
- package/src/domains/workflows/runtime/usage.ts +341 -0
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
- package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
- package/src/domains/workflows/runtime/workflow-module.ts +75 -0
- package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
- package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
- package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
- package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
- package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
- package/src/domains/workflows/runtime/workflows.ts +40 -0
- package/src/domains/workflows/runtime/worktree.ts +581 -0
- package/src/domains/workflows/workflows/code-review.ts +232 -0
- package/src/domains/workflows/workflows/diagnose.ts +154 -0
- package/src/domains/workflows/workflows/perf-review.ts +149 -0
- package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
- package/src/domains/workflows/workflows/research.ts +169 -0
- package/src/services/usage-service.ts +6 -30
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WorkflowProgressEvent } from "./types.ts";
|
|
3
|
+
import type { AgentChatMessage, AgentChatRole, AgentRowStatus, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
4
|
+
import { formatWorkflowUsageLine, type WorkflowUsageSnapshot } from "./usage.ts";
|
|
5
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
6
|
+
import { statusTextFromCounts, type WorkflowStatusCounts } from "./ui/workflow-format.ts";
|
|
7
|
+
import { toDisplayLine } from "./ui/display-text.ts";
|
|
8
|
+
import { renderWorkflowWidgetLines } from "./ui/workflow-widget.ts";
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
AgentChatMessage,
|
|
12
|
+
AgentChatRole,
|
|
13
|
+
AgentRowSnapshot,
|
|
14
|
+
AgentRowStatus,
|
|
15
|
+
PhaseSnapshot,
|
|
16
|
+
WorkflowCounterSnapshot,
|
|
17
|
+
WorkflowLaneItemSnapshot,
|
|
18
|
+
WorkflowLaneItemStatus,
|
|
19
|
+
WorkflowProgressSnapshot,
|
|
20
|
+
} from "./progress-types.ts";
|
|
21
|
+
|
|
22
|
+
interface AgentRow {
|
|
23
|
+
id: number;
|
|
24
|
+
label: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
status: AgentRowStatus;
|
|
27
|
+
startedAt?: number;
|
|
28
|
+
doneAt?: number;
|
|
29
|
+
toolUses: number;
|
|
30
|
+
lastTool?: string;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Phase {
|
|
35
|
+
title: string;
|
|
36
|
+
agents: AgentRow[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface WorkflowCounter {
|
|
40
|
+
key: string;
|
|
41
|
+
label: string;
|
|
42
|
+
value: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface WorkflowLaneItem {
|
|
46
|
+
lane: string;
|
|
47
|
+
title: string;
|
|
48
|
+
subtitle?: string;
|
|
49
|
+
status: WorkflowLaneItemStatus;
|
|
50
|
+
details?: string;
|
|
51
|
+
createdAt: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LOG_LIMIT = 24;
|
|
55
|
+
const AGENT_CHAT_LIMIT = 80;
|
|
56
|
+
const AGENT_CHAT_TEXT_LIMIT = 2_000;
|
|
57
|
+
/** Stored breadcrumbs stay single-line so every live surface keeps a stable height. */
|
|
58
|
+
const LOG_DISPLAY_LIMIT = 200;
|
|
59
|
+
const AGENT_ERROR_DISPLAY_LIMIT = 300;
|
|
60
|
+
const WIDGET_REFRESH_INTERVAL_MS = 1_000;
|
|
61
|
+
export const DEFAULT_LANE_ITEM_LIMIT = 200;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tracks live workflow state for widgets, footer/status text, result renderers,
|
|
65
|
+
* and headless stderr breadcrumbs.
|
|
66
|
+
*/
|
|
67
|
+
export class ProgressTracker {
|
|
68
|
+
private readonly phases: Phase[] = [];
|
|
69
|
+
private readonly logs: string[] = [];
|
|
70
|
+
private readonly counters = new Map<string, WorkflowCounter>();
|
|
71
|
+
private readonly summary = new Map<string, string | number>();
|
|
72
|
+
private readonly lanes = new Map<string, WorkflowLaneItem[]>();
|
|
73
|
+
private readonly laneOverflow = new Map<string, number>();
|
|
74
|
+
private readonly rowsById = new Map<number, AgentRow>();
|
|
75
|
+
private readonly agentChats = new Map<number, AgentChatMessage[]>();
|
|
76
|
+
private readonly agentFollowUps = new Map<number, (message: string) => Promise<void>>();
|
|
77
|
+
private readonly listeners = new Set<() => void>();
|
|
78
|
+
private readonly agentCounts: Record<AgentRowStatus, number> = { queued: 0, running: 0, done: 0, failed: 0 };
|
|
79
|
+
private readonly startedAt = Date.now();
|
|
80
|
+
private readonly laneItemLimit = laneItemLimitFromEnv();
|
|
81
|
+
private doneAt: number | undefined;
|
|
82
|
+
private currentPhase = "Workflow";
|
|
83
|
+
private nextAgentId = 1;
|
|
84
|
+
private lastStatusText: string | undefined;
|
|
85
|
+
private usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
86
|
+
private widgetRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
87
|
+
private readonly surfaceKey: string;
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
private readonly ctx: ExtensionContext,
|
|
91
|
+
private readonly title: string,
|
|
92
|
+
private readonly runId: string,
|
|
93
|
+
private readonly onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void,
|
|
94
|
+
) {
|
|
95
|
+
this.surfaceKey = `workflow:${runId}`;
|
|
96
|
+
this.ensurePhase(this.currentPhase);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private ensurePhase(title: string): Phase {
|
|
100
|
+
let phase = this.phases.find((candidate) => candidate.title === title);
|
|
101
|
+
if (!phase) {
|
|
102
|
+
phase = { title, agents: [] };
|
|
103
|
+
this.phases.push(phase);
|
|
104
|
+
}
|
|
105
|
+
return phase;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
phase(title: string): void {
|
|
109
|
+
this.currentPhase = title;
|
|
110
|
+
this.ensurePhase(title);
|
|
111
|
+
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${title}\n`);
|
|
112
|
+
this.publish();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
log(message: string): void {
|
|
116
|
+
this.logs.push(toDisplayLine(message, LOG_DISPLAY_LIMIT));
|
|
117
|
+
while (this.logs.length > LOG_LIMIT) this.logs.shift();
|
|
118
|
+
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${message}\n`);
|
|
119
|
+
this.publish();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
event(event: WorkflowProgressEvent): void {
|
|
123
|
+
switch (event.type) {
|
|
124
|
+
case "counter":
|
|
125
|
+
this.counters.set(event.key, { key: event.key, label: event.label, value: event.value });
|
|
126
|
+
break;
|
|
127
|
+
case "counter_delta": {
|
|
128
|
+
const current = this.counters.get(event.key);
|
|
129
|
+
this.counters.set(event.key, {
|
|
130
|
+
key: event.key,
|
|
131
|
+
label: event.label,
|
|
132
|
+
value: (current?.value ?? 0) + event.delta,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "lane_item": {
|
|
137
|
+
const lane = this.lanes.get(event.lane) ?? [];
|
|
138
|
+
lane.push({
|
|
139
|
+
lane: event.lane,
|
|
140
|
+
title: event.title,
|
|
141
|
+
subtitle: event.subtitle,
|
|
142
|
+
status: event.status,
|
|
143
|
+
details: event.details,
|
|
144
|
+
createdAt: Date.now(),
|
|
145
|
+
});
|
|
146
|
+
this.pruneLane(event.lane, lane);
|
|
147
|
+
this.lanes.set(event.lane, lane);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "summary":
|
|
151
|
+
this.summary.set(event.key, event.value);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
this.publish();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
agentQueued(phase: string | undefined, label: string, model?: string): number {
|
|
158
|
+
const id = this.nextAgentId++;
|
|
159
|
+
const row = { label, model, id, status: "queued" as const, toolUses: 0 };
|
|
160
|
+
this.ensurePhase(phase ?? this.currentPhase).agents.push(row);
|
|
161
|
+
this.rowsById.set(id, row);
|
|
162
|
+
this.agentCounts.queued++;
|
|
163
|
+
this.publish();
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
agentStart(phase: string | undefined, label: string, id?: number, model?: string): void {
|
|
168
|
+
const row = id === undefined ? undefined : this.findRowById(id);
|
|
169
|
+
if (row) {
|
|
170
|
+
this.transitionAgentStatus(row, "running");
|
|
171
|
+
row.startedAt = Date.now();
|
|
172
|
+
row.error = undefined;
|
|
173
|
+
} else {
|
|
174
|
+
const nextRow = {
|
|
175
|
+
label,
|
|
176
|
+
model,
|
|
177
|
+
id: this.nextAgentId++,
|
|
178
|
+
status: "running" as const,
|
|
179
|
+
startedAt: Date.now(),
|
|
180
|
+
toolUses: 0,
|
|
181
|
+
};
|
|
182
|
+
this.ensurePhase(phase ?? this.currentPhase).agents.push(nextRow);
|
|
183
|
+
this.rowsById.set(nextRow.id, nextRow);
|
|
184
|
+
this.agentCounts.running++;
|
|
185
|
+
}
|
|
186
|
+
this.publish();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
agentTool(label: string, tool: string, id?: number): void {
|
|
190
|
+
const row = this.findRow(label, id);
|
|
191
|
+
if (row) {
|
|
192
|
+
row.lastTool = tool;
|
|
193
|
+
row.toolUses += 1;
|
|
194
|
+
this.appendAgentChat(row.id, "tool", tool);
|
|
195
|
+
}
|
|
196
|
+
this.publish();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
agentMessage(id: number, role: AgentChatRole, text: string): void {
|
|
200
|
+
this.appendAgentChat(id, role, text);
|
|
201
|
+
this.publish();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
bindAgentFollowUp(id: number, send: (message: string) => Promise<void>): () => void {
|
|
205
|
+
this.agentFollowUps.set(id, send);
|
|
206
|
+
this.publish();
|
|
207
|
+
return () => {
|
|
208
|
+
if (this.agentFollowUps.get(id) === send) this.agentFollowUps.delete(id);
|
|
209
|
+
this.publish();
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
conversation(id: number): readonly AgentChatMessage[] {
|
|
214
|
+
return (this.agentChats.get(id) ?? []).map((message) => ({ ...message }));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async followUp(id: number, message: string): Promise<void> {
|
|
218
|
+
const text = toDisplayLine(message, AGENT_CHAT_TEXT_LIMIT);
|
|
219
|
+
if (!text) throw new Error("Enter a follow-up message.");
|
|
220
|
+
const send = this.agentFollowUps.get(id);
|
|
221
|
+
if (!send) throw new Error("This agent is no longer accepting follow-ups.");
|
|
222
|
+
await send(text);
|
|
223
|
+
this.appendAgentChat(id, "user", text);
|
|
224
|
+
this.publish();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
subscribe(listener: () => void): () => void {
|
|
228
|
+
this.listeners.add(listener);
|
|
229
|
+
return () => this.listeners.delete(listener);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
agentDone(label: string, id?: number): void {
|
|
233
|
+
const row = this.findRow(label, id);
|
|
234
|
+
if (row && row.status !== "failed") {
|
|
235
|
+
this.transitionAgentStatus(row, "done");
|
|
236
|
+
row.doneAt = Date.now();
|
|
237
|
+
}
|
|
238
|
+
this.publish();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
agentFailed(label: string, error: unknown, id?: number): void {
|
|
242
|
+
const row = this.findRow(label, id);
|
|
243
|
+
if (row) {
|
|
244
|
+
this.transitionAgentStatus(row, "failed");
|
|
245
|
+
row.doneAt = Date.now();
|
|
246
|
+
row.error = toDisplayLine(unknownErrorMessage(error), AGENT_ERROR_DISPLAY_LIMIT) || "agent failed";
|
|
247
|
+
this.appendAgentChat(row.id, "status", row.error);
|
|
248
|
+
}
|
|
249
|
+
this.publish();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
updateUsage(snapshot: WorkflowUsageSnapshot): void {
|
|
253
|
+
this.usageSnapshot = snapshot;
|
|
254
|
+
this.publish();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
snapshot(): WorkflowProgressSnapshot {
|
|
258
|
+
return {
|
|
259
|
+
runId: this.runId,
|
|
260
|
+
title: this.title,
|
|
261
|
+
startedAt: this.startedAt,
|
|
262
|
+
doneAt: this.doneAt,
|
|
263
|
+
currentPhase: this.currentPhase,
|
|
264
|
+
phases: this.phases.map((phase) => ({
|
|
265
|
+
title: phase.title,
|
|
266
|
+
agents: phase.agents.map((agent) => ({ ...agent })),
|
|
267
|
+
})),
|
|
268
|
+
counters: [...this.counters.values()].map((counter) => ({ ...counter })),
|
|
269
|
+
summary: [...this.summary.entries()],
|
|
270
|
+
lanes: [...this.lanes.entries()].map(([lane, items]) => [lane, items.map((item) => ({ ...item }))]),
|
|
271
|
+
laneOverflow: [...this.laneOverflow.entries()],
|
|
272
|
+
logs: [...this.logs],
|
|
273
|
+
usage: this.usageSnapshot,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
statusCounts(): WorkflowStatusCounts {
|
|
278
|
+
return this.statusCountsSnapshot();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private statusCountsSnapshot(): WorkflowStatusCounts {
|
|
282
|
+
return {
|
|
283
|
+
queued: this.agentCounts.queued,
|
|
284
|
+
running: this.agentCounts.running,
|
|
285
|
+
done: this.agentCounts.done,
|
|
286
|
+
failed: this.agentCounts.failed,
|
|
287
|
+
total: this.agentCounts.queued + this.agentCounts.running + this.agentCounts.done + this.agentCounts.failed,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private transitionAgentStatus(row: AgentRow, nextStatus: AgentRowStatus): void {
|
|
292
|
+
if (row.status === nextStatus) return;
|
|
293
|
+
this.agentCounts[row.status]--;
|
|
294
|
+
row.status = nextStatus;
|
|
295
|
+
this.agentCounts[nextStatus]++;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private pruneLane(laneName: string, lane: WorkflowLaneItem[]): void {
|
|
299
|
+
if (this.laneItemLimit <= 0) return;
|
|
300
|
+
while (lane.length > this.laneItemLimit) {
|
|
301
|
+
lane.shift();
|
|
302
|
+
this.laneOverflow.set(laneName, (this.laneOverflow.get(laneName) ?? 0) + 1);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private appendAgentChat(id: number, role: AgentChatRole, text: string): void {
|
|
307
|
+
const value = toDisplayLine(text, AGENT_CHAT_TEXT_LIMIT);
|
|
308
|
+
if (!value) return;
|
|
309
|
+
const messages = this.agentChats.get(id) ?? [];
|
|
310
|
+
messages.push({ role, text: value, createdAt: Date.now() });
|
|
311
|
+
while (messages.length > AGENT_CHAT_LIMIT) messages.shift();
|
|
312
|
+
this.agentChats.set(id, messages);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private findRow(label: string, id?: number): AgentRow | undefined {
|
|
316
|
+
if (id !== undefined) return this.findRowById(id);
|
|
317
|
+
for (let i = this.phases.length - 1; i >= 0; i--) {
|
|
318
|
+
const running = this.phases[i].agents.find(
|
|
319
|
+
(agent) => agent.label === label && (agent.status === "running" || agent.status === "queued"),
|
|
320
|
+
);
|
|
321
|
+
if (running) return running;
|
|
322
|
+
}
|
|
323
|
+
for (let i = this.phases.length - 1; i >= 0; i--) {
|
|
324
|
+
const matching = this.phases[i].agents.find((agent) => agent.label === label);
|
|
325
|
+
if (matching) return matching;
|
|
326
|
+
}
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private findRowById(id: number): AgentRow | undefined {
|
|
331
|
+
return this.rowsById.get(id);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private publish(): void {
|
|
335
|
+
this.publishSnapshot();
|
|
336
|
+
if (!this.ctx.hasUI) return;
|
|
337
|
+
this.publishWidget();
|
|
338
|
+
this.startWidgetRefresh();
|
|
339
|
+
this.publishStatus();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private publishWidget(): void {
|
|
343
|
+
this.ctx.ui.setWidget(
|
|
344
|
+
this.surfaceKey,
|
|
345
|
+
renderWorkflowWidgetLines(this.snapshot(), this.ctx.ui.theme),
|
|
346
|
+
{ placement: "aboveEditor" },
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private startWidgetRefresh(): void {
|
|
351
|
+
if (this.widgetRefreshInterval !== undefined) return;
|
|
352
|
+
this.widgetRefreshInterval = setInterval(() => this.publishWidget(), WIDGET_REFRESH_INTERVAL_MS);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private stopWidgetRefresh(): void {
|
|
356
|
+
if (this.widgetRefreshInterval === undefined) return;
|
|
357
|
+
clearInterval(this.widgetRefreshInterval);
|
|
358
|
+
this.widgetRefreshInterval = undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private publishStatus(): void {
|
|
362
|
+
const status = statusTextFromCounts(
|
|
363
|
+
{
|
|
364
|
+
title: this.title,
|
|
365
|
+
doneAt: this.doneAt,
|
|
366
|
+
currentPhase: this.currentPhase,
|
|
367
|
+
counters: [...this.counters.values()].map((counter) => ({ ...counter })),
|
|
368
|
+
},
|
|
369
|
+
this.statusCountsSnapshot(),
|
|
370
|
+
this.ctx.ui.theme,
|
|
371
|
+
);
|
|
372
|
+
const usage = formatWorkflowUsageLine(this.usageSnapshot);
|
|
373
|
+
const next = usage ? `${status} · ${usage}` : status;
|
|
374
|
+
if (next === this.lastStatusText) return;
|
|
375
|
+
this.ctx.ui.setStatus(this.surfaceKey, next);
|
|
376
|
+
this.lastStatusText = next;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Clear this run's live workflow surfaces. Final feedback is delivered by the result surface. */
|
|
380
|
+
done(): void {
|
|
381
|
+
this.doneAt = Date.now();
|
|
382
|
+
this.stopWidgetRefresh();
|
|
383
|
+
this.publishSnapshot();
|
|
384
|
+
if (!this.ctx.hasUI) return;
|
|
385
|
+
this.ctx.ui.setWidget(this.surfaceKey, undefined);
|
|
386
|
+
this.ctx.ui.setStatus(this.surfaceKey, undefined);
|
|
387
|
+
this.lastStatusText = undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private publishSnapshot(): void {
|
|
391
|
+
this.onSnapshot?.(this.snapshot());
|
|
392
|
+
for (const listener of this.listeners) listener();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function laneItemLimitFromEnv(): number {
|
|
397
|
+
const parsed = Number(process.env.PI_WORKFLOW_LANE_ITEM_LIMIT ?? "");
|
|
398
|
+
if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_LANE_ITEM_LIMIT;
|
|
399
|
+
return Math.trunc(parsed);
|
|
400
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
readonly details: ProviderUsageLimitDetails;
|
|
32
|
+
|
|
33
|
+
constructor(details: ProviderUsageLimitDetails) {
|
|
34
|
+
super(details.providerMessage);
|
|
35
|
+
this.details = details;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toJSON(): {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly code: string;
|
|
42
|
+
readonly details: ProviderUsageLimitDetails;
|
|
43
|
+
} {
|
|
44
|
+
return { name: this.name, message: this.message, code: this.code, details: this.details };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function providerUsageLimitFromMessages(
|
|
49
|
+
messages: readonly unknown[],
|
|
50
|
+
now = Date.now(),
|
|
51
|
+
): WorkflowProviderUsageLimitError | undefined {
|
|
52
|
+
const message = messages.findLast(isAssistantMessage);
|
|
53
|
+
if (!message || message.stopReason !== "error") return undefined;
|
|
54
|
+
const providerMessage = typeof message.errorMessage === "string" ? message.errorMessage.trim() : "";
|
|
55
|
+
if (!providerMessage || !isUsageLimitMessage(providerMessage)) return undefined;
|
|
56
|
+
const reset = parseProviderResetHint(providerMessage, now);
|
|
57
|
+
const provider = stringDetail(message.provider);
|
|
58
|
+
const model = stringDetail(message.model);
|
|
59
|
+
const api = stringDetail(message.api);
|
|
60
|
+
return new WorkflowProviderUsageLimitError({
|
|
61
|
+
stopReason: "error",
|
|
62
|
+
providerMessage,
|
|
63
|
+
...(provider ? { provider } : {}),
|
|
64
|
+
...(model ? { model } : {}),
|
|
65
|
+
...(api ? { api } : {}),
|
|
66
|
+
...reset,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createProviderUsageLimitPauseRecord(
|
|
71
|
+
error: WorkflowProviderUsageLimitError,
|
|
72
|
+
options: ResolvedWorkflowRunOptions,
|
|
73
|
+
replayableInvocation: boolean,
|
|
74
|
+
now = Date.now(),
|
|
75
|
+
): ProviderUsageLimitPauseRecord {
|
|
76
|
+
const attempt = options.usageLimitAttempt + 1;
|
|
77
|
+
const hintedDelay = error.details.resetAt === undefined
|
|
78
|
+
? WORKFLOW_USAGE_LIMIT_FALLBACK_DELAY_MS
|
|
79
|
+
: error.details.resetAt - now;
|
|
80
|
+
const delayMs = Math.min(
|
|
81
|
+
options.usageLimitMaxDelayMs,
|
|
82
|
+
Math.max(
|
|
83
|
+
WORKFLOW_USAGE_LIMIT_DELAY_MIN_MS,
|
|
84
|
+
hintedDelay > 0 ? hintedDelay : WORKFLOW_USAGE_LIMIT_FALLBACK_DELAY_MS,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
const autoResume = options.autoResumeOnUsageLimit
|
|
88
|
+
&& attempt < options.usageLimitMaxAttempts
|
|
89
|
+
&& replayableInvocation;
|
|
90
|
+
return {
|
|
91
|
+
message: autoResume
|
|
92
|
+
? `Provider usage limit paused this run; automatic attempt ${attempt + 1}/${options.usageLimitMaxAttempts} is scheduled.`
|
|
93
|
+
: `Provider usage limit paused this run at attempt ${attempt}/${options.usageLimitMaxAttempts}.`,
|
|
94
|
+
pause: {
|
|
95
|
+
kind: "provider_usage_limit",
|
|
96
|
+
reason: "provider_usage_limit",
|
|
97
|
+
providerMessage: error.details.providerMessage,
|
|
98
|
+
provider: error.details.provider,
|
|
99
|
+
model: error.details.model,
|
|
100
|
+
api: error.details.api,
|
|
101
|
+
resetHint: error.details.resetHint,
|
|
102
|
+
attempt,
|
|
103
|
+
nextEligibleAt: now + delayMs,
|
|
104
|
+
autoResume,
|
|
105
|
+
maxAttempts: options.usageLimitMaxAttempts,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function parseProviderResetHint(
|
|
111
|
+
message: string,
|
|
112
|
+
now = Date.now(),
|
|
113
|
+
): { readonly resetHint?: string; readonly resetAt?: number } {
|
|
114
|
+
const iso = message.match(
|
|
115
|
+
/(?:[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,
|
|
116
|
+
)?.[1];
|
|
117
|
+
if (iso) {
|
|
118
|
+
const parsed = Date.parse(iso);
|
|
119
|
+
return Number.isFinite(parsed) && parsed > now
|
|
120
|
+
? { resetHint: boundedHint(iso), resetAt: parsed }
|
|
121
|
+
: { resetHint: boundedHint(iso) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const labelled = message.match(
|
|
125
|
+
/(?: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,
|
|
126
|
+
);
|
|
127
|
+
if (labelled?.[1]) {
|
|
128
|
+
const delayMs = parseDurationMs(labelled[1]);
|
|
129
|
+
return delayMs === undefined
|
|
130
|
+
? { resetHint: boundedHint(labelled[0]) }
|
|
131
|
+
: { resetHint: boundedHint(labelled[0]), resetAt: now + delayMs };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const retryAfter = message.match(/retry[- ]after\s*[:=]\s*([^\s,;]+)/i);
|
|
135
|
+
if (retryAfter?.[1]) {
|
|
136
|
+
const seconds = Number(retryAfter[1]);
|
|
137
|
+
return Number.isFinite(seconds) && seconds >= 0
|
|
138
|
+
? { resetHint: boundedHint(retryAfter[0]), resetAt: now + seconds * 1_000 }
|
|
139
|
+
: { resetHint: boundedHint(retryAfter[0]) };
|
|
140
|
+
}
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isUsageLimitMessage(message: string): boolean {
|
|
145
|
+
return /(?:GoUsageLimitError|FreeUsageLimitError|insufficient_quota|rate_limit_error|ResourceExhausted)/i.test(message)
|
|
146
|
+
|| /\btoo many requests\b/i.test(message)
|
|
147
|
+
|| /\b(?:http|status(?: code)?|error)\s*[:=]?\s*429\b/i.test(message)
|
|
148
|
+
|| /\b429\b[^\n]*(?:rate|requests?|quota|limit)/i.test(message)
|
|
149
|
+
|| /\b(?:usage|quota|monthly|weekly|daily)\s+(?:window\s+)?limit\b/i.test(message)
|
|
150
|
+
|| /\bquota\s+(?:has\s+been\s+)?exceeded\b/i.test(message)
|
|
151
|
+
|| /\bexceeded\b[^\n]*\bquota\b/i.test(message)
|
|
152
|
+
|| /\b(?:usage|rate)\s+limit\s+(?:(?:has\s+been|was)\s+)?(?:reached|exceeded)\b/i.test(message);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseDurationMs(value: string): number | undefined {
|
|
156
|
+
const unitPattern = /(\d+(?:\.\d+)?)\s*(milliseconds?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)/gi;
|
|
157
|
+
let total = 0;
|
|
158
|
+
let matched = "";
|
|
159
|
+
for (const part of value.matchAll(unitPattern)) {
|
|
160
|
+
const amount = Number(part[1]);
|
|
161
|
+
const unit = part[2]?.toLowerCase();
|
|
162
|
+
if (!Number.isFinite(amount) || !unit) return undefined;
|
|
163
|
+
matched += part[0];
|
|
164
|
+
if (unit === "ms" || unit.startsWith("millisecond")) total += amount;
|
|
165
|
+
else if (unit === "s" || unit.startsWith("sec")) total += amount * 1_000;
|
|
166
|
+
else if (unit === "m" || unit.startsWith("min")) total += amount * 60_000;
|
|
167
|
+
else if (unit === "h" || unit.startsWith("hr") || unit.startsWith("hour")) total += amount * 3_600_000;
|
|
168
|
+
else total += amount * 86_400_000;
|
|
169
|
+
}
|
|
170
|
+
if (!matched || value.replace(/\s+/g, "") !== matched.replace(/\s+/g, "")) return undefined;
|
|
171
|
+
return Number.isFinite(total) && total >= 0 ? total : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function boundedHint(value: string): string {
|
|
175
|
+
return value.length <= MAX_RESET_HINT_LENGTH ? value : `${value.slice(0, MAX_RESET_HINT_LENGTH - 1)}…`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isAssistantMessage(value: unknown): value is {
|
|
179
|
+
readonly role: "assistant";
|
|
180
|
+
readonly stopReason?: unknown;
|
|
181
|
+
readonly errorMessage?: unknown;
|
|
182
|
+
readonly provider?: unknown;
|
|
183
|
+
readonly model?: unknown;
|
|
184
|
+
readonly api?: unknown;
|
|
185
|
+
} {
|
|
186
|
+
return typeof value === "object" && value !== null && "role" in value && value.role === "assistant";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stringDetail(value: unknown): string | undefined {
|
|
190
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
2
|
+
import { isPathWithin } from "./tree-fingerprint.ts";
|
|
3
|
+
|
|
4
|
+
export interface ReplayWorkspaceRoots {
|
|
5
|
+
readonly sessionCwd: string;
|
|
6
|
+
readonly workspaceRoot: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Map a path in either the disposable session or main workspace to one stable namespace. */
|
|
10
|
+
export function logicalWorkspacePath(path: string, roots: ReplayWorkspaceRoots): string | undefined {
|
|
11
|
+
for (const root of resolvedWorkspaceRoots(roots)) {
|
|
12
|
+
if (isPathWithin(root, path)) return `workspace:${portableRelativePath(root, path)}`;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Normalize absolute workspace paths embedded in generated system prompts. */
|
|
18
|
+
export function normalizeWorkspaceReferences(value: string, roots: ReplayWorkspaceRoots): string {
|
|
19
|
+
const paths = [...resolvedWorkspaceRoots(roots)].sort((left, right) => right.length - left.length);
|
|
20
|
+
return paths.reduce((normalized, path) => replacePathPrefix(normalized, path, "<workspace>"), value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function portableRelativePath(root: string, path: string): string {
|
|
24
|
+
const value = relative(root, path).split(sep).join("/");
|
|
25
|
+
return value.length === 0 ? "." : value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolvedWorkspaceRoots(roots: ReplayWorkspaceRoots): readonly string[] {
|
|
29
|
+
return [...new Set([resolve(roots.sessionCwd), resolve(roots.workspaceRoot)])];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function replacePathPrefix(value: string, prefix: string, replacement: string): string {
|
|
33
|
+
let result = "";
|
|
34
|
+
let cursor = 0;
|
|
35
|
+
while (cursor < value.length) {
|
|
36
|
+
const index = value.indexOf(prefix, cursor);
|
|
37
|
+
if (index === -1) return result + value.slice(cursor);
|
|
38
|
+
const next = value[index + prefix.length];
|
|
39
|
+
if (next !== undefined && next !== "/" && next !== "\\" && /[\p{L}\p{N}._-]/u.test(next)) {
|
|
40
|
+
result += value.slice(cursor, index + prefix.length);
|
|
41
|
+
cursor = index + prefix.length;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
result += value.slice(cursor, index) + replacement;
|
|
45
|
+
cursor = index + prefix.length;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|