@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.
Files changed (154) hide show
  1. package/README.md +51 -16
  2. package/dist/cli.js +46 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/agent.d.ts +44 -1
  5. package/dist/core/agent.js +617 -70
  6. package/dist/core/agent.js.map +1 -1
  7. package/dist/core/clipboard.d.ts +14 -0
  8. package/dist/core/clipboard.js +134 -0
  9. package/dist/core/clipboard.js.map +1 -0
  10. package/dist/core/codex.d.ts +8 -0
  11. package/dist/core/codex.js +28 -2
  12. package/dist/core/codex.js.map +1 -1
  13. package/dist/core/compaction.d.ts +23 -0
  14. package/dist/core/compaction.js +145 -0
  15. package/dist/core/compaction.js.map +1 -0
  16. package/dist/core/contextFormat.d.ts +21 -0
  17. package/dist/core/contextFormat.js +87 -0
  18. package/dist/core/contextFormat.js.map +1 -0
  19. package/dist/core/contextItem.d.ts +41 -0
  20. package/dist/core/contextItem.js +93 -0
  21. package/dist/core/contextItem.js.map +1 -0
  22. package/dist/core/contextStore.d.ts +48 -0
  23. package/dist/core/contextStore.js +306 -0
  24. package/dist/core/contextStore.js.map +1 -0
  25. package/dist/core/doctor.js +9 -8
  26. package/dist/core/doctor.js.map +1 -1
  27. package/dist/core/gemini.js +10 -4
  28. package/dist/core/gemini.js.map +1 -1
  29. package/dist/core/geminiWrapper.d.ts +43 -2
  30. package/dist/core/geminiWrapper.js +582 -42
  31. package/dist/core/geminiWrapper.js.map +1 -1
  32. package/dist/core/http.js +70 -6
  33. package/dist/core/http.js.map +1 -1
  34. package/dist/core/json.d.ts +1 -1
  35. package/dist/core/json.js +18 -20
  36. package/dist/core/json.js.map +1 -1
  37. package/dist/core/nvidia.d.ts +1 -1
  38. package/dist/core/nvidia.js +13 -4
  39. package/dist/core/nvidia.js.map +1 -1
  40. package/dist/core/ollama.js +13 -3
  41. package/dist/core/ollama.js.map +1 -1
  42. package/dist/core/openrouter.js +15 -6
  43. package/dist/core/openrouter.js.map +1 -1
  44. package/dist/core/reasoning.js +3 -0
  45. package/dist/core/reasoning.js.map +1 -1
  46. package/dist/core/session.js +9 -3
  47. package/dist/core/session.js.map +1 -1
  48. package/dist/core/tokenAccounting.d.ts +4 -0
  49. package/dist/core/tokenAccounting.js +75 -13
  50. package/dist/core/tokenAccounting.js.map +1 -1
  51. package/dist/core/types.d.ts +58 -3
  52. package/dist/core/types.js +30 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/core/updateCheck.d.ts +19 -0
  55. package/dist/core/updateCheck.js +103 -0
  56. package/dist/core/updateCheck.js.map +1 -0
  57. package/dist/core/workspace.d.ts +29 -0
  58. package/dist/core/workspace.js +1271 -92
  59. package/dist/core/workspace.js.map +1 -1
  60. package/dist/tui/App.d.ts +1 -0
  61. package/dist/tui/App.js +1346 -112
  62. package/dist/tui/App.js.map +1 -1
  63. package/dist/tui/commands.js +109 -6
  64. package/dist/tui/commands.js.map +1 -1
  65. package/dist/tui/components/ApprovalPanel.js +16 -1
  66. package/dist/tui/components/ApprovalPanel.js.map +1 -1
  67. package/dist/tui/components/CommandSuggestions.js +26 -3
  68. package/dist/tui/components/CommandSuggestions.js.map +1 -1
  69. package/dist/tui/components/Composer.d.ts +3 -0
  70. package/dist/tui/components/Composer.js +57 -5
  71. package/dist/tui/components/Composer.js.map +1 -1
  72. package/dist/tui/components/ExperimentalPanel.d.ts +1 -1
  73. package/dist/tui/components/ExperimentalPanel.js +5 -0
  74. package/dist/tui/components/ExperimentalPanel.js.map +1 -1
  75. package/dist/tui/components/OnboardingPanel.d.ts +12 -0
  76. package/dist/tui/components/OnboardingPanel.js +69 -21
  77. package/dist/tui/components/OnboardingPanel.js.map +1 -1
  78. package/dist/tui/components/StartupBanner.d.ts +4 -0
  79. package/dist/tui/components/StartupBanner.js +9 -0
  80. package/dist/tui/components/StartupBanner.js.map +1 -0
  81. package/dist/tui/components/Transcript.d.ts +7 -0
  82. package/dist/tui/components/Transcript.js +86 -16
  83. package/dist/tui/components/Transcript.js.map +1 -1
  84. package/dist/tui/contextCommands.d.ts +8 -0
  85. package/dist/tui/contextCommands.js +205 -0
  86. package/dist/tui/contextCommands.js.map +1 -0
  87. package/dist/tui/experimental/AnimatedText.d.ts +38 -0
  88. package/dist/tui/experimental/AnimatedText.js +55 -0
  89. package/dist/tui/experimental/AnimatedText.js.map +1 -0
  90. package/dist/tui/experimental/Banner.d.ts +10 -0
  91. package/dist/tui/experimental/Banner.js +33 -0
  92. package/dist/tui/experimental/Banner.js.map +1 -0
  93. package/dist/tui/experimental/CommandPalette.d.ts +11 -0
  94. package/dist/tui/experimental/CommandPalette.js +25 -0
  95. package/dist/tui/experimental/CommandPalette.js.map +1 -0
  96. package/dist/tui/experimental/ExperimentalShell.d.ts +58 -0
  97. package/dist/tui/experimental/ExperimentalShell.js +366 -0
  98. package/dist/tui/experimental/ExperimentalShell.js.map +1 -0
  99. package/dist/tui/experimental/ThemePicker.d.ts +13 -0
  100. package/dist/tui/experimental/ThemePicker.js +12 -0
  101. package/dist/tui/experimental/ThemePicker.js.map +1 -0
  102. package/dist/tui/experimental/attachments.d.ts +35 -0
  103. package/dist/tui/experimental/attachments.js +244 -0
  104. package/dist/tui/experimental/attachments.js.map +1 -0
  105. package/dist/tui/experimental/composer.d.ts +24 -0
  106. package/dist/tui/experimental/composer.js +84 -0
  107. package/dist/tui/experimental/composer.js.map +1 -0
  108. package/dist/tui/experimental/geminiPricing.d.ts +16 -0
  109. package/dist/tui/experimental/geminiPricing.js +39 -0
  110. package/dist/tui/experimental/geminiPricing.js.map +1 -0
  111. package/dist/tui/experimental/layout.d.ts +46 -0
  112. package/dist/tui/experimental/layout.js +112 -0
  113. package/dist/tui/experimental/layout.js.map +1 -0
  114. package/dist/tui/experimental/theme.d.ts +35 -0
  115. package/dist/tui/experimental/theme.js +86 -0
  116. package/dist/tui/experimental/theme.js.map +1 -0
  117. package/dist/tui/experimental/transcriptRows.d.ts +20 -0
  118. package/dist/tui/experimental/transcriptRows.js +169 -0
  119. package/dist/tui/experimental/transcriptRows.js.map +1 -0
  120. package/dist/tui/experimental/ultraModes.d.ts +46 -0
  121. package/dist/tui/experimental/ultraModes.js +95 -0
  122. package/dist/tui/experimental/ultraModes.js.map +1 -0
  123. package/dist/tui/experimental/ultramaxx.d.ts +19 -0
  124. package/dist/tui/experimental/ultramaxx.js +43 -0
  125. package/dist/tui/experimental/ultramaxx.js.map +1 -0
  126. package/dist/tui/format.d.ts +4 -2
  127. package/dist/tui/format.js +14 -0
  128. package/dist/tui/format.js.map +1 -1
  129. package/dist/tui/hosts.js +7 -1
  130. package/dist/tui/hosts.js.map +1 -1
  131. package/dist/tui/layout.d.ts +26 -0
  132. package/dist/tui/layout.js +66 -0
  133. package/dist/tui/layout.js.map +1 -0
  134. package/dist/tui/modelSelection.d.ts +1 -1
  135. package/dist/tui/modelSelection.js +8 -6
  136. package/dist/tui/modelSelection.js.map +1 -1
  137. package/dist/tui/modes.d.ts +7 -0
  138. package/dist/tui/modes.js +12 -0
  139. package/dist/tui/modes.js.map +1 -1
  140. package/dist/tui/onboardingPreferences.d.ts +37 -0
  141. package/dist/tui/onboardingPreferences.js +118 -0
  142. package/dist/tui/onboardingPreferences.js.map +1 -0
  143. package/dist/tui/runStatus.d.ts +50 -0
  144. package/dist/tui/runStatus.js +164 -0
  145. package/dist/tui/runStatus.js.map +1 -0
  146. package/dist/tui/types.d.ts +8 -0
  147. package/dist/tui/types.js.map +1 -1
  148. package/docs/architecture.md +115 -0
  149. package/docs/gemini-wrapper.md +23 -0
  150. package/docs/product-context.md +43 -0
  151. package/docs/releases/v1.0.1.md +25 -0
  152. package/docs/releases/v1.1.0.md +30 -0
  153. package/docs/releases/v1.2.0.md +28 -0
  154. package/package.json +4 -2
@@ -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
- 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);
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
- modelResponse = await this.client.chat({
154
+ const chatAttempts = this.chatWithRetry({
118
155
  model: this.options.model,
119
156
  messages,
120
- formatJson: true,
121
157
  reasoningEffort,
122
- signal: this.options.signal
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
- repairs += 1;
147
- yield {
148
- type: "status",
149
- message: `repairing model protocol: ${formatParseError(error)}`,
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: "final",
163
- message: "The model kept returning invalid tool protocol. Try a stronger coding model or switch advisors off for this task.",
164
- workState: "error"
196
+ type: "status",
197
+ message: `recovered malformed model protocol as ${recoveredResponse.tool_calls[0]?.name ?? "tool"} for ${recoveredPath}`,
198
+ workState: "planning"
165
199
  };
166
- await this.options.sessionStore?.append({
167
- type: "run.failed",
168
- runId,
169
- message: "The model kept returning invalid tool protocol.",
170
- 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)
171
212
  });
172
- 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;
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
- 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)) {
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
- yield {
206
- type: "assistant",
207
- message: parsedResponse.message,
208
- workState: "planning"
209
- };
210
- const toolCalls = parsedResponse.tool_calls.map(normalizeToolCall);
211
- 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) => ({
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 = toolCalls.every(isReadOnlyToolCall)
228
- ? await Promise.all(toolCallRecords.map((record) => executeToolSafely(this.tools, record.call, record.id)))
229
- : await executeToolCallsSequentially(this.tools, toolCallRecords);
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.thinkingMode === "adaptive" && stepIndex >= maxSteps && shouldExtendAdaptiveRun(task, toolResults, maxSteps)) {
283
- 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);
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
- "- 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\"}",
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; for explicit simple writes, write the requested file.",
413
- "Batch independent read-only tool calls in one response when it helps avoid extra thinking steps.",
414
- "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.",
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 isReadOnlyToolCall(toolCall) {
425
- 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);
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
- 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) {
442
886
  const results = [];
443
- for (const toolCall of toolCalls) {
444
- 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)))));
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
- ...toolResults.map((toolResult, index) => [
473
- `${index + 1}. ${toolResult.tool} (${toolResult.ok ? "ok" : "error"})`,
474
- `summary: ${toolResult.summary}`,
475
- `content: ${clipPromptValue(toolResult.content, toolResult.tool === "read_file" ? 12_000 : 6000)}`
476
- ].join("\n"))
477
- ].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
+ }
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 >= 32) {
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