@jameslovespancakes/pi-plus 1.0.19 → 1.0.21
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 +133 -246
- package/package.json +3 -2
- package/src/core/claude-remote/LICENSE.md +22 -0
- package/src/core/claude-remote/UPSTREAM.md +40 -0
- package/src/core/claude-remote/bridge.ts +392 -0
- package/src/core/claude-remote/protocol.ts +83 -0
- package/src/core/env.ts +7 -1
- package/src/domains/claude-remote/auth.ts +25 -0
- package/src/domains/claude-remote/index.ts +183 -0
- package/src/domains/claude-remote/picker.ts +36 -0
- package/src/domains/models/provider-picker.ts +3 -46
- package/src/domains/setup/index.ts +12 -1
- package/src/domains/workflows/index.ts +56 -104
- package/src/domains/workflows/runtime/advisory-challenge.ts +3 -3
- package/src/domains/workflows/runtime/agent-attempt.ts +3 -3
- package/src/domains/workflows/runtime/agent-options.ts +18 -0
- package/src/domains/workflows/runtime/agent-runner-types.ts +7 -3
- package/src/domains/workflows/runtime/agent-runner.ts +14 -6
- package/src/domains/workflows/runtime/agent-session.ts +34 -3
- package/src/domains/workflows/runtime/cancellation.ts +5 -0
- package/src/domains/workflows/runtime/engine.ts +19 -40
- package/src/domains/workflows/runtime/journal.ts +4 -4
- package/src/domains/workflows/runtime/live-agent.ts +37 -0
- package/src/domains/workflows/runtime/model-profiles.ts +2 -6
- package/src/domains/workflows/runtime/progress-types.ts +3 -1
- package/src/domains/workflows/runtime/progress.ts +69 -42
- package/src/domains/workflows/runtime/review/review-fix-workflow.ts +3 -3
- package/src/domains/workflows/runtime/types.ts +16 -15
- package/src/domains/workflows/runtime/ui/agent-transcript.ts +59 -0
- package/src/domains/workflows/runtime/ui/workflow-format.ts +6 -2
- package/src/domains/workflows/runtime/ui/workflow-inspector.ts +75 -61
- package/src/domains/workflows/runtime/ui/workflow-widget.ts +26 -66
- package/src/domains/workflows/runtime/workflow-advisory-utils.ts +5 -5
- package/src/domains/workflows/runtime/{background-workflows.ts → workflow-lifecycle.ts} +91 -75
- package/src/domains/workflows/runtime/workflow-management.ts +66 -0
- package/src/domains/workflows/runtime/workflow-run-controller.ts +16 -20
- package/src/domains/workflows/runtime/{workflow-run-background.ts → workflow-run-delivery.ts} +3 -3
- package/src/domains/workflows/runtime/workflow-run-record.ts +7 -4
- package/src/domains/workflows/workflows/code-review.ts +1 -1
- package/src/domains/workflows/workflows/diagnose.ts +1 -1
- package/src/domains/workflows/workflows/perf-review.ts +1 -1
- package/src/domains/workflows/workflows/refactor-scout.ts +1 -1
- package/src/domains/workflows/workflows/research.ts +4 -4
- package/src/ui/settings-picker.ts +26 -0
- package/src/domains/workflows/runtime/background-workflow-tool.ts +0 -75
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentTranscript } from "./live-agent.ts";
|
|
2
3
|
import type { WorkflowProgressEvent } from "./types.ts";
|
|
3
4
|
import type { AgentChatMessage, AgentChatRole, AgentRowStatus, WorkflowLaneItemStatus, WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
4
|
-
import {
|
|
5
|
+
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
5
6
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
6
|
-
import {
|
|
7
|
+
import type { WorkflowStatusCounts } from "./ui/workflow-format.ts";
|
|
7
8
|
import { toDisplayLine, toDisplayText } from "./ui/display-text.ts";
|
|
8
9
|
import { renderWorkflowWidgetLines } from "./ui/workflow-widget.ts";
|
|
9
10
|
|
|
@@ -23,6 +24,8 @@ interface AgentRow {
|
|
|
23
24
|
id: number;
|
|
24
25
|
label: string;
|
|
25
26
|
model?: string;
|
|
27
|
+
modelName?: string;
|
|
28
|
+
thinkingLevel?: string;
|
|
26
29
|
status: AgentRowStatus;
|
|
27
30
|
startedAt?: number;
|
|
28
31
|
doneAt?: number;
|
|
@@ -61,7 +64,7 @@ const WIDGET_REFRESH_INTERVAL_MS = 1_000;
|
|
|
61
64
|
export const DEFAULT_LANE_ITEM_LIMIT = 200;
|
|
62
65
|
|
|
63
66
|
/**
|
|
64
|
-
* Tracks live workflow state for widgets,
|
|
67
|
+
* Tracks live workflow state for widgets, result renderers,
|
|
65
68
|
* and headless stderr breadcrumbs.
|
|
66
69
|
*/
|
|
67
70
|
export class ProgressTracker {
|
|
@@ -73,25 +76,34 @@ export class ProgressTracker {
|
|
|
73
76
|
private readonly laneOverflow = new Map<string, number>();
|
|
74
77
|
private readonly rowsById = new Map<number, AgentRow>();
|
|
75
78
|
private readonly agentChats = new Map<number, AgentChatMessage[]>();
|
|
76
|
-
private readonly
|
|
79
|
+
private readonly agentTranscripts = new Map<number, () => AgentTranscript>();
|
|
80
|
+
private readonly agentStops = new Map<number, () => void>();
|
|
81
|
+
private readonly agentFollowUps = new Map<number, (message: string, steer?: boolean) => Promise<void>>();
|
|
77
82
|
private readonly listeners = new Set<() => void>();
|
|
78
|
-
private readonly agentCounts: Record<AgentRowStatus, number> = { queued: 0, running: 0, done: 0, failed: 0 };
|
|
83
|
+
private readonly agentCounts: Record<AgentRowStatus, number> = { queued: 0, running: 0, stopping: 0, stopped: 0, done: 0, failed: 0 };
|
|
79
84
|
private readonly startedAt = Date.now();
|
|
80
85
|
private readonly laneItemLimit = laneItemLimitFromEnv();
|
|
81
86
|
private doneAt: number | undefined;
|
|
82
87
|
private currentPhase = "Workflow";
|
|
83
88
|
private nextAgentId = 1;
|
|
84
|
-
private lastStatusText: string | undefined;
|
|
85
89
|
private usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
86
90
|
private widgetRefreshInterval: ReturnType<typeof setInterval> | undefined;
|
|
87
91
|
private readonly surfaceKey: string;
|
|
92
|
+
private readonly ctx: ExtensionContext;
|
|
93
|
+
private readonly title: string;
|
|
94
|
+
private readonly runId: string;
|
|
95
|
+
private readonly onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void;
|
|
88
96
|
|
|
89
97
|
constructor(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
ctx: ExtensionContext,
|
|
99
|
+
title: string,
|
|
100
|
+
runId: string,
|
|
101
|
+
onSnapshot?: (snapshot: WorkflowProgressSnapshot) => void,
|
|
94
102
|
) {
|
|
103
|
+
this.ctx = ctx;
|
|
104
|
+
this.title = title;
|
|
105
|
+
this.runId = runId;
|
|
106
|
+
this.onSnapshot = onSnapshot;
|
|
95
107
|
this.surfaceKey = `workflow:${runId}`;
|
|
96
108
|
this.ensurePhase(this.currentPhase);
|
|
97
109
|
}
|
|
@@ -154,9 +166,9 @@ export class ProgressTracker {
|
|
|
154
166
|
this.publish();
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
agentQueued(phase: string | undefined, label: string, model?: string): number {
|
|
169
|
+
agentQueued(phase: string | undefined, label: string, model?: string, modelName?: string, thinkingLevel?: string): number {
|
|
158
170
|
const id = this.nextAgentId++;
|
|
159
|
-
const row = { label, model, id, status: "queued" as const, toolUses: 0 };
|
|
171
|
+
const row = { label, model, modelName, thinkingLevel, id, status: "queued" as const, toolUses: 0 };
|
|
160
172
|
this.ensurePhase(phase ?? this.currentPhase).agents.push(row);
|
|
161
173
|
this.rowsById.set(id, row);
|
|
162
174
|
this.agentCounts.queued++;
|
|
@@ -201,7 +213,27 @@ export class ProgressTracker {
|
|
|
201
213
|
this.publish();
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
|
|
216
|
+
bindAgentTranscript(id: number, read: () => AgentTranscript): () => void {
|
|
217
|
+
this.agentTranscripts.set(id, read);
|
|
218
|
+
return () => {
|
|
219
|
+
const last = read();
|
|
220
|
+
const snapshot = { ...last, messages: [...last.messages], streaming: undefined };
|
|
221
|
+
this.agentTranscripts.set(id, () => snapshot);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
transcript(id: number): AgentTranscript | undefined {
|
|
226
|
+
return this.agentTranscripts.get(id)?.();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
agentChanged(id: number, model?: string, modelName?: string, thinkingLevel?: string): void {
|
|
230
|
+
const row = this.rowsById.get(id);
|
|
231
|
+
if (row) Object.assign(row, { model, modelName, thinkingLevel });
|
|
232
|
+
// Stream updates are UI-only; do not rewrite the durable run on every token.
|
|
233
|
+
for (const listener of this.listeners) listener();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
bindAgentFollowUp(id: number, send: (message: string, steer?: boolean) => Promise<void>): () => void {
|
|
205
237
|
this.agentFollowUps.set(id, send);
|
|
206
238
|
this.publish();
|
|
207
239
|
return () => {
|
|
@@ -210,16 +242,32 @@ export class ProgressTracker {
|
|
|
210
242
|
};
|
|
211
243
|
}
|
|
212
244
|
|
|
245
|
+
bindAgentStop(id: number, stop: () => void): () => void {
|
|
246
|
+
this.agentStops.set(id, stop);
|
|
247
|
+
return () => this.agentStops.delete(id);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
stopAgent(id: number): void {
|
|
251
|
+
const row = this.rowsById.get(id);
|
|
252
|
+
if (!row) throw new Error(`Unknown agent ${id}.`);
|
|
253
|
+
if (row.status !== "running" && row.status !== "queued") return;
|
|
254
|
+
const stop = this.agentStops.get(id);
|
|
255
|
+
if (!stop) throw new Error("Agent cannot be stopped right now.");
|
|
256
|
+
this.transitionAgentStatus(row, "stopping");
|
|
257
|
+
stop();
|
|
258
|
+
this.publish();
|
|
259
|
+
}
|
|
260
|
+
|
|
213
261
|
conversation(id: number): readonly AgentChatMessage[] {
|
|
214
262
|
return (this.agentChats.get(id) ?? []).map((message) => ({ ...message }));
|
|
215
263
|
}
|
|
216
264
|
|
|
217
|
-
async followUp(id: number, message: string): Promise<void> {
|
|
218
|
-
const text =
|
|
265
|
+
async followUp(id: number, message: string, steer = false): Promise<void> {
|
|
266
|
+
const text = message.trim();
|
|
219
267
|
if (!text) throw new Error("Enter a follow-up message.");
|
|
220
268
|
const send = this.agentFollowUps.get(id);
|
|
221
269
|
if (!send) throw new Error("This agent is no longer accepting follow-ups.");
|
|
222
|
-
await send(text);
|
|
270
|
+
await send(text, steer);
|
|
223
271
|
this.appendAgentChat(id, "user", text);
|
|
224
272
|
this.publish();
|
|
225
273
|
}
|
|
@@ -231,7 +279,7 @@ export class ProgressTracker {
|
|
|
231
279
|
|
|
232
280
|
agentDone(label: string, id?: number): void {
|
|
233
281
|
const row = this.findRow(label, id);
|
|
234
|
-
if (row && row.status
|
|
282
|
+
if (row && (row.status === "running" || row.status === "queued")) {
|
|
235
283
|
this.transitionAgentStatus(row, "done");
|
|
236
284
|
row.doneAt = Date.now();
|
|
237
285
|
}
|
|
@@ -241,7 +289,7 @@ export class ProgressTracker {
|
|
|
241
289
|
agentFailed(label: string, error: unknown, id?: number): void {
|
|
242
290
|
const row = this.findRow(label, id);
|
|
243
291
|
if (row) {
|
|
244
|
-
this.transitionAgentStatus(row, "failed");
|
|
292
|
+
this.transitionAgentStatus(row, row.status === "stopping" ? "stopped" : "failed");
|
|
245
293
|
row.doneAt = Date.now();
|
|
246
294
|
row.error = toDisplayLine(unknownErrorMessage(error), AGENT_ERROR_DISPLAY_LIMIT) || "agent failed";
|
|
247
295
|
this.appendAgentChat(row.id, "status", row.error);
|
|
@@ -281,10 +329,10 @@ export class ProgressTracker {
|
|
|
281
329
|
private statusCountsSnapshot(): WorkflowStatusCounts {
|
|
282
330
|
return {
|
|
283
331
|
queued: this.agentCounts.queued,
|
|
284
|
-
running: this.agentCounts.running,
|
|
332
|
+
running: this.agentCounts.running + this.agentCounts.stopping,
|
|
285
333
|
done: this.agentCounts.done,
|
|
286
|
-
failed: this.agentCounts.failed,
|
|
287
|
-
total: this.agentCounts.
|
|
334
|
+
failed: this.agentCounts.failed + this.agentCounts.stopped,
|
|
335
|
+
total: Object.values(this.agentCounts).reduce((sum, count) => sum + count, 0),
|
|
288
336
|
};
|
|
289
337
|
}
|
|
290
338
|
|
|
@@ -336,7 +384,6 @@ export class ProgressTracker {
|
|
|
336
384
|
if (!this.ctx.hasUI) return;
|
|
337
385
|
this.publishWidget();
|
|
338
386
|
this.startWidgetRefresh();
|
|
339
|
-
this.publishStatus();
|
|
340
387
|
}
|
|
341
388
|
|
|
342
389
|
private publishWidget(): void {
|
|
@@ -358,24 +405,6 @@ export class ProgressTracker {
|
|
|
358
405
|
this.widgetRefreshInterval = undefined;
|
|
359
406
|
}
|
|
360
407
|
|
|
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
408
|
/** Clear this run's live workflow surfaces. Final feedback is delivered by the result surface. */
|
|
380
409
|
done(): void {
|
|
381
410
|
this.doneAt = Date.now();
|
|
@@ -383,8 +412,6 @@ export class ProgressTracker {
|
|
|
383
412
|
this.publishSnapshot();
|
|
384
413
|
if (!this.ctx.hasUI) return;
|
|
385
414
|
this.ctx.ui.setWidget(this.surfaceKey, undefined);
|
|
386
|
-
this.ctx.ui.setStatus(this.surfaceKey, undefined);
|
|
387
|
-
this.lastStatusText = undefined;
|
|
388
415
|
}
|
|
389
416
|
|
|
390
417
|
private publishSnapshot(): void {
|
|
@@ -32,7 +32,7 @@ export interface ReviewFixWorkflowResult {
|
|
|
32
32
|
readonly fixes: readonly ReviewFixOutcome[];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export type ReviewFixWorkflowApi = Pick<WorkflowApi, "agent" | "parallel" | "phase" | "cwd" | "signal">;
|
|
35
|
+
export type ReviewFixWorkflowApi = Pick<WorkflowApi, "agent" | "modelProfile" | "parallel" | "phase" | "cwd" | "signal">;
|
|
36
36
|
|
|
37
37
|
/** Build an ephemeral workflow that generates one isolated patch preview per finding. */
|
|
38
38
|
export function createReviewFixWorkflow(
|
|
@@ -89,7 +89,7 @@ export async function runReviewFixWorkflow(
|
|
|
89
89
|
isolation: "worktree",
|
|
90
90
|
label: `fix:${issue.id}`,
|
|
91
91
|
phase: REVIEW_FIX_PHASE,
|
|
92
|
-
|
|
92
|
+
...api.modelProfile("medium"),
|
|
93
93
|
cacheKey: `review-fix:${issue.id}`,
|
|
94
94
|
tools: [...REVIEW_FIX_TOOLS],
|
|
95
95
|
toolHints: ["search"],
|
|
@@ -138,7 +138,7 @@ async function evaluateReviewFix(api: ReviewFixWorkflowApi, input: {
|
|
|
138
138
|
const evaluated = await api.agent(
|
|
139
139
|
`Independently evaluate a candidate repair. Your fresh worktree contains the exact reviewed baseline plus the captured patch. The implementer's report is not validation evidence. Inspect the finding, callers and tests. Reject incorrect repairs; return blocked if required validation is unavailable. Select at most six focused deterministic checks with executable and argument arrays. Require at least one meaningful behavior check. If a regression test is applicable, supply a test-only baselinePatch and specific expectedFailure so the engine can prove it fails before the repair and passes after. Do not edit, install dependencies, commit or change branches. The engine will execute checks independently.\nFinding: ${JSON.stringify(serializeReviewIssue(issue))}\nBaseline: ${isolated.baselineOid}\nPatch SHA-256: ${validation.patchHash}\nPatch:\n${isolated.patch}`,
|
|
140
140
|
{ isolation: "worktree", candidatePatch: { baselineOid: isolated.baselineOid, patch: isolated.patch },
|
|
141
|
-
label: `evaluate:${issue.id}`, phase: "Validate patch previews",
|
|
141
|
+
label: `evaluate:${issue.id}`, phase: "Validate patch previews", ...api.modelProfile("medium"), resume: "off",
|
|
142
142
|
tools: ["read", "bash", "grep", "find", "ls"], toolHints: ["search"], schema: PatchEvaluationSchema },
|
|
143
143
|
);
|
|
144
144
|
if (evaluated.baselineOid !== isolated.baselineOid || evaluated.patch !== isolated.patch) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Static, TSchema } from "typebox";
|
|
2
2
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { AgentTranscript } from "./live-agent.ts";
|
|
3
4
|
import type { WorkflowBudget } from "./budget.ts";
|
|
4
5
|
import type { Pipeline, WorkflowParallel } from "./concurrency.ts";
|
|
5
6
|
import type { PerfSink, PerfSnapshot } from "./perf.ts";
|
|
@@ -35,14 +36,13 @@ export interface WorkflowRunMetadata {
|
|
|
35
36
|
readonly recordPath: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export interface
|
|
39
|
+
export interface WorkflowOrigin {
|
|
39
40
|
/** Stable pi session id used to route and deduplicate completion delivery. */
|
|
40
41
|
readonly sessionId: string;
|
|
41
42
|
readonly requestedAt: number;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface WorkflowRunOptions {
|
|
45
|
-
inspect?: boolean;
|
|
46
46
|
perf?: boolean;
|
|
47
47
|
concurrency?: number;
|
|
48
48
|
parallelSubmissionLimit?: number;
|
|
@@ -64,8 +64,8 @@ export interface WorkflowRunOptions {
|
|
|
64
64
|
budget?: number;
|
|
65
65
|
/** Internal/test override for the generated run id. Omit to generate a new id. */
|
|
66
66
|
runId?: string;
|
|
67
|
-
/**
|
|
68
|
-
|
|
67
|
+
/** Owning session for durable completion delivery. */
|
|
68
|
+
origin?: WorkflowOrigin;
|
|
69
69
|
/** Replay completed agent results from this prior run id when call and execution context still match. */
|
|
70
70
|
resumeFromRunId?: string;
|
|
71
71
|
/** Explicitly allow resume to ignore only a workflow-source fingerprint mismatch. */
|
|
@@ -92,7 +92,9 @@ export interface WorkflowRunOptions {
|
|
|
92
92
|
export interface WorkflowProgressSource {
|
|
93
93
|
snapshot(): WorkflowProgressSnapshot;
|
|
94
94
|
conversation(agentId: number): readonly AgentChatMessage[];
|
|
95
|
-
|
|
95
|
+
stopAgent?(agentId: number): void;
|
|
96
|
+
transcript?(agentId: number): AgentTranscript | undefined;
|
|
97
|
+
followUp(agentId: number, message: string, steer?: boolean): Promise<void>;
|
|
96
98
|
subscribe(listener: () => void): () => void;
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -122,19 +124,16 @@ export type WorkflowProgressEvent =
|
|
|
122
124
|
|
|
123
125
|
export interface AgentOptions<S extends TSchema = TSchema> {
|
|
124
126
|
/** Label shown in the progress tree (e.g. "find:logic-bugs"). */
|
|
125
|
-
label
|
|
127
|
+
label: string;
|
|
126
128
|
/** Phase to group this agent under in the progress tree. */
|
|
127
129
|
phase?: string;
|
|
128
130
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* Anthropic shorthand; use "provider/id" for other providers.
|
|
131
|
+
* Required model id. Bare ids resolve as Anthropic shorthand;
|
|
132
|
+
* use "provider/id" for other providers. No implicit host fallback.
|
|
132
133
|
*/
|
|
133
|
-
model
|
|
134
|
-
/**
|
|
135
|
-
thinkingLevel
|
|
136
|
-
/** Exact configured model route to use when model/thinkingLevel do not override it. */
|
|
137
|
-
profile?: WorkflowModelProfileName;
|
|
134
|
+
model: string;
|
|
135
|
+
/** Required reasoning effort for this agent. */
|
|
136
|
+
thinkingLevel: ThinkingLevel;
|
|
138
137
|
/**
|
|
139
138
|
* Stable identity hint for resume replay. Use this for repeated logical calls
|
|
140
139
|
* with identical prompts/options, e.g. `${stage}:${item.id}`.
|
|
@@ -187,6 +186,8 @@ export interface AgentOptions<S extends TSchema = TSchema> {
|
|
|
187
186
|
* exports `meta` plus a default `async (api: WorkflowApi) => result`.
|
|
188
187
|
*/
|
|
189
188
|
export interface WorkflowApi {
|
|
189
|
+
/** Resolve an explicitly configured route; throws instead of inheriting the host. */
|
|
190
|
+
modelProfile(name: WorkflowModelProfileName): Pick<AgentOptions, "model" | "thinkingLevel">;
|
|
190
191
|
/** Run a schema subagent in an isolated worktree and return its structured result plus patch. */
|
|
191
192
|
agent<S extends TSchema>(
|
|
192
193
|
prompt: string,
|
|
@@ -197,7 +198,7 @@ export interface WorkflowApi {
|
|
|
197
198
|
/** Run a subagent and return validated structured output; rejects with a recoverable typed error on repair exhaustion. */
|
|
198
199
|
agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
|
|
199
200
|
/** Run a subagent and return its final assistant text. */
|
|
200
|
-
agent(prompt: string, opts
|
|
201
|
+
agent(prompt: string, opts: AgentOptions): Promise<string>;
|
|
201
202
|
/**
|
|
202
203
|
* Run another registered workflow inline as a sub-step and return its result. The child shares
|
|
203
204
|
* this run's concurrency cap, abort signal, and perf sink. Nests one level only: calling
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { AssistantMessageComponent, ToolExecutionComponent, UserMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { AgentTranscript } from "../live-agent.ts";
|
|
4
|
+
|
|
5
|
+
/** Native pi transcript components, driven by the child session's own messages. */
|
|
6
|
+
export class AgentTranscriptView {
|
|
7
|
+
private readonly components = new WeakMap<object, Component>();
|
|
8
|
+
private readonly tools = new Map<string, ToolExecutionComponent>();
|
|
9
|
+
private readonly results = new Map<string, object>();
|
|
10
|
+
|
|
11
|
+
render(transcript: AgentTranscript, width: number, tui: TUI, cwd: string): string[] {
|
|
12
|
+
const messages = transcript.streaming && !transcript.messages.includes(transcript.streaming)
|
|
13
|
+
? [...transcript.messages, transcript.streaming] : transcript.messages;
|
|
14
|
+
const rows: string[] = [];
|
|
15
|
+
for (const message of messages) {
|
|
16
|
+
if (message.role === "user") {
|
|
17
|
+
let component = this.components.get(message);
|
|
18
|
+
if (!component) {
|
|
19
|
+
const text = typeof message.content === "string" ? message.content
|
|
20
|
+
: message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
21
|
+
component = new UserMessageComponent(text);
|
|
22
|
+
this.components.set(message, component);
|
|
23
|
+
}
|
|
24
|
+
rows.push(...component.render(width));
|
|
25
|
+
} else if (message.role === "assistant") {
|
|
26
|
+
// Streaming messages can be mutated by pi; update the native component every render.
|
|
27
|
+
let component = this.components.get(message) as AssistantMessageComponent | undefined;
|
|
28
|
+
if (!component) {
|
|
29
|
+
component = new AssistantMessageComponent(message, false);
|
|
30
|
+
this.components.set(message, component);
|
|
31
|
+
}
|
|
32
|
+
component.updateContent(message, message === transcript.streaming);
|
|
33
|
+
rows.push(...component.render(width));
|
|
34
|
+
for (const part of message.content) {
|
|
35
|
+
if (part.type !== "toolCall") continue;
|
|
36
|
+
let tool = this.tools.get(part.id);
|
|
37
|
+
if (!tool) {
|
|
38
|
+
tool = new ToolExecutionComponent(part.name, part.id, part.arguments, { showImages: false }, undefined, tui, cwd);
|
|
39
|
+
this.tools.set(part.id, tool);
|
|
40
|
+
}
|
|
41
|
+
tool.updateArgs(part.arguments);
|
|
42
|
+
if (message !== transcript.streaming) tool.setArgsComplete();
|
|
43
|
+
const update = transcript.toolUpdates?.get(part.id);
|
|
44
|
+
if (update && this.results.get(part.id) !== update) {
|
|
45
|
+
tool.updateResult({ ...update.result, isError: update.isError }, update.isPartial);
|
|
46
|
+
this.results.set(part.id, update);
|
|
47
|
+
}
|
|
48
|
+
const result = messages.find((candidate) => candidate.role === "toolResult" && candidate.toolCallId === part.id);
|
|
49
|
+
if (result?.role === "toolResult" && this.results.get(part.id) !== result) {
|
|
50
|
+
tool.updateResult(result);
|
|
51
|
+
this.results.set(part.id, result);
|
|
52
|
+
}
|
|
53
|
+
rows.push(...tool.render(width));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return rows;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { AgentRowSnapshot, WorkflowLaneItemStatus, WorkflowProgressSnapshot
|
|
|
4
4
|
import { toDisplayLine } from "./display-text.ts";
|
|
5
5
|
import { formatWorkflowUsageLine } from "../usage.ts";
|
|
6
6
|
|
|
7
|
-
export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed";
|
|
7
|
+
export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed" | "stopping" | "stopped";
|
|
8
8
|
export type WorkflowThemeColor = Parameters<Theme["fg"]>[0];
|
|
9
9
|
|
|
10
10
|
export function formatDuration(ms: number): string {
|
|
@@ -42,6 +42,10 @@ export function statusIcon(status: WorkflowDisplayStatus, theme: Theme): string
|
|
|
42
42
|
case "error":
|
|
43
43
|
case "failed":
|
|
44
44
|
return theme.fg("error", "✗");
|
|
45
|
+
case "stopped":
|
|
46
|
+
return theme.fg("dim", "■");
|
|
47
|
+
case "stopping":
|
|
48
|
+
return theme.fg("warning", "◌");
|
|
45
49
|
case "running":
|
|
46
50
|
return theme.fg("accent", "●");
|
|
47
51
|
case "queued":
|
|
@@ -151,7 +155,7 @@ function countSnapshotAgents(snapshot: WorkflowProgressSnapshot): WorkflowStatus
|
|
|
151
155
|
const counts = { queued: 0, running: 0, done: 0, failed: 0, total: 0 };
|
|
152
156
|
for (const phase of snapshot.phases) {
|
|
153
157
|
for (const agent of phase.agents) {
|
|
154
|
-
counts[agent.status]++;
|
|
158
|
+
counts[agent.status === "stopped" ? "failed" : agent.status === "stopping" ? "running" : agent.status]++;
|
|
155
159
|
counts.total++;
|
|
156
160
|
}
|
|
157
161
|
}
|