@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,6 +1,6 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
3
|
-
import { isWorkflowPauseError } from "./cancellation.ts";
|
|
3
|
+
import { isWorkflowPauseError, WorkflowAgentStoppedError } from "./cancellation.ts";
|
|
4
4
|
import type { WorkflowAgentReservation } from "./agent-limits.ts";
|
|
5
5
|
import {
|
|
6
6
|
type AgentExecutionOptions,
|
|
@@ -132,7 +132,7 @@ export async function executeAgentAttempt(input: {
|
|
|
132
132
|
tags,
|
|
133
133
|
});
|
|
134
134
|
const result = await workspace.wrapResult(rawResult);
|
|
135
|
-
if (!identity) return { kind: "live-unrecordable", result };
|
|
135
|
+
if (!identity || handle.interacted) return { kind: "live-unrecordable", result };
|
|
136
136
|
if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
|
|
137
137
|
|
|
138
138
|
const contract = await validateReplayIdentity({
|
|
@@ -156,7 +156,7 @@ export async function executeAgentAttempt(input: {
|
|
|
156
156
|
if (
|
|
157
157
|
workspace?.kind === "isolated"
|
|
158
158
|
&& liveStarted
|
|
159
|
-
&& !rc.signal?.aborted
|
|
159
|
+
&& (!rc.signal?.aborted || rc.signal.reason instanceof WorkflowAgentStoppedError)
|
|
160
160
|
&& !isWorkflowPauseError(error)
|
|
161
161
|
) {
|
|
162
162
|
rc.worktrees.preserve(workspace.cwd);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { AgentOptions } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const WORKFLOW_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
|
|
5
|
+
|
|
6
|
+
export function isWorkflowThinkingLevel(value: string): value is ThinkingLevel {
|
|
7
|
+
return (WORKFLOW_THINKING_LEVELS as readonly string[]).includes(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Validate before admission or any model session can be created. */
|
|
11
|
+
export function assertAgentOptions(options: unknown): asserts options is AgentOptions {
|
|
12
|
+
const opts = options as Partial<AgentOptions> | undefined;
|
|
13
|
+
if (!opts || typeof opts.label !== "string" || !opts.label.trim()
|
|
14
|
+
|| typeof opts.model !== "string" || !opts.model.trim()
|
|
15
|
+
|| typeof opts.thinkingLevel !== "string" || !isWorkflowThinkingLevel(opts.thinkingLevel)) {
|
|
16
|
+
throw new Error("Every agent() requires explicit label, model, and thinkingLevel.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -13,6 +13,7 @@ import type { WorkflowJournal } from "./journal.ts";
|
|
|
13
13
|
import type { PerfSink } from "./perf.ts";
|
|
14
14
|
import type { AgentOptions, WorkflowProgressEvent } from "./types.ts";
|
|
15
15
|
import type { AgentChatRole } from "./progress-types.ts";
|
|
16
|
+
import type { AgentTranscript } from "./live-agent.ts";
|
|
16
17
|
import type { WorkflowUsageSink } from "./usage.ts";
|
|
17
18
|
import type { WorktreeBaseline, WorktreeRegistry } from "./worktree.ts";
|
|
18
19
|
|
|
@@ -34,16 +35,19 @@ export type AgentRunnerSession = Pick<
|
|
|
34
35
|
| "getLastAssistantText"
|
|
35
36
|
| "isStreaming"
|
|
36
37
|
| "followUp"
|
|
37
|
-
|
|
38
|
+
> & Partial<Pick<AgentSession, "setModel" | "setThinkingLevel" | "steer" | "getSteeringMessages" | "getFollowUpMessages">>;
|
|
38
39
|
|
|
39
40
|
export type CreateAgentSession = (options: CreateAgentSessionOptions) => Promise<{ session: AgentRunnerSession }>;
|
|
40
41
|
|
|
41
42
|
export interface AgentProgress {
|
|
42
|
-
agentQueued(phase: string | undefined, label: string, model?: string): number;
|
|
43
|
+
agentQueued(phase: string | undefined, label: string, model?: string, modelName?: string, thinkingLevel?: string): number;
|
|
44
|
+
bindAgentStop?(id: number, stop: () => void): () => void;
|
|
43
45
|
agentStart(phase: string | undefined, label: string, id?: number, model?: string): void;
|
|
44
46
|
agentTool(label: string, tool: string, id?: number): void;
|
|
45
47
|
agentMessage(id: number, role: AgentChatRole, text: string): void;
|
|
46
|
-
bindAgentFollowUp(id: number, send: (message: string) => Promise<void>): () => void;
|
|
48
|
+
bindAgentFollowUp(id: number, send: (message: string, steer?: boolean) => Promise<void>): () => void;
|
|
49
|
+
bindAgentTranscript?(id: number, read: () => AgentTranscript): () => void;
|
|
50
|
+
agentChanged?(id: number, model?: string, modelName?: string, thinkingLevel?: string): void;
|
|
47
51
|
agentDone(label: string, id?: number): void;
|
|
48
52
|
agentFailed(label: string, error: unknown, id?: number): void;
|
|
49
53
|
event(event: WorkflowProgressEvent): void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { assertAgentOptions } from "./agent-options.ts";
|
|
1
2
|
import { assertWorkflowBudgetAvailable } from "./budget.ts";
|
|
2
3
|
import { combinedAgentAttemptError } from "./agent-failure.ts";
|
|
3
4
|
import { WorkflowAgentTimeoutError } from "./agent-limits.ts";
|
|
4
|
-
import { abortReason, linkAbortSignal, throwIfAborted } from "./cancellation.ts";
|
|
5
|
+
import { abortReason, linkAbortSignal, throwIfAborted, WorkflowAgentStoppedError } from "./cancellation.ts";
|
|
5
6
|
import { executeAgentAttempt } from "./agent-attempt.ts";
|
|
6
7
|
import {
|
|
7
8
|
createAgentReplayPlan,
|
|
@@ -53,7 +54,8 @@ export async function runAgent(
|
|
|
53
54
|
throw new Error(`agent() prompt must be a string; received ${describeAgentPrompt(prompt)}`);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
assertAgentOptions(opts);
|
|
58
|
+
const label = opts.label;
|
|
57
59
|
const phase = opts.phase ?? "Workflow";
|
|
58
60
|
const tags: AgentRunTags = { label, phase };
|
|
59
61
|
|
|
@@ -62,14 +64,15 @@ export async function runAgent(
|
|
|
62
64
|
const routing = resolveAgentRouting(rc, opts, label);
|
|
63
65
|
const effectiveOpts = routing.thinkingLevel === opts.thinkingLevel
|
|
64
66
|
? opts
|
|
65
|
-
: { ...opts, thinkingLevel: routing.thinkingLevel };
|
|
67
|
+
: { ...opts, thinkingLevel: routing.thinkingLevel ?? opts.thinkingLevel };
|
|
66
68
|
const replay = createAgentReplayPlan(prompt, effectiveOpts);
|
|
67
69
|
if (!isReplayEnabled(replay)) assertWorkflowBudgetAvailable(rc.budget);
|
|
68
70
|
|
|
69
71
|
const modelLabel = describeAgentModel(routing.model);
|
|
70
|
-
const rowId = rc.progress.agentQueued(opts.phase, label, modelLabel);
|
|
72
|
+
const rowId = rc.progress.agentQueued(opts.phase, label, modelLabel, routing.model?.name, effectiveOpts.thinkingLevel);
|
|
71
73
|
rc.progress.agentMessage(rowId, "task", prompt);
|
|
72
74
|
const liveScope = createAgentLiveScope(rc, label);
|
|
75
|
+
const unbindStop = rc.progress.bindAgentStop?.(rowId, liveScope.stop);
|
|
73
76
|
try {
|
|
74
77
|
return await rc.semaphore.run(
|
|
75
78
|
async () => {
|
|
@@ -129,18 +132,20 @@ export async function runAgent(
|
|
|
129
132
|
}
|
|
130
133
|
continue;
|
|
131
134
|
}
|
|
135
|
+
throwIfAborted(agentRc.signal);
|
|
132
136
|
const settlement = await settleAgentAttempt({ rc: agentRc, label, tags, replay: attemptPlan, outcome });
|
|
133
137
|
if (settlement.kind === "retry-live") {
|
|
134
138
|
attemptPlan = { kind: "off" };
|
|
135
139
|
continue;
|
|
136
140
|
}
|
|
141
|
+
throwIfAborted(agentRc.signal);
|
|
137
142
|
rc.progress.agentDone(label, rowId);
|
|
138
143
|
return settlement.result;
|
|
139
144
|
}
|
|
140
145
|
},
|
|
141
146
|
{
|
|
142
147
|
onQueueWaitMs: (durationMs) => rc.perf.observe("agent.queue_wait_ms", durationMs, tags),
|
|
143
|
-
signal:
|
|
148
|
+
signal: liveScope.signal,
|
|
144
149
|
},
|
|
145
150
|
);
|
|
146
151
|
} catch (error) {
|
|
@@ -151,6 +156,7 @@ export async function runAgent(
|
|
|
151
156
|
rc.progress.log(`${label} failed: ${unknownErrorMessage(failure)}`);
|
|
152
157
|
throw failure;
|
|
153
158
|
} finally {
|
|
159
|
+
unbindStop?.();
|
|
154
160
|
liveScope.dispose();
|
|
155
161
|
}
|
|
156
162
|
}, tags);
|
|
@@ -164,6 +170,8 @@ function createAgentLiveScope(rc: RunContext, label: string) {
|
|
|
164
170
|
|
|
165
171
|
return {
|
|
166
172
|
signal: controller.signal,
|
|
173
|
+
// A local stop is recoverable by parallel(); it must not abort sibling agents.
|
|
174
|
+
stop: () => controller.abort(new WorkflowAgentStoppedError(`Agent ${label} stopped by user.`)),
|
|
167
175
|
reserve() {
|
|
168
176
|
const reservation = rc.agentLimiter.reserve(controller.signal);
|
|
169
177
|
let committed = false;
|
|
@@ -194,7 +202,7 @@ function resolveAgentRouting(
|
|
|
194
202
|
rc: RunContext,
|
|
195
203
|
opts: AgentExecutionOptions,
|
|
196
204
|
label: string,
|
|
197
|
-
): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] } {
|
|
205
|
+
): { readonly model: ResolvedAgentModel["model"]; readonly thinkingLevel: AgentExecutionOptions["thinkingLevel"] | undefined } {
|
|
198
206
|
try {
|
|
199
207
|
return resolveAgentModelProfile(
|
|
200
208
|
{
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { sendAgentInput, type AgentTranscript } from "./live-agent.ts";
|
|
1
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
4
|
import {
|
|
3
5
|
createAgentSessionFromServices,
|
|
@@ -45,7 +47,9 @@ export interface ResolvedAgentModel {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface AgentSessionHandle {
|
|
50
|
+
interacted?: boolean;
|
|
48
51
|
readonly session: AgentRunnerSession;
|
|
52
|
+
readonly cwd?: string;
|
|
49
53
|
readonly selectedSkills: readonly Skill[];
|
|
50
54
|
hasStructuredResult(): boolean;
|
|
51
55
|
structuredResult(): unknown;
|
|
@@ -124,6 +128,7 @@ export async function openAgentSession(input: {
|
|
|
124
128
|
throwIfAborted(rc.signal);
|
|
125
129
|
return {
|
|
126
130
|
session,
|
|
131
|
+
cwd,
|
|
127
132
|
selectedSkills: resources.selectedSkills,
|
|
128
133
|
hasStructuredResult: () => captured,
|
|
129
134
|
structuredResult: () => structuredResult,
|
|
@@ -149,13 +154,38 @@ export async function promptAgentSession(input: {
|
|
|
149
154
|
}): Promise<unknown> {
|
|
150
155
|
const { rc, handle, prompt, opts, label, rowId, tags } = input;
|
|
151
156
|
const { session } = handle;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
let streaming: AgentMessage | undefined;
|
|
158
|
+
const toolUpdates = new Map<string, NonNullable<AgentTranscript["toolUpdates"]> extends ReadonlyMap<string, infer V> ? V : never>();
|
|
159
|
+
const refresh = () => rc.progress.agentChanged?.(rowId,
|
|
160
|
+
session.model ? `${session.model.provider}/${session.model.id}` : undefined,
|
|
161
|
+
session.model?.name, session.thinkingLevel);
|
|
162
|
+
const unbindTranscript = rc.progress.bindAgentTranscript?.(rowId, () => ({
|
|
163
|
+
messages: session.messages, streaming, cwd: handle.cwd, toolUpdates,
|
|
164
|
+
steering: session.getSteeringMessages?.() ?? [],
|
|
165
|
+
followUp: session.getFollowUpMessages?.() ?? [],
|
|
166
|
+
}));
|
|
167
|
+
const unbindFollowUp = rc.progress.bindAgentFollowUp(rowId, async (message, steer) => {
|
|
168
|
+
await sendAgentInput(session, rc, message, steer);
|
|
169
|
+
handle.interacted = true;
|
|
170
|
+
refresh();
|
|
155
171
|
});
|
|
156
172
|
const unsubscribe = session.subscribe((event) => {
|
|
173
|
+
if (event.type === "message_update" || event.type === "message_start") streaming = event.message;
|
|
174
|
+
if (event.type === "message_end") streaming = undefined;
|
|
175
|
+
if (event.type === "tool_execution_update") {
|
|
176
|
+
toolUpdates.set(event.toolCallId, { result: event.partialResult, isPartial: true, isError: false });
|
|
177
|
+
} else if (event.type === "tool_execution_end") {
|
|
178
|
+
toolUpdates.set(event.toolCallId, { result: event.result, isPartial: false, isError: event.isError });
|
|
179
|
+
}
|
|
180
|
+
refresh();
|
|
181
|
+
if (event.type === "tool_execution_end") {
|
|
182
|
+
const output = event.result.content.filter((part: { type: string }) => part.type === "text")
|
|
183
|
+
.map((part: { text?: string }) => part.text ?? "").join("\n");
|
|
184
|
+
rc.progress.agentMessage(rowId, "tool", `${event.toolName}: ${output}`);
|
|
185
|
+
}
|
|
157
186
|
if (event.type === "tool_execution_start" && event.toolName !== undefined && event.toolName !== FINAL_TOOL) {
|
|
158
187
|
rc.progress.agentTool(label, event.toolName, rowId);
|
|
188
|
+
rc.progress.agentMessage(rowId, "tool", `${event.toolName} ${JSON.stringify(event.args)}`);
|
|
159
189
|
return;
|
|
160
190
|
}
|
|
161
191
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
@@ -217,6 +247,7 @@ export async function promptAgentSession(input: {
|
|
|
217
247
|
unlinkPromptAbort();
|
|
218
248
|
}
|
|
219
249
|
} finally {
|
|
250
|
+
unbindTranscript?.();
|
|
220
251
|
unbindFollowUp();
|
|
221
252
|
unsubscribe();
|
|
222
253
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Agent-local cancellation is recoverable and must not stop sibling tasks. */
|
|
2
|
+
export class WorkflowAgentStoppedError extends Error {
|
|
3
|
+
override readonly name = "WorkflowAgentStoppedError";
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
export class WorkflowAbortError extends Error {
|
|
2
7
|
constructor(message = "Workflow aborted") {
|
|
3
8
|
super(message);
|
|
@@ -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;
|