@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.
Files changed (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. 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
+ }