@doist/doistbot-cli 1.0.10 → 1.0.12
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/review-usage.js +4 -4
- package/dist/actions/review.js +31 -21
- package/dist/config.js +61 -16
- package/dist/index.js +8 -2
- package/dist/terminal.js +4 -0
- package/package.json +10 -9
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/dist/core/datadog-metrics.js +7 -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 +286 -67
- package/sandbox/dist/core/repository-map.js +1 -220
- package/sandbox/dist/core/repository-resolver.js +1 -166
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/helpers.js +3 -0
- package/sandbox/dist/providers/pi-review.js +53 -3
- package/sandbox/dist/providers/schemas.js +7 -0
- package/sandbox/dist/providers/types.js +4 -1
- 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 +4 -4
- 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/diff.js +1 -1
- package/sandbox/dist/tasks/review/engines/multi-focus.js +47 -12
- package/sandbox/dist/tasks/review/local-git.js +62 -8
- package/sandbox/dist/tasks/review/prompt.js +2 -0
- package/sandbox/dist/tasks/review/review-summary.js +1 -1
- package/sandbox/dist/tasks/review/summary-model.js +2 -2
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +18 -1
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -3,6 +3,45 @@ 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',
|
|
@@ -21,7 +60,7 @@ export const DEFAULT_OPENROUTER_MODELS = {
|
|
|
21
60
|
deepseek: 'deepseek/deepseek-v4-pro-0813',
|
|
22
61
|
gemini: 'google/gemini-3.1-pro-preview',
|
|
23
62
|
grok: 'x-ai/grok-4.6',
|
|
24
|
-
zai: 'z-ai/glm-5.
|
|
63
|
+
zai: 'z-ai/glm-5.3-flash',
|
|
25
64
|
};
|
|
26
65
|
// Gemini flash fallback model.
|
|
27
66
|
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.7-flash';
|
|
@@ -45,6 +84,7 @@ const PI_PROGRESS_HEARTBEAT_MS = 30_000;
|
|
|
45
84
|
const OPENROUTER_SESSION_ID_ENV_VAR = 'DOISTBOT_OPENROUTER_SESSION_ID';
|
|
46
85
|
const OPENROUTER_SESSION_ID_MAX_LENGTH = 256;
|
|
47
86
|
const PI_TOOLS_BY_PRESET = {
|
|
87
|
+
none: [],
|
|
48
88
|
'read-only': ['read', 'grep', 'find', 'ls'],
|
|
49
89
|
'workspace-inspection': ['read', 'bash', 'grep', 'find', 'ls'],
|
|
50
90
|
'shell-focused': ['read', 'bash', 'ls'],
|
|
@@ -54,6 +94,7 @@ function resolvePiCliPath() {
|
|
|
54
94
|
return path.join(path.dirname(piPackageEntryPath), 'cli.js');
|
|
55
95
|
}
|
|
56
96
|
const PI_CLI_PATH = resolvePiCliPath();
|
|
97
|
+
const PI_EXECUTION_LIMIT_EXTENSION_PATH = fileURLToPath(new URL('./pi-execution-limits-extension.js', import.meta.url));
|
|
57
98
|
let cachedNodeExecutable;
|
|
58
99
|
let cachedOpenRouterSessionExtensionPath;
|
|
59
100
|
export function resolveNodeExecutable({ execPath = process.execPath, argv0 = process.argv0, canExecute = canExecuteNode, } = {}) {
|
|
@@ -178,6 +219,47 @@ function createOpenRouterSessionExtension({ piProviderId, sessionId, }) {
|
|
|
178
219
|
cleanup() { },
|
|
179
220
|
};
|
|
180
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
|
+
}
|
|
181
263
|
function extractTextSegments(content) {
|
|
182
264
|
if (!Array.isArray(content)) {
|
|
183
265
|
return [];
|
|
@@ -221,9 +303,32 @@ function readStopReason(message) {
|
|
|
221
303
|
return typeof stopReason === 'string' ? stopReason : undefined;
|
|
222
304
|
}
|
|
223
305
|
function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt, lastToolUpdateAt, toolStartedAt, }) {
|
|
224
|
-
if (
|
|
225
|
-
|
|
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 };
|
|
226
329
|
}
|
|
330
|
+
if (!onProgress)
|
|
331
|
+
return undefined;
|
|
227
332
|
switch (event.type) {
|
|
228
333
|
case 'agent_start':
|
|
229
334
|
onProgress({ type: 'agent_start' });
|
|
@@ -231,25 +336,6 @@ function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt
|
|
|
231
336
|
case 'agent_end':
|
|
232
337
|
onProgress({ type: 'agent_end' });
|
|
233
338
|
return;
|
|
234
|
-
case 'message_update': {
|
|
235
|
-
const assistantMessageEvent = event.assistantMessageEvent;
|
|
236
|
-
if (!assistantMessageEvent || typeof assistantMessageEvent !== 'object') {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const messageEventType = assistantMessageEvent.type;
|
|
240
|
-
const now = Date.now();
|
|
241
|
-
if (messageEventType === 'thinking_delta' &&
|
|
242
|
-
now - lastThinkingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
243
|
-
lastThinkingAt.value = now;
|
|
244
|
-
onProgress({ type: 'thinking' });
|
|
245
|
-
}
|
|
246
|
-
if (messageEventType === 'text_delta' &&
|
|
247
|
-
now - lastRespondingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
248
|
-
lastRespondingAt.value = now;
|
|
249
|
-
onProgress({ type: 'responding' });
|
|
250
|
-
}
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
339
|
case 'tool_execution_start': {
|
|
254
340
|
const toolName = event.toolName;
|
|
255
341
|
if (typeof toolName === 'string' && toolName.length > 0) {
|
|
@@ -291,8 +377,10 @@ function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt
|
|
|
291
377
|
durationMs: startedAt == null ? undefined : Math.max(0, Date.now() - startedAt),
|
|
292
378
|
});
|
|
293
379
|
}
|
|
380
|
+
return undefined;
|
|
294
381
|
}
|
|
295
382
|
}
|
|
383
|
+
return undefined;
|
|
296
384
|
}
|
|
297
385
|
function toSortedToolUsage(toolUsageByName) {
|
|
298
386
|
return [...toolUsageByName.values()].sort((left, right) => {
|
|
@@ -302,7 +390,8 @@ function toSortedToolUsage(toolUsageByName) {
|
|
|
302
390
|
return left.toolName.localeCompare(right.toolName);
|
|
303
391
|
});
|
|
304
392
|
}
|
|
305
|
-
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);
|
|
306
395
|
return {
|
|
307
396
|
async invoke({ prompt, modelName, timeoutMs = 5 * 60 * 1000, images, }) {
|
|
308
397
|
const startedAt = Date.now();
|
|
@@ -319,6 +408,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
319
408
|
piProviderId: resolvedPiProviderId,
|
|
320
409
|
sessionId: openRouterSessionId,
|
|
321
410
|
});
|
|
411
|
+
const executionLimitExtension = createExecutionLimitExtension(normalizedExecutionLimits, startedAt);
|
|
322
412
|
const tools = PI_TOOLS_BY_PRESET[capabilityPreset].join(',');
|
|
323
413
|
const args = [
|
|
324
414
|
PI_CLI_PATH,
|
|
@@ -331,11 +421,11 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
331
421
|
...(noContextFiles ? ['--no-context-files'] : []),
|
|
332
422
|
'--model',
|
|
333
423
|
modelSpec,
|
|
334
|
-
'--tools',
|
|
335
|
-
tools,
|
|
424
|
+
...(capabilityPreset === 'none' ? ['--no-tools'] : ['--tools', tools]),
|
|
336
425
|
'--system-prompt',
|
|
337
426
|
systemPrompt,
|
|
338
427
|
...openRouterSessionExtension.args,
|
|
428
|
+
...executionLimitExtension.args,
|
|
339
429
|
...imageArgs,
|
|
340
430
|
];
|
|
341
431
|
const inheritedEnv = {
|
|
@@ -351,6 +441,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
351
441
|
GIT_TERMINAL_PROMPT: '0',
|
|
352
442
|
...extraEnv,
|
|
353
443
|
...openRouterSessionExtension.extraEnv,
|
|
444
|
+
...executionLimitExtension.extraEnv,
|
|
354
445
|
};
|
|
355
446
|
const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
|
|
356
447
|
if (piAuthProviderId) {
|
|
@@ -380,6 +471,100 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
380
471
|
const lastToolUpdateAt = new Map();
|
|
381
472
|
const toolStartedAt = new Map();
|
|
382
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
|
+
}
|
|
383
568
|
function parseStdout(chunk) {
|
|
384
569
|
stdoutBuffer += chunk;
|
|
385
570
|
const lines = stdoutBuffer.split(/\r?\n/);
|
|
@@ -395,7 +580,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
395
580
|
sessionHeaderSeen = true;
|
|
396
581
|
continue;
|
|
397
582
|
}
|
|
398
|
-
maybeEmitProgress({
|
|
583
|
+
const assistantMessageActivity = maybeEmitProgress({
|
|
399
584
|
event: parsed,
|
|
400
585
|
onProgress,
|
|
401
586
|
lastThinkingAt,
|
|
@@ -403,9 +588,33 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
403
588
|
lastToolUpdateAt,
|
|
404
589
|
toolStartedAt,
|
|
405
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
|
+
}
|
|
406
600
|
if (parsed.type === 'tool_execution_start') {
|
|
407
601
|
const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
|
|
408
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
|
+
}
|
|
409
618
|
const toolCallId = typeof parsed.toolCallId === 'string'
|
|
410
619
|
? parsed.toolCallId
|
|
411
620
|
: toolName;
|
|
@@ -439,7 +648,14 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
439
648
|
const startedAt = toolStartedAt.get(toolCallId);
|
|
440
649
|
toolStartedAt.delete(toolCallId);
|
|
441
650
|
if (startedAt != null) {
|
|
442
|
-
|
|
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);
|
|
443
659
|
}
|
|
444
660
|
if (parsed.isError === true) {
|
|
445
661
|
current.errorCount += 1;
|
|
@@ -448,37 +664,23 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
448
664
|
}
|
|
449
665
|
}
|
|
450
666
|
if (parsed.type === 'message_end') {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// "length"/"error"/"aborted" ending must surface
|
|
454
|
-
// even when there's no usable text.
|
|
455
|
-
const stopReason = readStopReason(parsed.message);
|
|
456
|
-
if (stopReason) {
|
|
457
|
-
lastAssistantStopReason = stopReason;
|
|
458
|
-
}
|
|
459
|
-
const result = extractAssistantMessageResult(parsed.message);
|
|
460
|
-
if (result?.text) {
|
|
461
|
-
lastAssistantText = result.text;
|
|
462
|
-
}
|
|
463
|
-
else if (result?.error) {
|
|
464
|
-
lastAssistantError = result.error;
|
|
667
|
+
if (recordAssistantMessage(parsed.message, Date.now())) {
|
|
668
|
+
assistantMessageCount += 1;
|
|
465
669
|
}
|
|
466
670
|
continue;
|
|
467
671
|
}
|
|
468
672
|
if (parsed.type === 'agent_end' && Array.isArray(parsed.messages)) {
|
|
673
|
+
const observedAt = Date.now();
|
|
674
|
+
let finalAssistantMessageCount = 0;
|
|
469
675
|
for (const message of parsed.messages) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
lastAssistantStopReason = stopReason;
|
|
473
|
-
}
|
|
474
|
-
const result = extractAssistantMessageResult(message);
|
|
475
|
-
if (result?.text) {
|
|
476
|
-
lastAssistantText = result.text;
|
|
477
|
-
}
|
|
478
|
-
else if (result?.error) {
|
|
479
|
-
lastAssistantError = result.error;
|
|
676
|
+
if (recordAssistantMessage(message, observedAt)) {
|
|
677
|
+
finalAssistantMessageCount += 1;
|
|
480
678
|
}
|
|
481
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);
|
|
482
684
|
}
|
|
483
685
|
}
|
|
484
686
|
catch {
|
|
@@ -489,10 +691,24 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
489
691
|
}
|
|
490
692
|
}
|
|
491
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
|
+
}
|
|
492
703
|
const timeoutHandle = setTimeout(() => {
|
|
493
704
|
timedOut = true;
|
|
494
|
-
child.kill('SIGTERM');
|
|
705
|
+
child.kill(normalizedExecutionLimits ? 'SIGKILL' : 'SIGTERM');
|
|
495
706
|
}, timeoutMs);
|
|
707
|
+
function clearTimers() {
|
|
708
|
+
clearTimeout(timeoutHandle);
|
|
709
|
+
for (const handle of limitTimeoutHandles)
|
|
710
|
+
clearTimeout(handle);
|
|
711
|
+
}
|
|
496
712
|
child.stdout.setEncoding('utf8');
|
|
497
713
|
child.stdout.on('data', (chunk) => {
|
|
498
714
|
parseStdout(chunk);
|
|
@@ -502,26 +718,28 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
502
718
|
stderr += chunk;
|
|
503
719
|
});
|
|
504
720
|
child.on('error', (error) => {
|
|
505
|
-
|
|
506
|
-
|
|
721
|
+
clearTimers();
|
|
722
|
+
const durationMs = Date.now() - startedAt;
|
|
723
|
+
resolveWithTelemetry(resolve, {
|
|
507
724
|
ok: false,
|
|
508
725
|
error: error.message,
|
|
509
|
-
durationMs
|
|
726
|
+
durationMs,
|
|
510
727
|
timedOut,
|
|
511
728
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
512
729
|
stopReason: lastAssistantStopReason,
|
|
513
730
|
});
|
|
514
731
|
});
|
|
515
732
|
child.on('close', (code, signal) => {
|
|
516
|
-
|
|
733
|
+
clearTimers();
|
|
734
|
+
const durationMs = Date.now() - startedAt;
|
|
517
735
|
if (stdoutBuffer.trim()) {
|
|
518
736
|
parseStdout('\n');
|
|
519
737
|
}
|
|
520
738
|
if (timedOut) {
|
|
521
|
-
resolve
|
|
739
|
+
resolveWithTelemetry(resolve, {
|
|
522
740
|
ok: false,
|
|
523
741
|
error: `Pi request timed out after ${timeoutMs}ms`,
|
|
524
|
-
durationMs
|
|
742
|
+
durationMs,
|
|
525
743
|
timedOut: true,
|
|
526
744
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
527
745
|
stopReason: lastAssistantStopReason,
|
|
@@ -529,10 +747,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
529
747
|
return;
|
|
530
748
|
}
|
|
531
749
|
if (signal) {
|
|
532
|
-
resolve
|
|
750
|
+
resolveWithTelemetry(resolve, {
|
|
533
751
|
ok: false,
|
|
534
752
|
error: `Pi process terminated with signal ${signal}`,
|
|
535
|
-
durationMs
|
|
753
|
+
durationMs,
|
|
536
754
|
timedOut: false,
|
|
537
755
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
538
756
|
stopReason: lastAssistantStopReason,
|
|
@@ -540,10 +758,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
540
758
|
return;
|
|
541
759
|
}
|
|
542
760
|
if (code !== 0) {
|
|
543
|
-
resolve
|
|
761
|
+
resolveWithTelemetry(resolve, {
|
|
544
762
|
ok: false,
|
|
545
763
|
error: stderr.trim() || `Pi exited with status ${String(code)}`,
|
|
546
|
-
durationMs
|
|
764
|
+
durationMs,
|
|
547
765
|
timedOut: false,
|
|
548
766
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
549
767
|
stopReason: lastAssistantStopReason,
|
|
@@ -552,22 +770,22 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
552
770
|
}
|
|
553
771
|
const output = lastAssistantText.trim();
|
|
554
772
|
if (!output) {
|
|
555
|
-
resolve
|
|
773
|
+
resolveWithTelemetry(resolve, {
|
|
556
774
|
ok: false,
|
|
557
775
|
error: lastAssistantError ||
|
|
558
776
|
stderr.trim() ||
|
|
559
777
|
'Pi session did not produce an assistant response',
|
|
560
|
-
durationMs
|
|
778
|
+
durationMs,
|
|
561
779
|
timedOut: false,
|
|
562
780
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
563
781
|
stopReason: lastAssistantStopReason,
|
|
564
782
|
});
|
|
565
783
|
return;
|
|
566
784
|
}
|
|
567
|
-
resolve
|
|
785
|
+
resolveWithTelemetry(resolve, {
|
|
568
786
|
ok: true,
|
|
569
787
|
output,
|
|
570
|
-
durationMs
|
|
788
|
+
durationMs,
|
|
571
789
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
572
790
|
stopReason: lastAssistantStopReason,
|
|
573
791
|
});
|
|
@@ -581,7 +799,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
581
799
|
},
|
|
582
800
|
};
|
|
583
801
|
}
|
|
584
|
-
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, }) {
|
|
585
803
|
return await createPiInvoker({
|
|
586
804
|
provider,
|
|
587
805
|
apiKey,
|
|
@@ -593,6 +811,7 @@ export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, c
|
|
|
593
811
|
systemPrompt,
|
|
594
812
|
noContextFiles,
|
|
595
813
|
openRouterSessionId,
|
|
814
|
+
executionLimits,
|
|
596
815
|
onProgress,
|
|
597
816
|
}).invoke({
|
|
598
817
|
prompt,
|