@larose/pi-web 0.3.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 (43) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +50 -0
  3. package/THIRD_PARTY_LICENSES.md +40 -0
  4. package/dist/client/home.js +1619 -0
  5. package/dist/client/session.js +3703 -0
  6. package/dist/server/api.js +485 -0
  7. package/dist/server/cli.js +51 -0
  8. package/dist/server/directory-browser.js +104 -0
  9. package/dist/server/errors.js +10 -0
  10. package/dist/server/event-buffer.js +40 -0
  11. package/dist/server/extension-ui.js +245 -0
  12. package/dist/server/git-workspaces.js +559 -0
  13. package/dist/server/runtime-registry.js +703 -0
  14. package/dist/server/server.js +190 -0
  15. package/dist/server/session-repository.js +374 -0
  16. package/package.json +46 -0
  17. package/public/home.html +139 -0
  18. package/public/session.html +144 -0
  19. package/public/styles.css +2463 -0
  20. package/screenshots/home.png +0 -0
  21. package/screenshots/session.png +0 -0
  22. package/src/client/display-title.ts +36 -0
  23. package/src/client/event-stream.ts +194 -0
  24. package/src/client/home.ts +1575 -0
  25. package/src/client/markdown.ts +98 -0
  26. package/src/client/message-queue.ts +67 -0
  27. package/src/client/path-combobox.ts +271 -0
  28. package/src/client/session.ts +2174 -0
  29. package/src/client/shared.ts +99 -0
  30. package/src/client/slash-completion.ts +184 -0
  31. package/src/client/transcript-activity.ts +188 -0
  32. package/src/client/usage-format.ts +156 -0
  33. package/src/client/workspace-browser.ts +36 -0
  34. package/src/server/api.ts +652 -0
  35. package/src/server/cli.ts +63 -0
  36. package/src/server/directory-browser.ts +137 -0
  37. package/src/server/errors.ts +11 -0
  38. package/src/server/event-buffer.ts +59 -0
  39. package/src/server/extension-ui.ts +359 -0
  40. package/src/server/git-workspaces.ts +750 -0
  41. package/src/server/runtime-registry.ts +943 -0
  42. package/src/server/server.ts +248 -0
  43. package/src/server/session-repository.ts +488 -0
@@ -0,0 +1,703 @@
1
+ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, ModelRuntime, } from "@earendil-works/pi-coding-agent";
2
+ import { AppError } from "./errors.js";
3
+ import { EventBuffer } from "./event-buffer.js";
4
+ import { WebExtensionUI, } from "./extension-ui.js";
5
+ function errorMessage(error) {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+ function sessionCommands(session) {
9
+ const commands = [];
10
+ for (const command of session.extensionRunner.getRegisteredCommands()) {
11
+ commands.push({
12
+ name: command.invocationName,
13
+ ...(command.description === undefined ? {} : { description: command.description }),
14
+ source: "extension",
15
+ sourceInfo: command.sourceInfo,
16
+ });
17
+ }
18
+ for (const template of session.promptTemplates) {
19
+ commands.push({
20
+ name: template.name,
21
+ ...(template.description === undefined ? {} : { description: template.description }),
22
+ source: "prompt",
23
+ sourceInfo: template.sourceInfo,
24
+ });
25
+ }
26
+ for (const skill of session.resourceLoader.getSkills().skills) {
27
+ commands.push({
28
+ name: `skill:${skill.name}`,
29
+ ...(skill.description === undefined ? {} : { description: skill.description }),
30
+ source: "skill",
31
+ sourceInfo: skill.sourceInfo,
32
+ });
33
+ }
34
+ return commands;
35
+ }
36
+ class SdkRuntimeSession {
37
+ extensionUI = new WebExtensionUI();
38
+ runtime;
39
+ listeners = new Set();
40
+ replacementListeners = new Set();
41
+ unsubscribeSession;
42
+ binding;
43
+ commandCatalogue;
44
+ currentSessionId;
45
+ currentSessionManager;
46
+ disposed = false;
47
+ constructor(runtime) {
48
+ this.runtime = runtime;
49
+ this.commandCatalogue = sessionCommands(runtime.session);
50
+ this.currentSessionId = runtime.session.sessionId;
51
+ this.currentSessionManager = runtime.session.sessionManager;
52
+ this.attachSession(runtime.session);
53
+ this.binding = this.bindSession(runtime.session);
54
+ void this.binding.catch(() => undefined);
55
+ runtime.setBeforeSessionInvalidate(() => this.extensionUI.reset());
56
+ runtime.setRebindSession(async (session) => {
57
+ const previousId = this.currentSessionId;
58
+ this.commandCatalogue = sessionCommands(session);
59
+ this.currentSessionId = session.sessionId;
60
+ this.currentSessionManager = session.sessionManager;
61
+ this.attachSession(session);
62
+ this.binding = this.bindSession(session);
63
+ void this.binding.catch(() => undefined);
64
+ for (const listener of this.replacementListeners) {
65
+ listener(previousId, session.sessionId);
66
+ }
67
+ await this.binding;
68
+ });
69
+ }
70
+ get sessionId() {
71
+ return this.currentSessionId;
72
+ }
73
+ get sessionManager() {
74
+ return this.currentSessionManager;
75
+ }
76
+ get sessionName() {
77
+ return this.runtime.session.sessionName;
78
+ }
79
+ get cwd() {
80
+ return this.runtime.session.sessionManager.getCwd();
81
+ }
82
+ get isStreaming() {
83
+ return this.runtime.session.isStreaming;
84
+ }
85
+ get isCompacting() {
86
+ return this.runtime.session.isCompacting;
87
+ }
88
+ get pendingMessageCount() {
89
+ return this.runtime.session.pendingMessageCount;
90
+ }
91
+ getSteeringMessages() {
92
+ return [...this.runtime.session.getSteeringMessages()];
93
+ }
94
+ getFollowUpMessages() {
95
+ return [...this.runtime.session.getFollowUpMessages()];
96
+ }
97
+ get messages() {
98
+ return this.runtime.session.messages;
99
+ }
100
+ get commands() {
101
+ return this.commandCatalogue;
102
+ }
103
+ get model() {
104
+ return this.runtime.session.model;
105
+ }
106
+ get thinkingLevel() {
107
+ return this.runtime.session.thinkingLevel;
108
+ }
109
+ get autoCompactionEnabled() {
110
+ return this.runtime.session.autoCompactionEnabled;
111
+ }
112
+ get agent() {
113
+ return this.runtime.session.agent;
114
+ }
115
+ get ready() {
116
+ return this.binding;
117
+ }
118
+ getSessionStats() {
119
+ return this.runtime.session.getSessionStats();
120
+ }
121
+ subscribe(listener) {
122
+ this.listeners.add(listener);
123
+ return () => this.listeners.delete(listener);
124
+ }
125
+ onSessionReplaced(listener) {
126
+ this.replacementListeners.add(listener);
127
+ return () => this.replacementListeners.delete(listener);
128
+ }
129
+ prompt(message, options) {
130
+ return this.runtime.session.prompt(message, options);
131
+ }
132
+ steer(message) {
133
+ return this.runtime.session.steer(message);
134
+ }
135
+ clearQueue() {
136
+ return this.runtime.session.clearQueue();
137
+ }
138
+ async restoreQueue(queue) {
139
+ for (const message of queue.steering) {
140
+ await this.runtime.session.sendUserMessage(message, {
141
+ deliverAs: "steer",
142
+ expandPromptTemplates: false,
143
+ });
144
+ }
145
+ for (const message of queue.followUp) {
146
+ await this.runtime.session.sendUserMessage(message, {
147
+ deliverAs: "followUp",
148
+ expandPromptTemplates: false,
149
+ });
150
+ }
151
+ }
152
+ abort() {
153
+ return this.runtime.session.abort();
154
+ }
155
+ setSessionName(name) {
156
+ this.runtime.session.setSessionName(name);
157
+ }
158
+ async dispose() {
159
+ if (this.disposed) {
160
+ return;
161
+ }
162
+ this.disposed = true;
163
+ this.extensionUI.dispose();
164
+ await this.runtime.dispose();
165
+ this.unsubscribeSession?.();
166
+ this.listeners.clear();
167
+ this.replacementListeners.clear();
168
+ }
169
+ attachSession(session) {
170
+ this.unsubscribeSession?.();
171
+ this.unsubscribeSession = session.subscribe((event) => {
172
+ for (const listener of this.listeners) {
173
+ listener(event);
174
+ }
175
+ });
176
+ }
177
+ bindSession(session) {
178
+ return session.bindExtensions({
179
+ mode: "rpc",
180
+ uiContext: this.extensionUI.context,
181
+ commandContextActions: {
182
+ waitForIdle: () => session.waitForIdle(),
183
+ newSession: (options) => this.runtime.newSession(options),
184
+ fork: async (entryId, options) => {
185
+ const result = await this.runtime.fork(entryId, options);
186
+ return { cancelled: result.cancelled };
187
+ },
188
+ navigateTree: async (targetId, options) => {
189
+ const result = await session.navigateTree(targetId, options);
190
+ return { cancelled: result.cancelled };
191
+ },
192
+ switchSession: (sessionPath, options) => this.runtime.switchSession(sessionPath, options),
193
+ reload: async () => {
194
+ await session.reload();
195
+ this.commandCatalogue = sessionCommands(session);
196
+ },
197
+ },
198
+ shutdownHandler: () => this.extensionUI.context.notify("An extension requested shutdown. Stop Pi Web from its server process.", "warning"),
199
+ onError: (error) => this.extensionUI.context.notify(`Extension error in ${error.extensionPath} (${error.event}): ${error.error}`, "error"),
200
+ });
201
+ }
202
+ }
203
+ export class RuntimeHandle {
204
+ events;
205
+ session;
206
+ initialCwd;
207
+ idleTimeoutMs;
208
+ onIdle;
209
+ onSessionIdChanged;
210
+ unsubscribeSession;
211
+ unsubscribeExtensionUI;
212
+ unsubscribeReplacement;
213
+ sessionId;
214
+ activeTools = new Set();
215
+ activePrompt;
216
+ connectedClients = 0;
217
+ disposed = false;
218
+ idleTimer;
219
+ lastSettledAt = Date.now();
220
+ operationTail = Promise.resolve();
221
+ queuedMessages = 0;
222
+ retrying = false;
223
+ suppressQueueUpdates = false;
224
+ suppressedQueueUpdate;
225
+ constructor(session, cwd, idleTimeoutMs, eventCapacity, onIdle, onSessionIdChanged = () => undefined) {
226
+ this.session = session;
227
+ this.sessionId = session.sessionId;
228
+ this.initialCwd = cwd;
229
+ this.idleTimeoutMs = idleTimeoutMs;
230
+ this.events = new EventBuffer(eventCapacity);
231
+ this.onIdle = onIdle;
232
+ this.onSessionIdChanged = onSessionIdChanged;
233
+ this.unsubscribeSession = session.subscribe((event) => this.onSessionEvent(event));
234
+ this.unsubscribeExtensionUI = session.extensionUI?.setPublisher((event) => {
235
+ this.events.publish(event);
236
+ if (event.type === "extension_ui_request" &&
237
+ (event.method === "select" ||
238
+ event.method === "confirm" ||
239
+ event.method === "input" ||
240
+ event.method === "editor")) {
241
+ this.cancelEviction();
242
+ }
243
+ if (event.type === "extension_ui_closed") {
244
+ this.scheduleEviction();
245
+ }
246
+ });
247
+ this.unsubscribeReplacement = session.onSessionReplaced?.((previousId, sessionId) => {
248
+ this.sessionId = sessionId;
249
+ this.onSessionIdChanged(this, previousId, sessionId, session.sessionManager);
250
+ this.events.publish({ type: "session_replaced", previousId, sessionId });
251
+ });
252
+ this.scheduleEviction();
253
+ }
254
+ get id() {
255
+ return this.sessionId;
256
+ }
257
+ get cwd() {
258
+ return this.session.cwd ?? this.initialCwd;
259
+ }
260
+ snapshot() {
261
+ const streamingMessage = this.session.agent.state.streamingMessage;
262
+ const stats = this.session.getSessionStats();
263
+ return {
264
+ id: this.id,
265
+ cwd: this.cwd,
266
+ ...(this.session.sessionName ? { sessionName: this.session.sessionName } : {}),
267
+ isStreaming: this.session.isStreaming,
268
+ isCompacting: this.session.isCompacting,
269
+ isWorking: this.isWorking(),
270
+ pendingMessageCount: this.session.pendingMessageCount,
271
+ queue: {
272
+ steering: [...this.session.getSteeringMessages()],
273
+ followUp: [...this.session.getFollowUpMessages()],
274
+ },
275
+ connectedClients: this.connectedClients,
276
+ model: this.session.model
277
+ ? {
278
+ provider: this.session.model.provider,
279
+ id: this.session.model.id,
280
+ name: this.session.model.name,
281
+ }
282
+ : null,
283
+ thinkingLevel: this.session.thinkingLevel,
284
+ autoCompactionEnabled: this.session.autoCompactionEnabled,
285
+ usage: {
286
+ tokens: stats.tokens,
287
+ context: stats.contextUsage ?? null,
288
+ },
289
+ messages: this.session.messages,
290
+ commands: this.session.commands,
291
+ extensionUI: this.session.extensionUI?.snapshot() ?? { pending: [], statuses: [], widgets: [] },
292
+ ...(streamingMessage ? { streamingMessage } : {}),
293
+ latestEventId: this.events.latestId,
294
+ };
295
+ }
296
+ attachClient(listener) {
297
+ this.assertOpen();
298
+ this.connectedClients += 1;
299
+ this.cancelEviction();
300
+ const unsubscribe = this.events.subscribe(listener);
301
+ let attached = true;
302
+ return () => {
303
+ if (!attached) {
304
+ return;
305
+ }
306
+ attached = false;
307
+ unsubscribe();
308
+ this.connectedClients -= 1;
309
+ this.scheduleEviction();
310
+ };
311
+ }
312
+ send(message) {
313
+ const trimmed = message.trim();
314
+ if (!trimmed) {
315
+ return Promise.reject(new AppError("invalid_message", "A message is required"));
316
+ }
317
+ return this.enqueue(async () => {
318
+ this.assertOpen();
319
+ this.cancelEviction();
320
+ if (this.session.isStreaming || this.activePrompt) {
321
+ await this.session.prompt(message, {
322
+ streamingBehavior: "steer",
323
+ preflightResult: () => undefined,
324
+ });
325
+ return { queued: true };
326
+ }
327
+ await this.startPrompt(message);
328
+ return { queued: false };
329
+ });
330
+ }
331
+ removePendingSteering(index, expectedMessage, expectedQueue) {
332
+ return this.enqueue(async () => {
333
+ this.assertOpen();
334
+ this.cancelEviction();
335
+ const steering = [...this.session.getSteeringMessages()];
336
+ const unchanged = steering.length === expectedQueue.length &&
337
+ steering.every((message, queueIndex) => message === expectedQueue[queueIndex]);
338
+ if (!unchanged || steering[index] !== expectedMessage) {
339
+ throw new AppError("stale_pending_message", "The pending message queue has changed", 409);
340
+ }
341
+ this.suppressQueueUpdates = true;
342
+ try {
343
+ const queue = this.session.clearQueue();
344
+ queue.steering.splice(index, 1);
345
+ await this.session.restoreQueue(queue);
346
+ }
347
+ finally {
348
+ this.suppressQueueUpdates = false;
349
+ const update = this.suppressedQueueUpdate;
350
+ this.suppressedQueueUpdate = undefined;
351
+ if (update) {
352
+ this.events.publish({ type: "agent_event", event: update });
353
+ }
354
+ this.scheduleEviction();
355
+ }
356
+ });
357
+ }
358
+ respondToExtensionUI(response) {
359
+ this.assertOpen();
360
+ if (!this.session.extensionUI?.respond(response)) {
361
+ throw new AppError("unknown_ui_request", `Unknown extension UI request: ${response.id}`, 409);
362
+ }
363
+ }
364
+ setSessionName(name) {
365
+ this.assertOpen();
366
+ this.session.setSessionName(name ?? "");
367
+ return this.session.sessionName;
368
+ }
369
+ abort() {
370
+ return this.enqueue(async () => {
371
+ this.assertOpen();
372
+ this.cancelEviction();
373
+ const queued = this.session.clearQueue();
374
+ this.queuedMessages = 0;
375
+ this.session.extensionUI?.cancelPending();
376
+ await this.session.abort();
377
+ this.retrying = false;
378
+ this.activeTools.clear();
379
+ this.lastSettledAt = Date.now();
380
+ this.scheduleEviction();
381
+ return { restoredMessages: [...queued.steering, ...queued.followUp] };
382
+ });
383
+ }
384
+ isEvictable() {
385
+ return !this.disposed && this.connectedClients === 0 && !this.isWorking();
386
+ }
387
+ async dispose() {
388
+ if (this.disposed) {
389
+ return;
390
+ }
391
+ this.disposed = true;
392
+ this.cancelEviction();
393
+ this.session.clearQueue();
394
+ try {
395
+ await this.session.abort();
396
+ }
397
+ finally {
398
+ this.events.publish({ type: "runtime_disposed" });
399
+ this.unsubscribeSession();
400
+ this.unsubscribeExtensionUI?.();
401
+ this.unsubscribeReplacement?.();
402
+ await this.session.dispose();
403
+ }
404
+ }
405
+ enqueue(operation) {
406
+ const result = this.operationTail.then(operation, operation);
407
+ this.operationTail = result.then(() => undefined, () => undefined);
408
+ return result;
409
+ }
410
+ async startPrompt(message) {
411
+ let preflightSettled = false;
412
+ let acceptPrompt;
413
+ let rejectPrompt;
414
+ const accepted = new Promise((resolve, reject) => {
415
+ acceptPrompt = resolve;
416
+ rejectPrompt = reject;
417
+ });
418
+ const completion = (async () => {
419
+ await this.session.ready;
420
+ await this.session.prompt(message, {
421
+ preflightResult: (success) => {
422
+ if (preflightSettled) {
423
+ return;
424
+ }
425
+ preflightSettled = true;
426
+ if (success) {
427
+ acceptPrompt();
428
+ }
429
+ else {
430
+ rejectPrompt(new AppError("prompt_rejected", "Pi rejected the message", 409));
431
+ }
432
+ },
433
+ });
434
+ })();
435
+ this.activePrompt = completion;
436
+ void completion
437
+ .then(() => {
438
+ if (preflightSettled) {
439
+ return;
440
+ }
441
+ preflightSettled = true;
442
+ acceptPrompt();
443
+ })
444
+ .catch((error) => {
445
+ if (!preflightSettled) {
446
+ preflightSettled = true;
447
+ rejectPrompt(error);
448
+ }
449
+ else {
450
+ this.events.publish({ type: "runtime_error", message: errorMessage(error) });
451
+ }
452
+ })
453
+ .finally(() => {
454
+ if (this.activePrompt === completion) {
455
+ this.activePrompt = undefined;
456
+ }
457
+ this.scheduleEviction();
458
+ });
459
+ await accepted;
460
+ }
461
+ onSessionEvent(event) {
462
+ if (this.disposed) {
463
+ return;
464
+ }
465
+ switch (event.type) {
466
+ case "agent_start":
467
+ this.cancelEviction();
468
+ break;
469
+ case "tool_execution_start":
470
+ this.activeTools.add(event.toolCallId);
471
+ this.cancelEviction();
472
+ break;
473
+ case "tool_execution_end":
474
+ this.activeTools.delete(event.toolCallId);
475
+ break;
476
+ case "queue_update":
477
+ this.queuedMessages = event.steering.length + event.followUp.length;
478
+ if (this.queuedMessages > 0) {
479
+ this.cancelEviction();
480
+ }
481
+ break;
482
+ case "compaction_start":
483
+ case "auto_retry_start":
484
+ case "summarization_retry_scheduled":
485
+ case "summarization_retry_attempt_start":
486
+ this.retrying = true;
487
+ this.cancelEviction();
488
+ break;
489
+ case "compaction_end":
490
+ this.retrying = event.willRetry;
491
+ break;
492
+ case "auto_retry_end":
493
+ case "summarization_retry_finished":
494
+ this.retrying = false;
495
+ break;
496
+ case "agent_settled":
497
+ this.retrying = false;
498
+ this.activeTools.clear();
499
+ this.lastSettledAt = Date.now();
500
+ this.scheduleEviction();
501
+ break;
502
+ }
503
+ if (event.type === "queue_update" && this.suppressQueueUpdates) {
504
+ this.suppressedQueueUpdate = event;
505
+ return;
506
+ }
507
+ this.events.publish({ type: "agent_event", event });
508
+ }
509
+ isWorking() {
510
+ return (this.session.isStreaming ||
511
+ this.session.isCompacting ||
512
+ this.activePrompt !== undefined ||
513
+ this.activeTools.size > 0 ||
514
+ this.queuedMessages > 0 ||
515
+ (this.session.extensionUI?.snapshot().pending.length ?? 0) > 0 ||
516
+ this.retrying);
517
+ }
518
+ scheduleEviction() {
519
+ this.cancelEviction();
520
+ if (!this.isEvictable()) {
521
+ return;
522
+ }
523
+ const elapsed = Date.now() - this.lastSettledAt;
524
+ const delay = Math.max(0, this.idleTimeoutMs - elapsed);
525
+ this.idleTimer = setTimeout(() => {
526
+ this.idleTimer = undefined;
527
+ if (this.isEvictable()) {
528
+ this.onIdle(this);
529
+ }
530
+ }, delay);
531
+ this.idleTimer.unref();
532
+ }
533
+ cancelEviction() {
534
+ if (!this.idleTimer) {
535
+ return;
536
+ }
537
+ clearTimeout(this.idleTimer);
538
+ this.idleTimer = undefined;
539
+ }
540
+ assertOpen() {
541
+ if (this.disposed) {
542
+ throw new AppError("runtime_closed", "The session runtime is closed", 409);
543
+ }
544
+ }
545
+ }
546
+ export class RuntimeRegistry {
547
+ repository;
548
+ options;
549
+ handles = new Map();
550
+ starts = new Map();
551
+ deletions = new Map();
552
+ closing = false;
553
+ constructor(repository, options) {
554
+ this.repository = repository;
555
+ this.options = {
556
+ idleTimeoutMs: options.idleTimeoutMs ?? 10 * 60_000,
557
+ eventCapacity: options.eventCapacity ?? 512,
558
+ createSession: options.createSession,
559
+ };
560
+ }
561
+ async get(id) {
562
+ if (this.closing) {
563
+ throw new AppError("server_stopping", "The server is stopping", 503);
564
+ }
565
+ const deleting = this.deletions.get(id);
566
+ if (deleting) {
567
+ await deleting;
568
+ }
569
+ if (this.closing) {
570
+ throw new AppError("server_stopping", "The server is stopping", 503);
571
+ }
572
+ const existing = this.handles.get(id);
573
+ if (existing) {
574
+ return existing;
575
+ }
576
+ const starting = this.starts.get(id);
577
+ if (starting) {
578
+ return starting;
579
+ }
580
+ const start = this.start(id);
581
+ this.starts.set(id, start);
582
+ try {
583
+ return await start;
584
+ }
585
+ finally {
586
+ if (this.starts.get(id) === start) {
587
+ this.starts.delete(id);
588
+ }
589
+ }
590
+ }
591
+ getActive(id) {
592
+ return this.handles.get(id);
593
+ }
594
+ activeIds() {
595
+ return [...this.handles.keys()];
596
+ }
597
+ async delete(id) {
598
+ if (this.closing) {
599
+ throw new AppError("server_stopping", "The server is stopping", 503);
600
+ }
601
+ const existing = this.deletions.get(id);
602
+ if (existing) {
603
+ return existing;
604
+ }
605
+ const deletion = this.deleteSession(id);
606
+ this.deletions.set(id, deletion);
607
+ try {
608
+ await deletion;
609
+ }
610
+ finally {
611
+ if (this.deletions.get(id) === deletion) {
612
+ this.deletions.delete(id);
613
+ }
614
+ }
615
+ }
616
+ async close() {
617
+ if (this.closing) {
618
+ return;
619
+ }
620
+ this.closing = true;
621
+ await Promise.allSettled([...this.starts.values()]);
622
+ await Promise.allSettled([...this.deletions.values()]);
623
+ const handles = [...this.handles.values()];
624
+ this.handles.clear();
625
+ await Promise.allSettled(handles.map((handle) => handle.dispose()));
626
+ }
627
+ async deleteSession(id) {
628
+ const starting = this.starts.get(id);
629
+ if (starting) {
630
+ await starting.catch(() => undefined);
631
+ }
632
+ const handle = this.handles.get(id);
633
+ if (handle?.isWorking()) {
634
+ throw new AppError("session_busy", "The session is busy and cannot be deleted", 409);
635
+ }
636
+ if (handle) {
637
+ for (const [key, candidate] of this.handles) {
638
+ if (candidate === handle) {
639
+ this.handles.delete(key);
640
+ }
641
+ }
642
+ await handle.dispose();
643
+ }
644
+ await this.repository.delete(id);
645
+ }
646
+ async start(id) {
647
+ const manager = await this.repository.openManager(id);
648
+ const session = await this.options.createSession(manager);
649
+ if (this.closing) {
650
+ await session.dispose();
651
+ throw new AppError("server_stopping", "The server is stopping", 503);
652
+ }
653
+ const handle = new RuntimeHandle(session, manager.getCwd(), this.options.idleTimeoutMs, this.options.eventCapacity, (idleHandle) => void this.evict(idleHandle), (changedHandle, previousId, sessionId, replacementManager) => {
654
+ this.rekey(changedHandle, previousId, sessionId, replacementManager);
655
+ });
656
+ this.handles.set(id, handle);
657
+ return handle;
658
+ }
659
+ rekey(handle, previousId, sessionId, manager) {
660
+ this.repository.adoptManager(manager);
661
+ if (this.handles.get(previousId) === handle) {
662
+ this.handles.delete(previousId);
663
+ }
664
+ const existing = this.handles.get(sessionId);
665
+ if (existing && existing !== handle) {
666
+ void existing.dispose();
667
+ }
668
+ this.handles.set(sessionId, handle);
669
+ }
670
+ async evict(handle) {
671
+ if (!handle.isEvictable()) {
672
+ return;
673
+ }
674
+ for (const [id, candidate] of this.handles) {
675
+ if (candidate === handle) {
676
+ this.handles.delete(id);
677
+ }
678
+ }
679
+ await handle.dispose();
680
+ }
681
+ }
682
+ export async function createRuntimeRegistry(repository) {
683
+ const modelRuntime = await ModelRuntime.create();
684
+ const agentDir = getAgentDir();
685
+ const createSession = async (manager) => {
686
+ const createRuntime = async ({ cwd, sessionManager, sessionStartEvent, }) => {
687
+ const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
688
+ const created = await createAgentSessionFromServices({
689
+ services,
690
+ sessionManager,
691
+ ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
692
+ });
693
+ return { ...created, services, diagnostics: services.diagnostics };
694
+ };
695
+ const runtime = await createAgentSessionRuntime(createRuntime, {
696
+ cwd: manager.getCwd(),
697
+ agentDir,
698
+ sessionManager: manager,
699
+ });
700
+ return new SdkRuntimeSession(runtime);
701
+ };
702
+ return new RuntimeRegistry(repository, { createSession });
703
+ }