@tt-a1i/openpi 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import { mkdir, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type AgentSession,
|
|
5
|
+
type AgentSessionEvent,
|
|
6
|
+
type AgentSessionRuntime,
|
|
7
|
+
type CreateAgentSessionRuntimeFactory,
|
|
8
|
+
ProjectTrustStore,
|
|
9
|
+
SessionManager,
|
|
10
|
+
SettingsManager,
|
|
11
|
+
createAgentSessionFromServices,
|
|
12
|
+
createAgentSessionRuntime,
|
|
13
|
+
createAgentSessionServices,
|
|
14
|
+
getAgentDir,
|
|
15
|
+
hasTrustRequiringProjectResources,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
type WebModelSelectionOptions,
|
|
19
|
+
type WebPromptOptions,
|
|
20
|
+
type WebRuntimeController,
|
|
21
|
+
type WebRuntimeEvent,
|
|
22
|
+
type WebSessionCreationOptions,
|
|
23
|
+
WebRuntimeRequestError,
|
|
24
|
+
} from "./types.ts";
|
|
25
|
+
import { projectMessage } from "../protocol/types.ts";
|
|
26
|
+
import { elapsed, traceWeb } from "../trace.ts";
|
|
27
|
+
import {
|
|
28
|
+
applyHttpProxySettings,
|
|
29
|
+
configureHttpDispatcher,
|
|
30
|
+
type HttpDispatcherLease,
|
|
31
|
+
} from "../http-dispatcher.ts";
|
|
32
|
+
import {
|
|
33
|
+
acquireWebHostLease,
|
|
34
|
+
type WebHostLease,
|
|
35
|
+
} from "./web-host-lease.ts";
|
|
36
|
+
|
|
37
|
+
const STARTUP_TIMEOUT_MS = 15_000;
|
|
38
|
+
const BOOTSTRAP_WORKSPACE_DIRECTORY = ".bootstrap-workspace";
|
|
39
|
+
|
|
40
|
+
type PromptTrace = {
|
|
41
|
+
commandId: string;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
startedAt: number;
|
|
44
|
+
started: boolean;
|
|
45
|
+
queued: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function errorText(error: unknown) {
|
|
49
|
+
return error instanceof Error ? error.message : String(error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function canonicalDirectory(path: string) {
|
|
53
|
+
const canonical = await realpath(resolve(path));
|
|
54
|
+
if (!(await stat(canonical)).isDirectory()) {
|
|
55
|
+
throw new Error("Workspace path is not a directory");
|
|
56
|
+
}
|
|
57
|
+
return canonical;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class PiWebRuntime implements WebRuntimeController {
|
|
61
|
+
private runtime: AgentSessionRuntime;
|
|
62
|
+
private unsubscribeSession?: () => void;
|
|
63
|
+
private readonly listeners = new Set<(event: WebRuntimeEvent) => void>();
|
|
64
|
+
private readonly retainedRuntimes = new Set<AgentSessionRuntime>();
|
|
65
|
+
private readonly retainedSubscriptions = new Map<AgentSessionRuntime, () => void>();
|
|
66
|
+
private readonly inFlightRuntimes = new Map<AgentSessionRuntime, number>();
|
|
67
|
+
private readonly promptOperations = new Set<Promise<void>>();
|
|
68
|
+
private readonly runtimeOperations = new Set<Promise<void>>();
|
|
69
|
+
private readonly candidateRuntimes = new Set<AgentSessionRuntime>();
|
|
70
|
+
private readonly runtimeDisposals = new Set<Promise<void>>();
|
|
71
|
+
private runtimeDisposalFailure?: unknown;
|
|
72
|
+
private readonly runtimeDisposalPromises = new WeakMap<
|
|
73
|
+
AgentSessionRuntime,
|
|
74
|
+
Promise<void>
|
|
75
|
+
>();
|
|
76
|
+
private controllerMutation: Promise<void> = Promise.resolve();
|
|
77
|
+
private promptAdmission: Promise<void> = Promise.resolve();
|
|
78
|
+
private activePromptTrace?: PromptTrace;
|
|
79
|
+
private readonly pendingPromptTraces: PromptTrace[] = [];
|
|
80
|
+
private liveMessageKey?: string;
|
|
81
|
+
private liveMessageSequence = 0;
|
|
82
|
+
private readonly webSessionDirectory: string;
|
|
83
|
+
private readonly dispatcherLease: HttpDispatcherLease;
|
|
84
|
+
private readonly webHostLease: WebHostLease;
|
|
85
|
+
private disposed = false;
|
|
86
|
+
private disposePromise?: Promise<void>;
|
|
87
|
+
private hasSelectedWorkspace: boolean;
|
|
88
|
+
|
|
89
|
+
private constructor(
|
|
90
|
+
runtime: AgentSessionRuntime,
|
|
91
|
+
webSessionDirectory: string,
|
|
92
|
+
dispatcherLease: HttpDispatcherLease,
|
|
93
|
+
webHostLease: WebHostLease,
|
|
94
|
+
workspaceSelected: boolean,
|
|
95
|
+
) {
|
|
96
|
+
this.runtime = runtime;
|
|
97
|
+
this.webSessionDirectory = webSessionDirectory;
|
|
98
|
+
this.dispatcherLease = dispatcherLease;
|
|
99
|
+
this.webHostLease = webHostLease;
|
|
100
|
+
this.hasSelectedWorkspace = workspaceSelected;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static async create(cwd: string) {
|
|
104
|
+
const canonicalCwd = await canonicalDirectory(cwd);
|
|
105
|
+
return PiWebRuntime.createForWorkspace(canonicalCwd, true);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static async createWithoutWorkspace() {
|
|
109
|
+
const webSessionDirectory = join(getAgentDir(), "web-sessions");
|
|
110
|
+
await mkdir(webSessionDirectory, { recursive: true, mode: 0o700 });
|
|
111
|
+
const bootstrapDirectory = join(
|
|
112
|
+
webSessionDirectory,
|
|
113
|
+
BOOTSTRAP_WORKSPACE_DIRECTORY,
|
|
114
|
+
);
|
|
115
|
+
await mkdir(bootstrapDirectory, { recursive: true, mode: 0o700 });
|
|
116
|
+
const canonicalCwd = await canonicalDirectory(bootstrapDirectory);
|
|
117
|
+
return PiWebRuntime.createForWorkspace(canonicalCwd, false);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private static async createForWorkspace(
|
|
121
|
+
canonicalCwd: string,
|
|
122
|
+
workspaceSelected: boolean,
|
|
123
|
+
) {
|
|
124
|
+
const webSessionDirectory = join(getAgentDir(), "web-sessions");
|
|
125
|
+
const webHostLease = await acquireWebHostLease(webSessionDirectory);
|
|
126
|
+
let runtime: PiWebRuntime | undefined;
|
|
127
|
+
try {
|
|
128
|
+
const created = await PiWebRuntime.createRuntime(
|
|
129
|
+
canonicalCwd,
|
|
130
|
+
workspaceSelected
|
|
131
|
+
? SessionManager.create(canonicalCwd, webSessionDirectory)
|
|
132
|
+
: SessionManager.inMemory(canonicalCwd),
|
|
133
|
+
);
|
|
134
|
+
runtime = new PiWebRuntime(
|
|
135
|
+
created.runtime,
|
|
136
|
+
webSessionDirectory,
|
|
137
|
+
created.dispatcherLease,
|
|
138
|
+
webHostLease,
|
|
139
|
+
workspaceSelected,
|
|
140
|
+
);
|
|
141
|
+
if (workspaceSelected) await runtime.startRuntimeSession();
|
|
142
|
+
return runtime;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
try {
|
|
145
|
+
if (runtime) await runtime.dispose();
|
|
146
|
+
else await webHostLease.release();
|
|
147
|
+
} catch (cleanupError) {
|
|
148
|
+
throw new AggregateError(
|
|
149
|
+
[error, cleanupError],
|
|
150
|
+
"Failed to start and clean up the Web runtime",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get cwd() {
|
|
158
|
+
return this.runtime.cwd;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get workspaceSelected() {
|
|
162
|
+
return this.hasSelectedWorkspace;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get sessionDirectory() {
|
|
166
|
+
return this.webSessionDirectory;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get sessionManager() {
|
|
170
|
+
return this.runtime.session.sessionManager;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
isIdle() {
|
|
174
|
+
return !this.runtime.session.isStreaming;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
listModels() {
|
|
178
|
+
const current = this.runtime.session.model;
|
|
179
|
+
const available = [...this.runtime.services.modelRuntime.getAvailableSnapshot()];
|
|
180
|
+
if (
|
|
181
|
+
current &&
|
|
182
|
+
!available.some(
|
|
183
|
+
(model) => model.provider === current.provider && model.id === current.id,
|
|
184
|
+
)
|
|
185
|
+
) {
|
|
186
|
+
available.unshift(current);
|
|
187
|
+
}
|
|
188
|
+
return available.map((model) => ({
|
|
189
|
+
provider: model.provider,
|
|
190
|
+
id: model.id,
|
|
191
|
+
name: model.name,
|
|
192
|
+
label: model.name || `${model.provider}/${model.id}`,
|
|
193
|
+
current: current?.provider === model.provider && current.id === model.id,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setModel(
|
|
198
|
+
provider: string,
|
|
199
|
+
modelId: string,
|
|
200
|
+
options?: WebModelSelectionOptions,
|
|
201
|
+
) {
|
|
202
|
+
return this.serializeControllerMutation(() =>
|
|
203
|
+
this.applyModelSelection(provider, modelId, options),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async applyModelSelection(
|
|
208
|
+
provider: string,
|
|
209
|
+
modelId: string,
|
|
210
|
+
options?: WebModelSelectionOptions,
|
|
211
|
+
) {
|
|
212
|
+
this.assertActive();
|
|
213
|
+
this.assertWorkspaceSelected();
|
|
214
|
+
const agentRuntime = this.runtime;
|
|
215
|
+
if (
|
|
216
|
+
options?.expectedSessionId !== undefined &&
|
|
217
|
+
options.expectedSessionId !==
|
|
218
|
+
agentRuntime.session.sessionManager.getSessionId()
|
|
219
|
+
) {
|
|
220
|
+
throw new WebRuntimeRequestError(
|
|
221
|
+
"Only the active Web session accepts model selection",
|
|
222
|
+
"SESSION_CONFLICT",
|
|
223
|
+
409,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const { modelRuntime } = agentRuntime.services;
|
|
227
|
+
const model = modelRuntime.getModel(provider, modelId);
|
|
228
|
+
if (
|
|
229
|
+
!model ||
|
|
230
|
+
!modelRuntime
|
|
231
|
+
.getAvailableSnapshot()
|
|
232
|
+
.some((item) => item.provider === provider && item.id === modelId)
|
|
233
|
+
) {
|
|
234
|
+
throw new WebRuntimeRequestError(
|
|
235
|
+
"Model is not available",
|
|
236
|
+
"MODEL_NOT_AVAILABLE",
|
|
237
|
+
400,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
this.retainRuntimeReference(agentRuntime);
|
|
241
|
+
try {
|
|
242
|
+
await agentRuntime.session.setModel(model);
|
|
243
|
+
const current = agentRuntime.session.model;
|
|
244
|
+
const selected = modelRuntime
|
|
245
|
+
.getAvailableSnapshot()
|
|
246
|
+
.map((item) => ({
|
|
247
|
+
provider: item.provider,
|
|
248
|
+
id: item.id,
|
|
249
|
+
name: item.name,
|
|
250
|
+
label: item.name || `${item.provider}/${item.id}`,
|
|
251
|
+
current:
|
|
252
|
+
current?.provider === item.provider && current.id === item.id,
|
|
253
|
+
}))
|
|
254
|
+
.find((item) => item.current);
|
|
255
|
+
if (!selected) throw new Error("Model selection was not confirmed");
|
|
256
|
+
this.emit("model_select", { provider, modelId });
|
|
257
|
+
return selected;
|
|
258
|
+
} finally {
|
|
259
|
+
this.releaseRuntimeReference(agentRuntime);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
subscribe(listener: (event: WebRuntimeEvent) => void) {
|
|
264
|
+
this.listeners.add(listener);
|
|
265
|
+
return () => this.listeners.delete(listener);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async sendPrompt(content: string, options?: WebPromptOptions) {
|
|
269
|
+
this.assertActive();
|
|
270
|
+
this.assertWorkspaceSelected();
|
|
271
|
+
const agentRuntime = this.runtime;
|
|
272
|
+
const session = agentRuntime.session;
|
|
273
|
+
const sessionId = session.sessionManager.getSessionId();
|
|
274
|
+
if (
|
|
275
|
+
options?.expectedSessionId !== undefined &&
|
|
276
|
+
options.expectedSessionId !== sessionId
|
|
277
|
+
) {
|
|
278
|
+
throw new WebRuntimeRequestError(
|
|
279
|
+
"Only the active Web session accepts messages",
|
|
280
|
+
"SESSION_CONFLICT",
|
|
281
|
+
409,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const previousAdmission = this.promptAdmission;
|
|
285
|
+
let releaseAdmission: () => void = () => undefined;
|
|
286
|
+
this.promptAdmission = new Promise<void>((resolveAdmission) => {
|
|
287
|
+
releaseAdmission = resolveAdmission;
|
|
288
|
+
});
|
|
289
|
+
const startedAt = performance.now();
|
|
290
|
+
const queued = session.isStreaming;
|
|
291
|
+
const promptTrace: PromptTrace | undefined = options?.commandId
|
|
292
|
+
? {
|
|
293
|
+
commandId: options.commandId,
|
|
294
|
+
sessionId,
|
|
295
|
+
startedAt,
|
|
296
|
+
started: false,
|
|
297
|
+
queued,
|
|
298
|
+
}
|
|
299
|
+
: undefined;
|
|
300
|
+
this.retainRuntimeReference(agentRuntime);
|
|
301
|
+
let resolveRequest: () => void = () => undefined;
|
|
302
|
+
let rejectRequest: (error: unknown) => void = () => undefined;
|
|
303
|
+
const requestAdmission = new Promise<void>((resolveRequestAdmission, reject) => {
|
|
304
|
+
resolveRequest = resolveRequestAdmission;
|
|
305
|
+
rejectRequest = reject;
|
|
306
|
+
});
|
|
307
|
+
const operation = (async () => {
|
|
308
|
+
let preflightObserved = false;
|
|
309
|
+
let admitted = false;
|
|
310
|
+
let agentLifecycleStarted = false;
|
|
311
|
+
let queuedForAgent = false;
|
|
312
|
+
let unsubscribePromptLifecycle: (() => void) | undefined;
|
|
313
|
+
try {
|
|
314
|
+
await previousAdmission;
|
|
315
|
+
this.assertActive();
|
|
316
|
+
if (promptTrace && agentRuntime === this.runtime) {
|
|
317
|
+
this.pendingPromptTraces.push(promptTrace);
|
|
318
|
+
this.activePromptTrace ??= this.pendingPromptTraces.shift();
|
|
319
|
+
}
|
|
320
|
+
if (promptTrace) {
|
|
321
|
+
traceWeb("prompt_dispatch_started", {
|
|
322
|
+
commandId: promptTrace.commandId,
|
|
323
|
+
sessionId,
|
|
324
|
+
chars: content.length,
|
|
325
|
+
provider: session.model?.provider,
|
|
326
|
+
modelId: session.model?.id,
|
|
327
|
+
});
|
|
328
|
+
traceWeb("prompt_preflight_started", {
|
|
329
|
+
commandId: promptTrace.commandId,
|
|
330
|
+
sessionId,
|
|
331
|
+
elapsedMs: elapsed(startedAt),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
const pendingMessagesBefore = session.pendingMessageCount;
|
|
335
|
+
unsubscribePromptLifecycle = session.subscribe((event) => {
|
|
336
|
+
if (event.type === "agent_start") agentLifecycleStarted = true;
|
|
337
|
+
if (
|
|
338
|
+
event.type === "queue_update" &&
|
|
339
|
+
event.steering.length + event.followUp.length > pendingMessagesBefore
|
|
340
|
+
) {
|
|
341
|
+
queuedForAgent = true;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
await session.prompt(content, {
|
|
345
|
+
...(session.isStreaming
|
|
346
|
+
? { streamingBehavior: "followUp" as const }
|
|
347
|
+
: {}),
|
|
348
|
+
source: "rpc",
|
|
349
|
+
preflightResult: (accepted) => {
|
|
350
|
+
preflightObserved = true;
|
|
351
|
+
admitted = accepted;
|
|
352
|
+
releaseAdmission();
|
|
353
|
+
if (promptTrace) {
|
|
354
|
+
traceWeb(
|
|
355
|
+
accepted
|
|
356
|
+
? "prompt_preflight_accepted"
|
|
357
|
+
: "prompt_preflight_rejected",
|
|
358
|
+
{
|
|
359
|
+
commandId: promptTrace.commandId,
|
|
360
|
+
sessionId,
|
|
361
|
+
elapsedMs: elapsed(startedAt),
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (accepted) {
|
|
366
|
+
resolveRequest();
|
|
367
|
+
} else {
|
|
368
|
+
rejectRequest(
|
|
369
|
+
new WebRuntimeRequestError(
|
|
370
|
+
"Prompt was rejected before admission",
|
|
371
|
+
"PROMPT_REJECTED",
|
|
372
|
+
422,
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
unsubscribePromptLifecycle();
|
|
379
|
+
unsubscribePromptLifecycle = undefined;
|
|
380
|
+
if (!preflightObserved || !admitted) {
|
|
381
|
+
rejectRequest(
|
|
382
|
+
new WebRuntimeRequestError(
|
|
383
|
+
preflightObserved
|
|
384
|
+
? "Prompt was rejected before admission"
|
|
385
|
+
: "Pi completed the prompt without confirming admission",
|
|
386
|
+
"PROMPT_REJECTED",
|
|
387
|
+
422,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
admitted &&
|
|
393
|
+
options?.commandId &&
|
|
394
|
+
!agentLifecycleStarted &&
|
|
395
|
+
!queuedForAgent
|
|
396
|
+
) {
|
|
397
|
+
this.emit("prompt_settled", {
|
|
398
|
+
commandId: options.commandId,
|
|
399
|
+
sessionId,
|
|
400
|
+
outcome: "handled",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (promptTrace) {
|
|
404
|
+
traceWeb("prompt_operation_settled", {
|
|
405
|
+
commandId: promptTrace.commandId,
|
|
406
|
+
sessionId,
|
|
407
|
+
elapsedMs: elapsed(startedAt),
|
|
408
|
+
});
|
|
409
|
+
promptTrace.started =
|
|
410
|
+
this.activePromptTrace?.commandId === promptTrace.commandId
|
|
411
|
+
? this.activePromptTrace.started
|
|
412
|
+
: promptTrace.started;
|
|
413
|
+
if (!promptTrace.queued && !promptTrace.started) {
|
|
414
|
+
this.removePromptTrace(promptTrace);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
releaseAdmission();
|
|
419
|
+
if (!admitted) {
|
|
420
|
+
rejectRequest(
|
|
421
|
+
new WebRuntimeRequestError(
|
|
422
|
+
errorText(error),
|
|
423
|
+
"PROMPT_REJECTED",
|
|
424
|
+
422,
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
} else if (admitted) {
|
|
428
|
+
this.emit("prompt_failed", {
|
|
429
|
+
...(options?.commandId ? { commandId: options.commandId } : {}),
|
|
430
|
+
sessionId,
|
|
431
|
+
error: errorText(error),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (promptTrace) {
|
|
435
|
+
traceWeb("prompt_operation_failed", {
|
|
436
|
+
commandId: promptTrace.commandId,
|
|
437
|
+
sessionId,
|
|
438
|
+
elapsedMs: elapsed(startedAt),
|
|
439
|
+
error: errorText(error),
|
|
440
|
+
});
|
|
441
|
+
this.removePromptTrace(promptTrace);
|
|
442
|
+
}
|
|
443
|
+
} finally {
|
|
444
|
+
unsubscribePromptLifecycle?.();
|
|
445
|
+
releaseAdmission();
|
|
446
|
+
this.releaseRuntimeReference(agentRuntime);
|
|
447
|
+
}
|
|
448
|
+
})();
|
|
449
|
+
this.promptOperations.add(operation);
|
|
450
|
+
void operation.then(
|
|
451
|
+
() => this.promptOperations.delete(operation),
|
|
452
|
+
() => this.promptOperations.delete(operation),
|
|
453
|
+
);
|
|
454
|
+
await requestAdmission;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
newSession(workspacePath: string, options?: WebSessionCreationOptions) {
|
|
458
|
+
return this.serializeControllerMutation(() =>
|
|
459
|
+
this.createNewSession(workspacePath, options),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private async createNewSession(
|
|
464
|
+
workspacePath: string,
|
|
465
|
+
options?: WebSessionCreationOptions,
|
|
466
|
+
) {
|
|
467
|
+
const cwd = await canonicalDirectory(workspacePath);
|
|
468
|
+
this.assertActive();
|
|
469
|
+
const replacement = await PiWebRuntime.createRuntime(
|
|
470
|
+
cwd,
|
|
471
|
+
SessionManager.create(cwd, this.webSessionDirectory),
|
|
472
|
+
this.dispatcherLease,
|
|
473
|
+
);
|
|
474
|
+
await this.activateCandidate(replacement.runtime);
|
|
475
|
+
this.hasSelectedWorkspace = true;
|
|
476
|
+
const sessionPath = this.runtime.session.sessionManager.getSessionFile();
|
|
477
|
+
this.emit("session_switched", {
|
|
478
|
+
...(options?.commandId ? { commandId: options.commandId } : {}),
|
|
479
|
+
...(sessionPath ? { sessionPath } : {}),
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
cancelled: false,
|
|
483
|
+
...(options?.commandId ? { commandId: options.commandId } : {}),
|
|
484
|
+
...(sessionPath ? { sessionPath } : {}),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
switchSession(sessionPath: string) {
|
|
489
|
+
return this.serializeControllerMutation(() =>
|
|
490
|
+
this.switchActiveSession(sessionPath),
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private async switchActiveSession(sessionPath: string) {
|
|
495
|
+
if (this.runtime.session.sessionManager.getSessionFile() === sessionPath) {
|
|
496
|
+
return { cancelled: false };
|
|
497
|
+
}
|
|
498
|
+
const retained = [...this.retainedRuntimes].find(
|
|
499
|
+
(candidate) =>
|
|
500
|
+
candidate.session.sessionManager.getSessionFile() === sessionPath,
|
|
501
|
+
);
|
|
502
|
+
if (retained) {
|
|
503
|
+
await this.promoteRetainedRuntime(retained);
|
|
504
|
+
this.hasSelectedWorkspace = true;
|
|
505
|
+
this.emit("session_switched", { sessionPath });
|
|
506
|
+
return { cancelled: false };
|
|
507
|
+
}
|
|
508
|
+
const sessionManager = SessionManager.open(
|
|
509
|
+
sessionPath,
|
|
510
|
+
this.webSessionDirectory,
|
|
511
|
+
);
|
|
512
|
+
const cwd = await canonicalDirectory(sessionManager.getCwd());
|
|
513
|
+
this.assertActive();
|
|
514
|
+
const replacement = await PiWebRuntime.createRuntime(
|
|
515
|
+
cwd,
|
|
516
|
+
sessionManager,
|
|
517
|
+
this.dispatcherLease,
|
|
518
|
+
);
|
|
519
|
+
await this.activateCandidate(replacement.runtime);
|
|
520
|
+
this.hasSelectedWorkspace = true;
|
|
521
|
+
this.emit("session_switched", { sessionPath });
|
|
522
|
+
return { cancelled: false };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
dispose() {
|
|
526
|
+
this.disposePromise ??= this.disposeInternal();
|
|
527
|
+
return this.disposePromise;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private async disposeInternal() {
|
|
531
|
+
this.disposed = true;
|
|
532
|
+
this.unsubscribeSession?.();
|
|
533
|
+
this.unsubscribeSession = undefined;
|
|
534
|
+
const runtimes = new Set([
|
|
535
|
+
this.runtime,
|
|
536
|
+
...this.retainedRuntimes,
|
|
537
|
+
...this.candidateRuntimes,
|
|
538
|
+
]);
|
|
539
|
+
for (const retained of this.retainedRuntimes) {
|
|
540
|
+
this.retainedSubscriptions.get(retained)?.();
|
|
541
|
+
}
|
|
542
|
+
const failures: unknown[] = [];
|
|
543
|
+
try {
|
|
544
|
+
const aborts = await Promise.allSettled(
|
|
545
|
+
[...runtimes].map((runtime) => runtime.session.abort()),
|
|
546
|
+
);
|
|
547
|
+
failures.push(
|
|
548
|
+
...aborts
|
|
549
|
+
.filter((result) => result.status === "rejected")
|
|
550
|
+
.map((result) => result.reason),
|
|
551
|
+
);
|
|
552
|
+
await Promise.all([...this.promptOperations]);
|
|
553
|
+
await Promise.all([...this.runtimeOperations]);
|
|
554
|
+
const finalRuntimes = new Set([
|
|
555
|
+
...runtimes,
|
|
556
|
+
this.runtime,
|
|
557
|
+
...this.retainedRuntimes,
|
|
558
|
+
...this.candidateRuntimes,
|
|
559
|
+
]);
|
|
560
|
+
const lateRuntimes = [...finalRuntimes].filter(
|
|
561
|
+
(runtime) => !runtimes.has(runtime),
|
|
562
|
+
);
|
|
563
|
+
const lateAborts = await Promise.allSettled(
|
|
564
|
+
lateRuntimes.map((runtime) => runtime.session.abort()),
|
|
565
|
+
);
|
|
566
|
+
failures.push(
|
|
567
|
+
...lateAborts
|
|
568
|
+
.filter((result) => result.status === "rejected")
|
|
569
|
+
.map((result) => result.reason),
|
|
570
|
+
);
|
|
571
|
+
await Promise.allSettled(
|
|
572
|
+
[...finalRuntimes].map((runtime) => this.disposeAgentRuntime(runtime)),
|
|
573
|
+
);
|
|
574
|
+
await Promise.allSettled([...this.runtimeDisposals]);
|
|
575
|
+
if (this.runtimeDisposalFailure !== undefined) {
|
|
576
|
+
failures.push(this.runtimeDisposalFailure);
|
|
577
|
+
this.runtimeDisposalFailure = undefined;
|
|
578
|
+
}
|
|
579
|
+
} finally {
|
|
580
|
+
try {
|
|
581
|
+
await this.dispatcherLease.release();
|
|
582
|
+
} catch (error) {
|
|
583
|
+
failures.push(error);
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
await this.webHostLease.release();
|
|
587
|
+
} catch (error) {
|
|
588
|
+
failures.push(error);
|
|
589
|
+
}
|
|
590
|
+
this.retainedRuntimes.clear();
|
|
591
|
+
this.retainedSubscriptions.clear();
|
|
592
|
+
this.candidateRuntimes.clear();
|
|
593
|
+
this.runtimeOperations.clear();
|
|
594
|
+
this.listeners.clear();
|
|
595
|
+
}
|
|
596
|
+
if (failures.length > 0) {
|
|
597
|
+
throw new AggregateError(failures, "Failed to dispose the Web runtime");
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private static async createRuntime(
|
|
602
|
+
cwd: string,
|
|
603
|
+
sessionManager: SessionManager,
|
|
604
|
+
dispatcherLease?: HttpDispatcherLease,
|
|
605
|
+
) {
|
|
606
|
+
const agentDir = getAgentDir();
|
|
607
|
+
let sharedDispatcherLease = dispatcherLease;
|
|
608
|
+
let ownsDispatcherLease = false;
|
|
609
|
+
const trustStore = new ProjectTrustStore(agentDir);
|
|
610
|
+
const createRuntime: CreateAgentSessionRuntimeFactory = async (options) => {
|
|
611
|
+
const projectTrusted =
|
|
612
|
+
!hasTrustRequiringProjectResources(options.cwd) ||
|
|
613
|
+
trustStore.get(options.cwd) === true;
|
|
614
|
+
const settingsManager = SettingsManager.create(
|
|
615
|
+
options.cwd,
|
|
616
|
+
options.agentDir,
|
|
617
|
+
{ projectTrusted },
|
|
618
|
+
);
|
|
619
|
+
const httpProxyConfigured = applyHttpProxySettings(
|
|
620
|
+
settingsManager.getGlobalSettings().httpProxy,
|
|
621
|
+
);
|
|
622
|
+
if (!sharedDispatcherLease) {
|
|
623
|
+
sharedDispatcherLease = configureHttpDispatcher(
|
|
624
|
+
settingsManager.getHttpIdleTimeoutMs(),
|
|
625
|
+
);
|
|
626
|
+
ownsDispatcherLease = true;
|
|
627
|
+
}
|
|
628
|
+
const services = await createAgentSessionServices({
|
|
629
|
+
cwd: options.cwd,
|
|
630
|
+
agentDir: options.agentDir,
|
|
631
|
+
settingsManager,
|
|
632
|
+
modelRuntimeSignal: AbortSignal.timeout(STARTUP_TIMEOUT_MS),
|
|
633
|
+
});
|
|
634
|
+
const extensionErrors = services.resourceLoader
|
|
635
|
+
.getExtensions()
|
|
636
|
+
.errors.map(({ path, error }) => `Failed to load extension "${path}": ${error}`);
|
|
637
|
+
const errors = [
|
|
638
|
+
...services.diagnostics
|
|
639
|
+
.filter((diagnostic) => diagnostic.type === "error")
|
|
640
|
+
.map((diagnostic) => diagnostic.message),
|
|
641
|
+
...extensionErrors,
|
|
642
|
+
];
|
|
643
|
+
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
644
|
+
const created = await createAgentSessionFromServices({
|
|
645
|
+
services,
|
|
646
|
+
sessionManager: options.sessionManager,
|
|
647
|
+
sessionStartEvent: options.sessionStartEvent,
|
|
648
|
+
});
|
|
649
|
+
const model = created.session.model;
|
|
650
|
+
traceWeb("provider_config", {
|
|
651
|
+
provider: model?.provider,
|
|
652
|
+
modelId: model?.id,
|
|
653
|
+
api: model?.api,
|
|
654
|
+
baseOrigin: model?.baseUrl ? new URL(model.baseUrl).origin : undefined,
|
|
655
|
+
httpIdleTimeoutMs: sharedDispatcherLease.timeoutMs,
|
|
656
|
+
providerRetry: settingsManager.getProviderRetrySettings(),
|
|
657
|
+
httpProxyConfigured,
|
|
658
|
+
});
|
|
659
|
+
return {
|
|
660
|
+
...created,
|
|
661
|
+
services,
|
|
662
|
+
diagnostics: services.diagnostics,
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
try {
|
|
666
|
+
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
667
|
+
cwd,
|
|
668
|
+
agentDir,
|
|
669
|
+
sessionManager,
|
|
670
|
+
});
|
|
671
|
+
if (!sharedDispatcherLease) {
|
|
672
|
+
throw new Error("HTTP dispatcher lease was not created");
|
|
673
|
+
}
|
|
674
|
+
return { runtime, dispatcherLease: sharedDispatcherLease };
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (ownsDispatcherLease) await sharedDispatcherLease?.release();
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private async startRuntimeSession() {
|
|
682
|
+
const runtime = this.runtime;
|
|
683
|
+
await this.initializeRuntimeSession(runtime);
|
|
684
|
+
this.attachActiveSession(runtime, runtime.session);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private async initializeRuntimeSession(runtime: AgentSessionRuntime) {
|
|
688
|
+
await this.bindExtensions(runtime, runtime.session);
|
|
689
|
+
runtime.setRebindSession(async (replacement) => {
|
|
690
|
+
this.assertActiveRuntime(runtime);
|
|
691
|
+
await this.bindExtensions(runtime, replacement);
|
|
692
|
+
this.assertActiveRuntime(runtime);
|
|
693
|
+
this.attachActiveSession(runtime, replacement);
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private async bindExtensions(
|
|
698
|
+
runtime: AgentSessionRuntime,
|
|
699
|
+
session: AgentSession,
|
|
700
|
+
) {
|
|
701
|
+
const startedAt = performance.now();
|
|
702
|
+
traceWeb("extensions_bind_started", {
|
|
703
|
+
sessionId: session.sessionManager.getSessionId(),
|
|
704
|
+
cwd: runtime.cwd,
|
|
705
|
+
});
|
|
706
|
+
await session.bindExtensions({ mode: "print" });
|
|
707
|
+
traceWeb("extensions_bind_finished", {
|
|
708
|
+
sessionId: session.sessionManager.getSessionId(),
|
|
709
|
+
elapsedMs: elapsed(startedAt),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private attachActiveSession(
|
|
714
|
+
runtime: AgentSessionRuntime,
|
|
715
|
+
session: AgentSession,
|
|
716
|
+
) {
|
|
717
|
+
this.assertActiveRuntime(runtime);
|
|
718
|
+
const unsubscribe = session.subscribe((event) =>
|
|
719
|
+
this.projectEvent(session, event),
|
|
720
|
+
);
|
|
721
|
+
const previous = this.unsubscribeSession;
|
|
722
|
+
this.unsubscribeSession = unsubscribe;
|
|
723
|
+
previous?.();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private projectEvent(session: AgentSession, event: AgentSessionEvent) {
|
|
727
|
+
if (session !== this.runtime.session) return;
|
|
728
|
+
if (event.type === "message_start" && event.message.role === "user") {
|
|
729
|
+
if (!this.activePromptTrace) {
|
|
730
|
+
this.activePromptTrace = this.pendingPromptTraces.shift();
|
|
731
|
+
} else if (this.activePromptTrace.started && this.pendingPromptTraces.length > 0) {
|
|
732
|
+
this.activePromptTrace = this.pendingPromptTraces.shift();
|
|
733
|
+
}
|
|
734
|
+
if (this.activePromptTrace) this.activePromptTrace.started = true;
|
|
735
|
+
}
|
|
736
|
+
const promptTrace = this.activePromptTrace;
|
|
737
|
+
if (promptTrace) {
|
|
738
|
+
const eventDetail: Record<string, unknown> = {
|
|
739
|
+
commandId: promptTrace.commandId,
|
|
740
|
+
sessionId: promptTrace.sessionId,
|
|
741
|
+
type: event.type,
|
|
742
|
+
elapsedMs: elapsed(promptTrace.startedAt),
|
|
743
|
+
};
|
|
744
|
+
if (event.type === "message_update") {
|
|
745
|
+
eventDetail.contentChars = projectMessage(event.message).content.length;
|
|
746
|
+
}
|
|
747
|
+
if (event.type === "message_start" || event.type === "message_end") {
|
|
748
|
+
eventDetail.role = event.message.role;
|
|
749
|
+
const message = event.message as { stopReason?: unknown; errorMessage?: unknown };
|
|
750
|
+
if (typeof message.stopReason === "string") {
|
|
751
|
+
eventDetail.stopReason = message.stopReason;
|
|
752
|
+
}
|
|
753
|
+
if (typeof message.errorMessage === "string") {
|
|
754
|
+
eventDetail.errorMessage = message.errorMessage;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (event.type === "auto_retry_start") {
|
|
758
|
+
eventDetail.attempt = event.attempt;
|
|
759
|
+
eventDetail.maxAttempts = event.maxAttempts;
|
|
760
|
+
eventDetail.delayMs = event.delayMs;
|
|
761
|
+
eventDetail.errorMessage = event.errorMessage;
|
|
762
|
+
}
|
|
763
|
+
if (event.type === "auto_retry_end") {
|
|
764
|
+
eventDetail.attempt = event.attempt;
|
|
765
|
+
eventDetail.success = event.success;
|
|
766
|
+
if (event.finalError) eventDetail.finalError = event.finalError;
|
|
767
|
+
}
|
|
768
|
+
if (event.type === "agent_end") eventDetail.willRetry = event.willRetry;
|
|
769
|
+
traceWeb("agent_event", eventDetail);
|
|
770
|
+
}
|
|
771
|
+
switch (event.type) {
|
|
772
|
+
case "agent_start":
|
|
773
|
+
case "agent_settled":
|
|
774
|
+
this.emit(event.type);
|
|
775
|
+
if (
|
|
776
|
+
event.type === "agent_settled" &&
|
|
777
|
+
this.activePromptTrace?.started &&
|
|
778
|
+
this.pendingPromptTraces.length === 0
|
|
779
|
+
) {
|
|
780
|
+
this.activePromptTrace = undefined;
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case "auto_retry_start":
|
|
784
|
+
this.emit(event.type, {
|
|
785
|
+
attempt: event.attempt,
|
|
786
|
+
maxAttempts: event.maxAttempts,
|
|
787
|
+
delayMs: event.delayMs,
|
|
788
|
+
});
|
|
789
|
+
break;
|
|
790
|
+
case "message_start":
|
|
791
|
+
this.liveMessageKey = `live-${++this.liveMessageSequence}`;
|
|
792
|
+
this.emit(event.type, {
|
|
793
|
+
message: projectMessage(event.message),
|
|
794
|
+
messageKey: this.liveMessageKey,
|
|
795
|
+
});
|
|
796
|
+
break;
|
|
797
|
+
case "message_update":
|
|
798
|
+
case "message_end":
|
|
799
|
+
this.emit(event.type, {
|
|
800
|
+
message: projectMessage(event.message),
|
|
801
|
+
...(this.liveMessageKey ? { messageKey: this.liveMessageKey } : {}),
|
|
802
|
+
});
|
|
803
|
+
if (event.type === "message_end") this.liveMessageKey = undefined;
|
|
804
|
+
break;
|
|
805
|
+
case "tool_execution_start":
|
|
806
|
+
case "tool_execution_end":
|
|
807
|
+
this.emit(event.type, {
|
|
808
|
+
toolName: event.toolName,
|
|
809
|
+
toolCallId: event.toolCallId,
|
|
810
|
+
...(event.type === "tool_execution_end"
|
|
811
|
+
? { isError: event.isError }
|
|
812
|
+
: {}),
|
|
813
|
+
});
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private emit(type: string, detail?: Record<string, unknown>) {
|
|
819
|
+
for (const listener of this.listeners) listener({ type, detail });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private removePromptTrace(trace: PromptTrace) {
|
|
823
|
+
const pendingIndex = this.pendingPromptTraces.indexOf(trace);
|
|
824
|
+
if (pendingIndex !== -1) this.pendingPromptTraces.splice(pendingIndex, 1);
|
|
825
|
+
if (this.activePromptTrace !== trace) return;
|
|
826
|
+
this.activePromptTrace = this.pendingPromptTraces.shift();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private retainRuntimeReference(runtime: AgentSessionRuntime) {
|
|
830
|
+
this.inFlightRuntimes.set(
|
|
831
|
+
runtime,
|
|
832
|
+
(this.inFlightRuntimes.get(runtime) ?? 0) + 1,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
private releaseRuntimeReference(runtime: AgentSessionRuntime) {
|
|
837
|
+
const remaining = (this.inFlightRuntimes.get(runtime) ?? 1) - 1;
|
|
838
|
+
if (remaining > 0) this.inFlightRuntimes.set(runtime, remaining);
|
|
839
|
+
else this.inFlightRuntimes.delete(runtime);
|
|
840
|
+
this.releaseRetainedRuntime(runtime);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private trackRuntimeOperation<T>(operation: Promise<T>) {
|
|
844
|
+
const settlement = operation.then(
|
|
845
|
+
() => undefined,
|
|
846
|
+
() => undefined,
|
|
847
|
+
);
|
|
848
|
+
this.runtimeOperations.add(settlement);
|
|
849
|
+
void settlement.then(() => this.runtimeOperations.delete(settlement));
|
|
850
|
+
return operation;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private serializeControllerMutation<T>(operation: () => Promise<T>) {
|
|
854
|
+
const result = (this.controllerMutation ?? Promise.resolve()).then(() => {
|
|
855
|
+
this.assertActive();
|
|
856
|
+
return operation();
|
|
857
|
+
});
|
|
858
|
+
this.controllerMutation = result.then(
|
|
859
|
+
() => undefined,
|
|
860
|
+
() => undefined,
|
|
861
|
+
);
|
|
862
|
+
return this.trackRuntimeOperation(result);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private disposeAgentRuntime(runtime: AgentSessionRuntime) {
|
|
866
|
+
const existing = this.runtimeDisposalPromises.get(runtime);
|
|
867
|
+
if (existing) return existing;
|
|
868
|
+
const disposal = Promise.resolve().then(() => runtime.dispose());
|
|
869
|
+
this.runtimeDisposalPromises.set(runtime, disposal);
|
|
870
|
+
this.runtimeDisposals.add(disposal);
|
|
871
|
+
void disposal.catch(() => undefined);
|
|
872
|
+
void disposal.then(
|
|
873
|
+
() => this.runtimeDisposals.delete(disposal),
|
|
874
|
+
(error) => {
|
|
875
|
+
this.runtimeDisposals.delete(disposal);
|
|
876
|
+
this.runtimeDisposalFailure ??= error;
|
|
877
|
+
this.emit("runtime_dispose_failed", { error: errorText(error) });
|
|
878
|
+
},
|
|
879
|
+
);
|
|
880
|
+
return disposal;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private retainRuntime(runtime: AgentSessionRuntime) {
|
|
884
|
+
this.retainedRuntimes.add(runtime);
|
|
885
|
+
const unsubscribe = runtime.session.subscribe((event) => {
|
|
886
|
+
if (event.type !== "agent_settled") return;
|
|
887
|
+
this.emit("session_progress", {
|
|
888
|
+
sessionId: runtime.session.sessionManager.getSessionId(),
|
|
889
|
+
});
|
|
890
|
+
this.releaseRetainedRuntime(runtime);
|
|
891
|
+
});
|
|
892
|
+
this.retainedSubscriptions.set(runtime, unsubscribe);
|
|
893
|
+
this.releaseRetainedRuntime(runtime);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private async promoteRetainedRuntime(runtime: AgentSessionRuntime) {
|
|
897
|
+
const previous = this.runtime;
|
|
898
|
+
this.retainedSubscriptions.get(runtime)?.();
|
|
899
|
+
this.retainedSubscriptions.delete(runtime);
|
|
900
|
+
this.retainedRuntimes.delete(runtime);
|
|
901
|
+
this.unsubscribeSession?.();
|
|
902
|
+
this.unsubscribeSession = undefined;
|
|
903
|
+
this.resetPromptTraces();
|
|
904
|
+
this.retainRuntime(previous);
|
|
905
|
+
this.runtime = runtime;
|
|
906
|
+
this.attachActiveSession(runtime, runtime.session);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private releaseRetainedRuntime(runtime: AgentSessionRuntime) {
|
|
910
|
+
if (!this.retainedRuntimes.has(runtime)) return;
|
|
911
|
+
if (runtime.session.isStreaming || this.inFlightRuntimes.has(runtime)) return;
|
|
912
|
+
this.retainedSubscriptions.get(runtime)?.();
|
|
913
|
+
this.retainedSubscriptions.delete(runtime);
|
|
914
|
+
this.retainedRuntimes.delete(runtime);
|
|
915
|
+
void this.disposeAgentRuntime(runtime);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
private async activateCandidate(runtime: AgentSessionRuntime) {
|
|
919
|
+
this.candidateRuntimes.add(runtime);
|
|
920
|
+
try {
|
|
921
|
+
await this.replaceRuntime(runtime);
|
|
922
|
+
} finally {
|
|
923
|
+
this.candidateRuntimes.delete(runtime);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private async replaceRuntime(replacement: AgentSessionRuntime) {
|
|
928
|
+
const previous = this.runtime;
|
|
929
|
+
try {
|
|
930
|
+
this.assertActive();
|
|
931
|
+
await this.initializeRuntimeSession(replacement);
|
|
932
|
+
this.assertActive();
|
|
933
|
+
if (this.runtime !== previous) {
|
|
934
|
+
throw new Error("The active Web runtime changed during replacement");
|
|
935
|
+
}
|
|
936
|
+
} catch (error) {
|
|
937
|
+
try {
|
|
938
|
+
await this.disposeAgentRuntime(replacement);
|
|
939
|
+
} catch (cleanupError) {
|
|
940
|
+
throw new AggregateError(
|
|
941
|
+
[error, cleanupError],
|
|
942
|
+
"Failed to activate and dispose the replacement Web runtime",
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
947
|
+
this.runtime = replacement;
|
|
948
|
+
try {
|
|
949
|
+
this.attachActiveSession(replacement, replacement.session);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
this.runtime = previous;
|
|
952
|
+
try {
|
|
953
|
+
await this.disposeAgentRuntime(replacement);
|
|
954
|
+
} catch (cleanupError) {
|
|
955
|
+
throw new AggregateError(
|
|
956
|
+
[error, cleanupError],
|
|
957
|
+
"Failed to attach and dispose the replacement Web runtime",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
this.resetPromptTraces();
|
|
963
|
+
this.retainRuntime(previous);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private assertActive() {
|
|
967
|
+
if (this.disposed) throw new Error("Web runtime is stopped");
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
private assertWorkspaceSelected() {
|
|
971
|
+
if (this.hasSelectedWorkspace) return;
|
|
972
|
+
throw new WebRuntimeRequestError(
|
|
973
|
+
"Choose a workspace before using the Web runtime",
|
|
974
|
+
"WORKSPACE_REQUIRED",
|
|
975
|
+
409,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private assertActiveRuntime(runtime: AgentSessionRuntime) {
|
|
980
|
+
this.assertActive();
|
|
981
|
+
if (runtime !== this.runtime) {
|
|
982
|
+
this.releaseRetainedRuntime(runtime);
|
|
983
|
+
throw new Error("A retained Web runtime cannot replace its Session");
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private resetPromptTraces() {
|
|
988
|
+
this.activePromptTrace = undefined;
|
|
989
|
+
this.pendingPromptTraces.length = 0;
|
|
990
|
+
}
|
|
991
|
+
}
|