@todoist/doistbot-cli 0.0.0-stage → 1.0.16
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 +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { endGeneration, startGeneration } from './langfuse-observation.js';
|
|
7
|
+
import { logger } from './logging.js';
|
|
8
|
+
import { PI_EXECUTION_LIMIT_ENV } from './pi-execution-limits-extension.js';
|
|
9
|
+
import { approvedSkillArgs, buildToolEnvironment } from './tool-environment.js';
|
|
10
|
+
import { recordEvent } from './tracing.js';
|
|
11
|
+
export function toPiInvocationTelemetryFields(telemetry) {
|
|
12
|
+
return {
|
|
13
|
+
turn_count: telemetry.turnCount,
|
|
14
|
+
assistant_message_count: telemetry.assistantMessageCount,
|
|
15
|
+
tool_call_count: telemetry.toolCallCount,
|
|
16
|
+
tool_wall_clock_duration_ms: telemetry.toolWallClockDurationMs,
|
|
17
|
+
non_tool_duration_ms: telemetry.nonToolDurationMs,
|
|
18
|
+
...(telemetry.timeToFirstActivityMs == null
|
|
19
|
+
? {}
|
|
20
|
+
: { time_to_first_activity_ms: telemetry.timeToFirstActivityMs }),
|
|
21
|
+
...(telemetry.timeToFirstThinkingMs == null
|
|
22
|
+
? {}
|
|
23
|
+
: { time_to_first_thinking_ms: telemetry.timeToFirstThinkingMs }),
|
|
24
|
+
...(telemetry.timeToFirstResponseMs == null
|
|
25
|
+
? {}
|
|
26
|
+
: { time_to_first_response_ms: telemetry.timeToFirstResponseMs }),
|
|
27
|
+
...(telemetry.timeToFirstToolCallMs == null
|
|
28
|
+
? {}
|
|
29
|
+
: { time_to_first_tool_call_ms: telemetry.timeToFirstToolCallMs }),
|
|
30
|
+
...(telemetry.finalizationAfterLastToolMs == null
|
|
31
|
+
? {}
|
|
32
|
+
: { finalization_after_last_tool_ms: telemetry.finalizationAfterLastToolMs }),
|
|
33
|
+
soft_limit_reached: telemetry.softLimit != null,
|
|
34
|
+
...(telemetry.softLimit
|
|
35
|
+
? {
|
|
36
|
+
soft_limit_trigger: telemetry.softLimit.trigger,
|
|
37
|
+
soft_limit_elapsed_ms: telemetry.softLimit.elapsedMs,
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
40
|
+
hard_limit_reached: telemetry.hardLimit != null,
|
|
41
|
+
...(telemetry.hardLimit
|
|
42
|
+
? {
|
|
43
|
+
hard_limit_trigger: telemetry.hardLimit.trigger,
|
|
44
|
+
hard_limit_elapsed_ms: telemetry.hardLimit.elapsedMs,
|
|
45
|
+
}
|
|
46
|
+
: {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
|
|
50
|
+
deepseek: 'openrouter',
|
|
51
|
+
gemini: 'google',
|
|
52
|
+
grok: 'openrouter',
|
|
53
|
+
zai: 'openrouter',
|
|
54
|
+
openai: 'openai',
|
|
55
|
+
};
|
|
56
|
+
export const DEFAULT_PI_MODELS = {
|
|
57
|
+
deepseek: 'deepseek-v4-pro',
|
|
58
|
+
gemini: 'gemini-3.1-pro-preview',
|
|
59
|
+
grok: 'grok-4.6',
|
|
60
|
+
zai: 'glm5.2',
|
|
61
|
+
openai: 'gpt-6.1-sol',
|
|
62
|
+
};
|
|
63
|
+
export const DEFAULT_OPENROUTER_MODELS = {
|
|
64
|
+
deepseek: 'deepseek/deepseek-v4-pro-0813',
|
|
65
|
+
gemini: 'google/gemini-3.1-pro-preview',
|
|
66
|
+
grok: 'x-ai/grok-4.6',
|
|
67
|
+
zai: 'z-ai/glm-5.3-flash',
|
|
68
|
+
};
|
|
69
|
+
// Gemini flash fallback model.
|
|
70
|
+
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.8-flash';
|
|
71
|
+
export const MAX_PI_THINKING_BY_PROVIDER = {
|
|
72
|
+
deepseek: 'high',
|
|
73
|
+
gemini: 'high',
|
|
74
|
+
grok: 'high',
|
|
75
|
+
zai: 'high',
|
|
76
|
+
openai: 'xhigh',
|
|
77
|
+
};
|
|
78
|
+
const PI_API_KEY_ENV_VAR_BY_PI_PROVIDER = {
|
|
79
|
+
google: 'GEMINI_API_KEY',
|
|
80
|
+
openai: 'OPENAI_API_KEY',
|
|
81
|
+
'openai-codex': 'OPENAI_API_KEY',
|
|
82
|
+
openrouter: 'OPENROUTER_API_KEY',
|
|
83
|
+
};
|
|
84
|
+
const PI_PROVIDER_IDS = new Set(['google', 'openai', 'openai-codex', 'openrouter']);
|
|
85
|
+
export const PI_AUTH_API_KEY_PREFIX = 'pi-auth://';
|
|
86
|
+
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.';
|
|
87
|
+
const PI_PROGRESS_HEARTBEAT_MS = 30_000;
|
|
88
|
+
const OPENROUTER_SESSION_ID_ENV_VAR = 'DOISTBOT_OPENROUTER_SESSION_ID';
|
|
89
|
+
const OPENROUTER_SESSION_ID_MAX_LENGTH = 256;
|
|
90
|
+
const PI_TOOLS_BY_PRESET = {
|
|
91
|
+
none: [],
|
|
92
|
+
'read-only': ['read', 'grep', 'find', 'ls'],
|
|
93
|
+
'workspace-inspection': ['read', 'bash', 'grep', 'find', 'ls'],
|
|
94
|
+
'shell-focused': ['read', 'bash', 'ls'],
|
|
95
|
+
autofix: [
|
|
96
|
+
'read',
|
|
97
|
+
'edit',
|
|
98
|
+
'write',
|
|
99
|
+
'grep',
|
|
100
|
+
'find',
|
|
101
|
+
'ls',
|
|
102
|
+
'verify',
|
|
103
|
+
'query_service',
|
|
104
|
+
'inspect_git',
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
function resolvePiCliPath() {
|
|
108
|
+
const piPackageEntryPath = fileURLToPath(import.meta.resolve('@earendil-works/pi-coding-agent'));
|
|
109
|
+
return path.join(path.dirname(piPackageEntryPath), 'cli.js');
|
|
110
|
+
}
|
|
111
|
+
const PI_CLI_PATH = resolvePiCliPath();
|
|
112
|
+
const PI_EXECUTION_LIMIT_EXTENSION_PATH = fileURLToPath(new URL('./pi-execution-limits-extension.js', import.meta.url));
|
|
113
|
+
const PI_TOOL_OUTPUT_FILTER_EXTENSION_PATH = fileURLToPath(new URL('./pi-tool-output-filter-extension.js', import.meta.url));
|
|
114
|
+
const PI_AUTOFIX_EXTENSION_PATH = fileURLToPath(new URL('./pi-autofix-extension.js', import.meta.url));
|
|
115
|
+
let cachedNodeExecutable;
|
|
116
|
+
let cachedOpenRouterSessionExtensionPath;
|
|
117
|
+
export function resolveNodeExecutable({ execPath = process.execPath, argv0 = process.argv0, canExecute = canExecuteNode, } = {}) {
|
|
118
|
+
const canUseCachedExecutable = execPath === process.execPath && argv0 === process.argv0 && canExecute === canExecuteNode;
|
|
119
|
+
if (canUseCachedExecutable && cachedNodeExecutable) {
|
|
120
|
+
return cachedNodeExecutable;
|
|
121
|
+
}
|
|
122
|
+
for (const candidate of [execPath, argv0, 'node']) {
|
|
123
|
+
if (candidate && canExecute(candidate)) {
|
|
124
|
+
if (canUseCachedExecutable) {
|
|
125
|
+
cachedNodeExecutable = candidate;
|
|
126
|
+
}
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (canUseCachedExecutable) {
|
|
131
|
+
cachedNodeExecutable = 'node';
|
|
132
|
+
}
|
|
133
|
+
return 'node';
|
|
134
|
+
}
|
|
135
|
+
function canExecuteNode(candidate) {
|
|
136
|
+
const result = spawnSync(candidate, ['--version'], { stdio: 'ignore' });
|
|
137
|
+
return result.status === 0;
|
|
138
|
+
}
|
|
139
|
+
function getPiModelSpec({ provider, piProviderId, modelName, thinkingLevel, }) {
|
|
140
|
+
const providerId = piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
|
|
141
|
+
const resolvedModelName = modelName ??
|
|
142
|
+
(providerId === 'openrouter' ? DEFAULT_OPENROUTER_MODELS[provider] : undefined) ??
|
|
143
|
+
DEFAULT_PI_MODELS[provider];
|
|
144
|
+
return `${providerId}/${resolvedModelName}:${thinkingLevel}`;
|
|
145
|
+
}
|
|
146
|
+
export function parsePiAuthProviderId(apiKey) {
|
|
147
|
+
if (!apiKey.startsWith(PI_AUTH_API_KEY_PREFIX)) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const providerId = apiKey.slice(PI_AUTH_API_KEY_PREFIX.length);
|
|
151
|
+
return PI_PROVIDER_IDS.has(providerId)
|
|
152
|
+
? providerId
|
|
153
|
+
: undefined;
|
|
154
|
+
}
|
|
155
|
+
function getImageExtension(mimeType) {
|
|
156
|
+
switch (mimeType) {
|
|
157
|
+
case 'image/png':
|
|
158
|
+
return '.png';
|
|
159
|
+
case 'image/jpeg':
|
|
160
|
+
return '.jpg';
|
|
161
|
+
case 'image/webp':
|
|
162
|
+
return '.webp';
|
|
163
|
+
case 'image/gif':
|
|
164
|
+
return '.gif';
|
|
165
|
+
case 'image/svg+xml':
|
|
166
|
+
return '.svg';
|
|
167
|
+
default:
|
|
168
|
+
return '.img';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function createImageArgs(images) {
|
|
172
|
+
if (!images?.length) {
|
|
173
|
+
return {
|
|
174
|
+
imageArgs: [],
|
|
175
|
+
cleanup() { },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), 'doistbot-pi-images-'));
|
|
179
|
+
const imageArgs = images.map((image, index) => {
|
|
180
|
+
const filePath = path.join(tempDir, `image-${index + 1}${getImageExtension(image.mimeType)}`);
|
|
181
|
+
writeFileSync(filePath, Buffer.from(image.data, 'base64'));
|
|
182
|
+
return `@${filePath}`;
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
imageArgs,
|
|
186
|
+
cleanup() {
|
|
187
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function normalizeOpenRouterSessionId(sessionId) {
|
|
192
|
+
const trimmed = sessionId?.trim();
|
|
193
|
+
if (!trimmed) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return trimmed.slice(0, OPENROUTER_SESSION_ID_MAX_LENGTH);
|
|
197
|
+
}
|
|
198
|
+
function getOpenRouterSessionExtensionPath() {
|
|
199
|
+
if (cachedOpenRouterSessionExtensionPath) {
|
|
200
|
+
return cachedOpenRouterSessionExtensionPath;
|
|
201
|
+
}
|
|
202
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), 'doistbot-pi-openrouter-session-'));
|
|
203
|
+
const extensionPath = path.join(tempDir, 'openrouter-session-id.mjs');
|
|
204
|
+
writeFileSync(extensionPath, `export default function(pi) {
|
|
205
|
+
const sessionId = process.env[${JSON.stringify(OPENROUTER_SESSION_ID_ENV_VAR)}]
|
|
206
|
+
if (!sessionId) return
|
|
207
|
+
|
|
208
|
+
pi.on("before_provider_request", (event) => {
|
|
209
|
+
const payload = event.payload
|
|
210
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined
|
|
211
|
+
|
|
212
|
+
return { ...payload, session_id: sessionId }
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
`);
|
|
216
|
+
process.once('exit', () => {
|
|
217
|
+
rmSync(tempDir, { force: true, recursive: true });
|
|
218
|
+
});
|
|
219
|
+
cachedOpenRouterSessionExtensionPath = extensionPath;
|
|
220
|
+
return extensionPath;
|
|
221
|
+
}
|
|
222
|
+
function createOpenRouterSessionExtension({ piProviderId, sessionId, }) {
|
|
223
|
+
const normalizedSessionId = normalizeOpenRouterSessionId(sessionId);
|
|
224
|
+
if (piProviderId !== 'openrouter' || !normalizedSessionId) {
|
|
225
|
+
return {
|
|
226
|
+
args: [],
|
|
227
|
+
extraEnv: {},
|
|
228
|
+
cleanup() { },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
args: ['--extension', getOpenRouterSessionExtensionPath()],
|
|
233
|
+
extraEnv: {
|
|
234
|
+
[OPENROUTER_SESSION_ID_ENV_VAR]: normalizedSessionId,
|
|
235
|
+
},
|
|
236
|
+
cleanup() { },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function createExecutionLimitExtension(limits, startedAtMs) {
|
|
240
|
+
if (!limits) {
|
|
241
|
+
return { args: [], extraEnv: {} };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
args: ['--extension', PI_EXECUTION_LIMIT_EXTENSION_PATH],
|
|
245
|
+
extraEnv: {
|
|
246
|
+
[PI_EXECUTION_LIMIT_ENV.startedAtMs]: String(startedAtMs),
|
|
247
|
+
...(limits.softTimeMs == null
|
|
248
|
+
? {}
|
|
249
|
+
: { [PI_EXECUTION_LIMIT_ENV.softTimeMs]: String(limits.softTimeMs) }),
|
|
250
|
+
...(limits.softToolCalls == null
|
|
251
|
+
? {}
|
|
252
|
+
: { [PI_EXECUTION_LIMIT_ENV.softToolCalls]: String(limits.softToolCalls) }),
|
|
253
|
+
...(limits.hardTimeMs == null
|
|
254
|
+
? {}
|
|
255
|
+
: { [PI_EXECUTION_LIMIT_ENV.hardTimeMs]: String(limits.hardTimeMs) }),
|
|
256
|
+
...(limits.hardToolCalls == null
|
|
257
|
+
? {}
|
|
258
|
+
: { [PI_EXECUTION_LIMIT_ENV.hardToolCalls]: String(limits.hardToolCalls) }),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function normalizePiExecutionLimits(limits) {
|
|
263
|
+
if (!limits)
|
|
264
|
+
return undefined;
|
|
265
|
+
function positiveInteger(value) {
|
|
266
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
267
|
+
}
|
|
268
|
+
const softTimeMs = positiveInteger(limits.softTimeMs);
|
|
269
|
+
const softToolCalls = positiveInteger(limits.softToolCalls);
|
|
270
|
+
const hardTimeMs = positiveInteger(limits.hardTimeMs);
|
|
271
|
+
const hardToolCalls = positiveInteger(limits.hardToolCalls);
|
|
272
|
+
const normalized = {
|
|
273
|
+
...(softTimeMs == null ? {} : { softTimeMs }),
|
|
274
|
+
...(softToolCalls == null ? {} : { softToolCalls }),
|
|
275
|
+
...(hardTimeMs == null ? {} : { hardTimeMs }),
|
|
276
|
+
...(hardToolCalls == null ? {} : { hardToolCalls }),
|
|
277
|
+
};
|
|
278
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
279
|
+
}
|
|
280
|
+
function extractTextSegments(content) {
|
|
281
|
+
if (!Array.isArray(content)) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
return content
|
|
285
|
+
.flatMap((item) => {
|
|
286
|
+
if (!item || typeof item !== 'object' || !('type' in item) || !('text' in item)) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
const type = item.type;
|
|
290
|
+
const text = item.text;
|
|
291
|
+
return type === 'text' && typeof text === 'string' ? [text.trim()] : [];
|
|
292
|
+
})
|
|
293
|
+
.filter(Boolean);
|
|
294
|
+
}
|
|
295
|
+
function extractAssistantMessageResult(message) {
|
|
296
|
+
if (!message || typeof message !== 'object') {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const role = message.role;
|
|
300
|
+
if (role !== 'assistant') {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const content = message.content;
|
|
304
|
+
const text = extractTextSegments(content).join('\n').trim();
|
|
305
|
+
if (text) {
|
|
306
|
+
return { text };
|
|
307
|
+
}
|
|
308
|
+
const errorMessage = message.errorMessage;
|
|
309
|
+
return typeof errorMessage === 'string' && errorMessage.trim()
|
|
310
|
+
? { error: errorMessage.trim() }
|
|
311
|
+
: null;
|
|
312
|
+
}
|
|
313
|
+
function readStopReason(message) {
|
|
314
|
+
if (!message || typeof message !== 'object') {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
const stopReason = message.stopReason;
|
|
318
|
+
// pi's stopReason follows the PiStopReason contract; cast rather than
|
|
319
|
+
// re-validate against a runtime list that would duplicate the union.
|
|
320
|
+
return typeof stopReason === 'string' ? stopReason : undefined;
|
|
321
|
+
}
|
|
322
|
+
function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt, lastToolUpdateAt, toolStartedAt, }) {
|
|
323
|
+
if (event.type === 'message_update') {
|
|
324
|
+
const assistantMessageEvent = event.assistantMessageEvent;
|
|
325
|
+
if (!assistantMessageEvent || typeof assistantMessageEvent !== 'object') {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
const messageEventType = assistantMessageEvent.type;
|
|
329
|
+
if (messageEventType !== 'thinking_delta' && messageEventType !== 'text_delta') {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
if (onProgress &&
|
|
334
|
+
messageEventType === 'thinking_delta' &&
|
|
335
|
+
now - lastThinkingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
336
|
+
lastThinkingAt.value = now;
|
|
337
|
+
onProgress({ type: 'thinking' });
|
|
338
|
+
}
|
|
339
|
+
if (onProgress &&
|
|
340
|
+
messageEventType === 'text_delta' &&
|
|
341
|
+
now - lastRespondingAt.value >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
342
|
+
lastRespondingAt.value = now;
|
|
343
|
+
onProgress({ type: 'responding' });
|
|
344
|
+
}
|
|
345
|
+
return { type: messageEventType, observedAt: now };
|
|
346
|
+
}
|
|
347
|
+
if (!onProgress)
|
|
348
|
+
return undefined;
|
|
349
|
+
switch (event.type) {
|
|
350
|
+
case 'agent_start':
|
|
351
|
+
onProgress({ type: 'agent_start' });
|
|
352
|
+
return;
|
|
353
|
+
case 'agent_end':
|
|
354
|
+
onProgress({ type: 'agent_end' });
|
|
355
|
+
return;
|
|
356
|
+
case 'tool_execution_start': {
|
|
357
|
+
const toolName = event.toolName;
|
|
358
|
+
if (typeof toolName === 'string' && toolName.length > 0) {
|
|
359
|
+
const toolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : undefined;
|
|
360
|
+
onProgress({ type: 'tool_start', toolName, ...(toolCallId ? { toolCallId } : {}) });
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
case 'tool_execution_update': {
|
|
365
|
+
const toolName = event.toolName;
|
|
366
|
+
if (typeof toolName !== 'string' || toolName.length === 0) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const rawToolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : undefined;
|
|
370
|
+
const toolCallId = rawToolCallId ?? toolName;
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
const lastUpdateAt = lastToolUpdateAt.get(toolCallId) ?? 0;
|
|
373
|
+
if (now - lastUpdateAt >= PI_PROGRESS_HEARTBEAT_MS) {
|
|
374
|
+
lastToolUpdateAt.set(toolCallId, now);
|
|
375
|
+
onProgress({
|
|
376
|
+
type: 'tool_update',
|
|
377
|
+
toolName,
|
|
378
|
+
...(rawToolCallId ? { toolCallId: rawToolCallId } : {}),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
case 'tool_execution_end': {
|
|
384
|
+
const toolName = event.toolName;
|
|
385
|
+
if (typeof toolName === 'string' && toolName.length > 0) {
|
|
386
|
+
const rawToolCallId = typeof event.toolCallId === 'string' ? event.toolCallId : undefined;
|
|
387
|
+
const toolCallId = rawToolCallId ?? toolName;
|
|
388
|
+
const startedAt = toolStartedAt.get(toolCallId);
|
|
389
|
+
onProgress({
|
|
390
|
+
type: 'tool_end',
|
|
391
|
+
toolName,
|
|
392
|
+
...(rawToolCallId ? { toolCallId: rawToolCallId } : {}),
|
|
393
|
+
isError: event.isError === true,
|
|
394
|
+
durationMs: startedAt == null ? undefined : Math.max(0, Date.now() - startedAt),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
function toSortedToolUsage(toolUsageByName) {
|
|
403
|
+
return [...toolUsageByName.values()].sort((left, right) => {
|
|
404
|
+
if (right.totalDurationMs !== left.totalDurationMs) {
|
|
405
|
+
return right.totalDurationMs - left.totalDurationMs;
|
|
406
|
+
}
|
|
407
|
+
return left.toolName.localeCompare(right.toolName);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv, capabilityPreset = 'workspace-inspection', thinkingLevel, systemPrompt = DOISTBOT_PI_SYSTEM_PROMPT, noContextFiles = false, openRouterSessionId, executionLimits, onProgress, }) {
|
|
411
|
+
const normalizedExecutionLimits = normalizePiExecutionLimits(executionLimits);
|
|
412
|
+
return {
|
|
413
|
+
async invoke({ prompt, modelName, timeoutMs = 5 * 60 * 1000, images, }) {
|
|
414
|
+
const startedAt = Date.now();
|
|
415
|
+
const piAuthProviderId = parsePiAuthProviderId(apiKey);
|
|
416
|
+
const resolvedPiProviderId = piAuthProviderId ?? piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
|
|
417
|
+
const modelSpec = getPiModelSpec({
|
|
418
|
+
provider,
|
|
419
|
+
piProviderId: resolvedPiProviderId,
|
|
420
|
+
modelName,
|
|
421
|
+
thinkingLevel: thinkingLevel ?? MAX_PI_THINKING_BY_PROVIDER[provider],
|
|
422
|
+
});
|
|
423
|
+
const { imageArgs, cleanup } = createImageArgs(images);
|
|
424
|
+
const openRouterSessionExtension = createOpenRouterSessionExtension({
|
|
425
|
+
piProviderId: resolvedPiProviderId,
|
|
426
|
+
sessionId: openRouterSessionId,
|
|
427
|
+
});
|
|
428
|
+
const executionLimitExtension = createExecutionLimitExtension(normalizedExecutionLimits, startedAt);
|
|
429
|
+
const tools = PI_TOOLS_BY_PRESET[capabilityPreset].join(',');
|
|
430
|
+
const args = [
|
|
431
|
+
PI_CLI_PATH,
|
|
432
|
+
'--mode',
|
|
433
|
+
'json',
|
|
434
|
+
'--no-session',
|
|
435
|
+
'--no-extensions',
|
|
436
|
+
...(process.env.SANDBOX_PROFILE || capabilityPreset === 'autofix'
|
|
437
|
+
? ['--extension', PI_TOOL_OUTPUT_FILTER_EXTENSION_PATH]
|
|
438
|
+
: []),
|
|
439
|
+
'--no-skills',
|
|
440
|
+
...(capabilityPreset === 'autofix' ? [] : approvedSkillArgs()),
|
|
441
|
+
'--no-prompt-templates',
|
|
442
|
+
...(noContextFiles ? ['--no-context-files'] : []),
|
|
443
|
+
'--model',
|
|
444
|
+
modelSpec,
|
|
445
|
+
...(capabilityPreset === 'none' ? ['--no-tools'] : ['--tools', tools]),
|
|
446
|
+
'--system-prompt',
|
|
447
|
+
systemPrompt,
|
|
448
|
+
...openRouterSessionExtension.args,
|
|
449
|
+
...executionLimitExtension.args,
|
|
450
|
+
...(capabilityPreset === 'autofix'
|
|
451
|
+
? ['--extension', PI_AUTOFIX_EXTENSION_PATH]
|
|
452
|
+
: []),
|
|
453
|
+
...imageArgs,
|
|
454
|
+
];
|
|
455
|
+
const childEnv = buildToolEnvironment({
|
|
456
|
+
...extraEnv,
|
|
457
|
+
...openRouterSessionExtension.extraEnv,
|
|
458
|
+
...executionLimitExtension.extraEnv,
|
|
459
|
+
});
|
|
460
|
+
const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
|
|
461
|
+
if (piAuthProviderId) {
|
|
462
|
+
delete childEnv[apiKeyEnvVar];
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
childEnv[apiKeyEnvVar] = apiKey;
|
|
466
|
+
}
|
|
467
|
+
const generation = startGeneration('pi.invoke', prompt);
|
|
468
|
+
try {
|
|
469
|
+
const child = spawn(resolveNodeExecutable(), args, {
|
|
470
|
+
cwd,
|
|
471
|
+
env: childEnv,
|
|
472
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
473
|
+
});
|
|
474
|
+
child.stdin?.on('error', () => { });
|
|
475
|
+
child.stdin?.end(prompt);
|
|
476
|
+
let timedOut = false;
|
|
477
|
+
let stdoutBuffer = '';
|
|
478
|
+
let stderr = '';
|
|
479
|
+
let lastAssistantText = '';
|
|
480
|
+
let lastAssistantError = '';
|
|
481
|
+
let lastAssistantStopReason;
|
|
482
|
+
let sessionHeaderSeen = false;
|
|
483
|
+
const lastThinkingAt = { value: 0 };
|
|
484
|
+
const lastRespondingAt = { value: 0 };
|
|
485
|
+
const lastToolUpdateAt = new Map();
|
|
486
|
+
const toolStartedAt = new Map();
|
|
487
|
+
const toolUsageByName = new Map();
|
|
488
|
+
let turnCount = 0;
|
|
489
|
+
let assistantMessageCount = 0;
|
|
490
|
+
let toolCallCount = 0;
|
|
491
|
+
let activeToolCount = 0;
|
|
492
|
+
let toolBusyStartedAt;
|
|
493
|
+
let toolWallClockDurationMs = 0;
|
|
494
|
+
let lastToolEndedAt;
|
|
495
|
+
let firstThinkingAt;
|
|
496
|
+
let firstResponseAt;
|
|
497
|
+
let firstToolCallAt;
|
|
498
|
+
let softLimit;
|
|
499
|
+
let hardLimit;
|
|
500
|
+
function markLimit(level, trigger) {
|
|
501
|
+
if ((level === 'soft' && softLimit) || (level === 'hard' && hardLimit)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const observation = {
|
|
505
|
+
level,
|
|
506
|
+
trigger,
|
|
507
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
508
|
+
toolCallCount,
|
|
509
|
+
};
|
|
510
|
+
if (level === 'soft')
|
|
511
|
+
softLimit = observation;
|
|
512
|
+
else
|
|
513
|
+
hardLimit = observation;
|
|
514
|
+
onProgress?.({ type: 'limit_reached', ...observation });
|
|
515
|
+
}
|
|
516
|
+
function buildTelemetry(durationMs) {
|
|
517
|
+
const now = startedAt + durationMs;
|
|
518
|
+
const activeToolDurationMs = activeToolCount > 0 && toolBusyStartedAt != null
|
|
519
|
+
? Math.max(0, now - toolBusyStartedAt)
|
|
520
|
+
: 0;
|
|
521
|
+
const totalToolWallClockDurationMs = toolWallClockDurationMs + activeToolDurationMs;
|
|
522
|
+
const activityStarts = [
|
|
523
|
+
firstThinkingAt,
|
|
524
|
+
firstResponseAt,
|
|
525
|
+
firstToolCallAt,
|
|
526
|
+
].filter((value) => value != null);
|
|
527
|
+
const firstActivityAt = activityStarts.length > 0 ? Math.min(...activityStarts) : undefined;
|
|
528
|
+
return {
|
|
529
|
+
turnCount,
|
|
530
|
+
assistantMessageCount,
|
|
531
|
+
toolCallCount,
|
|
532
|
+
toolWallClockDurationMs: totalToolWallClockDurationMs,
|
|
533
|
+
nonToolDurationMs: Math.max(0, durationMs - totalToolWallClockDurationMs),
|
|
534
|
+
...(firstActivityAt == null
|
|
535
|
+
? {}
|
|
536
|
+
: { timeToFirstActivityMs: firstActivityAt - startedAt }),
|
|
537
|
+
...(firstThinkingAt == null
|
|
538
|
+
? {}
|
|
539
|
+
: { timeToFirstThinkingMs: firstThinkingAt - startedAt }),
|
|
540
|
+
...(firstResponseAt == null
|
|
541
|
+
? {}
|
|
542
|
+
: { timeToFirstResponseMs: firstResponseAt - startedAt }),
|
|
543
|
+
...(firstToolCallAt == null
|
|
544
|
+
? {}
|
|
545
|
+
: { timeToFirstToolCallMs: firstToolCallAt - startedAt }),
|
|
546
|
+
...(lastToolEndedAt == null
|
|
547
|
+
? {}
|
|
548
|
+
: {
|
|
549
|
+
finalizationAfterLastToolMs: Math.max(0, now - lastToolEndedAt),
|
|
550
|
+
}),
|
|
551
|
+
...(softLimit ? { softLimit } : {}),
|
|
552
|
+
...(hardLimit ? { hardLimit } : {}),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function resolveWithTelemetry(resolve, result) {
|
|
556
|
+
resolve({
|
|
557
|
+
...result,
|
|
558
|
+
telemetry: buildTelemetry(result.durationMs),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function recordAssistantMessage(message, observedAt) {
|
|
562
|
+
if (!message ||
|
|
563
|
+
typeof message !== 'object' ||
|
|
564
|
+
message.role !== 'assistant') {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
const stopReason = readStopReason(message);
|
|
568
|
+
if (stopReason) {
|
|
569
|
+
lastAssistantStopReason = stopReason;
|
|
570
|
+
}
|
|
571
|
+
const result = extractAssistantMessageResult(message);
|
|
572
|
+
if (result?.text) {
|
|
573
|
+
lastAssistantText = result.text;
|
|
574
|
+
firstResponseAt ??= observedAt;
|
|
575
|
+
}
|
|
576
|
+
else if (result?.error) {
|
|
577
|
+
lastAssistantError = result.error;
|
|
578
|
+
firstResponseAt ??= observedAt;
|
|
579
|
+
}
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
function parseStdout(chunk) {
|
|
583
|
+
stdoutBuffer += chunk;
|
|
584
|
+
const lines = stdoutBuffer.split(/\r?\n/);
|
|
585
|
+
stdoutBuffer = lines.pop() ?? '';
|
|
586
|
+
for (const line of lines) {
|
|
587
|
+
const trimmed = line.trim();
|
|
588
|
+
if (!trimmed) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
const parsed = JSON.parse(trimmed);
|
|
593
|
+
if (parsed.type === 'doistbot_tool_audit') {
|
|
594
|
+
logger.info('Sandbox tool query', {
|
|
595
|
+
tool: parsed.tool,
|
|
596
|
+
input: parsed.input,
|
|
597
|
+
});
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (parsed.type === 'doistbot_verification_result' ||
|
|
601
|
+
parsed.type === 'doistbot_tool_result') {
|
|
602
|
+
const { type, ...fields } = parsed;
|
|
603
|
+
recordEvent(type === 'doistbot_verification_result'
|
|
604
|
+
? 'autofix.check.result'
|
|
605
|
+
: 'autofix.tool.result', {
|
|
606
|
+
...fields,
|
|
607
|
+
output: typeof fields.output === 'string'
|
|
608
|
+
? fields.output.slice(-4000)
|
|
609
|
+
: '',
|
|
610
|
+
});
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (parsed.type === 'session') {
|
|
614
|
+
sessionHeaderSeen = true;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
const assistantMessageActivity = maybeEmitProgress({
|
|
618
|
+
event: parsed,
|
|
619
|
+
onProgress,
|
|
620
|
+
lastThinkingAt,
|
|
621
|
+
lastRespondingAt,
|
|
622
|
+
lastToolUpdateAt,
|
|
623
|
+
toolStartedAt,
|
|
624
|
+
});
|
|
625
|
+
if (parsed.type === 'turn_start') {
|
|
626
|
+
turnCount += 1;
|
|
627
|
+
}
|
|
628
|
+
if (assistantMessageActivity?.type === 'thinking_delta') {
|
|
629
|
+
firstThinkingAt ??= assistantMessageActivity.observedAt;
|
|
630
|
+
}
|
|
631
|
+
else if (assistantMessageActivity?.type === 'text_delta') {
|
|
632
|
+
firstResponseAt ??= assistantMessageActivity.observedAt;
|
|
633
|
+
}
|
|
634
|
+
if (parsed.type === 'tool_execution_start') {
|
|
635
|
+
const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
|
|
636
|
+
if (toolName) {
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
toolCallCount += 1;
|
|
639
|
+
firstToolCallAt ??= now;
|
|
640
|
+
if (activeToolCount === 0) {
|
|
641
|
+
toolBusyStartedAt = now;
|
|
642
|
+
}
|
|
643
|
+
activeToolCount += 1;
|
|
644
|
+
if (normalizedExecutionLimits?.softToolCalls != null &&
|
|
645
|
+
toolCallCount >= normalizedExecutionLimits.softToolCalls) {
|
|
646
|
+
markLimit('soft', 'tool_calls');
|
|
647
|
+
}
|
|
648
|
+
if (normalizedExecutionLimits?.hardToolCalls != null &&
|
|
649
|
+
toolCallCount >= normalizedExecutionLimits.hardToolCalls) {
|
|
650
|
+
markLimit('hard', 'tool_calls');
|
|
651
|
+
}
|
|
652
|
+
const toolCallId = typeof parsed.toolCallId === 'string'
|
|
653
|
+
? parsed.toolCallId
|
|
654
|
+
: toolName;
|
|
655
|
+
toolStartedAt.set(toolCallId, Date.now());
|
|
656
|
+
const current = toolUsageByName.get(toolName);
|
|
657
|
+
if (current) {
|
|
658
|
+
current.calls += 1;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
toolUsageByName.set(toolName, {
|
|
662
|
+
toolName,
|
|
663
|
+
calls: 1,
|
|
664
|
+
totalDurationMs: 0,
|
|
665
|
+
errorCount: 0,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (parsed.type === 'tool_execution_end') {
|
|
671
|
+
const toolName = typeof parsed.toolName === 'string' ? parsed.toolName : null;
|
|
672
|
+
if (toolName) {
|
|
673
|
+
const current = toolUsageByName.get(toolName) ?? {
|
|
674
|
+
toolName,
|
|
675
|
+
calls: 0,
|
|
676
|
+
totalDurationMs: 0,
|
|
677
|
+
errorCount: 0,
|
|
678
|
+
};
|
|
679
|
+
const toolCallId = typeof parsed.toolCallId === 'string'
|
|
680
|
+
? parsed.toolCallId
|
|
681
|
+
: toolName;
|
|
682
|
+
const startedAt = toolStartedAt.get(toolCallId);
|
|
683
|
+
toolStartedAt.delete(toolCallId);
|
|
684
|
+
if (startedAt != null) {
|
|
685
|
+
activeToolCount = Math.max(0, activeToolCount - 1);
|
|
686
|
+
const now = Date.now();
|
|
687
|
+
lastToolEndedAt = now;
|
|
688
|
+
if (activeToolCount === 0 && toolBusyStartedAt != null) {
|
|
689
|
+
toolWallClockDurationMs += Math.max(0, now - toolBusyStartedAt);
|
|
690
|
+
toolBusyStartedAt = undefined;
|
|
691
|
+
}
|
|
692
|
+
current.totalDurationMs += Math.max(0, now - startedAt);
|
|
693
|
+
}
|
|
694
|
+
if (parsed.isError === true) {
|
|
695
|
+
current.errorCount += 1;
|
|
696
|
+
}
|
|
697
|
+
toolUsageByName.set(toolName, current);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (parsed.type === 'message_end') {
|
|
701
|
+
if (recordAssistantMessage(parsed.message, Date.now())) {
|
|
702
|
+
assistantMessageCount += 1;
|
|
703
|
+
}
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (parsed.type === 'agent_end' && Array.isArray(parsed.messages)) {
|
|
707
|
+
const observedAt = Date.now();
|
|
708
|
+
let finalAssistantMessageCount = 0;
|
|
709
|
+
for (const message of parsed.messages) {
|
|
710
|
+
if (recordAssistantMessage(message, observedAt)) {
|
|
711
|
+
finalAssistantMessageCount += 1;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// agent_end repeats messages that were already
|
|
715
|
+
// delivered through message_end. Use the larger
|
|
716
|
+
// count so fallback-only output is captured once.
|
|
717
|
+
assistantMessageCount = Math.max(assistantMessageCount, finalAssistantMessageCount);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
if (!sessionHeaderSeen && !lastAssistantText) {
|
|
722
|
+
lastAssistantText = trimmed;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const result = await new Promise((resolve) => {
|
|
728
|
+
const limitTimeoutHandles = [];
|
|
729
|
+
const softTimeMs = normalizedExecutionLimits?.softTimeMs;
|
|
730
|
+
if (softTimeMs != null) {
|
|
731
|
+
limitTimeoutHandles.push(setTimeout(() => markLimit('soft', 'time'), Math.max(0, softTimeMs - (Date.now() - startedAt))));
|
|
732
|
+
}
|
|
733
|
+
const hardTimeMs = normalizedExecutionLimits?.hardTimeMs;
|
|
734
|
+
if (hardTimeMs != null) {
|
|
735
|
+
limitTimeoutHandles.push(setTimeout(() => markLimit('hard', 'time'), Math.max(0, hardTimeMs - (Date.now() - startedAt))));
|
|
736
|
+
}
|
|
737
|
+
let forceKillHandle;
|
|
738
|
+
const timeoutHandle = setTimeout(() => {
|
|
739
|
+
timedOut = true;
|
|
740
|
+
child.kill(normalizedExecutionLimits ? 'SIGKILL' : 'SIGTERM');
|
|
741
|
+
if (capabilityPreset === 'autofix')
|
|
742
|
+
forceKillHandle = setTimeout(() => child.kill('SIGKILL'), 2000);
|
|
743
|
+
}, timeoutMs);
|
|
744
|
+
function clearTimers() {
|
|
745
|
+
clearTimeout(timeoutHandle);
|
|
746
|
+
if (forceKillHandle)
|
|
747
|
+
clearTimeout(forceKillHandle);
|
|
748
|
+
for (const handle of limitTimeoutHandles)
|
|
749
|
+
clearTimeout(handle);
|
|
750
|
+
}
|
|
751
|
+
child.stdout.setEncoding('utf8');
|
|
752
|
+
child.stdout.on('data', (chunk) => {
|
|
753
|
+
parseStdout(chunk);
|
|
754
|
+
});
|
|
755
|
+
child.stderr.setEncoding('utf8');
|
|
756
|
+
child.stderr.on('data', (chunk) => {
|
|
757
|
+
stderr += chunk;
|
|
758
|
+
});
|
|
759
|
+
child.on('error', (error) => {
|
|
760
|
+
clearTimers();
|
|
761
|
+
const durationMs = Date.now() - startedAt;
|
|
762
|
+
resolveWithTelemetry(resolve, {
|
|
763
|
+
ok: false,
|
|
764
|
+
error: error.message,
|
|
765
|
+
durationMs,
|
|
766
|
+
timedOut,
|
|
767
|
+
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
768
|
+
stopReason: lastAssistantStopReason,
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
child.on('close', (code, signal) => {
|
|
772
|
+
clearTimers();
|
|
773
|
+
const durationMs = Date.now() - startedAt;
|
|
774
|
+
if (stdoutBuffer.trim()) {
|
|
775
|
+
parseStdout('\n');
|
|
776
|
+
}
|
|
777
|
+
if (timedOut) {
|
|
778
|
+
resolveWithTelemetry(resolve, {
|
|
779
|
+
ok: false,
|
|
780
|
+
error: `Pi request timed out after ${timeoutMs}ms`,
|
|
781
|
+
durationMs,
|
|
782
|
+
timedOut: true,
|
|
783
|
+
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
784
|
+
stopReason: lastAssistantStopReason,
|
|
785
|
+
});
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (signal) {
|
|
789
|
+
resolveWithTelemetry(resolve, {
|
|
790
|
+
ok: false,
|
|
791
|
+
error: `Pi process terminated with signal ${signal}`,
|
|
792
|
+
durationMs,
|
|
793
|
+
timedOut: false,
|
|
794
|
+
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
795
|
+
stopReason: lastAssistantStopReason,
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (code !== 0) {
|
|
800
|
+
resolveWithTelemetry(resolve, {
|
|
801
|
+
ok: false,
|
|
802
|
+
error: stderr.trim() || `Pi exited with status ${String(code)}`,
|
|
803
|
+
durationMs,
|
|
804
|
+
timedOut: false,
|
|
805
|
+
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
806
|
+
stopReason: lastAssistantStopReason,
|
|
807
|
+
});
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const output = lastAssistantText.trim();
|
|
811
|
+
if (!output) {
|
|
812
|
+
resolveWithTelemetry(resolve, {
|
|
813
|
+
ok: false,
|
|
814
|
+
error: lastAssistantError ||
|
|
815
|
+
stderr.trim() ||
|
|
816
|
+
'Pi session did not produce an assistant response',
|
|
817
|
+
durationMs,
|
|
818
|
+
timedOut: false,
|
|
819
|
+
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
820
|
+
stopReason: lastAssistantStopReason,
|
|
821
|
+
});
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
resolveWithTelemetry(resolve, {
|
|
825
|
+
ok: true,
|
|
826
|
+
output,
|
|
827
|
+
durationMs,
|
|
828
|
+
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
829
|
+
stopReason: lastAssistantStopReason,
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
endGeneration(generation, result.ok ? { output: result.output } : { error: result.error });
|
|
834
|
+
return result;
|
|
835
|
+
}
|
|
836
|
+
finally {
|
|
837
|
+
cleanup();
|
|
838
|
+
openRouterSessionExtension.cleanup();
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
export async function invokePiPrompt({ provider, apiKey, piProviderId, prompt, cwd, modelName, timeoutMs = 5 * 60 * 1000, extraEnv, images, capabilityPreset, thinkingLevel, systemPrompt, noContextFiles, openRouterSessionId, executionLimits, onProgress, }) {
|
|
844
|
+
return await createPiInvoker({
|
|
845
|
+
provider,
|
|
846
|
+
apiKey,
|
|
847
|
+
piProviderId,
|
|
848
|
+
cwd,
|
|
849
|
+
extraEnv,
|
|
850
|
+
capabilityPreset,
|
|
851
|
+
thinkingLevel,
|
|
852
|
+
systemPrompt,
|
|
853
|
+
noContextFiles,
|
|
854
|
+
openRouterSessionId,
|
|
855
|
+
executionLimits,
|
|
856
|
+
onProgress,
|
|
857
|
+
}).invoke({
|
|
858
|
+
prompt,
|
|
859
|
+
modelName,
|
|
860
|
+
timeoutMs,
|
|
861
|
+
images,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
export function buildPiImageContent({ dataBase64, contentType, }) {
|
|
865
|
+
return {
|
|
866
|
+
type: 'image',
|
|
867
|
+
data: dataBase64,
|
|
868
|
+
mimeType: contentType,
|
|
869
|
+
};
|
|
870
|
+
}
|