@doist/doistbot-cli 1.0.9 → 1.0.11
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.
- package/dist/actions/auth.js +1 -1
- package/dist/actions/review-usage.js +4 -4
- package/dist/actions/review.js +32 -21
- package/dist/config.js +59 -13
- package/dist/index.js +8 -2
- package/dist/terminal.js +5 -0
- package/package.json +4 -4
- package/sandbox/dist/core/datadog-metrics.js +9 -2
- package/sandbox/dist/core/label-parsing.js +1 -22
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +4 -0
- package/sandbox/dist/core/pi.js +290 -67
- package/sandbox/dist/core/repository-map.js +1 -220
- package/sandbox/dist/core/repository-resolver.js +1 -166
- package/sandbox/dist/core/tracing.js +1 -1
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/index.js +1 -0
- package/sandbox/dist/providers/pi-review.js +53 -3
- package/sandbox/dist/tasks/chat/chat.js +95 -45
- package/sandbox/dist/tasks/chat/prompt.js +5 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
- package/sandbox/dist/tasks/issue-triage/context.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +2 -1
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +4 -7
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +65 -41
- package/sandbox/dist/tasks/issue-triage/output.js +1 -1
- package/sandbox/dist/tasks/issue-triage/resolution.js +1 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +2 -12
- package/sandbox/dist/tasks/issue-triage/triage.js +2 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +14 -0
- package/sandbox/dist/tasks/review/engines/shared.js +2 -0
- package/sandbox/dist/tasks/review/review.js +5 -3
- package/sandbox/dist/tasks/review/summary-model.js +4 -3
- package/sandbox/dist/tasks/review/thinking-level.js +2 -0
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -3,28 +3,71 @@ 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
|
+
import { PI_EXECUTION_LIMIT_ENV } from './pi-execution-limits-extension.js';
|
|
7
|
+
export function toPiInvocationTelemetryFields(telemetry) {
|
|
8
|
+
return {
|
|
9
|
+
turn_count: telemetry.turnCount,
|
|
10
|
+
assistant_message_count: telemetry.assistantMessageCount,
|
|
11
|
+
tool_call_count: telemetry.toolCallCount,
|
|
12
|
+
tool_wall_clock_duration_ms: telemetry.toolWallClockDurationMs,
|
|
13
|
+
non_tool_duration_ms: telemetry.nonToolDurationMs,
|
|
14
|
+
...(telemetry.timeToFirstActivityMs == null
|
|
15
|
+
? {}
|
|
16
|
+
: { time_to_first_activity_ms: telemetry.timeToFirstActivityMs }),
|
|
17
|
+
...(telemetry.timeToFirstThinkingMs == null
|
|
18
|
+
? {}
|
|
19
|
+
: { time_to_first_thinking_ms: telemetry.timeToFirstThinkingMs }),
|
|
20
|
+
...(telemetry.timeToFirstResponseMs == null
|
|
21
|
+
? {}
|
|
22
|
+
: { time_to_first_response_ms: telemetry.timeToFirstResponseMs }),
|
|
23
|
+
...(telemetry.timeToFirstToolCallMs == null
|
|
24
|
+
? {}
|
|
25
|
+
: { time_to_first_tool_call_ms: telemetry.timeToFirstToolCallMs }),
|
|
26
|
+
...(telemetry.finalizationAfterLastToolMs == null
|
|
27
|
+
? {}
|
|
28
|
+
: { finalization_after_last_tool_ms: telemetry.finalizationAfterLastToolMs }),
|
|
29
|
+
soft_limit_reached: telemetry.softLimit != null,
|
|
30
|
+
...(telemetry.softLimit
|
|
31
|
+
? {
|
|
32
|
+
soft_limit_trigger: telemetry.softLimit.trigger,
|
|
33
|
+
soft_limit_elapsed_ms: telemetry.softLimit.elapsedMs,
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
hard_limit_reached: telemetry.hardLimit != null,
|
|
37
|
+
...(telemetry.hardLimit
|
|
38
|
+
? {
|
|
39
|
+
hard_limit_trigger: telemetry.hardLimit.trigger,
|
|
40
|
+
hard_limit_elapsed_ms: telemetry.hardLimit.elapsedMs,
|
|
41
|
+
}
|
|
42
|
+
: {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
6
45
|
export const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
|
|
7
46
|
deepseek: 'openrouter',
|
|
8
47
|
gemini: 'google',
|
|
48
|
+
grok: 'openrouter',
|
|
9
49
|
zai: 'openrouter',
|
|
10
50
|
openai: 'openai',
|
|
11
51
|
};
|
|
12
52
|
export const DEFAULT_PI_MODELS = {
|
|
13
53
|
deepseek: 'deepseek-v4-pro',
|
|
14
54
|
gemini: 'gemini-3.1-pro-preview',
|
|
55
|
+
grok: 'grok-4.6',
|
|
15
56
|
zai: 'glm5.2',
|
|
16
57
|
openai: 'gpt-5.6-terra',
|
|
17
58
|
};
|
|
18
59
|
export const DEFAULT_OPENROUTER_MODELS = {
|
|
19
60
|
deepseek: 'deepseek/deepseek-v4-pro-0813',
|
|
20
61
|
gemini: 'google/gemini-3.1-pro-preview',
|
|
21
|
-
|
|
62
|
+
grok: 'x-ai/grok-4.6',
|
|
63
|
+
zai: 'z-ai/glm-5.3-flash',
|
|
22
64
|
};
|
|
23
65
|
// Gemini flash fallback model.
|
|
24
66
|
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.7-flash';
|
|
25
67
|
export const MAX_PI_THINKING_BY_PROVIDER = {
|
|
26
68
|
deepseek: 'high',
|
|
27
69
|
gemini: 'high',
|
|
70
|
+
grok: 'high',
|
|
28
71
|
zai: 'high',
|
|
29
72
|
openai: 'xhigh',
|
|
30
73
|
};
|
|
@@ -41,6 +84,7 @@ const PI_PROGRESS_HEARTBEAT_MS = 30_000;
|
|
|
41
84
|
const OPENROUTER_SESSION_ID_ENV_VAR = 'DOISTBOT_OPENROUTER_SESSION_ID';
|
|
42
85
|
const OPENROUTER_SESSION_ID_MAX_LENGTH = 256;
|
|
43
86
|
const PI_TOOLS_BY_PRESET = {
|
|
87
|
+
none: [],
|
|
44
88
|
'read-only': ['read', 'grep', 'find', 'ls'],
|
|
45
89
|
'workspace-inspection': ['read', 'bash', 'grep', 'find', 'ls'],
|
|
46
90
|
'shell-focused': ['read', 'bash', 'ls'],
|
|
@@ -50,6 +94,7 @@ function resolvePiCliPath() {
|
|
|
50
94
|
return path.join(path.dirname(piPackageEntryPath), 'cli.js');
|
|
51
95
|
}
|
|
52
96
|
const PI_CLI_PATH = resolvePiCliPath();
|
|
97
|
+
const PI_EXECUTION_LIMIT_EXTENSION_PATH = fileURLToPath(new URL('./pi-execution-limits-extension.js', import.meta.url));
|
|
53
98
|
let cachedNodeExecutable;
|
|
54
99
|
let cachedOpenRouterSessionExtensionPath;
|
|
55
100
|
export function resolveNodeExecutable({ execPath = process.execPath, argv0 = process.argv0, canExecute = canExecuteNode, } = {}) {
|
|
@@ -174,6 +219,47 @@ function createOpenRouterSessionExtension({ piProviderId, sessionId, }) {
|
|
|
174
219
|
cleanup() { },
|
|
175
220
|
};
|
|
176
221
|
}
|
|
222
|
+
function createExecutionLimitExtension(limits, startedAtMs) {
|
|
223
|
+
if (!limits) {
|
|
224
|
+
return { args: [], extraEnv: {} };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
args: ['--extension', PI_EXECUTION_LIMIT_EXTENSION_PATH],
|
|
228
|
+
extraEnv: {
|
|
229
|
+
[PI_EXECUTION_LIMIT_ENV.startedAtMs]: String(startedAtMs),
|
|
230
|
+
...(limits.softTimeMs == null
|
|
231
|
+
? {}
|
|
232
|
+
: { [PI_EXECUTION_LIMIT_ENV.softTimeMs]: String(limits.softTimeMs) }),
|
|
233
|
+
...(limits.softToolCalls == null
|
|
234
|
+
? {}
|
|
235
|
+
: { [PI_EXECUTION_LIMIT_ENV.softToolCalls]: String(limits.softToolCalls) }),
|
|
236
|
+
...(limits.hardTimeMs == null
|
|
237
|
+
? {}
|
|
238
|
+
: { [PI_EXECUTION_LIMIT_ENV.hardTimeMs]: String(limits.hardTimeMs) }),
|
|
239
|
+
...(limits.hardToolCalls == null
|
|
240
|
+
? {}
|
|
241
|
+
: { [PI_EXECUTION_LIMIT_ENV.hardToolCalls]: String(limits.hardToolCalls) }),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function normalizePiExecutionLimits(limits) {
|
|
246
|
+
if (!limits)
|
|
247
|
+
return undefined;
|
|
248
|
+
function positiveInteger(value) {
|
|
249
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
250
|
+
}
|
|
251
|
+
const softTimeMs = positiveInteger(limits.softTimeMs);
|
|
252
|
+
const softToolCalls = positiveInteger(limits.softToolCalls);
|
|
253
|
+
const hardTimeMs = positiveInteger(limits.hardTimeMs);
|
|
254
|
+
const hardToolCalls = positiveInteger(limits.hardToolCalls);
|
|
255
|
+
const normalized = {
|
|
256
|
+
...(softTimeMs == null ? {} : { softTimeMs }),
|
|
257
|
+
...(softToolCalls == null ? {} : { softToolCalls }),
|
|
258
|
+
...(hardTimeMs == null ? {} : { hardTimeMs }),
|
|
259
|
+
...(hardToolCalls == null ? {} : { hardToolCalls }),
|
|
260
|
+
};
|
|
261
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
262
|
+
}
|
|
177
263
|
function extractTextSegments(content) {
|
|
178
264
|
if (!Array.isArray(content)) {
|
|
179
265
|
return [];
|
|
@@ -217,9 +303,32 @@ function readStopReason(message) {
|
|
|
217
303
|
return typeof stopReason === 'string' ? stopReason : undefined;
|
|
218
304
|
}
|
|
219
305
|
function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt, lastToolUpdateAt, toolStartedAt, }) {
|
|
220
|
-
if (
|
|
221
|
-
|
|
306
|
+
if (event.type === 'message_update') {
|
|
307
|
+
const assistantMessageEvent = event.assistantMessageEvent;
|
|
308
|
+
if (!assistantMessageEvent || typeof assistantMessageEvent !== 'object') {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
const messageEventType = assistantMessageEvent.type;
|
|
312
|
+
if (messageEventType !== 'thinking_delta' && messageEventType !== 'text_delta') {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
if (onProgress &&
|
|
317
|
+
messageEventType === 'thinking_delta' &&
|
|
318
|
+
now - lastThinkingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
319
|
+
lastThinkingAt.value = now;
|
|
320
|
+
onProgress({ type: 'thinking' });
|
|
321
|
+
}
|
|
322
|
+
if (onProgress &&
|
|
323
|
+
messageEventType === 'text_delta' &&
|
|
324
|
+
now - lastRespondingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
325
|
+
lastRespondingAt.value = now;
|
|
326
|
+
onProgress({ type: 'responding' });
|
|
327
|
+
}
|
|
328
|
+
return { type: messageEventType, observedAt: now };
|
|
222
329
|
}
|
|
330
|
+
if (!onProgress)
|
|
331
|
+
return undefined;
|
|
223
332
|
switch (event.type) {
|
|
224
333
|
case 'agent_start':
|
|
225
334
|
onProgress({ type: 'agent_start' });
|
|
@@ -227,25 +336,6 @@ function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt
|
|
|
227
336
|
case 'agent_end':
|
|
228
337
|
onProgress({ type: 'agent_end' });
|
|
229
338
|
return;
|
|
230
|
-
case 'message_update': {
|
|
231
|
-
const assistantMessageEvent = event.assistantMessageEvent;
|
|
232
|
-
if (!assistantMessageEvent || typeof assistantMessageEvent !== 'object') {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const messageEventType = assistantMessageEvent.type;
|
|
236
|
-
const now = Date.now();
|
|
237
|
-
if (messageEventType === 'thinking_delta' &&
|
|
238
|
-
now - lastThinkingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
239
|
-
lastThinkingAt.value = now;
|
|
240
|
-
onProgress({ type: 'thinking' });
|
|
241
|
-
}
|
|
242
|
-
if (messageEventType === 'text_delta' &&
|
|
243
|
-
now - lastRespondingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
244
|
-
lastRespondingAt.value = now;
|
|
245
|
-
onProgress({ type: 'responding' });
|
|
246
|
-
}
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
339
|
case 'tool_execution_start': {
|
|
250
340
|
const toolName = event.toolName;
|
|
251
341
|
if (typeof toolName === 'string' && toolName.length > 0) {
|
|
@@ -287,8 +377,10 @@ function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt
|
|
|
287
377
|
durationMs: startedAt == null ? undefined : Math.max(0, Date.now() - startedAt),
|
|
288
378
|
});
|
|
289
379
|
}
|
|
380
|
+
return undefined;
|
|
290
381
|
}
|
|
291
382
|
}
|
|
383
|
+
return undefined;
|
|
292
384
|
}
|
|
293
385
|
function toSortedToolUsage(toolUsageByName) {
|
|
294
386
|
return [...toolUsageByName.values()].sort((left, right) => {
|
|
@@ -298,7 +390,8 @@ function toSortedToolUsage(toolUsageByName) {
|
|
|
298
390
|
return left.toolName.localeCompare(right.toolName);
|
|
299
391
|
});
|
|
300
392
|
}
|
|
301
|
-
export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv, capabilityPreset = 'workspace-inspection', thinkingLevel, systemPrompt = DOISTBOT_PI_SYSTEM_PROMPT, noContextFiles = false, openRouterSessionId, onProgress, }) {
|
|
393
|
+
export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv, capabilityPreset = 'workspace-inspection', thinkingLevel, systemPrompt = DOISTBOT_PI_SYSTEM_PROMPT, noContextFiles = false, openRouterSessionId, executionLimits, onProgress, }) {
|
|
394
|
+
const normalizedExecutionLimits = normalizePiExecutionLimits(executionLimits);
|
|
302
395
|
return {
|
|
303
396
|
async invoke({ prompt, modelName, timeoutMs = 5 * 60 * 1000, images, }) {
|
|
304
397
|
const startedAt = Date.now();
|
|
@@ -315,6 +408,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
315
408
|
piProviderId: resolvedPiProviderId,
|
|
316
409
|
sessionId: openRouterSessionId,
|
|
317
410
|
});
|
|
411
|
+
const executionLimitExtension = createExecutionLimitExtension(normalizedExecutionLimits, startedAt);
|
|
318
412
|
const tools = PI_TOOLS_BY_PRESET[capabilityPreset].join(',');
|
|
319
413
|
const args = [
|
|
320
414
|
PI_CLI_PATH,
|
|
@@ -327,11 +421,11 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
327
421
|
...(noContextFiles ? ['--no-context-files'] : []),
|
|
328
422
|
'--model',
|
|
329
423
|
modelSpec,
|
|
330
|
-
'--tools',
|
|
331
|
-
tools,
|
|
424
|
+
...(capabilityPreset === 'none' ? ['--no-tools'] : ['--tools', tools]),
|
|
332
425
|
'--system-prompt',
|
|
333
426
|
systemPrompt,
|
|
334
427
|
...openRouterSessionExtension.args,
|
|
428
|
+
...executionLimitExtension.args,
|
|
335
429
|
...imageArgs,
|
|
336
430
|
];
|
|
337
431
|
const inheritedEnv = {
|
|
@@ -347,6 +441,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
347
441
|
GIT_TERMINAL_PROMPT: '0',
|
|
348
442
|
...extraEnv,
|
|
349
443
|
...openRouterSessionExtension.extraEnv,
|
|
444
|
+
...executionLimitExtension.extraEnv,
|
|
350
445
|
};
|
|
351
446
|
const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
|
|
352
447
|
if (piAuthProviderId) {
|
|
@@ -376,6 +471,100 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
376
471
|
const lastToolUpdateAt = new Map();
|
|
377
472
|
const toolStartedAt = new Map();
|
|
378
473
|
const toolUsageByName = new Map();
|
|
474
|
+
let turnCount = 0;
|
|
475
|
+
let assistantMessageCount = 0;
|
|
476
|
+
let toolCallCount = 0;
|
|
477
|
+
let activeToolCount = 0;
|
|
478
|
+
let toolBusyStartedAt;
|
|
479
|
+
let toolWallClockDurationMs = 0;
|
|
480
|
+
let lastToolEndedAt;
|
|
481
|
+
let firstThinkingAt;
|
|
482
|
+
let firstResponseAt;
|
|
483
|
+
let firstToolCallAt;
|
|
484
|
+
let softLimit;
|
|
485
|
+
let hardLimit;
|
|
486
|
+
function markLimit(level, trigger) {
|
|
487
|
+
if ((level === 'soft' && softLimit) || (level === 'hard' && hardLimit)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const observation = {
|
|
491
|
+
level,
|
|
492
|
+
trigger,
|
|
493
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
494
|
+
toolCallCount,
|
|
495
|
+
};
|
|
496
|
+
if (level === 'soft')
|
|
497
|
+
softLimit = observation;
|
|
498
|
+
else
|
|
499
|
+
hardLimit = observation;
|
|
500
|
+
onProgress?.({ type: 'limit_reached', ...observation });
|
|
501
|
+
}
|
|
502
|
+
function buildTelemetry(durationMs) {
|
|
503
|
+
const now = startedAt + durationMs;
|
|
504
|
+
const activeToolDurationMs = activeToolCount > 0 && toolBusyStartedAt != null
|
|
505
|
+
? Math.max(0, now - toolBusyStartedAt)
|
|
506
|
+
: 0;
|
|
507
|
+
const totalToolWallClockDurationMs = toolWallClockDurationMs + activeToolDurationMs;
|
|
508
|
+
const activityStarts = [
|
|
509
|
+
firstThinkingAt,
|
|
510
|
+
firstResponseAt,
|
|
511
|
+
firstToolCallAt,
|
|
512
|
+
].filter((value) => value != null);
|
|
513
|
+
const firstActivityAt = activityStarts.length > 0 ? Math.min(...activityStarts) : undefined;
|
|
514
|
+
return {
|
|
515
|
+
turnCount,
|
|
516
|
+
assistantMessageCount,
|
|
517
|
+
toolCallCount,
|
|
518
|
+
toolWallClockDurationMs: totalToolWallClockDurationMs,
|
|
519
|
+
nonToolDurationMs: Math.max(0, durationMs - totalToolWallClockDurationMs),
|
|
520
|
+
...(firstActivityAt == null
|
|
521
|
+
? {}
|
|
522
|
+
: { timeToFirstActivityMs: firstActivityAt - startedAt }),
|
|
523
|
+
...(firstThinkingAt == null
|
|
524
|
+
? {}
|
|
525
|
+
: { timeToFirstThinkingMs: firstThinkingAt - startedAt }),
|
|
526
|
+
...(firstResponseAt == null
|
|
527
|
+
? {}
|
|
528
|
+
: { timeToFirstResponseMs: firstResponseAt - startedAt }),
|
|
529
|
+
...(firstToolCallAt == null
|
|
530
|
+
? {}
|
|
531
|
+
: { timeToFirstToolCallMs: firstToolCallAt - startedAt }),
|
|
532
|
+
...(lastToolEndedAt == null
|
|
533
|
+
? {}
|
|
534
|
+
: {
|
|
535
|
+
finalizationAfterLastToolMs: Math.max(0, now - lastToolEndedAt),
|
|
536
|
+
}),
|
|
537
|
+
...(softLimit ? { softLimit } : {}),
|
|
538
|
+
...(hardLimit ? { hardLimit } : {}),
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function resolveWithTelemetry(resolve, result) {
|
|
542
|
+
resolve({
|
|
543
|
+
...result,
|
|
544
|
+
telemetry: buildTelemetry(result.durationMs),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function recordAssistantMessage(message, observedAt) {
|
|
548
|
+
if (!message ||
|
|
549
|
+
typeof message !== 'object' ||
|
|
550
|
+
message.role !== 'assistant') {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
const stopReason = readStopReason(message);
|
|
554
|
+
if (stopReason) {
|
|
555
|
+
lastAssistantStopReason = stopReason;
|
|
556
|
+
}
|
|
557
|
+
const result = extractAssistantMessageResult(message);
|
|
558
|
+
if (result?.text) {
|
|
559
|
+
lastAssistantText = result.text;
|
|
560
|
+
firstResponseAt ??= observedAt;
|
|
561
|
+
}
|
|
562
|
+
else if (result?.error) {
|
|
563
|
+
lastAssistantError = result.error;
|
|
564
|
+
firstResponseAt ??= observedAt;
|
|
565
|
+
}
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
379
568
|
function parseStdout(chunk) {
|
|
380
569
|
stdoutBuffer += chunk;
|
|
381
570
|
const lines = stdoutBuffer.split(/\r?\n/);
|
|
@@ -391,7 +580,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
391
580
|
sessionHeaderSeen = true;
|
|
392
581
|
continue;
|
|
393
582
|
}
|
|
394
|
-
maybeEmitProgress({
|
|
583
|
+
const assistantMessageActivity = maybeEmitProgress({
|
|
395
584
|
event: parsed,
|
|
396
585
|
onProgress,
|
|
397
586
|
lastThinkingAt,
|
|
@@ -399,9 +588,33 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
399
588
|
lastToolUpdateAt,
|
|
400
589
|
toolStartedAt,
|
|
401
590
|
});
|
|
591
|
+
if (parsed.type === 'turn_start') {
|
|
592
|
+
turnCount += 1;
|
|
593
|
+
}
|
|
594
|
+
if (assistantMessageActivity?.type === 'thinking_delta') {
|
|
595
|
+
firstThinkingAt ??= assistantMessageActivity.observedAt;
|
|
596
|
+
}
|
|
597
|
+
else if (assistantMessageActivity?.type === 'text_delta') {
|
|
598
|
+
firstResponseAt ??= assistantMessageActivity.observedAt;
|
|
599
|
+
}
|
|
402
600
|
if (parsed.type === 'tool_execution_start') {
|
|
403
601
|
const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
|
|
404
602
|
if (toolName) {
|
|
603
|
+
const now = Date.now();
|
|
604
|
+
toolCallCount += 1;
|
|
605
|
+
firstToolCallAt ??= now;
|
|
606
|
+
if (activeToolCount === 0) {
|
|
607
|
+
toolBusyStartedAt = now;
|
|
608
|
+
}
|
|
609
|
+
activeToolCount += 1;
|
|
610
|
+
if (normalizedExecutionLimits?.softToolCalls != null &&
|
|
611
|
+
toolCallCount >= normalizedExecutionLimits.softToolCalls) {
|
|
612
|
+
markLimit('soft', 'tool_calls');
|
|
613
|
+
}
|
|
614
|
+
if (normalizedExecutionLimits?.hardToolCalls != null &&
|
|
615
|
+
toolCallCount >= normalizedExecutionLimits.hardToolCalls) {
|
|
616
|
+
markLimit('hard', 'tool_calls');
|
|
617
|
+
}
|
|
405
618
|
const toolCallId = typeof parsed.toolCallId === 'string'
|
|
406
619
|
? parsed.toolCallId
|
|
407
620
|
: toolName;
|
|
@@ -435,7 +648,14 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
435
648
|
const startedAt = toolStartedAt.get(toolCallId);
|
|
436
649
|
toolStartedAt.delete(toolCallId);
|
|
437
650
|
if (startedAt != null) {
|
|
438
|
-
|
|
651
|
+
activeToolCount = Math.max(0, activeToolCount - 1);
|
|
652
|
+
const now = Date.now();
|
|
653
|
+
lastToolEndedAt = now;
|
|
654
|
+
if (activeToolCount === 0 && toolBusyStartedAt != null) {
|
|
655
|
+
toolWallClockDurationMs += Math.max(0, now - toolBusyStartedAt);
|
|
656
|
+
toolBusyStartedAt = undefined;
|
|
657
|
+
}
|
|
658
|
+
current.totalDurationMs += Math.max(0, now - startedAt);
|
|
439
659
|
}
|
|
440
660
|
if (parsed.isError === true) {
|
|
441
661
|
current.errorCount += 1;
|
|
@@ -444,37 +664,23 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
444
664
|
}
|
|
445
665
|
}
|
|
446
666
|
if (parsed.type === 'message_end') {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// "length"/"error"/"aborted" ending must surface
|
|
450
|
-
// even when there's no usable text.
|
|
451
|
-
const stopReason = readStopReason(parsed.message);
|
|
452
|
-
if (stopReason) {
|
|
453
|
-
lastAssistantStopReason = stopReason;
|
|
454
|
-
}
|
|
455
|
-
const result = extractAssistantMessageResult(parsed.message);
|
|
456
|
-
if (result?.text) {
|
|
457
|
-
lastAssistantText = result.text;
|
|
458
|
-
}
|
|
459
|
-
else if (result?.error) {
|
|
460
|
-
lastAssistantError = result.error;
|
|
667
|
+
if (recordAssistantMessage(parsed.message, Date.now())) {
|
|
668
|
+
assistantMessageCount += 1;
|
|
461
669
|
}
|
|
462
670
|
continue;
|
|
463
671
|
}
|
|
464
672
|
if (parsed.type === 'agent_end' && Array.isArray(parsed.messages)) {
|
|
673
|
+
const observedAt = Date.now();
|
|
674
|
+
let finalAssistantMessageCount = 0;
|
|
465
675
|
for (const message of parsed.messages) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
lastAssistantStopReason = stopReason;
|
|
469
|
-
}
|
|
470
|
-
const result = extractAssistantMessageResult(message);
|
|
471
|
-
if (result?.text) {
|
|
472
|
-
lastAssistantText = result.text;
|
|
473
|
-
}
|
|
474
|
-
else if (result?.error) {
|
|
475
|
-
lastAssistantError = result.error;
|
|
676
|
+
if (recordAssistantMessage(message, observedAt)) {
|
|
677
|
+
finalAssistantMessageCount += 1;
|
|
476
678
|
}
|
|
477
679
|
}
|
|
680
|
+
// agent_end repeats messages that were already
|
|
681
|
+
// delivered through message_end. Use the larger
|
|
682
|
+
// count so fallback-only output is captured once.
|
|
683
|
+
assistantMessageCount = Math.max(assistantMessageCount, finalAssistantMessageCount);
|
|
478
684
|
}
|
|
479
685
|
}
|
|
480
686
|
catch {
|
|
@@ -485,10 +691,24 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
485
691
|
}
|
|
486
692
|
}
|
|
487
693
|
return await new Promise((resolve) => {
|
|
694
|
+
const limitTimeoutHandles = [];
|
|
695
|
+
const softTimeMs = normalizedExecutionLimits?.softTimeMs;
|
|
696
|
+
if (softTimeMs != null) {
|
|
697
|
+
limitTimeoutHandles.push(setTimeout(() => markLimit('soft', 'time'), Math.max(0, softTimeMs - (Date.now() - startedAt))));
|
|
698
|
+
}
|
|
699
|
+
const hardTimeMs = normalizedExecutionLimits?.hardTimeMs;
|
|
700
|
+
if (hardTimeMs != null) {
|
|
701
|
+
limitTimeoutHandles.push(setTimeout(() => markLimit('hard', 'time'), Math.max(0, hardTimeMs - (Date.now() - startedAt))));
|
|
702
|
+
}
|
|
488
703
|
const timeoutHandle = setTimeout(() => {
|
|
489
704
|
timedOut = true;
|
|
490
|
-
child.kill('SIGTERM');
|
|
705
|
+
child.kill(normalizedExecutionLimits ? 'SIGKILL' : 'SIGTERM');
|
|
491
706
|
}, timeoutMs);
|
|
707
|
+
function clearTimers() {
|
|
708
|
+
clearTimeout(timeoutHandle);
|
|
709
|
+
for (const handle of limitTimeoutHandles)
|
|
710
|
+
clearTimeout(handle);
|
|
711
|
+
}
|
|
492
712
|
child.stdout.setEncoding('utf8');
|
|
493
713
|
child.stdout.on('data', (chunk) => {
|
|
494
714
|
parseStdout(chunk);
|
|
@@ -498,26 +718,28 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
498
718
|
stderr += chunk;
|
|
499
719
|
});
|
|
500
720
|
child.on('error', (error) => {
|
|
501
|
-
|
|
502
|
-
|
|
721
|
+
clearTimers();
|
|
722
|
+
const durationMs = Date.now() - startedAt;
|
|
723
|
+
resolveWithTelemetry(resolve, {
|
|
503
724
|
ok: false,
|
|
504
725
|
error: error.message,
|
|
505
|
-
durationMs
|
|
726
|
+
durationMs,
|
|
506
727
|
timedOut,
|
|
507
728
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
508
729
|
stopReason: lastAssistantStopReason,
|
|
509
730
|
});
|
|
510
731
|
});
|
|
511
732
|
child.on('close', (code, signal) => {
|
|
512
|
-
|
|
733
|
+
clearTimers();
|
|
734
|
+
const durationMs = Date.now() - startedAt;
|
|
513
735
|
if (stdoutBuffer.trim()) {
|
|
514
736
|
parseStdout('\n');
|
|
515
737
|
}
|
|
516
738
|
if (timedOut) {
|
|
517
|
-
resolve
|
|
739
|
+
resolveWithTelemetry(resolve, {
|
|
518
740
|
ok: false,
|
|
519
741
|
error: `Pi request timed out after ${timeoutMs}ms`,
|
|
520
|
-
durationMs
|
|
742
|
+
durationMs,
|
|
521
743
|
timedOut: true,
|
|
522
744
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
523
745
|
stopReason: lastAssistantStopReason,
|
|
@@ -525,10 +747,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
525
747
|
return;
|
|
526
748
|
}
|
|
527
749
|
if (signal) {
|
|
528
|
-
resolve
|
|
750
|
+
resolveWithTelemetry(resolve, {
|
|
529
751
|
ok: false,
|
|
530
752
|
error: `Pi process terminated with signal ${signal}`,
|
|
531
|
-
durationMs
|
|
753
|
+
durationMs,
|
|
532
754
|
timedOut: false,
|
|
533
755
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
534
756
|
stopReason: lastAssistantStopReason,
|
|
@@ -536,10 +758,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
536
758
|
return;
|
|
537
759
|
}
|
|
538
760
|
if (code !== 0) {
|
|
539
|
-
resolve
|
|
761
|
+
resolveWithTelemetry(resolve, {
|
|
540
762
|
ok: false,
|
|
541
763
|
error: stderr.trim() || `Pi exited with status ${String(code)}`,
|
|
542
|
-
durationMs
|
|
764
|
+
durationMs,
|
|
543
765
|
timedOut: false,
|
|
544
766
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
545
767
|
stopReason: lastAssistantStopReason,
|
|
@@ -548,22 +770,22 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
548
770
|
}
|
|
549
771
|
const output = lastAssistantText.trim();
|
|
550
772
|
if (!output) {
|
|
551
|
-
resolve
|
|
773
|
+
resolveWithTelemetry(resolve, {
|
|
552
774
|
ok: false,
|
|
553
775
|
error: lastAssistantError ||
|
|
554
776
|
stderr.trim() ||
|
|
555
777
|
'Pi session did not produce an assistant response',
|
|
556
|
-
durationMs
|
|
778
|
+
durationMs,
|
|
557
779
|
timedOut: false,
|
|
558
780
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
559
781
|
stopReason: lastAssistantStopReason,
|
|
560
782
|
});
|
|
561
783
|
return;
|
|
562
784
|
}
|
|
563
|
-
resolve
|
|
785
|
+
resolveWithTelemetry(resolve, {
|
|
564
786
|
ok: true,
|
|
565
787
|
output,
|
|
566
|
-
durationMs
|
|
788
|
+
durationMs,
|
|
567
789
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
568
790
|
stopReason: lastAssistantStopReason,
|
|
569
791
|
});
|
|
@@ -577,7 +799,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
577
799
|
},
|
|
578
800
|
};
|
|
579
801
|
}
|
|
580
|
-
export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, cwd, modelName, timeoutMs = 5 * 60 * 1000, extraEnv, images, capabilityPreset, thinkingLevel, systemPrompt, noContextFiles, openRouterSessionId, onProgress, }) {
|
|
802
|
+
export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, cwd, modelName, timeoutMs = 5 * 60 * 1000, extraEnv, images, capabilityPreset, thinkingLevel, systemPrompt, noContextFiles, openRouterSessionId, executionLimits, onProgress, }) {
|
|
581
803
|
return await createPiInvoker({
|
|
582
804
|
provider,
|
|
583
805
|
apiKey,
|
|
@@ -589,6 +811,7 @@ export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, c
|
|
|
589
811
|
systemPrompt,
|
|
590
812
|
noContextFiles,
|
|
591
813
|
openRouterSessionId,
|
|
814
|
+
executionLimits,
|
|
592
815
|
onProgress,
|
|
593
816
|
}).invoke({
|
|
594
817
|
prompt,
|