@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.
Files changed (180) hide show
  1. package/.env.example +17 -1
  2. package/README.md +113 -23
  3. package/SECURITY.md +7 -1
  4. package/dist/cli.js +103 -14
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/agent.d.ts +47 -1
  7. package/dist/core/agent.js +667 -76
  8. package/dist/core/agent.js.map +1 -1
  9. package/dist/core/cleanup.d.ts +3 -0
  10. package/dist/core/cleanup.js +29 -0
  11. package/dist/core/cleanup.js.map +1 -0
  12. package/dist/core/clipboard.d.ts +14 -0
  13. package/dist/core/clipboard.js +134 -0
  14. package/dist/core/clipboard.js.map +1 -0
  15. package/dist/core/codex.d.ts +8 -0
  16. package/dist/core/codex.js +28 -2
  17. package/dist/core/codex.js.map +1 -1
  18. package/dist/core/compaction.d.ts +23 -0
  19. package/dist/core/compaction.js +145 -0
  20. package/dist/core/compaction.js.map +1 -0
  21. package/dist/core/contextFormat.d.ts +21 -0
  22. package/dist/core/contextFormat.js +87 -0
  23. package/dist/core/contextFormat.js.map +1 -0
  24. package/dist/core/contextItem.d.ts +41 -0
  25. package/dist/core/contextItem.js +93 -0
  26. package/dist/core/contextItem.js.map +1 -0
  27. package/dist/core/contextStore.d.ts +48 -0
  28. package/dist/core/contextStore.js +306 -0
  29. package/dist/core/contextStore.js.map +1 -0
  30. package/dist/core/doctor.d.ts +4 -1
  31. package/dist/core/doctor.js +122 -3
  32. package/dist/core/doctor.js.map +1 -1
  33. package/dist/core/gemini.js +10 -4
  34. package/dist/core/gemini.js.map +1 -1
  35. package/dist/core/geminiWrapper.d.ts +92 -0
  36. package/dist/core/geminiWrapper.js +1258 -0
  37. package/dist/core/geminiWrapper.js.map +1 -0
  38. package/dist/core/http.js +70 -6
  39. package/dist/core/http.js.map +1 -1
  40. package/dist/core/json.d.ts +1 -1
  41. package/dist/core/json.js +81 -19
  42. package/dist/core/json.js.map +1 -1
  43. package/dist/core/memory.d.ts +16 -0
  44. package/dist/core/memory.js +108 -0
  45. package/dist/core/memory.js.map +1 -0
  46. package/dist/core/modelClient.js +7 -0
  47. package/dist/core/modelClient.js.map +1 -1
  48. package/dist/core/nvidia.d.ts +1 -1
  49. package/dist/core/nvidia.js +13 -4
  50. package/dist/core/nvidia.js.map +1 -1
  51. package/dist/core/ollama.js +13 -3
  52. package/dist/core/ollama.js.map +1 -1
  53. package/dist/core/openrouter.js +15 -6
  54. package/dist/core/openrouter.js.map +1 -1
  55. package/dist/core/projectInit.d.ts +6 -0
  56. package/dist/core/projectInit.js +44 -0
  57. package/dist/core/projectInit.js.map +1 -0
  58. package/dist/core/reasoning.js +6 -0
  59. package/dist/core/reasoning.js.map +1 -1
  60. package/dist/core/session.d.ts +1 -0
  61. package/dist/core/session.js +55 -3
  62. package/dist/core/session.js.map +1 -1
  63. package/dist/core/tokenAccounting.d.ts +4 -0
  64. package/dist/core/tokenAccounting.js +75 -13
  65. package/dist/core/tokenAccounting.js.map +1 -1
  66. package/dist/core/types.d.ts +65 -5
  67. package/dist/core/types.js +30 -1
  68. package/dist/core/types.js.map +1 -1
  69. package/dist/core/updateCheck.d.ts +19 -0
  70. package/dist/core/updateCheck.js +103 -0
  71. package/dist/core/updateCheck.js.map +1 -0
  72. package/dist/core/workspace.d.ts +37 -0
  73. package/dist/core/workspace.js +1535 -84
  74. package/dist/core/workspace.js.map +1 -1
  75. package/dist/tui/App.d.ts +1 -0
  76. package/dist/tui/App.js +1841 -140
  77. package/dist/tui/App.js.map +1 -1
  78. package/dist/tui/commands.js +141 -9
  79. package/dist/tui/commands.js.map +1 -1
  80. package/dist/tui/components/ApprovalPanel.js +16 -1
  81. package/dist/tui/components/ApprovalPanel.js.map +1 -1
  82. package/dist/tui/components/CommandSuggestions.js +33 -5
  83. package/dist/tui/components/CommandSuggestions.js.map +1 -1
  84. package/dist/tui/components/Composer.d.ts +3 -0
  85. package/dist/tui/components/Composer.js +57 -5
  86. package/dist/tui/components/Composer.js.map +1 -1
  87. package/dist/tui/components/ExperimentalPanel.d.ts +10 -0
  88. package/dist/tui/components/ExperimentalPanel.js +38 -0
  89. package/dist/tui/components/ExperimentalPanel.js.map +1 -0
  90. package/dist/tui/components/Header.js +3 -3
  91. package/dist/tui/components/Header.js.map +1 -1
  92. package/dist/tui/components/OnboardingPanel.d.ts +25 -1
  93. package/dist/tui/components/OnboardingPanel.js +87 -25
  94. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  95. package/dist/tui/components/Sidebar.js +17 -13
  96. package/dist/tui/components/Sidebar.js.map +1 -1
  97. package/dist/tui/components/StartupBanner.d.ts +4 -0
  98. package/dist/tui/components/StartupBanner.js +9 -0
  99. package/dist/tui/components/StartupBanner.js.map +1 -0
  100. package/dist/tui/components/Transcript.d.ts +7 -0
  101. package/dist/tui/components/Transcript.js +87 -17
  102. package/dist/tui/components/Transcript.js.map +1 -1
  103. package/dist/tui/contextCommands.d.ts +8 -0
  104. package/dist/tui/contextCommands.js +205 -0
  105. package/dist/tui/contextCommands.js.map +1 -0
  106. package/dist/tui/experimental/AnimatedText.d.ts +38 -0
  107. package/dist/tui/experimental/AnimatedText.js +55 -0
  108. package/dist/tui/experimental/AnimatedText.js.map +1 -0
  109. package/dist/tui/experimental/Banner.d.ts +10 -0
  110. package/dist/tui/experimental/Banner.js +33 -0
  111. package/dist/tui/experimental/Banner.js.map +1 -0
  112. package/dist/tui/experimental/CommandPalette.d.ts +11 -0
  113. package/dist/tui/experimental/CommandPalette.js +25 -0
  114. package/dist/tui/experimental/CommandPalette.js.map +1 -0
  115. package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
  116. package/dist/tui/experimental/ExperimentalShell.js +366 -0
  117. package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
  118. package/dist/tui/experimental/ThemePicker.d.ts +13 -0
  119. package/dist/tui/experimental/ThemePicker.js +12 -0
  120. package/dist/tui/experimental/ThemePicker.js.map +1 -0
  121. package/dist/tui/experimental/attachments.d.ts +35 -0
  122. package/dist/tui/experimental/attachments.js +244 -0
  123. package/dist/tui/experimental/attachments.js.map +1 -0
  124. package/dist/tui/experimental/composer.d.ts +24 -0
  125. package/dist/tui/experimental/composer.js +84 -0
  126. package/dist/tui/experimental/composer.js.map +1 -0
  127. package/dist/tui/experimental/geminiPricing.d.ts +16 -0
  128. package/dist/tui/experimental/geminiPricing.js +39 -0
  129. package/dist/tui/experimental/geminiPricing.js.map +1 -0
  130. package/dist/tui/experimental/layout.d.ts +46 -0
  131. package/dist/tui/experimental/layout.js +112 -0
  132. package/dist/tui/experimental/layout.js.map +1 -0
  133. package/dist/tui/experimental/theme.d.ts +35 -0
  134. package/dist/tui/experimental/theme.js +86 -0
  135. package/dist/tui/experimental/theme.js.map +1 -0
  136. package/dist/tui/experimental/transcriptRows.d.ts +20 -0
  137. package/dist/tui/experimental/transcriptRows.js +169 -0
  138. package/dist/tui/experimental/transcriptRows.js.map +1 -0
  139. package/dist/tui/experimental/ultraModes.d.ts +46 -0
  140. package/dist/tui/experimental/ultraModes.js +95 -0
  141. package/dist/tui/experimental/ultraModes.js.map +1 -0
  142. package/dist/tui/experimental/ultramaxx.d.ts +19 -0
  143. package/dist/tui/experimental/ultramaxx.js +43 -0
  144. package/dist/tui/experimental/ultramaxx.js.map +1 -0
  145. package/dist/tui/format.d.ts +4 -2
  146. package/dist/tui/format.js +21 -7
  147. package/dist/tui/format.js.map +1 -1
  148. package/dist/tui/hosts.js +7 -1
  149. package/dist/tui/hosts.js.map +1 -1
  150. package/dist/tui/layout.d.ts +26 -0
  151. package/dist/tui/layout.js +66 -0
  152. package/dist/tui/layout.js.map +1 -0
  153. package/dist/tui/modelSelection.d.ts +1 -1
  154. package/dist/tui/modelSelection.js +8 -6
  155. package/dist/tui/modelSelection.js.map +1 -1
  156. package/dist/tui/modes.d.ts +8 -1
  157. package/dist/tui/modes.js +20 -2
  158. package/dist/tui/modes.js.map +1 -1
  159. package/dist/tui/onboardingPreferences.d.ts +37 -0
  160. package/dist/tui/onboardingPreferences.js +118 -0
  161. package/dist/tui/onboardingPreferences.js.map +1 -0
  162. package/dist/tui/runStatus.d.ts +50 -0
  163. package/dist/tui/runStatus.js +164 -0
  164. package/dist/tui/runStatus.js.map +1 -0
  165. package/dist/tui/types.d.ts +8 -0
  166. package/dist/tui/types.js.map +1 -1
  167. package/docs/architecture.md +115 -0
  168. package/docs/gemini-wrapper.md +110 -0
  169. package/docs/product-context.md +43 -0
  170. package/docs/releases/v0.1.1-beta.md +18 -0
  171. package/docs/releases/v0.2.1.md +1 -1
  172. package/docs/releases/v0.3.1-beta.md +4 -0
  173. package/docs/releases/v0.4.0.md +1 -1
  174. package/docs/releases/v1.0.0.md +28 -0
  175. package/docs/releases/v1.0.1.md +25 -0
  176. package/docs/releases/v1.1.0.md +30 -0
  177. package/docs/releases/v1.2.0.md +28 -0
  178. package/docs/showcase/patchpilot-banner.png +0 -0
  179. package/docs/showcase/patchpilot-logo.png +0 -0
  180. package/package.json +8 -3
@@ -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
- let maxSteps = resolveMaxSteps(task, this.options.maxSteps, this.options.thinkingMode);
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
- const modelResponse = await this.client.chat({
111
- model: this.options.model,
112
- messages,
113
- formatJson: true,
114
- reasoningEffort,
115
- signal: this.options.signal
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
- repairs += 1;
129
- yield {
130
- type: "status",
131
- message: `repairing model protocol: ${formatParseError(error)}`,
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: "final",
145
- message: "The model kept returning invalid tool protocol. Try a stronger coding model or switch advisors off for this task.",
146
- workState: "error"
196
+ type: "status",
197
+ message: `recovered malformed model protocol as ${recoveredResponse.tool_calls[0]?.name ?? "tool"} for ${recoveredPath}`,
198
+ workState: "planning"
147
199
  };
148
- await this.options.sessionStore?.append({
149
- type: "run.failed",
150
- runId,
151
- message: "The model kept returning invalid tool protocol.",
152
- failedAt: new Date().toISOString()
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
- return;
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
- if (looksLikeClarification(parsedResponse.message)) {
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
- yield {
188
- type: "assistant",
189
- message: parsedResponse.message,
190
- workState: "planning"
191
- };
192
- const toolCalls = parsedResponse.tool_calls.map(normalizeToolCall);
193
- const toolCallRecords = toolCalls.map((toolCall) => ({
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 = toolCalls.every(isReadOnlyToolCall)
210
- ? await Promise.all(toolCallRecords.map((record) => executeToolSafely(this.tools, record.call, record.id)))
211
- : await executeToolCallsSequentially(this.tools, toolCallRecords);
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.thinkingMode === "adaptive" && stepIndex >= maxSteps && shouldExtendAdaptiveRun(task, toolResults, maxSteps)) {
265
- const nextMaxSteps = Math.min(32, maxSteps + 4);
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 buildSystemPrompt(workspaceRoot, subagentContext, workspaceSummary, permissions) {
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
- ? "Build+bypass mode may run write, script, test, or shell tools without per-tool approval. Keep actions narrow and avoid broad destructive commands."
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
- "- write_file: {\"path\":\"test2/test.txt\",\"content\":\"full file content\"}",
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; for explicit simple writes, write the requested file.",
371
- "Batch independent read-only tool calls in one response when it helps avoid extra thinking steps.",
372
- "Prefer parallel read-only context gathering over one file per step.",
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 isReadOnlyToolCall(toolCall) {
383
- return getToolSpec(toolCall.name)?.sideEffects === "none";
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
- async function executeToolCallsSequentially(tools, toolCalls) {
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 (const toolCall of toolCalls) {
402
- results.push(await executeToolSafely(tools, toolCall.call, toolCall.id));
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
- ...toolResults.map((toolResult, index) => [
431
- `${index + 1}. ${toolResult.tool} (${toolResult.ok ? "ok" : "error"})`,
432
- `summary: ${toolResult.summary}`,
433
- `content: ${clipPromptValue(toolResult.content, toolResult.tool === "read_file" ? 12_000 : 6000)}`
434
- ].join("\n"))
435
- ].join("\n\n");
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 >= 32) {
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