@calliopelabs/cli 2.2.0 → 2.5.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 (244) hide show
  1. package/README.md +17 -0
  2. package/dist/agents/agent-config-loader.js +1 -1
  3. package/dist/agents/agent-config-presets.js +13 -13
  4. package/dist/agents/agent-config-presets.js.map +1 -1
  5. package/dist/agents/agent-config-types.d.ts +1 -1
  6. package/dist/agents/agent-config-types.d.ts.map +1 -1
  7. package/dist/agents/council-types.d.ts +2 -0
  8. package/dist/agents/council-types.d.ts.map +1 -1
  9. package/dist/agents/council-types.js.map +1 -1
  10. package/dist/agents/council.d.ts +5 -0
  11. package/dist/agents/council.d.ts.map +1 -1
  12. package/dist/agents/council.js +150 -14
  13. package/dist/agents/council.js.map +1 -1
  14. package/dist/agents/dynamic-tools.d.ts.map +1 -1
  15. package/dist/agents/dynamic-tools.js +39 -10
  16. package/dist/agents/dynamic-tools.js.map +1 -1
  17. package/dist/agents/orchestrator.d.ts +7 -0
  18. package/dist/agents/orchestrator.d.ts.map +1 -1
  19. package/dist/agents/orchestrator.js +48 -7
  20. package/dist/agents/orchestrator.js.map +1 -1
  21. package/dist/agents/sdk-backend.js +1 -1
  22. package/dist/agents/sdk-backend.js.map +1 -1
  23. package/dist/agents/swarm-types.d.ts +1 -0
  24. package/dist/agents/swarm-types.d.ts.map +1 -1
  25. package/dist/agents/swarm.d.ts +5 -0
  26. package/dist/agents/swarm.d.ts.map +1 -1
  27. package/dist/agents/swarm.js +85 -17
  28. package/dist/agents/swarm.js.map +1 -1
  29. package/dist/agents/types.d.ts +1 -0
  30. package/dist/agents/types.d.ts.map +1 -1
  31. package/dist/agents/types.js.map +1 -1
  32. package/dist/api-server.d.ts +9 -0
  33. package/dist/api-server.d.ts.map +1 -1
  34. package/dist/api-server.js +75 -4
  35. package/dist/api-server.js.map +1 -1
  36. package/dist/auto-checkpoint.d.ts.map +1 -1
  37. package/dist/auto-checkpoint.js +50 -17
  38. package/dist/auto-checkpoint.js.map +1 -1
  39. package/dist/auto-compressor.d.ts +14 -0
  40. package/dist/auto-compressor.d.ts.map +1 -1
  41. package/dist/auto-compressor.js +67 -10
  42. package/dist/auto-compressor.js.map +1 -1
  43. package/dist/background-jobs.d.ts.map +1 -1
  44. package/dist/background-jobs.js +8 -3
  45. package/dist/background-jobs.js.map +1 -1
  46. package/dist/bin.d.ts +8 -0
  47. package/dist/bin.d.ts.map +1 -1
  48. package/dist/bin.js +63 -4
  49. package/dist/bin.js.map +1 -1
  50. package/dist/branching.d.ts.map +1 -1
  51. package/dist/branching.js +14 -1
  52. package/dist/branching.js.map +1 -1
  53. package/dist/checkpoint.d.ts.map +1 -1
  54. package/dist/checkpoint.js +13 -1
  55. package/dist/checkpoint.js.map +1 -1
  56. package/dist/cli/agent.d.ts.map +1 -1
  57. package/dist/cli/agent.js +130 -62
  58. package/dist/cli/agent.js.map +1 -1
  59. package/dist/cli/commands.d.ts.map +1 -1
  60. package/dist/cli/commands.js +218 -11
  61. package/dist/cli/commands.js.map +1 -1
  62. package/dist/cli/index.d.ts.map +1 -1
  63. package/dist/cli/index.js +55 -6
  64. package/dist/cli/index.js.map +1 -1
  65. package/dist/cli/types.d.ts +3 -0
  66. package/dist/cli/types.d.ts.map +1 -1
  67. package/dist/cli/types.js +2 -1
  68. package/dist/cli/types.js.map +1 -1
  69. package/dist/config.d.ts +1 -0
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +15 -3
  72. package/dist/config.js.map +1 -1
  73. package/dist/diff.d.ts.map +1 -1
  74. package/dist/diff.js +42 -4
  75. package/dist/diff.js.map +1 -1
  76. package/dist/env-expansion.d.ts +15 -0
  77. package/dist/env-expansion.d.ts.map +1 -0
  78. package/dist/env-expansion.js +43 -0
  79. package/dist/env-expansion.js.map +1 -0
  80. package/dist/errors.d.ts.map +1 -1
  81. package/dist/errors.js +30 -3
  82. package/dist/errors.js.map +1 -1
  83. package/dist/headless.d.ts.map +1 -1
  84. package/dist/headless.js +59 -4
  85. package/dist/headless.js.map +1 -1
  86. package/dist/hooks.d.ts +8 -2
  87. package/dist/hooks.d.ts.map +1 -1
  88. package/dist/hooks.js +97 -11
  89. package/dist/hooks.js.map +1 -1
  90. package/dist/idle-eviction.d.ts.map +1 -1
  91. package/dist/idle-eviction.js +8 -1
  92. package/dist/idle-eviction.js.map +1 -1
  93. package/dist/iteration-ledger.d.ts +111 -2
  94. package/dist/iteration-ledger.d.ts.map +1 -1
  95. package/dist/iteration-ledger.js +327 -19
  96. package/dist/iteration-ledger.js.map +1 -1
  97. package/dist/iteration-limit.d.ts +5 -0
  98. package/dist/iteration-limit.d.ts.map +1 -0
  99. package/dist/iteration-limit.js +17 -0
  100. package/dist/iteration-limit.js.map +1 -0
  101. package/dist/markdown.d.ts.map +1 -1
  102. package/dist/markdown.js +32 -10
  103. package/dist/markdown.js.map +1 -1
  104. package/dist/mcp.d.ts +35 -5
  105. package/dist/mcp.d.ts.map +1 -1
  106. package/dist/mcp.js +191 -12
  107. package/dist/mcp.js.map +1 -1
  108. package/dist/memory.d.ts.map +1 -1
  109. package/dist/memory.js +4 -9
  110. package/dist/memory.js.map +1 -1
  111. package/dist/model-detection.d.ts +14 -1
  112. package/dist/model-detection.d.ts.map +1 -1
  113. package/dist/model-detection.js +307 -114
  114. package/dist/model-detection.js.map +1 -1
  115. package/dist/model-router.js +7 -7
  116. package/dist/model-router.js.map +1 -1
  117. package/dist/parallel-tools.d.ts +9 -1
  118. package/dist/parallel-tools.d.ts.map +1 -1
  119. package/dist/parallel-tools.js +6 -5
  120. package/dist/parallel-tools.js.map +1 -1
  121. package/dist/plugins.d.ts +37 -0
  122. package/dist/plugins.d.ts.map +1 -1
  123. package/dist/plugins.js +87 -0
  124. package/dist/plugins.js.map +1 -1
  125. package/dist/prevent-sleep.d.ts +10 -0
  126. package/dist/prevent-sleep.d.ts.map +1 -0
  127. package/dist/prevent-sleep.js +85 -0
  128. package/dist/prevent-sleep.js.map +1 -0
  129. package/dist/providers/anthropic.d.ts.map +1 -1
  130. package/dist/providers/anthropic.js +36 -2
  131. package/dist/providers/anthropic.js.map +1 -1
  132. package/dist/providers/bedrock.d.ts.map +1 -1
  133. package/dist/providers/bedrock.js +81 -17
  134. package/dist/providers/bedrock.js.map +1 -1
  135. package/dist/providers/compat.d.ts.map +1 -1
  136. package/dist/providers/compat.js +21 -6
  137. package/dist/providers/compat.js.map +1 -1
  138. package/dist/providers/index.d.ts.map +1 -1
  139. package/dist/providers/index.js +2 -0
  140. package/dist/providers/index.js.map +1 -1
  141. package/dist/providers/openai-compat-shims.d.ts +31 -0
  142. package/dist/providers/openai-compat-shims.d.ts.map +1 -0
  143. package/dist/providers/openai-compat-shims.js +179 -0
  144. package/dist/providers/openai-compat-shims.js.map +1 -0
  145. package/dist/providers/openai.d.ts.map +1 -1
  146. package/dist/providers/types.d.ts.map +1 -1
  147. package/dist/providers/types.js +19 -10
  148. package/dist/providers/types.js.map +1 -1
  149. package/dist/risk.d.ts.map +1 -1
  150. package/dist/risk.js +15 -5
  151. package/dist/risk.js.map +1 -1
  152. package/dist/sandbox-native.d.ts +1 -0
  153. package/dist/sandbox-native.d.ts.map +1 -1
  154. package/dist/sandbox-native.js +37 -5
  155. package/dist/sandbox-native.js.map +1 -1
  156. package/dist/scope.d.ts +10 -0
  157. package/dist/scope.d.ts.map +1 -1
  158. package/dist/scope.js +75 -15
  159. package/dist/scope.js.map +1 -1
  160. package/dist/scuttlebot/client.d.ts +83 -0
  161. package/dist/scuttlebot/client.d.ts.map +1 -0
  162. package/dist/scuttlebot/client.js +350 -0
  163. package/dist/scuttlebot/client.js.map +1 -0
  164. package/dist/scuttlebot/config.d.ts +28 -0
  165. package/dist/scuttlebot/config.d.ts.map +1 -0
  166. package/dist/scuttlebot/config.js +91 -0
  167. package/dist/scuttlebot/config.js.map +1 -0
  168. package/dist/scuttlebot/http-client.d.ts +63 -0
  169. package/dist/scuttlebot/http-client.d.ts.map +1 -0
  170. package/dist/scuttlebot/http-client.js +124 -0
  171. package/dist/scuttlebot/http-client.js.map +1 -0
  172. package/dist/scuttlebot/index.d.ts +13 -0
  173. package/dist/scuttlebot/index.d.ts.map +1 -0
  174. package/dist/scuttlebot/index.js +10 -0
  175. package/dist/scuttlebot/index.js.map +1 -0
  176. package/dist/scuttlebot/irc-client.d.ts +124 -0
  177. package/dist/scuttlebot/irc-client.d.ts.map +1 -0
  178. package/dist/scuttlebot/irc-client.js +599 -0
  179. package/dist/scuttlebot/irc-client.js.map +1 -0
  180. package/dist/skills.d.ts +19 -0
  181. package/dist/skills.d.ts.map +1 -1
  182. package/dist/skills.js +98 -10
  183. package/dist/skills.js.map +1 -1
  184. package/dist/smart-router.js +4 -4
  185. package/dist/smart-router.js.map +1 -1
  186. package/dist/storage.d.ts +18 -3
  187. package/dist/storage.d.ts.map +1 -1
  188. package/dist/storage.js +182 -14
  189. package/dist/storage.js.map +1 -1
  190. package/dist/tools.d.ts.map +1 -1
  191. package/dist/tools.js +233 -39
  192. package/dist/tools.js.map +1 -1
  193. package/dist/trust.d.ts +16 -3
  194. package/dist/trust.d.ts.map +1 -1
  195. package/dist/trust.js +23 -4
  196. package/dist/trust.js.map +1 -1
  197. package/dist/types.d.ts.map +1 -1
  198. package/dist/types.js +18 -12
  199. package/dist/types.js.map +1 -1
  200. package/dist/ui/agent.d.ts +1 -1
  201. package/dist/ui/agent.d.ts.map +1 -1
  202. package/dist/ui/agent.js +175 -121
  203. package/dist/ui/agent.js.map +1 -1
  204. package/dist/ui/chat-input.d.ts +3 -1
  205. package/dist/ui/chat-input.d.ts.map +1 -1
  206. package/dist/ui/chat-input.js +82 -17
  207. package/dist/ui/chat-input.js.map +1 -1
  208. package/dist/ui/commands.d.ts +4 -0
  209. package/dist/ui/commands.d.ts.map +1 -1
  210. package/dist/ui/commands.js +562 -39
  211. package/dist/ui/commands.js.map +1 -1
  212. package/dist/ui/completions.d.ts.map +1 -1
  213. package/dist/ui/completions.js +2 -0
  214. package/dist/ui/completions.js.map +1 -1
  215. package/dist/ui/index.d.ts.map +1 -1
  216. package/dist/ui/index.js +288 -60
  217. package/dist/ui/index.js.map +1 -1
  218. package/dist/ui/input-utils.d.ts +20 -0
  219. package/dist/ui/input-utils.d.ts.map +1 -0
  220. package/dist/ui/input-utils.js +35 -0
  221. package/dist/ui/input-utils.js.map +1 -0
  222. package/dist/ui/messages.d.ts +6 -2
  223. package/dist/ui/messages.d.ts.map +1 -1
  224. package/dist/ui/messages.js +42 -11
  225. package/dist/ui/messages.js.map +1 -1
  226. package/dist/ui/modals.d.ts +21 -1
  227. package/dist/ui/modals.d.ts.map +1 -1
  228. package/dist/ui/modals.js +67 -5
  229. package/dist/ui/modals.js.map +1 -1
  230. package/dist/ui/status-bar.d.ts +4 -1
  231. package/dist/ui/status-bar.d.ts.map +1 -1
  232. package/dist/ui/status-bar.js +12 -1
  233. package/dist/ui/status-bar.js.map +1 -1
  234. package/dist/ui/types.d.ts +3 -0
  235. package/dist/ui/types.d.ts.map +1 -1
  236. package/package.json +4 -7
  237. package/dist/completion.d.ts +0 -75
  238. package/dist/completion.d.ts.map +0 -1
  239. package/dist/completion.js +0 -234
  240. package/dist/completion.js.map +0 -1
  241. package/dist/keyboard.d.ts +0 -57
  242. package/dist/keyboard.d.ts.map +0 -1
  243. package/dist/keyboard.js +0 -265
  244. package/dist/keyboard.js.map +0 -1
package/dist/ui/agent.js CHANGED
@@ -4,7 +4,6 @@
4
4
  * Core agent execution loop, tool handling, and message validation.
5
5
  * Extracted from TerminalChat using an AgentContext state bag.
6
6
  */
7
- import { spawn } from 'child_process';
8
7
  import * as config from '../config.js';
9
8
  import { chat } from '../providers/index.js';
10
9
  import { estimateContextUsage, needsSummarization } from '../providers/types.js';
@@ -17,13 +16,17 @@ import { getAvailableProviders } from '../providers/index.js';
17
16
  import * as storage from '../storage.js';
18
17
  import * as hooks from '../hooks.js';
19
18
  import * as modelRouter from '../model-router.js';
19
+ import { scuttlebotClient } from '../scuttlebot/index.js';
20
20
  import * as summarization from '../summarization.js';
21
+ import { autoCompress } from '../auto-compressor.js';
21
22
  import { executeParallel, getParallelizationStats } from '../parallel-tools.js';
22
23
  import { setMood } from '../companions.js';
23
24
  import { checkAndWarnContextLimit } from './context.js';
24
25
  import { smartRoute } from '../smart-router.js';
25
26
  import { shouldCheckpoint, createCheckpoint } from '../auto-checkpoint.js';
26
27
  import { recordEvent } from '../terminal-recording.js';
28
+ import { startPreventSleep, stopPreventSleep } from '../prevent-sleep.js';
29
+ import { resolveIterationLimit, formatIterationProgress, isFiniteIterationLimit, } from '../iteration-limit.js';
27
30
  // ============================================================================
28
31
  // Tool Result Truncation
29
32
  // ============================================================================
@@ -44,6 +47,21 @@ function truncateToolResult(content, modelLimit) {
44
47
  const trimmed = content.slice(0, half) + `\n\n... [truncated ${content.length - maxChars} chars] ...\n\n` + content.slice(-half);
45
48
  return trimmed;
46
49
  }
50
+ function summarizeMessageContent(content) {
51
+ if (typeof content === 'string') {
52
+ return content;
53
+ }
54
+ return content
55
+ .map(block => {
56
+ if (block.type === 'text')
57
+ return block.text;
58
+ if (block.type === 'image')
59
+ return '[image]';
60
+ return '[content]';
61
+ })
62
+ .join(' ')
63
+ .trim();
64
+ }
47
65
  // ============================================================================
48
66
  // Validate and Repair Messages
49
67
  // ============================================================================
@@ -121,8 +139,19 @@ export async function runAgentImpl(ctx, content) {
121
139
  ctx.addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
122
140
  }
123
141
  }
124
- const maxIterations = config.get('maxIterations') || Infinity; // 0 = unlimited
142
+ const maxIterations = resolveIterationLimit(config.get('maxIterations'));
125
143
  let completedNaturally = false;
144
+ const hasParentRun = Boolean(ctx.ledger?.getActiveRun('loop') ||
145
+ ctx.ledger?.getActiveRun('workflow') ||
146
+ ctx.ledger?.getActiveRun('swarm') ||
147
+ ctx.ledger?.getActiveRun('council'));
148
+ const runId = ctx.ledger && !hasParentRun
149
+ ? ctx.ledger.startRun('agent', summarizeMessageContent(content), {
150
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : null,
151
+ })
152
+ : undefined;
153
+ let runStatus;
154
+ let runErrorSummary;
126
155
  // Check context limit and warn if approaching capacity
127
156
  // Uses model's actual context length from API when available
128
157
  let currentContextTokens = ctx.estimateContextTokens();
@@ -130,32 +159,14 @@ export async function runAgentImpl(ctx, content) {
130
159
  let contextPercentage = (currentContextTokens / modelLimit) * 100;
131
160
  // Adaptive preserveRecent: small models keep fewer messages to leave room for output
132
161
  const preserveRecent = modelLimit < 8000 ? 2 : modelLimit < 16000 ? 4 : modelLimit < 32000 ? 6 : modelLimit < 64000 ? 10 : 15;
133
- // Auto-compact if we're over 75% capacity to prevent API errors
134
- if (contextPercentage > 75) {
135
- ctx.addMessage('system', `🔄 Context at ${Math.round(contextPercentage)}% - auto-compacting to prevent errors...`);
136
- const result = summarization.summarizeConversation(ctx.llmMessages.current, {
137
- maxTokens: Math.floor(modelLimit * 0.7), // Target 70% of limit after compaction
138
- preserveRecent,
139
- });
140
- if (result.summarizedCount > 0) {
141
- ctx.llmMessages.current = result.messages;
142
- currentContextTokens = ctx.estimateContextTokens();
143
- contextPercentage = (currentContextTokens / modelLimit) * 100;
144
- ctx.setContextTokens(currentContextTokens);
145
- ctx.addMessage('system', `✓ Compacted ${result.summarizedCount} messages. Now at ${Math.round(contextPercentage)}% (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K)`);
146
- }
147
- else {
148
- // If compaction didn't help enough, force-trim old messages
149
- if (contextPercentage > 98) {
150
- const systemMsgs = ctx.llmMessages.current.filter(m => m.role === 'system');
151
- const recentMsgs = ctx.llmMessages.current.filter(m => m.role !== 'system').slice(-5);
152
- ctx.llmMessages.current = [...systemMsgs, ...recentMsgs];
153
- currentContextTokens = ctx.estimateContextTokens();
154
- contextPercentage = (currentContextTokens / modelLimit) * 100;
155
- ctx.setContextTokens(currentContextTokens);
156
- ctx.addMessage('system', `⚠️ Force-trimmed to last 5 messages (${Math.round(contextPercentage)}%). Use /clear for a full reset.`);
157
- }
158
- }
162
+ // Auto-compact using the new auto-compressor
163
+ const autoCompressResult = await autoCompress(ctx.llmMessages.current, modelLimit, ctx.provider, effectiveModel);
164
+ if (autoCompressResult.compressed) {
165
+ ctx.llmMessages.current = autoCompressResult.messages;
166
+ currentContextTokens = ctx.estimateContextTokens();
167
+ contextPercentage = (currentContextTokens / modelLimit) * 100;
168
+ ctx.setContextTokens(currentContextTokens);
169
+ ctx.addMessage('system', `🔄 Auto-compressed ${autoCompressResult.summarizedCount} messages using ${autoCompressResult.method} (${Math.round(autoCompressResult.originalTokens / 1000)}K → ${Math.round(autoCompressResult.compressedTokens / 1000)}K tokens)`);
159
170
  }
160
171
  else if (contextPercentage > 65) {
161
172
  ctx.addMessage('system', `⚠️ Context at ${Math.round(contextPercentage)}% capacity (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K tokens)
@@ -170,31 +181,26 @@ export async function runAgentImpl(ctx, content) {
170
181
  }
171
182
  for (let i = 0; i < maxIterations; i++) {
172
183
  // Start ledger tracking for this iteration
173
- ctx.ledger?.startIteration(i + 1);
184
+ if (ctx.ledger) {
185
+ ctx.ledger.startIteration(ctx.ledger.getNextIterationNumber());
186
+ }
174
187
  // Safety check at start of each iteration - context may have grown from tool results
175
188
  if (i > 0) {
176
189
  const iterContextTokens = ctx.estimateContextTokens();
177
- const iterContextPercentage = (iterContextTokens / modelLimit) * 100;
178
- if (iterContextPercentage > 75) {
179
- ctx.addMessage('system', `🔄 Context grew to ${Math.round(iterContextPercentage)}% - auto-compacting...`);
180
- const result = summarization.summarizeConversation(ctx.llmMessages.current, {
181
- maxTokens: Math.floor(modelLimit * 0.7),
182
- preserveRecent,
183
- });
184
- if (result.summarizedCount > 0) {
185
- ctx.llmMessages.current = result.messages;
186
- ctx.setContextTokens(ctx.estimateContextTokens());
187
- ctx.addMessage('system', `✓ Compacted ${result.summarizedCount} messages during iteration ${i + 1}`);
188
- }
190
+ const iterAutoCompressResult = await autoCompress(ctx.llmMessages.current, modelLimit, ctx.provider, effectiveModel);
191
+ if (iterAutoCompressResult.compressed) {
192
+ ctx.llmMessages.current = iterAutoCompressResult.messages;
193
+ ctx.setContextTokens(ctx.estimateContextTokens());
194
+ ctx.addMessage('system', `🔄 Auto-compressed ${iterAutoCompressResult.summarizedCount} messages during iteration ${i + 1} (${iterAutoCompressResult.method})`);
189
195
  }
190
196
  }
191
197
  try {
192
198
  // Update thinking state for LLM call
193
199
  ctx.setThinkingState({
194
200
  status: i === 0 ? 'Analyzing request...' : 'Processing response...',
195
- detail: `Iteration ${i + 1}/${maxIterations}`,
201
+ detail: `Iteration ${formatIterationProgress(i + 1, maxIterations)}`,
196
202
  iteration: i + 1,
197
- maxIterations,
203
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : undefined,
198
204
  });
199
205
  ctx.setActivityState({
200
206
  action: i === 0 ? 'Analyzing request' : 'Processing',
@@ -212,7 +218,7 @@ export async function runAgentImpl(ctx, content) {
212
218
  status: `Retrying... (attempt ${attempt + 1})`,
213
219
  detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
214
220
  iteration: i + 1,
215
- maxIterations,
221
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : undefined,
216
222
  });
217
223
  };
218
224
  ctx.debugLog('chat', 'WAITING for LLM response', `iteration=${i + 1}`);
@@ -276,6 +282,8 @@ export async function runAgentImpl(ctx, content) {
276
282
  ctx.setBreakerHealth?.(ctx.circuitBreaker.getHealth());
277
283
  if (breakerResult.tripped) {
278
284
  ctx.addMessage('system', `\u26a0\ufe0f Circuit breaker tripped: ${breakerResult.breaker}\n${breakerResult.message}\n\nUse /breaker resume to continue, /breaker status for details.`);
285
+ runStatus = 'stopped';
286
+ runErrorSummary = breakerResult.message;
279
287
  completedNaturally = true;
280
288
  break;
281
289
  }
@@ -287,6 +295,10 @@ export async function runAgentImpl(ctx, content) {
287
295
  content: response.content,
288
296
  toolCalls: response.toolCalls,
289
297
  });
298
+ // Mirror any text the assistant produced alongside the tool calls
299
+ if (scuttlebotClient.isEnabled() && response.content) {
300
+ scuttlebotClient.mirrorAssistant(response.content).catch(() => { });
301
+ }
290
302
  const preChecks = [];
291
303
  const executableTools = [];
292
304
  for (const toolCall of response.toolCalls) {
@@ -360,7 +372,7 @@ export async function runAgentImpl(ctx, content) {
360
372
  status: `Executing ${executableTools.length} tools in parallel...`,
361
373
  detail: `${parallelStats.stages} stages, up to ${parallelStats.maxParallel}x speedup`,
362
374
  iteration: i + 1,
363
- maxIterations,
375
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : undefined,
364
376
  });
365
377
  ctx.setActivityState({
366
378
  action: `Executing ${executableTools.length} tools`,
@@ -370,8 +382,8 @@ export async function runAgentImpl(ctx, content) {
370
382
  // Execute in parallel using dependency-aware staging
371
383
  ctx.debugLog('tools', 'PARALLEL exec start', `count=${executableTools.length}`);
372
384
  const results = await executeParallel(executableTools, async (call) => {
373
- const result = await executeTool(call, process.cwd());
374
- return result.result;
385
+ const result = await executeTool(call, ctx.sessionRef.current?.projectPath ?? process.cwd());
386
+ return { result: result.result, isError: result.isError };
375
387
  }, (completed, total, current) => {
376
388
  const args = current.arguments;
377
389
  const target = args.path || args.command?.substring(0, 30) || current.name;
@@ -384,7 +396,7 @@ export async function runAgentImpl(ctx, content) {
384
396
  status: `Executing tools... (${completed + 1}/${total})`,
385
397
  detail: current.name,
386
398
  iteration: i + 1,
387
- maxIterations,
399
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : undefined,
388
400
  });
389
401
  });
390
402
  ctx.debugLog('tools', 'PARALLEL exec done', `results=${results.length}`);
@@ -392,10 +404,14 @@ export async function runAgentImpl(ctx, content) {
392
404
  for (const result of results) {
393
405
  const toolCall = result.toolCall;
394
406
  const args = toolCall.arguments;
407
+ // A tool-level failure (result.isError) or a thrown exception (result.error)
408
+ // both count as a failure — mirror the sequential branch.
409
+ const failed = result.isError || !!result.error;
410
+ const failureText = result.error || result.result;
395
411
  recordEvent('tool_call', toolCall.name, { name: toolCall.name, arguments: args });
396
- recordEvent('tool_result', (result.result || result.error || '').slice(0, 1000), { name: toolCall.name, isError: !!result.error });
412
+ recordEvent('tool_result', (result.result || result.error || '').slice(0, 1000), { name: toolCall.name, isError: failed });
397
413
  // Record in iteration ledger
398
- ctx.ledger?.recordAction(toolCall.name, args, result.error ? 'error' : 'ok', result.error || undefined);
414
+ ctx.ledger?.recordAction(toolCall.name, args, failed ? 'error' : 'ok', failed ? failureText : undefined);
399
415
  // Execute post-tool hooks
400
416
  hooks.executeHooks('post-tool', {
401
417
  tool: toolCall.name,
@@ -410,11 +426,11 @@ export async function runAgentImpl(ctx, content) {
410
426
  ctx.addMessage('tool', thought);
411
427
  }
412
428
  else if (result.error) {
413
- ctx.addMessage('tool', `Error: ${result.error}`);
429
+ ctx.addMessage('tool', `Error: ${result.error}`, true);
414
430
  }
415
431
  else {
416
432
  const preview = result.result.split('\n').slice(0, 3).join('\n');
417
- ctx.addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
433
+ ctx.addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''), failed);
418
434
  }
419
435
  ctx.llmMessages.current.push({
420
436
  role: 'tool',
@@ -454,7 +470,7 @@ export async function runAgentImpl(ctx, content) {
454
470
  detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
455
471
  thinking: thought,
456
472
  iteration: i + 1,
457
- maxIterations,
473
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : undefined,
458
474
  });
459
475
  }
460
476
  else {
@@ -463,7 +479,7 @@ export async function runAgentImpl(ctx, content) {
463
479
  detail: toolPreview.substring(0, 60),
464
480
  thinking: undefined,
465
481
  iteration: i + 1,
466
- maxIterations,
482
+ maxIterations: isFiniteIterationLimit(maxIterations) ? maxIterations : undefined,
467
483
  });
468
484
  }
469
485
  ctx.debugLog('tools', 'EXEC', toolCall.name, toolPreview.substring(0, 30));
@@ -484,7 +500,7 @@ export async function runAgentImpl(ctx, content) {
484
500
  detail: chunk.trimEnd().split('\n').pop()?.substring(0, 60),
485
501
  });
486
502
  } : undefined;
487
- const result = await executeTool(toolCall, process.cwd(), 60000, shellStreamCallback);
503
+ const result = await executeTool(toolCall, ctx.sessionRef.current?.projectPath ?? process.cwd(), 60000, shellStreamCallback);
488
504
  ctx.debugLog('tools', 'DONE', toolCall.name);
489
505
  recordEvent('tool_result', result.result.slice(0, 1000), { name: toolCall.name, isError: result.isError });
490
506
  // Record in iteration ledger
@@ -545,7 +561,7 @@ export async function runAgentImpl(ctx, content) {
545
561
  else {
546
562
  const display = result.displayResult || result.result;
547
563
  const preview = display.split('\n').slice(0, 5).join('\n');
548
- ctx.addMessage('tool', preview + (display.split('\n').length > 5 ? '\n...' : ''));
564
+ ctx.addMessage('tool', preview + (display.split('\n').length > 5 ? '\n...' : ''), result.isError);
549
565
  }
550
566
  if (toolCall.name !== 'ask_question') {
551
567
  ctx.llmMessages.current.push({
@@ -567,6 +583,12 @@ export async function runAgentImpl(ctx, content) {
567
583
  ctx.llmMessages.current.push({ role: 'assistant', content: response.content });
568
584
  ctx.addMessage('assistant', response.content);
569
585
  recordEvent('output', response.content.slice(0, 5000));
586
+ // Mirror assistant message to scuttlebot
587
+ if (scuttlebotClient.isEnabled()) {
588
+ scuttlebotClient.mirrorAssistant(response.content).catch(() => {
589
+ // Silent fail
590
+ });
591
+ }
570
592
  ctx.setStreamingResponse('');
571
593
  ctx.setContextTokens(ctx.estimateContextTokens());
572
594
  checkAndWarnContextLimit(ctx.actualProvider, ctx.actualModel, ctx.estimateContextTokens(), ctx.addMessage);
@@ -578,6 +600,7 @@ export async function runAgentImpl(ctx, content) {
578
600
  continue; // Loop again to get continuation
579
601
  }
580
602
  completedNaturally = true;
603
+ runStatus = 'completed';
581
604
  // End iteration ledger entry
582
605
  ctx.ledger?.endIteration('success');
583
606
  // Auto-save full message history for session persistence
@@ -609,6 +632,8 @@ export async function runAgentImpl(ctx, content) {
609
632
  ctx.setBreakerHealth?.(ctx.circuitBreaker.getHealth());
610
633
  if (breakerResult.tripped) {
611
634
  ctx.addMessage('system', `\u26a0\ufe0f Circuit breaker tripped: ${breakerResult.breaker}\n${breakerResult.message}\n\nUse /breaker resume to continue.`);
635
+ runStatus = 'stopped';
636
+ runErrorSummary = breakerResult.message;
612
637
  completedNaturally = true;
613
638
  break;
614
639
  }
@@ -635,18 +660,30 @@ export async function runAgentImpl(ctx, content) {
635
660
  }
636
661
  // Non-retryable errors (auth, etc.) still kill the session
637
662
  completedNaturally = true;
663
+ runStatus = 'failed';
664
+ runErrorSummary = errorMsg;
638
665
  // On error, clear queued messages to prevent infinite retry loop
639
666
  const currentQueuedOnError = ctx.queuedMessagesRef.current;
640
667
  if (currentQueuedOnError.length > 0) {
641
668
  ctx.addMessage('system', `\u26a0\ufe0f Cleared ${currentQueuedOnError.length} queued message(s) due to error. Use /clear to reset conversation.`);
642
669
  ctx.setQueuedMessages([]);
643
670
  }
671
+ if (runId && runStatus) {
672
+ ctx.ledger?.finishRun(runId, runStatus, { errorSummary: runErrorSummary });
673
+ }
644
674
  return; // Exit early on error - don't process queued messages
645
675
  }
646
676
  }
647
677
  // Only show warning if we actually hit the iteration limit (not errors or natural completion)
648
- if (!completedNaturally) {
678
+ if (!completedNaturally && isFiniteIterationLimit(maxIterations)) {
649
679
  ctx.addMessage('system', `⚠️ Reached ${maxIterations} iterations limit. Task may be incomplete. Adjust with /set maxIterations <number>.`);
680
+ if (!runStatus) {
681
+ runStatus = 'stopped';
682
+ runErrorSummary = `Reached ${maxIterations} iterations limit`;
683
+ }
684
+ }
685
+ if (runId) {
686
+ ctx.ledger?.finishRun(runId, runStatus || 'completed', { errorSummary: runErrorSummary });
650
687
  }
651
688
  // Update context tokens after agent run
652
689
  ctx.setContextTokens(ctx.estimateContextTokens());
@@ -684,29 +721,6 @@ export async function runAgentImpl(ctx, content) {
684
721
  // ============================================================================
685
722
  // Run Loop
686
723
  // ============================================================================
687
- /**
688
- * Start caffeinate to prevent system sleep during long operations (macOS).
689
- */
690
- function startCaffeinate() {
691
- if (process.platform !== 'darwin')
692
- return null;
693
- try {
694
- const proc = spawn('caffeinate', ['-di'], { stdio: 'ignore', detached: true });
695
- proc.unref();
696
- return proc;
697
- }
698
- catch {
699
- return null;
700
- }
701
- }
702
- function stopCaffeinate(proc) {
703
- if (proc) {
704
- try {
705
- proc.kill('SIGTERM');
706
- }
707
- catch { /* already dead */ }
708
- }
709
- }
710
724
  /**
711
725
  * Agent loop - runs prompt repeatedly until completion promise or max iterations.
712
726
  */
@@ -714,55 +728,95 @@ export async function runLoopImpl(ctx, prompt, maxIter, completionPromise) {
714
728
  ctx.setIsProcessing(true);
715
729
  setMood('focused');
716
730
  // Prevent system sleep during long agent loops (macOS)
717
- const caffeinateProc = startCaffeinate();
718
- for (let i = 0; i < maxIter; i++) {
719
- // Check if cancelled
720
- if (ctx.loopCancelledRef.current) {
721
- ctx.addMessage('system', '🛑 Loop cancelled by user');
722
- break;
723
- }
724
- ctx.setLoopIteration(i + 1);
725
- ctx.addMessage('system', `🔄 Loop iteration ${i + 1}/${maxIter}`);
726
- // First iteration: send original prompt. Subsequent: send continuation.
727
- const iterationPrompt = i === 0
728
- ? prompt
729
- : `Continue working on the task: "${prompt}"\n\nThis is iteration ${i + 1}. Review what you've done so far and continue making progress.`;
730
- ctx.llmMessages.current.push({ role: 'user', content: iterationPrompt });
731
- try {
732
- // Run the agent
733
- await runAgentImpl(ctx, iterationPrompt);
734
- // Check for completion promise in the last assistant message
735
- if (completionPromise) {
736
- const lastMessage = ctx.llmMessages.current[ctx.llmMessages.current.length - 1];
737
- if (lastMessage?.role === 'assistant') {
738
- const content = typeof lastMessage.content === 'string'
739
- ? lastMessage.content
740
- : JSON.stringify(lastMessage.content);
741
- if (content.includes(completionPromise)) {
742
- ctx.addMessage('system', `🎉 Completion promise "${completionPromise}" detected! Loop finished.`);
743
- break;
731
+ startPreventSleep();
732
+ let completedIterations = 0;
733
+ let loopOutcome = 'running';
734
+ let loopErrorSummary;
735
+ const loopRunId = ctx.ledger?.startRun('loop', prompt, {
736
+ completionPromise,
737
+ maxIterations: isFiniteIterationLimit(maxIter) ? maxIter : null,
738
+ });
739
+ try {
740
+ for (let i = 0; i < maxIter; i++) {
741
+ // Check if cancelled
742
+ if (ctx.loopCancelledRef.current) {
743
+ ctx.addMessage('system', '🛑 Loop cancelled by user');
744
+ loopOutcome = 'cancelled';
745
+ break;
746
+ }
747
+ completedIterations = i + 1;
748
+ ctx.setLoopIteration(completedIterations);
749
+ ctx.addMessage('system', `🔄 Loop iteration ${formatIterationProgress(completedIterations, maxIter)}`);
750
+ // First iteration: send original prompt. Subsequent: send continuation.
751
+ const iterationPrompt = i === 0
752
+ ? prompt
753
+ : `Continue working on the task: "${prompt}"\n\nThis is iteration ${i + 1}. Review what you've done so far and continue making progress.`;
754
+ try {
755
+ // Run the agent
756
+ await runAgentImpl(ctx, iterationPrompt);
757
+ // Check for completion promise in the last assistant message
758
+ if (completionPromise) {
759
+ const lastMessage = ctx.llmMessages.current[ctx.llmMessages.current.length - 1];
760
+ if (lastMessage?.role === 'assistant') {
761
+ const content = typeof lastMessage.content === 'string'
762
+ ? lastMessage.content
763
+ : JSON.stringify(lastMessage.content);
764
+ if (content.includes(completionPromise)) {
765
+ ctx.addMessage('system', `🎉 Completion promise "${completionPromise}" detected! Loop finished.`);
766
+ loopOutcome = 'promise-met';
767
+ break;
768
+ }
744
769
  }
745
770
  }
771
+ // Check cancelled again after agent run
772
+ if (ctx.loopCancelledRef.current) {
773
+ ctx.addMessage('system', '🛑 Loop cancelled by user');
774
+ loopOutcome = 'cancelled';
775
+ break;
776
+ }
777
+ // Small delay between iterations
778
+ if (i + 1 < maxIter) {
779
+ await new Promise(r => setTimeout(r, 500));
780
+ }
746
781
  }
747
- // Check cancelled again after agent run
748
- if (ctx.loopCancelledRef.current) {
749
- ctx.addMessage('system', '🛑 Loop cancelled by user');
782
+ catch (error) {
783
+ loopErrorSummary = error instanceof Error ? error.message : String(error);
784
+ ctx.addMessage('error', `Loop error: ${loopErrorSummary}`);
785
+ loopOutcome = 'error';
750
786
  break;
751
787
  }
752
- // Small delay between iterations
753
- await new Promise(r => setTimeout(r, 500));
754
788
  }
755
- catch (error) {
756
- ctx.addMessage('error', `Loop error: ${error instanceof Error ? error.message : String(error)}`);
757
- break;
789
+ if (loopOutcome === 'running') {
790
+ if (completionPromise) {
791
+ ctx.addMessage('system', `⚠️ Loop stopped after ${completedIterations} iteration${completedIterations === 1 ? '' : 's'} without matching completion promise "${completionPromise}".`);
792
+ }
793
+ else {
794
+ ctx.addMessage('system', `✅ Loop completed ${completedIterations} iteration${completedIterations === 1 ? '' : 's'}.`);
795
+ }
758
796
  }
759
797
  }
760
- // If we completed all iterations without hitting completion promise
761
- if (!ctx.loopCancelledRef.current && !completionPromise) {
762
- ctx.addMessage('system', `✅ Loop completed ${maxIter} iterations`);
798
+ finally {
799
+ if (loopRunId) {
800
+ const finalStatus = loopOutcome === 'cancelled'
801
+ ? 'cancelled'
802
+ : loopOutcome === 'error'
803
+ ? 'failed'
804
+ : loopOutcome === 'promise-met'
805
+ ? 'completed'
806
+ : completionPromise
807
+ ? 'stopped'
808
+ : 'completed';
809
+ ctx.ledger?.finishRun(loopRunId, finalStatus, {
810
+ errorSummary: loopErrorSummary
811
+ || (finalStatus === 'cancelled' ? 'Stopped by user' : undefined)
812
+ || (finalStatus === 'stopped' && completionPromise
813
+ ? `Stopped after ${completedIterations} iterations without matching completion promise`
814
+ : undefined),
815
+ });
816
+ }
817
+ ctx.setLoopActive(false);
818
+ ctx.setIsProcessing(false);
819
+ stopPreventSleep();
763
820
  }
764
- ctx.setLoopActive(false);
765
- ctx.setIsProcessing(false);
766
- stopCaffeinate(caffeinateProc);
767
821
  }
768
822
  //# sourceMappingURL=agent.js.map