@hachej/boring-agent 0.1.64 → 0.1.66

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.
@@ -0,0 +1,1812 @@
1
+ import {
2
+ noopTelemetry,
3
+ safeCapture
4
+ } from "./chunk-AQBXNPMD.js";
5
+ import {
6
+ createLogger
7
+ } from "./chunk-AJZHR626.js";
8
+ import {
9
+ ErrorCode
10
+ } from "./chunk-XZKU7FBV.js";
11
+
12
+ // src/server/harness/pi-coding-agent/createHarness.ts
13
+ import { AsyncLocalStorage } from "async_hooks";
14
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
15
+ import { extname, join as join2 } from "path";
16
+ import {
17
+ createAgentSession,
18
+ SessionManager,
19
+ AuthStorage,
20
+ ModelRegistry,
21
+ DefaultResourceLoader,
22
+ SettingsManager as SettingsManager2,
23
+ getAgentDir as getAgentDir2,
24
+ loadSkills
25
+ } from "@mariozechner/pi-coding-agent";
26
+
27
+ // src/server/harness/pi-coding-agent/tool-adapter.ts
28
+ var BORING_TOOL_ERROR_MARKER = "__boringToolError";
29
+ function markToolResultErrorDetails(details) {
30
+ return details && typeof details === "object" && !Array.isArray(details) ? { ...details, [BORING_TOOL_ERROR_MARKER]: true } : { [BORING_TOOL_ERROR_MARKER]: true, details };
31
+ }
32
+ function unmarkToolResultErrorDetails(details) {
33
+ if (!details || typeof details !== "object" || Array.isArray(details)) return { isMarked: false, details };
34
+ const record = { ...details };
35
+ if (record[BORING_TOOL_ERROR_MARKER] !== true) return { isMarked: false, details };
36
+ delete record[BORING_TOOL_ERROR_MARKER];
37
+ if (Object.keys(record).length === 1 && "details" in record) return { isMarked: true, details: record.details };
38
+ return { isMarked: true, details: record };
39
+ }
40
+ function toolTelemetryProperties(toolName, sessionId, status, startedAt, result) {
41
+ const properties = {
42
+ toolName,
43
+ status,
44
+ durationMs: Date.now() - startedAt
45
+ };
46
+ if (sessionId) properties.sessionId = sessionId;
47
+ const errorCode = result?.details?.code;
48
+ if (status === "error") {
49
+ properties.errorCode = ErrorCode.safeParse(errorCode).success ? errorCode : ErrorCode.enum.TOOL_EXECUTION_ERROR;
50
+ }
51
+ return properties;
52
+ }
53
+ function adaptToolForPi(tool, sessionId, telemetry = noopTelemetry, getRunContext) {
54
+ return {
55
+ name: tool.name,
56
+ label: tool.name,
57
+ description: tool.description,
58
+ parameters: tool.parameters,
59
+ promptSnippet: tool.promptSnippet ?? tool.description,
60
+ async execute(toolCallId, params, signal, onUpdate, _ctx) {
61
+ const startedAt = Date.now();
62
+ let emittedFailure = false;
63
+ try {
64
+ const runContext = getRunContext?.();
65
+ const result = await tool.execute(params, {
66
+ toolCallId,
67
+ abortSignal: signal ?? new AbortController().signal,
68
+ onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0,
69
+ sessionId,
70
+ userId: runContext?.userId,
71
+ userEmail: runContext?.userEmail,
72
+ userEmailVerified: runContext?.userEmailVerified,
73
+ workspaceId: runContext?.workspaceId,
74
+ requestId: runContext?.requestId
75
+ });
76
+ safeCapture(telemetry, {
77
+ name: result.isError ? "agent.tool.failed" : "agent.tool.completed",
78
+ properties: toolTelemetryProperties(
79
+ tool.name,
80
+ sessionId,
81
+ result.isError ? "error" : "ok",
82
+ startedAt,
83
+ result
84
+ )
85
+ });
86
+ if (result.isError) {
87
+ emittedFailure = true;
88
+ return {
89
+ content: result.content,
90
+ details: markToolResultErrorDetails(result.details)
91
+ };
92
+ }
93
+ return {
94
+ content: result.content,
95
+ details: result.details
96
+ };
97
+ } catch (error) {
98
+ if (!emittedFailure) {
99
+ safeCapture(telemetry, {
100
+ name: "agent.tool.failed",
101
+ properties: toolTelemetryProperties(tool.name, sessionId, "error", startedAt)
102
+ });
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+ };
108
+ }
109
+ function adaptToolsForPi(tools, sessionId, telemetry, getRunContext) {
110
+ return tools.map((tool) => adaptToolForPi(tool, sessionId, telemetry, getRunContext));
111
+ }
112
+
113
+ // src/server/harness/pi-coding-agent/piFollowUpQueueCompat.ts
114
+ function createPiFollowUpQueueCompat() {
115
+ let queue = [];
116
+ const seenNonces = /* @__PURE__ */ new Set();
117
+ function record(text, options) {
118
+ const nonce = options?.clientNonce;
119
+ if (nonce && seenNonces.has(nonce)) return false;
120
+ queue.push({
121
+ text,
122
+ displayText: options?.displayText ?? text,
123
+ clientNonce: options?.clientNonce,
124
+ clientSeq: options?.clientSeq
125
+ });
126
+ if (nonce) seenNonces.add(nonce);
127
+ return true;
128
+ }
129
+ function clear(piSession, options) {
130
+ syncWithPi(piSession);
131
+ const removed = removeNativeFollowUp(options);
132
+ if (!options?.clientNonce && options?.clientSeq === void 0) {
133
+ removePiQueuedFollowUp(piSession);
134
+ return;
135
+ }
136
+ for (const item of removed) removePiQueuedFollowUp(piSession, item.request.text, item.textOrdinal);
137
+ }
138
+ function syncWithPi(piSession) {
139
+ if (!queue.length) return;
140
+ const piTexts = readPiQueuedFollowUpTexts(piSession);
141
+ if (piTexts.length === 0) {
142
+ queue = [];
143
+ return;
144
+ }
145
+ if (piTexts.length >= queue.length) return;
146
+ const remaining = [];
147
+ let searchEnd = queue.length;
148
+ for (let index = piTexts.length - 1; index >= 0; index -= 1) {
149
+ const text = piTexts[index];
150
+ let matchIndex = -1;
151
+ for (let queueIndex = searchEnd - 1; queueIndex >= 0; queueIndex -= 1) {
152
+ const item = queue[queueIndex];
153
+ if (item.text === text || item.displayText === text) {
154
+ matchIndex = queueIndex;
155
+ break;
156
+ }
157
+ }
158
+ if (matchIndex < 0) break;
159
+ remaining.unshift(queue[matchIndex]);
160
+ searchEnd = matchIndex;
161
+ }
162
+ if (remaining.length !== piTexts.length) return;
163
+ queue = remaining;
164
+ }
165
+ function removeNativeFollowUp(options) {
166
+ if (!queue.length) {
167
+ if (!hasFollowUpSelector(options)) seenNonces.clear();
168
+ return [];
169
+ }
170
+ const removed = [];
171
+ const next = [];
172
+ const textCounts = /* @__PURE__ */ new Map();
173
+ for (const request of queue) {
174
+ const textOrdinal = textCounts.get(request.text) ?? 0;
175
+ textCounts.set(request.text, textOrdinal + 1);
176
+ if (matchesFollowUpSelector(request, options)) removed.push({ request, textOrdinal });
177
+ else next.push(request);
178
+ }
179
+ queue = next;
180
+ if (queue.length === 0) seenNonces.clear();
181
+ return removed;
182
+ }
183
+ return { record, clear };
184
+ }
185
+ function removePiQueuedFollowUp(piSession, text, textOrdinal = 0) {
186
+ const queue = piQueueAccess(piSession);
187
+ if (!text) {
188
+ queue.clearAll();
189
+ return;
190
+ }
191
+ if (queue.queuedMessages) {
192
+ removeFirstMatchingOrdinal(queue.queuedMessages, (message) => userMessageText(message) === text, textOrdinal);
193
+ }
194
+ if (queue.followUpMessages) {
195
+ removeFirstMatchingOrdinal(queue.followUpMessages, (message) => message === text, textOrdinal);
196
+ }
197
+ queue.emitUpdate();
198
+ }
199
+ function readPiQueuedFollowUpTexts(piSession) {
200
+ const queue = piQueueAccess(piSession);
201
+ if (queue.queuedMessages) {
202
+ return queue.queuedMessages.map(userMessageText).filter((text) => text.length > 0);
203
+ }
204
+ if (queue.followUpMessages) return [...queue.followUpMessages];
205
+ return [];
206
+ }
207
+ function piQueueAccess(piSession) {
208
+ const session = piSession;
209
+ const followUpMessages = Array.isArray(session._followUpMessages) ? session._followUpMessages : void 0;
210
+ const queuedMessages = Array.isArray(session.agent?.followUpQueue?.messages) ? session.agent.followUpQueue.messages : void 0;
211
+ return {
212
+ followUpMessages,
213
+ queuedMessages,
214
+ clearAll() {
215
+ session.agent?.clearFollowUpQueue?.();
216
+ if (followUpMessages) followUpMessages.length = 0;
217
+ session._emitQueueUpdate?.();
218
+ },
219
+ emitUpdate() {
220
+ session._emitQueueUpdate?.();
221
+ }
222
+ };
223
+ }
224
+ function userMessageText(message) {
225
+ const content = message.content;
226
+ if (!Array.isArray(content)) return "";
227
+ return content.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
228
+ }
229
+ function removeFirstMatchingOrdinal(items, matches, ordinal) {
230
+ let seen = 0;
231
+ const index = items.findIndex((item) => {
232
+ if (!matches(item)) return false;
233
+ if (seen++ !== ordinal) return false;
234
+ return true;
235
+ });
236
+ if (index >= 0) items.splice(index, 1);
237
+ }
238
+ function hasFollowUpSelector(options) {
239
+ return Boolean(options?.clientNonce) || options?.clientSeq !== void 0;
240
+ }
241
+ function matchesFollowUpSelector(item, options) {
242
+ if (!hasFollowUpSelector(options)) return true;
243
+ if (options?.clientNonce) return item.clientNonce === options.clientNonce;
244
+ return options?.clientSeq !== void 0 && item.clientSeq === options.clientSeq;
245
+ }
246
+
247
+ // src/server/pi-chat/PiAgentSessionAdapter.ts
248
+ function normalizePromptInput(input) {
249
+ if (typeof input === "string") return { text: input };
250
+ return input;
251
+ }
252
+ function createPiAgentSessionAdapter(session, options = {}) {
253
+ const followUpQueue = createPiFollowUpQueueCompat();
254
+ const adapter = {
255
+ readSnapshot() {
256
+ return {
257
+ state: session.state,
258
+ messages: session.messages,
259
+ isStreaming: session.isStreaming,
260
+ isRetrying: session.isRetrying,
261
+ retryAttempt: session.retryAttempt,
262
+ pendingMessageCount: session.pendingMessageCount,
263
+ steeringMessages: session.getSteeringMessages(),
264
+ followUpMessages: session.getFollowUpMessages(),
265
+ followUpMode: session.followUpMode,
266
+ sessionId: options.sessionId ?? session.sessionId,
267
+ sessionName: session.sessionName
268
+ };
269
+ },
270
+ currentModel() {
271
+ const model = session.model;
272
+ return typeof model?.provider === "string" && typeof model?.id === "string" ? { provider: model.provider, id: model.id } : void 0;
273
+ },
274
+ subscribe(listener) {
275
+ return session.subscribe(listener);
276
+ },
277
+ async prompt(input) {
278
+ const { text, options: options2 } = normalizePromptInput(input);
279
+ await session.prompt(text, options2);
280
+ },
281
+ async followUp(text, followUpOptions) {
282
+ const accepted = followUpQueue.record(text, followUpOptions);
283
+ if (!accepted) return;
284
+ await session.followUp(text);
285
+ },
286
+ clearFollowUp(selector) {
287
+ followUpQueue.clear(session, selector);
288
+ },
289
+ async abort() {
290
+ await session.abort();
291
+ }
292
+ };
293
+ if (typeof session.abortRetry === "function") {
294
+ adapter.abortRetry = () => session.abortRetry?.();
295
+ }
296
+ if (options.continueQueuedFollowUp) {
297
+ adapter.continueQueuedFollowUp = options.continueQueuedFollowUp;
298
+ }
299
+ return adapter;
300
+ }
301
+
302
+ // src/server/harness/pi-coding-agent/sessions.ts
303
+ import { randomUUID } from "crypto";
304
+ import {
305
+ readdir,
306
+ readFile,
307
+ stat as fsStat,
308
+ rm,
309
+ mkdir,
310
+ writeFile,
311
+ appendFile,
312
+ rename,
313
+ open
314
+ } from "fs/promises";
315
+ import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync } from "fs";
316
+ import { join, basename, resolve } from "path";
317
+ import { homedir } from "os";
318
+
319
+ // src/server/config/env.ts
320
+ function getEnv(name) {
321
+ return process.env[name];
322
+ }
323
+ function getEnvSnapshot() {
324
+ return { ...process.env };
325
+ }
326
+ function setEnvDefault(name, value) {
327
+ if (process.env[name] !== void 0) return false;
328
+ process.env[name] = value;
329
+ return true;
330
+ }
331
+
332
+ // src/server/harness/pi-coding-agent/sessions.ts
333
+ import {
334
+ parseSessionEntries,
335
+ CURRENT_SESSION_VERSION
336
+ } from "@mariozechner/pi-coding-agent";
337
+ function sessionBaseDir(explicitRoot) {
338
+ const explicit = explicitRoot?.trim();
339
+ if (explicit) return resolve(explicit);
340
+ const configured = getEnv(SESSION_ROOT_ENV)?.trim();
341
+ return configured ? resolve(configured) : join(homedir(), ".pi", "agent", "sessions");
342
+ }
343
+ function defaultSessionDir(cwd, explicitRoot) {
344
+ const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
345
+ return join(sessionBaseDir(explicitRoot), safePath);
346
+ }
347
+ var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
348
+ var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
349
+ var SESSION_ROOT_ENV = "BORING_AGENT_SESSION_ROOT";
350
+ var SUMMARY_PREFIX_BYTES = 64 * 1024;
351
+ var DEFAULT_LEGACY_WORKSPACE_ID = "default";
352
+ function sessionDirForNamespace(namespace, explicitRoot) {
353
+ const safeNamespace = namespace.trim();
354
+ if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
355
+ throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
356
+ }
357
+ return join(sessionBaseDir(explicitRoot), safeNamespace);
358
+ }
359
+ function normalizeListOptions(options) {
360
+ return {
361
+ limit: options?.limit === void 0 ? void 0 : Math.max(0, options.limit),
362
+ offset: Math.max(0, options?.offset ?? 0),
363
+ includeId: options?.includeId
364
+ };
365
+ }
366
+ var PiSessionStore = class {
367
+ cwd;
368
+ sessionDir;
369
+ allowLegacyUnscopedAccess;
370
+ prefixCache = /* @__PURE__ */ new Map();
371
+ listInFlight = /* @__PURE__ */ new Map();
372
+ constructor(cwd, options) {
373
+ this.cwd = cwd;
374
+ if (typeof options === "string") {
375
+ this.sessionDir = options;
376
+ this.allowLegacyUnscopedAccess = true;
377
+ return;
378
+ }
379
+ this.allowLegacyUnscopedAccess = true;
380
+ this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace, options.sessionRoot) : defaultSessionDir(options?.storageCwd ?? cwd, options?.sessionRoot));
381
+ }
382
+ getSessionDir() {
383
+ return this.sessionDir;
384
+ }
385
+ async list(ctx, options) {
386
+ const normalizedOptions = normalizeListOptions(options);
387
+ const inFlightKey = JSON.stringify([
388
+ ctx.workspaceId,
389
+ ctx.userId ?? null,
390
+ normalizedOptions.limit ?? null,
391
+ normalizedOptions.offset,
392
+ normalizedOptions.includeId ?? null
393
+ ]);
394
+ const inFlight = this.listInFlight.get(inFlightKey);
395
+ if (inFlight) return inFlight;
396
+ const promise = this.listUncached(ctx, normalizedOptions);
397
+ this.listInFlight.set(inFlightKey, promise);
398
+ try {
399
+ return await promise;
400
+ } finally {
401
+ if (this.listInFlight.get(inFlightKey) === promise) this.listInFlight.delete(inFlightKey);
402
+ }
403
+ }
404
+ async listUncached(ctx, options) {
405
+ const files = await readdir(this.sessionDir).catch(() => []);
406
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
407
+ const filepaths = jsonlFiles.map((f) => join(this.sessionDir, f));
408
+ const fileStats = await Promise.all(filepaths.map(async (filepath) => {
409
+ try {
410
+ return { filepath, stat: await fsStat(filepath) };
411
+ } catch {
412
+ return null;
413
+ }
414
+ }));
415
+ const existingFiles = fileStats.filter((item) => item !== null);
416
+ const referencedPiFiles = await this.referencedPiFiles(existingFiles);
417
+ const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({
418
+ ...file,
419
+ sortMtimeMs: await this.sessionSortMtimeMs(file)
420
+ })));
421
+ visibleFiles.sort((a, b) => b.sortMtimeMs - a.sortMtimeMs);
422
+ const { offset, limit } = options;
423
+ const pageSummaries = await this.summarizeVisiblePage(visibleFiles, { ctx, offset, limit });
424
+ const includeId = options.includeId;
425
+ if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
426
+ const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
427
+ return includeSummary ? [...pageSummaries, includeSummary] : pageSummaries;
428
+ }
429
+ async create(ctx, init) {
430
+ await mkdir(this.sessionDir, { recursive: true });
431
+ const id = randomUUID();
432
+ const now = (/* @__PURE__ */ new Date()).toISOString();
433
+ const header = {
434
+ type: "session",
435
+ version: CURRENT_SESSION_VERSION,
436
+ id,
437
+ timestamp: now,
438
+ cwd: this.cwd,
439
+ boringSessionCtx: normalizeSessionCtx(ctx) ?? {}
440
+ };
441
+ const lines = [JSON.stringify(header)];
442
+ if (init?.title) {
443
+ const infoEntry = {
444
+ type: "session_info",
445
+ id: randomUUID(),
446
+ parentId: null,
447
+ timestamp: now,
448
+ name: init.title
449
+ };
450
+ lines.push(JSON.stringify(infoEntry));
451
+ }
452
+ const filepath = join(this.sessionDir, `${id}.jsonl`);
453
+ await writeFile(filepath, lines.join("\n") + "\n", "utf-8");
454
+ return {
455
+ id,
456
+ title: init?.title ?? "New session",
457
+ createdAt: now,
458
+ updatedAt: now,
459
+ turnCount: 0
460
+ };
461
+ }
462
+ async load(ctx, sessionId) {
463
+ const resolved = await this.resolveSessionTranscript(ctx, sessionId);
464
+ const title = extractTitle(resolved.sessionEntries) ?? extractTitle(resolved.linkedEntries) ?? "New session";
465
+ const turnCount = countUserTurns(resolved.transcriptEntries);
466
+ const updatedAtMs = Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
467
+ return {
468
+ id: resolved.resolvedSessionId,
469
+ title,
470
+ createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
471
+ updatedAt: new Date(updatedAtMs).toISOString(),
472
+ turnCount
473
+ };
474
+ }
475
+ /**
476
+ * Returns the persisted pi message objects in file order so callers can run
477
+ * them through buildPiChatHistory — the same canonical projection the live
478
+ * event path uses. This is the cold-load counterpart to the live snapshot.
479
+ */
480
+ async loadEntries(ctx, sessionId) {
481
+ const resolved = await this.resolveSessionTranscript(ctx, sessionId);
482
+ const messages = resolved.transcriptEntries.filter((entry) => entry.type === "message").map((entry) => entry.message);
483
+ return { id: resolved.resolvedSessionId, messages };
484
+ }
485
+ async resolveSessionTranscript(ctx, sessionId) {
486
+ const filepath = await this.resolveSessionFile(sessionId, ctx);
487
+ let content;
488
+ try {
489
+ content = await readFile(filepath, "utf-8");
490
+ } catch {
491
+ throw new Error(`Session not found: ${sessionId}`);
492
+ }
493
+ const fileEntries = safeParseEntries(content);
494
+ if (fileEntries.some((e) => e.type === "ui_snapshot")) {
495
+ const compacted = fileEntries.filter((e) => e.type !== "ui_snapshot").map((e) => JSON.stringify(e)).join("\n") + "\n";
496
+ const tmp = `${filepath}.compact-${randomUUID()}`;
497
+ try {
498
+ await writeFile(tmp, compacted, "utf-8");
499
+ await rename(tmp, filepath);
500
+ } catch {
501
+ await rm(tmp, { force: true }).catch(() => {
502
+ });
503
+ }
504
+ }
505
+ const header = fileEntries.find(
506
+ (e) => e.type === "session"
507
+ );
508
+ if (!this.headerBelongsToCtx(header, ctx)) throw new Error(`Session not found: ${sessionId}`);
509
+ const sessionEntries = fileEntries.filter(
510
+ (e) => e.type !== "session" && e.type !== "ui_snapshot"
511
+ );
512
+ const fileStat = await fsStat(filepath);
513
+ const linkedPiFile = extractPiSessionFilePath(fileEntries);
514
+ const linked = linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
515
+ const linkedEntries = linked?.entries.filter(
516
+ (e) => e.type !== "session"
517
+ ) ?? [];
518
+ const transcriptEntries = linkedEntries.length > 0 ? linkedEntries : sessionEntries;
519
+ return {
520
+ resolvedSessionId: header?.id ?? sessionId,
521
+ header,
522
+ sessionEntries,
523
+ linkedEntries,
524
+ transcriptEntries,
525
+ fileStat,
526
+ linkedMtimeMs: linked?.mtime.getTime()
527
+ };
528
+ }
529
+ // Synchronous variant used during session initialization so that no async
530
+ // I/O hop is introduced before createAgentSession (which would break test
531
+ // timing when fake timers are in use). The file is tiny (metadata only).
532
+ loadPiSessionFileSync(ctx, sessionId) {
533
+ if (!SAFE_ID.test(sessionId)) return null;
534
+ try {
535
+ const direct = join(this.sessionDir, `${sessionId}.jsonl`);
536
+ let filepath = direct;
537
+ let content;
538
+ try {
539
+ content = readFileSync(direct, "utf-8");
540
+ } catch {
541
+ const files = readdirSync(this.sessionDir).filter(
542
+ (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
543
+ );
544
+ if (files.length === 0) return null;
545
+ filepath = join(this.sessionDir, files[0]);
546
+ content = readFileSync(filepath, "utf-8");
547
+ }
548
+ const entries = safeParseEntries(content);
549
+ const header = entries.find((entry) => entry.type === "session");
550
+ if (!this.headerBelongsToCtx(header, ctx)) return null;
551
+ const linkedPiFile = extractPiSessionFilePath(entries);
552
+ if (linkedPiFile) return linkedPiFile;
553
+ if (!isTimestampNamedPiSessionFile(filepath, sessionId)) return null;
554
+ const existingWrapper = this.findWrapperReferencingNativeSessionSync(filepath);
555
+ if (existingWrapper) {
556
+ const existingEntries = parseJsonlPrefixEntries(readJsonlPrefixSync(existingWrapper));
557
+ if (extractSessionHeaderId(existingEntries) !== sessionId) return null;
558
+ const wrapperHeader = existingEntries.find((entry) => entry.type === "session");
559
+ if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
560
+ return extractPiSessionFilePath(existingEntries);
561
+ }
562
+ this.ensureWrapperForNativeSessionSync(sessionId, filepath, entries, ctx);
563
+ return filepath;
564
+ } catch {
565
+ return null;
566
+ }
567
+ }
568
+ async loadPiSessionFile(ctx, sessionId) {
569
+ if (!SAFE_ID.test(sessionId)) return null;
570
+ try {
571
+ const direct = join(this.sessionDir, `${sessionId}.jsonl`);
572
+ let filepath = direct;
573
+ let content;
574
+ try {
575
+ content = await readFile(direct, "utf-8");
576
+ } catch {
577
+ const files = await readdir(this.sessionDir).catch(() => []);
578
+ const match = files.find(
579
+ (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
580
+ );
581
+ if (!match) return null;
582
+ filepath = join(this.sessionDir, match);
583
+ content = await readFile(filepath, "utf-8");
584
+ }
585
+ const entries = safeParseEntries(content);
586
+ const header = entries.find((entry) => entry.type === "session");
587
+ if (!this.headerBelongsToCtx(header, ctx)) return null;
588
+ const linkedPiFile = extractPiSessionFilePath(entries);
589
+ if (linkedPiFile) return linkedPiFile;
590
+ if (!isTimestampNamedPiSessionFile(filepath, sessionId)) return null;
591
+ const existingWrapper = await this.findWrapperReferencingNativeSession(filepath);
592
+ if (existingWrapper) {
593
+ const wrapperSessionId = await this.readSessionFileId(existingWrapper);
594
+ if (wrapperSessionId !== sessionId) return null;
595
+ const wrapperEntries = parseJsonlPrefixEntries(await readJsonlPrefix(existingWrapper));
596
+ const wrapperHeader = wrapperEntries.find((entry) => entry.type === "session");
597
+ if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
598
+ return extractPiSessionFilePath(wrapperEntries);
599
+ }
600
+ return await this.ensureWrapperForNativeSession(sessionId, filepath, ctx);
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+ async savePiSessionFile(ctx, sessionId, piFilePath) {
606
+ const filepath = await this.resolveSessionFile(sessionId, ctx);
607
+ const entry = JSON.stringify({
608
+ type: "pi_session_file",
609
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
610
+ path: piFilePath
611
+ });
612
+ await appendFile(filepath, entry + "\n");
613
+ }
614
+ async delete(ctx, sessionId) {
615
+ const filepath = await this.resolveSessionFile(sessionId, ctx).catch(
616
+ () => null
617
+ );
618
+ if (!filepath) return;
619
+ const fileSessionId = await this.readSessionFileId(filepath);
620
+ if (fileSessionId && fileSessionId !== sessionId) return;
621
+ const linkedPiFile = await this.linkedPiFileFor(filepath);
622
+ await rm(filepath, { force: true });
623
+ this.prefixCache.delete(filepath);
624
+ if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
625
+ await rm(linkedPiFile, { force: true });
626
+ this.prefixCache.delete(linkedPiFile);
627
+ }
628
+ }
629
+ async resolveSessionFile(sessionId, ctx) {
630
+ if (!SAFE_ID.test(sessionId)) {
631
+ throw new Error(`Session not found: ${sessionId}`);
632
+ }
633
+ const direct = join(this.sessionDir, `${sessionId}.jsonl`);
634
+ try {
635
+ await fsStat(direct);
636
+ if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
637
+ return direct;
638
+ } catch {
639
+ }
640
+ const files = await readdir(this.sessionDir).catch(() => []);
641
+ const match = files.find(
642
+ (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
643
+ );
644
+ if (!match) throw new Error(`Session not found: ${sessionId}`);
645
+ const matchedPath = join(this.sessionDir, match);
646
+ if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) {
647
+ if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
648
+ return matchedPath;
649
+ }
650
+ const existingWrapper = await this.findWrapperReferencingNativeSession(matchedPath);
651
+ if (existingWrapper) {
652
+ const wrapperSessionId = await this.readSessionFileId(existingWrapper);
653
+ if (wrapperSessionId === sessionId) {
654
+ if (ctx) await this.assertFileBelongsToCtx(existingWrapper, ctx, sessionId);
655
+ return existingWrapper;
656
+ }
657
+ throw new Error(`Session not found: ${sessionId}`);
658
+ }
659
+ return this.ensureWrapperForNativeSession(sessionId, matchedPath, ctx);
660
+ }
661
+ async assertFileBelongsToCtx(filepath, ctx, sessionId) {
662
+ const entries = parseJsonlPrefixEntries(await readJsonlPrefix(filepath));
663
+ const header = entries.find((entry) => entry.type === "session");
664
+ if (!this.headerBelongsToCtx(header, ctx)) throw new Error(`Session not found: ${sessionId}`);
665
+ }
666
+ async readSessionFileId(filepath) {
667
+ try {
668
+ const entries = parseJsonlPrefixEntries(await readJsonlPrefix(filepath));
669
+ return extractSessionHeaderId(entries);
670
+ } catch {
671
+ return null;
672
+ }
673
+ }
674
+ async linkedPiFileFor(filepath) {
675
+ try {
676
+ const content = await readFile(filepath, "utf-8");
677
+ return extractPiSessionFilePath(safeParseEntries(content));
678
+ } catch {
679
+ return null;
680
+ }
681
+ }
682
+ async referencedPiFiles(files) {
683
+ const referenced = /* @__PURE__ */ new Set();
684
+ await Promise.all(files.map(async ({ filepath, stat }) => {
685
+ try {
686
+ const piFilePath = (await this.readPrefixCache(filepath, stat)).referencedPiFile;
687
+ if (piFilePath && resolve(piFilePath) !== resolve(filepath)) {
688
+ referenced.add(resolve(piFilePath));
689
+ }
690
+ } catch {
691
+ }
692
+ }));
693
+ return referenced;
694
+ }
695
+ async sessionSortMtimeMs({ filepath, stat }) {
696
+ let sortMtimeMs = stat.mtime.getTime();
697
+ try {
698
+ const linkedPiFile = (await this.readPrefixCache(filepath, stat)).referencedPiFile;
699
+ if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
700
+ const linkedStat = await fsStat(linkedPiFile);
701
+ sortMtimeMs = Math.max(sortMtimeMs, linkedStat.mtime.getTime());
702
+ }
703
+ } catch {
704
+ }
705
+ return sortMtimeMs;
706
+ }
707
+ async summarizeFile(ctx, filepath, existingStat) {
708
+ try {
709
+ const fileStat = existingStat ?? await fsStat(filepath);
710
+ const cached = this.cachedPrefix(filepath, fileStat);
711
+ if (cached && "summary" in cached && cached.sessionCtx !== void 0 && this.storedCtxBelongsToCtx(cached.sessionCtx, ctx) && await this.cachedSummaryIsFresh(filepath, cached)) {
712
+ return cached.summary ?? null;
713
+ }
714
+ const content = await readJsonlPrefix(filepath);
715
+ const firstNewline = content.indexOf("\n");
716
+ if (firstNewline === -1) return null;
717
+ const header = JSON.parse(
718
+ content.slice(0, firstNewline)
719
+ );
720
+ if (header.type !== "session") return null;
721
+ const sessionCtx = readHeaderSessionCtx(header);
722
+ if (!this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
723
+ const entries = parseJsonlPrefixEntries(content);
724
+ const sessionEntries = entries.filter(
725
+ (e) => e.type !== "session"
726
+ );
727
+ const linkedPiFile = extractPiSessionFilePath(entries);
728
+ const linked = linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? await this.readLinkedPiSessionSummary(linkedPiFile) : null;
729
+ const linkedEntries = linked?.entries.filter(
730
+ (e) => e.type !== "session"
731
+ ) ?? [];
732
+ const title = extractTitle(sessionEntries) ?? extractTitle(linkedEntries) ?? firstUserMessage(linkedEntries) ?? firstUserMessage(sessionEntries) ?? "New session";
733
+ const turnCount = [...sessionEntries, ...linkedEntries].filter(
734
+ (e) => e.type === "message" && e.message?.role === "user"
735
+ ).length;
736
+ const updatedAtMs = Math.max(fileStat.mtime.getTime(), linked?.mtime.getTime() ?? 0);
737
+ const summary = {
738
+ id: header.id,
739
+ title,
740
+ createdAt: header.timestamp,
741
+ updatedAt: new Date(updatedAtMs).toISOString(),
742
+ turnCount
743
+ };
744
+ this.prefixCache.set(filepath, {
745
+ mtimeMs: fileStat.mtime.getTime(),
746
+ size: Number(fileStat.size),
747
+ referencedPiFile: linkedPiFile,
748
+ sessionCtx,
749
+ ...linked ? { linkedMtimeMs: linked.mtime.getTime(), linkedSize: linked.size } : {},
750
+ summary
751
+ });
752
+ return summary;
753
+ } catch {
754
+ return null;
755
+ }
756
+ }
757
+ cachedPrefix(filepath, fileStat) {
758
+ const cached = this.prefixCache.get(filepath);
759
+ if (!cached) return void 0;
760
+ if (cached.mtimeMs !== fileStat.mtime.getTime() || cached.size !== Number(fileStat.size)) return void 0;
761
+ return cached;
762
+ }
763
+ async cachedSummaryIsFresh(filepath, cached) {
764
+ const linkedPiFile = cached.referencedPiFile;
765
+ if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
766
+ try {
767
+ const linkedStat = await fsStat(linkedPiFile);
768
+ return cached.linkedMtimeMs === linkedStat.mtime.getTime() && cached.linkedSize === Number(linkedStat.size);
769
+ } catch {
770
+ return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
771
+ }
772
+ }
773
+ async readPrefixCache(filepath, fileStat) {
774
+ const cached = this.cachedPrefix(filepath, fileStat);
775
+ if (cached) return cached;
776
+ const content = await readJsonlPrefix(filepath);
777
+ const entries = parseJsonlPrefixEntries(content);
778
+ const entry = {
779
+ mtimeMs: fileStat.mtime.getTime(),
780
+ size: Number(fileStat.size),
781
+ referencedPiFile: extractPiSessionFilePath(entries),
782
+ sessionCtx: readHeaderSessionCtx(entries.find((item) => item.type === "session"))
783
+ };
784
+ this.prefixCache.set(filepath, entry);
785
+ return entry;
786
+ }
787
+ async summarizeVisiblePage(visibleFiles, options) {
788
+ if (options.limit === 0) return [];
789
+ const page = [];
790
+ let validSeen = 0;
791
+ let index = 0;
792
+ const batchSize = options.limit === void 0 ? Math.max(1, visibleFiles.length) : Math.max(1, options.limit);
793
+ while (index < visibleFiles.length && (options.limit === void 0 || page.length < options.limit)) {
794
+ const batch = visibleFiles.slice(index, index + batchSize);
795
+ index += batch.length;
796
+ const summaries = await Promise.all(
797
+ batch.map(({ filepath, stat }) => this.summarizeFile(options.ctx, filepath, stat))
798
+ );
799
+ for (const summary of summaries) {
800
+ if (!summary) continue;
801
+ if (validSeen < options.offset) {
802
+ validSeen += 1;
803
+ continue;
804
+ }
805
+ if (options.limit !== void 0 && page.length >= options.limit) break;
806
+ page.push(summary);
807
+ validSeen += 1;
808
+ }
809
+ }
810
+ return page;
811
+ }
812
+ async summarizeIncludedSession(ctx, sessionId, referencedPiFiles) {
813
+ try {
814
+ const filepath = await this.resolveSessionFile(sessionId, ctx);
815
+ if (referencedPiFiles.has(resolve(filepath))) return null;
816
+ return this.summarizeFile(ctx, filepath);
817
+ } catch {
818
+ return null;
819
+ }
820
+ }
821
+ findWrapperReferencingNativeSessionSync(nativePath) {
822
+ const resolvedNativePath = resolve(nativePath);
823
+ try {
824
+ const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
825
+ for (const file of files) {
826
+ const filepath = join(this.sessionDir, file);
827
+ if (resolve(filepath) === resolvedNativePath) continue;
828
+ try {
829
+ const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries(readJsonlPrefixSync(filepath)));
830
+ if (linkedPiFile && resolve(linkedPiFile) === resolvedNativePath) return filepath;
831
+ } catch {
832
+ }
833
+ }
834
+ } catch {
835
+ return null;
836
+ }
837
+ return null;
838
+ }
839
+ async findWrapperReferencingNativeSession(nativePath) {
840
+ const resolvedNativePath = resolve(nativePath);
841
+ const files = await readdir(this.sessionDir).catch(() => []);
842
+ for (const file of files) {
843
+ if (!file.endsWith(".jsonl")) continue;
844
+ const filepath = join(this.sessionDir, file);
845
+ if (resolve(filepath) === resolvedNativePath) continue;
846
+ try {
847
+ const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries(await readJsonlPrefix(filepath)));
848
+ if (linkedPiFile && resolve(linkedPiFile) === resolvedNativePath) return filepath;
849
+ } catch {
850
+ }
851
+ }
852
+ return null;
853
+ }
854
+ ensureWrapperForNativeSessionSync(sessionId, nativePath, entries, ctx) {
855
+ const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
856
+ if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
857
+ try {
858
+ readFileSync(wrapperPath, "utf-8");
859
+ return wrapperPath;
860
+ } catch {
861
+ }
862
+ try {
863
+ writeFileSync(
864
+ wrapperPath,
865
+ buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
866
+ { encoding: "utf-8", flag: "wx" }
867
+ );
868
+ } catch (error) {
869
+ if (error.code !== "EEXIST") throw error;
870
+ }
871
+ this.prefixCache.delete(wrapperPath);
872
+ return wrapperPath;
873
+ }
874
+ async ensureWrapperForNativeSession(sessionId, nativePath, ctx) {
875
+ const wrapperPath = join(this.sessionDir, `${sessionId}.jsonl`);
876
+ if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
877
+ try {
878
+ await fsStat(wrapperPath);
879
+ return wrapperPath;
880
+ } catch {
881
+ }
882
+ const entries = parseJsonlPrefixEntries(await readJsonlPrefix(nativePath));
883
+ try {
884
+ await writeFile(
885
+ wrapperPath,
886
+ buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
887
+ { encoding: "utf-8", flag: "wx" }
888
+ );
889
+ } catch (error) {
890
+ if (error.code !== "EEXIST") throw error;
891
+ }
892
+ this.prefixCache.delete(wrapperPath);
893
+ return wrapperPath;
894
+ }
895
+ async readLinkedPiSession(filepath) {
896
+ try {
897
+ const [fileStat, content] = await Promise.all([
898
+ fsStat(filepath),
899
+ readFile(filepath, "utf-8")
900
+ ]);
901
+ return { entries: safeParseEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+ async readLinkedPiSessionSummary(filepath) {
907
+ try {
908
+ const [fileStat, content] = await Promise.all([
909
+ fsStat(filepath),
910
+ readJsonlPrefix(filepath)
911
+ ]);
912
+ return { entries: parseJsonlPrefixEntries(content), mtime: fileStat.mtime, size: Number(fileStat.size) };
913
+ } catch {
914
+ return null;
915
+ }
916
+ }
917
+ headerBelongsToCtx(header, ctx) {
918
+ return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
919
+ }
920
+ storedCtxBelongsToCtx(storedCtx, ctx) {
921
+ if (storedCtx === null) return this.allowLegacyUnscopedAccess && isLegacyUnscopedCtx(ctx);
922
+ return sameSessionCtx(storedCtx, ctx);
923
+ }
924
+ };
925
+ async function readJsonlPrefix(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
926
+ const handle = await open(filepath, "r");
927
+ try {
928
+ const buffer = Buffer.alloc(maxBytes);
929
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
930
+ let content = buffer.subarray(0, bytesRead).toString("utf-8");
931
+ if (bytesRead === maxBytes) {
932
+ const lastNewline = content.lastIndexOf("\n");
933
+ if (lastNewline >= 0) content = content.slice(0, lastNewline + 1);
934
+ }
935
+ return content;
936
+ } finally {
937
+ await handle.close();
938
+ }
939
+ }
940
+ function readJsonlPrefixSync(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
941
+ const fd = openSync(filepath, "r");
942
+ try {
943
+ const buffer = Buffer.alloc(maxBytes);
944
+ const bytesRead = readSync(fd, buffer, 0, maxBytes, 0);
945
+ let content = buffer.subarray(0, bytesRead).toString("utf-8");
946
+ if (bytesRead === maxBytes) {
947
+ const lastNewline = content.lastIndexOf("\n");
948
+ if (lastNewline >= 0) content = content.slice(0, lastNewline + 1);
949
+ }
950
+ return content;
951
+ } finally {
952
+ closeSync(fd);
953
+ }
954
+ }
955
+ function extractPiSessionFilePath(entries) {
956
+ let piFilePath = null;
957
+ for (const e of entries) {
958
+ const rec = e;
959
+ if (rec.type === "pi_session_file" && typeof rec.path === "string") {
960
+ piFilePath = rec.path;
961
+ }
962
+ }
963
+ return piFilePath;
964
+ }
965
+ function readHeaderSessionCtx(header) {
966
+ if (!header || !Object.prototype.hasOwnProperty.call(header, "boringSessionCtx")) return null;
967
+ const raw = header.boringSessionCtx;
968
+ if (!raw || typeof raw !== "object") return {};
969
+ return normalizeSessionCtx(raw) ?? {};
970
+ }
971
+ function normalizeSessionCtx(ctx) {
972
+ if (!ctx?.workspaceId && !ctx?.userId) return void 0;
973
+ return {
974
+ ...ctx.workspaceId ? { workspaceId: ctx.workspaceId } : {},
975
+ ...ctx.userId ? { userId: ctx.userId } : {}
976
+ };
977
+ }
978
+ function sameSessionCtx(a, b) {
979
+ return (a?.workspaceId ?? "") === (b?.workspaceId ?? "") && (a?.userId ?? "") === (b?.userId ?? "");
980
+ }
981
+ function isEmptySessionCtx(ctx) {
982
+ return !ctx?.workspaceId && !ctx?.userId;
983
+ }
984
+ function isLegacyUnscopedCtx(ctx) {
985
+ return isEmptySessionCtx(ctx) || ctx?.workspaceId === DEFAULT_LEGACY_WORKSPACE_ID && !ctx.userId;
986
+ }
987
+ function buildNativePiSessionWrapper(sessionId, cwd, piFilePath, entries, ctx) {
988
+ const nativeHeader = entries.find((entry) => entry.type === "session");
989
+ const timestamp = nativeHeader?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
990
+ const header = {
991
+ type: "session",
992
+ version: CURRENT_SESSION_VERSION,
993
+ id: sessionId,
994
+ timestamp,
995
+ cwd: nativeHeader?.cwd ?? cwd,
996
+ ...ctx !== void 0 ? { boringSessionCtx: normalizeSessionCtx(ctx) ?? {} } : {}
997
+ };
998
+ return [
999
+ header,
1000
+ {
1001
+ type: "pi_session_file",
1002
+ timestamp,
1003
+ path: piFilePath
1004
+ }
1005
+ ].map((line) => JSON.stringify(line)).join("\n") + "\n";
1006
+ }
1007
+ function extractSessionHeaderId(entries) {
1008
+ const header = entries.find((entry) => entry.type === "session");
1009
+ return header?.id ?? null;
1010
+ }
1011
+ function isTimestampNamedPiSessionFile(filepath, sessionId) {
1012
+ return basename(filepath).endsWith(`_${sessionId}.jsonl`);
1013
+ }
1014
+ function countUserTurns(entries) {
1015
+ return entries.filter(
1016
+ (e) => e.type === "message" && e.message?.role === "user"
1017
+ ).length;
1018
+ }
1019
+ function extractTitle(entries) {
1020
+ const last = entries.filter((e) => e.type === "session_info").pop();
1021
+ return last?.name;
1022
+ }
1023
+ function firstUserMessage(entries) {
1024
+ for (const e of entries) {
1025
+ if (e.type !== "message") continue;
1026
+ const msg = e.message;
1027
+ if (msg?.role !== "user") continue;
1028
+ const text = textFromPiContent(msg.content);
1029
+ if (text) return text.slice(0, 80);
1030
+ }
1031
+ }
1032
+ function safeParseEntries(content) {
1033
+ try {
1034
+ return parseSessionEntries(content);
1035
+ } catch {
1036
+ const results = [];
1037
+ for (const line of content.split("\n")) {
1038
+ if (!line.trim()) continue;
1039
+ try {
1040
+ results.push(JSON.parse(line));
1041
+ } catch {
1042
+ }
1043
+ }
1044
+ return results;
1045
+ }
1046
+ }
1047
+ function parseJsonlPrefixEntries(content) {
1048
+ const entries = [];
1049
+ for (const line of content.split("\n")) {
1050
+ if (!line.trim()) continue;
1051
+ try {
1052
+ entries.push(JSON.parse(line));
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ return entries;
1057
+ }
1058
+ function textFromPiContent(content) {
1059
+ if (typeof content === "string") return content;
1060
+ if (!Array.isArray(content)) return "";
1061
+ return content.map((part) => {
1062
+ const item = part;
1063
+ return item?.type === "text" && typeof item.text === "string" ? item.text : "";
1064
+ }).join("");
1065
+ }
1066
+
1067
+ // src/server/models/modelConfig.ts
1068
+ import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
1069
+ var INFOMANIAK_PROVIDER = "infomaniak";
1070
+ var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
1071
+ var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
1072
+ var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW = 2e5;
1073
+ var DEFAULT_INFOMANIAK_MODELS = [
1074
+ "moonshotai/Kimi-K2.6",
1075
+ "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8",
1076
+ "Qwen/Qwen3.5-122B-A10B-FP8"
1077
+ ];
1078
+ function clean(value) {
1079
+ const trimmed = value?.trim();
1080
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
1081
+ }
1082
+ function readPositiveInt(name, fallback) {
1083
+ const raw = clean(getEnv(name));
1084
+ if (!raw) return fallback;
1085
+ const parsed = Number(raw);
1086
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
1087
+ }
1088
+ function readBoolean(name, fallback) {
1089
+ const raw = clean(getEnv(name))?.toLowerCase();
1090
+ if (!raw) return fallback;
1091
+ if (["1", "true", "yes", "on"].includes(raw)) return true;
1092
+ if (["0", "false", "no", "off"].includes(raw)) return false;
1093
+ return fallback;
1094
+ }
1095
+ function readModelInput(name) {
1096
+ const raw = clean(getEnv(name));
1097
+ if (!raw) return ["text"];
1098
+ const parsed = raw.split(",").map((part) => part.trim()).filter((part) => part === "text" || part === "image");
1099
+ return parsed.length > 0 ? parsed : ["text"];
1100
+ }
1101
+ function readMaxTokensField(name, fallback = "max_completion_tokens") {
1102
+ const raw = clean(getEnv(name));
1103
+ return raw === "max_tokens" || raw === "max_completion_tokens" ? raw : fallback;
1104
+ }
1105
+ function buildOpenAICompletionsCompat(envPrefix, defaults = {}) {
1106
+ return {
1107
+ supportsStore: readBoolean(`${envPrefix}_SUPPORTS_STORE`, defaults.supportsStore ?? false),
1108
+ supportsDeveloperRole: readBoolean(`${envPrefix}_SUPPORTS_DEVELOPER_ROLE`, defaults.supportsDeveloperRole ?? true),
1109
+ supportsReasoningEffort: readBoolean(`${envPrefix}_SUPPORTS_REASONING_EFFORT`, defaults.supportsReasoningEffort ?? true),
1110
+ supportsUsageInStreaming: readBoolean(`${envPrefix}_SUPPORTS_USAGE_IN_STREAMING`, defaults.supportsUsageInStreaming ?? true),
1111
+ maxTokensField: readMaxTokensField(`${envPrefix}_MAX_TOKENS_FIELD`, defaults.maxTokensField)
1112
+ };
1113
+ }
1114
+ function readApiKeyEnv(candidates) {
1115
+ for (const candidate of candidates) {
1116
+ const envName = clean(candidate);
1117
+ if (envName && clean(getEnv(envName))) return envName;
1118
+ }
1119
+ return void 0;
1120
+ }
1121
+ function buildOpenAICompatibleProviderConfig(opts) {
1122
+ return {
1123
+ baseUrl: opts.baseUrl,
1124
+ apiKey: opts.apiKeyEnv,
1125
+ api: "openai-completions",
1126
+ models: opts.models.map((model) => ({
1127
+ id: model.id,
1128
+ name: model.name ?? model.id,
1129
+ api: "openai-completions",
1130
+ reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
1131
+ input: readModelInput(`${opts.envPrefix}_INPUT`),
1132
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
1133
+ contextWindow: readPositiveInt(
1134
+ `${opts.envPrefix}_CONTEXT_WINDOW`,
1135
+ DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
1136
+ ),
1137
+ maxTokens: readPositiveInt(
1138
+ `${opts.envPrefix}_MAX_TOKENS`,
1139
+ DEFAULT_CUSTOM_MODEL_MAX_TOKENS
1140
+ ),
1141
+ compat: buildOpenAICompletionsCompat(opts.envPrefix, opts.compatDefaults)
1142
+ }))
1143
+ };
1144
+ }
1145
+ function readInfomaniakBaseUrl() {
1146
+ const explicit = clean(getEnv("BORING_AGENT_INFOMANIAK_BASE_URL"));
1147
+ if (explicit) return explicit;
1148
+ const productId = clean(getEnv("BORING_AGENT_INFOMANIAK_PRODUCT_ID"));
1149
+ if (!productId) return void 0;
1150
+ return `${INFOMANIAK_API_BASE}/2/ai/${productId}/openai/v1`;
1151
+ }
1152
+ function readInfomaniakModelIds() {
1153
+ const configured = clean(getEnv("BORING_AGENT_INFOMANIAK_MODELS"))?.split(",").map((part) => part.trim()).filter(Boolean);
1154
+ const ids = configured?.length ? configured : [...DEFAULT_INFOMANIAK_MODELS];
1155
+ const legacyModel = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL"));
1156
+ if (legacyModel && !ids.includes(legacyModel)) ids.push(legacyModel);
1157
+ return Array.from(new Set(ids));
1158
+ }
1159
+ function readInfomaniakProvider() {
1160
+ const modelIds = readInfomaniakModelIds();
1161
+ const defaultModelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID")) ?? modelIds[0];
1162
+ const baseUrl = readInfomaniakBaseUrl();
1163
+ const apiKeyEnv = readApiKeyEnv([
1164
+ clean(getEnv("BORING_AGENT_INFOMANIAK_API_KEY_ENV")) ?? "",
1165
+ "INFOMANIAK_API_TOKEN",
1166
+ "BORING_AGENT_INFOMANIAK_API_KEY"
1167
+ ]);
1168
+ if (!defaultModelId || !baseUrl || !apiKeyEnv) return void 0;
1169
+ const provider = clean(getEnv("BORING_AGENT_INFOMANIAK_PROVIDER")) ?? INFOMANIAK_PROVIDER;
1170
+ const modelName = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME"));
1171
+ const models = modelIds.map((id) => {
1172
+ const model = { id };
1173
+ if (id === defaultModelId && modelName) model.name = modelName;
1174
+ return model;
1175
+ });
1176
+ if (!models.some((model) => model.id === defaultModelId)) {
1177
+ models.push({ id: defaultModelId, name: modelName });
1178
+ }
1179
+ return {
1180
+ provider,
1181
+ models: models.map((model) => ({ provider, id: model.id })),
1182
+ defaultModel: { provider, id: defaultModelId },
1183
+ config: buildOpenAICompatibleProviderConfig({
1184
+ models,
1185
+ apiKeyEnv,
1186
+ baseUrl,
1187
+ envPrefix: "BORING_AGENT_INFOMANIAK",
1188
+ // Infomaniak's OpenAI-compatible chat endpoint rejects OpenAI's newer
1189
+ // `developer` role (surfacing as "400 Unexpected message role"). Hosts
1190
+ // can opt back in with BORING_AGENT_INFOMANIAK_SUPPORTS_DEVELOPER_ROLE=1.
1191
+ compatDefaults: { supportsDeveloperRole: false, supportsReasoningEffort: false }
1192
+ })
1193
+ };
1194
+ }
1195
+ function readCustomProvider() {
1196
+ const provider = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_PROVIDER"));
1197
+ const modelId = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_ID"));
1198
+ const baseUrl = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_BASE_URL"));
1199
+ const apiKeyEnv = readApiKeyEnv([
1200
+ clean(getEnv("BORING_AGENT_CUSTOM_MODEL_API_KEY_ENV")) ?? "",
1201
+ "BORING_AGENT_CUSTOM_MODEL_API_KEY"
1202
+ ]);
1203
+ if (!provider || !modelId || !baseUrl || !apiKeyEnv) return void 0;
1204
+ const modelName = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME"));
1205
+ const model = { id: modelId };
1206
+ if (modelName) model.name = modelName;
1207
+ return {
1208
+ provider,
1209
+ models: [{ provider, id: modelId }],
1210
+ defaultModel: { provider, id: modelId },
1211
+ config: buildOpenAICompatibleProviderConfig({
1212
+ models: [model],
1213
+ apiKeyEnv,
1214
+ baseUrl,
1215
+ envPrefix: "BORING_AGENT_CUSTOM_MODEL"
1216
+ })
1217
+ };
1218
+ }
1219
+ function registerConfiguredModelProviders(registry) {
1220
+ const registered = [];
1221
+ const seen = /* @__PURE__ */ new Set();
1222
+ for (const provider of [readInfomaniakProvider(), readCustomProvider()]) {
1223
+ if (!provider) continue;
1224
+ if (seen.has(provider.provider)) continue;
1225
+ registry.registerProvider(provider.provider, provider.config);
1226
+ registered.push(...provider.models);
1227
+ seen.add(provider.provider);
1228
+ }
1229
+ return registered;
1230
+ }
1231
+ function readPiSettingsDefaultModel() {
1232
+ try {
1233
+ const settings = SettingsManager.create(process.cwd(), getAgentDir());
1234
+ const provider = clean(settings.getDefaultProvider());
1235
+ const id = clean(settings.getDefaultModel());
1236
+ return provider && id ? { provider, id } : void 0;
1237
+ } catch {
1238
+ return void 0;
1239
+ }
1240
+ }
1241
+ function readConfiguredDefaultModel() {
1242
+ const encoded = clean(getEnv("BORING_AGENT_DEFAULT_MODEL"));
1243
+ if (encoded) {
1244
+ const idx = encoded.indexOf(":");
1245
+ if (idx > 0 && idx < encoded.length - 1) {
1246
+ return { provider: encoded.slice(0, idx), id: encoded.slice(idx + 1) };
1247
+ }
1248
+ }
1249
+ const explicitProvider = clean(getEnv("BORING_AGENT_DEFAULT_MODEL_PROVIDER"));
1250
+ const explicitId = clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID"));
1251
+ if (explicitProvider && explicitId) {
1252
+ return { provider: explicitProvider, id: explicitId };
1253
+ }
1254
+ return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
1255
+ }
1256
+
1257
+ // src/server/piPackages.ts
1258
+ var PI_PACKAGE_RESOURCE_FILTERS = [
1259
+ "extensions",
1260
+ "skills",
1261
+ "prompts",
1262
+ "themes"
1263
+ ];
1264
+ function sortedFilterValues(source) {
1265
+ return Object.fromEntries(
1266
+ PI_PACKAGE_RESOURCE_FILTERS.map((filter) => {
1267
+ const value = source[filter];
1268
+ return [filter, value ? [...value].sort() : value];
1269
+ })
1270
+ );
1271
+ }
1272
+ function hasResourceFilters(source) {
1273
+ return PI_PACKAGE_RESOURCE_FILTERS.some((filter) => source[filter] !== void 0);
1274
+ }
1275
+ function piPackageSourceKey(source) {
1276
+ if (typeof source === "string") return JSON.stringify({ source });
1277
+ if (!hasResourceFilters(source)) return JSON.stringify({ source: source.source });
1278
+ return JSON.stringify({
1279
+ source: source.source,
1280
+ ...sortedFilterValues(source)
1281
+ });
1282
+ }
1283
+ function compactPiPackages(sources) {
1284
+ const seen = /* @__PURE__ */ new Set();
1285
+ const result = [];
1286
+ for (const source of sources) {
1287
+ if (!source) continue;
1288
+ const key = piPackageSourceKey(source);
1289
+ if (seen.has(key)) continue;
1290
+ seen.add(key);
1291
+ result.push(source);
1292
+ }
1293
+ return result;
1294
+ }
1295
+ function mergePiPackageSources(base = [], additional = []) {
1296
+ return compactPiPackages([...base, ...additional]);
1297
+ }
1298
+
1299
+ // src/server/harness/pi-coding-agent/createHarness.ts
1300
+ var WORKSPACE_PATHS_GUIDELINE = [
1301
+ "## Workspace paths",
1302
+ "",
1303
+ '- The "Current working directory" line in this prompt is the workspace root. Tool path arguments must be relative to it (e.g. `README.md`, `src/foo.ts`).',
1304
+ "- Never pass an absolute path or a path that walks outside the workspace (no leading `/`, no `..` that escapes the root). The sandbox will reject it and the call is wasted.",
1305
+ "- For `find`/`grep`/`ls`: omit the `path` argument to search from the workspace root. Pass `path` only when you need to restrict to a subdirectory, and only as a workspace-relative path.",
1306
+ "- For `read`/`edit`/`write`: pass workspace-relative paths only."
1307
+ ].join("\n");
1308
+ var PYTHON_RUNTIME_GUIDELINE = [
1309
+ "## Python runtime",
1310
+ "",
1311
+ "- Python 3 and the Astral `uv` package manager are available on PATH.",
1312
+ "- Run scripts with `python3`.",
1313
+ "- Install/manage packages with `uv pip install <pkg>` (fast; targets the workspace venv at `.boring-agent/venv`). `uv` is the canonical package manager here \u2014 don't assume only `pip`.",
1314
+ "- Create venvs with `uv venv` if needed."
1315
+ ].join("\n");
1316
+ function composeSystemPromptAppend(hostAppend) {
1317
+ return [WORKSPACE_PATHS_GUIDELINE, PYTHON_RUNTIME_GUIDELINE, hostAppend?.trim()].filter(Boolean).join("\n\n");
1318
+ }
1319
+ function withPiHarnessDefaults(pi) {
1320
+ const { noContextFiles = true, noSkills = true, ...rest } = pi ?? {};
1321
+ return { ...rest, noContextFiles, noSkills };
1322
+ }
1323
+ function buildDynamicPromptExtension(source) {
1324
+ return (pi) => {
1325
+ pi.on("before_agent_start", async (event) => {
1326
+ const extra = (await source())?.trim();
1327
+ if (!extra) return;
1328
+ return { systemPrompt: `${event.systemPrompt}
1329
+
1330
+ ${extra}` };
1331
+ });
1332
+ };
1333
+ }
1334
+ function buildToolErrorResultExtension() {
1335
+ return (pi) => {
1336
+ pi.on("tool_result", async (event) => {
1337
+ const marked = unmarkToolResultErrorDetails(event.details);
1338
+ if (!marked.isMarked) return;
1339
+ return {
1340
+ details: marked.details,
1341
+ isError: true
1342
+ };
1343
+ });
1344
+ };
1345
+ }
1346
+ function modelUnavailableError(input) {
1347
+ return Object.assign(new Error("Requested model is not available."), {
1348
+ statusCode: 400,
1349
+ code: ErrorCode.enum.TOOL_INVALID_INPUT,
1350
+ details: { provider: input.model?.provider, model: input.model?.id }
1351
+ });
1352
+ }
1353
+ function meteredSlashCommandPromptError(command) {
1354
+ return Object.assign(new Error("Slash command prompt execution is disabled while metering is configured."), {
1355
+ statusCode: 409,
1356
+ code: ErrorCode.enum.METERING_UNSUPPORTED_COMMAND,
1357
+ details: { command }
1358
+ });
1359
+ }
1360
+ function meteredExtensionCommandContext(ctx, command) {
1361
+ const block = () => {
1362
+ throw meteredSlashCommandPromptError(command);
1363
+ };
1364
+ const guarded = Object.defineProperties({}, Object.getOwnPropertyDescriptors(ctx));
1365
+ guarded.compact = block;
1366
+ guarded.newSession = async () => block();
1367
+ guarded.fork = async () => block();
1368
+ guarded.navigateTree = async () => block();
1369
+ guarded.switchSession = async () => block();
1370
+ return guarded;
1371
+ }
1372
+ function resolveRequestedModel(modelRegistry, input, options = {}) {
1373
+ const requestedId = input.model?.id;
1374
+ if (!input.model || !requestedId) return void 0;
1375
+ const model = modelRegistry.find(input.model.provider, requestedId);
1376
+ const available = modelRegistry.getAvailable();
1377
+ const hasAuth = Boolean(model) && available.some(
1378
+ (m) => m.provider === model.provider && m.id === model.id
1379
+ );
1380
+ if (!model || !hasAuth) {
1381
+ if (options.strict) throw modelUnavailableError(input);
1382
+ return void 0;
1383
+ }
1384
+ return model;
1385
+ }
1386
+ function resolveDefaultModel(modelRegistry) {
1387
+ const configured = readConfiguredDefaultModel();
1388
+ if (configured) {
1389
+ const model = modelRegistry.find(configured.provider, configured.id);
1390
+ if (model) return model;
1391
+ }
1392
+ return void 0;
1393
+ }
1394
+ function sessionCtxForInput(input, ctx) {
1395
+ return normalizeSessionCtx2(input.ctx ?? { workspaceId: ctx.workspaceId }) ?? {};
1396
+ }
1397
+ function sessionCtxFromRunContext(ctx) {
1398
+ return normalizeSessionCtx2({ workspaceId: ctx.workspaceId, userId: ctx.userId }) ?? {};
1399
+ }
1400
+ function normalizeSessionCtx2(ctx) {
1401
+ if (!ctx?.workspaceId && !ctx?.userId) return void 0;
1402
+ return {
1403
+ ...ctx.workspaceId ? { workspaceId: ctx.workspaceId } : {},
1404
+ ...ctx.userId ? { userId: ctx.userId } : {}
1405
+ };
1406
+ }
1407
+ function sessionCacheKey(sessionId, ctx) {
1408
+ return JSON.stringify([sessionId, ctx.workspaceId ?? "", ctx.userId ?? ""]);
1409
+ }
1410
+ function readSettingsFileIfPresent(path) {
1411
+ return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
1412
+ }
1413
+ function mergeInjectedProjectPackages(settingsJson, piPackages) {
1414
+ const settings = settingsJson ? JSON.parse(settingsJson) : {};
1415
+ const configuredPackages = Array.isArray(settings.packages) ? settings.packages : [];
1416
+ return JSON.stringify({
1417
+ ...settings,
1418
+ packages: mergePiPackageSources(configuredPackages, piPackages)
1419
+ });
1420
+ }
1421
+ function createResourceSettingsManager(cwd, agentDir, piPackages) {
1422
+ if (piPackages.length === 0) return SettingsManager2.create(cwd, agentDir);
1423
+ const globalSettingsPath = join2(agentDir, "settings.json");
1424
+ const projectSettingsPath = join2(cwd, ".pi", "settings.json");
1425
+ let globalSettingsOverrideJson;
1426
+ let projectSettingsOverrideJson;
1427
+ const storage = {
1428
+ withLock(scope, fn) {
1429
+ if (scope === "global") {
1430
+ const current2 = globalSettingsOverrideJson ?? readSettingsFileIfPresent(globalSettingsPath);
1431
+ const next2 = fn(current2);
1432
+ if (next2 !== void 0) globalSettingsOverrideJson = next2;
1433
+ return;
1434
+ }
1435
+ const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(projectSettingsPath), piPackages);
1436
+ const next = fn(current);
1437
+ if (next !== void 0) projectSettingsOverrideJson = next;
1438
+ }
1439
+ };
1440
+ return SettingsManager2.fromStorage(storage);
1441
+ }
1442
+ async function applyRequestedSessionOptions(handle, input, options = {}) {
1443
+ const requestedModel = resolveRequestedModel(handle.modelRegistry, input, { strict: options.strictModelResolution });
1444
+ if (requestedModel) {
1445
+ const current = handle.piSession.model;
1446
+ if (!current || current.provider !== requestedModel.provider || current.id !== requestedModel.id) {
1447
+ await handle.piSession.setModel(requestedModel);
1448
+ }
1449
+ }
1450
+ if (input.thinkingLevel) {
1451
+ handle.piSession.setThinkingLevel(input.thinkingLevel);
1452
+ }
1453
+ }
1454
+ var log = createLogger("pi-harness");
1455
+ function deriveSourcePlugin(sourceInfo) {
1456
+ if (!sourceInfo) return void 0;
1457
+ const path = typeof sourceInfo.path === "string" ? sourceInfo.path : "";
1458
+ const source = typeof sourceInfo.source === "string" ? sourceInfo.source : "";
1459
+ const runtimePlugin = path.match(/[/\\]\.pi[/\\]extensions[/\\]([^/\\]+)/);
1460
+ if (runtimePlugin) return runtimePlugin[1];
1461
+ const provisionedSkill = path.match(/[/\\]\.boring-agent[/\\]skills[/\\]([^/\\]+)/);
1462
+ if (provisionedSkill) return provisionedSkill[1];
1463
+ if (source.startsWith("npm:")) return source.slice(4) || void 0;
1464
+ if (source.startsWith("git/")) return source.split("/").filter(Boolean).pop();
1465
+ const fileExtension = path.match(/[/\\]extensions[/\\]([^/\\]+)\.[cm]?[tj]sx?$/);
1466
+ if (fileExtension) return fileExtension[1];
1467
+ return void 0;
1468
+ }
1469
+ function normalizeSlashCommandInfo(command) {
1470
+ const sourcePlugin = deriveSourcePlugin(command.sourceInfo);
1471
+ return {
1472
+ name: command.name,
1473
+ ...command.description ? { description: command.description } : {},
1474
+ source: command.source,
1475
+ ...sourcePlugin ? { sourcePlugin } : {}
1476
+ };
1477
+ }
1478
+ function rememberQueuedFollowUpRunContexts(piSession, state, getRunContext) {
1479
+ const agent = piSession.agent;
1480
+ if (!agent || typeof agent.followUp !== "function") return () => {
1481
+ };
1482
+ const originalFollowUp = agent.followUp;
1483
+ const wrappedFollowUp = function(message) {
1484
+ const ctx = getRunContext();
1485
+ if (ctx && message && typeof message === "object") state.queuedFollowUpContexts.set(message, ctx);
1486
+ return originalFollowUp.call(this, message);
1487
+ };
1488
+ agent.followUp = wrappedFollowUp;
1489
+ return () => {
1490
+ if (agent.followUp === wrappedFollowUp) agent.followUp = originalFollowUp;
1491
+ };
1492
+ }
1493
+ function updateRunContextStateFromPiEvent(state, event, activateRunContext) {
1494
+ if (event.type !== "message_start") return;
1495
+ const message = event.message;
1496
+ if (!message || typeof message !== "object") return;
1497
+ const ctx = state.queuedFollowUpContexts.get(message);
1498
+ if (!ctx) return;
1499
+ state.queuedFollowUpContexts.delete(message);
1500
+ activateRunContext(ctx);
1501
+ }
1502
+ function createPiCodingAgentHarness(opts) {
1503
+ const pi = withPiHarnessDefaults(opts.pi);
1504
+ const sessionStore = new PiSessionStore(opts.runtimeCwd ?? opts.cwd, {
1505
+ sessionNamespace: opts.sessionNamespace,
1506
+ sessionRoot: opts.sessionRoot,
1507
+ sessionDir: opts.sessionDir,
1508
+ storageCwd: opts.cwd
1509
+ });
1510
+ const piSessions = /* @__PURE__ */ new Map();
1511
+ const runContextStorage = new AsyncLocalStorage();
1512
+ const effectiveSkillPaths = [];
1513
+ const effectivePackages = [];
1514
+ const effectiveExtensionPaths = [];
1515
+ const refreshEffectiveResources = () => {
1516
+ const dynamic = pi.getHotReloadableResources?.() ?? {};
1517
+ effectiveSkillPaths.splice(
1518
+ 0,
1519
+ effectiveSkillPaths.length,
1520
+ ...pi.additionalSkillPaths ?? [],
1521
+ ...dynamic.additionalSkillPaths ?? []
1522
+ );
1523
+ effectivePackages.splice(
1524
+ 0,
1525
+ effectivePackages.length,
1526
+ ...mergePiPackageSources(pi.packages ?? [], dynamic.packages ?? [])
1527
+ );
1528
+ effectiveExtensionPaths.splice(
1529
+ 0,
1530
+ effectiveExtensionPaths.length,
1531
+ ...pi.extensionPaths ?? [],
1532
+ ...dynamic.extensionPaths ?? []
1533
+ );
1534
+ };
1535
+ refreshEffectiveResources();
1536
+ const piSessionCreations = /* @__PURE__ */ new Map();
1537
+ async function getOrCreatePiSession(sessionId, input, ctx) {
1538
+ const sessionCtx = sessionCtxForInput(input, ctx);
1539
+ const sessionKey = sessionCacheKey(sessionId, sessionCtx);
1540
+ const existing = piSessions.get(sessionKey);
1541
+ if (existing) {
1542
+ await applyRequestedSessionOptions(existing, input, { strictModelResolution: pi.strictModelResolution });
1543
+ return existing;
1544
+ }
1545
+ const inFlight = piSessionCreations.get(sessionKey);
1546
+ if (inFlight) {
1547
+ const handle = await inFlight;
1548
+ await applyRequestedSessionOptions(handle, input, { strictModelResolution: pi.strictModelResolution });
1549
+ return handle;
1550
+ }
1551
+ const creation = createPiSession(sessionId, sessionCtx, input, ctx);
1552
+ piSessionCreations.set(sessionKey, creation);
1553
+ try {
1554
+ return await creation;
1555
+ } finally {
1556
+ if (piSessionCreations.get(sessionKey) === creation) piSessionCreations.delete(sessionKey);
1557
+ }
1558
+ }
1559
+ async function bindRunContext(ctx, run) {
1560
+ return await runContextStorage.run(ctx, run);
1561
+ }
1562
+ function createRunBoundAdapter(handle, sessionId, ctx) {
1563
+ const adapter = createPiAgentSessionAdapter(handle.piSession, {
1564
+ sessionId,
1565
+ ...handle.piSession.agent && typeof handle.piSession.agent.continue === "function" ? { continueQueuedFollowUp: () => handle.piSession.agent.continue() } : {}
1566
+ });
1567
+ return {
1568
+ ...adapter,
1569
+ prompt: (promptInput) => bindRunContext(ctx, () => adapter.prompt(promptInput)),
1570
+ followUp: (text, options) => bindRunContext(ctx, () => adapter.followUp(text, options)),
1571
+ clearFollowUp: (options) => adapter.clearFollowUp(options),
1572
+ ...adapter.continueQueuedFollowUp ? { continueQueuedFollowUp: () => bindRunContext(ctx, () => adapter.continueQueuedFollowUp()) } : {}
1573
+ };
1574
+ }
1575
+ async function createPiSession(sessionId, sessionCtx, input, ctx) {
1576
+ const authStorage = AuthStorage.create();
1577
+ const modelRegistry = ModelRegistry.create(authStorage);
1578
+ registerConfiguredModelProviders(modelRegistry);
1579
+ const savedPiFile = sessionStore.loadPiSessionFileSync(sessionCtx, sessionId);
1580
+ let sessionManager;
1581
+ let isNewPiSession = false;
1582
+ const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
1583
+ const nativeSessionDir = sessionStore.getSessionDir();
1584
+ if (savedPiFile) {
1585
+ try {
1586
+ sessionManager = SessionManager.open(savedPiFile, void 0, runtimeCwd);
1587
+ } catch {
1588
+ sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
1589
+ isNewPiSession = true;
1590
+ }
1591
+ } else {
1592
+ sessionManager = SessionManager.create(runtimeCwd, nativeSessionDir);
1593
+ isNewPiSession = true;
1594
+ }
1595
+ const resolvedModel = resolveRequestedModel(modelRegistry, input, { strict: pi.strictModelResolution });
1596
+ const model = resolvedModel ?? resolveDefaultModel(modelRegistry);
1597
+ refreshEffectiveResources();
1598
+ const composedSystemPromptAppend = composeSystemPromptAppend(opts.systemPromptAppend);
1599
+ const dynamicPromptExtension = opts.systemPromptDynamic ? buildDynamicPromptExtension(opts.systemPromptDynamic) : void 0;
1600
+ const agentDir = getAgentDir2();
1601
+ const toolErrorResultExtension = buildToolErrorResultExtension();
1602
+ const extensionFactories = [
1603
+ toolErrorResultExtension,
1604
+ ...dynamicPromptExtension ? [dynamicPromptExtension] : [],
1605
+ ...pi.extensionFactories ?? []
1606
+ ];
1607
+ const settingsManager = createResourceSettingsManager(
1608
+ opts.cwd,
1609
+ agentDir,
1610
+ effectivePackages
1611
+ );
1612
+ const resourceLoader = new DefaultResourceLoader({
1613
+ cwd: opts.cwd,
1614
+ agentDir,
1615
+ settingsManager,
1616
+ appendSystemPromptOverride: (base) => [...base, composedSystemPromptAppend],
1617
+ ...effectiveExtensionPaths.length ? { additionalExtensionPaths: effectiveExtensionPaths } : {},
1618
+ ...extensionFactories.length ? { extensionFactories } : {},
1619
+ ...pi.noContextFiles ? { noContextFiles: true } : {},
1620
+ ...pi.noSkills ? { noSkills: true } : {},
1621
+ ...effectiveSkillPaths.length ? { additionalSkillPaths: effectiveSkillPaths } : {},
1622
+ // skillsOverride REPLACES Pi's resolved skill set, which includes
1623
+ // skills contributed by host-declared pi packages (e.g.
1624
+ // @hachej/boring-pi → boring-plugin-authoring). Only trigger it for
1625
+ // the explicit `noSkills` opt-out, where the host wants a clean slate.
1626
+ // Passing additionalSkillPaths is not, by itself, a request to throw
1627
+ // away package skills — those should keep flowing through Pi's loader
1628
+ // and merge with the additional paths.
1629
+ ...pi.noSkills ? {
1630
+ skillsOverride: () => loadSkills({
1631
+ cwd: opts.cwd,
1632
+ agentDir,
1633
+ skillPaths: effectiveSkillPaths,
1634
+ includeDefaults: false
1635
+ })
1636
+ } : {}
1637
+ });
1638
+ await resourceLoader?.reload();
1639
+ const runContextState = { queuedFollowUpContexts: /* @__PURE__ */ new WeakMap() };
1640
+ const { session: piSession } = await createAgentSession({
1641
+ cwd: runtimeCwd,
1642
+ // Suppress Pi's built-in filesystem/shell tools while keeping Boring's
1643
+ // adapted tool catalog active. Do NOT pass an explicit empty tool-name
1644
+ // allowlist: in the current Pi SDK that disables custom tools too.
1645
+ noTools: "builtin",
1646
+ customTools: adaptToolsForPi(opts.tools, input.sessionId, opts.telemetry, () => runContextStorage.getStore()),
1647
+ model,
1648
+ thinkingLevel: input.thinkingLevel ?? "off",
1649
+ sessionManager,
1650
+ authStorage,
1651
+ modelRegistry,
1652
+ ...resourceLoader ? { resourceLoader } : {}
1653
+ });
1654
+ if (isNewPiSession) {
1655
+ const piFile = sessionManager.getSessionFile();
1656
+ if (piFile) {
1657
+ sessionStore.savePiSessionFile(sessionCtx, sessionId, piFile).catch(() => {
1658
+ });
1659
+ }
1660
+ }
1661
+ const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
1662
+ const unsubscribePiRunContextListener = piSession.subscribe((event) => updateRunContextStateFromPiEvent(runContextState, event, (ctx2) => runContextStorage.enterWith(ctx2)));
1663
+ const unsubscribeRunContextListener = () => {
1664
+ unsubscribePiRunContextListener();
1665
+ restoreFollowUpContextWrapper();
1666
+ };
1667
+ const handle = {
1668
+ piSession,
1669
+ modelRegistry,
1670
+ sessionManager,
1671
+ resourceLoader,
1672
+ sessionId,
1673
+ sessionCtx,
1674
+ runContextState,
1675
+ unsubscribeRunContextListener
1676
+ };
1677
+ piSessions.set(sessionCacheKey(sessionId, sessionCtx), handle);
1678
+ return handle;
1679
+ }
1680
+ async function reloadPiSession(sessionId) {
1681
+ const handles = piSessionHandlesFor(sessionId);
1682
+ if (handles.length === 0) return false;
1683
+ refreshEffectiveResources();
1684
+ await Promise.all(handles.map((handle) => handle.piSession.reload()));
1685
+ return true;
1686
+ }
1687
+ function disposePiSession(sessionId, ctx) {
1688
+ if (ctx) {
1689
+ const key = sessionCacheKey(sessionId, ctx);
1690
+ const handle = piSessions.get(key);
1691
+ if (!handle) return;
1692
+ handle.unsubscribeRunContextListener();
1693
+ handle.piSession.dispose();
1694
+ piSessions.delete(key);
1695
+ return;
1696
+ }
1697
+ for (const [key, handle] of piSessions) {
1698
+ if (handle.sessionId !== sessionId) continue;
1699
+ handle.unsubscribeRunContextListener();
1700
+ handle.piSession.dispose();
1701
+ piSessions.delete(key);
1702
+ }
1703
+ }
1704
+ function piSessionHandlesFor(sessionId) {
1705
+ return [...piSessions.values()].filter((handle) => handle.sessionId === sessionId);
1706
+ }
1707
+ async function getOrCreatePiSessionForCommand(sessionId, ctx) {
1708
+ const sessionCtx = sessionCtxFromRunContext(ctx);
1709
+ const existing = piSessions.get(sessionCacheKey(sessionId, sessionCtx));
1710
+ if (existing) return existing;
1711
+ return getOrCreatePiSession(sessionId, { sessionId, content: "", ctx: sessionCtx }, ctx);
1712
+ }
1713
+ const originalDelete = sessionStore.delete.bind(sessionStore);
1714
+ sessionStore.delete = async (ctx, sessionId) => {
1715
+ await originalDelete(ctx, sessionId);
1716
+ disposePiSession(sessionId, ctx);
1717
+ };
1718
+ return {
1719
+ id: "pi-coding-agent",
1720
+ placement: "server",
1721
+ sessions: sessionStore,
1722
+ /**
1723
+ * Pi exposes the resolved system prompt as a getter on AgentSession.
1724
+ * Sessions are created lazily on the first prompt, so callers may see
1725
+ * `undefined` for a session that hasn't been written to yet — that's
1726
+ * the expected pre-first-turn state, not an error.
1727
+ */
1728
+ getSystemPrompt(sessionId) {
1729
+ return piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
1730
+ },
1731
+ hasPiSession(sessionId, ctx) {
1732
+ return ctx ? piSessions.has(sessionCacheKey(sessionId, ctx)) : piSessionHandlesFor(sessionId).length > 0;
1733
+ },
1734
+ /**
1735
+ * Surface Pi's skill/extension load diagnostics for a session so silent
1736
+ * load failures (bad SKILL.md, extension import errors) reach the UI and
1737
+ * the agent. Returns [] when the session has no live pi session yet.
1738
+ * The resourceLoader getters are synchronous.
1739
+ */
1740
+ getResourceDiagnostics(sessionId) {
1741
+ const handle = piSessionHandlesFor(sessionId)[0];
1742
+ if (!handle) return [];
1743
+ const out = [];
1744
+ const seen = /* @__PURE__ */ new Set();
1745
+ const push = (entry) => {
1746
+ const key = `${entry.source}
1747
+ ${entry.message}`;
1748
+ if (seen.has(key)) return;
1749
+ seen.add(key);
1750
+ out.push(entry);
1751
+ };
1752
+ for (const diagnostic of handle.resourceLoader.getSkills().diagnostics) {
1753
+ push({
1754
+ source: "pi-skills",
1755
+ message: diagnostic.path && !diagnostic.message.includes(diagnostic.path) ? `${diagnostic.message} (${diagnostic.path})` : diagnostic.message,
1756
+ ...diagnostic.path ? { path: diagnostic.path } : {}
1757
+ });
1758
+ }
1759
+ for (const error of handle.resourceLoader.getExtensions().errors) {
1760
+ push({
1761
+ source: "pi-extensions",
1762
+ message: error.error.includes(error.path) ? error.error : `${error.error} (${error.path})`,
1763
+ path: error.path
1764
+ });
1765
+ }
1766
+ return out;
1767
+ },
1768
+ reloadSession: reloadPiSession,
1769
+ async getSlashCommands(sessionId, ctx) {
1770
+ const handle = await getOrCreatePiSessionForCommand(sessionId, ctx);
1771
+ return handle.resourceLoader.getExtensions().runtime.getCommands().map(normalizeSlashCommandInfo);
1772
+ },
1773
+ async executeSlashCommand(sessionId, name, args, ctx) {
1774
+ const handle = await getOrCreatePiSessionForCommand(sessionId, ctx);
1775
+ await bindRunContext(ctx, async () => {
1776
+ const command = handle.piSession.extensionRunner.getCommand(name);
1777
+ if (command) {
1778
+ const commandContext = handle.piSession.extensionRunner.createCommandContext();
1779
+ await command.handler(args, ctx.allowPromptDispatch === false ? meteredExtensionCommandContext(commandContext, name) : commandContext);
1780
+ return;
1781
+ }
1782
+ const knownCommand = handle.resourceLoader.getExtensions().runtime.getCommands().some((candidate) => candidate.name === name);
1783
+ if (!knownCommand) throw new Error(`command '${name}' not registered in session '${sessionId}'`);
1784
+ if (ctx.allowPromptDispatch === false) throw meteredSlashCommandPromptError(name);
1785
+ const text = args.trim() ? `/${name} ${args}` : `/${name}`;
1786
+ await handle.piSession.prompt(text);
1787
+ });
1788
+ },
1789
+ async getPiSessionAdapter(input, ctx) {
1790
+ if (!input.sessionId) throw new Error("sessionId is required to create a Pi session adapter");
1791
+ const handle = await getOrCreatePiSession(input.sessionId, input, ctx);
1792
+ return createRunBoundAdapter(handle, input.sessionId, ctx);
1793
+ }
1794
+ };
1795
+ }
1796
+
1797
+ export {
1798
+ getEnv,
1799
+ getEnvSnapshot,
1800
+ setEnvDefault,
1801
+ PiSessionStore,
1802
+ registerConfiguredModelProviders,
1803
+ readConfiguredDefaultModel,
1804
+ PI_PACKAGE_RESOURCE_FILTERS,
1805
+ piPackageSourceKey,
1806
+ compactPiPackages,
1807
+ mergePiPackageSources,
1808
+ withPiHarnessDefaults,
1809
+ createResourceSettingsManager,
1810
+ deriveSourcePlugin,
1811
+ createPiCodingAgentHarness
1812
+ };