@hachej/boring-agent 0.1.78 → 0.1.80

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.
@@ -1,2794 +0,0 @@
1
- import {
2
- AgentFilesystemRequiredError,
3
- AgentNotImplementedError,
4
- sessionStreamPath
5
- } from "./chunk-ZUEITFIJ.js";
6
- import {
7
- extractToolUiMetadata,
8
- sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
9
- } from "./chunk-ORURYKNY.js";
10
- import {
11
- createLogger
12
- } from "./chunk-AJZHR626.js";
13
- import {
14
- ErrorCode
15
- } from "./chunk-6AEK34XU.js";
16
-
17
- // src/server/pi-chat/metering.ts
18
- var meteringLogger = createLogger("pi-chat-metering");
19
- var defaultMeteringErrorLogger = (message, error) => {
20
- meteringLogger.warn(message, { error });
21
- };
22
- function promptRunId(sessionId, clientNonce) {
23
- return `pi-run:${sessionId}:prompt:${clientNonce}`;
24
- }
25
- function followUpRunId(sessionId, clientNonce, clientSeq) {
26
- return `pi-run:${sessionId}:followup:${clientNonce}:${clientSeq}`;
27
- }
28
- function followUpMatches(run, selector) {
29
- const fu = run.followUp;
30
- if (!fu) return false;
31
- if (selector.clientNonce !== void 0) return fu.clientNonce === selector.clientNonce;
32
- if (selector.clientSeq !== void 0) return fu.clientSeq === selector.clientSeq;
33
- return false;
34
- }
35
- function takeQueuedFollowUp(state, selector) {
36
- const index = state.queued.findIndex((run) => followUpMatches(run, selector));
37
- if (index < 0) return void 0;
38
- return state.queued.splice(index, 1)[0];
39
- }
40
- function isRecord(value) {
41
- return typeof value === "object" && value !== null && !Array.isArray(value);
42
- }
43
- function readTokenCount(value) {
44
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
45
- }
46
- function normalizeMeteringUsage(value) {
47
- if (!isRecord(value)) return void 0;
48
- const cost = isRecord(value.cost) ? value.cost : {};
49
- return {
50
- input: readTokenCount(value.input),
51
- output: readTokenCount(value.output),
52
- cacheRead: readTokenCount(value.cacheRead),
53
- cacheWrite: readTokenCount(value.cacheWrite),
54
- cost: {
55
- input: readTokenCount(cost.input),
56
- output: readTokenCount(cost.output),
57
- cacheRead: readTokenCount(cost.cacheRead),
58
- cacheWrite: readTokenCount(cost.cacheWrite),
59
- total: readTokenCount(cost.total)
60
- }
61
- };
62
- }
63
- var PiChatMeteringCoordinator = class {
64
- sessions = /* @__PURE__ */ new Map();
65
- inflightOps = /* @__PURE__ */ new Set();
66
- runInstances = 0;
67
- sink;
68
- logError;
69
- constructor(sink, logError) {
70
- this.sink = sink;
71
- this.logError = logError ?? defaultMeteringErrorLogger;
72
- }
73
- /** Reserve a prompt run. Throws (fail closed) when the sink rejects. */
74
- async reservePrompt(input) {
75
- const stateKey = input.stateKey ?? input.sessionId;
76
- const state = this.sessionState(stateKey);
77
- const runId = promptRunId(input.sessionId, input.clientNonce);
78
- const existing = this.findRun(state, runId);
79
- if (existing) {
80
- await existing.reservation;
81
- return "duplicate";
82
- }
83
- const run = this.createRun(input, "prompt", runId);
84
- state.pendingPrompts.push(run);
85
- return this.materializeReservation(state, run, input, stateKey);
86
- }
87
- /** Reserve a follow-up run. Throws (fail closed) when the sink rejects.
88
- * Returns 'duplicate' when this selector already has a tracked/consumed run. */
89
- async reserveFollowUp(input) {
90
- const stateKey = input.stateKey ?? input.sessionId;
91
- const state = this.sessionState(stateKey);
92
- const runId = followUpRunId(input.sessionId, input.clientNonce, input.clientSeq);
93
- const existing = this.findRun(state, runId);
94
- if (existing) {
95
- await existing.reservation;
96
- return "duplicate";
97
- }
98
- if (state.consumedFollowUpNonces.has(input.clientNonce)) return "duplicate";
99
- const run = this.createRun(input, "followup", runId);
100
- run.followUp = { clientNonce: input.clientNonce, clientSeq: input.clientSeq };
101
- state.queued.push(run);
102
- return this.materializeReservation(state, run, input, stateKey);
103
- }
104
- /**
105
- * Acquire the reservation for a freshly-registered run. The reserve is put on
106
- * the run's ops chain so a concurrent release/settle is ordered strictly
107
- * after the reservation row exists. Returns 'cancelled' (skip execution) when
108
- * a concurrent stop/interrupt/delete terminated the run while the reserve was
109
- * in flight; throws (fail closed) when the sink rejects.
110
- */
111
- async materializeReservation(state, run, input, stateKey) {
112
- run.reservation = this.applyReservation(run, input);
113
- const reserveOp = run.reservation.catch(() => {
114
- });
115
- run.ops = reserveOp;
116
- this.inflightOps.add(reserveOp);
117
- void reserveOp.finally(() => this.inflightOps.delete(reserveOp));
118
- try {
119
- await run.reservation;
120
- } catch (err) {
121
- this.removeReservingRun(state, run, stateKey);
122
- throw err;
123
- }
124
- return run.terminal ? "cancelled" : "created";
125
- }
126
- /** True while a non-terminal prompt run exists for this nonce (accept →
127
- * agent-end). Used to suppress duplicate-nonce execution for the full run
128
- * lifetime, not just until the user message-start is consumed. */
129
- hasPromptRun(sessionId, clientNonce) {
130
- const state = this.sessions.get(sessionId);
131
- if (!state) return false;
132
- const run = this.findRun(state, promptRunId(sessionId, clientNonce));
133
- return run !== void 0 && !run.terminal;
134
- }
135
- /** True while a non-terminal follow-up run exists for this selector (queued
136
- * or consumed-and-active). Used to suppress duplicate follow-up enqueues. */
137
- hasFollowUpRun(sessionId, selector) {
138
- const state = this.sessions.get(sessionId);
139
- if (!state) return false;
140
- if (selector.clientNonce !== void 0 && state.consumedFollowUpNonces.has(selector.clientNonce)) return true;
141
- if (state.queued.some((run) => followUpMatches(run, selector))) return true;
142
- const active = state.active;
143
- return active !== void 0 && !active.terminal && active.followUp !== void 0 && followUpMatches(active, selector);
144
- }
145
- /** The accepted prompt failed before/without running (sync throw or run rejection). */
146
- failPromptRun(sessionId, clientNonce, stateKey = sessionId) {
147
- const state = this.sessions.get(stateKey);
148
- if (!state) return;
149
- const runId = promptRunId(sessionId, clientNonce);
150
- const pendingIndex = state.pendingPrompts.findIndex((run) => run.scope.runId === runId);
151
- if (pendingIndex >= 0) {
152
- const [run] = state.pendingPrompts.splice(pendingIndex, 1);
153
- if (run) this.finishRun(run, "error");
154
- } else if (state.active?.scope.runId === runId) {
155
- this.finishRun(state.active, "error");
156
- state.active = void 0;
157
- }
158
- this.pruneSession(stateKey, state);
159
- }
160
- /** A queued follow-up was rejected by the adapter before being queued. */
161
- failFollowUpRun(sessionId, selector) {
162
- const state = this.sessions.get(sessionId);
163
- if (!state) return;
164
- const run = takeQueuedFollowUp(state, selector);
165
- if (!run) return;
166
- this.release(run, "run-rejected");
167
- this.pruneSession(sessionId, state);
168
- }
169
- /**
170
- * A queued follow-up is being re-posted as a plain prompt (interrupt
171
- * fallback for runtimes without continueQueuedFollowUp). No
172
- * `followup-consumed` event will arrive, so bind its reservation to the
173
- * next agent-start instead.
174
- */
175
- promoteQueuedToPrompt(sessionId, selector) {
176
- const state = this.sessions.get(sessionId);
177
- if (!state) return;
178
- const run = takeQueuedFollowUp(state, selector);
179
- if (!run) return;
180
- state.pendingPrompts.push(run);
181
- }
182
- /**
183
- * A promoted-to-prompt follow-up failed before agent-start (the fallback
184
- * repost rejected). Release its reservation instead of stranding it in
185
- * pendingPrompts, where a later agent-start would otherwise misattribute
186
- * usage to it.
187
- */
188
- failPromotedFollowUp(sessionId, selector, stateKey = sessionId) {
189
- const state = this.sessions.get(stateKey);
190
- if (!state || selector.clientNonce === void 0 || selector.clientSeq === void 0) return;
191
- const runId = followUpRunId(sessionId, selector.clientNonce, selector.clientSeq);
192
- const index = state.pendingPrompts.findIndex((run2) => run2.scope.runId === runId);
193
- if (index < 0) return;
194
- const [run] = state.pendingPrompts.splice(index, 1);
195
- if (run) this.release(run, "run-rejected");
196
- this.pruneSession(stateKey, state);
197
- }
198
- /**
199
- * Release prompt runs reserved but not yet bound to an agent-start —
200
- * e.g. a stop/interrupt landing in the window between acceptance and the
201
- * native agent_start. Without this they would sit `active` in the store
202
- * until their TTL, holding the user's balance. No charge.
203
- */
204
- releasePending(sessionId) {
205
- const state = this.sessions.get(sessionId);
206
- if (!state) return;
207
- for (const run of state.pendingPrompts) this.release(run, "cancelled");
208
- state.pendingPrompts = [];
209
- this.pruneSession(sessionId, state);
210
- }
211
- /** Queue cleared via selector or entirely; release affected reservations. */
212
- releaseQueued(sessionId, selector) {
213
- const state = this.sessions.get(sessionId);
214
- if (!state) return;
215
- if (selector) {
216
- const run = takeQueuedFollowUp(state, selector);
217
- if (run) this.release(run, "queue-cleared");
218
- } else {
219
- for (const run of state.queued) this.release(run, "queue-cleared");
220
- state.queued = [];
221
- }
222
- this.pruneSession(sessionId, state);
223
- }
224
- /** Session deleted: tear down every non-terminal run without charging. */
225
- releaseSession(sessionId) {
226
- const state = this.sessions.get(sessionId);
227
- if (!state) return;
228
- for (const run of state.queued) this.release(run, "cancelled");
229
- for (const run of state.pendingPrompts) this.release(run, "cancelled");
230
- if (state.active) this.finishRun(state.active, "aborted");
231
- this.sessions.delete(sessionId);
232
- }
233
- /**
234
- * Feed one native adapter event and its mapped PiChatEvents through the
235
- * correlation state machine. Must be called after the mapped events were
236
- * published (the mapper assigns turn ids during mapping).
237
- */
238
- observe(sessionId, nativeEvent, mappedEvents) {
239
- const state = this.sessions.get(sessionId);
240
- if (!state) return;
241
- for (const event of mappedEvents) {
242
- switch (event.type) {
243
- case "agent-start": {
244
- let next = state.pendingPrompts.shift();
245
- while (next && next.terminal) next = state.pendingPrompts.shift();
246
- if (next) {
247
- if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
248
- state.active = next;
249
- }
250
- break;
251
- }
252
- case "message-start": {
253
- if (event.role === "user" && event.clientSeq !== void 0) {
254
- this.consumeFollowUp(state, event);
255
- }
256
- break;
257
- }
258
- case "followup-consumed": {
259
- this.consumeFollowUp(state, event);
260
- break;
261
- }
262
- case "auto-retry-start": {
263
- if (state.active) {
264
- state.active.recordedMessageIds.clear();
265
- state.active.lastIdlessUsageKey = void 0;
266
- }
267
- break;
268
- }
269
- case "agent-end": {
270
- this.harvestAgentEndUsage(state, nativeEvent);
271
- if (isRecord(nativeEvent) && nativeEvent.willRetry === true) break;
272
- if (state.active && !state.active.terminal) this.finishRun(state.active, event.status);
273
- state.active = void 0;
274
- break;
275
- }
276
- default:
277
- break;
278
- }
279
- }
280
- this.observeMessageEndUsage(state, nativeEvent);
281
- this.pruneSession(sessionId, state);
282
- }
283
- /** Promote a queued follow-up to the active run, settling the previous one. */
284
- consumeFollowUp(state, selector) {
285
- const run = takeQueuedFollowUp(state, selector);
286
- if (!run) return;
287
- if (run.followUp?.clientNonce) state.consumedFollowUpNonces.add(run.followUp.clientNonce);
288
- if (state.active && !state.active.terminal) this.finishRun(state.active, "ok");
289
- state.active = run;
290
- }
291
- /** Test/diagnostic hook: resolves after every queued sink call settles. */
292
- async flush() {
293
- while (this.inflightOps.size > 0) {
294
- await Promise.all([...this.inflightOps]);
295
- }
296
- }
297
- observeMessageEndUsage(state, nativeEvent) {
298
- if (!isRecord(nativeEvent) || nativeEvent.type !== "message_end") return;
299
- this.recordAssistantUsage(state, nativeEvent.message);
300
- }
301
- /**
302
- * Some failure/abort paths never emit message_end; the final assistant
303
- * message (and its usage) only rides on agent_end's messages array. Runs
304
- * with already-recorded usage dedupe via recordedMessageIds.
305
- */
306
- harvestAgentEndUsage(state, nativeEvent) {
307
- if (!isRecord(nativeEvent) || !Array.isArray(nativeEvent.messages)) return;
308
- for (let index = nativeEvent.messages.length - 1; index >= 0; index -= 1) {
309
- const message = nativeEvent.messages[index];
310
- if (!isRecord(message) || message.role !== "assistant") continue;
311
- this.recordAssistantUsage(state, message, { isAgentEndFinal: true });
312
- return;
313
- }
314
- }
315
- recordAssistantUsage(state, message, opts = {}) {
316
- if (!isRecord(message) || message.role !== "assistant") return;
317
- const usage = normalizeMeteringUsage(message.usage);
318
- if (!usage) return;
319
- const run = state.active ?? state.pendingPrompts[0];
320
- if (!run || run.terminal) {
321
- this.logError("assistant usage arrived with no reserved run; usage not metered", { messageRole: "assistant" });
322
- return;
323
- }
324
- const messageId = typeof message.id === "string" && message.id.length > 0 ? message.id : void 0;
325
- if (messageId) {
326
- if (run.recordedMessageIds.has(messageId)) return;
327
- run.recordedMessageIds.add(messageId);
328
- } else {
329
- const stopReason2 = typeof message.stopReason === "string" ? message.stopReason : "";
330
- const signature = `sig:${usage.input}:${usage.output}:${usage.cacheRead}:${usage.cacheWrite}:${usage.cost.total}:${stopReason2}`;
331
- if (opts.isAgentEndFinal && signature === run.lastIdlessUsageKey) return;
332
- run.lastIdlessUsageKey = signature;
333
- }
334
- run.usageCount += 1;
335
- const model = typeof message.model === "string" && message.model.length > 0 ? message.model : void 0;
336
- const provider = typeof message.provider === "string" && message.provider.length > 0 ? message.provider : void 0;
337
- const stopReason = typeof message.stopReason === "string" ? message.stopReason : void 0;
338
- const usageId = messageId ? `pi-usage:${run.scope.sessionId}:message:${messageId}` : run.reservationId ? `pi-usage:reservation:${run.reservationId}:${run.usageCount}` : `pi-usage:${run.scope.runId}:${run.instanceId}:${run.usageCount}`;
339
- this.enqueue(
340
- run,
341
- async () => {
342
- try {
343
- const result = await this.sink.recordUsage({
344
- ...run.scope,
345
- reservationId: run.reservationId,
346
- usageId,
347
- messageId,
348
- model: model || provider ? { provider, id: model } : void 0,
349
- usage,
350
- stopReason
351
- });
352
- if (result.billedMicros > 0) run.billableUsageCount += 1;
353
- } catch (error) {
354
- const couldBill = usage.input + usage.output + usage.cacheRead + usage.cacheWrite > 0 || usage.cost.total > 0;
355
- if (couldBill) run.usageWriteFailed = true;
356
- throw error;
357
- }
358
- },
359
- "recordUsage failed"
360
- );
361
- }
362
- /** Build a run synchronously (no await) so it can be registered before the
363
- * reserve sink call, closing the concurrent-duplicate race. */
364
- createRun(input, kind, runId) {
365
- return {
366
- scope: {
367
- workspaceId: input.workspaceId,
368
- userId: input.userId,
369
- userEmail: input.userEmail,
370
- userEmailVerified: input.userEmailVerified,
371
- sessionId: input.sessionId,
372
- runId,
373
- source: "pi-chat"
374
- },
375
- kind,
376
- reservationId: void 0,
377
- instanceId: this.runInstances += 1,
378
- usageCount: 0,
379
- billableUsageCount: 0,
380
- recordedMessageIds: /* @__PURE__ */ new Set(),
381
- lastIdlessUsageKey: void 0,
382
- usageWriteFailed: false,
383
- reservation: Promise.resolve(),
384
- terminal: false,
385
- ops: Promise.resolve()
386
- };
387
- }
388
- /** Acquire the host reservation; throws (fail closed) on a rejecting sink. */
389
- async applyReservation(run, input) {
390
- const result = await this.sink.reserveRun({
391
- ...run.scope,
392
- kind: run.kind,
393
- message: input.message,
394
- model: input.model
395
- });
396
- run.reservationId = result?.reservationId;
397
- }
398
- /** Remove a still-reserving run whose reservation failed, from either list. */
399
- removeReservingRun(state, run, sessionId) {
400
- const pendingIndex = state.pendingPrompts.indexOf(run);
401
- if (pendingIndex >= 0) state.pendingPrompts.splice(pendingIndex, 1);
402
- const queuedIndex = state.queued.indexOf(run);
403
- if (queuedIndex >= 0) state.queued.splice(queuedIndex, 1);
404
- this.pruneSession(sessionId, state);
405
- }
406
- findRun(state, runId) {
407
- if (state.active && !state.active.terminal && state.active.scope.runId === runId) return state.active;
408
- const pending = state.pendingPrompts.find((run) => run.scope.runId === runId);
409
- if (pending) return pending;
410
- return state.queued.find((run) => run.scope.runId === runId);
411
- }
412
- finishRun(run, status) {
413
- if (run.terminal) return;
414
- run.terminal = true;
415
- this.enqueue(
416
- run,
417
- () => {
418
- if (run.usageWriteFailed) {
419
- return this.sink.releaseRun({ ...run.scope, reservationId: run.reservationId, reason: "usage-write-failed" });
420
- }
421
- if (run.billableUsageCount > 0) {
422
- return this.sink.settleRun({ ...run.scope, reservationId: run.reservationId, status });
423
- }
424
- const didPaidWork = status === "ok";
425
- if (didPaidWork) {
426
- return this.sink.releaseRun({ ...run.scope, reservationId: run.reservationId, reason: "fallback-hold-charge" });
427
- }
428
- return this.sink.releaseRun({
429
- ...run.scope,
430
- reservationId: run.reservationId,
431
- reason: status === "error" ? "error-before-usage" : "cancelled"
432
- });
433
- },
434
- "finishRun failed"
435
- );
436
- }
437
- release(run, reason) {
438
- if (run.terminal) return;
439
- run.terminal = true;
440
- this.enqueue(
441
- run,
442
- () => this.sink.releaseRun({ ...run.scope, reservationId: run.reservationId, reason }),
443
- "releaseRun failed"
444
- );
445
- }
446
- enqueue(run, op, failureMessage) {
447
- const chained = run.ops.then(op).catch((error) => {
448
- this.logError(`${failureMessage} (run ${run.scope.runId})`, error);
449
- });
450
- run.ops = chained;
451
- this.inflightOps.add(chained);
452
- void chained.finally(() => this.inflightOps.delete(chained));
453
- }
454
- sessionState(sessionId) {
455
- let state = this.sessions.get(sessionId);
456
- if (!state) {
457
- state = { pendingPrompts: [], queued: [], consumedFollowUpNonces: /* @__PURE__ */ new Set() };
458
- this.sessions.set(sessionId, state);
459
- }
460
- return state;
461
- }
462
- pruneSession(sessionId, state) {
463
- if (!state.active && state.pendingPrompts.length === 0 && state.queued.length === 0 && state.consumedFollowUpNonces.size === 0) {
464
- this.sessions.delete(sessionId);
465
- }
466
- }
467
- };
468
-
469
- // src/server/events/eventStreamStore.ts
470
- var COMPONENT_PAD = 16;
471
- var ZERO_COMPONENT = "0".repeat(COMPONENT_PAD);
472
- var EventStreamStoreError = class extends Error {
473
- code = "INTERNAL_ERROR";
474
- constructor(message) {
475
- super(message);
476
- this.name = "EventStreamStoreError";
477
- }
478
- };
479
- function formatOffset(seq) {
480
- if (seq === -1) return "-1";
481
- if (!Number.isInteger(seq) || seq < -1) {
482
- throw new EventStreamStoreError(`Invalid event stream sequence: ${seq}.`);
483
- }
484
- return `${ZERO_COMPONENT}_${String(seq).padStart(COMPONENT_PAD, "0")}`;
485
- }
486
- function parseOffset(offset) {
487
- if (offset === "-1") return -1;
488
- const match = /^\d+_(\d+)$/.exec(offset);
489
- const sequence = match?.[1];
490
- if (!sequence) {
491
- throw new EventStreamStoreError(`Invalid event stream offset: "${offset}".`);
492
- }
493
- return parseInt(sequence, 10);
494
- }
495
-
496
- // src/server/pi-chat/piChatHistory.ts
497
- function isRecord2(value) {
498
- return typeof value === "object" && value !== null && !Array.isArray(value);
499
- }
500
- function optionalString(value) {
501
- return typeof value === "string" && value.length > 0 ? value : void 0;
502
- }
503
- function messageRole(message) {
504
- return optionalString(message.role) ?? optionalString(message.type);
505
- }
506
- function messageTimestamp(message) {
507
- const timestamp = message.timestamp;
508
- if (typeof timestamp === "number" && Number.isFinite(timestamp)) return new Date(timestamp).toISOString();
509
- return optionalString(timestamp);
510
- }
511
- function entryMessageId(entry, message, index, sessionId) {
512
- return optionalString(entry.id) ?? optionalString(message.id) ?? `pi:${sessionId}:message:${index}:${messageRole(message) ?? "unknown"}:${messageTimestamp(message) ?? "no-ts"}`;
513
- }
514
- function entryPiEntryId(entry, message) {
515
- return optionalString(entry.id) ?? optionalString(message.id);
516
- }
517
- function textFromContent(content) {
518
- if (typeof content === "string") return content;
519
- if (!Array.isArray(content)) return void 0;
520
- const text = content.map((part) => {
521
- if (!isRecord2(part)) return "";
522
- if (part.type === "text" && typeof part.text === "string") return part.text;
523
- return "";
524
- }).join("");
525
- return text.length > 0 ? text : void 0;
526
- }
527
- function filePartsFromContent(content, messageId) {
528
- if (!Array.isArray(content)) return [];
529
- return content.flatMap((part, index) => {
530
- if (!isRecord2(part) || part.type !== "image") return [];
531
- const mediaType = optionalString(part.mimeType);
532
- return [
533
- {
534
- type: "file",
535
- id: `${messageId}:file:${index}`,
536
- ...optionalString(part.filename) ? { filename: optionalString(part.filename) } : {},
537
- ...mediaType ? { mediaType } : {},
538
- ...imagePartUrl(part, mediaType) ? { url: imagePartUrl(part, mediaType) } : {},
539
- ...optionalString(part.path) ? { path: optionalString(part.path) } : {}
540
- }
541
- ];
542
- });
543
- }
544
- function imagePartUrl(part, mediaType) {
545
- const existing = optionalString(part.url);
546
- if (existing) return existing;
547
- const data = optionalString(part.data);
548
- if (!data) return void 0;
549
- if (data.startsWith("data:")) return data;
550
- return `data:${mediaType ?? "application/octet-stream"};base64,${data}`;
551
- }
552
- function userParts(message, messageId) {
553
- const parts = [];
554
- const text = textFromContent(message.content);
555
- if (text !== void 0) parts.push({ type: "text", id: `${messageId}:text:0`, text });
556
- parts.push(...filePartsFromContent(message.content, messageId));
557
- return parts;
558
- }
559
- function systemParts(message, messageId) {
560
- return [{ type: "text", id: `${messageId}:text:0`, text: textFromContent(message.content) ?? optionalString(message.text) ?? "" }];
561
- }
562
- function toolInputFromCall(part) {
563
- if ("arguments" in part) return part.arguments;
564
- if ("input" in part) return part.input;
565
- if ("args" in part) return part.args;
566
- return void 0;
567
- }
568
- function assistantParts(message, messageId) {
569
- const content = message.content;
570
- if (typeof content === "string") return [{ type: "text", id: `${messageId}:text:0`, text: content }];
571
- if (!Array.isArray(content)) return [];
572
- return content.flatMap((part, index) => {
573
- if (!isRecord2(part)) return [];
574
- if (part.type === "text" && typeof part.text === "string") {
575
- return [{ type: "text", id: `${messageId}:text:${index}`, text: part.text }];
576
- }
577
- if ((part.type === "thinking" || part.type === "reasoning") && (typeof part.thinking === "string" || typeof part.text === "string")) {
578
- return [
579
- {
580
- type: "reasoning",
581
- id: optionalString(part.id) ?? `${messageId}:reasoning:${index}`,
582
- text: typeof part.thinking === "string" ? part.thinking : String(part.text ?? ""),
583
- state: "done"
584
- }
585
- ];
586
- }
587
- if (part.type === "toolCall") {
588
- const toolCallId = optionalString(part.id) ?? `${messageId}:tool:${index}`;
589
- const state = part.state === "output-error" ? "output-error" : part.state === "output-available" ? "output-available" : "input-available";
590
- return [
591
- {
592
- type: "tool-call",
593
- id: toolCallId,
594
- toolName: optionalString(part.name) ?? optionalString(part.toolName) ?? "unknown",
595
- input: toolInputFromCall(part),
596
- state,
597
- output: part.output,
598
- errorText: optionalString(part.errorText),
599
- ui: extractToolUiMetadata({ details: { ui: part.ui } })
600
- }
601
- ];
602
- }
603
- return [];
604
- });
605
- }
606
- function updateToolResult(messages, message) {
607
- const toolCallId = optionalString(message.toolCallId);
608
- if (!toolCallId) return;
609
- for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
610
- const candidate = messages[messageIndex];
611
- if (!candidate || candidate.role !== "assistant") continue;
612
- const partIndex = candidate.parts.findIndex((part) => part.type === "tool-call" && part.id === toolCallId);
613
- if (partIndex < 0) continue;
614
- const current = candidate.parts[partIndex];
615
- if (current?.type !== "tool-call") return;
616
- const nextPart = {
617
- ...current,
618
- output: toolResultOutput(message),
619
- state: message.isError === true ? "output-error" : "output-available",
620
- errorText: message.isError === true ? toolResultErrorText(message) : current.errorText,
621
- ui: current.ui ?? extractToolUiMetadata({ details: { ui: isRecord2(message.details) ? message.details.ui : void 0 } })
622
- };
623
- candidate.parts = [...candidate.parts.slice(0, partIndex), nextPart, ...candidate.parts.slice(partIndex + 1)];
624
- return;
625
- }
626
- }
627
- function toolResultOutput(message) {
628
- if ("details" in message && message.details !== void 0) {
629
- return {
630
- content: message.content,
631
- details: message.details
632
- };
633
- }
634
- return message.content;
635
- }
636
- function toolResultErrorText(message) {
637
- const text = textFromContent(message.content);
638
- return text ?? optionalString(message.errorText);
639
- }
640
- function buildPiChatHistory(entries, options) {
641
- const messages = [];
642
- entries.forEach((rawEntry, index) => {
643
- const entry = isRecord2(rawEntry) && "message" in rawEntry ? { id: optionalString(rawEntry.id), message: rawEntry.message } : { message: rawEntry };
644
- if (!isRecord2(entry.message)) return;
645
- const role = messageRole(entry.message);
646
- if (role === "toolResult") {
647
- updateToolResult(messages, entry.message);
648
- return;
649
- }
650
- const id = entryMessageId(entry, entry.message, index, options.sessionId);
651
- const piEntryId = entryPiEntryId(entry, entry.message);
652
- const base = {
653
- id,
654
- createdAt: messageTimestamp(entry.message),
655
- piEntryId,
656
- turnId: optionalString(entry.message.turnId) ?? options.messageTurnIds?.get(id) ?? options.turnId
657
- };
658
- if (role === "user") {
659
- messages.push({ ...base, role: "user", status: "done", parts: userParts(entry.message, id) });
660
- return;
661
- }
662
- if (role === "assistant") {
663
- const status = entry.message.stopReason === "aborted" ? "aborted" : entry.message.stopReason === "error" ? "error" : "done";
664
- messages.push({ ...base, role: "assistant", status, parts: assistantParts(entry.message, id) });
665
- return;
666
- }
667
- if (role === "system") {
668
- messages.push({ ...base, role: "system", status: "done", parts: systemParts(entry.message, id) });
669
- return;
670
- }
671
- if (role === "custom" && entry.message.display !== false) {
672
- messages.push({ ...base, role: "system", status: "done", parts: systemParts(entry.message, id) });
673
- }
674
- });
675
- return messages;
676
- }
677
-
678
- // src/server/pi-chat/piChatSnapshot.ts
679
- function queueId(sessionId, index, text) {
680
- return `queue:${sessionId}:followup:${index}:${stableTextHash(text)}`;
681
- }
682
- function stableTextHash(text) {
683
- let hash = 0;
684
- for (let index = 0; index < text.length; index += 1) {
685
- hash = hash * 31 + text.charCodeAt(index) >>> 0;
686
- }
687
- return hash.toString(36);
688
- }
689
- function buildPiChatQueuedFollowUps(sessionId, followUpMessages) {
690
- return followUpMessages.map((displayText, index) => ({
691
- id: queueId(sessionId, index, displayText),
692
- kind: "followup",
693
- displayText
694
- }));
695
- }
696
- function statusFromSnapshot(snapshot, error) {
697
- if (error) return "error";
698
- if (snapshot.isStreaming) return "streaming";
699
- return "idle";
700
- }
701
- function errorFromSnapshot(snapshot) {
702
- const state = snapshot.state;
703
- if (typeof state !== "object" || state === null || !("errorMessage" in state)) return void 0;
704
- const message = state.errorMessage;
705
- if (typeof message !== "string" || message.length === 0) return void 0;
706
- return {
707
- code: ErrorCode.enum.INTERNAL_ERROR,
708
- message,
709
- retryable: false
710
- };
711
- }
712
- function buildPiChatSnapshot(adapter, options) {
713
- const piSnapshot = adapter.readSnapshot();
714
- const sessionId = options.sessionId ?? piSnapshot.sessionId;
715
- const error = options.error ?? errorFromSnapshot(piSnapshot);
716
- const status = options.status ?? statusFromSnapshot(piSnapshot, error);
717
- return {
718
- protocolVersion: 1,
719
- sessionId,
720
- seq: options.seq,
721
- status,
722
- activeTurnId: options.activeTurnId,
723
- messages: buildPiChatHistory(piSnapshot.messages, {
724
- sessionId,
725
- messageTurnIds: options.messageTurnIds
726
- }),
727
- queue: { followUps: buildPiChatQueuedFollowUps(sessionId, piSnapshot.followUpMessages) },
728
- followUpMode: "one-at-a-time",
729
- error
730
- };
731
- }
732
-
733
- // src/server/pi-chat/piChatEvents.ts
734
- var FILE_CHANGE_OPS = /* @__PURE__ */ new Set(["write", "edit", "unlink", "rename", "mkdir"]);
735
- var PiChatEventMapper = class {
736
- seq;
737
- sessionId;
738
- activeTurnId;
739
- activeAssistantMessageId;
740
- pendingUserStarts = [];
741
- toolCallMessageIds = /* @__PURE__ */ new Map();
742
- endedMessageIds = /* @__PURE__ */ new Set();
743
- endedAssistantTurnIds = /* @__PURE__ */ new Set();
744
- errorEmittedTurnIds = /* @__PURE__ */ new Set();
745
- constructor(options) {
746
- this.sessionId = options.sessionId;
747
- this.seq = Math.max(0, Math.floor(options.initialSeq ?? 0));
748
- }
749
- get latestSeq() {
750
- return this.seq;
751
- }
752
- mapSynthetic(event) {
753
- return this.event(event);
754
- }
755
- map(event) {
756
- if (!isRecord3(event) || typeof event.type !== "string") return [];
757
- switch (event.type) {
758
- case "agent_start": {
759
- const turnId = optionalString2(event.turnId) ?? this.createTurnId();
760
- this.activeTurnId = turnId;
761
- this.pendingUserStarts.length = 0;
762
- this.endedMessageIds.clear();
763
- this.endedAssistantTurnIds.clear();
764
- this.errorEmittedTurnIds.clear();
765
- return [this.event({ type: "agent-start", turnId })];
766
- }
767
- case "agent_end": {
768
- const turnId = this.activeTurnId ?? this.createTurnId();
769
- const status = agentEndStatus(event);
770
- const mapped = [
771
- ...this.mapAgentEndFinalAssistant(event, turnId),
772
- ...this.mapAgentEndError(event, turnId, status),
773
- // willRetry marks a non-terminal end (auto-retry coming) so once-per-settle
774
- // consumers can ignore it; mirrors mapAgentEndError's own willRetry gate.
775
- this.event({ type: "agent-end", turnId, status, ...event.willRetry === true ? { willRetry: true } : {} })
776
- ];
777
- this.activeAssistantMessageId = void 0;
778
- this.toolCallMessageIds.clear();
779
- if (status !== "error") this.activeTurnId = void 0;
780
- return mapped;
781
- }
782
- case "message_start":
783
- return this.mapMessageStart(event);
784
- case "message_update":
785
- return this.mapMessageUpdate(event);
786
- case "message_end":
787
- return this.mapMessageEnd(event);
788
- case "tool_execution_end":
789
- return this.mapToolExecutionEnd(event);
790
- case "queue_update":
791
- return [
792
- this.event({
793
- type: "queue-updated",
794
- queue: { followUps: buildPiChatQueuedFollowUps(this.sessionId, readStringArray(event.followUp)) }
795
- })
796
- ];
797
- case "auto_retry_start":
798
- this.resetDedupForRetry();
799
- return [
800
- this.event({
801
- type: "auto-retry-start",
802
- attempt: numberValue(event.attempt) ?? 0,
803
- maxAttempts: numberValue(event.maxAttempts) ?? 0,
804
- delayMs: numberValue(event.delayMs) ?? 0,
805
- errorMessage: optionalString2(event.errorMessage) ?? ""
806
- })
807
- ];
808
- case "auto_retry_end":
809
- return [
810
- this.event({
811
- type: "auto-retry-end",
812
- success: event.success === true,
813
- attempt: numberValue(event.attempt) ?? 0,
814
- finalError: optionalString2(event.finalError)
815
- })
816
- ];
817
- case "ui_command":
818
- case "ui-command":
819
- return [this.event({ type: "ui-command", command: event.command ?? event.data ?? {}, displayOnly: true })];
820
- default:
821
- return [];
822
- }
823
- }
824
- createTurnId() {
825
- return `turn:${this.sessionId}:${this.seq + 1}`;
826
- }
827
- resetDedupForRetry() {
828
- this.endedMessageIds.clear();
829
- if (this.activeTurnId) this.endedAssistantTurnIds.delete(this.activeTurnId);
830
- else this.endedAssistantTurnIds.clear();
831
- this.activeAssistantMessageId = void 0;
832
- this.toolCallMessageIds.clear();
833
- }
834
- event(event) {
835
- this.seq += 1;
836
- return { ...event, seq: this.seq };
837
- }
838
- mapMessageStart(event) {
839
- if (!isRecord3(event.message)) return [];
840
- const message = event.message;
841
- const rawRole = messageRole2(message);
842
- if (rawRole !== "user" && rawRole !== "assistant") return [];
843
- const role = rawRole;
844
- const messageId = messageIdFrom(message) ?? fallbackMessageId(this.sessionId, role, this.seq + 1);
845
- const text = textFromContent2(message.content) ?? optionalString2(message.text);
846
- if (role === "assistant") this.activeAssistantMessageId = messageId;
847
- if (role === "user" && messageIdFrom(message) === void 0) this.pendingUserStarts.push({ messageId, text });
848
- const clientNonce = optionalString2(message.clientNonce);
849
- const clientSeq = numberValue(message.clientSeq);
850
- const start = this.event({
851
- type: "message-start",
852
- messageId,
853
- role,
854
- clientNonce,
855
- clientSeq,
856
- createdAt: messageTimestamp2(message),
857
- text,
858
- files: filePartsFromContent2(message.content, messageId)
859
- });
860
- if (role === "user" && clientNonce !== void 0 && clientSeq !== void 0) {
861
- return [start, this.event({ type: "followup-consumed", clientNonce, clientSeq, messageId })];
862
- }
863
- return [start];
864
- }
865
- mapMessageUpdate(event) {
866
- if (!isRecord3(event.assistantMessageEvent)) return [];
867
- const assistantEvent = event.assistantMessageEvent;
868
- const messageId = this.messageUpdateId(event, assistantEvent);
869
- const partId = partIdFromAssistantEvent(assistantEvent);
870
- switch (assistantEvent.type) {
871
- case "text_delta":
872
- return [this.event({ type: "message-delta", messageId, partId, kind: "text", delta: stringValue(assistantEvent.delta) })];
873
- case "text_end":
874
- return [this.event({ type: "message-part-end", messageId, partId, kind: "text", text: stringValue(assistantEvent.content) })];
875
- case "thinking_delta":
876
- return [this.event({ type: "message-delta", messageId, partId, kind: "reasoning", delta: stringValue(assistantEvent.delta) })];
877
- case "thinking_end":
878
- return [this.event({ type: "message-part-end", messageId, partId, kind: "reasoning", text: stringValue(assistantEvent.content) })];
879
- case "toolcall_end":
880
- return this.mapToolCallEnd(messageId, assistantEvent);
881
- case "done": {
882
- const usage = isRecord3(assistantEvent.message) ? assistantEvent.message.usage : void 0;
883
- return usage === void 0 ? [] : [this.event({ type: "usage", usage })];
884
- }
885
- case "error": {
886
- const errorMessage = errorMessageFromAssistantError(assistantEvent);
887
- if (this.activeTurnId) this.errorEmittedTurnIds.add(this.activeTurnId);
888
- return [
889
- this.event({
890
- type: "error",
891
- turnId: this.activeTurnId,
892
- retryable: false,
893
- error: {
894
- code: assistantEvent.reason === "aborted" ? ErrorCode.enum.ABORTED : ErrorCode.enum.INTERNAL_ERROR,
895
- message: errorMessage,
896
- retryable: false
897
- }
898
- })
899
- ];
900
- }
901
- default:
902
- return [];
903
- }
904
- }
905
- messageUpdateId(event, assistantEvent) {
906
- const fromEventMessage = isRecord3(event.message) ? messageIdFrom(event.message) : void 0;
907
- const fromPartial = isRecord3(assistantEvent.partial) ? messageIdFrom(assistantEvent.partial) : void 0;
908
- const fromFinal = isRecord3(assistantEvent.message) ? messageIdFrom(assistantEvent.message) : void 0;
909
- const id = fromEventMessage ?? fromPartial ?? fromFinal ?? this.activeAssistantMessageId ?? fallbackMessageId(this.sessionId, "assistant", this.seq + 1);
910
- this.activeAssistantMessageId = id;
911
- return id;
912
- }
913
- mapToolCallEnd(messageId, assistantEvent) {
914
- if (!isRecord3(assistantEvent.toolCall)) return [];
915
- const toolCall = assistantEvent.toolCall;
916
- const toolCallId = optionalString2(toolCall.id) ?? `${messageId}:tool:${partIdFromAssistantEvent(assistantEvent)}`;
917
- this.toolCallMessageIds.set(toolCallId, messageId);
918
- return [
919
- this.event({
920
- type: "tool-call",
921
- messageId,
922
- toolCallId,
923
- toolName: optionalString2(toolCall.name) ?? "unknown",
924
- input: toolCall.arguments ?? {},
925
- ui: sanitizeToolUiMetadata(toolCall.ui)
926
- })
927
- ];
928
- }
929
- mapMessageEnd(event) {
930
- if (!isRecord3(event.message)) return [];
931
- const final = buildPiChatHistory([event.message], { sessionId: this.sessionId, turnId: this.activeTurnId })[0];
932
- if (!final) return [];
933
- const messageId = this.finalMessageId(final);
934
- const canonicalFinal = final.id === messageId ? final : rewriteMessageId(final, messageId);
935
- if (this.endedMessageIds.has(messageId)) return [];
936
- this.endedMessageIds.add(messageId);
937
- if (canonicalFinal.role === "assistant") {
938
- if (this.activeTurnId && isTerminalAssistantFinal(canonicalFinal)) this.endedAssistantTurnIds.add(this.activeTurnId);
939
- this.activeAssistantMessageId = void 0;
940
- }
941
- return [this.event({ type: "message-end", messageId, final: canonicalFinal })];
942
- }
943
- mapAgentEndFinalAssistant(event, turnId) {
944
- if (this.endedAssistantTurnIds.has(turnId)) return [];
945
- const messages = readRecordArray(event.messages);
946
- const lastAssistantIndex = findLastIndex(messages, (message) => messageRole2(message) === "assistant");
947
- if (lastAssistantIndex < 0) return [];
948
- if (isContentlessTerminalAssistant(messages[lastAssistantIndex])) return [];
949
- const finalAssistant = this.buildAgentEndFinalAssistant(messages, lastAssistantIndex, turnId);
950
- if (!finalAssistant) return [];
951
- if (this.endedMessageIds.has(finalAssistant.id)) return [];
952
- this.endedMessageIds.add(finalAssistant.id);
953
- this.activeAssistantMessageId = void 0;
954
- return [this.event({ type: "message-end", messageId: finalAssistant.id, final: finalAssistant })];
955
- }
956
- /**
957
- * Surfaces a turn failure as an explicit `error` event. Some failures (e.g.
958
- * "No API key for provider") never produce an assistant `error` stream
959
- * event — the message only rides on agent_end's final assistant entry
960
- * (stopReason 'error' + errorMessage), which is also where the snapshot
961
- * reads it from. Without this, live clients see an empty assistant message
962
- * and an error agent-end but never the error text. Skipped when pi will
963
- * auto-retry (auto_retry_* events own that flow) or when an error event was
964
- * already emitted for this turn.
965
- */
966
- mapAgentEndError(event, turnId, status) {
967
- if (status !== "error" || event.willRetry === true) return [];
968
- if (this.errorEmittedTurnIds.has(turnId)) return [];
969
- this.errorEmittedTurnIds.add(turnId);
970
- return [
971
- this.event({
972
- type: "error",
973
- turnId,
974
- retryable: false,
975
- error: {
976
- code: ErrorCode.enum.INTERNAL_ERROR,
977
- message: agentEndErrorMessage(event),
978
- retryable: false
979
- }
980
- })
981
- ];
982
- }
983
- finalMessageId(final) {
984
- if (final.role === "assistant" && this.activeAssistantMessageId) return this.activeAssistantMessageId;
985
- if (final.role !== "user") return final.id;
986
- const text = messageText(final);
987
- const exactIndex = this.pendingUserStarts.findIndex((start) => start.text === text);
988
- const fallbackIndex = exactIndex >= 0 ? exactIndex : this.pendingUserStarts.findIndex((start) => start.text === void 0 || text === void 0);
989
- if (fallbackIndex < 0) return final.id;
990
- const [matched] = this.pendingUserStarts.splice(fallbackIndex, 1);
991
- return matched?.messageId ?? final.id;
992
- }
993
- buildAgentEndFinalAssistant(messages, lastAssistantIndex, turnId) {
994
- const rawAssistant = messages[lastAssistantIndex];
995
- if (!rawAssistant) return void 0;
996
- if (messageIdFrom(rawAssistant) === void 0) {
997
- return buildPiChatHistory([{ id: this.activeAssistantMessageId ?? fallbackAgentEndAssistantId(this.sessionId, turnId), message: rawAssistant }], {
998
- sessionId: this.sessionId,
999
- turnId
1000
- })[0];
1001
- }
1002
- const assistants = buildPiChatHistory(messages, { sessionId: this.sessionId, turnId }).filter((message) => message.role === "assistant");
1003
- return assistants[assistants.length - 1];
1004
- }
1005
- mapToolExecutionEnd(event) {
1006
- const toolCallId = optionalString2(event.toolCallId);
1007
- if (!toolCallId) return [];
1008
- const messageId = this.toolCallMessageIds.get(toolCallId) ?? this.activeAssistantMessageId ?? fallbackMessageId(this.sessionId, "assistant", this.seq + 1);
1009
- const result = event.result;
1010
- const mapped = [];
1011
- for (const fileChange of extractFileChanges(isRecord3(result) ? result.details : void 0)) {
1012
- mapped.push(this.event({ type: "file-changed", path: fileChange.path, changeType: fileChange.op }));
1013
- }
1014
- mapped.push(
1015
- this.event({
1016
- type: "tool-result",
1017
- messageId,
1018
- toolCallId,
1019
- output: result ?? {},
1020
- isError: event.isError === true,
1021
- errorText: event.isError === true ? toolErrorText(result) : void 0,
1022
- ui: sanitizeToolUiMetadata(isRecord3(result) && isRecord3(result.details) ? result.details.ui : void 0)
1023
- })
1024
- );
1025
- return mapped;
1026
- }
1027
- };
1028
- function isRecord3(value) {
1029
- return typeof value === "object" && value !== null && !Array.isArray(value);
1030
- }
1031
- function optionalString2(value) {
1032
- return typeof value === "string" && value.length > 0 ? value : void 0;
1033
- }
1034
- function stringValue(value) {
1035
- return typeof value === "string" ? value : "";
1036
- }
1037
- function numberValue(value) {
1038
- return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
1039
- }
1040
- function readStringArray(value) {
1041
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1042
- }
1043
- function readRecordArray(value) {
1044
- return Array.isArray(value) ? value.filter((item) => isRecord3(item)) : [];
1045
- }
1046
- function findLastIndex(items, predicate) {
1047
- for (let index = items.length - 1; index >= 0; index -= 1) {
1048
- if (predicate(items[index])) return index;
1049
- }
1050
- return -1;
1051
- }
1052
- function messageRole2(message) {
1053
- return optionalString2(message.role) ?? optionalString2(message.type);
1054
- }
1055
- function messageIdFrom(message) {
1056
- return optionalString2(message.id) ?? optionalString2(message.messageId);
1057
- }
1058
- function messageTimestamp2(message) {
1059
- const timestamp = message.timestamp;
1060
- if (typeof timestamp === "number" && Number.isFinite(timestamp)) return new Date(timestamp).toISOString();
1061
- return optionalString2(timestamp);
1062
- }
1063
- function fallbackMessageId(sessionId, role, seq) {
1064
- return `pi:${sessionId}:event:${seq}:${role}`;
1065
- }
1066
- function fallbackAgentEndAssistantId(sessionId, turnId) {
1067
- return `pi:${sessionId}:turn:${turnId}:assistant`;
1068
- }
1069
- function textFromContent2(content) {
1070
- if (typeof content === "string") return content;
1071
- if (!Array.isArray(content)) return void 0;
1072
- const text = content.map((part) => isRecord3(part) && part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
1073
- return text.length > 0 ? text : void 0;
1074
- }
1075
- function isContentlessTerminalAssistant(message) {
1076
- if (message.stopReason !== "aborted" && message.stopReason !== "error") return false;
1077
- return !hasDisplayableAssistantContent(message.content);
1078
- }
1079
- function hasDisplayableAssistantContent(content) {
1080
- if (typeof content === "string") return content.length > 0;
1081
- if (!Array.isArray(content)) return false;
1082
- return content.some((part) => {
1083
- if (!isRecord3(part)) return false;
1084
- if (part.type === "toolCall") return true;
1085
- if (part.type === "text") return typeof part.text === "string" && part.text.length > 0;
1086
- if (part.type === "thinking" || part.type === "reasoning") {
1087
- return typeof part.thinking === "string" && part.thinking.length > 0 || typeof part.text === "string" && part.text.length > 0;
1088
- }
1089
- return false;
1090
- });
1091
- }
1092
- function filePartsFromContent2(content, messageId) {
1093
- if (!Array.isArray(content)) return [];
1094
- return content.flatMap((part, index) => {
1095
- if (!isRecord3(part) || part.type !== "image") return [];
1096
- const mediaType = optionalString2(part.mimeType);
1097
- const url = imagePartUrl2(part, mediaType);
1098
- return [{
1099
- type: "file",
1100
- id: `${messageId}:file:${index}`,
1101
- ...optionalString2(part.filename) ? { filename: optionalString2(part.filename) } : {},
1102
- ...mediaType ? { mediaType } : {},
1103
- ...url ? { url } : {},
1104
- ...optionalString2(part.path) ? { path: optionalString2(part.path) } : {}
1105
- }];
1106
- });
1107
- }
1108
- function imagePartUrl2(part, mediaType) {
1109
- const existing = optionalString2(part.url);
1110
- if (existing) return existing;
1111
- const data = optionalString2(part.data);
1112
- if (!data) return void 0;
1113
- if (data.startsWith("data:")) return data;
1114
- return `data:${mediaType ?? "application/octet-stream"};base64,${data}`;
1115
- }
1116
- function messageText(message) {
1117
- const text = message.parts.filter((part) => part.type === "text").map((part) => part.text).join("");
1118
- return text.length > 0 ? text : void 0;
1119
- }
1120
- function isTerminalAssistantFinal(message) {
1121
- return message.role === "assistant" && !message.parts.some((part) => part.type === "tool-call");
1122
- }
1123
- function rewriteMessageId(message, id) {
1124
- return {
1125
- ...message,
1126
- id,
1127
- parts: message.parts.map((part, index) => {
1128
- if (part.type === "text") return { ...part, id: rewritePartId(part.id, message.id, id, `text:${index}`) };
1129
- if (part.type === "file") return { ...part, id: rewritePartId(part.id, message.id, id, `file:${index}`) };
1130
- if (part.type === "reasoning") return { ...part, id: rewritePartId(part.id, message.id, id, `reasoning:${index}`) };
1131
- return part;
1132
- })
1133
- };
1134
- }
1135
- function rewritePartId(partId, previousMessageId, nextMessageId, fallbackSuffix) {
1136
- if (partId?.startsWith(previousMessageId)) return `${nextMessageId}${partId.slice(previousMessageId.length)}`;
1137
- return `${nextMessageId}:${fallbackSuffix}`;
1138
- }
1139
- function partIdFromAssistantEvent(assistantEvent) {
1140
- const contentIndex = assistantEvent.contentIndex;
1141
- return typeof contentIndex === "number" && Number.isInteger(contentIndex) && contentIndex >= 0 ? String(contentIndex) : "0";
1142
- }
1143
- function agentEndStatus(event) {
1144
- const messages = Array.isArray(event.messages) ? event.messages : [];
1145
- const lastAssistant = [...messages].reverse().find((message) => isRecord3(message) && message.role === "assistant");
1146
- if (lastAssistant?.stopReason === "aborted") return "aborted";
1147
- if (lastAssistant?.stopReason === "error" || event.willRetry === true) return "error";
1148
- return "ok";
1149
- }
1150
- function agentEndErrorMessage(event) {
1151
- const messages = Array.isArray(event.messages) ? event.messages : [];
1152
- const lastAssistant = [...messages].reverse().find((message) => isRecord3(message) && message.role === "assistant");
1153
- if (typeof lastAssistant?.errorMessage === "string" && lastAssistant.errorMessage.length > 0) return lastAssistant.errorMessage;
1154
- return "Agent turn failed.";
1155
- }
1156
- function errorMessageFromAssistantError(assistantEvent) {
1157
- if (isRecord3(assistantEvent.error) && typeof assistantEvent.error.errorMessage === "string") return assistantEvent.error.errorMessage;
1158
- return assistantEvent.reason === "aborted" ? "Aborted" : "Unknown error";
1159
- }
1160
- function normalizeFileChangeEntry(value) {
1161
- if (!isRecord3(value)) return null;
1162
- const op = value.op;
1163
- const path = value.path;
1164
- if (typeof op !== "string" || !FILE_CHANGE_OPS.has(op)) return null;
1165
- if (typeof path !== "string" || path.length === 0) return null;
1166
- return { op, path };
1167
- }
1168
- function extractFileChanges(details) {
1169
- if (!isRecord3(details)) return [];
1170
- const entries = details.fileChanges;
1171
- if (Array.isArray(entries)) return entries.map(normalizeFileChangeEntry).filter((entry) => entry !== null);
1172
- const single = normalizeFileChangeEntry(details.fileChange);
1173
- return single ? [single] : [];
1174
- }
1175
- function toolErrorText(result) {
1176
- if (!isRecord3(result)) return void 0;
1177
- const content = result.content;
1178
- const text = textFromContent2(content);
1179
- return text ?? optionalString2(result.errorText);
1180
- }
1181
-
1182
- // src/server/pi-chat/piChatReplayBuffer.ts
1183
- var PI_CHAT_REPLAY_GAP = "replay_gap";
1184
- var PI_CHAT_CURSOR_AHEAD = "cursor_ahead";
1185
- var PiChatReplayBuffer = class {
1186
- maxEvents;
1187
- events = [];
1188
- latest = 0;
1189
- subscribers = /* @__PURE__ */ new Set();
1190
- constructor(options = {}) {
1191
- this.maxEvents = Math.max(1, Math.floor(options.maxEvents ?? 1e3));
1192
- this.latest = Math.max(0, Math.floor(options.initialLatestSeq ?? 0));
1193
- }
1194
- get latestSeq() {
1195
- return this.latest;
1196
- }
1197
- get minReplaySeq() {
1198
- return this.events[0]?.seq ?? this.latest + 1;
1199
- }
1200
- get size() {
1201
- return this.events.length;
1202
- }
1203
- publish(event) {
1204
- if (event.seq <= this.latest) {
1205
- throw new Error(`Pi chat event seq must increase monotonically: latest=${this.latest} next=${event.seq}`);
1206
- }
1207
- this.latest = event.seq;
1208
- this.events.push(event);
1209
- if (this.events.length > this.maxEvents) {
1210
- this.events.splice(0, this.events.length - this.maxEvents);
1211
- }
1212
- for (const subscriber of this.subscribers) {
1213
- subscriber(event);
1214
- }
1215
- }
1216
- validateCursor(cursor) {
1217
- const normalizedCursor = Math.floor(cursor);
1218
- if (!Number.isFinite(cursor) || normalizedCursor !== cursor || cursor < 0) {
1219
- return { type: PI_CHAT_REPLAY_GAP, latestSeq: this.latest, minReplaySeq: this.minReplaySeq };
1220
- }
1221
- if (cursor > this.latest) {
1222
- return { type: PI_CHAT_CURSOR_AHEAD, latestSeq: this.latest, minReplaySeq: this.minReplaySeq };
1223
- }
1224
- if (this.events.length === 0 && this.latest > 0 && cursor < this.latest) {
1225
- return { type: PI_CHAT_REPLAY_GAP, latestSeq: this.latest, minReplaySeq: this.minReplaySeq };
1226
- }
1227
- if (this.events.length > 0 && cursor < this.minReplaySeq - 1) {
1228
- return { type: PI_CHAT_REPLAY_GAP, latestSeq: this.latest, minReplaySeq: this.minReplaySeq };
1229
- }
1230
- return null;
1231
- }
1232
- replay(cursor, upperSeq = this.latest) {
1233
- const error = this.validateCursor(cursor);
1234
- if (error) return error;
1235
- const events = this.events.filter((event) => event.seq > cursor && event.seq <= upperSeq);
1236
- return { type: "ok", events, latestSeq: this.latest, minReplaySeq: this.minReplaySeq };
1237
- }
1238
- subscribe(cursor, subscriber, options = {}) {
1239
- const error = this.validateCursor(cursor);
1240
- if (error) return error;
1241
- const replayUpperBound = this.latest;
1242
- const pendingLiveEvents = [];
1243
- let replaying = true;
1244
- const liveSubscriber = (event) => {
1245
- if (replaying) {
1246
- pendingLiveEvents.push(event);
1247
- return;
1248
- }
1249
- subscriber(event);
1250
- };
1251
- this.subscribers.add(liveSubscriber);
1252
- let active = true;
1253
- const unsubscribe = () => {
1254
- if (!active) return;
1255
- active = false;
1256
- this.subscribers.delete(liveSubscriber);
1257
- };
1258
- options.beforeReplay?.();
1259
- const replay = this.replay(cursor, replayUpperBound);
1260
- if (replay.type !== "ok") {
1261
- unsubscribe();
1262
- return replay;
1263
- }
1264
- for (const event of replay.events) {
1265
- subscriber(event);
1266
- }
1267
- replaying = false;
1268
- for (const event of pendingLiveEvents) {
1269
- subscriber(event);
1270
- }
1271
- return {
1272
- type: "ok",
1273
- unsubscribe,
1274
- replayed: replay.events,
1275
- latestSeq: this.latest,
1276
- minReplaySeq: this.minReplaySeq
1277
- };
1278
- }
1279
- clearSubscribers() {
1280
- this.subscribers.clear();
1281
- }
1282
- };
1283
-
1284
- // src/server/pi-chat/piChatMessageMetadataReconciler.ts
1285
- var PiChatMessageMetadataReconciler = class {
1286
- promptMetadata = /* @__PURE__ */ new Map();
1287
- followUpMetadata = /* @__PURE__ */ new Map();
1288
- consumingFollowUpMetadata = /* @__PURE__ */ new Map();
1289
- clearSession(sessionId) {
1290
- this.promptMetadata.delete(sessionId);
1291
- this.followUpMetadata.delete(sessionId);
1292
- this.consumingFollowUpMetadata.delete(sessionId);
1293
- }
1294
- clearFollowUps(sessionId) {
1295
- this.followUpMetadata.delete(sessionId);
1296
- }
1297
- recordPrompt(sessionId, payload) {
1298
- const metadata = this.promptMetadata.get(sessionId) ?? [];
1299
- const displayText = payload.displayMessage ?? payload.message;
1300
- const files = filePartsFromPromptPayload(payload);
1301
- metadata.push({
1302
- displayText,
1303
- ...displayText !== payload.message ? { serverText: payload.message } : {},
1304
- clientNonce: payload.clientNonce,
1305
- recordedAt: Date.now(),
1306
- ...files ? { files } : {}
1307
- });
1308
- this.promptMetadata.set(sessionId, metadata);
1309
- }
1310
- removePrompt(sessionId, selector) {
1311
- const metadata = this.promptMetadata.get(sessionId);
1312
- if (!metadata) return;
1313
- const index = selector.clientNonce ? metadata.findIndex((entry) => entry.clientNonce === selector.clientNonce) : metadata.findIndex((entry) => matchesMetadataText(entry, selector.displayText));
1314
- if (index < 0) return;
1315
- metadata.splice(index, 1);
1316
- if (metadata.length > 0) this.promptMetadata.set(sessionId, metadata);
1317
- else this.promptMetadata.delete(sessionId);
1318
- }
1319
- hasPrompt(sessionId, selector) {
1320
- const metadata = this.promptMetadata.get(sessionId);
1321
- if (!metadata) return false;
1322
- if (selector.clientNonce) return metadata.some((entry) => entry.clientNonce === selector.clientNonce);
1323
- return metadata.some((entry) => matchesMetadataText(entry, selector.displayText));
1324
- }
1325
- recordFollowUp(sessionId, payload) {
1326
- const metadata = this.followUpMetadata.get(sessionId) ?? [];
1327
- const displayText = payload.displayMessage ?? payload.message;
1328
- metadata.push({
1329
- displayText,
1330
- ...displayText !== payload.message ? { serverText: payload.message } : {},
1331
- clientNonce: payload.clientNonce,
1332
- clientSeq: payload.clientSeq,
1333
- recordedAt: Date.now()
1334
- });
1335
- this.followUpMetadata.set(sessionId, metadata);
1336
- }
1337
- removeFollowUp(sessionId, selector) {
1338
- this.removeFollowUpFrom(this.consumingFollowUpMetadata, sessionId, selector);
1339
- this.removeFollowUpFrom(this.followUpMetadata, sessionId, selector);
1340
- }
1341
- recordConsumingFollowUp(sessionId, followUp, serverText) {
1342
- const metadata = this.consumingFollowUpMetadata.get(sessionId) ?? [];
1343
- metadata.push({
1344
- displayText: followUp.displayText,
1345
- ...serverText && serverText !== followUp.displayText ? { serverText } : {},
1346
- clientNonce: followUp.clientNonce,
1347
- clientSeq: followUp.clientSeq,
1348
- recordedAt: Date.now()
1349
- });
1350
- this.consumingFollowUpMetadata.set(sessionId, metadata);
1351
- }
1352
- findFollowUpForQueueItem(sessionId, followUp) {
1353
- const entries = [
1354
- ...this.consumingFollowUpMetadata.get(sessionId) ?? [],
1355
- ...this.followUpMetadata.get(sessionId) ?? []
1356
- ];
1357
- return entries.find((entry) => matchesFollowUpSelector(entry, followUp)) ?? entries.find((entry) => matchesMetadataText(entry, followUp.displayText));
1358
- }
1359
- enrichSnapshot(sessionId, snapshot) {
1360
- const messages = this.enrichSnapshotMessages(sessionId, snapshot.messages);
1361
- const followUps = this.enrichQueuedFollowUps(sessionId, snapshot.queue.followUps);
1362
- this.syncFromQueue(sessionId, followUps);
1363
- return { ...snapshot, messages, queue: { followUps } };
1364
- }
1365
- enrichEvent(sessionId, event) {
1366
- if (event.type === "queue-updated") {
1367
- const followUps = this.enrichQueuedFollowUps(sessionId, event.queue.followUps);
1368
- this.syncFromQueue(sessionId, followUps);
1369
- return { ...event, queue: { followUps } };
1370
- }
1371
- if (event.type === "message-start" && event.role === "user" && !hasFollowUpSelector(event)) {
1372
- const promptMetadata = this.findPrompt(sessionId, event.text);
1373
- if (promptMetadata) return withMessageStartMetadata(event, promptMetadata);
1374
- const metadata = this.findFollowUp(sessionId, event.text);
1375
- if (metadata) return withMessageStartMetadata(event, metadata);
1376
- }
1377
- if (event.type === "message-end" && event.final.role === "user" && !hasMessageSelector(event.final)) {
1378
- const text = messageText2(event.final);
1379
- const promptMetadata = this.findPrompt(sessionId, text);
1380
- if (promptMetadata) return { ...event, final: withMessageSelector(event.final, promptMetadata) };
1381
- const metadata = this.findFollowUp(sessionId, text);
1382
- if (metadata) return { ...event, final: withMessageSelector(event.final, metadata) };
1383
- }
1384
- return event;
1385
- }
1386
- consumeEvent(sessionId, event) {
1387
- this.consumeFollowUpFromEvent(sessionId, event);
1388
- this.consumePromptFromEvent(sessionId, event);
1389
- }
1390
- enrichQueuedFollowUps(sessionId, followUps) {
1391
- const metadata = this.followUpMetadata.get(sessionId) ?? [];
1392
- if (metadata.length === 0) return followUps;
1393
- const followUpTextCounts = countTexts(followUps.map((followUp) => followUp.displayText));
1394
- const metadataTextCounts = countMetadataTexts(metadata);
1395
- return followUps.map((followUp, index) => {
1396
- if (hasFollowUpSelector(followUp)) return followUp;
1397
- const indexed = metadata[index];
1398
- if (matchesMetadataText(indexed, followUp.displayText)) return withQueuedSelector(followUp, indexed);
1399
- if ((followUpTextCounts.get(followUp.displayText) ?? 0) !== 1) return followUp;
1400
- if ((metadataTextCounts.get(followUp.displayText) ?? 0) !== 1) return followUp;
1401
- const matched = metadata.find((entry) => matchesMetadataText(entry, followUp.displayText));
1402
- return matched ? withQueuedSelector(followUp, matched) : followUp;
1403
- });
1404
- }
1405
- syncFromTexts(sessionId, followUps) {
1406
- if (followUps.length === 0) {
1407
- this.followUpMetadata.delete(sessionId);
1408
- return;
1409
- }
1410
- const metadata = this.enrichQueuedFollowUps(sessionId, buildPiChatQueuedFollowUps(sessionId, followUps));
1411
- this.syncFromQueue(sessionId, metadata);
1412
- }
1413
- enrichSnapshotMessages(sessionId, messages) {
1414
- const followUpMetadata = [
1415
- ...this.consumingFollowUpMetadata.get(sessionId) ?? [],
1416
- ...this.followUpMetadata.get(sessionId) ?? []
1417
- ];
1418
- const promptMetadata = [...this.promptMetadata.get(sessionId) ?? []];
1419
- if (followUpMetadata.length === 0 && promptMetadata.length === 0) return messages;
1420
- return messages.map((message) => {
1421
- if (message.role !== "user" || hasMessageSelector(message)) return message;
1422
- const text = messageText2(message);
1423
- const prompt = takeFirstMessageMetadata(promptMetadata, text, message);
1424
- if (prompt) return withMessageSelector(message, prompt);
1425
- const followUp = takeFirstMessageMetadata(followUpMetadata, text, message);
1426
- return followUp ? withMessageSelector(message, followUp) : message;
1427
- });
1428
- }
1429
- removeFollowUpFrom(source, sessionId, selector) {
1430
- const metadata = source.get(sessionId);
1431
- if (!metadata) return;
1432
- const index = metadata.findIndex((entry) => matchesFollowUpSelector(entry, selector));
1433
- if (index < 0) return;
1434
- metadata.splice(index, 1);
1435
- if (metadata.length > 0) source.set(sessionId, metadata);
1436
- else source.delete(sessionId);
1437
- }
1438
- consumeFollowUpFromEvent(sessionId, event) {
1439
- if (event.type === "followup-consumed") {
1440
- this.removeFollowUp(sessionId, event);
1441
- return;
1442
- }
1443
- if (event.type !== "message-start" || event.role !== "user") return;
1444
- if (hasFollowUpSelector(event)) {
1445
- this.removeFollowUp(sessionId, event);
1446
- return;
1447
- }
1448
- this.removeFirstFollowUpByText(sessionId, event.text);
1449
- }
1450
- findFollowUp(sessionId, text) {
1451
- if (!text) return void 0;
1452
- const consuming = this.consumingFollowUpMetadata.get(sessionId)?.find((entry) => matchesMetadataText(entry, text));
1453
- if (consuming) return consuming;
1454
- return this.followUpMetadata.get(sessionId)?.find((entry) => matchesMetadataText(entry, text));
1455
- }
1456
- findPrompt(sessionId, text) {
1457
- if (!text) return void 0;
1458
- return this.promptMetadata.get(sessionId)?.find((entry) => matchesMetadataText(entry, text));
1459
- }
1460
- consumePromptFromEvent(sessionId, event) {
1461
- if (event.type === "message-start" && event.role === "user") {
1462
- if (event.clientSeq !== void 0) return;
1463
- const prompt = event.clientNonce ? this.promptMetadata.get(sessionId)?.find((entry) => entry.clientNonce === event.clientNonce) : this.findPrompt(sessionId, event.text);
1464
- if (prompt?.serverText) return;
1465
- if (event.clientNonce) {
1466
- this.removePrompt(sessionId, { clientNonce: event.clientNonce });
1467
- return;
1468
- }
1469
- this.removePrompt(sessionId, { displayText: event.text });
1470
- return;
1471
- }
1472
- if (event.type !== "message-end" || event.final.role !== "user") return;
1473
- if (event.final.clientSeq !== void 0) return;
1474
- if (event.final.clientNonce) {
1475
- this.removePrompt(sessionId, { clientNonce: event.final.clientNonce });
1476
- return;
1477
- }
1478
- this.removePrompt(sessionId, { displayText: messageText2(event.final) });
1479
- }
1480
- removeFirstFollowUpByText(sessionId, text) {
1481
- if (!text) return;
1482
- if (this.removeFirstFollowUpByTextFrom(this.consumingFollowUpMetadata, sessionId, text)) return;
1483
- this.removeFirstFollowUpByTextFrom(this.followUpMetadata, sessionId, text);
1484
- }
1485
- removeFirstFollowUpByTextFrom(source, sessionId, text) {
1486
- const metadata = source.get(sessionId);
1487
- if (!metadata) return false;
1488
- const index = metadata.findIndex((entry) => matchesMetadataText(entry, text));
1489
- if (index < 0) return false;
1490
- metadata.splice(index, 1);
1491
- if (metadata.length > 0) source.set(sessionId, metadata);
1492
- else source.delete(sessionId);
1493
- return true;
1494
- }
1495
- syncFromQueue(sessionId, followUps) {
1496
- const previous = this.followUpMetadata.get(sessionId) ?? [];
1497
- const recordedAt = Date.now();
1498
- const metadata = followUps.filter(hasFollowUpSelector).map((followUp) => {
1499
- const previousMatch = previous.find((entry) => matchesFollowUpSelector(entry, followUp));
1500
- return {
1501
- displayText: followUp.displayText,
1502
- ...previousMatch?.serverText ? { serverText: previousMatch.serverText } : {},
1503
- clientNonce: followUp.clientNonce,
1504
- clientSeq: followUp.clientSeq,
1505
- recordedAt: previousMatch?.recordedAt ?? recordedAt
1506
- };
1507
- });
1508
- if (metadata.length > 0) this.followUpMetadata.set(sessionId, metadata);
1509
- else this.followUpMetadata.delete(sessionId);
1510
- }
1511
- };
1512
- function hasFollowUpSelector(payload) {
1513
- return Boolean(payload.clientNonce) || payload.clientSeq !== void 0;
1514
- }
1515
- function followUpSelector(followUp) {
1516
- return {
1517
- ...followUp.clientNonce ? { clientNonce: followUp.clientNonce } : {},
1518
- ...followUp.clientSeq !== void 0 ? { clientSeq: followUp.clientSeq } : {}
1519
- };
1520
- }
1521
- function hasMessageSelector(message) {
1522
- return Boolean(message.clientNonce) || message.clientSeq !== void 0;
1523
- }
1524
- function matchesFollowUpSelector(entry, selector) {
1525
- if (selector.clientNonce) return entry.clientNonce === selector.clientNonce;
1526
- return selector.clientSeq !== void 0 && entry.clientSeq === selector.clientSeq;
1527
- }
1528
- function matchesMetadataText(entry, text) {
1529
- if (!entry || !text) return false;
1530
- return entry.displayText === text || entry.serverText === text;
1531
- }
1532
- function withMessageStartMetadata(event, metadata) {
1533
- return {
1534
- ...event,
1535
- text: metadata.displayText,
1536
- ...metadata.clientNonce ? { clientNonce: metadata.clientNonce } : {},
1537
- ...metadata.clientSeq !== void 0 ? { clientSeq: metadata.clientSeq } : {},
1538
- ...metadata.files && metadata.files.length > 0 ? { files: metadata.files } : {}
1539
- };
1540
- }
1541
- function withMessageSelector(message, metadata) {
1542
- return {
1543
- ...withMessageFiles(withMessageDisplayText(message, metadata.displayText), metadata.files),
1544
- ...metadata.clientNonce ? { clientNonce: metadata.clientNonce } : {},
1545
- ...metadata.clientSeq !== void 0 ? { clientSeq: metadata.clientSeq } : {}
1546
- };
1547
- }
1548
- function withMessageDisplayText(message, displayText) {
1549
- let replaced = false;
1550
- const parts = message.parts.flatMap((part) => {
1551
- if (part.type !== "text") return [part];
1552
- if (replaced) return [];
1553
- replaced = true;
1554
- return [{ ...part, text: displayText }];
1555
- });
1556
- return { ...message, parts };
1557
- }
1558
- function withMessageFiles(message, files) {
1559
- if (!files || files.length === 0) return message;
1560
- const nonFileParts = message.parts.filter((part) => part.type !== "file");
1561
- return { ...message, parts: [...nonFileParts, ...files] };
1562
- }
1563
- function filePartsFromPromptPayload(payload) {
1564
- if (!payload.attachments || payload.attachments.length === 0) return void 0;
1565
- return payload.attachments.map((attachment, index) => ({
1566
- type: "file",
1567
- id: `prompt:${payload.clientNonce}:file:${index}`,
1568
- filename: attachment.filename,
1569
- mediaType: attachment.mediaType,
1570
- url: attachment.url,
1571
- path: attachment.path
1572
- }));
1573
- }
1574
- function withQueuedSelector(followUp, metadata) {
1575
- return {
1576
- ...followUp,
1577
- displayText: metadata.displayText,
1578
- ...metadata.clientNonce ? { clientNonce: metadata.clientNonce } : {},
1579
- ...metadata.clientSeq !== void 0 ? { clientSeq: metadata.clientSeq } : {}
1580
- };
1581
- }
1582
- function countTexts(texts) {
1583
- const counts = /* @__PURE__ */ new Map();
1584
- for (const text of texts) counts.set(text, (counts.get(text) ?? 0) + 1);
1585
- return counts;
1586
- }
1587
- function countMetadataTexts(entries) {
1588
- const counts = /* @__PURE__ */ new Map();
1589
- for (const entry of entries) {
1590
- counts.set(entry.displayText, (counts.get(entry.displayText) ?? 0) + 1);
1591
- if (entry.serverText) counts.set(entry.serverText, (counts.get(entry.serverText) ?? 0) + 1);
1592
- }
1593
- return counts;
1594
- }
1595
- function messageText2(message) {
1596
- let text = "";
1597
- for (const part of message.parts) {
1598
- if (part.type !== "text") continue;
1599
- text += text ? `
1600
- ${part.text}` : part.text;
1601
- }
1602
- return text === "" ? void 0 : text;
1603
- }
1604
- function takeFirstMessageMetadata(metadata, text, message) {
1605
- if (!text) return void 0;
1606
- const createdAt = messageCreatedAtMs(message);
1607
- if (createdAt === void 0) return void 0;
1608
- const index = metadata.findIndex((entry2) => {
1609
- if (!matchesMetadataText(entry2, text)) return false;
1610
- return entry2.recordedAt !== void 0 && createdAt >= entry2.recordedAt;
1611
- });
1612
- if (index < 0) return void 0;
1613
- const [entry] = metadata.splice(index, 1);
1614
- return entry;
1615
- }
1616
- function messageCreatedAtMs(message) {
1617
- if (!message.createdAt) return void 0;
1618
- const timestamp = Date.parse(message.createdAt);
1619
- return Number.isFinite(timestamp) ? timestamp : void 0;
1620
- }
1621
-
1622
- // src/server/pi-chat/harnessPiChatService.ts
1623
- var MAX_PROMPT_IMAGE_BYTES = 10 * 1024 * 1024;
1624
- var PROMPT_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".avif", ".gif", ".jpg", ".jpeg", ".png", ".webp"]);
1625
- var HarnessPiChatService = class {
1626
- harness;
1627
- sessionStore;
1628
- workdir;
1629
- workspace;
1630
- eventStore;
1631
- allowAttachments;
1632
- channels = /* @__PURE__ */ new Map();
1633
- // Single-flight guard so concurrent cold callers (e.g. two browser tabs each
1634
- // opening /events while the session is still being created) converge on one
1635
- // LiveSessionChannel instead of racing through ensureChannel and orphaning
1636
- // the loser's adapter subscription.
1637
- channelCreations = /* @__PURE__ */ new Map();
1638
- messageMetadata = new PiChatMessageMetadataReconciler();
1639
- activePromptRuns = /* @__PURE__ */ new Map();
1640
- syntheticPromptFailures = /* @__PURE__ */ new Map();
1641
- activeSyntheticPromptErrors = /* @__PURE__ */ new Map();
1642
- metering;
1643
- constructor(options) {
1644
- this.harness = options.harness;
1645
- this.sessionStore = options.sessionStore;
1646
- this.workdir = options.workdir;
1647
- this.workspace = options.workspace;
1648
- this.eventStore = options.eventStore;
1649
- this.allowAttachments = options.allowAttachments !== false;
1650
- this.metering = options.metering ? new PiChatMeteringCoordinator(options.metering, options.meteringLogger) : void 0;
1651
- }
1652
- /** Test/diagnostic hook: resolves once queued metering sink calls settle. */
1653
- async flushMetering() {
1654
- await this.metering?.flush();
1655
- }
1656
- async listSessions(ctx, options) {
1657
- return this.sessionStore.list(toSessionCtx(ctx), options);
1658
- }
1659
- async createSession(ctx, init) {
1660
- return this.sessionStore.create(toSessionCtx(ctx), init);
1661
- }
1662
- async deleteSession(ctx, sessionId) {
1663
- const sessionCtx = toSessionCtx(ctx);
1664
- const sessionKey = sessionCacheKey(sessionId, sessionCtx);
1665
- try {
1666
- await this.sessionStore.load(sessionCtx, sessionId);
1667
- } catch (error) {
1668
- throw normalizeSessionAccessError(error, sessionId);
1669
- }
1670
- const channel = this.channels.get(sessionKey);
1671
- if (channel) {
1672
- const activeRun = this.activePromptRuns.get(sessionKey);
1673
- await channel.adapter.abort();
1674
- await activeRun?.catch(() => {
1675
- });
1676
- }
1677
- channel?.unsubscribe();
1678
- this.channels.delete(sessionKey);
1679
- this.metering?.releaseSession(sessionKey);
1680
- this.messageMetadata.clearSession(sessionKey);
1681
- this.syntheticPromptFailures.delete(sessionKey);
1682
- this.activeSyntheticPromptErrors.delete(sessionKey);
1683
- await this.sessionStore.delete(sessionCtx, sessionId);
1684
- }
1685
- async readState(ctx, sessionId) {
1686
- const sessionKey = this.sessionKey(ctx, sessionId);
1687
- const channel = this.channels.get(sessionKey);
1688
- if (!channel && !this.harnessMayHaveLiveSession(ctx, sessionId)) {
1689
- const persisted = await this.readPersistedState(ctx, sessionId);
1690
- if (persisted) return persisted;
1691
- }
1692
- const adapter = await this.getAdapter(ctx, sessionId, "");
1693
- const snapshot = this.messageMetadata.enrichSnapshot(sessionKey, buildPiChatSnapshot(adapter, {
1694
- seq: channel?.buffer.latestSeq ?? 0,
1695
- sessionId,
1696
- activeTurnId: channel?.activeTurnId,
1697
- messageTurnIds: channel?.messageTurnIds
1698
- }));
1699
- return this.enrichSyntheticPromptFailures(sessionKey, snapshot);
1700
- }
1701
- harnessMayHaveLiveSession(ctx, sessionId) {
1702
- return typeof this.harness.hasPiSession === "function" ? this.harness.hasPiSession(sessionId, toSessionCtx(ctx)) : true;
1703
- }
1704
- async readPersistedState(ctx, sessionId) {
1705
- if (!this.sessionStore.loadEntries) return null;
1706
- try {
1707
- const { id, messages } = await this.sessionStore.loadEntries(toSessionCtx(ctx), sessionId);
1708
- return {
1709
- protocolVersion: 1,
1710
- sessionId: id,
1711
- seq: await this.readDurableLatestPiChatSeq(sessionStreamPath(this.sessionKey(ctx, id))),
1712
- status: "idle",
1713
- messages: buildPiChatHistory(messages, { sessionId: id }),
1714
- queue: { followUps: [] },
1715
- followUpMode: "one-at-a-time"
1716
- };
1717
- } catch {
1718
- return null;
1719
- }
1720
- }
1721
- async subscribe(ctx, sessionId, cursor, subscriber) {
1722
- const channel = await this.getChannel(ctx, sessionId);
1723
- const result = channel.buffer.subscribe(cursor, subscriber);
1724
- if (result.type !== "ok") return result;
1725
- return { type: "ok", unsubscribe: result.unsubscribe, closed: channel.closed };
1726
- }
1727
- async prompt(ctx, sessionId, payload) {
1728
- this.assertAttachmentsAllowed(payload);
1729
- const sessionKey = this.sessionKey(ctx, sessionId);
1730
- const adapter = await this.getAdapter(ctx, sessionId, payload);
1731
- const channel = await this.ensureChannel(ctx, sessionId, adapter);
1732
- const outcome = await this.metering?.reservePrompt({
1733
- workspaceId: ctx.workspaceId,
1734
- userId: ctx.authSubject,
1735
- userEmail: ctx.authEmail,
1736
- userEmailVerified: ctx.authEmailVerified,
1737
- sessionId,
1738
- stateKey: sessionKey,
1739
- clientNonce: payload.clientNonce,
1740
- message: payload.message,
1741
- model: adapter.currentModel?.() ?? payload.model
1742
- }) ?? "created";
1743
- if (outcome === "duplicate") {
1744
- return {
1745
- accepted: true,
1746
- cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0,
1747
- clientNonce: payload.clientNonce,
1748
- duplicate: true
1749
- };
1750
- }
1751
- if (outcome === "cancelled") throw promptCancelledError();
1752
- this.messageMetadata.recordPrompt(sessionKey, payload);
1753
- const receiptCursor = nextPromptReceiptCursor(channel);
1754
- try {
1755
- const input = await toPiPromptInput(payload, this.workspace);
1756
- const run = this.trackActiveRun(sessionKey, this.runAndDrainPublishQueue(channel, adapter.prompt(input)));
1757
- run.catch((error) => {
1758
- this.metering?.failPromptRun(sessionId, payload.clientNonce, sessionKey);
1759
- if (!this.messageMetadata.hasPrompt(sessionKey, { clientNonce: payload.clientNonce, displayText: payload.displayMessage ?? payload.message })) return;
1760
- this.publishPromptRunError(sessionKey, sessionId, channel, payload, error);
1761
- });
1762
- } catch (err) {
1763
- this.metering?.failPromptRun(sessionId, payload.clientNonce, sessionKey);
1764
- this.messageMetadata.removePrompt(sessionKey, { clientNonce: payload.clientNonce });
1765
- throw err;
1766
- }
1767
- return { accepted: true, cursor: receiptCursor, clientNonce: payload.clientNonce };
1768
- }
1769
- assertAttachmentsAllowed(payload) {
1770
- if (this.allowAttachments || !payload.attachments || payload.attachments.length === 0) return;
1771
- throw new AgentFilesystemRequiredError();
1772
- }
1773
- async followUp(ctx, sessionId, payload) {
1774
- const sessionKey = this.sessionKey(ctx, sessionId);
1775
- const adapter = await this.getAdapter(ctx, sessionId, payload.message);
1776
- const channel = await this.ensureChannel(ctx, sessionId, adapter);
1777
- const outcome = await this.metering?.reserveFollowUp({
1778
- workspaceId: ctx.workspaceId,
1779
- userId: ctx.authSubject,
1780
- userEmail: ctx.authEmail,
1781
- userEmailVerified: ctx.authEmailVerified,
1782
- sessionId,
1783
- stateKey: sessionKey,
1784
- clientNonce: payload.clientNonce,
1785
- clientSeq: payload.clientSeq,
1786
- message: payload.message,
1787
- model: adapter.currentModel?.()
1788
- }) ?? "created";
1789
- if (outcome === "duplicate") {
1790
- return {
1791
- accepted: true,
1792
- queued: true,
1793
- cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0,
1794
- clientNonce: payload.clientNonce,
1795
- clientSeq: payload.clientSeq,
1796
- duplicate: true
1797
- };
1798
- }
1799
- if (outcome === "cancelled") throw promptCancelledError();
1800
- this.messageMetadata.recordFollowUp(sessionKey, payload);
1801
- try {
1802
- await adapter.followUp(payload.message, {
1803
- displayText: payload.displayMessage ?? payload.message,
1804
- clientNonce: payload.clientNonce,
1805
- clientSeq: payload.clientSeq
1806
- });
1807
- } catch (err) {
1808
- this.metering?.failFollowUpRun(sessionKey, payload);
1809
- this.messageMetadata.removeFollowUp(sessionKey, payload);
1810
- throw err;
1811
- }
1812
- await this.drainPublishQueue(channel);
1813
- return { accepted: true, queued: true, cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0, clientNonce: payload.clientNonce, clientSeq: payload.clientSeq };
1814
- }
1815
- async clearQueue(ctx, sessionId, payload) {
1816
- const sessionKey = this.sessionKey(ctx, sessionId);
1817
- const adapter = await this.getAdapter(ctx, sessionId, "");
1818
- if (hasFollowUpSelector(payload)) {
1819
- const before = adapter.readSnapshot().followUpMessages.length;
1820
- adapter.clearFollowUp(payload);
1821
- await this.drainPublishQueue(this.channels.get(sessionKey));
1822
- const after = adapter.readSnapshot().followUpMessages.length;
1823
- if (after < before) {
1824
- this.messageMetadata.removeFollowUp(sessionKey, payload);
1825
- this.metering?.releaseQueued(sessionKey, payload);
1826
- }
1827
- return { accepted: true, cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0, cleared: Math.max(0, before - after) };
1828
- }
1829
- const clearedQueue = this.clearAllFollowUps(adapter, sessionId, sessionKey);
1830
- await this.drainPublishQueue(this.channels.get(sessionKey));
1831
- this.metering?.releaseQueued(sessionKey);
1832
- return { accepted: true, cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0, cleared: clearedQueue.length };
1833
- }
1834
- async interrupt(ctx, sessionId, _payload) {
1835
- const sessionKey = this.sessionKey(ctx, sessionId);
1836
- const adapter = await this.getAdapter(ctx, sessionId, "");
1837
- const snapshot = adapter.readSnapshot();
1838
- const wasActive = snapshot.isStreaming || snapshot.isRetrying;
1839
- const nextFollowUp = wasActive ? this.nextFollowUpForInterrupt(sessionId, sessionKey, adapter) : void 0;
1840
- const activeRun = this.activePromptRuns.get(sessionKey);
1841
- adapter.abortRetry?.();
1842
- if (wasActive) await adapter.abort();
1843
- await this.drainPublishQueue(this.channels.get(sessionKey));
1844
- await activeRun?.catch(() => {
1845
- });
1846
- this.metering?.releasePending(sessionKey);
1847
- if (nextFollowUp) await this.autoPostInterruptedFollowUp(sessionId, sessionKey, adapter, nextFollowUp);
1848
- return { accepted: true, cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0 };
1849
- }
1850
- async stop(ctx, sessionId, _payload) {
1851
- const sessionKey = this.sessionKey(ctx, sessionId);
1852
- const adapter = await this.getAdapter(ctx, sessionId, "");
1853
- const clearedQueue = this.clearAllFollowUps(adapter, sessionId, sessionKey);
1854
- this.metering?.releaseQueued(sessionKey);
1855
- this.metering?.releasePending(sessionKey);
1856
- await adapter.abort();
1857
- await this.drainPublishQueue(this.channels.get(sessionKey));
1858
- return { accepted: true, stopped: true, cursor: this.channels.get(sessionKey)?.buffer.latestSeq ?? 0, clearedQueue: buildPiChatQueuedFollowUps(sessionId, clearedQueue) };
1859
- }
1860
- clearAllFollowUps(adapter, sessionId, sessionKey) {
1861
- const before = [...adapter.readSnapshot().followUpMessages];
1862
- adapter.clearFollowUp();
1863
- const after = adapter.readSnapshot().followUpMessages;
1864
- this.messageMetadata.syncFromTexts(sessionKey, after);
1865
- return removedFollowUps(before, after);
1866
- }
1867
- nextFollowUpForInterrupt(sessionId, sessionKey, adapter) {
1868
- const followUps = this.messageMetadata.enrichQueuedFollowUps(
1869
- sessionKey,
1870
- buildPiChatQueuedFollowUps(sessionId, adapter.readSnapshot().followUpMessages)
1871
- );
1872
- return followUps[0];
1873
- }
1874
- async autoPostInterruptedFollowUp(sessionId, sessionKey, adapter, followUp) {
1875
- const metadata = this.messageMetadata.findFollowUpForQueueItem(sessionKey, followUp);
1876
- this.messageMetadata.recordConsumingFollowUp(sessionKey, followUp, metadata?.serverText);
1877
- if (adapter.continueQueuedFollowUp) {
1878
- try {
1879
- await this.trackActiveRun(sessionKey, this.runAndDrainPublishQueue(this.channels.get(sessionKey), adapter.continueQueuedFollowUp()));
1880
- } catch (err) {
1881
- this.metering?.failFollowUpRun(sessionKey, followUp);
1882
- throw err;
1883
- }
1884
- return;
1885
- }
1886
- if (!this.canClearAutoPostedFollowUpForFallback(adapter, followUp)) {
1887
- throw new AutoPostFollowUpError("Cannot auto-post queued follow-up because this runtime cannot safely remove only the consumed queued item.");
1888
- }
1889
- this.clearAutoPostedFollowUpForFallback(sessionId, sessionKey, adapter, followUp);
1890
- this.metering?.promoteQueuedToPrompt(sessionKey, followUp);
1891
- try {
1892
- await this.runPrompt(sessionKey, adapter, metadata?.serverText ?? followUp.displayText);
1893
- } catch (err) {
1894
- this.metering?.failPromotedFollowUp(sessionId, followUp, sessionKey);
1895
- await adapter.followUp(metadata?.serverText ?? followUp.displayText, {
1896
- displayText: followUp.displayText,
1897
- clientNonce: followUp.clientNonce,
1898
- clientSeq: followUp.clientSeq
1899
- });
1900
- if (followUp.clientNonce && followUp.clientSeq !== void 0) {
1901
- this.messageMetadata.recordFollowUp(sessionKey, {
1902
- message: metadata?.serverText ?? followUp.displayText,
1903
- displayMessage: followUp.displayText,
1904
- clientNonce: followUp.clientNonce,
1905
- clientSeq: followUp.clientSeq
1906
- });
1907
- }
1908
- throw err;
1909
- }
1910
- }
1911
- async runPrompt(sessionKey, adapter, input) {
1912
- await this.trackActiveRun(sessionKey, this.runAndDrainPublishQueue(this.channels.get(sessionKey), adapter.prompt(input)));
1913
- }
1914
- async trackActiveRun(sessionKey, run) {
1915
- this.activePromptRuns.set(sessionKey, run);
1916
- try {
1917
- await run;
1918
- } finally {
1919
- if (this.activePromptRuns.get(sessionKey) === run) this.activePromptRuns.delete(sessionKey);
1920
- }
1921
- }
1922
- clearAutoPostedFollowUpForFallback(sessionId, sessionKey, adapter, followUp) {
1923
- if (adapter.readSnapshot().followUpMessages.length <= 1) {
1924
- this.clearAllFollowUps(adapter, sessionId, sessionKey);
1925
- return true;
1926
- }
1927
- if (hasFollowUpSelector(followUp)) {
1928
- adapter.clearFollowUp(followUpSelector(followUp));
1929
- return true;
1930
- }
1931
- return false;
1932
- }
1933
- canClearAutoPostedFollowUpForFallback(adapter, followUp) {
1934
- return hasFollowUpSelector(followUp) || adapter.readSnapshot().followUpMessages.length <= 1;
1935
- }
1936
- enrichSyntheticPromptFailures(sessionKey, snapshot) {
1937
- const failures = this.syntheticPromptFailures.get(sessionKey);
1938
- if (!failures || failures.length === 0) return snapshot;
1939
- const activeError = this.activeSyntheticPromptErrors.get(sessionKey);
1940
- return {
1941
- ...snapshot,
1942
- status: activeError ? "error" : snapshot.status,
1943
- error: activeError ?? snapshot.error,
1944
- messages: mergeSyntheticMessages(snapshot.messages, failures.map((failure) => failure.message))
1945
- };
1946
- }
1947
- publishChannelEvents(sessionId, channel, events, afterPublish) {
1948
- if (!this.eventStore) {
1949
- const publishedEvents = [];
1950
- for (const event of events) {
1951
- const enriched = this.messageMetadata.enrichEvent(channel.sessionKey, event);
1952
- publishedEvents.push(enriched);
1953
- this.publishChannelEventSync(channel, enriched);
1954
- }
1955
- afterPublish?.(publishedEvents);
1956
- return;
1957
- }
1958
- const next = channel.publishQueue.then(async () => {
1959
- const publishedEvents = [];
1960
- for (const event of events) {
1961
- const enriched = this.messageMetadata.enrichEvent(channel.sessionKey, event);
1962
- publishedEvents.push(enriched);
1963
- await this.eventStore?.appendAgentEvent(sessionId, enriched, { idempotencyKey: String(enriched.seq), streamPath: channel.streamPath });
1964
- this.publishChannelEventSync(channel, enriched);
1965
- }
1966
- afterPublish?.(publishedEvents);
1967
- }).catch((error) => {
1968
- channel.rejectClosed(error);
1969
- throw error;
1970
- });
1971
- channel.publishQueue = next;
1972
- next.catch(() => {
1973
- });
1974
- }
1975
- publishChannelEventSync(channel, event) {
1976
- const sessionKey = channel.sessionKey;
1977
- if (event.type === "agent-start") {
1978
- channel.activeTurnId = event.turnId;
1979
- this.activeSyntheticPromptErrors.delete(sessionKey);
1980
- }
1981
- if (event.type === "message-start" && channel.activeTurnId) {
1982
- channel.messageTurnIds.set(event.messageId, channel.activeTurnId);
1983
- }
1984
- if (event.type === "message-end" && channel.activeTurnId) {
1985
- channel.messageTurnIds.set(event.messageId, channel.activeTurnId);
1986
- channel.messageTurnIds.set(event.final.id, channel.activeTurnId);
1987
- }
1988
- if (event.type === "agent-end" && channel.activeTurnId === event.turnId) channel.activeTurnId = void 0;
1989
- this.messageMetadata.consumeEvent(sessionKey, event);
1990
- channel.buffer.publish(event);
1991
- }
1992
- publishPromptRunError(sessionKey, sessionId, channel, payload, error) {
1993
- if (!channel) return;
1994
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1995
- const messageId = `prompt-error:${payload.clientNonce}:user`;
1996
- const message = promptPayloadMessage(payload, messageId, createdAt, channel.activeTurnId);
1997
- const messageEvent = channel.mapper.mapSynthetic({
1998
- type: "message-start",
1999
- messageId,
2000
- role: "user",
2001
- clientNonce: payload.clientNonce,
2002
- text: payload.displayMessage ?? payload.message,
2003
- files: promptPayloadFileParts(payload, messageId),
2004
- createdAt
2005
- });
2006
- const promptError = {
2007
- code: ErrorCode.enum.INTERNAL_ERROR,
2008
- message: error instanceof Error && error.message ? error.message : "Prompt failed before the agent run completed.",
2009
- retryable: false
2010
- };
2011
- const errorEvent = channel.mapper.mapSynthetic({
2012
- type: "error",
2013
- turnId: channel.activeTurnId,
2014
- retryable: false,
2015
- error: promptError
2016
- });
2017
- this.publishChannelEvents(sessionId, channel, [messageEvent, errorEvent], () => {
2018
- const failures = this.syntheticPromptFailures.get(sessionKey) ?? [];
2019
- failures.push({ message, error: promptError });
2020
- this.syntheticPromptFailures.set(sessionKey, failures);
2021
- this.activeSyntheticPromptErrors.set(sessionKey, promptError);
2022
- channel.activeTurnId = void 0;
2023
- });
2024
- }
2025
- async runAndDrainPublishQueue(channel, run) {
2026
- let runError;
2027
- try {
2028
- await run;
2029
- } catch (error) {
2030
- runError = error;
2031
- }
2032
- try {
2033
- await this.drainPublishQueue(channel);
2034
- } catch (error) {
2035
- if (runError === void 0) throw error;
2036
- }
2037
- if (runError !== void 0) throw runError;
2038
- }
2039
- async drainPublishQueue(channel) {
2040
- await channel?.publishQueue;
2041
- }
2042
- async getAdapter(ctx, sessionId, input, options) {
2043
- if (!this.harness.getPiSessionAdapter) throw new Error("pi-native harness adapter unavailable");
2044
- if (options?.authorize !== false) await this.assertCanAccessSession(ctx, sessionId);
2045
- const message = typeof input === "string" ? input : input.message;
2046
- const sendInput = {
2047
- sessionId,
2048
- content: message,
2049
- message,
2050
- ctx: toSessionCtx(ctx),
2051
- ...typeof input !== "string" && input.model ? { model: input.model } : {},
2052
- ...typeof input !== "string" && input.thinkingLevel ? { thinkingLevel: input.thinkingLevel } : {},
2053
- ...typeof input !== "string" && input.attachments ? { attachments: input.attachments } : {}
2054
- };
2055
- return this.harness.getPiSessionAdapter(sendInput, {
2056
- abortSignal: new AbortController().signal,
2057
- workdir: this.workdir,
2058
- workspaceId: ctx.workspaceId,
2059
- requestId: ctx.requestId,
2060
- userId: ctx.authSubject,
2061
- userEmail: ctx.authEmail,
2062
- userEmailVerified: ctx.authEmailVerified
2063
- });
2064
- }
2065
- async getChannel(ctx, sessionId) {
2066
- await this.assertCanAccessSession(ctx, sessionId);
2067
- const sessionKey = this.sessionKey(ctx, sessionId);
2068
- const existing = this.channels.get(sessionKey);
2069
- if (existing) return existing;
2070
- return this.createChannelOnce(sessionKey, sessionId, () => this.getAdapter(ctx, sessionId, "", { authorize: false }));
2071
- }
2072
- async ensureChannel(ctx, sessionId, adapter) {
2073
- const sessionKey = this.sessionKey(ctx, sessionId);
2074
- const existing = this.channels.get(sessionKey);
2075
- if (existing) return existing;
2076
- return this.createChannelOnce(sessionKey, sessionId, async () => adapter);
2077
- }
2078
- /**
2079
- * Resolve (or create) the single LiveSessionChannel for a session, coalescing
2080
- * concurrent cold callers onto one in-flight creation. Without this guard two
2081
- * tabs opening /events on a not-yet-live session both fall through the channel
2082
- * cache miss, build separate channels + adapter subscriptions, and the second
2083
- * `this.channels.set` orphans the first — so the first tab's stream silently
2084
- * stops receiving events.
2085
- */
2086
- async createChannelOnce(sessionKey, sessionId, resolveAdapter) {
2087
- const inFlight = this.channelCreations.get(sessionKey);
2088
- if (inFlight) return inFlight;
2089
- const creation = (async () => {
2090
- const existing = this.channels.get(sessionKey);
2091
- if (existing) return existing;
2092
- const adapter = await resolveAdapter();
2093
- return this.buildChannel(sessionKey, sessionId, adapter);
2094
- })();
2095
- this.channelCreations.set(sessionKey, creation);
2096
- try {
2097
- return await creation;
2098
- } finally {
2099
- if (this.channelCreations.get(sessionKey) === creation) this.channelCreations.delete(sessionKey);
2100
- }
2101
- }
2102
- async buildChannel(sessionKey, sessionId, adapter) {
2103
- const existing = this.channels.get(sessionKey);
2104
- if (existing) return existing;
2105
- const streamPath = sessionStreamPath(sessionKey);
2106
- await this.eventStore?.createStream(streamPath);
2107
- const initialSeq = await this.readDurableLatestPiChatSeq(streamPath);
2108
- const buffer = new PiChatReplayBuffer({ initialLatestSeq: initialSeq });
2109
- const mapper = new PiChatEventMapper({ sessionId, initialSeq: buffer.latestSeq });
2110
- const closed = deferred();
2111
- closed.promise.catch(() => {
2112
- });
2113
- const channel = {
2114
- sessionKey,
2115
- streamPath,
2116
- buffer,
2117
- adapter,
2118
- unsubscribe: () => {
2119
- },
2120
- mapper,
2121
- publishQueue: Promise.resolve(),
2122
- closed: closed.promise,
2123
- rejectClosed: closed.reject,
2124
- messageTurnIds: /* @__PURE__ */ new Map()
2125
- };
2126
- const unsubscribe = adapter.subscribe((event) => {
2127
- const mappedEvents = mapper.map(event);
2128
- this.publishChannelEvents(sessionId, channel, mappedEvents, (enrichedEvents) => {
2129
- this.metering?.observe(sessionKey, event, enrichedEvents);
2130
- });
2131
- });
2132
- channel.unsubscribe = unsubscribe;
2133
- this.channels.set(sessionKey, channel);
2134
- return channel;
2135
- }
2136
- async readDurableLatestPiChatSeq(streamPath) {
2137
- if (!this.eventStore) return 0;
2138
- const meta = await this.eventStore.getStreamMeta(streamPath);
2139
- if (!meta) return 0;
2140
- const tailIndex = parseOffset(meta.nextOffset);
2141
- if (tailIndex < 0) return 0;
2142
- const tail = await this.eventStore.readEvents(streamPath, {
2143
- offset: formatOffset(tailIndex - 1),
2144
- limit: 1
2145
- });
2146
- const envelope = tail.events[0]?.data;
2147
- const seq = envelope?.chunk?.seq;
2148
- return typeof seq === "number" && Number.isInteger(seq) && seq >= 0 ? seq : tailIndex + 1;
2149
- }
2150
- async assertCanAccessSession(ctx, sessionId) {
2151
- try {
2152
- await this.sessionStore.load(toSessionCtx(ctx), sessionId);
2153
- } catch (error) {
2154
- throw normalizeSessionAccessError(error, sessionId);
2155
- }
2156
- }
2157
- sessionKey(ctx, sessionId) {
2158
- return sessionCacheKey(sessionId, toSessionCtx(ctx));
2159
- }
2160
- };
2161
- var AutoPostFollowUpError = class extends Error {
2162
- };
2163
- function promptCancelledError() {
2164
- return Object.assign(new Error("request cancelled before execution"), {
2165
- statusCode: 409,
2166
- code: ErrorCode.enum.ABORTED,
2167
- retryable: true
2168
- });
2169
- }
2170
- function deferred() {
2171
- let resolve2;
2172
- let reject;
2173
- const promise = new Promise((nextResolve, nextReject) => {
2174
- resolve2 = nextResolve;
2175
- reject = nextReject;
2176
- });
2177
- return { promise, resolve: resolve2, reject };
2178
- }
2179
- function normalizeSessionAccessError(error, sessionId) {
2180
- if (error?.code === ErrorCode.enum.SESSION_NOT_FOUND || isPlainSessionNotFound(error, sessionId)) {
2181
- return Object.assign(new Error("session not found"), {
2182
- code: ErrorCode.enum.SESSION_NOT_FOUND
2183
- });
2184
- }
2185
- return error;
2186
- }
2187
- function isPlainSessionNotFound(error, sessionId) {
2188
- return error instanceof Error && (error.message === "session not found" || error.message === `Session not found: ${sessionId}`);
2189
- }
2190
- function nextPromptReceiptCursor(channel) {
2191
- return (channel?.buffer.latestSeq ?? 0) + 1;
2192
- }
2193
- function promptPayloadFileParts(payload, messageId) {
2194
- if (!payload.attachments || payload.attachments.length === 0) return void 0;
2195
- return payload.attachments.map((attachment, index) => ({
2196
- type: "file",
2197
- id: `${messageId}:file:${index}`,
2198
- filename: attachment.filename,
2199
- mediaType: attachment.mediaType,
2200
- url: attachment.url,
2201
- path: attachment.path
2202
- }));
2203
- }
2204
- function promptPayloadMessage(payload, messageId, createdAt, turnId) {
2205
- const displayText = payload.displayMessage ?? payload.message;
2206
- return {
2207
- id: messageId,
2208
- role: "user",
2209
- status: "done",
2210
- clientNonce: payload.clientNonce,
2211
- createdAt,
2212
- turnId,
2213
- parts: [
2214
- ...displayText ? [{ type: "text", id: `${messageId}:text:0`, text: displayText }] : [],
2215
- ...promptPayloadFileParts(payload, messageId) ?? []
2216
- ]
2217
- };
2218
- }
2219
- function mergeSyntheticMessages(messages, syntheticMessages) {
2220
- const existingIds = new Set(messages.map((message) => message.id));
2221
- const merged = [...messages];
2222
- for (const synthetic of syntheticMessages) {
2223
- if (existingIds.has(synthetic.id)) continue;
2224
- const syntheticTime = messageTime(synthetic);
2225
- const insertAt = syntheticTime === void 0 ? -1 : merged.findIndex((message) => {
2226
- const timestamp = messageTime(message);
2227
- return timestamp !== void 0 && timestamp > syntheticTime;
2228
- });
2229
- if (insertAt < 0) merged.push(synthetic);
2230
- else merged.splice(insertAt, 0, synthetic);
2231
- }
2232
- return merged;
2233
- }
2234
- function messageTime(message) {
2235
- if (!message.createdAt) return void 0;
2236
- const timestamp = Date.parse(message.createdAt);
2237
- return Number.isFinite(timestamp) ? timestamp : void 0;
2238
- }
2239
- async function toPiPromptInput(payload, workspace) {
2240
- const images = await promptImagesFromAttachments(payload.attachments, workspace);
2241
- if (images.length === 0) return payload.message;
2242
- return { text: payload.message, options: { images } };
2243
- }
2244
- async function promptImagesFromAttachments(attachments, workspace) {
2245
- const images = [];
2246
- for (const attachment of attachments ?? []) {
2247
- const match = attachment.url.match(/^data:(image\/[^;]+);base64,(.+)$/);
2248
- if (match) {
2249
- const [, mimeType, data] = match;
2250
- images.push({ type: "image", mimeType, data });
2251
- continue;
2252
- }
2253
- if (!workspace?.readBinaryFile || !isWorkspaceImageAttachment(attachment)) continue;
2254
- try {
2255
- const stat = await workspace.stat(attachment.path);
2256
- if (stat.kind !== "file" || stat.size <= 0 || stat.size > MAX_PROMPT_IMAGE_BYTES) continue;
2257
- const bytes = await workspace.readBinaryFile(attachment.path);
2258
- if (bytes.byteLength <= 0 || bytes.byteLength > MAX_PROMPT_IMAGE_BYTES) continue;
2259
- const detectedMimeType = detectPromptImageMimeType(bytes);
2260
- if (!detectedMimeType) continue;
2261
- images.push({
2262
- type: "image",
2263
- mimeType: detectedMimeType,
2264
- data: Buffer.from(bytes).toString("base64")
2265
- });
2266
- } catch {
2267
- }
2268
- }
2269
- return images;
2270
- }
2271
- function isWorkspaceImageAttachment(attachment) {
2272
- if (!attachment.mediaType?.startsWith("image/") || !attachment.path) return false;
2273
- const lowerPath = attachment.path.toLowerCase();
2274
- return [...PROMPT_IMAGE_EXTENSIONS].some((ext) => lowerPath.endsWith(ext));
2275
- }
2276
- function detectPromptImageMimeType(bytes) {
2277
- const buffer = Buffer.from(bytes);
2278
- if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
2279
- return "image/png";
2280
- }
2281
- if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
2282
- return "image/jpeg";
2283
- }
2284
- if (buffer.length >= 6) {
2285
- const gif = buffer.subarray(0, 6).toString("ascii");
2286
- if (gif === "GIF87a" || gif === "GIF89a") return "image/gif";
2287
- }
2288
- if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
2289
- return "image/webp";
2290
- }
2291
- if (buffer.length >= 12 && buffer.subarray(4, 8).toString("ascii") === "ftyp") {
2292
- const brand = buffer.subarray(8, 12).toString("ascii");
2293
- if (brand === "avif" || brand === "avis") return "image/avif";
2294
- }
2295
- return null;
2296
- }
2297
- function removedFollowUps(before, after) {
2298
- const afterCounts = /* @__PURE__ */ new Map();
2299
- for (const text of after) afterCounts.set(text, (afterCounts.get(text) ?? 0) + 1);
2300
- const removed = [];
2301
- for (const text of before) {
2302
- const count = afterCounts.get(text) ?? 0;
2303
- if (count > 0) {
2304
- afterCounts.set(text, count - 1);
2305
- continue;
2306
- }
2307
- removed.push(text);
2308
- }
2309
- return removed;
2310
- }
2311
- function toSessionCtx(ctx) {
2312
- return { workspaceId: ctx.workspaceId, userId: ctx.authSubject };
2313
- }
2314
- function sessionCacheKey(sessionId, ctx) {
2315
- return JSON.stringify([sessionId, ctx.workspaceId ?? "", ctx.userId ?? ""]);
2316
- }
2317
-
2318
- // src/server/runtime/pureRuntime.ts
2319
- import { chmod, mkdir, mkdtemp } from "fs/promises";
2320
- import { tmpdir } from "os";
2321
- import { join, resolve } from "path";
2322
- var PURE_RUNTIME_CWD_NAME = ".runtime-none";
2323
- var defaultPureRuntimeCwd;
2324
- async function createPureRuntimeCwd(sessionRoot) {
2325
- const explicitRoot = sessionRoot?.trim();
2326
- if (!explicitRoot) {
2327
- defaultPureRuntimeCwd ??= createDefaultPureRuntimeCwd();
2328
- return defaultPureRuntimeCwd;
2329
- }
2330
- const cwd = join(resolve(explicitRoot), PURE_RUNTIME_CWD_NAME);
2331
- await mkdir(cwd, { recursive: true, mode: 448 });
2332
- await chmod(cwd, 448);
2333
- return cwd;
2334
- }
2335
- async function createDefaultPureRuntimeCwd() {
2336
- const cwd = await mkdtemp(join(tmpdir(), "boring-agent-pure-"));
2337
- await chmod(cwd, 448);
2338
- return cwd;
2339
- }
2340
-
2341
- // src/server/createAgent.ts
2342
- var DEFAULT_WORKDIR = "";
2343
- var DEFAULT_LIVE_BUFFER_SIZE = 1e3;
2344
- function createAgent(config) {
2345
- return createAgentRuntimeBridge(config).agent;
2346
- }
2347
- function createAgentRuntimeBridge(config, options = {}) {
2348
- if (config.sessions && !config.harnessFactory) {
2349
- throw new Error("createAgent sessions override requires a harnessFactory that uses the same SessionStore");
2350
- }
2351
- const runtimeLoader = createRuntimeLoader(config, options);
2352
- const getRuntime = runtimeLoader.get;
2353
- const live = new AgentLiveEventBuffer(DEFAULT_LIVE_BUFFER_SIZE);
2354
- const sessionContexts = /* @__PURE__ */ new Map();
2355
- const startedSessions = /* @__PURE__ */ new Map();
2356
- const sendLocks = /* @__PURE__ */ new Map();
2357
- const sessions = createFacadeSessionStore(config.sessions, getRuntime, live, sessionContexts, startedSessions);
2358
- const readiness = createReadiness(config);
2359
- async function ensureSession(input, runtime) {
2360
- if (input.sessionId) {
2361
- const ctx = await authorizeSessionAccess(runtime, input.sessionId, input.ctx, sessionContexts);
2362
- return { sessionId: input.sessionId, sessionKey: sessionCacheKey2(input.sessionId, ctx), ctx };
2363
- }
2364
- const service = runtime.service;
2365
- const created = await service.createSession?.(toPiRequestContext(input.ctx), {
2366
- title: contentToText(input.content ?? input.message).slice(0, 80) || void 0
2367
- });
2368
- if (!created) throw new Error("agent session creation is unavailable");
2369
- rememberSessionCtx(sessionContexts, created.id, input.ctx);
2370
- return { sessionId: created.id, sessionKey: sessionCacheKey2(created.id, input.ctx), ctx: input.ctx };
2371
- }
2372
- async function ensureBridge(sessionKey, sessionId, ctx, service) {
2373
- const state = live.ensure(sessionKey);
2374
- if (state.bridge || state.closed) return state;
2375
- state.bridgePromise ??= (async () => {
2376
- const subscribeFrom = state.latestIndex === 0 ? 0 : Number.MAX_SAFE_INTEGER;
2377
- let result = await service.subscribe(toPiRequestContext(ctx), sessionId, subscribeFrom, (chunk) => {
2378
- live.publish(sessionKey, sessionId, chunk);
2379
- });
2380
- if (result.type !== "ok") {
2381
- result = await service.subscribe(toPiRequestContext(ctx), sessionId, result.latestSeq, (chunk) => {
2382
- live.publish(sessionKey, sessionId, chunk);
2383
- });
2384
- }
2385
- if (result.type !== "ok") throw new AgentNotImplementedError("Historical pi-chat replay is not implemented until T1.");
2386
- state.bridge = result;
2387
- result.closed?.finally(() => live.close(sessionKey)).catch(() => live.close(sessionKey));
2388
- return state;
2389
- })();
2390
- try {
2391
- return await state.bridgePromise;
2392
- } finally {
2393
- state.bridgePromise = void 0;
2394
- }
2395
- }
2396
- async function start(input) {
2397
- assertFilesystemAttachmentsAllowed(config, input);
2398
- const runtime = await getRuntime();
2399
- const { sessionId, sessionKey, ctx } = await ensureSession(input, runtime);
2400
- startedSessions.set(sessionKey, { sessionId, ctx });
2401
- await ensureBridge(sessionKey, sessionId, ctx, runtime.service);
2402
- const startIndex = live.latestIndex(sessionKey);
2403
- await runtime.service.prompt(toPiRequestContext(ctx), sessionId, toPromptPayload(input));
2404
- return { sessionId, startIndex };
2405
- }
2406
- const agent = {
2407
- sessions,
2408
- readiness,
2409
- start,
2410
- stream(sessionId, options2) {
2411
- return authorizedStream(sessionId, options2);
2412
- },
2413
- async *send(input) {
2414
- const release = input.sessionId ? await acquireSessionLock(sendLocks, sessionCacheKey2(input.sessionId, input.ctx)) : void 0;
2415
- try {
2416
- const receipt = await start(input);
2417
- let turnId;
2418
- for await (const event of authorizedStream(receipt.sessionId, { startIndex: receipt.startIndex, ctx: input.ctx })) {
2419
- yield event;
2420
- if (event.chunk.type === "agent-start") turnId = event.chunk.turnId;
2421
- if (isSendTerminalEvent(event, turnId)) break;
2422
- }
2423
- } finally {
2424
- release?.();
2425
- }
2426
- },
2427
- async resolveInput(_sessionId, _requestId, _response) {
2428
- throw new AgentNotImplementedError("resolveInput is not implemented until T1.");
2429
- },
2430
- async interrupt(sessionId, ctx) {
2431
- const runtime = await getRuntime();
2432
- const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
2433
- return runtime.service.interrupt(toPiRequestContext(accessCtx), sessionId, {});
2434
- },
2435
- async stop(sessionId, ctx) {
2436
- const runtime = await getRuntime();
2437
- const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
2438
- const receipt = await runtime.service.stop(toPiRequestContext(accessCtx), sessionId, {});
2439
- const sessionKey = sessionCacheKey2(sessionId, accessCtx);
2440
- live.close(sessionKey);
2441
- startedSessions.delete(sessionKey);
2442
- return receipt;
2443
- },
2444
- async dispose() {
2445
- const runtime = await runtimeLoader.current();
2446
- let stopError;
2447
- try {
2448
- if (runtime) {
2449
- await Promise.all([...startedSessions.values()].map(
2450
- (started) => runtime.service.stop(toPiRequestContext(started.ctx), started.sessionId, {})
2451
- ));
2452
- }
2453
- } catch (error) {
2454
- stopError = error;
2455
- } finally {
2456
- live.dispose();
2457
- startedSessions.clear();
2458
- sessionContexts.clear();
2459
- if (config.runtime !== "none") await config.runtime.dispose?.();
2460
- }
2461
- if (stopError) throw stopError;
2462
- }
2463
- };
2464
- return {
2465
- agent,
2466
- getRuntime,
2467
- currentRuntime: runtimeLoader.current
2468
- };
2469
- async function* authorizedStream(sessionId, options2) {
2470
- const runtime = await getRuntime();
2471
- const accessCtx = await authorizeSessionAccess(runtime, sessionId, options2.ctx, sessionContexts);
2472
- yield* live.stream(sessionCacheKey2(sessionId, accessCtx), options2.startIndex);
2473
- }
2474
- }
2475
- function createRuntimeLoader(config, options) {
2476
- let runtimePromise;
2477
- return {
2478
- get() {
2479
- runtimePromise ??= createRuntime(config, options);
2480
- return runtimePromise;
2481
- },
2482
- current() {
2483
- return runtimePromise;
2484
- }
2485
- };
2486
- }
2487
- async function createRuntime(config, options) {
2488
- const harnessFactory = config.harnessFactory ?? (await import("./createHarness-RZUU6MJQ.js")).createPiCodingAgentHarness;
2489
- const pureRuntimeCwd = config.runtime === "none" ? await createPureRuntimeCwd(config.sessionStorageRoot) : void 0;
2490
- const harnessInput = {
2491
- tools: config.tools ?? [],
2492
- cwd: pureRuntimeCwd ?? config.workdir ?? DEFAULT_WORKDIR,
2493
- runtimeCwd: pureRuntimeCwd ?? options.harness?.runtimeCwd ?? options.service?.workdir ?? config.workdir,
2494
- sessionStorageCwd: pureRuntimeCwd ? DEFAULT_WORKDIR : void 0,
2495
- systemPromptAppend: config.systemPromptAppend,
2496
- systemPromptDynamic: config.systemPromptDynamic,
2497
- sessionRoot: config.sessionStorageRoot,
2498
- telemetry: config.telemetry
2499
- };
2500
- const harness = await harnessFactory(harnessInput);
2501
- const sessionStore = config.sessions ?? harness.sessions;
2502
- return {
2503
- harness,
2504
- sessionStore,
2505
- service: new HarnessPiChatService({
2506
- harness,
2507
- sessionStore,
2508
- workdir: pureRuntimeCwd ?? options.service?.workdir ?? config.workdir ?? DEFAULT_WORKDIR,
2509
- workspace: options.service?.workspace,
2510
- eventStore: options.service?.eventStore,
2511
- allowAttachments: config.runtime !== "none",
2512
- metering: config.metering
2513
- })
2514
- };
2515
- }
2516
- function assertFilesystemAttachmentsAllowed(config, input) {
2517
- if (config.runtime !== "none" || !input.attachments || input.attachments.length === 0) return;
2518
- throw new AgentFilesystemRequiredError();
2519
- }
2520
- function createReadiness(config) {
2521
- const requirements = [...config.readinessRequirements ?? []];
2522
- return {
2523
- requirements,
2524
- async status() {
2525
- return requirements.map((key) => ({
2526
- key,
2527
- ready: false,
2528
- message: "readiness status is not available in the core facade"
2529
- }));
2530
- }
2531
- };
2532
- }
2533
- function createFacadeSessionStore(baseStore, getRuntime, live, sessionContexts, startedSessions) {
2534
- const store = async () => baseStore ?? (await getRuntime()).harness.sessions;
2535
- return {
2536
- async list(ctx, options) {
2537
- const summaries = await (await store()).list(ctx, options);
2538
- return summaries.filter((summary) => canAccessStoredSessionCtx(summary.id, ctx, sessionContexts));
2539
- },
2540
- async create(ctx, init) {
2541
- const created = await (await store()).create(ctx, init);
2542
- rememberSessionCtx(sessionContexts, created.id, ctx);
2543
- return created;
2544
- },
2545
- async load(ctx, sessionId) {
2546
- const runtime = await getRuntime();
2547
- const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
2548
- return (await store()).load(accessCtx ?? {}, sessionId);
2549
- },
2550
- async delete(ctx, sessionId) {
2551
- const runtime = await getRuntime();
2552
- const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
2553
- await runtime.service.deleteSession(toPiRequestContext(accessCtx), sessionId);
2554
- const sessionKey = sessionCacheKey2(sessionId, accessCtx);
2555
- live.close(sessionKey);
2556
- sessionContexts.delete(sessionKey);
2557
- startedSessions.delete(sessionKey);
2558
- }
2559
- };
2560
- }
2561
- function toPiRequestContext(ctx) {
2562
- return {
2563
- workspaceId: ctx?.workspaceId,
2564
- authSubject: ctx?.userId,
2565
- requestId: "agent-core"
2566
- };
2567
- }
2568
- function toPromptPayload(input) {
2569
- return {
2570
- message: contentToText(input.content ?? input.message),
2571
- clientNonce: `agent:${Date.now()}:${Math.random().toString(36).slice(2)}`,
2572
- ...input.model ? { model: input.model } : {},
2573
- ...input.thinkingLevel ? { thinkingLevel: input.thinkingLevel } : {},
2574
- ...input.attachments ? { attachments: input.attachments } : {}
2575
- };
2576
- }
2577
- function contentToText(content) {
2578
- if (content === void 0) return "";
2579
- if (typeof content === "string") return content;
2580
- return content.map((part) => part.text).filter((text) => typeof text === "string" && text.length > 0).join("\n");
2581
- }
2582
- async function loadSession(store, ctx, sessionId) {
2583
- try {
2584
- await store.load(ctx, sessionId);
2585
- } catch (error) {
2586
- throw normalizeSessionLoadError(error, sessionId);
2587
- }
2588
- }
2589
- async function assertLoadedSessionVisible(store, ctx, sessionId) {
2590
- const summaries = await store.list(ctx, { includeId: sessionId });
2591
- if (!summaries.some((summary) => summary.id === sessionId)) {
2592
- throw stableAgentError(ErrorCode.enum.UNAUTHORIZED, "session context mismatch");
2593
- }
2594
- }
2595
- function normalizeSessionLoadError(error, sessionId) {
2596
- if (error?.code === ErrorCode.enum.SESSION_NOT_FOUND || isPlainSessionNotFound2(error, sessionId)) {
2597
- return stableAgentError(ErrorCode.enum.SESSION_NOT_FOUND, "session not found");
2598
- }
2599
- return error;
2600
- }
2601
- async function authorizeSessionAccess(runtime, sessionId, callerCtx, sessionContexts) {
2602
- const requestedKey = sessionCacheKey2(sessionId, callerCtx);
2603
- const hasStoredCtx = sessionContexts.has(requestedKey);
2604
- const storedCtx = sessionContexts.get(requestedKey);
2605
- if (hasStoredCtx && callerCtx && !sameSessionCtx(callerCtx, storedCtx)) {
2606
- throw stableAgentError(ErrorCode.enum.UNAUTHORIZED, "session context mismatch");
2607
- }
2608
- if (hasStoredCtx && !callerCtx && !isEmptySessionCtx(storedCtx)) {
2609
- throw stableAgentError(ErrorCode.enum.UNAUTHORIZED, "session context required");
2610
- }
2611
- const accessCtx = callerCtx ?? storedCtx ?? {};
2612
- await loadSession(runtime.sessionStore, accessCtx, sessionId);
2613
- if (!hasStoredCtx) {
2614
- await assertLoadedSessionVisible(runtime.sessionStore, accessCtx, sessionId);
2615
- rememberSessionCtx(sessionContexts, sessionId, accessCtx);
2616
- }
2617
- return accessCtx;
2618
- }
2619
- function rememberSessionCtx(sessionContexts, sessionId, ctx) {
2620
- sessionContexts.set(sessionCacheKey2(sessionId, ctx), isEmptySessionCtx(ctx) ? void 0 : { workspaceId: ctx?.workspaceId, userId: ctx?.userId });
2621
- }
2622
- function sameSessionCtx(a, b) {
2623
- return (a?.workspaceId ?? "") === (b?.workspaceId ?? "") && (a?.userId ?? "") === (b?.userId ?? "");
2624
- }
2625
- function isEmptySessionCtx(ctx) {
2626
- return !ctx?.workspaceId && !ctx?.userId;
2627
- }
2628
- function canAccessStoredSessionCtx(sessionId, callerCtx, sessionContexts) {
2629
- const sessionKey = sessionCacheKey2(sessionId, callerCtx);
2630
- if (!sessionContexts.has(sessionKey)) return true;
2631
- const storedCtx = sessionContexts.get(sessionKey);
2632
- return sameSessionCtx(callerCtx, storedCtx);
2633
- }
2634
- function sessionCacheKey2(sessionId, ctx) {
2635
- return JSON.stringify([sessionId, ctx?.workspaceId ?? "", ctx?.userId ?? ""]);
2636
- }
2637
- function stableAgentError(code, message) {
2638
- return Object.assign(new Error(message), { code });
2639
- }
2640
- async function acquireSessionLock(locks, sessionId) {
2641
- const previous = locks.get(sessionId) ?? Promise.resolve();
2642
- let release;
2643
- const current = previous.catch(() => {
2644
- }).then(() => new Promise((resolve2) => {
2645
- release = resolve2;
2646
- }));
2647
- locks.set(sessionId, current);
2648
- await previous.catch(() => {
2649
- });
2650
- return () => {
2651
- release();
2652
- if (locks.get(sessionId) === current) locks.delete(sessionId);
2653
- };
2654
- }
2655
- function isPlainSessionNotFound2(error, sessionId) {
2656
- return error instanceof Error && (error.message === "session not found" || error.message === `Session not found: ${sessionId}` || error.message === `missing session ${sessionId}`);
2657
- }
2658
- function isSendTerminalEvent(event, turnId) {
2659
- const chunk = event.chunk;
2660
- if (chunk.type === "error") return !chunk.turnId || chunk.turnId === turnId;
2661
- return chunk.type === "agent-end" && chunk.willRetry !== true && chunk.turnId === turnId;
2662
- }
2663
- var AgentLiveEventBuffer = class {
2664
- constructor(maxEvents) {
2665
- this.maxEvents = maxEvents;
2666
- }
2667
- maxEvents;
2668
- sessions = /* @__PURE__ */ new Map();
2669
- eventIndexes = /* @__PURE__ */ new Map();
2670
- latestIndex(sessionKey) {
2671
- return this.eventIndexes.get(sessionKey) ?? 0;
2672
- }
2673
- ensure(sessionKey) {
2674
- let state = this.sessions.get(sessionKey);
2675
- if (!state || state.closed) {
2676
- const latestIndex = this.latestIndex(sessionKey);
2677
- state = {
2678
- events: [],
2679
- subscribers: /* @__PURE__ */ new Set(),
2680
- latestIndex,
2681
- evictedThroughIndex: latestIndex - 1,
2682
- closed: false
2683
- };
2684
- this.sessions.set(sessionKey, state);
2685
- }
2686
- return state;
2687
- }
2688
- publish(sessionKey, sessionId, chunk) {
2689
- const state = this.ensure(sessionKey);
2690
- if (state.closed) return;
2691
- const eventIndex = this.latestIndex(sessionKey);
2692
- this.eventIndexes.set(sessionKey, eventIndex + 1);
2693
- const event = {
2694
- v: 1,
2695
- eventIndex,
2696
- timestamp: Date.now(),
2697
- sessionId,
2698
- chunk
2699
- };
2700
- state.latestIndex = event.eventIndex + 1;
2701
- state.events.push(event);
2702
- if (state.events.length > this.maxEvents) {
2703
- const evicted = state.events.splice(0, state.events.length - this.maxEvents);
2704
- state.evictedThroughIndex = evicted[evicted.length - 1]?.eventIndex ?? state.evictedThroughIndex;
2705
- }
2706
- for (const subscriber of state.subscribers) subscriber.push(event);
2707
- }
2708
- stream(sessionKey, startIndex) {
2709
- const state = this.ensure(sessionKey);
2710
- this.assertReplayable(state, startIndex);
2711
- return {
2712
- [Symbol.asyncIterator]: () => {
2713
- this.assertReplayable(state, startIndex);
2714
- return createLiveIterator(state, startIndex);
2715
- }
2716
- };
2717
- }
2718
- close(sessionKey) {
2719
- const state = this.sessions.get(sessionKey);
2720
- if (!state) return;
2721
- state.closed = true;
2722
- state.bridge?.unsubscribe();
2723
- for (const subscriber of state.subscribers) subscriber.close();
2724
- state.subscribers.clear();
2725
- }
2726
- dispose() {
2727
- for (const [sessionId] of this.sessions) this.close(sessionId);
2728
- this.sessions.clear();
2729
- }
2730
- assertReplayable(state, startIndex) {
2731
- if (!Number.isInteger(startIndex) || startIndex < 0) {
2732
- throw cursorOutOfRangeError("startIndex must be a non-negative integer", {
2733
- startIndex,
2734
- latestIndex: state.latestIndex
2735
- });
2736
- }
2737
- if (startIndex <= state.evictedThroughIndex) {
2738
- throw new AgentNotImplementedError("Historical stream replay is not implemented until T1.");
2739
- }
2740
- if (startIndex > state.latestIndex) {
2741
- throw cursorOutOfRangeError(`startIndex ${startIndex} is ahead of next eventIndex ${state.latestIndex}`, {
2742
- startIndex,
2743
- latestIndex: state.latestIndex
2744
- });
2745
- }
2746
- }
2747
- };
2748
- function cursorOutOfRangeError(message, details) {
2749
- return Object.assign(new RangeError(message), {
2750
- code: ErrorCode.enum.CURSOR_OUT_OF_RANGE,
2751
- details
2752
- });
2753
- }
2754
- function createLiveIterator(state, startIndex) {
2755
- const queued = [];
2756
- const waiters = [];
2757
- let active = true;
2758
- const subscriber = {
2759
- push(event) {
2760
- if (!active) return;
2761
- const waiter = waiters.shift();
2762
- if (waiter) waiter({ value: event, done: false });
2763
- else queued.push(event);
2764
- },
2765
- close() {
2766
- if (!active) return;
2767
- active = false;
2768
- while (waiters.length > 0) waiters.shift()?.({ value: void 0, done: true });
2769
- }
2770
- };
2771
- state.subscribers.add(subscriber);
2772
- queued.push(...state.events.filter((event) => event.eventIndex >= startIndex));
2773
- return {
2774
- async next() {
2775
- if (queued.length > 0) return { value: queued.shift(), done: false };
2776
- if (!active || state.closed) return { value: void 0, done: true };
2777
- return new Promise((resolve2) => waiters.push(resolve2));
2778
- },
2779
- async return() {
2780
- active = false;
2781
- state.subscribers.delete(subscriber);
2782
- while (waiters.length > 0) waiters.shift()?.({ value: void 0, done: true });
2783
- return { value: void 0, done: true };
2784
- }
2785
- };
2786
- }
2787
-
2788
- export {
2789
- PI_CHAT_REPLAY_GAP,
2790
- PI_CHAT_CURSOR_AHEAD,
2791
- normalizeMeteringUsage,
2792
- createAgent,
2793
- createAgentRuntimeBridge
2794
- };