@jx-grxf/patchpilot 0.4.0 → 1.2.0
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/.env.example +17 -1
- package/README.md +113 -23
- package/SECURITY.md +7 -1
- package/dist/cli.js +103 -14
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +47 -1
- package/dist/core/agent.js +667 -76
- package/dist/core/agent.js.map +1 -1
- package/dist/core/cleanup.d.ts +3 -0
- package/dist/core/cleanup.js +29 -0
- package/dist/core/cleanup.js.map +1 -0
- package/dist/core/clipboard.d.ts +14 -0
- package/dist/core/clipboard.js +134 -0
- package/dist/core/clipboard.js.map +1 -0
- package/dist/core/codex.d.ts +8 -0
- package/dist/core/codex.js +28 -2
- package/dist/core/codex.js.map +1 -1
- package/dist/core/compaction.d.ts +23 -0
- package/dist/core/compaction.js +145 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/contextFormat.d.ts +21 -0
- package/dist/core/contextFormat.js +87 -0
- package/dist/core/contextFormat.js.map +1 -0
- package/dist/core/contextItem.d.ts +41 -0
- package/dist/core/contextItem.js +93 -0
- package/dist/core/contextItem.js.map +1 -0
- package/dist/core/contextStore.d.ts +48 -0
- package/dist/core/contextStore.js +306 -0
- package/dist/core/contextStore.js.map +1 -0
- package/dist/core/doctor.d.ts +4 -1
- package/dist/core/doctor.js +122 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/gemini.js +10 -4
- package/dist/core/gemini.js.map +1 -1
- package/dist/core/geminiWrapper.d.ts +92 -0
- package/dist/core/geminiWrapper.js +1258 -0
- package/dist/core/geminiWrapper.js.map +1 -0
- package/dist/core/http.js +70 -6
- package/dist/core/http.js.map +1 -1
- package/dist/core/json.d.ts +1 -1
- package/dist/core/json.js +81 -19
- package/dist/core/json.js.map +1 -1
- package/dist/core/memory.d.ts +16 -0
- package/dist/core/memory.js +108 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/modelClient.js +7 -0
- package/dist/core/modelClient.js.map +1 -1
- package/dist/core/nvidia.d.ts +1 -1
- package/dist/core/nvidia.js +13 -4
- package/dist/core/nvidia.js.map +1 -1
- package/dist/core/ollama.js +13 -3
- package/dist/core/ollama.js.map +1 -1
- package/dist/core/openrouter.js +15 -6
- package/dist/core/openrouter.js.map +1 -1
- package/dist/core/projectInit.d.ts +6 -0
- package/dist/core/projectInit.js +44 -0
- package/dist/core/projectInit.js.map +1 -0
- package/dist/core/reasoning.js +6 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.d.ts +1 -0
- package/dist/core/session.js +55 -3
- package/dist/core/session.js.map +1 -1
- package/dist/core/tokenAccounting.d.ts +4 -0
- package/dist/core/tokenAccounting.js +75 -13
- package/dist/core/tokenAccounting.js.map +1 -1
- package/dist/core/types.d.ts +65 -5
- package/dist/core/types.js +30 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/updateCheck.d.ts +19 -0
- package/dist/core/updateCheck.js +103 -0
- package/dist/core/updateCheck.js.map +1 -0
- package/dist/core/workspace.d.ts +37 -0
- package/dist/core/workspace.js +1535 -84
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1841 -140
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +141 -9
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/components/ApprovalPanel.js +16 -1
- package/dist/tui/components/ApprovalPanel.js.map +1 -1
- package/dist/tui/components/CommandSuggestions.js +33 -5
- package/dist/tui/components/CommandSuggestions.js.map +1 -1
- package/dist/tui/components/Composer.d.ts +3 -0
- package/dist/tui/components/Composer.js +57 -5
- package/dist/tui/components/Composer.js.map +1 -1
- package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
- package/dist/tui/components/ExperimentalPanel.js +38 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -0
- package/dist/tui/components/Header.js +3 -3
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +25 -1
- package/dist/tui/components/OnboardingPanel.js +87 -25
- package/dist/tui/components/OnboardingPanel.js.map +1 -1
- package/dist/tui/components/Sidebar.js +17 -13
- package/dist/tui/components/Sidebar.js.map +1 -1
- package/dist/tui/components/StartupBanner.d.ts +4 -0
- package/dist/tui/components/StartupBanner.js +9 -0
- package/dist/tui/components/StartupBanner.js.map +1 -0
- package/dist/tui/components/Transcript.d.ts +7 -0
- package/dist/tui/components/Transcript.js +87 -17
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/contextCommands.d.ts +8 -0
- package/dist/tui/contextCommands.js +205 -0
- package/dist/tui/contextCommands.js.map +1 -0
- package/dist/tui/experimental/AnimatedText.d.ts +38 -0
- package/dist/tui/experimental/AnimatedText.js +55 -0
- package/dist/tui/experimental/AnimatedText.js.map +1 -0
- package/dist/tui/experimental/Banner.d.ts +10 -0
- package/dist/tui/experimental/Banner.js +33 -0
- package/dist/tui/experimental/Banner.js.map +1 -0
- package/dist/tui/experimental/CommandPalette.d.ts +11 -0
- package/dist/tui/experimental/CommandPalette.js +25 -0
- package/dist/tui/experimental/CommandPalette.js.map +1 -0
- package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
- package/dist/tui/experimental/ExperimentalShell.js +366 -0
- package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
- package/dist/tui/experimental/ThemePicker.d.ts +13 -0
- package/dist/tui/experimental/ThemePicker.js +12 -0
- package/dist/tui/experimental/ThemePicker.js.map +1 -0
- package/dist/tui/experimental/attachments.d.ts +35 -0
- package/dist/tui/experimental/attachments.js +244 -0
- package/dist/tui/experimental/attachments.js.map +1 -0
- package/dist/tui/experimental/composer.d.ts +24 -0
- package/dist/tui/experimental/composer.js +84 -0
- package/dist/tui/experimental/composer.js.map +1 -0
- package/dist/tui/experimental/geminiPricing.d.ts +16 -0
- package/dist/tui/experimental/geminiPricing.js +39 -0
- package/dist/tui/experimental/geminiPricing.js.map +1 -0
- package/dist/tui/experimental/layout.d.ts +46 -0
- package/dist/tui/experimental/layout.js +112 -0
- package/dist/tui/experimental/layout.js.map +1 -0
- package/dist/tui/experimental/theme.d.ts +35 -0
- package/dist/tui/experimental/theme.js +86 -0
- package/dist/tui/experimental/theme.js.map +1 -0
- package/dist/tui/experimental/transcriptRows.d.ts +20 -0
- package/dist/tui/experimental/transcriptRows.js +169 -0
- package/dist/tui/experimental/transcriptRows.js.map +1 -0
- package/dist/tui/experimental/ultraModes.d.ts +46 -0
- package/dist/tui/experimental/ultraModes.js +95 -0
- package/dist/tui/experimental/ultraModes.js.map +1 -0
- package/dist/tui/experimental/ultramaxx.d.ts +19 -0
- package/dist/tui/experimental/ultramaxx.js +43 -0
- package/dist/tui/experimental/ultramaxx.js.map +1 -0
- package/dist/tui/format.d.ts +4 -2
- package/dist/tui/format.js +21 -7
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/hosts.js +7 -1
- package/dist/tui/hosts.js.map +1 -1
- package/dist/tui/layout.d.ts +26 -0
- package/dist/tui/layout.js +66 -0
- package/dist/tui/layout.js.map +1 -0
- package/dist/tui/modelSelection.d.ts +1 -1
- package/dist/tui/modelSelection.js +8 -6
- package/dist/tui/modelSelection.js.map +1 -1
- package/dist/tui/modes.d.ts +8 -1
- package/dist/tui/modes.js +20 -2
- package/dist/tui/modes.js.map +1 -1
- package/dist/tui/onboardingPreferences.d.ts +37 -0
- package/dist/tui/onboardingPreferences.js +118 -0
- package/dist/tui/onboardingPreferences.js.map +1 -0
- package/dist/tui/runStatus.d.ts +50 -0
- package/dist/tui/runStatus.js +164 -0
- package/dist/tui/runStatus.js.map +1 -0
- package/dist/tui/types.d.ts +8 -0
- package/dist/tui/types.js.map +1 -1
- package/docs/architecture.md +115 -0
- package/docs/gemini-wrapper.md +110 -0
- package/docs/product-context.md +43 -0
- package/docs/releases/v0.1.1-beta.md +18 -0
- package/docs/releases/v0.2.1.md +1 -1
- package/docs/releases/v0.3.1-beta.md +4 -0
- package/docs/releases/v0.4.0.md +1 -1
- package/docs/releases/v1.0.0.md +28 -0
- package/docs/releases/v1.0.1.md +25 -0
- package/docs/releases/v1.1.0.md +30 -0
- package/docs/releases/v1.2.0.md +28 -0
- package/docs/showcase/patchpilot-banner.png +0 -0
- package/docs/showcase/patchpilot-logo.png +0 -0
- package/package.json +8 -3
package/dist/core/agent.js
CHANGED
|
@@ -4,6 +4,8 @@ import { platform, release, type } from "node:os";
|
|
|
4
4
|
import { createModelClient } from "./modelClient.js";
|
|
5
5
|
import { resolveProviderReasoning } from "./reasoning.js";
|
|
6
6
|
import { formatSubagentContext, runSubagentAdvisors } from "./subagents.js";
|
|
7
|
+
import { MAX_TOOL_CALLS_PER_RESPONSE } from "./types.js";
|
|
8
|
+
import { estimateTokens } from "./tokenAccounting.js";
|
|
7
9
|
import { getToolSpec, WorkspaceTools } from "./workspace.js";
|
|
8
10
|
export class AgentRunner {
|
|
9
11
|
client;
|
|
@@ -16,10 +18,25 @@ export class AgentRunner {
|
|
|
16
18
|
ollamaUrl: options.ollamaUrl,
|
|
17
19
|
workspace: options.workspace
|
|
18
20
|
});
|
|
21
|
+
const documentAnalyzer = this.client.analyzeFile && (this.client.supportsFileAnalysis?.() ?? true)
|
|
22
|
+
? async (request) => {
|
|
23
|
+
const result = await this.client.analyzeFile?.({
|
|
24
|
+
model: options.model,
|
|
25
|
+
path: request.path,
|
|
26
|
+
prompt: request.prompt,
|
|
27
|
+
signal: request.signal
|
|
28
|
+
});
|
|
29
|
+
return result?.content ?? "";
|
|
30
|
+
}
|
|
31
|
+
: undefined;
|
|
19
32
|
this.tools = new WorkspaceTools({
|
|
20
33
|
root: options.workspace,
|
|
21
34
|
allowWrite: options.allowWrite,
|
|
22
35
|
allowShell: options.allowShell,
|
|
36
|
+
allowShellMetacharacters: options.allowShellMetacharacters,
|
|
37
|
+
allowExternalFileAnalysis: options.allowExternalFileAnalysis,
|
|
38
|
+
documentAnalyzer,
|
|
39
|
+
memoryEnabled: options.memoryEnabled,
|
|
23
40
|
signal: options.signal,
|
|
24
41
|
approvalHandler: options.approvalHandler
|
|
25
42
|
});
|
|
@@ -35,15 +52,34 @@ export class AgentRunner {
|
|
|
35
52
|
startedAt: new Date().toISOString()
|
|
36
53
|
});
|
|
37
54
|
const workspaceSummary = await buildWorkspaceSummary(this.tools.root);
|
|
38
|
-
|
|
55
|
+
const ultramaxx = Boolean(this.options.ultramaxx);
|
|
56
|
+
let maxSteps = resolveMaxSteps(task, this.options.maxSteps, this.options.thinkingMode, ultramaxx);
|
|
39
57
|
const reasoningEffort = resolveProviderReasoning({
|
|
40
58
|
provider: this.options.provider,
|
|
41
59
|
model: this.options.model,
|
|
42
|
-
requested: resolveReasoningEffort(task, this.options.reasoningEffort)
|
|
60
|
+
requested: ultramaxx ? "xhigh" : resolveReasoningEffort(task, this.options.reasoningEffort)
|
|
43
61
|
});
|
|
44
62
|
let stepIndex = 0;
|
|
45
63
|
let repairs = 0;
|
|
64
|
+
let malformedResponses = 0;
|
|
65
|
+
let lastReadFilePath = "";
|
|
46
66
|
let subagentContext = "";
|
|
67
|
+
let todos = [];
|
|
68
|
+
const expectsTodos = shouldExpectTodos(task, ultramaxx);
|
|
69
|
+
let didNudgeForTodos = false;
|
|
70
|
+
let didPushBackForTodos = false;
|
|
71
|
+
let didPushBackForVerification = false;
|
|
72
|
+
let hadWrite = false;
|
|
73
|
+
let verifiedSinceLastWrite = true;
|
|
74
|
+
let emptyToolBatches = 0;
|
|
75
|
+
const recentToolSignatures = [];
|
|
76
|
+
if (ultramaxx) {
|
|
77
|
+
yield {
|
|
78
|
+
type: "status",
|
|
79
|
+
message: "ultramaxx backend escalation active: xhigh reasoning, mandatory todos, expanded verification guard",
|
|
80
|
+
workState: "planning"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
47
83
|
if (this.options.subagents && shouldUseSubagents(task)) {
|
|
48
84
|
yield {
|
|
49
85
|
type: "status",
|
|
@@ -71,11 +107,17 @@ export class AgentRunner {
|
|
|
71
107
|
const messages = [
|
|
72
108
|
{
|
|
73
109
|
role: "system",
|
|
74
|
-
content: buildSystemPrompt(this.tools.root, subagentContext, workspaceSummary, {
|
|
110
|
+
content: buildSystemPrompt(this.tools.root, subagentContext, workspaceSummary, this.options.resumeContext ?? "", {
|
|
75
111
|
mode: this.options.mode ?? (this.options.allowWrite || this.options.allowShell ? "bypass" : "plan"),
|
|
76
112
|
allowWrite: this.options.allowWrite,
|
|
77
113
|
allowShell: this.options.allowShell,
|
|
78
114
|
hasApprovalHandler: Boolean(this.options.approvalHandler)
|
|
115
|
+
}, {
|
|
116
|
+
allowExternalFileAnalysis: Boolean(this.options.allowExternalFileAnalysis),
|
|
117
|
+
memoryEnabled: Boolean(this.options.memoryEnabled),
|
|
118
|
+
allowShellMetacharacters: Boolean(this.options.allowShellMetacharacters),
|
|
119
|
+
ultramaxx,
|
|
120
|
+
expectsTodos
|
|
79
121
|
})
|
|
80
122
|
},
|
|
81
123
|
{
|
|
@@ -107,13 +149,34 @@ export class AgentRunner {
|
|
|
107
149
|
step: stepIndex + 1,
|
|
108
150
|
createdAt: new Date().toISOString()
|
|
109
151
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
152
|
+
let modelResponse;
|
|
153
|
+
try {
|
|
154
|
+
const chatAttempts = this.chatWithRetry({
|
|
155
|
+
model: this.options.model,
|
|
156
|
+
messages,
|
|
157
|
+
reasoningEffort,
|
|
158
|
+
requestWorkState,
|
|
159
|
+
attemptLabel: `step ${stepIndex + 1}`
|
|
160
|
+
});
|
|
161
|
+
for (;;) {
|
|
162
|
+
const nextAttempt = await chatAttempts.next();
|
|
163
|
+
if (nextAttempt.done) {
|
|
164
|
+
modelResponse = nextAttempt.value;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
yield nextAttempt.value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
await this.options.sessionStore?.append({
|
|
173
|
+
type: "run.failed",
|
|
174
|
+
runId,
|
|
175
|
+
message,
|
|
176
|
+
failedAt: new Date().toISOString()
|
|
177
|
+
});
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
117
180
|
const rawResponse = modelResponse.content;
|
|
118
181
|
yield {
|
|
119
182
|
type: "metrics",
|
|
@@ -125,38 +188,89 @@ export class AgentRunner {
|
|
|
125
188
|
parsedResponse = parseAgentResponse(rawResponse);
|
|
126
189
|
}
|
|
127
190
|
catch (error) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
workState: "planning"
|
|
133
|
-
};
|
|
134
|
-
messages.push({
|
|
135
|
-
role: "assistant",
|
|
136
|
-
content: rawResponse
|
|
137
|
-
});
|
|
138
|
-
messages.push({
|
|
139
|
-
role: "user",
|
|
140
|
-
content: "Your previous response was invalid. Return exactly one JSON object now. Do not explain. Use either {\"action\":\"tools\",\"message\":\"...\",\"tool_calls\":[...]} or {\"action\":\"final\",\"message\":\"...\"}. For simple file edits, call write_file with a workspace-relative path."
|
|
141
|
-
});
|
|
142
|
-
if (repairs >= 3) {
|
|
191
|
+
const recoveredResponse = recoverMalformedToolResponse(rawResponse);
|
|
192
|
+
if (recoveredResponse) {
|
|
193
|
+
parsedResponse = recoveredResponse;
|
|
194
|
+
const recoveredPath = recoveredResponse.tool_calls[0]?.arguments.path ?? "workspace file";
|
|
143
195
|
yield {
|
|
144
|
-
type: "
|
|
145
|
-
message:
|
|
146
|
-
workState: "
|
|
196
|
+
type: "status",
|
|
197
|
+
message: `recovered malformed model protocol as ${recoveredResponse.tool_calls[0]?.name ?? "tool"} for ${recoveredPath}`,
|
|
198
|
+
workState: "planning"
|
|
147
199
|
};
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
repairs += 1;
|
|
203
|
+
malformedResponses += 1;
|
|
204
|
+
yield {
|
|
205
|
+
type: "status",
|
|
206
|
+
message: `repairing model protocol: ${formatParseError(error)}`,
|
|
207
|
+
workState: "planning"
|
|
208
|
+
};
|
|
209
|
+
messages.push({
|
|
210
|
+
role: "assistant",
|
|
211
|
+
content: clipPromptValue(rawResponse, 2000)
|
|
153
212
|
});
|
|
154
|
-
|
|
213
|
+
messages.push({
|
|
214
|
+
role: "user",
|
|
215
|
+
content: "Your previous response was invalid. Return exactly one JSON object now. Do not explain. Use either {\"action\":\"tools\",\"message\":\"...\",\"tool_calls\":[...]} or {\"action\":\"final\",\"message\":\"...\"}. For simple file edits, call write_file with a workspace-relative path."
|
|
216
|
+
});
|
|
217
|
+
if (repairs >= 3 || malformedResponses >= 3) {
|
|
218
|
+
yield {
|
|
219
|
+
type: "final",
|
|
220
|
+
message: "The model kept returning invalid tool protocol. Try a stronger coding model or switch advisors off for this task.",
|
|
221
|
+
workState: "error"
|
|
222
|
+
};
|
|
223
|
+
await this.options.sessionStore?.append({
|
|
224
|
+
type: "run.failed",
|
|
225
|
+
runId,
|
|
226
|
+
message: "The model kept returning invalid tool protocol.",
|
|
227
|
+
failedAt: new Date().toISOString()
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
155
232
|
}
|
|
156
|
-
continue;
|
|
157
233
|
}
|
|
158
234
|
repairs = 0;
|
|
159
235
|
if (parsedResponse.action === "final") {
|
|
236
|
+
if (expectsTodos && hasOpenTodos(todos) && !didPushBackForTodos) {
|
|
237
|
+
didPushBackForTodos = true;
|
|
238
|
+
messages.push({
|
|
239
|
+
role: "assistant",
|
|
240
|
+
content: JSON.stringify(parsedResponse)
|
|
241
|
+
});
|
|
242
|
+
messages.push({
|
|
243
|
+
role: "user",
|
|
244
|
+
content: "Your todo list still has pending or in_progress items. Update the todo list first, then return final when the work is genuinely complete."
|
|
245
|
+
});
|
|
246
|
+
stepIndex += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ultramaxx && hadWrite && !verifiedSinceLastWrite && !didPushBackForVerification) {
|
|
250
|
+
didPushBackForVerification = true;
|
|
251
|
+
messages.push({
|
|
252
|
+
role: "assistant",
|
|
253
|
+
content: JSON.stringify(parsedResponse)
|
|
254
|
+
});
|
|
255
|
+
messages.push({
|
|
256
|
+
role: "user",
|
|
257
|
+
content: "You changed files in ultramaxx mode without verification since the last write. Run tests, a script, shell verification, or git_diff before final."
|
|
258
|
+
});
|
|
259
|
+
stepIndex += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (expectsTodos && isTodoOnlyFinalResponse(parsedResponse.message)) {
|
|
263
|
+
messages.push({
|
|
264
|
+
role: "assistant",
|
|
265
|
+
content: JSON.stringify(parsedResponse)
|
|
266
|
+
});
|
|
267
|
+
messages.push({
|
|
268
|
+
role: "user",
|
|
269
|
+
content: "Your final answer was invalid because it only reported todo/status progress or deferred to an earlier step. Return a real final answer now with the actual findings, changes made, verification run, and any remaining risks. Do not mention update_todo as the outcome."
|
|
270
|
+
});
|
|
271
|
+
stepIndex += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
160
274
|
yield {
|
|
161
275
|
type: "final",
|
|
162
276
|
message: parsedResponse.message,
|
|
@@ -170,7 +284,13 @@ export class AgentRunner {
|
|
|
170
284
|
});
|
|
171
285
|
return;
|
|
172
286
|
}
|
|
173
|
-
|
|
287
|
+
yield {
|
|
288
|
+
type: "assistant",
|
|
289
|
+
message: parsedResponse.message,
|
|
290
|
+
workState: "planning"
|
|
291
|
+
};
|
|
292
|
+
const toolCalls = parsedResponse.tool_calls.slice(0, MAX_TOOL_CALLS_PER_RESPONSE).map(normalizeToolCall);
|
|
293
|
+
if (toolCalls.length === 0 && looksLikeClarification(parsedResponse.message)) {
|
|
174
294
|
yield {
|
|
175
295
|
type: "final",
|
|
176
296
|
message: parsedResponse.message,
|
|
@@ -184,17 +304,107 @@ export class AgentRunner {
|
|
|
184
304
|
});
|
|
185
305
|
return;
|
|
186
306
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
307
|
+
if (toolCalls.length === 0) {
|
|
308
|
+
emptyToolBatches += 1;
|
|
309
|
+
messages.push({
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: JSON.stringify(parsedResponse)
|
|
312
|
+
});
|
|
313
|
+
if (shouldStopAfterEmptyToolBatches(emptyToolBatches)) {
|
|
314
|
+
yield {
|
|
315
|
+
type: "final",
|
|
316
|
+
message: "Stopped because the model returned a tool action without tool calls twice. Retry the task or switch models.",
|
|
317
|
+
workState: "error"
|
|
318
|
+
};
|
|
319
|
+
await this.options.sessionStore?.append({
|
|
320
|
+
type: "run.failed",
|
|
321
|
+
runId,
|
|
322
|
+
message: "Model returned empty tool batches repeatedly.",
|
|
323
|
+
failedAt: new Date().toISOString()
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
messages.push({
|
|
328
|
+
role: "user",
|
|
329
|
+
content: "You returned action:\"tools\" without any tool_calls. Either call one valid tool now or return action:\"final\" with the completed answer."
|
|
330
|
+
});
|
|
331
|
+
stepIndex += 1;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
emptyToolBatches = 0;
|
|
335
|
+
const { todoCalls, workspaceCalls } = splitTodoToolCalls(toolCalls);
|
|
336
|
+
if (expectsTodos && todos.length === 0 && workspaceCalls.length > 0 && !didNudgeForTodos) {
|
|
337
|
+
didNudgeForTodos = true;
|
|
338
|
+
messages.push({
|
|
339
|
+
role: "assistant",
|
|
340
|
+
content: JSON.stringify({
|
|
341
|
+
action: "tools",
|
|
342
|
+
message: parsedResponse.message,
|
|
343
|
+
tool_calls: toolCalls
|
|
344
|
+
})
|
|
345
|
+
});
|
|
346
|
+
messages.push({
|
|
347
|
+
role: "user",
|
|
348
|
+
content: "This task has multiple steps. Call update_todo with a concrete 2-6 item plan first, then continue with the needed tools."
|
|
349
|
+
});
|
|
350
|
+
stepIndex += 1;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const repeatedCall = findRepeatedToolCall(workspaceCalls, recentToolSignatures);
|
|
354
|
+
if (repeatedCall) {
|
|
355
|
+
messages.push({
|
|
356
|
+
role: "assistant",
|
|
357
|
+
content: JSON.stringify({
|
|
358
|
+
action: "tools",
|
|
359
|
+
message: parsedResponse.message,
|
|
360
|
+
tool_calls: toolCalls
|
|
361
|
+
})
|
|
362
|
+
});
|
|
363
|
+
messages.push({
|
|
364
|
+
role: "user",
|
|
365
|
+
content: `You already ran ${repeatedCall.name} with the same arguments repeatedly. Act on the previous result or return final instead of repeating it.`
|
|
366
|
+
});
|
|
367
|
+
stepIndex += 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const todoResults = [];
|
|
371
|
+
for (const todoCall of todoCalls) {
|
|
372
|
+
todos = normalizeTodoItems(todoCall.arguments, todos);
|
|
373
|
+
const summary = summarizeTodos(todos);
|
|
374
|
+
yield {
|
|
375
|
+
type: "todo",
|
|
376
|
+
items: todos,
|
|
377
|
+
summary,
|
|
378
|
+
workState: "planning"
|
|
379
|
+
};
|
|
380
|
+
await this.options.sessionStore?.append({
|
|
381
|
+
type: "todo.updated",
|
|
382
|
+
runId,
|
|
383
|
+
items: todos,
|
|
384
|
+
summary,
|
|
385
|
+
createdAt: new Date().toISOString()
|
|
386
|
+
});
|
|
387
|
+
todoResults.push({
|
|
388
|
+
tool: "update_todo",
|
|
389
|
+
ok: true,
|
|
390
|
+
summary,
|
|
391
|
+
content: JSON.stringify({ items: todos }),
|
|
392
|
+
toolCallId: createToolCallId("update_todo"),
|
|
393
|
+
category: "state",
|
|
394
|
+
preview: undefined,
|
|
395
|
+
approval: undefined,
|
|
396
|
+
metadata: {
|
|
397
|
+
items: todos
|
|
398
|
+
},
|
|
399
|
+
workState: "planning"
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const toolCallRecords = workspaceCalls.map((toolCall) => ({
|
|
194
403
|
id: createToolCallId(toolCall.name),
|
|
195
404
|
call: toolCall,
|
|
196
405
|
workState: workStateForTool(toolCall.name)
|
|
197
406
|
}));
|
|
407
|
+
const workspaceCallById = new Map(toolCallRecords.map((record) => [record.id, record.call]));
|
|
198
408
|
for (const record of toolCallRecords) {
|
|
199
409
|
await this.options.sessionStore?.append({
|
|
200
410
|
type: "tool.requested",
|
|
@@ -206,10 +416,25 @@ export class AgentRunner {
|
|
|
206
416
|
createdAt: new Date().toISOString()
|
|
207
417
|
});
|
|
208
418
|
}
|
|
209
|
-
const toolResults =
|
|
210
|
-
|
|
211
|
-
|
|
419
|
+
const toolResults = [
|
|
420
|
+
...todoResults,
|
|
421
|
+
...(await executeToolCallsWithReadParallelism(this.tools, toolCallRecords))
|
|
422
|
+
];
|
|
212
423
|
for (const toolResult of toolResults) {
|
|
424
|
+
if (isWriteToolResult(toolResult)) {
|
|
425
|
+
hadWrite = true;
|
|
426
|
+
verifiedSinceLastWrite = false;
|
|
427
|
+
}
|
|
428
|
+
else if (isVerificationToolResult(toolResult)) {
|
|
429
|
+
verifiedSinceLastWrite = true;
|
|
430
|
+
}
|
|
431
|
+
const sourceCall = workspaceCallById.get(toolResult.toolCallId);
|
|
432
|
+
if (toolResult.ok && sourceCall?.name === "read_file") {
|
|
433
|
+
const readPath = readToolString(sourceCall.arguments.path);
|
|
434
|
+
if (readPath) {
|
|
435
|
+
lastReadFilePath = readPath;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
213
438
|
if (toolResult.approval) {
|
|
214
439
|
yield {
|
|
215
440
|
type: "approval",
|
|
@@ -235,6 +460,7 @@ export class AgentRunner {
|
|
|
235
460
|
toolCallId: toolResult.toolCallId,
|
|
236
461
|
category: toolResult.category,
|
|
237
462
|
preview: toolResult.preview,
|
|
463
|
+
content: toolResult.ok ? undefined : toolResult.content,
|
|
238
464
|
metadata: toolResult.metadata
|
|
239
465
|
};
|
|
240
466
|
await this.options.sessionStore?.append({
|
|
@@ -260,9 +486,24 @@ export class AgentRunner {
|
|
|
260
486
|
role: "user",
|
|
261
487
|
content: formatToolResultsForPrompt(toolResults)
|
|
262
488
|
});
|
|
489
|
+
compactTranscript(messages);
|
|
263
490
|
stepIndex += 1;
|
|
264
|
-
if (this.options.
|
|
265
|
-
|
|
491
|
+
if (this.options.shouldStopAfterStep?.()) {
|
|
492
|
+
yield {
|
|
493
|
+
type: "final",
|
|
494
|
+
message: "Stopped after the current step.",
|
|
495
|
+
workState: "done"
|
|
496
|
+
};
|
|
497
|
+
await this.options.sessionStore?.append({
|
|
498
|
+
type: "run.failed",
|
|
499
|
+
runId,
|
|
500
|
+
message: "Stopped after the current step.",
|
|
501
|
+
failedAt: new Date().toISOString()
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (this.options.thinkingMode === "adaptive" && stepIndex >= maxSteps && shouldExtendAdaptiveRun(task, toolResults, maxSteps, ultramaxx ? 60 : 32)) {
|
|
506
|
+
const nextMaxSteps = Math.min(ultramaxx ? 60 : 32, maxSteps + 4);
|
|
266
507
|
if (nextMaxSteps > maxSteps) {
|
|
267
508
|
maxSteps = nextMaxSteps;
|
|
268
509
|
yield {
|
|
@@ -285,6 +526,73 @@ export class AgentRunner {
|
|
|
285
526
|
failedAt: new Date().toISOString()
|
|
286
527
|
});
|
|
287
528
|
}
|
|
529
|
+
async *chatWithRetry(options) {
|
|
530
|
+
const maxAttempts = 3;
|
|
531
|
+
let lastError = null;
|
|
532
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
533
|
+
try {
|
|
534
|
+
return await this.client.chat({
|
|
535
|
+
model: options.model,
|
|
536
|
+
messages: options.messages,
|
|
537
|
+
formatJson: true,
|
|
538
|
+
reasoningEffort: options.reasoningEffort,
|
|
539
|
+
signal: this.options.signal
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
lastError = error;
|
|
544
|
+
if (this.options.signal?.aborted || attempt >= maxAttempts || !isRetryableModelError(error)) {
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
yield {
|
|
548
|
+
type: "status",
|
|
549
|
+
message: `provider retry ${attempt + 1}/${maxAttempts} after ${formatRetryableModelError(error)}`,
|
|
550
|
+
workState: options.requestWorkState
|
|
551
|
+
};
|
|
552
|
+
await delay(modelRetryDelayMs(attempt));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
throw lastError;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
export function recoverMalformedToolResponse(rawContent) {
|
|
559
|
+
const targetPath = readWriteFileToolPath(rawContent);
|
|
560
|
+
if (!targetPath) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
if (!/"tool_calls"\s*:/.test(rawContent) || !/"name"\s*:\s*"write_file"/.test(rawContent)) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const htmlContent = readFirstRegexGroup(rawContent, /(```(?:html)?\s*)([\s\S]*?<\/html>)\s*```/i, 2) ??
|
|
567
|
+
readFirstRegexGroup(rawContent, /(<!doctype html[\s\S]*?<\/html>)/i) ??
|
|
568
|
+
readFirstRegexGroup(rawContent, /(<html[\s\S]*?<\/html>)/i);
|
|
569
|
+
if (!htmlContent) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
action: "tools",
|
|
574
|
+
message: "Recovered malformed HTML tool response.",
|
|
575
|
+
tool_calls: [
|
|
576
|
+
{
|
|
577
|
+
name: "write_file",
|
|
578
|
+
arguments: {
|
|
579
|
+
path: targetPath,
|
|
580
|
+
content: htmlContent.trim()
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
]
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function readFirstRegexGroup(value, pattern, groupIndex = 1) {
|
|
587
|
+
const match = value.match(pattern);
|
|
588
|
+
const group = match?.[groupIndex];
|
|
589
|
+
return typeof group === "string" && group.trim() ? group : null;
|
|
590
|
+
}
|
|
591
|
+
function readWriteFileToolPath(rawContent) {
|
|
592
|
+
return readFirstRegexGroup(rawContent, /"tool_calls"\s*:\s*\[[\s\S]*?"name"\s*:\s*"write_file"[\s\S]*?"arguments"\s*:\s*\{[\s\S]*?"path"\s*:\s*"([^"]+)"/);
|
|
593
|
+
}
|
|
594
|
+
function readToolString(value) {
|
|
595
|
+
return typeof value === "string" ? value : "";
|
|
288
596
|
}
|
|
289
597
|
function shouldUseSubagents(task) {
|
|
290
598
|
const normalizedTask = task.toLowerCase();
|
|
@@ -293,7 +601,38 @@ function shouldUseSubagents(task) {
|
|
|
293
601
|
}
|
|
294
602
|
return /\b(repo|repository|projekt|project|code|file|datei|test|build|fix|debug|implement|refactor|review|analyze|analyse|analysiere|prüf|pruef|bewerte|architektur|erklär|erklaer|such|find|install|commit|diff|patch|src|readme|sprache|programmiersprache|stack|framework|dependencies|abhängigkeiten|abhaengigkeiten|typescript|javascript|node|swift|python|c)\b/.test(normalizedTask);
|
|
295
603
|
}
|
|
296
|
-
function
|
|
604
|
+
export function shouldExpectTodos(task, ultramaxx = false) {
|
|
605
|
+
if (ultramaxx) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
const words = task.trim().split(/\s+/).filter(Boolean).length;
|
|
609
|
+
return (words > 8 &&
|
|
610
|
+
/\b(implement|refactor|fix|debug|add|build|migrate|rewrite|hardening|verify|test|release|umsetz|reparier|baue|füge|fuege|prüf|pruef)\b/i.test(task));
|
|
611
|
+
}
|
|
612
|
+
export function isTodoOnlyFinalResponse(message) {
|
|
613
|
+
const normalized = message.trim().toLowerCase().replace(/\s+/g, " ");
|
|
614
|
+
if (!normalized) {
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
const mentionsTodoState = /\b(update_todo|todo(?:s|-list| list|-liste| liste)?|checklist|aufgabenliste)\b/i.test(normalized);
|
|
618
|
+
const mentionsProgressOnly = /\b(updated|aktualisiert|completed|complete|done|erledigt|abgeschlossen|marked|gepflegt)\b/i.test(normalized);
|
|
619
|
+
const defersToEarlierStep = /\b(previous|prior|earlier|above|already|before)\b/i.test(normalized) ||
|
|
620
|
+
/\b(vorherig|vorherigen|vorher|oben|zuvor|bereits|letzten schritt)\b/i.test(normalized);
|
|
621
|
+
const mentionsDeferredAnswer = /\b(summary|overview|answer|result|findings|zusammenfassung|überblick|ueberblick|antwort|ergebnis)\b/i.test(normalized);
|
|
622
|
+
const hasConcreteOutcome = /\b(fixed|implemented|changed|added|removed|verified|tested|ran|created|gefixt|implementiert|geändert|geaendert|ergänzt|ergaenzt|verifiziert|getestet)\b|(?:^|\s)(?:src|tests|docs)\//i.test(normalized);
|
|
623
|
+
const wordCount = normalized.split(/\s+/).filter(Boolean).length;
|
|
624
|
+
if (mentionsTodoState && defersToEarlierStep) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
if (mentionsTodoState && mentionsProgressOnly && !hasConcreteOutcome && wordCount <= 18) {
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
if (mentionsDeferredAnswer && defersToEarlierStep && !hasConcreteOutcome && wordCount <= 24) {
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
return /^(done|complete|completed|erledigt|fertig|ok)[.! ]*$/i.test(normalized);
|
|
634
|
+
}
|
|
635
|
+
function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, resumeContext, permissions, experimental) {
|
|
297
636
|
const workspaceLabel = path.basename(workspaceRoot) || "workspace";
|
|
298
637
|
const writePolicy = permissions.allowWrite
|
|
299
638
|
? "write tools bypass approval for this run"
|
|
@@ -305,6 +644,9 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, per
|
|
|
305
644
|
: permissions.hasApprovalHandler && permissions.mode === "build"
|
|
306
645
|
? "shell and test tools require interactive approval"
|
|
307
646
|
: "shell and test tools are unavailable";
|
|
647
|
+
const bypassPolicy = permissions.allowWrite && permissions.allowShell
|
|
648
|
+
? "Build+bypass mode may run write, script, test, or shell tools without per-tool approval. Keep actions narrow and avoid broad destructive commands."
|
|
649
|
+
: "Only explicitly enabled permission groups bypass approval. Unavailable tool groups stay blocked.";
|
|
308
650
|
return [
|
|
309
651
|
"You are PatchPilot, a local coding agent running inside a terminal TUI.",
|
|
310
652
|
"You help inspect, edit, test, and explain code inside one workspace.",
|
|
@@ -316,7 +658,7 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, per
|
|
|
316
658
|
permissions.mode === "plan"
|
|
317
659
|
? "Plan mode is read-only: inspect files, explain findings, and return an implementation plan. Do not call write_file, apply_patch, run_script, run_tests, or run_shell."
|
|
318
660
|
: permissions.mode === "bypass"
|
|
319
|
-
?
|
|
661
|
+
? bypassPolicy
|
|
320
662
|
: "Build mode may request write, script, test, or shell tools when necessary. Prefer focused tool calls and keep risky actions easy to approve.",
|
|
321
663
|
`All tool paths are relative to the workspace root. If the workspace is named "${workspaceLabel}", do not prefix paths with "${workspaceLabel}/". Use "." for the workspace root.`,
|
|
322
664
|
"Treat short questions about this project, its language, stack, quality, architecture, dependencies, tests, or files as workspace questions.",
|
|
@@ -327,8 +669,37 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, per
|
|
|
327
669
|
"Never pass placeholder examples like relative/path, path/to/file, or <path> as tool arguments.",
|
|
328
670
|
"For repository summaries, inspect README.md, package.json, tests, docs, and top-level source files before answering.",
|
|
329
671
|
"For implementation tasks, first inspect the narrowest relevant files, then edit only what is needed.",
|
|
672
|
+
experimental.expectsTodos
|
|
673
|
+
? "This task requires proactive todos. Your FIRST tool call MUST be update_todo with a concrete 2-6 item plan before reading or editing. After each completed step, call update_todo to mark progress. Exactly one item may be in_progress."
|
|
674
|
+
: "If a task needs more than one tool call or touches more than one file, your FIRST tool call MUST be update_todo with a concrete 2-6 item plan. Tiny one-file edits, single-file reads, and direct answers do not need todos.",
|
|
675
|
+
"Todo example required: user asks to fix a provider bug and run tests -> first call update_todo, then inspect, edit, verify, and mark items completed.",
|
|
676
|
+
"Todo example not required: user asks what package manager this repo uses -> inspect package files or answer directly.",
|
|
677
|
+
experimental.ultramaxx
|
|
678
|
+
? "ULTRAMAXX mode is active: use xhigh care, keep todos mandatory, verify after writes before final, and do not skip self-checks."
|
|
679
|
+
: "",
|
|
680
|
+
experimental.expectsTodos
|
|
681
|
+
? "Final answers must contain the actual outcome: findings, changes made, verification run, and remaining risks when relevant. Never use final just to say the todo list was updated or that the answer is in a previous step."
|
|
682
|
+
: "",
|
|
330
683
|
"When diagnosing a failure, form a concrete hypothesis, gather targeted evidence with tools, then fix the smallest cause.",
|
|
684
|
+
experimental.allowExternalFileAnalysis
|
|
685
|
+
? "Experimental file analysis is enabled: inspect_document may inspect supported absolute paths outside the workspace when the user provides them."
|
|
686
|
+
: "Experimental file analysis is disabled: inspect_document is limited to the workspace.",
|
|
687
|
+
experimental.memoryEnabled
|
|
688
|
+
? "Experimental memory is enabled: use memory_search for relevant durable context and memory_remember when the user asks you to remember something or states durable project guidance."
|
|
689
|
+
: "Experimental memory is disabled.",
|
|
690
|
+
experimental.allowShellMetacharacters
|
|
691
|
+
? "Experimental shell metacharacters are enabled: run_shell may use pipes, &&, and ;. Redirects, shell expansion, background jobs, OR chains, and multiline commands still require explicit approval even in bypass."
|
|
692
|
+
: "Experimental shell metacharacters are disabled: run_shell may use simple commands and pipes only.",
|
|
331
693
|
workspaceSummary ? ["", "Workspace context:", workspaceSummary].join("\n") : "",
|
|
694
|
+
resumeContext
|
|
695
|
+
? [
|
|
696
|
+
"",
|
|
697
|
+
"Resumed session context:",
|
|
698
|
+
resumeContext,
|
|
699
|
+
"",
|
|
700
|
+
"Use this as compact historical context. Re-read files before making claims about current workspace contents."
|
|
701
|
+
].join("\n")
|
|
702
|
+
: "",
|
|
332
703
|
subagentContext
|
|
333
704
|
? [
|
|
334
705
|
"",
|
|
@@ -349,17 +720,33 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, per
|
|
|
349
720
|
"{\"action\":\"final\",\"message\":\"short useful answer\"}",
|
|
350
721
|
"",
|
|
351
722
|
"Available tools:",
|
|
723
|
+
"- update_todo: {\"items\":[{\"id\":\"inspect\",\"content\":\"Inspect relevant files\",\"status\":\"in_progress\"},{\"id\":\"verify\",\"content\":\"Run checks\",\"status\":\"pending\"}]} to maintain the visible task checklist. For multi-step implementation work, call this before and after meaningful task changes.",
|
|
352
724
|
"- list_files: {\"path\":\".\"}",
|
|
725
|
+
"- find_files: {\"query\":\"agent\",\"limit\":80} for matching workspace file paths without reading file contents",
|
|
353
726
|
"- read_file: {\"path\":\"src/index.ts\"}",
|
|
354
727
|
"- read_range: {\"path\":\"src/index.ts\",\"start\":1,\"end\":80}",
|
|
355
728
|
"- file_info: {\"path\":\"src/index.ts\"}",
|
|
356
729
|
"- search_text: {\"query\":\"functionName\"}",
|
|
357
|
-
"- inspect_document: {\"path\":\"docs/spec.pdf\"} for pdf, docx, and text/code files",
|
|
730
|
+
"- inspect_document: {\"path\":\"docs/spec.pdf\",\"mode\":\"auto\"} for pdf, docx, images, and text/code files. Use mode \"local\" or \"ocr\" only when the user explicitly asks for local text/OCR extraction.",
|
|
731
|
+
...(experimental.memoryEnabled
|
|
732
|
+
? [
|
|
733
|
+
"- memory_search: {\"query\":\"provider setup\",\"limit\":5}",
|
|
734
|
+
"- memory_remember: {\"content\":\"durable note\",\"tags\":[\"project\"]}"
|
|
735
|
+
]
|
|
736
|
+
: []),
|
|
358
737
|
"- git_status: {} for current branch and dirty files",
|
|
359
738
|
"- git_diff: {\"path\":\"src/index.ts\"} or {} for all current changes",
|
|
739
|
+
"- git_log: {\"limit\":8} for recent commits without shell access",
|
|
740
|
+
"- git_show: {\"revision\":\"HEAD\",\"path\":\"src/index.ts\"} or {\"revision\":\"HEAD\"} for a compact revision summary",
|
|
360
741
|
"- list_changed_files: {}",
|
|
361
742
|
"- list_scripts: {} for package manager scripts from package.json",
|
|
362
|
-
"-
|
|
743
|
+
"- repo_overview: {} for package metadata, Git state, and top-level files",
|
|
744
|
+
"- test_list: {} for likely tests and test-related scripts without running them",
|
|
745
|
+
"- dependency_tree: {} for top-level package dependencies",
|
|
746
|
+
"- write_file: {\"path\":\"test2/test.txt\",\"content\":\"full file content\"} for new files or intentional full-file replacement",
|
|
747
|
+
"- edit_file: {\"path\":\"src/index.ts\",\"find\":\"old unique text\",\"replace\":\"new text\"} or {\"path\":\"src/index.ts\",\"startLine\":10,\"endLine\":12,\"expected\":\"current lines\",\"replacement\":\"new lines\"} for existing files. Include expected with line-range edits when you have read the target lines.",
|
|
748
|
+
"- create_pdf: {\"path\":\"docs/summary.pdf\",\"title\":\"Summary\",\"content\":\"plain text\"}",
|
|
749
|
+
"- create_docx: {\"path\":\"docs/summary.docx\",\"title\":\"Summary\",\"content\":\"plain text\"}",
|
|
363
750
|
"- apply_patch: {\"patch\":\"unified git patch\"}",
|
|
364
751
|
"- run_script: {\"script\":\"test\"}",
|
|
365
752
|
"- run_tests: {}",
|
|
@@ -367,9 +754,9 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, per
|
|
|
367
754
|
"",
|
|
368
755
|
"Act like a coding agent. For simple create/edit/run requests, use tools directly instead of over-warning.",
|
|
369
756
|
"Do not call search_text with an empty query. Use list_files {\"path\":\".\"} to inspect a directory.",
|
|
370
|
-
"Prefer reading before risky edits
|
|
371
|
-
"Batch
|
|
372
|
-
"
|
|
757
|
+
"Prefer reading before risky edits. For existing files, prefer edit_file or apply_patch over full-file write_file.",
|
|
758
|
+
"Batch small related tool calls in one response when it helps avoid extra thinking steps.",
|
|
759
|
+
"Keep update_todo current: mark exactly what you are doing as in_progress and completed tasks as completed.",
|
|
373
760
|
"In final answers, separate verified facts from remaining risks.",
|
|
374
761
|
"Keep tool requests and final answers compact."
|
|
375
762
|
].join("\n");
|
|
@@ -379,8 +766,36 @@ function looksLikeClarification(message) {
|
|
|
379
766
|
return (normalizedMessage.endsWith("?") &&
|
|
380
767
|
/\b(what|which|please provide|would you like|do you want|can you specify|welche|was genau|bitte)\b/.test(normalizedMessage));
|
|
381
768
|
}
|
|
382
|
-
function
|
|
383
|
-
return
|
|
769
|
+
export function shouldStopAfterEmptyToolBatches(emptyToolBatches) {
|
|
770
|
+
return emptyToolBatches >= 2;
|
|
771
|
+
}
|
|
772
|
+
export function findRepeatedToolCall(toolCalls, recentSignatures) {
|
|
773
|
+
for (const toolCall of toolCalls) {
|
|
774
|
+
const signature = toolCallSignature(toolCall);
|
|
775
|
+
recentSignatures.push(signature);
|
|
776
|
+
while (recentSignatures.length > 6) {
|
|
777
|
+
recentSignatures.shift();
|
|
778
|
+
}
|
|
779
|
+
if (recentSignatures.filter((item) => item === signature).length >= 3) {
|
|
780
|
+
return toolCall;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
function toolCallSignature(toolCall) {
|
|
786
|
+
return `${toolCall.name}:${stableStringify(toolCall.arguments)}`;
|
|
787
|
+
}
|
|
788
|
+
function stableStringify(value) {
|
|
789
|
+
if (Array.isArray(value)) {
|
|
790
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
791
|
+
}
|
|
792
|
+
if (isRecord(value)) {
|
|
793
|
+
return `{${Object.keys(value)
|
|
794
|
+
.sort()
|
|
795
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
796
|
+
.join(",")}}`;
|
|
797
|
+
}
|
|
798
|
+
return JSON.stringify(value);
|
|
384
799
|
}
|
|
385
800
|
function normalizeToolCall(toolCall) {
|
|
386
801
|
if (toolCall.name === "search_text") {
|
|
@@ -396,13 +811,108 @@ function normalizeToolCall(toolCall) {
|
|
|
396
811
|
}
|
|
397
812
|
return toolCall;
|
|
398
813
|
}
|
|
399
|
-
|
|
814
|
+
function splitTodoToolCalls(toolCalls) {
|
|
815
|
+
return {
|
|
816
|
+
todoCalls: toolCalls.filter((toolCall) => toolCall.name === "update_todo"),
|
|
817
|
+
workspaceCalls: toolCalls.filter((toolCall) => toolCall.name !== "update_todo")
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
export function normalizeTodoItems(argumentsValue, existingItems = []) {
|
|
821
|
+
const rawItems = Array.isArray(argumentsValue.items) ? argumentsValue.items : Array.isArray(argumentsValue.todos) ? argumentsValue.todos : [];
|
|
822
|
+
const existingById = new Map(existingItems.map((item) => [item.id, item]));
|
|
823
|
+
const normalizedItems = [];
|
|
824
|
+
for (const rawItem of rawItems.slice(0, 12)) {
|
|
825
|
+
if (!isRecord(rawItem)) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const content = readTodoString(rawItem.content) || readTodoString(rawItem.text) || readTodoString(rawItem.task);
|
|
829
|
+
if (!content) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const explicitId = readTodoString(rawItem.id);
|
|
833
|
+
const id = sanitizeTodoId(explicitId || existingById.get(content)?.id || content);
|
|
834
|
+
const status = normalizeTodoStatus(rawItem.status);
|
|
835
|
+
normalizedItems.push({
|
|
836
|
+
id,
|
|
837
|
+
content: content.slice(0, 120),
|
|
838
|
+
status
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return dedupeTodoItems(normalizedItems);
|
|
842
|
+
}
|
|
843
|
+
function summarizeTodos(items) {
|
|
844
|
+
const completed = items.filter((item) => item.status === "completed").length;
|
|
845
|
+
const active = items.find((item) => item.status === "in_progress");
|
|
846
|
+
return active ? `${completed}/${items.length} done, working on ${active.content}` : `${completed}/${items.length} todos done`;
|
|
847
|
+
}
|
|
848
|
+
function hasOpenTodos(items) {
|
|
849
|
+
return items.some((item) => item.status === "pending" || item.status === "in_progress");
|
|
850
|
+
}
|
|
851
|
+
function dedupeTodoItems(items) {
|
|
852
|
+
const seen = new Set();
|
|
853
|
+
const deduped = [];
|
|
854
|
+
for (const item of items) {
|
|
855
|
+
const uniqueId = seen.has(item.id) ? `${item.id}-${deduped.length + 1}` : item.id;
|
|
856
|
+
seen.add(uniqueId);
|
|
857
|
+
deduped.push({ ...item, id: uniqueId });
|
|
858
|
+
}
|
|
859
|
+
return deduped;
|
|
860
|
+
}
|
|
861
|
+
function normalizeTodoStatus(value) {
|
|
862
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase().replace(/[- ]/g, "_") : "";
|
|
863
|
+
if (normalized === "done" || normalized === "complete" || normalized === "completed" || normalized === "checked") {
|
|
864
|
+
return "completed";
|
|
865
|
+
}
|
|
866
|
+
if (normalized === "active" || normalized === "doing" || normalized === "current" || normalized === "in_progress") {
|
|
867
|
+
return "in_progress";
|
|
868
|
+
}
|
|
869
|
+
return "pending";
|
|
870
|
+
}
|
|
871
|
+
function sanitizeTodoId(value) {
|
|
872
|
+
const normalized = value
|
|
873
|
+
.trim()
|
|
874
|
+
.toLowerCase()
|
|
875
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
876
|
+
.replace(/^-+|-+$/g, "");
|
|
877
|
+
return normalized.slice(0, 40) || `todo-${Date.now().toString(36)}`;
|
|
878
|
+
}
|
|
879
|
+
function readTodoString(value) {
|
|
880
|
+
return typeof value === "string" ? value.trim() : "";
|
|
881
|
+
}
|
|
882
|
+
function isRecord(value) {
|
|
883
|
+
return typeof value === "object" && value !== null;
|
|
884
|
+
}
|
|
885
|
+
export async function executeToolCallsWithReadParallelism(tools, toolCalls) {
|
|
400
886
|
const results = [];
|
|
401
|
-
for (
|
|
402
|
-
|
|
887
|
+
for (let index = 0; index < toolCalls.length;) {
|
|
888
|
+
const currentCall = toolCalls[index];
|
|
889
|
+
if (!isParallelSafeToolCall(currentCall)) {
|
|
890
|
+
results.push(await executeToolSafely(tools, currentCall.call, currentCall.id));
|
|
891
|
+
index += 1;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const batch = [];
|
|
895
|
+
while (index < toolCalls.length && isParallelSafeToolCall(toolCalls[index])) {
|
|
896
|
+
batch.push(toolCalls[index]);
|
|
897
|
+
index += 1;
|
|
898
|
+
}
|
|
899
|
+
results.push(...(await Promise.all(batch.map((toolCall) => executeToolSafely(tools, toolCall.call, toolCall.id)))));
|
|
403
900
|
}
|
|
404
901
|
return results;
|
|
405
902
|
}
|
|
903
|
+
function isParallelSafeToolCall(toolCall) {
|
|
904
|
+
if (toolCall.call.name === "inspect_document") {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
const spec = getToolSpec(toolCall.call.name);
|
|
908
|
+
return spec.sideEffects === "none" && spec.permission === "none" && spec.category !== "state";
|
|
909
|
+
}
|
|
910
|
+
function isWriteToolResult(toolResult) {
|
|
911
|
+
return toolResult.ok && (toolResult.category === "write" || getToolSpec(toolResult.tool).sideEffects === "write");
|
|
912
|
+
}
|
|
913
|
+
function isVerificationToolResult(toolResult) {
|
|
914
|
+
return toolResult.ok && (toolResult.category === "test" || toolResult.category === "shell" || toolResult.tool === "git_diff");
|
|
915
|
+
}
|
|
406
916
|
async function executeToolSafely(tools, toolCall, toolCallId) {
|
|
407
917
|
const toolResult = await tools.execute(toolCall).catch((error) => ({
|
|
408
918
|
ok: false,
|
|
@@ -426,13 +936,58 @@ async function executeToolSafely(tools, toolCall, toolCallId) {
|
|
|
426
936
|
}
|
|
427
937
|
function formatToolResultsForPrompt(toolResults) {
|
|
428
938
|
return [
|
|
429
|
-
"Tool results
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
939
|
+
"Tool results are encoded as JSON. Treat content as context; do not copy raw file content into response JSON unless it is properly escaped.",
|
|
940
|
+
JSON.stringify({
|
|
941
|
+
tool_results: toolResults.map((toolResult, index) => ({
|
|
942
|
+
index: index + 1,
|
|
943
|
+
tool: toolResult.tool,
|
|
944
|
+
ok: toolResult.ok,
|
|
945
|
+
summary: toolResult.summary,
|
|
946
|
+
metadata: toolResult.metadata,
|
|
947
|
+
content: clipPromptValue(toolResult.content, toolResult.tool === "read_file" ? 12_000 : 6000)
|
|
948
|
+
}))
|
|
949
|
+
}, null, 2)
|
|
950
|
+
].join("\n");
|
|
951
|
+
}
|
|
952
|
+
export function compactTranscript(messages, tokenBudget = 32_000) {
|
|
953
|
+
const toolResultIndexes = messages
|
|
954
|
+
.map((message, index) => ({ message, index }))
|
|
955
|
+
.filter((item) => item.message.role === "user" && item.message.content.includes("\"tool_results\""))
|
|
956
|
+
.map((item) => item.index);
|
|
957
|
+
if (toolResultIndexes.length <= 2 && estimateTokens(messages.map((message) => message.content).join("\n")) <= tokenBudget) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const fullResultIndexes = new Set(toolResultIndexes.slice(-2));
|
|
961
|
+
for (const index of toolResultIndexes) {
|
|
962
|
+
if (fullResultIndexes.has(index) || messages[index]?.content.startsWith("Compacted earlier tool results:")) {
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
messages[index] = {
|
|
966
|
+
role: "user",
|
|
967
|
+
content: `Compacted earlier tool results: ${summarizeToolResultsForCompaction(messages[index]?.content ?? "")}`
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function summarizeToolResultsForCompaction(content) {
|
|
972
|
+
const jsonStart = content.indexOf("{");
|
|
973
|
+
if (jsonStart < 0) {
|
|
974
|
+
return clipPromptValue(content.replace(/\s+/g, " ").trim(), 240);
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
const parsed = JSON.parse(content.slice(jsonStart));
|
|
978
|
+
const results = Array.isArray(parsed.tool_results) ? parsed.tool_results : [];
|
|
979
|
+
return results
|
|
980
|
+
.map((result, index) => {
|
|
981
|
+
const tool = typeof result.tool === "string" ? result.tool : `tool ${index + 1}`;
|
|
982
|
+
const status = result.ok === false ? "failed" : "ok";
|
|
983
|
+
const summary = typeof result.summary === "string" ? result.summary.replace(/\s+/g, " ").trim() : "";
|
|
984
|
+
return `${tool} ${status}${summary ? `: ${clipPromptValue(summary, 140)}` : ""}`;
|
|
985
|
+
})
|
|
986
|
+
.join("; ");
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return clipPromptValue(content.replace(/\s+/g, " ").trim(), 240);
|
|
990
|
+
}
|
|
436
991
|
}
|
|
437
992
|
function workStateForTool(tool) {
|
|
438
993
|
const category = getToolSpec(tool).category;
|
|
@@ -454,15 +1009,23 @@ function createToolCallId(tool) {
|
|
|
454
1009
|
return `${tool}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
455
1010
|
}
|
|
456
1011
|
async function buildWorkspaceSummary(workspaceRoot) {
|
|
457
|
-
const [packageJson, tsconfig, readme] = await Promise.all([
|
|
1012
|
+
const [patchPilotInstructions, packageJson, tsconfig, readme, productContext, architecture, commands] = await Promise.all([
|
|
1013
|
+
readWorkspaceFile(workspaceRoot, "PATCHPILOT.md", 4000),
|
|
458
1014
|
readWorkspaceFile(workspaceRoot, "package.json", 4000),
|
|
459
1015
|
readWorkspaceFile(workspaceRoot, "tsconfig.json", 1600),
|
|
460
|
-
readWorkspaceFile(workspaceRoot, "README.md", 3000)
|
|
1016
|
+
readWorkspaceFile(workspaceRoot, "README.md", 3000),
|
|
1017
|
+
readWorkspaceFile(workspaceRoot, "docs/product-context.md", 4000),
|
|
1018
|
+
readWorkspaceFile(workspaceRoot, "docs/architecture.md", 2200),
|
|
1019
|
+
readWorkspaceFile(workspaceRoot, "src/tui/commands.ts", 2400)
|
|
461
1020
|
]);
|
|
462
1021
|
return [
|
|
1022
|
+
patchPilotInstructions ? `PATCHPILOT.md instructions:\n${patchPilotInstructions}` : "",
|
|
1023
|
+
productContext ? `PatchPilot product context:\n${productContext}` : "",
|
|
463
1024
|
packageJson ? `package.json:\n${packageJson}` : "",
|
|
464
1025
|
tsconfig ? `tsconfig.json:\n${tsconfig}` : "",
|
|
465
|
-
readme ? `README excerpt:\n${readme}` : ""
|
|
1026
|
+
readme ? `README excerpt:\n${readme}` : "",
|
|
1027
|
+
architecture ? `Architecture excerpt:\n${architecture}` : "",
|
|
1028
|
+
commands ? `TUI command definitions excerpt:\n${commands}` : ""
|
|
466
1029
|
]
|
|
467
1030
|
.filter(Boolean)
|
|
468
1031
|
.join("\n\n");
|
|
@@ -477,22 +1040,34 @@ async function readWorkspaceFile(workspaceRoot, relativePath, maxLength) {
|
|
|
477
1040
|
const content = await readFile(normalizedFile, "utf8").catch(() => "");
|
|
478
1041
|
return clipPromptValue(content.trim(), maxLength);
|
|
479
1042
|
}
|
|
480
|
-
function resolveMaxSteps(task, configuredMaxSteps, thinkingMode) {
|
|
1043
|
+
function resolveMaxSteps(task, configuredMaxSteps, thinkingMode, ultramaxx = false) {
|
|
481
1044
|
if (thinkingMode !== "adaptive") {
|
|
482
|
-
return configuredMaxSteps;
|
|
1045
|
+
return Math.max(2, configuredMaxSteps);
|
|
483
1046
|
}
|
|
484
1047
|
const words = task.trim().split(/\s+/).filter(Boolean).length;
|
|
485
1048
|
const looksComplex = shouldUseSubagents(task) || words > 18 || /\b(implement|refactor|debug|fix|review|architektur|performance|pipeline|context|memory|provider)\b/i.test(task);
|
|
1049
|
+
if (ultramaxx) {
|
|
1050
|
+
return Math.max(40, configuredMaxSteps);
|
|
1051
|
+
}
|
|
486
1052
|
const adaptiveSteps = looksComplex ? Math.max(configuredMaxSteps, 12) : Math.min(configuredMaxSteps, 5);
|
|
487
1053
|
return Math.max(3, Math.min(20, adaptiveSteps));
|
|
488
1054
|
}
|
|
489
|
-
function shouldExtendAdaptiveRun(task, toolResults, currentMaxSteps) {
|
|
490
|
-
if (currentMaxSteps >=
|
|
1055
|
+
function shouldExtendAdaptiveRun(task, toolResults, currentMaxSteps, ceiling = 32) {
|
|
1056
|
+
if (currentMaxSteps >= ceiling) {
|
|
491
1057
|
return false;
|
|
492
1058
|
}
|
|
493
|
-
const hasUsefulProgress = toolResults.some((result) => result.ok
|
|
1059
|
+
const hasUsefulProgress = toolResults.some((result) => result.ok &&
|
|
1060
|
+
(result.category === "write" ||
|
|
1061
|
+
result.category === "test" ||
|
|
1062
|
+
result.category === "shell" ||
|
|
1063
|
+
result.tool === "git_diff" ||
|
|
1064
|
+
(result.tool === "update_todo" && todoMetadataHasCompletedItem(result.metadata))));
|
|
494
1065
|
const hasRecoverableFailure = toolResults.some((result) => !result.ok && /not found|missing|requires|denied|failed|unreadable/i.test(result.summary));
|
|
495
|
-
return hasUsefulProgress || hasRecoverableFailure || shouldUseSubagents(task);
|
|
1066
|
+
return hasUsefulProgress || hasRecoverableFailure || (shouldUseSubagents(task) && hasUsefulProgress);
|
|
1067
|
+
}
|
|
1068
|
+
function todoMetadataHasCompletedItem(metadata) {
|
|
1069
|
+
const items = Array.isArray(metadata?.items) ? metadata.items : [];
|
|
1070
|
+
return items.some((item) => isRecord(item) && item.status === "completed");
|
|
496
1071
|
}
|
|
497
1072
|
function resolveReasoningEffort(task, effort) {
|
|
498
1073
|
if (effort !== "adaptive") {
|
|
@@ -513,4 +1088,20 @@ function clipPromptValue(value, maxLength) {
|
|
|
513
1088
|
}
|
|
514
1089
|
return `${value.slice(0, maxLength)}\n...[clipped ${value.length - maxLength} chars]`;
|
|
515
1090
|
}
|
|
1091
|
+
function isRetryableModelError(error) {
|
|
1092
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1093
|
+
return /\b(429|500|502|503|504|rate limit|timeout|timed out|socket|econnreset|network|temporar|could not be reached|cannot reach)\b/i.test(message);
|
|
1094
|
+
}
|
|
1095
|
+
function formatRetryableModelError(error) {
|
|
1096
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1097
|
+
return clipPromptValue(message.replace(/\s+/g, " ").trim(), 120);
|
|
1098
|
+
}
|
|
1099
|
+
function modelRetryDelayMs(attempt) {
|
|
1100
|
+
return Math.min(4000, 300 * 2 ** Math.max(0, attempt - 1) + Math.floor(Math.random() * 250));
|
|
1101
|
+
}
|
|
1102
|
+
function delay(durationMs) {
|
|
1103
|
+
return new Promise((resolve) => {
|
|
1104
|
+
setTimeout(resolve, durationMs);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
516
1107
|
//# sourceMappingURL=agent.js.map
|