@doist/doistbot-cli 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/actions/auth.js +78 -24
  2. package/dist/actions/doctor.js +14 -7
  3. package/dist/actions/review.js +67 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +77 -5
  6. package/dist/env.js +1 -0
  7. package/dist/runtime.js +1 -1
  8. package/dist/terminal.js +2 -0
  9. package/package.json +4 -4
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
  12. package/sandbox/dist/core/check-run.js +16 -0
  13. package/sandbox/dist/core/datadog-metrics.js +6 -52
  14. package/sandbox/dist/core/logger-span-processor.js +78 -0
  15. package/sandbox/dist/core/pi.js +285 -174
  16. package/sandbox/dist/core/prompt-template.js +45 -3
  17. package/sandbox/dist/core/repository-map.js +24 -0
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +80 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/credentials.js +12 -2
  22. package/sandbox/dist/providers/gemini.js +5 -1
  23. package/sandbox/dist/providers/helpers.js +63 -17
  24. package/sandbox/dist/providers/index.js +4 -0
  25. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  26. package/sandbox/dist/providers/pi-review.js +19 -2
  27. package/sandbox/dist/tasks/chat/chat.js +173 -102
  28. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  29. package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
  30. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  31. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  32. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  33. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  34. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
  35. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  36. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  37. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  38. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  39. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  40. package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
  41. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  42. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  43. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  44. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  45. package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
  46. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  47. package/sandbox/dist/tasks/review/check-run.js +2 -1
  48. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  49. package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
  50. package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
  51. package/sandbox/dist/tasks/review/engines/shared.js +43 -6
  52. package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
  53. package/sandbox/dist/tasks/review/local.js +7 -4
  54. package/sandbox/dist/tasks/review/review-summary.js +48 -7
  55. package/sandbox/dist/tasks/review/review.js +267 -186
  56. package/sandbox/dist/tasks/review/runner.js +33 -7
  57. package/sandbox/dist/tasks/review/summary-model.js +189 -20
  58. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  59. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  60. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
  61. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
@@ -3,41 +3,55 @@ import { mkdtempSync, rmSync, writeFileSync } from 'fs';
3
3
  import { tmpdir } from 'os';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
7
- gemini: 'openrouter',
6
+ export const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
7
+ deepseek: 'openrouter',
8
+ gemini: 'google',
9
+ zai: 'openrouter',
8
10
  openai: 'openai',
9
11
  };
10
12
  export const DEFAULT_PI_MODELS = {
13
+ deepseek: 'deepseek-v4-pro',
11
14
  gemini: 'gemini-3.1-pro-preview',
15
+ zai: 'glm5.2',
12
16
  openai: 'gpt-5.4',
13
17
  };
14
18
  export const DEFAULT_OPENROUTER_MODELS = {
19
+ deepseek: 'deepseek/deepseek-v4-pro',
15
20
  gemini: 'google/gemini-3.1-pro-preview',
21
+ zai: 'z-ai/glm-5.2',
16
22
  };
23
+ // Gemini flash fallback model.
24
+ export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.5-flash';
17
25
  export const MAX_PI_THINKING_BY_PROVIDER = {
26
+ deepseek: 'high',
18
27
  gemini: 'high',
28
+ zai: 'high',
19
29
  openai: 'xhigh',
20
30
  };
21
31
  const PI_API_KEY_ENV_VAR_BY_PI_PROVIDER = {
32
+ google: 'GEMINI_API_KEY',
22
33
  openai: 'OPENAI_API_KEY',
23
34
  'openai-codex': 'OPENAI_API_KEY',
24
35
  openrouter: 'OPENROUTER_API_KEY',
25
36
  };
26
- const PI_PROVIDER_IDS = new Set(['openai', 'openai-codex', 'openrouter']);
37
+ const PI_PROVIDER_IDS = new Set(['google', 'openai', 'openai-codex', 'openrouter']);
27
38
  export const PI_AUTH_API_KEY_PREFIX = 'pi-auth://';
28
39
  const DOISTBOT_PI_SYSTEM_PROMPT = 'You are running inside doistbot in a non-interactive environment. Inspect the repository, configuration, documentation, and relevant history when they can improve accuracy. Use available tools proactively to gather evidence, avoid guessing when the workspace can answer the question, and follow the task prompt exactly.';
29
40
  const PI_PROGRESS_HEARTBEAT_MS = 30_000;
41
+ const OPENROUTER_SESSION_ID_ENV_VAR = 'DOISTBOT_OPENROUTER_SESSION_ID';
42
+ const OPENROUTER_SESSION_ID_MAX_LENGTH = 256;
30
43
  const PI_TOOLS_BY_PRESET = {
31
44
  'read-only': ['read', 'grep', 'find', 'ls'],
32
45
  'workspace-inspection': ['read', 'bash', 'grep', 'find', 'ls'],
33
46
  'shell-focused': ['read', 'bash', 'ls'],
34
47
  };
35
48
  function resolvePiCliPath() {
36
- const piPackageEntryPath = fileURLToPath(import.meta.resolve('@mariozechner/pi-coding-agent'));
49
+ const piPackageEntryPath = fileURLToPath(import.meta.resolve('@earendil-works/pi-coding-agent'));
37
50
  return path.join(path.dirname(piPackageEntryPath), 'cli.js');
38
51
  }
39
52
  const PI_CLI_PATH = resolvePiCliPath();
40
53
  let cachedNodeExecutable;
54
+ let cachedOpenRouterSessionExtensionPath;
41
55
  export function resolveNodeExecutable({ execPath = process.execPath, argv0 = process.argv0, canExecute = canExecuteNode, } = {}) {
42
56
  const canUseCachedExecutable = execPath === process.execPath && argv0 === process.argv0 && canExecute === canExecuteNode;
43
57
  if (canUseCachedExecutable && cachedNodeExecutable) {
@@ -67,7 +81,7 @@ function getPiModelSpec({ provider, piProviderId, modelName, thinkingLevel, }) {
67
81
  DEFAULT_PI_MODELS[provider];
68
82
  return `${providerId}/${resolvedModelName}:${thinkingLevel}`;
69
83
  }
70
- function parsePiAuthProviderId(apiKey) {
84
+ export function parsePiAuthProviderId(apiKey) {
71
85
  if (!apiKey.startsWith(PI_AUTH_API_KEY_PREFIX)) {
72
86
  return undefined;
73
87
  }
@@ -112,6 +126,54 @@ function createImageArgs(images) {
112
126
  },
113
127
  };
114
128
  }
129
+ function normalizeOpenRouterSessionId(sessionId) {
130
+ const trimmed = sessionId?.trim();
131
+ if (!trimmed) {
132
+ return undefined;
133
+ }
134
+ return trimmed.slice(0, OPENROUTER_SESSION_ID_MAX_LENGTH);
135
+ }
136
+ function getOpenRouterSessionExtensionPath() {
137
+ if (cachedOpenRouterSessionExtensionPath) {
138
+ return cachedOpenRouterSessionExtensionPath;
139
+ }
140
+ const tempDir = mkdtempSync(path.join(tmpdir(), 'doistbot-pi-openrouter-session-'));
141
+ const extensionPath = path.join(tempDir, 'openrouter-session-id.mjs');
142
+ writeFileSync(extensionPath, `export default function(pi) {
143
+ const sessionId = process.env[${JSON.stringify(OPENROUTER_SESSION_ID_ENV_VAR)}]
144
+ if (!sessionId) return
145
+
146
+ pi.on("before_provider_request", (event) => {
147
+ const payload = event.payload
148
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined
149
+
150
+ return { ...payload, session_id: sessionId }
151
+ })
152
+ }
153
+ `);
154
+ process.once('exit', () => {
155
+ rmSync(tempDir, { force: true, recursive: true });
156
+ });
157
+ cachedOpenRouterSessionExtensionPath = extensionPath;
158
+ return extensionPath;
159
+ }
160
+ function createOpenRouterSessionExtension({ piProviderId, sessionId, }) {
161
+ const normalizedSessionId = normalizeOpenRouterSessionId(sessionId);
162
+ if (piProviderId !== 'openrouter' || !normalizedSessionId) {
163
+ return {
164
+ args: [],
165
+ extraEnv: {},
166
+ cleanup() { },
167
+ };
168
+ }
169
+ return {
170
+ args: ['--extension', getOpenRouterSessionExtensionPath()],
171
+ extraEnv: {
172
+ [OPENROUTER_SESSION_ID_ENV_VAR]: normalizedSessionId,
173
+ },
174
+ cleanup() { },
175
+ };
176
+ }
115
177
  function extractTextSegments(content) {
116
178
  if (!Array.isArray(content)) {
117
179
  return [];
@@ -145,6 +207,15 @@ function extractAssistantMessageResult(message) {
145
207
  ? { error: errorMessage.trim() }
146
208
  : null;
147
209
  }
210
+ function readStopReason(message) {
211
+ if (!message || typeof message !== 'object') {
212
+ return undefined;
213
+ }
214
+ const stopReason = message.stopReason;
215
+ // pi's stopReason follows the PiStopReason contract; cast rather than
216
+ // re-validate against a runtime list that would duplicate the union.
217
+ return typeof stopReason === 'string' ? stopReason : undefined;
218
+ }
148
219
  function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt, lastToolUpdateAt, toolStartedAt, }) {
149
220
  if (!onProgress) {
150
221
  return;
@@ -178,7 +249,8 @@ function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt
178
249
  case 'tool_execution_start': {
179
250
  const toolName = event.toolName;
180
251
  if (typeof toolName === 'string' && toolName.length > 0) {
181
- onProgress({ type: 'tool_start', toolName });
252
+ const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : undefined;
253
+ onProgress({ type: 'tool_start', toolName, ...(toolCallId ? { toolCallId } : {}) });
182
254
  }
183
255
  return;
184
256
  }
@@ -187,23 +259,30 @@ function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt
187
259
  if (typeof toolName !== 'string' || toolName.length === 0) {
188
260
  return;
189
261
  }
190
- const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : toolName;
262
+ const rawToolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : undefined;
263
+ const toolCallId = rawToolCallId ?? toolName;
191
264
  const now = Date.now();
192
265
  const lastUpdateAt = lastToolUpdateAt.get(toolCallId) ?? 0;
193
266
  if (now - lastUpdateAt >= PI_PROGRESS_HEARTBEAT_MS) {
194
267
  lastToolUpdateAt.set(toolCallId, now);
195
- onProgress({ type: 'tool_update', toolName });
268
+ onProgress({
269
+ type: 'tool_update',
270
+ toolName,
271
+ ...(rawToolCallId ? { toolCallId: rawToolCallId } : {}),
272
+ });
196
273
  }
197
274
  return;
198
275
  }
199
276
  case 'tool_execution_end': {
200
277
  const toolName = event.toolName;
201
278
  if (typeof toolName === 'string' && toolName.length > 0) {
202
- const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : toolName;
279
+ const rawToolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : undefined;
280
+ const toolCallId = rawToolCallId ?? toolName;
203
281
  const startedAt = toolStartedAt.get(toolCallId);
204
282
  onProgress({
205
283
  type: 'tool_end',
206
284
  toolName,
285
+ ...(rawToolCallId ? { toolCallId: rawToolCallId } : {}),
207
286
  isError: event.isError === true,
208
287
  durationMs: startedAt == null ? undefined : Math.max(0, Date.now() - startedAt),
209
288
  });
@@ -219,7 +298,7 @@ function toSortedToolUsage(toolUsageByName) {
219
298
  return left.toolName.localeCompare(right.toolName);
220
299
  });
221
300
  }
222
- export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv, capabilityPreset = 'workspace-inspection', thinkingLevel, systemPrompt = DOISTBOT_PI_SYSTEM_PROMPT, onProgress, }) {
301
+ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv, capabilityPreset = 'workspace-inspection', thinkingLevel, systemPrompt = DOISTBOT_PI_SYSTEM_PROMPT, noContextFiles = false, openRouterSessionId, onProgress, }) {
223
302
  return {
224
303
  async invoke({ prompt, modelName, timeoutMs = 5 * 60 * 1000, images, }) {
225
304
  const startedAt = Date.now();
@@ -232,6 +311,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
232
311
  thinkingLevel: thinkingLevel ?? MAX_PI_THINKING_BY_PROVIDER[provider],
233
312
  });
234
313
  const { imageArgs, cleanup } = createImageArgs(images);
314
+ const openRouterSessionExtension = createOpenRouterSessionExtension({
315
+ piProviderId: resolvedPiProviderId,
316
+ sessionId: openRouterSessionId,
317
+ });
235
318
  const tools = PI_TOOLS_BY_PRESET[capabilityPreset].join(',');
236
319
  const args = [
237
320
  PI_CLI_PATH,
@@ -241,12 +324,14 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
241
324
  '--no-extensions',
242
325
  '--no-skills',
243
326
  '--no-prompt-templates',
327
+ ...(noContextFiles ? ['--no-context-files'] : []),
244
328
  '--model',
245
329
  modelSpec,
246
330
  '--tools',
247
331
  tools,
248
332
  '--system-prompt',
249
333
  systemPrompt,
334
+ ...openRouterSessionExtension.args,
250
335
  ...imageArgs,
251
336
  ];
252
337
  const childEnv = {
@@ -255,6 +340,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
255
340
  PATH: process.env.PATH,
256
341
  GIT_TERMINAL_PROMPT: '0',
257
342
  ...extraEnv,
343
+ ...openRouterSessionExtension.extraEnv,
258
344
  };
259
345
  const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
260
346
  if (piAuthProviderId) {
@@ -264,205 +350,228 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
264
350
  childEnv[apiKeyEnvVar] = apiKey;
265
351
  }
266
352
  delete childEnv.GITHUB_TOKEN;
267
- const child = spawn(resolveNodeExecutable(), args, {
268
- cwd,
269
- env: childEnv,
270
- stdio: ['pipe', 'pipe', 'pipe'],
271
- });
272
- child.stdin?.on('error', () => { });
273
- child.stdin?.end(prompt);
274
- let timedOut = false;
275
- let stdoutBuffer = '';
276
- let stderr = '';
277
- let lastAssistantText = '';
278
- let lastAssistantError = '';
279
- let sessionHeaderSeen = false;
280
- const lastThinkingAt = { value: 0 };
281
- const lastRespondingAt = { value: 0 };
282
- const lastToolUpdateAt = new Map();
283
- const toolStartedAt = new Map();
284
- const toolUsageByName = new Map();
285
- function parseStdout(chunk) {
286
- stdoutBuffer += chunk;
287
- const lines = stdoutBuffer.split(/\r?\n/);
288
- stdoutBuffer = lines.pop() ?? '';
289
- for (const line of lines) {
290
- const trimmed = line.trim();
291
- if (!trimmed) {
292
- continue;
293
- }
294
- try {
295
- const parsed = JSON.parse(trimmed);
296
- if (parsed.type === 'session') {
297
- sessionHeaderSeen = true;
353
+ try {
354
+ const child = spawn(resolveNodeExecutable(), args, {
355
+ cwd,
356
+ env: childEnv,
357
+ stdio: ['pipe', 'pipe', 'pipe'],
358
+ });
359
+ child.stdin?.on('error', () => { });
360
+ child.stdin?.end(prompt);
361
+ let timedOut = false;
362
+ let stdoutBuffer = '';
363
+ let stderr = '';
364
+ let lastAssistantText = '';
365
+ let lastAssistantError = '';
366
+ let lastAssistantStopReason;
367
+ let sessionHeaderSeen = false;
368
+ const lastThinkingAt = { value: 0 };
369
+ const lastRespondingAt = { value: 0 };
370
+ const lastToolUpdateAt = new Map();
371
+ const toolStartedAt = new Map();
372
+ const toolUsageByName = new Map();
373
+ function parseStdout(chunk) {
374
+ stdoutBuffer += chunk;
375
+ const lines = stdoutBuffer.split(/\r?\n/);
376
+ stdoutBuffer = lines.pop() ?? '';
377
+ for (const line of lines) {
378
+ const trimmed = line.trim();
379
+ if (!trimmed) {
298
380
  continue;
299
381
  }
300
- maybeEmitProgress({
301
- event: parsed,
302
- onProgress,
303
- lastThinkingAt,
304
- lastRespondingAt,
305
- lastToolUpdateAt,
306
- toolStartedAt,
307
- });
308
- if (parsed.type === 'tool_execution_start') {
309
- const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
310
- if (toolName) {
311
- const toolCallId = typeof parsed.toolCallId === 'string'
312
- ? parsed.toolCallId
313
- : toolName;
314
- toolStartedAt.set(toolCallId, Date.now());
315
- const current = toolUsageByName.get(toolName);
316
- if (current) {
317
- current.calls += 1;
382
+ try {
383
+ const parsed = JSON.parse(trimmed);
384
+ if (parsed.type === 'session') {
385
+ sessionHeaderSeen = true;
386
+ continue;
387
+ }
388
+ maybeEmitProgress({
389
+ event: parsed,
390
+ onProgress,
391
+ lastThinkingAt,
392
+ lastRespondingAt,
393
+ lastToolUpdateAt,
394
+ toolStartedAt,
395
+ });
396
+ if (parsed.type === 'tool_execution_start') {
397
+ const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
398
+ if (toolName) {
399
+ const toolCallId = typeof parsed.toolCallId === 'string'
400
+ ? parsed.toolCallId
401
+ : toolName;
402
+ toolStartedAt.set(toolCallId, Date.now());
403
+ const current = toolUsageByName.get(toolName);
404
+ if (current) {
405
+ current.calls += 1;
406
+ }
407
+ else {
408
+ toolUsageByName.set(toolName, {
409
+ toolName,
410
+ calls: 1,
411
+ totalDurationMs: 0,
412
+ errorCount: 0,
413
+ });
414
+ }
318
415
  }
319
- else {
320
- toolUsageByName.set(toolName, {
416
+ }
417
+ if (parsed.type === 'tool_execution_end') {
418
+ const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
419
+ if (toolName) {
420
+ const current = toolUsageByName.get(toolName) ?? {
321
421
  toolName,
322
- calls: 1,
422
+ calls: 0,
323
423
  totalDurationMs: 0,
324
424
  errorCount: 0,
325
- });
425
+ };
426
+ const toolCallId = typeof parsed.toolCallId === 'string'
427
+ ? parsed.toolCallId
428
+ : toolName;
429
+ const startedAt = toolStartedAt.get(toolCallId);
430
+ toolStartedAt.delete(toolCallId);
431
+ if (startedAt != null) {
432
+ current.totalDurationMs += Math.max(0, Date.now() - startedAt);
433
+ }
434
+ if (parsed.isError === true) {
435
+ current.errorCount += 1;
436
+ }
437
+ toolUsageByName.set(toolName, current);
326
438
  }
327
439
  }
328
- }
329
- if (parsed.type === 'tool_execution_end') {
330
- const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
331
- if (toolName) {
332
- const current = toolUsageByName.get(toolName) ?? {
333
- toolName,
334
- calls: 0,
335
- totalDurationMs: 0,
336
- errorCount: 0,
337
- };
338
- const toolCallId = typeof parsed.toolCallId === 'string'
339
- ? parsed.toolCallId
340
- : toolName;
341
- const startedAt = toolStartedAt.get(toolCallId);
342
- toolStartedAt.delete(toolCallId);
343
- if (startedAt != null) {
344
- current.totalDurationMs += Math.max(0, Date.now() - startedAt);
345
- }
346
- if (parsed.isError === true) {
347
- current.errorCount += 1;
440
+ if (parsed.type === 'message_end') {
441
+ // Read the stop reason regardless of whether the
442
+ // message carried text or an error — a terminal
443
+ // "length"/"error"/"aborted" ending must surface
444
+ // even when there's no usable text.
445
+ const stopReason = readStopReason(parsed.message);
446
+ if (stopReason) {
447
+ lastAssistantStopReason = stopReason;
348
448
  }
349
- toolUsageByName.set(toolName, current);
350
- }
351
- }
352
- if (parsed.type === 'message_end') {
353
- const result = extractAssistantMessageResult(parsed.message);
354
- if (result?.text) {
355
- lastAssistantText = result.text;
356
- }
357
- else if (result?.error) {
358
- lastAssistantError = result.error;
359
- }
360
- continue;
361
- }
362
- if (parsed.type === 'agent_end' && Array.isArray(parsed.messages)) {
363
- for (const message of parsed.messages) {
364
- const result = extractAssistantMessageResult(message);
449
+ const result = extractAssistantMessageResult(parsed.message);
365
450
  if (result?.text) {
366
451
  lastAssistantText = result.text;
367
452
  }
368
453
  else if (result?.error) {
369
454
  lastAssistantError = result.error;
370
455
  }
456
+ continue;
457
+ }
458
+ if (parsed.type === 'agent_end' && Array.isArray(parsed.messages)) {
459
+ for (const message of parsed.messages) {
460
+ const stopReason = readStopReason(message);
461
+ if (stopReason) {
462
+ lastAssistantStopReason = stopReason;
463
+ }
464
+ const result = extractAssistantMessageResult(message);
465
+ if (result?.text) {
466
+ lastAssistantText = result.text;
467
+ }
468
+ else if (result?.error) {
469
+ lastAssistantError = result.error;
470
+ }
471
+ }
371
472
  }
372
473
  }
373
- }
374
- catch {
375
- if (!sessionHeaderSeen && !lastAssistantText) {
376
- lastAssistantText = trimmed;
474
+ catch {
475
+ if (!sessionHeaderSeen && !lastAssistantText) {
476
+ lastAssistantText = trimmed;
477
+ }
377
478
  }
378
479
  }
379
480
  }
380
- }
381
- const result = await new Promise((resolve) => {
382
- const timeoutHandle = setTimeout(() => {
383
- timedOut = true;
384
- child.kill('SIGTERM');
385
- }, timeoutMs);
386
- child.stdout.setEncoding('utf8');
387
- child.stdout.on('data', (chunk) => {
388
- parseStdout(chunk);
389
- });
390
- child.stderr.setEncoding('utf8');
391
- child.stderr.on('data', (chunk) => {
392
- stderr += chunk;
393
- });
394
- child.on('error', (error) => {
395
- clearTimeout(timeoutHandle);
396
- resolve({
397
- ok: false,
398
- error: error.message,
399
- durationMs: Date.now() - startedAt,
400
- timedOut,
401
- toolUsage: toSortedToolUsage(toolUsageByName),
481
+ return await new Promise((resolve) => {
482
+ const timeoutHandle = setTimeout(() => {
483
+ timedOut = true;
484
+ child.kill('SIGTERM');
485
+ }, timeoutMs);
486
+ child.stdout.setEncoding('utf8');
487
+ child.stdout.on('data', (chunk) => {
488
+ parseStdout(chunk);
402
489
  });
403
- });
404
- child.on('close', (code, signal) => {
405
- clearTimeout(timeoutHandle);
406
- if (stdoutBuffer.trim()) {
407
- parseStdout('\n');
408
- }
409
- if (timedOut) {
410
- resolve({
411
- ok: false,
412
- error: `Pi request timed out after ${timeoutMs}ms`,
413
- durationMs: Date.now() - startedAt,
414
- timedOut: true,
415
- toolUsage: toSortedToolUsage(toolUsageByName),
416
- });
417
- return;
418
- }
419
- if (signal) {
420
- resolve({
421
- ok: false,
422
- error: `Pi process terminated with signal ${signal}`,
423
- durationMs: Date.now() - startedAt,
424
- timedOut: false,
425
- toolUsage: toSortedToolUsage(toolUsageByName),
426
- });
427
- return;
428
- }
429
- if (code !== 0) {
490
+ child.stderr.setEncoding('utf8');
491
+ child.stderr.on('data', (chunk) => {
492
+ stderr += chunk;
493
+ });
494
+ child.on('error', (error) => {
495
+ clearTimeout(timeoutHandle);
430
496
  resolve({
431
497
  ok: false,
432
- error: stderr.trim() || `Pi exited with status ${String(code)}`,
498
+ error: error.message,
433
499
  durationMs: Date.now() - startedAt,
434
- timedOut: false,
500
+ timedOut,
435
501
  toolUsage: toSortedToolUsage(toolUsageByName),
502
+ stopReason: lastAssistantStopReason,
436
503
  });
437
- return;
438
- }
439
- const output = lastAssistantText.trim();
440
- if (!output) {
504
+ });
505
+ child.on('close', (code, signal) => {
506
+ clearTimeout(timeoutHandle);
507
+ if (stdoutBuffer.trim()) {
508
+ parseStdout('\n');
509
+ }
510
+ if (timedOut) {
511
+ resolve({
512
+ ok: false,
513
+ error: `Pi request timed out after ${timeoutMs}ms`,
514
+ durationMs: Date.now() - startedAt,
515
+ timedOut: true,
516
+ toolUsage: toSortedToolUsage(toolUsageByName),
517
+ stopReason: lastAssistantStopReason,
518
+ });
519
+ return;
520
+ }
521
+ if (signal) {
522
+ resolve({
523
+ ok: false,
524
+ error: `Pi process terminated with signal ${signal}`,
525
+ durationMs: Date.now() - startedAt,
526
+ timedOut: false,
527
+ toolUsage: toSortedToolUsage(toolUsageByName),
528
+ stopReason: lastAssistantStopReason,
529
+ });
530
+ return;
531
+ }
532
+ if (code !== 0) {
533
+ resolve({
534
+ ok: false,
535
+ error: stderr.trim() || `Pi exited with status ${String(code)}`,
536
+ durationMs: Date.now() - startedAt,
537
+ timedOut: false,
538
+ toolUsage: toSortedToolUsage(toolUsageByName),
539
+ stopReason: lastAssistantStopReason,
540
+ });
541
+ return;
542
+ }
543
+ const output = lastAssistantText.trim();
544
+ if (!output) {
545
+ resolve({
546
+ ok: false,
547
+ error: lastAssistantError ||
548
+ stderr.trim() ||
549
+ 'Pi session did not produce an assistant response',
550
+ durationMs: Date.now() - startedAt,
551
+ timedOut: false,
552
+ toolUsage: toSortedToolUsage(toolUsageByName),
553
+ stopReason: lastAssistantStopReason,
554
+ });
555
+ return;
556
+ }
441
557
  resolve({
442
- ok: false,
443
- error: lastAssistantError ||
444
- stderr.trim() ||
445
- 'Pi session did not produce an assistant response',
558
+ ok: true,
559
+ output,
446
560
  durationMs: Date.now() - startedAt,
447
- timedOut: false,
448
561
  toolUsage: toSortedToolUsage(toolUsageByName),
562
+ stopReason: lastAssistantStopReason,
449
563
  });
450
- return;
451
- }
452
- resolve({
453
- ok: true,
454
- output,
455
- durationMs: Date.now() - startedAt,
456
- toolUsage: toSortedToolUsage(toolUsageByName),
457
564
  });
458
565
  });
459
- });
460
- cleanup();
461
- return result;
566
+ }
567
+ finally {
568
+ cleanup();
569
+ openRouterSessionExtension.cleanup();
570
+ }
462
571
  },
463
572
  };
464
573
  }
465
- export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, cwd, modelName, timeoutMs = 5 * 60 * 1000, extraEnv, images, capabilityPreset, thinkingLevel, systemPrompt, onProgress, }) {
574
+ export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, cwd, modelName, timeoutMs = 5 * 60 * 1000, extraEnv, images, capabilityPreset, thinkingLevel, systemPrompt, noContextFiles, openRouterSessionId, onProgress, }) {
466
575
  return await createPiInvoker({
467
576
  provider,
468
577
  apiKey,
@@ -472,6 +581,8 @@ export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, c
472
581
  capabilityPreset,
473
582
  thinkingLevel,
474
583
  systemPrompt,
584
+ noContextFiles,
585
+ openRouterSessionId,
475
586
  onProgress,
476
587
  }).invoke({
477
588
  prompt,