@jx-grxf/patchpilot 1.0.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/README.md +51 -16
- package/dist/cli.js +46 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/agent.d.ts +44 -1
- package/dist/core/agent.js +617 -70
- package/dist/core/agent.js.map +1 -1
- 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.js +9 -8
- 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 +43 -2
- package/dist/core/geminiWrapper.js +582 -42
- package/dist/core/geminiWrapper.js.map +1 -1
- 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 +18 -20
- package/dist/core/json.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/reasoning.js +3 -0
- package/dist/core/reasoning.js.map +1 -1
- package/dist/core/session.js +9 -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 +58 -3
- 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 +29 -0
- package/dist/core/workspace.js +1271 -92
- package/dist/core/workspace.js.map +1 -1
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +1346 -112
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/commands.js +109 -6
- 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 +26 -3
- 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 +1 -1
- package/dist/tui/components/ExperimentalPanel.js +5 -0
- package/dist/tui/components/ExperimentalPanel.js.map +1 -1
- package/dist/tui/components/OnboardingPanel.d.ts +12 -0
- package/dist/tui/components/OnboardingPanel.js +69 -21
- package/dist/tui/components/OnboardingPanel.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 +86 -16
- 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 +14 -0
- 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 +7 -0
- package/dist/tui/modes.js +12 -0
- 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 +23 -0
- package/docs/product-context.md +43 -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/package.json +4 -2
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,11 +18,24 @@ 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,
|
|
23
37
|
allowExternalFileAnalysis: options.allowExternalFileAnalysis,
|
|
38
|
+
documentAnalyzer,
|
|
24
39
|
memoryEnabled: options.memoryEnabled,
|
|
25
40
|
signal: options.signal,
|
|
26
41
|
approvalHandler: options.approvalHandler
|
|
@@ -37,15 +52,34 @@ export class AgentRunner {
|
|
|
37
52
|
startedAt: new Date().toISOString()
|
|
38
53
|
});
|
|
39
54
|
const workspaceSummary = await buildWorkspaceSummary(this.tools.root);
|
|
40
|
-
|
|
55
|
+
const ultramaxx = Boolean(this.options.ultramaxx);
|
|
56
|
+
let maxSteps = resolveMaxSteps(task, this.options.maxSteps, this.options.thinkingMode, ultramaxx);
|
|
41
57
|
const reasoningEffort = resolveProviderReasoning({
|
|
42
58
|
provider: this.options.provider,
|
|
43
59
|
model: this.options.model,
|
|
44
|
-
requested: resolveReasoningEffort(task, this.options.reasoningEffort)
|
|
60
|
+
requested: ultramaxx ? "xhigh" : resolveReasoningEffort(task, this.options.reasoningEffort)
|
|
45
61
|
});
|
|
46
62
|
let stepIndex = 0;
|
|
47
63
|
let repairs = 0;
|
|
64
|
+
let malformedResponses = 0;
|
|
65
|
+
let lastReadFilePath = "";
|
|
48
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
|
+
}
|
|
49
83
|
if (this.options.subagents && shouldUseSubagents(task)) {
|
|
50
84
|
yield {
|
|
51
85
|
type: "status",
|
|
@@ -80,7 +114,10 @@ export class AgentRunner {
|
|
|
80
114
|
hasApprovalHandler: Boolean(this.options.approvalHandler)
|
|
81
115
|
}, {
|
|
82
116
|
allowExternalFileAnalysis: Boolean(this.options.allowExternalFileAnalysis),
|
|
83
|
-
memoryEnabled: Boolean(this.options.memoryEnabled)
|
|
117
|
+
memoryEnabled: Boolean(this.options.memoryEnabled),
|
|
118
|
+
allowShellMetacharacters: Boolean(this.options.allowShellMetacharacters),
|
|
119
|
+
ultramaxx,
|
|
120
|
+
expectsTodos
|
|
84
121
|
})
|
|
85
122
|
},
|
|
86
123
|
{
|
|
@@ -114,13 +151,21 @@ export class AgentRunner {
|
|
|
114
151
|
});
|
|
115
152
|
let modelResponse;
|
|
116
153
|
try {
|
|
117
|
-
|
|
154
|
+
const chatAttempts = this.chatWithRetry({
|
|
118
155
|
model: this.options.model,
|
|
119
156
|
messages,
|
|
120
|
-
formatJson: true,
|
|
121
157
|
reasoningEffort,
|
|
122
|
-
|
|
158
|
+
requestWorkState,
|
|
159
|
+
attemptLabel: `step ${stepIndex + 1}`
|
|
123
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
|
+
}
|
|
124
169
|
}
|
|
125
170
|
catch (error) {
|
|
126
171
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -143,38 +188,89 @@ export class AgentRunner {
|
|
|
143
188
|
parsedResponse = parseAgentResponse(rawResponse);
|
|
144
189
|
}
|
|
145
190
|
catch (error) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
workState: "planning"
|
|
151
|
-
};
|
|
152
|
-
messages.push({
|
|
153
|
-
role: "assistant",
|
|
154
|
-
content: clipPromptValue(rawResponse, 2000)
|
|
155
|
-
});
|
|
156
|
-
messages.push({
|
|
157
|
-
role: "user",
|
|
158
|
-
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."
|
|
159
|
-
});
|
|
160
|
-
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";
|
|
161
195
|
yield {
|
|
162
|
-
type: "
|
|
163
|
-
message:
|
|
164
|
-
workState: "
|
|
196
|
+
type: "status",
|
|
197
|
+
message: `recovered malformed model protocol as ${recoveredResponse.tool_calls[0]?.name ?? "tool"} for ${recoveredPath}`,
|
|
198
|
+
workState: "planning"
|
|
165
199
|
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
171
212
|
});
|
|
172
|
-
|
|
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;
|
|
173
232
|
}
|
|
174
|
-
continue;
|
|
175
233
|
}
|
|
176
234
|
repairs = 0;
|
|
177
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
|
+
}
|
|
178
274
|
yield {
|
|
179
275
|
type: "final",
|
|
180
276
|
message: parsedResponse.message,
|
|
@@ -188,7 +284,13 @@ export class AgentRunner {
|
|
|
188
284
|
});
|
|
189
285
|
return;
|
|
190
286
|
}
|
|
191
|
-
|
|
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)) {
|
|
192
294
|
yield {
|
|
193
295
|
type: "final",
|
|
194
296
|
message: parsedResponse.message,
|
|
@@ -202,17 +304,107 @@ export class AgentRunner {
|
|
|
202
304
|
});
|
|
203
305
|
return;
|
|
204
306
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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) => ({
|
|
212
403
|
id: createToolCallId(toolCall.name),
|
|
213
404
|
call: toolCall,
|
|
214
405
|
workState: workStateForTool(toolCall.name)
|
|
215
406
|
}));
|
|
407
|
+
const workspaceCallById = new Map(toolCallRecords.map((record) => [record.id, record.call]));
|
|
216
408
|
for (const record of toolCallRecords) {
|
|
217
409
|
await this.options.sessionStore?.append({
|
|
218
410
|
type: "tool.requested",
|
|
@@ -224,10 +416,25 @@ export class AgentRunner {
|
|
|
224
416
|
createdAt: new Date().toISOString()
|
|
225
417
|
});
|
|
226
418
|
}
|
|
227
|
-
const toolResults =
|
|
228
|
-
|
|
229
|
-
|
|
419
|
+
const toolResults = [
|
|
420
|
+
...todoResults,
|
|
421
|
+
...(await executeToolCallsWithReadParallelism(this.tools, toolCallRecords))
|
|
422
|
+
];
|
|
230
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
|
+
}
|
|
231
438
|
if (toolResult.approval) {
|
|
232
439
|
yield {
|
|
233
440
|
type: "approval",
|
|
@@ -253,6 +460,7 @@ export class AgentRunner {
|
|
|
253
460
|
toolCallId: toolResult.toolCallId,
|
|
254
461
|
category: toolResult.category,
|
|
255
462
|
preview: toolResult.preview,
|
|
463
|
+
content: toolResult.ok ? undefined : toolResult.content,
|
|
256
464
|
metadata: toolResult.metadata
|
|
257
465
|
};
|
|
258
466
|
await this.options.sessionStore?.append({
|
|
@@ -278,9 +486,24 @@ export class AgentRunner {
|
|
|
278
486
|
role: "user",
|
|
279
487
|
content: formatToolResultsForPrompt(toolResults)
|
|
280
488
|
});
|
|
489
|
+
compactTranscript(messages);
|
|
281
490
|
stepIndex += 1;
|
|
282
|
-
if (this.options.
|
|
283
|
-
|
|
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);
|
|
284
507
|
if (nextMaxSteps > maxSteps) {
|
|
285
508
|
maxSteps = nextMaxSteps;
|
|
286
509
|
yield {
|
|
@@ -303,6 +526,73 @@ export class AgentRunner {
|
|
|
303
526
|
failedAt: new Date().toISOString()
|
|
304
527
|
});
|
|
305
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 : "";
|
|
306
596
|
}
|
|
307
597
|
function shouldUseSubagents(task) {
|
|
308
598
|
const normalizedTask = task.toLowerCase();
|
|
@@ -311,6 +601,37 @@ function shouldUseSubagents(task) {
|
|
|
311
601
|
}
|
|
312
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);
|
|
313
603
|
}
|
|
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
|
+
}
|
|
314
635
|
function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, resumeContext, permissions, experimental) {
|
|
315
636
|
const workspaceLabel = path.basename(workspaceRoot) || "workspace";
|
|
316
637
|
const writePolicy = permissions.allowWrite
|
|
@@ -348,6 +669,17 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, res
|
|
|
348
669
|
"Never pass placeholder examples like relative/path, path/to/file, or <path> as tool arguments.",
|
|
349
670
|
"For repository summaries, inspect README.md, package.json, tests, docs, and top-level source files before answering.",
|
|
350
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
|
+
: "",
|
|
351
683
|
"When diagnosing a failure, form a concrete hypothesis, gather targeted evidence with tools, then fix the smallest cause.",
|
|
352
684
|
experimental.allowExternalFileAnalysis
|
|
353
685
|
? "Experimental file analysis is enabled: inspect_document may inspect supported absolute paths outside the workspace when the user provides them."
|
|
@@ -355,6 +687,9 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, res
|
|
|
355
687
|
experimental.memoryEnabled
|
|
356
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."
|
|
357
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.",
|
|
358
693
|
workspaceSummary ? ["", "Workspace context:", workspaceSummary].join("\n") : "",
|
|
359
694
|
resumeContext
|
|
360
695
|
? [
|
|
@@ -385,12 +720,14 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, res
|
|
|
385
720
|
"{\"action\":\"final\",\"message\":\"short useful answer\"}",
|
|
386
721
|
"",
|
|
387
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.",
|
|
388
724
|
"- list_files: {\"path\":\".\"}",
|
|
725
|
+
"- find_files: {\"query\":\"agent\",\"limit\":80} for matching workspace file paths without reading file contents",
|
|
389
726
|
"- read_file: {\"path\":\"src/index.ts\"}",
|
|
390
727
|
"- read_range: {\"path\":\"src/index.ts\",\"start\":1,\"end\":80}",
|
|
391
728
|
"- file_info: {\"path\":\"src/index.ts\"}",
|
|
392
729
|
"- search_text: {\"query\":\"functionName\"}",
|
|
393
|
-
"- 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.",
|
|
394
731
|
...(experimental.memoryEnabled
|
|
395
732
|
? [
|
|
396
733
|
"- memory_search: {\"query\":\"provider setup\",\"limit\":5}",
|
|
@@ -399,9 +736,17 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, res
|
|
|
399
736
|
: []),
|
|
400
737
|
"- git_status: {} for current branch and dirty files",
|
|
401
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",
|
|
402
741
|
"- list_changed_files: {}",
|
|
403
742
|
"- list_scripts: {} for package manager scripts from package.json",
|
|
404
|
-
"-
|
|
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\"}",
|
|
405
750
|
"- apply_patch: {\"patch\":\"unified git patch\"}",
|
|
406
751
|
"- run_script: {\"script\":\"test\"}",
|
|
407
752
|
"- run_tests: {}",
|
|
@@ -409,9 +754,9 @@ function buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, res
|
|
|
409
754
|
"",
|
|
410
755
|
"Act like a coding agent. For simple create/edit/run requests, use tools directly instead of over-warning.",
|
|
411
756
|
"Do not call search_text with an empty query. Use list_files {\"path\":\".\"} to inspect a directory.",
|
|
412
|
-
"Prefer reading before risky edits
|
|
413
|
-
"Batch
|
|
414
|
-
"
|
|
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.",
|
|
415
760
|
"In final answers, separate verified facts from remaining risks.",
|
|
416
761
|
"Keep tool requests and final answers compact."
|
|
417
762
|
].join("\n");
|
|
@@ -421,8 +766,36 @@ function looksLikeClarification(message) {
|
|
|
421
766
|
return (normalizedMessage.endsWith("?") &&
|
|
422
767
|
/\b(what|which|please provide|would you like|do you want|can you specify|welche|was genau|bitte)\b/.test(normalizedMessage));
|
|
423
768
|
}
|
|
424
|
-
function
|
|
425
|
-
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);
|
|
426
799
|
}
|
|
427
800
|
function normalizeToolCall(toolCall) {
|
|
428
801
|
if (toolCall.name === "search_text") {
|
|
@@ -438,13 +811,108 @@ function normalizeToolCall(toolCall) {
|
|
|
438
811
|
}
|
|
439
812
|
return toolCall;
|
|
440
813
|
}
|
|
441
|
-
|
|
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) {
|
|
442
886
|
const results = [];
|
|
443
|
-
for (
|
|
444
|
-
|
|
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)))));
|
|
445
900
|
}
|
|
446
901
|
return results;
|
|
447
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
|
+
}
|
|
448
916
|
async function executeToolSafely(tools, toolCall, toolCallId) {
|
|
449
917
|
const toolResult = await tools.execute(toolCall).catch((error) => ({
|
|
450
918
|
ok: false,
|
|
@@ -468,13 +936,58 @@ async function executeToolSafely(tools, toolCall, toolCallId) {
|
|
|
468
936
|
}
|
|
469
937
|
function formatToolResultsForPrompt(toolResults) {
|
|
470
938
|
return [
|
|
471
|
-
"Tool results
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
}
|
|
478
991
|
}
|
|
479
992
|
function workStateForTool(tool) {
|
|
480
993
|
const category = getToolSpec(tool).category;
|
|
@@ -496,17 +1009,23 @@ function createToolCallId(tool) {
|
|
|
496
1009
|
return `${tool}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
497
1010
|
}
|
|
498
1011
|
async function buildWorkspaceSummary(workspaceRoot) {
|
|
499
|
-
const [patchPilotInstructions, packageJson, tsconfig, readme] = await Promise.all([
|
|
1012
|
+
const [patchPilotInstructions, packageJson, tsconfig, readme, productContext, architecture, commands] = await Promise.all([
|
|
500
1013
|
readWorkspaceFile(workspaceRoot, "PATCHPILOT.md", 4000),
|
|
501
1014
|
readWorkspaceFile(workspaceRoot, "package.json", 4000),
|
|
502
1015
|
readWorkspaceFile(workspaceRoot, "tsconfig.json", 1600),
|
|
503
|
-
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)
|
|
504
1020
|
]);
|
|
505
1021
|
return [
|
|
506
1022
|
patchPilotInstructions ? `PATCHPILOT.md instructions:\n${patchPilotInstructions}` : "",
|
|
1023
|
+
productContext ? `PatchPilot product context:\n${productContext}` : "",
|
|
507
1024
|
packageJson ? `package.json:\n${packageJson}` : "",
|
|
508
1025
|
tsconfig ? `tsconfig.json:\n${tsconfig}` : "",
|
|
509
|
-
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}` : ""
|
|
510
1029
|
]
|
|
511
1030
|
.filter(Boolean)
|
|
512
1031
|
.join("\n\n");
|
|
@@ -521,22 +1040,34 @@ async function readWorkspaceFile(workspaceRoot, relativePath, maxLength) {
|
|
|
521
1040
|
const content = await readFile(normalizedFile, "utf8").catch(() => "");
|
|
522
1041
|
return clipPromptValue(content.trim(), maxLength);
|
|
523
1042
|
}
|
|
524
|
-
function resolveMaxSteps(task, configuredMaxSteps, thinkingMode) {
|
|
1043
|
+
function resolveMaxSteps(task, configuredMaxSteps, thinkingMode, ultramaxx = false) {
|
|
525
1044
|
if (thinkingMode !== "adaptive") {
|
|
526
|
-
return configuredMaxSteps;
|
|
1045
|
+
return Math.max(2, configuredMaxSteps);
|
|
527
1046
|
}
|
|
528
1047
|
const words = task.trim().split(/\s+/).filter(Boolean).length;
|
|
529
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
|
+
}
|
|
530
1052
|
const adaptiveSteps = looksComplex ? Math.max(configuredMaxSteps, 12) : Math.min(configuredMaxSteps, 5);
|
|
531
1053
|
return Math.max(3, Math.min(20, adaptiveSteps));
|
|
532
1054
|
}
|
|
533
|
-
function shouldExtendAdaptiveRun(task, toolResults, currentMaxSteps) {
|
|
534
|
-
if (currentMaxSteps >=
|
|
1055
|
+
function shouldExtendAdaptiveRun(task, toolResults, currentMaxSteps, ceiling = 32) {
|
|
1056
|
+
if (currentMaxSteps >= ceiling) {
|
|
535
1057
|
return false;
|
|
536
1058
|
}
|
|
537
|
-
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))));
|
|
538
1065
|
const hasRecoverableFailure = toolResults.some((result) => !result.ok && /not found|missing|requires|denied|failed|unreadable/i.test(result.summary));
|
|
539
|
-
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");
|
|
540
1071
|
}
|
|
541
1072
|
function resolveReasoningEffort(task, effort) {
|
|
542
1073
|
if (effort !== "adaptive") {
|
|
@@ -557,4 +1088,20 @@ function clipPromptValue(value, maxLength) {
|
|
|
557
1088
|
}
|
|
558
1089
|
return `${value.slice(0, maxLength)}\n...[clipped ${value.length - maxLength} chars]`;
|
|
559
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
|
+
}
|
|
560
1107
|
//# sourceMappingURL=agent.js.map
|