@jameslovespancakes/pi-plus 1.0.20 → 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 +21 -3
- package/package.json +1 -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 +54 -14
- 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/domains/workflows/runtime/background-workflow-tool.ts +0 -75
|
@@ -3,6 +3,7 @@ import type { Static, TSchema } from "typebox";
|
|
|
3
3
|
import { bindParallel, bindPipeline, Semaphore } from "./concurrency.ts";
|
|
4
4
|
import { WorkflowAgentLimiter } from "./agent-limits.ts";
|
|
5
5
|
import { defaultAgentRetryScheduler, type AgentRetryScheduler } from "./agent-retry.ts";
|
|
6
|
+
import { assertAgentOptions } from "./agent-options.ts";
|
|
6
7
|
import { resolveWorkflowModelProfiles, type ResolvedWorkflowModelProfiles } from "./model-profiles.ts";
|
|
7
8
|
import { abortReason, isWorkflowPauseError, linkAbortSignal, throwIfAborted } from "./cancellation.ts";
|
|
8
9
|
import { createBudget } from "./budget.ts";
|
|
@@ -15,8 +16,6 @@ import {
|
|
|
15
16
|
type ResolvedWorkflowRunOptions,
|
|
16
17
|
} from "./options.ts";
|
|
17
18
|
import type { AgentOptions, IsolatedAgentResult, LoadedWorkflow, WorkflowApi, WorkflowProgressEvent, WorkflowRef, WorkflowRunOptions } from "./types.ts";
|
|
18
|
-
import { WorkflowInspector } from "./ui/workflow-inspector.ts";
|
|
19
|
-
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./ui/workflow-viewer-layout.ts";
|
|
20
19
|
import { createWorkflowJournal, createWorkflowRunId, pruneWorkflowJournals, workflowJournalPath } from "./journal.ts";
|
|
21
20
|
import { WorktreeRegistry } from "./worktree.ts";
|
|
22
21
|
import { runFinalizers } from "./finalizers.ts";
|
|
@@ -101,7 +100,9 @@ export async function runResolvedWorkflow(
|
|
|
101
100
|
const progressSource = {
|
|
102
101
|
snapshot: () => progress.snapshot(),
|
|
103
102
|
conversation: (agentId: number) => progress.conversation(agentId),
|
|
104
|
-
|
|
103
|
+
stopAgent: (agentId: number) => progress.stopAgent(agentId),
|
|
104
|
+
transcript: (agentId: number) => progress.transcript(agentId),
|
|
105
|
+
followUp: (agentId: number, message: string, steer?: boolean) => progress.followUp(agentId, message, steer),
|
|
105
106
|
subscribe: (listener: () => void) => progress.subscribe(listener),
|
|
106
107
|
};
|
|
107
108
|
const perf = resolvedOptions.perfRecorder ?? createPerfRecorder(resolvedOptions.perf);
|
|
@@ -133,33 +134,6 @@ export async function runResolvedWorkflow(
|
|
|
133
134
|
const unlinkOptionAbortSignal = linkAbortSignal(resolvedOptions.signal, runAbortController);
|
|
134
135
|
const workflowOutcome = await captureOutcome(async () => {
|
|
135
136
|
await notifyLifecycleObserver(progress, "progress source callback", () => resolvedOptions.onProgressSource?.(progressSource));
|
|
136
|
-
if (resolvedOptions.inspect && ctx.hasUI && ctx.mode === "tui") {
|
|
137
|
-
let unsubscribe: (() => void) | undefined;
|
|
138
|
-
void ctx.ui
|
|
139
|
-
.custom<void>(
|
|
140
|
-
(tui, theme, _keybindings, done) => {
|
|
141
|
-
unsubscribe = progressSource.subscribe(() => tui.requestRender());
|
|
142
|
-
return new WorkflowInspector(
|
|
143
|
-
() => progress.snapshot(),
|
|
144
|
-
tui,
|
|
145
|
-
theme,
|
|
146
|
-
() => done(undefined),
|
|
147
|
-
undefined,
|
|
148
|
-
progressSource,
|
|
149
|
-
);
|
|
150
|
-
},
|
|
151
|
-
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
152
|
-
)
|
|
153
|
-
.catch((error: unknown) => {
|
|
154
|
-
try {
|
|
155
|
-
progress.log(`inspector failed: ${unknownErrorMessage(error)}`);
|
|
156
|
-
} catch {
|
|
157
|
-
// Inspector reporting is detached and must never become another rejection.
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
.finally(() => unsubscribe?.());
|
|
161
|
-
}
|
|
162
|
-
|
|
163
137
|
const journal = await createWorkflowJournal({ resumePath, writePath: journalPath });
|
|
164
138
|
durableRun.transition({ state: "running", progress: progress.snapshot() });
|
|
165
139
|
await durableRun.flush().catch(() => undefined);
|
|
@@ -185,7 +159,7 @@ export async function runResolvedWorkflow(
|
|
|
185
159
|
agentLimiter: new WorkflowAgentLimiter(resolvedOptions.maxAgents),
|
|
186
160
|
agentTimeoutMs: resolvedOptions.agentTimeoutMs,
|
|
187
161
|
agentRetries: resolvedOptions.agentRetries,
|
|
188
|
-
pauseOnProviderUsageLimit: resolvedOptions.
|
|
162
|
+
pauseOnProviderUsageLimit: resolvedOptions.origin !== undefined,
|
|
189
163
|
resumeEditedWorkflow: resolvedOptions.resumeEditedWorkflow,
|
|
190
164
|
retryScheduler: dependencies.retryScheduler ?? defaultAgentRetryScheduler,
|
|
191
165
|
modelProfiles,
|
|
@@ -214,11 +188,8 @@ export async function runResolvedWorkflow(
|
|
|
214
188
|
? undefined
|
|
215
189
|
: backgroundPauseError(workflowOutcome.error, ctx.signal, resolvedOptions.signal);
|
|
216
190
|
const willPauseWorkflow = workflowPause !== undefined
|
|
217
|
-
&& (!(workflowPause instanceof WorkflowProviderUsageLimitError) || resolvedOptions.
|
|
218
|
-
const preserveFailedWorktrees = !workflowOutcome.ok
|
|
219
|
-
&& !willPauseWorkflow
|
|
220
|
-
&& !ctx.signal?.aborted
|
|
221
|
-
&& !resolvedOptions.signal?.aborted;
|
|
191
|
+
&& (!(workflowPause instanceof WorkflowProviderUsageLimitError) || resolvedOptions.origin !== undefined);
|
|
192
|
+
const preserveFailedWorktrees = !workflowOutcome.ok && !willPauseWorkflow;
|
|
222
193
|
if (preserveFailedWorktrees) worktrees.preserveRecoverable();
|
|
223
194
|
const finalizationOutcome = await captureOutcome(() =>
|
|
224
195
|
finalizeWorkflowRun({
|
|
@@ -229,7 +200,7 @@ export async function runResolvedWorkflow(
|
|
|
229
200
|
progress,
|
|
230
201
|
runStore,
|
|
231
202
|
worktrees,
|
|
232
|
-
preserveWorktrees: preserveFailedWorktrees,
|
|
203
|
+
preserveWorktrees: preserveFailedWorktrees || worktrees.preservedPaths.length > 0,
|
|
233
204
|
unlinkSignals: [unlinkContextAbortSignal, unlinkOptionAbortSignal],
|
|
234
205
|
}),
|
|
235
206
|
);
|
|
@@ -259,7 +230,7 @@ export async function runResolvedWorkflow(
|
|
|
259
230
|
const pauseError = backgroundPauseError(pauseCandidate, ctx.signal, resolvedOptions.signal);
|
|
260
231
|
if (pauseError) {
|
|
261
232
|
if (pauseError instanceof WorkflowProviderUsageLimitError) {
|
|
262
|
-
if (resolvedOptions.
|
|
233
|
+
if (resolvedOptions.origin === undefined) {
|
|
263
234
|
durableRun.transition({
|
|
264
235
|
state: "failed",
|
|
265
236
|
progress: progress.snapshot(),
|
|
@@ -428,6 +399,7 @@ function createWorkflowAgent(
|
|
|
428
399
|
function agent<S extends TSchema>(prompt: string, opts: AgentOptions<S> & { schema: S }): Promise<Static<S>>;
|
|
429
400
|
function agent(prompt: string, opts?: AgentOptions): Promise<string>;
|
|
430
401
|
function agent(prompt: string, agentOpts?: AgentOptions): Promise<unknown> {
|
|
402
|
+
assertAgentOptions(agentOpts);
|
|
431
403
|
const scopedOptions = scope.agentOptions(agentOpts);
|
|
432
404
|
const executionOptions: AgentExecutionOptions =
|
|
433
405
|
scopedOptions.isolation === "worktree" && mod.isolatedWorktreeBaseline !== undefined
|
|
@@ -486,6 +458,13 @@ export async function runWorkflowWithContext(
|
|
|
486
458
|
};
|
|
487
459
|
|
|
488
460
|
const api: WorkflowApi = {
|
|
461
|
+
modelProfile(name) {
|
|
462
|
+
const profile = rc.modelProfiles[name];
|
|
463
|
+
if (!profile || profile.source === "host" || !profile.model || !profile.thinkingLevel) {
|
|
464
|
+
throw new Error(`Configure workflow profile ${name} with an explicit model and thinkingLevel in workflow-models.json.`);
|
|
465
|
+
}
|
|
466
|
+
return { model: `${profile.model.provider}/${profile.model.id}`, thinkingLevel: profile.thinkingLevel };
|
|
467
|
+
},
|
|
489
468
|
agent,
|
|
490
469
|
workflow,
|
|
491
470
|
parallel: bindParallel({
|
|
@@ -510,7 +489,7 @@ export async function runWorkflowWithContext(
|
|
|
510
489
|
}
|
|
511
490
|
|
|
512
491
|
interface WorkflowScope {
|
|
513
|
-
agentOptions(opts: AgentOptions
|
|
492
|
+
agentOptions(opts: AgentOptions): AgentOptions;
|
|
514
493
|
phase(title: string): void;
|
|
515
494
|
log(message: string): void;
|
|
516
495
|
event(event: WorkflowProgressEvent): void;
|
|
@@ -523,7 +502,7 @@ function createWorkflowScope(progress: WorkflowProgress, prefix: string, namespa
|
|
|
523
502
|
return {
|
|
524
503
|
agentOptions(opts) {
|
|
525
504
|
const phase = opts?.phase ? display(opts.phase) : currentPhase;
|
|
526
|
-
return
|
|
505
|
+
return { ...opts, phase };
|
|
527
506
|
},
|
|
528
507
|
phase(title) {
|
|
529
508
|
currentPhase = display(title);
|
|
@@ -81,12 +81,12 @@ export function workflowJournalPath(cwd: string, runId: string): string {
|
|
|
81
81
|
return join(workflowRunsDir(cwd), `${validateWorkflowRunId(runId)}.jsonl`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export function agentJournalKey(prompt: string, opts: AgentOptions = {}, worktreeBaseline?: WorktreeBaseline): string {
|
|
84
|
+
export function agentJournalKey(prompt: string, opts: Partial<AgentOptions> = {}, worktreeBaseline?: WorktreeBaseline): string {
|
|
85
85
|
const capture = captureAgentJournalKey(prompt, opts, worktreeBaseline);
|
|
86
86
|
return capture.kind === "verified" ? capture.key : `agent:unverifiable:${randomUUID()}`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export function hashAgentCall(prompt: string, opts: AgentOptions = {}, worktreeBaseline?: WorktreeBaseline): string {
|
|
89
|
+
export function hashAgentCall(prompt: string, opts: Partial<AgentOptions> = {}, worktreeBaseline?: WorktreeBaseline): string {
|
|
90
90
|
const capture = captureAgentCallHash(prompt, opts, worktreeBaseline);
|
|
91
91
|
return capture.kind === "verified"
|
|
92
92
|
? capture.hash
|
|
@@ -96,7 +96,7 @@ export function hashAgentCall(prompt: string, opts: AgentOptions = {}, worktreeB
|
|
|
96
96
|
/** Capture a replay key without allowing hostile or oversized schemas to escape as exceptions. */
|
|
97
97
|
export function captureAgentJournalKey(
|
|
98
98
|
prompt: string,
|
|
99
|
-
opts: AgentOptions = {},
|
|
99
|
+
opts: Partial<AgentOptions> = {},
|
|
100
100
|
worktreeBaseline?: WorktreeBaseline,
|
|
101
101
|
): AgentJournalKeyCapture {
|
|
102
102
|
const behavior = captureAgentCallHash(prompt, opts, worktreeBaseline);
|
|
@@ -118,7 +118,7 @@ type AgentCallHashCapture =
|
|
|
118
118
|
|
|
119
119
|
function captureAgentCallHash(
|
|
120
120
|
prompt: string,
|
|
121
|
-
opts: AgentOptions
|
|
121
|
+
opts: Partial<AgentOptions>,
|
|
122
122
|
worktreeBaseline: WorktreeBaseline | undefined,
|
|
123
123
|
): AgentCallHashCapture {
|
|
124
124
|
try {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { AgentRunnerSession, RunContext } from "./agent-runner-types.ts";
|
|
3
|
+
import { isWorkflowThinkingLevel } from "./agent-options.ts";
|
|
4
|
+
import { resolveAgentModel } from "./agent-session.ts";
|
|
5
|
+
|
|
6
|
+
export interface AgentTranscript {
|
|
7
|
+
readonly cwd?: string;
|
|
8
|
+
readonly messages: readonly AgentMessage[];
|
|
9
|
+
readonly streaming?: AgentMessage;
|
|
10
|
+
readonly steering: readonly string[];
|
|
11
|
+
readonly followUp: readonly string[];
|
|
12
|
+
readonly toolUpdates?: ReadonlyMap<string, { result: AgentToolResult<unknown>; isPartial: boolean; isError: boolean }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Commands are scoped to the child SDK session; parent command handlers never run here. */
|
|
16
|
+
export async function sendAgentInput(session: AgentRunnerSession, rc: RunContext, text: string, steer = false): Promise<void> {
|
|
17
|
+
const [command, ...rest] = text.trim().split(/\s+/);
|
|
18
|
+
const argument = rest.join(" ");
|
|
19
|
+
if (command === "/model") {
|
|
20
|
+
if (!argument || !session.setModel) throw new Error("Usage: /model provider/model");
|
|
21
|
+
const model = resolveAgentModel(argument, rc.modelRegistry, rc.hostModel).model;
|
|
22
|
+
if (!model) throw new Error("Model is unavailable.");
|
|
23
|
+
await session.setModel(model);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (command === "/thinking") {
|
|
27
|
+
if (!isWorkflowThinkingLevel(argument) || !session.setThinkingLevel) {
|
|
28
|
+
throw new Error("Usage: /thinking off|minimal|low|medium|high|xhigh|max");
|
|
29
|
+
}
|
|
30
|
+
session.setThinkingLevel(argument);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (command.startsWith("/")) throw new Error("Supported agent commands: /model provider/model, /thinking level.");
|
|
34
|
+
if (!session.isStreaming) throw new Error("This agent has finished its current turn.");
|
|
35
|
+
if (steer && session.steer) await session.steer(text);
|
|
36
|
+
else await session.followUp(text);
|
|
37
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
6
6
|
import { getAgentDir, type ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { isMissingPathError } from "./filesystem-error.ts";
|
|
8
8
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
9
|
+
import { isWorkflowThinkingLevel } from "./agent-options.ts";
|
|
9
10
|
|
|
10
11
|
export const WORKFLOW_MODEL_PROFILE_NAMES = ["small", "medium", "big"] as const;
|
|
11
12
|
export type WorkflowModelProfileName = (typeof WORKFLOW_MODEL_PROFILE_NAMES)[number];
|
|
@@ -56,8 +57,7 @@ export class WorkflowModelProfileConfigError extends Error {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
export
|
|
60
|
-
const THINKING_LEVELS = new Set<ThinkingLevel>(WORKFLOW_THINKING_LEVELS);
|
|
60
|
+
export { WORKFLOW_THINKING_LEVELS, isWorkflowThinkingLevel } from "./agent-options.ts";
|
|
61
61
|
const HOST_FALLBACK_THINKING: Record<WorkflowModelProfileName, ThinkingLevel> = {
|
|
62
62
|
small: "low",
|
|
63
63
|
medium: "medium",
|
|
@@ -184,10 +184,6 @@ export function isWorkflowModelProfileName(value: string): value is WorkflowMode
|
|
|
184
184
|
return WORKFLOW_MODEL_PROFILE_NAMES.includes(value as WorkflowModelProfileName);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
export function isWorkflowThinkingLevel(value: string): value is ThinkingLevel {
|
|
188
|
-
return THINKING_LEVELS.has(value as ThinkingLevel);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
187
|
function parseWorkflowModelProfileFile(value: unknown, configPath: string): WorkflowModelProfileFile {
|
|
192
188
|
if (!isRecord(value)) throw invalidConfig(configPath, "the root must be an object");
|
|
193
189
|
assertOnlyKeys(value, ["profiles"], configPath);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
2
2
|
|
|
3
|
-
export type AgentRowStatus = "queued" | "running" | "done" | "failed";
|
|
3
|
+
export type AgentRowStatus = "queued" | "running" | "stopping" | "stopped" | "done" | "failed";
|
|
4
4
|
export type AgentChatRole = "task" | "user" | "assistant" | "tool" | "status";
|
|
5
5
|
export type WorkflowLaneItemStatus = "pending" | "running" | "success" | "warning" | "error";
|
|
6
6
|
|
|
@@ -15,6 +15,8 @@ export interface AgentRowSnapshot {
|
|
|
15
15
|
readonly label: string;
|
|
16
16
|
/** provider/id of the model actually routed to this agent. */
|
|
17
17
|
readonly model?: string;
|
|
18
|
+
readonly modelName?: string;
|
|
19
|
+
readonly thinkingLevel?: string;
|
|
18
20
|
readonly status: AgentRowStatus;
|
|
19
21
|
readonly startedAt?: number;
|
|
20
22
|
readonly doneAt?: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
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
5
|
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
@@ -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;
|
|
@@ -73,9 +76,11 @@ 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;
|
|
@@ -161,9 +166,9 @@ export class ProgressTracker {
|
|
|
161
166
|
this.publish();
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
agentQueued(phase: string | undefined, label: string, model?: string): number {
|
|
169
|
+
agentQueued(phase: string | undefined, label: string, model?: string, modelName?: string, thinkingLevel?: string): number {
|
|
165
170
|
const id = this.nextAgentId++;
|
|
166
|
-
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 };
|
|
167
172
|
this.ensurePhase(phase ?? this.currentPhase).agents.push(row);
|
|
168
173
|
this.rowsById.set(id, row);
|
|
169
174
|
this.agentCounts.queued++;
|
|
@@ -208,7 +213,27 @@ export class ProgressTracker {
|
|
|
208
213
|
this.publish();
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
|
|
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 {
|
|
212
237
|
this.agentFollowUps.set(id, send);
|
|
213
238
|
this.publish();
|
|
214
239
|
return () => {
|
|
@@ -217,16 +242,32 @@ export class ProgressTracker {
|
|
|
217
242
|
};
|
|
218
243
|
}
|
|
219
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
|
+
|
|
220
261
|
conversation(id: number): readonly AgentChatMessage[] {
|
|
221
262
|
return (this.agentChats.get(id) ?? []).map((message) => ({ ...message }));
|
|
222
263
|
}
|
|
223
264
|
|
|
224
|
-
async followUp(id: number, message: string): Promise<void> {
|
|
225
|
-
const text =
|
|
265
|
+
async followUp(id: number, message: string, steer = false): Promise<void> {
|
|
266
|
+
const text = message.trim();
|
|
226
267
|
if (!text) throw new Error("Enter a follow-up message.");
|
|
227
268
|
const send = this.agentFollowUps.get(id);
|
|
228
269
|
if (!send) throw new Error("This agent is no longer accepting follow-ups.");
|
|
229
|
-
await send(text);
|
|
270
|
+
await send(text, steer);
|
|
230
271
|
this.appendAgentChat(id, "user", text);
|
|
231
272
|
this.publish();
|
|
232
273
|
}
|
|
@@ -238,7 +279,7 @@ export class ProgressTracker {
|
|
|
238
279
|
|
|
239
280
|
agentDone(label: string, id?: number): void {
|
|
240
281
|
const row = this.findRow(label, id);
|
|
241
|
-
if (row && row.status
|
|
282
|
+
if (row && (row.status === "running" || row.status === "queued")) {
|
|
242
283
|
this.transitionAgentStatus(row, "done");
|
|
243
284
|
row.doneAt = Date.now();
|
|
244
285
|
}
|
|
@@ -248,7 +289,7 @@ export class ProgressTracker {
|
|
|
248
289
|
agentFailed(label: string, error: unknown, id?: number): void {
|
|
249
290
|
const row = this.findRow(label, id);
|
|
250
291
|
if (row) {
|
|
251
|
-
this.transitionAgentStatus(row, "failed");
|
|
292
|
+
this.transitionAgentStatus(row, row.status === "stopping" ? "stopped" : "failed");
|
|
252
293
|
row.doneAt = Date.now();
|
|
253
294
|
row.error = toDisplayLine(unknownErrorMessage(error), AGENT_ERROR_DISPLAY_LIMIT) || "agent failed";
|
|
254
295
|
this.appendAgentChat(row.id, "status", row.error);
|
|
@@ -288,10 +329,10 @@ export class ProgressTracker {
|
|
|
288
329
|
private statusCountsSnapshot(): WorkflowStatusCounts {
|
|
289
330
|
return {
|
|
290
331
|
queued: this.agentCounts.queued,
|
|
291
|
-
running: this.agentCounts.running,
|
|
332
|
+
running: this.agentCounts.running + this.agentCounts.stopping,
|
|
292
333
|
done: this.agentCounts.done,
|
|
293
|
-
failed: this.agentCounts.failed,
|
|
294
|
-
total: this.agentCounts.
|
|
334
|
+
failed: this.agentCounts.failed + this.agentCounts.stopped,
|
|
335
|
+
total: Object.values(this.agentCounts).reduce((sum, count) => sum + count, 0),
|
|
295
336
|
};
|
|
296
337
|
}
|
|
297
338
|
|
|
@@ -371,7 +412,6 @@ export class ProgressTracker {
|
|
|
371
412
|
this.publishSnapshot();
|
|
372
413
|
if (!this.ctx.hasUI) return;
|
|
373
414
|
this.ctx.ui.setWidget(this.surfaceKey, undefined);
|
|
374
|
-
this.ctx.ui.setStatus(this.surfaceKey, undefined);
|
|
375
415
|
}
|
|
376
416
|
|
|
377
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
|
}
|