@pentoshi/clai 4.8.4 → 4.9.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 (267) hide show
  1. package/README.md +69 -3
  2. package/dist/agent/finalize-gate.js +0 -9
  3. package/dist/agent/finalize-gate.js.map +1 -1
  4. package/dist/agent/plan-mode-reminders.js +5 -5
  5. package/dist/agent/plan-mode-reminders.js.map +1 -1
  6. package/dist/agent/plan-tool.d.ts +1 -0
  7. package/dist/agent/plan-tool.js +50 -21
  8. package/dist/agent/plan-tool.js.map +1 -1
  9. package/dist/agent/reliability-policy.d.ts +9 -0
  10. package/dist/agent/reliability-policy.js +36 -1
  11. package/dist/agent/reliability-policy.js.map +1 -1
  12. package/dist/agent/runner.d.ts +4 -0
  13. package/dist/agent/runner.js +193 -116
  14. package/dist/agent/runner.js.map +1 -1
  15. package/dist/agent/task-analyzer.d.ts +8 -18
  16. package/dist/agent/task-analyzer.js +75 -182
  17. package/dist/agent/task-analyzer.js.map +1 -1
  18. package/dist/agent/task-evidence.js +10 -0
  19. package/dist/agent/task-evidence.js.map +1 -1
  20. package/dist/agent/tool-call-parser.js +14 -13
  21. package/dist/agent/tool-call-parser.js.map +1 -1
  22. package/dist/app/adapters/current-agent-adapter.d.ts +5 -1
  23. package/dist/app/adapters/current-agent-adapter.js +2 -1
  24. package/dist/app/adapters/current-agent-adapter.js.map +1 -1
  25. package/dist/app/commands/catalog.js +5 -0
  26. package/dist/app/commands/catalog.js.map +1 -1
  27. package/dist/app/controllers/session-compact-helper.js +3 -6
  28. package/dist/app/controllers/session-compact-helper.js.map +1 -1
  29. package/dist/app/ports/transcript-item.d.ts +4 -0
  30. package/dist/classic/app/ClassicApp.js +1 -1
  31. package/dist/classic/app/ClassicApp.js.map +1 -1
  32. package/dist/classic/app/action-handlers.js +4 -0
  33. package/dist/classic/app/action-handlers.js.map +1 -1
  34. package/dist/classic/app/app-wiring.js +1 -0
  35. package/dist/classic/app/app-wiring.js.map +1 -1
  36. package/dist/classic/app/wiring-lifecycle.js +1 -0
  37. package/dist/classic/app/wiring-lifecycle.js.map +1 -1
  38. package/dist/classic/blocks/compacted-lines.js +3 -0
  39. package/dist/classic/blocks/compacted-lines.js.map +1 -1
  40. package/dist/classic/blocks/thinking-lines.js +11 -2
  41. package/dist/classic/blocks/thinking-lines.js.map +1 -1
  42. package/dist/classic/bootstrap/start-classic.js +3 -0
  43. package/dist/classic/bootstrap/start-classic.js.map +1 -1
  44. package/dist/classic/chrome/Composer.d.ts +6 -0
  45. package/dist/classic/chrome/Composer.js +2 -3
  46. package/dist/classic/chrome/Composer.js.map +1 -1
  47. package/dist/classic/chrome/composer-controller.d.ts +6 -1
  48. package/dist/classic/chrome/composer-controller.js +18 -8
  49. package/dist/classic/chrome/composer-controller.js.map +1 -1
  50. package/dist/classic/chrome/editor-view.d.ts +2 -1
  51. package/dist/classic/chrome/editor-view.js +1 -1
  52. package/dist/classic/chrome/editor-view.js.map +1 -1
  53. package/dist/classic/chrome/status-rows.js +3 -2
  54. package/dist/classic/chrome/status-rows.js.map +1 -1
  55. package/dist/classic/panels/TextEditorPanel.d.ts +3 -0
  56. package/dist/classic/panels/TextEditorPanel.js +7 -0
  57. package/dist/classic/panels/TextEditorPanel.js.map +1 -0
  58. package/dist/classic/panels/panel-controller.js +28 -1
  59. package/dist/classic/panels/panel-controller.js.map +1 -1
  60. package/dist/classic/panels/panel-effect.d.ts +3 -0
  61. package/dist/classic/panels/panel-effect.js.map +1 -1
  62. package/dist/classic/panels/panel-effects.js +3 -0
  63. package/dist/classic/panels/panel-effects.js.map +1 -1
  64. package/dist/classic/panels/panel-host.js +3 -0
  65. package/dist/classic/panels/panel-host.js.map +1 -1
  66. package/dist/classic/panels/panel-types.d.ts +2 -0
  67. package/dist/classic/panels/secret-panel.d.ts +1 -1
  68. package/dist/classic/panels/secret-panel.js +4 -2
  69. package/dist/classic/panels/secret-panel.js.map +1 -1
  70. package/dist/classic/panels/text-editor-panel.d.ts +33 -0
  71. package/dist/classic/panels/text-editor-panel.js +101 -0
  72. package/dist/classic/panels/text-editor-panel.js.map +1 -0
  73. package/dist/llm/adapters/anthropic-tools.d.ts +2 -1
  74. package/dist/llm/adapters/anthropic-tools.js +55 -4
  75. package/dist/llm/adapters/anthropic-tools.js.map +1 -1
  76. package/dist/llm/anthropic.js +1 -1
  77. package/dist/llm/anthropic.js.map +1 -1
  78. package/dist/llm/aws-mantle.js +2 -0
  79. package/dist/llm/aws-mantle.js.map +1 -1
  80. package/dist/llm/meta.js +62 -68
  81. package/dist/llm/meta.js.map +1 -1
  82. package/dist/llm/provider.js +7 -7
  83. package/dist/llm/request-plan.js +7 -2
  84. package/dist/llm/request-plan.js.map +1 -1
  85. package/dist/llm/tool-protocol.d.ts +1 -0
  86. package/dist/llm/tool-protocol.js +7 -0
  87. package/dist/llm/tool-protocol.js.map +1 -1
  88. package/dist/mcp/client.d.ts +27 -0
  89. package/dist/mcp/client.js +119 -0
  90. package/dist/mcp/client.js.map +1 -0
  91. package/dist/mcp/config-file.d.ts +35 -0
  92. package/dist/mcp/config-file.js +217 -0
  93. package/dist/mcp/config-file.js.map +1 -0
  94. package/dist/mcp/discovery.d.ts +4 -0
  95. package/dist/mcp/discovery.js +409 -0
  96. package/dist/mcp/discovery.js.map +1 -0
  97. package/dist/mcp/format.d.ts +17 -0
  98. package/dist/mcp/format.js +122 -0
  99. package/dist/mcp/format.js.map +1 -0
  100. package/dist/mcp/index.d.ts +15 -0
  101. package/dist/mcp/index.js +16 -0
  102. package/dist/mcp/index.js.map +1 -0
  103. package/dist/mcp/jsonrpc.d.ts +40 -0
  104. package/dist/mcp/jsonrpc.js +175 -0
  105. package/dist/mcp/jsonrpc.js.map +1 -0
  106. package/dist/mcp/manager.d.ts +36 -0
  107. package/dist/mcp/manager.js +300 -0
  108. package/dist/mcp/manager.js.map +1 -0
  109. package/dist/mcp/mentions.d.ts +9 -0
  110. package/dist/mcp/mentions.js +44 -0
  111. package/dist/mcp/mentions.js.map +1 -0
  112. package/dist/mcp/names.d.ts +15 -0
  113. package/dist/mcp/names.js +59 -0
  114. package/dist/mcp/names.js.map +1 -0
  115. package/dist/mcp/results.d.ts +6 -0
  116. package/dist/mcp/results.js +182 -0
  117. package/dist/mcp/results.js.map +1 -0
  118. package/dist/mcp/runtime.d.ts +107 -0
  119. package/dist/mcp/runtime.js +505 -0
  120. package/dist/mcp/runtime.js.map +1 -0
  121. package/dist/mcp/substitution.d.ts +13 -0
  122. package/dist/mcp/substitution.js +74 -0
  123. package/dist/mcp/substitution.js.map +1 -0
  124. package/dist/mcp/transport-http.d.ts +53 -0
  125. package/dist/mcp/transport-http.js +433 -0
  126. package/dist/mcp/transport-http.js.map +1 -0
  127. package/dist/mcp/transport-stdio.d.ts +39 -0
  128. package/dist/mcp/transport-stdio.js +268 -0
  129. package/dist/mcp/transport-stdio.js.map +1 -0
  130. package/dist/mcp/transport.d.ts +20 -0
  131. package/dist/mcp/transport.js +44 -0
  132. package/dist/mcp/transport.js.map +1 -0
  133. package/dist/mcp/types.d.ts +179 -0
  134. package/dist/mcp/types.js +30 -0
  135. package/dist/mcp/types.js.map +1 -0
  136. package/dist/mcp/validation.d.ts +16 -0
  137. package/dist/mcp/validation.js +222 -0
  138. package/dist/mcp/validation.js.map +1 -0
  139. package/dist/modes/agent.d.ts +2 -0
  140. package/dist/modes/agent.js.map +1 -1
  141. package/dist/noninteractive/start-noninteractive.js +4 -0
  142. package/dist/noninteractive/start-noninteractive.js.map +1 -1
  143. package/dist/prompts/embedded.js +2 -2
  144. package/dist/prompts/embedded.js.map +1 -1
  145. package/dist/prompts/index.d.ts +3 -0
  146. package/dist/prompts/index.js +70 -55
  147. package/dist/prompts/index.js.map +1 -1
  148. package/dist/prompts/system.agent.md +34 -10
  149. package/dist/prompts/system.ask.md +8 -0
  150. package/dist/safety/classifier.js +4 -0
  151. package/dist/safety/classifier.js.map +1 -1
  152. package/dist/store/logs.js +17 -6
  153. package/dist/store/logs.js.map +1 -1
  154. package/dist/store/plan.d.ts +2 -0
  155. package/dist/store/plan.js.map +1 -1
  156. package/dist/store/session-workspace.js +2 -1
  157. package/dist/store/session-workspace.js.map +1 -1
  158. package/dist/tools/definitions.js +18 -0
  159. package/dist/tools/definitions.js.map +1 -1
  160. package/dist/tools/external-tools.d.ts +30 -0
  161. package/dist/tools/external-tools.js +52 -0
  162. package/dist/tools/external-tools.js.map +1 -0
  163. package/dist/tools/registry.js +36 -5
  164. package/dist/tools/registry.js.map +1 -1
  165. package/dist/tui-v2/bootstrap/start-tui-v2.js +3 -0
  166. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
  167. package/dist/tui-v2/components/completion/completion-menu.d.ts +4 -6
  168. package/dist/tui-v2/components/completion/completion-menu.js +34 -16
  169. package/dist/tui-v2/components/completion/completion-menu.js.map +1 -1
  170. package/dist/tui-v2/components/composer/composer-input-box.js +3 -1
  171. package/dist/tui-v2/components/composer/composer-input-box.js.map +1 -1
  172. package/dist/tui-v2/components/modal/secret-modal.js +6 -4
  173. package/dist/tui-v2/components/modal/secret-modal.js.map +1 -1
  174. package/dist/tui-v2/components/modal/text-editor-modal.d.ts +19 -0
  175. package/dist/tui-v2/components/modal/text-editor-modal.js +85 -0
  176. package/dist/tui-v2/components/modal/text-editor-modal.js.map +1 -0
  177. package/dist/tui-v2/components/overlay/overlay-host.js +2 -1
  178. package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
  179. package/dist/tui-v2/components/transcript/compacted-row.js +12 -3
  180. package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
  181. package/dist/tui-v2/components/transcript/thinking-block.d.ts +9 -1
  182. package/dist/tui-v2/components/transcript/thinking-block.js +253 -49
  183. package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
  184. package/dist/tui-v2/components/transcript/thinking-presentation.d.ts +46 -0
  185. package/dist/tui-v2/components/transcript/thinking-presentation.js +84 -0
  186. package/dist/tui-v2/components/transcript/thinking-presentation.js.map +1 -0
  187. package/dist/tui-v2/components/transcript/transcript-row.d.ts +2 -0
  188. package/dist/tui-v2/components/transcript/transcript-row.js +7 -2
  189. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  190. package/dist/tui-v2/components/transcript/transcript-view.js +195 -83
  191. package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
  192. package/dist/tui-v2/composer/composer-editor.js +54 -39
  193. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  194. package/dist/tui-v2/composer/composer-highlight.d.ts +20 -0
  195. package/dist/tui-v2/composer/composer-highlight.js +97 -0
  196. package/dist/tui-v2/composer/composer-highlight.js.map +1 -0
  197. package/dist/tui-v2/composer/textarea-keybindings.d.ts +5 -0
  198. package/dist/tui-v2/composer/textarea-keybindings.js +12 -0
  199. package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -1
  200. package/dist/ui/mentions.js +4 -1
  201. package/dist/ui/mentions.js.map +1 -1
  202. package/dist/ui-core/actions/action-id.d.ts +1 -1
  203. package/dist/ui-core/actions/action-id.js +1 -0
  204. package/dist/ui-core/actions/action-id.js.map +1 -1
  205. package/dist/ui-core/actions/format-shortcuts.js +5 -0
  206. package/dist/ui-core/actions/format-shortcuts.js.map +1 -1
  207. package/dist/ui-core/actions/keymap.js +3 -0
  208. package/dist/ui-core/actions/keymap.js.map +1 -1
  209. package/dist/ui-core/bootstrap/composition-root.d.ts +3 -0
  210. package/dist/ui-core/bootstrap/composition-root.js +5 -1
  211. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  212. package/dist/ui-core/commands/command-handlers.js +2 -0
  213. package/dist/ui-core/commands/command-handlers.js.map +1 -1
  214. package/dist/ui-core/commands/mcp-commands.d.ts +3 -0
  215. package/dist/ui-core/commands/mcp-commands.js +310 -0
  216. package/dist/ui-core/commands/mcp-commands.js.map +1 -0
  217. package/dist/ui-core/commands/picker-commands.d.ts +1 -1
  218. package/dist/ui-core/commands/picker-commands.js +2 -2
  219. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  220. package/dist/ui-core/commands/session-commands.js +1 -0
  221. package/dist/ui-core/commands/session-commands.js.map +1 -1
  222. package/dist/ui-core/composer/completion-viewport.d.ts +40 -0
  223. package/dist/ui-core/composer/completion-viewport.js +105 -0
  224. package/dist/ui-core/composer/completion-viewport.js.map +1 -0
  225. package/dist/ui-core/controllers/overlay-controller.d.ts +24 -3
  226. package/dist/ui-core/controllers/overlay-controller.js +35 -4
  227. package/dist/ui-core/controllers/overlay-controller.js.map +1 -1
  228. package/dist/ui-core/rendering/artifact-pager-source.d.ts +1 -0
  229. package/dist/ui-core/rendering/artifact-pager-source.js +67 -1
  230. package/dist/ui-core/rendering/artifact-pager-source.js.map +1 -1
  231. package/dist/ui-core/rendering/duration.d.ts +3 -1
  232. package/dist/ui-core/rendering/duration.js +14 -0
  233. package/dist/ui-core/rendering/duration.js.map +1 -1
  234. package/dist/ui-core/rendering/exit-summary.js +4 -4
  235. package/dist/ui-core/rendering/format-help.js +4 -0
  236. package/dist/ui-core/rendering/format-help.js.map +1 -1
  237. package/dist/ui-core/rendering/intro-header.js +4 -4
  238. package/dist/ui-core/rendering/open-tool-output.js +90 -32
  239. package/dist/ui-core/rendering/open-tool-output.js.map +1 -1
  240. package/dist/ui-core/rendering/theme.d.ts +5 -3
  241. package/dist/ui-core/rendering/theme.js +8 -4
  242. package/dist/ui-core/rendering/theme.js.map +1 -1
  243. package/dist/ui-core/rendering/wordmark.d.ts +2 -2
  244. package/dist/ui-core/rendering/wordmark.js +62 -62
  245. package/dist/ui-core/rendering/wordmark.js.map +1 -1
  246. package/dist/ui-core/state/thinking-copy.d.ts +13 -0
  247. package/dist/ui-core/state/thinking-copy.js +33 -0
  248. package/dist/ui-core/state/thinking-copy.js.map +1 -0
  249. package/dist/ui-core/state/transcript-hydrate.js +20 -0
  250. package/dist/ui-core/state/transcript-hydrate.js.map +1 -1
  251. package/dist/ui-core/state/transcript-reducer.js +140 -62
  252. package/dist/ui-core/state/transcript-reducer.js.map +1 -1
  253. package/dist/ui-core/state/transcript-store.d.ts +10 -0
  254. package/dist/ui-core/state/transcript-store.js +235 -31
  255. package/dist/ui-core/state/transcript-store.js.map +1 -1
  256. package/dist/ui-core/state/transcript-types.d.ts +10 -0
  257. package/dist/ui-core/state/transcript-types.js +1 -0
  258. package/dist/ui-core/state/transcript-types.js.map +1 -1
  259. package/dist/ui-core/state/transcript-window.d.ts +32 -0
  260. package/dist/ui-core/state/transcript-window.js +63 -0
  261. package/dist/ui-core/state/transcript-window.js.map +1 -0
  262. package/dist/version.generated.d.ts +2 -2
  263. package/dist/version.generated.js +2 -2
  264. package/package.json +1 -1
  265. package/dist/tui-v2/composer/skill-highlight.d.ts +0 -3
  266. package/dist/tui-v2/composer/skill-highlight.js +0 -63
  267. package/dist/tui-v2/composer/skill-highlight.js.map +0 -1
@@ -1,4 +1,6 @@
1
1
  import { join } from "node:path";
2
+ import { hasMcpMentionSyntax } from "../mcp/mentions.js";
3
+ import { isCanonicalToolName } from "../mcp/names.js";
2
4
  import { streamWithProvider } from "../llm/router.js";
3
5
  import { isProviderFailureStatus } from "../llm/key-rotation.js";
4
6
  import { REQUEST_CONTEXT_PREFIX, upsertRequestContextMessage, } from "../llm/system-messages.js";
@@ -6,6 +8,7 @@ import { operationUsageFromError } from "../llm/operation-ledger.js";
6
8
  import { contextAttemptFromOperationUsage } from "../llm/context-snapshot.js";
7
9
  import { modelContextWindow } from "../llm/token-usage.js";
8
10
  import { providerInputTokenBudget } from "../llm/context-windows.js";
11
+ import { resolveBuiltInProfile } from "../llm/provider-profiles.js";
9
12
  import { streamAlreadyEmitted } from "../llm/stream-progress.js";
10
13
  import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, createStreamRecoveryState, resetStreamRecoveryState, } from "./stream-recovery.js";
11
14
  import { modelSupportsVision, resolveToolDialect } from "../llm/capabilities.js";
@@ -49,7 +52,7 @@ import { compactMessagesWithSummary, shouldApplyAutoCompact, COMPACTION_MEMORY_P
49
52
  import { buildContextBreakdown, contextBreakdownAuditPayload, describeDominantContextBlock, toolSchemaHash, } from "./context-breakdown.js";
50
53
  import { recordRequestTokenObservation } from "../llm/token-estimate-calibration.js";
51
54
  import { accountAssembledRequest, RequestOverLimitError, } from "./request-accounting.js";
52
- import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, resolveStepMaxTokens, } from "./reliability-policy.js";
55
+ import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, MAX_OUTPUT_BUDGET_CONTINUATIONS, MAX_STEP_COMPLETION_TOKENS, outputBudgetWasExhausted, resolveStepMaxTokens, } from "./reliability-policy.js";
53
56
  import { auditLog } from "../store/logs.js";
54
57
  import { loadProjectContext } from "../store/project.js";
55
58
  import { loadAgentInstructions } from "../instructions/load.js";
@@ -378,6 +381,7 @@ export async function runAgentTurn(prompt, options = {}) {
378
381
  emit({ type: "turn-end", outcome, finalAnswer: rendered, steps });
379
382
  return outcome;
380
383
  };
384
+ let mcpLease;
381
385
  try {
382
386
  emit({
383
387
  type: "turn-start",
@@ -387,6 +391,17 @@ export async function runAgentTurn(prompt, options = {}) {
387
391
  : {}),
388
392
  });
389
393
  const config = getConfig();
394
+ const mcpRuntime = options.mcp;
395
+ const mcpMentioned = hasMcpMentionSyntax(prompt);
396
+ if (mcpRuntime &&
397
+ (mcpRuntime.getState().selection.mode !== "off" || mcpMentioned)) {
398
+ await mcpRuntime.ensureReady();
399
+ }
400
+ if (mcpRuntime && mcpMentioned)
401
+ mcpRuntime.applyMentionSelection(prompt);
402
+ const mcpToolDefinitions = mcpRuntime?.toolDefinitions({ ...(agentMode === "ask" ? { askMode: true } : {}) }) ?? [];
403
+ mcpLease = mcpRuntime?.beginTurn();
404
+ const mcpToolNames = mcpToolDefinitions.map((definition) => definition.name);
390
405
  const maxSteps = options.maxSteps ?? 70;
391
406
  const confirmPort = options.confirm ?? stdioConfirmPort;
392
407
  const projectContext = await loadProjectContext();
@@ -402,7 +417,7 @@ export async function runAgentTurn(prompt, options = {}) {
402
417
  // image.view is different from optimistic user-attachment handling: once
403
418
  // the tool succeeds, the model must actually receive and inspect its bytes.
404
419
  // Offer it only with affirmative capability evidence for the active route.
405
- const routeToolNames = (routeProvider, routeModel) => availableToolNames().filter((name) => {
420
+ const routeToolNames = (routeProvider, routeModel) => [...availableToolNames(), ...mcpToolNames].filter((name) => {
406
421
  if (name === "image.ocr")
407
422
  return imageOcrEnabled;
408
423
  if (name === "image.view") {
@@ -448,16 +463,17 @@ export async function runAgentTurn(prompt, options = {}) {
448
463
  const inputTokenBudget = providerInputTokenBudget(provider, model);
449
464
  const useCompactSystemPrompt = inputTokenBudget !== undefined;
450
465
  const resolveNativeTools = (p, m) => {
451
- const dialect = resolveToolDialect(p, m, config.toolCalling);
466
+ const dialect = resolveToolDialect(p, m, options.toolCalling ?? config.toolCalling);
452
467
  return { dialect, native: dialect !== "none" };
453
468
  };
454
469
  let { dialect: toolDialect, native: nativeToolsActive } = resolveNativeTools(provider, model);
455
470
  const selectToolDefs = (native, compact, routeProvider = provider, routeModel = model) => {
456
471
  if (!native)
457
472
  return undefined;
458
- const base = compact
459
- ? getCompactToolDefinitions()
460
- : getToolDefinitions();
473
+ const base = [
474
+ ...(compact ? getCompactToolDefinitions() : getToolDefinitions()),
475
+ ...mcpToolDefinitions,
476
+ ];
461
477
  const allow = new Set([
462
478
  ...routeToolNames(routeProvider, routeModel),
463
479
  ...RUNNER_META_TOOL_NAMES,
@@ -570,6 +586,12 @@ export async function runAgentTurn(prompt, options = {}) {
570
586
  const systemSections = [
571
587
  renderRequestEnvironmentContext({ plan: activePlan }),
572
588
  ];
589
+ const mcpContext = mcpRuntime?.promptContext({
590
+ nativeTools: false,
591
+ ...(agentMode === "ask" ? { askMode: true } : {}),
592
+ });
593
+ if (mcpContext)
594
+ systemSections.push(mcpContext);
573
595
  if (projectContext) {
574
596
  systemSections.push(`Project context from .clai/context.md:\n${projectContext}`);
575
597
  }
@@ -723,13 +745,16 @@ export async function runAgentTurn(prompt, options = {}) {
723
745
  ? "mode"
724
746
  : content.includes("OUTCOME")
725
747
  ? "outcome"
726
- : content.includes("WORKFLOW") || content.includes("FOCUS")
748
+ : content.includes("WORKFLOW") ||
749
+ content.includes("FOCUS") ||
750
+ content.startsWith("WORK PROFILE")
727
751
  ? "focus"
728
752
  : "context",
729
753
  content,
730
754
  mandatory: content.startsWith(SKILLS_CATALOG_PREFIX)
731
755
  ? selectedSkillNames.length > 0
732
756
  : content.startsWith("ACTIVE PLAN") ||
757
+ content.startsWith("MCP TOOL CONTEXT") ||
733
758
  content.startsWith("ENGAGEMENT SCOPE") ||
734
759
  content.startsWith("REQUEST ENVIRONMENT") ||
735
760
  content.startsWith("Project context from .clai/context.md:") ||
@@ -923,7 +948,6 @@ export async function runAgentTurn(prompt, options = {}) {
923
948
  });
924
949
  };
925
950
  const loopGuard = new LoopGuard();
926
- let lastExactPromptTokens = 0;
927
951
  // Uncalibrated estimate for the request currently in flight. Paired with the
928
952
  // provider's reported prompt size below so the estimator learns this route's
929
953
  // bias instead of permanently over-reporting it.
@@ -961,6 +985,7 @@ export async function runAgentTurn(prompt, options = {}) {
961
985
  // to actually act instead of silently returning an empty answer.
962
986
  let emptyVisibleRetries = 0;
963
987
  let truncatedBudgetRounds = 0;
988
+ let continuationBudgetFloor = 0;
964
989
  let retryWithoutThinking = false;
965
990
  // Robust stream-failure recovery. When a provider stream/complete fails we
966
991
  // try working approaches (backoff, compaction, thinking-off, provider
@@ -1276,7 +1301,11 @@ export async function runAgentTurn(prompt, options = {}) {
1276
1301
  }
1277
1302
  async function executeSingleTool(rawCall, toolEventId, parentSignal) {
1278
1303
  const scratchDir = scratchDirFor(safeCwd());
1279
- let call = normalizeToolCall(rawCall);
1304
+ const normalizedCall = normalizeToolCall(rawCall);
1305
+ const canonicalMcpName = mcpRuntime?.canonicalizeToolName(normalizedCall.name);
1306
+ let call = canonicalMcpName && canonicalMcpName !== normalizedCall.name
1307
+ ? { ...normalizedCall, name: canonicalMcpName }
1308
+ : normalizedCall;
1280
1309
  const emitVisibleSyntheticReceipt = (result, summary) => {
1281
1310
  if (!alreadyPrintedIds.has(toolEventId)) {
1282
1311
  writeToolCall(toolEventId, call);
@@ -1630,7 +1659,7 @@ export async function runAgentTurn(prompt, options = {}) {
1630
1659
  }
1631
1660
  }
1632
1661
  const scope = await loadScopeForSession(session.sessionId);
1633
- const decision = classifyToolCall(call, { scope });
1662
+ const decision = mcpRuntime?.classify(call.name) ?? classifyToolCall(call, { scope });
1634
1663
  await auditLog("tool.classified", {
1635
1664
  call,
1636
1665
  decision,
@@ -1654,7 +1683,9 @@ export async function runAgentTurn(prompt, options = {}) {
1654
1683
  call.name === "terminal.send" ||
1655
1684
  ((call.name === "shell.exec" || call.name === "shell.start") &&
1656
1685
  !isPlanModeAllowedShellCommand(cmd));
1657
- const allowed = isPlanModeAllowedTool(call.name) && !shellBlocked;
1686
+ const allowed = (isPlanModeAllowedTool(call.name) ||
1687
+ mcpRuntime?.classify(call.name)?.level === "safe") &&
1688
+ !shellBlocked;
1658
1689
  if (!allowed) {
1659
1690
  const reason = `plan mode — ${call.name} is blocked (gather-only). ` +
1660
1691
  `Use any recon/enum/scan/research tool; do not write project files or run active exploits. ` +
@@ -2086,58 +2117,62 @@ export async function runAgentTurn(prompt, options = {}) {
2086
2117
  * could freeze for minutes after "cancelling stalled tool".
2087
2118
  */
2088
2119
  const runToolWithForcedSettle = () => {
2089
- const work = runToolCall(call, {
2090
- signal: toolAc.signal,
2091
- requestSecret: options.requestSecret ?? stdioSecretRequester,
2092
- onOutput: (chunk) => {
2093
- if (toolAc.signal.aborted)
2094
- return;
2095
- resetStallTimer();
2096
- printLive(chunk);
2097
- },
2098
- confirmed: true,
2099
- userPrompt: prompt,
2100
- // image.view needs the active route to check vision support and size
2101
- // images to the provider's per-image budget.
2102
- llmProvider: provider,
2103
- llmModel: model,
2104
- sessionId: session.sessionId,
2105
- ...(delegation?.taskId ? { taskId: delegation.taskId } : {}),
2106
- ...(delegation ? { delegationId: delegation.id } : {}),
2107
- ...(dispatchedTaskId ? { parentTaskId: dispatchedTaskId } : {}),
2108
- wakeOnCompletion: true,
2109
- monitor: {
2110
- toolName: call.name,
2111
- toolEventId,
2112
- },
2113
- ...(engagementAction && scope
2114
- ? {
2115
- engagementAuthorization: {
2116
- target: engagementDecision?.normalizedTarget || engagementAction.target,
2117
- ...(scope.expiresAt ? { expiresAt: scope.expiresAt } : {}),
2118
- },
2119
- authorizeNetworkHop: async (url, resolvedAddresses) => {
2120
- const hop = actionFromUrl({
2121
- url,
2122
- method: engagementAction.method,
2123
- phase: engagementAction.phase,
2124
- capability: engagementAction.capability,
2125
- resolvedAddresses,
2126
- });
2127
- const hopDecision = evaluateEngagementAction(scope, hop);
2128
- await auditLog("engagement.policy.hop", {
2129
- ...(engagementGraph ? { engagementId: engagementGraph.id } : {}),
2130
- ...(engagementRecord ? { actionId: engagementRecord.id } : {}),
2131
- url,
2132
- resolvedAddresses,
2133
- allowed: hopDecision.allowed,
2134
- reason: hopDecision.reason,
2135
- });
2136
- return { allowed: hopDecision.allowed, reason: hopDecision.reason };
2137
- },
2138
- }
2139
- : {}),
2140
- });
2120
+ const work = mcpRuntime !== undefined &&
2121
+ (mcpRuntime.getTool(call.name) !== undefined ||
2122
+ isCanonicalToolName(call.name))
2123
+ ? mcpRuntime.callTool(call.name, call.args, { signal: toolAc.signal })
2124
+ : runToolCall(call, {
2125
+ signal: toolAc.signal,
2126
+ requestSecret: options.requestSecret ?? stdioSecretRequester,
2127
+ onOutput: (chunk) => {
2128
+ if (toolAc.signal.aborted)
2129
+ return;
2130
+ resetStallTimer();
2131
+ printLive(chunk);
2132
+ },
2133
+ confirmed: true,
2134
+ userPrompt: prompt,
2135
+ // image.view needs the active route to check vision support and size
2136
+ // images to the provider's per-image budget.
2137
+ llmProvider: provider,
2138
+ llmModel: model,
2139
+ sessionId: session.sessionId,
2140
+ ...(delegation?.taskId ? { taskId: delegation.taskId } : {}),
2141
+ ...(delegation ? { delegationId: delegation.id } : {}),
2142
+ ...(dispatchedTaskId ? { parentTaskId: dispatchedTaskId } : {}),
2143
+ wakeOnCompletion: true,
2144
+ monitor: {
2145
+ toolName: call.name,
2146
+ toolEventId,
2147
+ },
2148
+ ...(engagementAction && scope
2149
+ ? {
2150
+ engagementAuthorization: {
2151
+ target: engagementDecision?.normalizedTarget || engagementAction.target,
2152
+ ...(scope.expiresAt ? { expiresAt: scope.expiresAt } : {}),
2153
+ },
2154
+ authorizeNetworkHop: async (url, resolvedAddresses) => {
2155
+ const hop = actionFromUrl({
2156
+ url,
2157
+ method: engagementAction.method,
2158
+ phase: engagementAction.phase,
2159
+ capability: engagementAction.capability,
2160
+ resolvedAddresses,
2161
+ });
2162
+ const hopDecision = evaluateEngagementAction(scope, hop);
2163
+ await auditLog("engagement.policy.hop", {
2164
+ ...(engagementGraph ? { engagementId: engagementGraph.id } : {}),
2165
+ ...(engagementRecord ? { actionId: engagementRecord.id } : {}),
2166
+ url,
2167
+ resolvedAddresses,
2168
+ allowed: hopDecision.allowed,
2169
+ reason: hopDecision.reason,
2170
+ });
2171
+ return { allowed: hopDecision.allowed, reason: hopDecision.reason };
2172
+ },
2173
+ }
2174
+ : {}),
2175
+ });
2141
2176
  return new Promise((resolve, reject) => {
2142
2177
  let settled = false;
2143
2178
  const finishOk = (r) => {
@@ -2886,8 +2921,7 @@ export async function runAgentTurn(prompt, options = {}) {
2886
2921
  });
2887
2922
  }
2888
2923
  async function maybeAutoCompact(reason, force = false) {
2889
- const beforeRequestTokens = estimateNextRequestTokens(messages);
2890
- const beforeTokens = Math.max(beforeRequestTokens, lastExactPromptTokens);
2924
+ const beforeTokens = estimateNextRequestTokens(messages);
2891
2925
  const contextLimitTokens = currentContextLimitTokens();
2892
2926
  const compactTrigger = autoCompactTriggerTokens(getReliabilityPolicy(), {
2893
2927
  provider,
@@ -3025,7 +3059,6 @@ export async function runAgentTurn(prompt, options = {}) {
3025
3059
  messages.splice(0, messages.length, ...candidateMessages);
3026
3060
  compactionAttempts.recordSuccess(attemptKey);
3027
3061
  loopGuard.resetReadOnly();
3028
- lastExactPromptTokens = 0;
3029
3062
  // The snapshot predates the rewrite: replaying it would resurrect the
3030
3063
  // pre-compaction history. The next successful request re-seeds it.
3031
3064
  lastSuccessfulRequestSnapshot = undefined;
@@ -3172,6 +3205,21 @@ export async function runAgentTurn(prompt, options = {}) {
3172
3205
  }
3173
3206
  }
3174
3207
  const contextLimitTokens = currentContextLimitTokens();
3208
+ const routeOutputTokenLimit = resolveBuiltInProfile({
3209
+ provider,
3210
+ model,
3211
+ }).limits.outputTokens;
3212
+ stepMaxTokens = resolveStepMaxTokens({
3213
+ nativeToolsActive,
3214
+ toolsAttached,
3215
+ recoveryNudge: retryWithoutThinking,
3216
+ truncationDepth: truncatedBudgetRounds,
3217
+ thinkingEnabled: Boolean(config.thinking?.enabled) && !retryWithoutThinking,
3218
+ minimumTokens: continuationBudgetFloor,
3219
+ ...(routeOutputTokenLimit !== undefined
3220
+ ? { outputTokenLimit: routeOutputTokenLimit }
3221
+ : {}),
3222
+ });
3175
3223
  await auditLog("agent.turn", {
3176
3224
  provider,
3177
3225
  model,
@@ -3187,13 +3235,10 @@ export async function runAgentTurn(prompt, options = {}) {
3187
3235
  ? { contextLimitTokens }
3188
3236
  : {}),
3189
3237
  }),
3190
- maxTokensBudget: resolveStepMaxTokens({
3191
- nativeToolsActive,
3192
- toolsAttached,
3193
- recoveryNudge: retryWithoutThinking,
3194
- truncationDepth: truncatedBudgetRounds,
3195
- thinkingEnabled: Boolean(config.thinking?.enabled) && !retryWithoutThinking,
3196
- }),
3238
+ maxTokensBudget: stepMaxTokens,
3239
+ ...(routeOutputTokenLimit !== undefined
3240
+ ? { outputTokenLimit: routeOutputTokenLimit }
3241
+ : {}),
3197
3242
  });
3198
3243
  // Resume / mid-turn abort can leave orphan tool rows or a user
3199
3244
  // "continue" before tool results. Heal first so multi-key retry and
@@ -3202,14 +3247,6 @@ export async function runAgentTurn(prompt, options = {}) {
3202
3247
  // ok=true so the model doesn't thrash on fake exit=130 failures.
3203
3248
  repairToolProtocol(messages);
3204
3249
  assertValidToolProtocol(messages);
3205
- // E3: adaptive completion budget (still large enough for writes).
3206
- stepMaxTokens = resolveStepMaxTokens({
3207
- nativeToolsActive,
3208
- toolsAttached,
3209
- recoveryNudge: retryWithoutThinking,
3210
- truncationDepth: truncatedBudgetRounds,
3211
- thinkingEnabled: Boolean(config.thinking?.enabled) && !retryWithoutThinking,
3212
- });
3213
3250
  try {
3214
3251
  // MR-007: the fit verdict is taken on the final assembled request —
3215
3252
  // after protocol repair and every live-state reinjection — and a
@@ -3540,7 +3577,6 @@ export async function runAgentTurn(prompt, options = {}) {
3540
3577
  model = completion.model;
3541
3578
  if (completion.usage) {
3542
3579
  if (completion.usage.exact && completion.usage.promptTokens > 0) {
3543
- lastExactPromptTokens = completion.usage.promptTokens;
3544
3580
  recordRequestTokenObservation({
3545
3581
  provider: completion.provider,
3546
3582
  model: completion.model,
@@ -3602,6 +3638,11 @@ export async function runAgentTurn(prompt, options = {}) {
3602
3638
  ...assistantTextResult,
3603
3639
  visible: continuedVisible,
3604
3640
  };
3641
+ const retryReasoning = completion.reasoningArtifacts?.length || completion.reasoningBlock
3642
+ ? completion
3643
+ : assistantText.hasThinking
3644
+ ? { reasoningBlock: { text: assistantText.thinkContent } }
3645
+ : completion;
3605
3646
  const commitAssistantRetry = (historyText) => {
3606
3647
  const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
3607
3648
  if (!hasShownToolCall) {
@@ -3619,7 +3660,7 @@ export async function runAgentTurn(prompt, options = {}) {
3619
3660
  });
3620
3661
  }
3621
3662
  }
3622
- pushAssistantHistory(historyText, completion);
3663
+ pushAssistantHistory(historyText, retryReasoning);
3623
3664
  interruptedVisible = "";
3624
3665
  interruptedReasoning = "";
3625
3666
  lowYieldResumptions = 0;
@@ -3805,45 +3846,72 @@ export async function runAgentTurn(prompt, options = {}) {
3805
3846
  malformedNativeArgsRounds = 0;
3806
3847
  }
3807
3848
  }
3808
- if (!canonicalAssistantVisible.trim() && !call) {
3809
- const completionTokens = completion.usage?.completionTokens ?? 0;
3810
- const hitOutputLimit = completion.finishReason === "length" ||
3811
- (completionTokens > 0 && stepMaxTokens > 0 && completionTokens >= stepMaxTokens - 64);
3812
- const truncatedRoundText = collapseRepeatedText(completion.text ?? "");
3813
- if (hitOutputLimit && truncatedRoundText.trim() && truncatedBudgetRounds < 2) {
3814
- truncatedBudgetRounds += 1;
3815
- writeNotice("warn", "response hit the output token limit — continuing from where it stopped");
3816
- messages.push({
3817
- role: "assistant",
3818
- content: sanitizeAssistantText(truncatedRoundText),
3819
- });
3820
- messages.push(recoveryUserMessage("Your previous response was cut off by the output token limit before it completed. " +
3821
- "Continue directly from where it stopped do not restart the analysis or repeat prior text. " +
3822
- "Finish briefly: emit the next tool call, or the final answer if the task is complete."));
3823
- continue;
3824
- }
3825
- if (hitOutputLimit &&
3826
- !truncatedRoundText.trim() &&
3827
- assistantText.hasThinking &&
3828
- truncatedBudgetRounds < 4) {
3849
+ const completionTokens = completion.usage?.completionTokens ?? 0;
3850
+ const completionRouteProfile = resolveBuiltInProfile({
3851
+ provider,
3852
+ model,
3853
+ });
3854
+ const completionBudget = completionRouteProfile.limits.outputTokens === undefined
3855
+ ? stepMaxTokens
3856
+ : Math.min(stepMaxTokens, completionRouteProfile.limits.outputTokens);
3857
+ const hitOutputLimit = outputBudgetWasExhausted({
3858
+ finishReason: completion.finishReason,
3859
+ completionTokens,
3860
+ requestedMaxTokens: completionBudget,
3861
+ });
3862
+ const incompleteNativeStream = nativeToolCalls.length === 0 && streamedNativeCallNames.size > 0;
3863
+ const outputLimitLooksLikeTool = incompleteNativeStream ||
3864
+ countToolFences(assistantText.visible) > 0 ||
3865
+ looksLikeTruncatedToolCall(assistantText.visible);
3866
+ if (hitOutputLimit && !call && !outputLimitLooksLikeTool) {
3867
+ if (truncatedBudgetRounds < MAX_OUTPUT_BUDGET_CONTINUATIONS) {
3829
3868
  truncatedBudgetRounds += 1;
3830
- interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, assistantText.thinkContent);
3869
+ const desiredContinuationBudget = Math.min(MAX_STEP_COMPLETION_TOKENS, Math.max(completionBudget, completionBudget * 2));
3870
+ continuationBudgetFloor =
3871
+ completionRouteProfile.limits.outputTokens === undefined
3872
+ ? desiredContinuationBudget
3873
+ : Math.min(desiredContinuationBudget, completionRouteProfile.limits.outputTokens);
3874
+ retryWithoutThinking =
3875
+ completionRouteProfile.reasoning.generation !== "mandatory";
3876
+ if (assistantText.hasThinking) {
3877
+ interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, assistantText.thinkContent);
3878
+ }
3831
3879
  const preservedBudgetReasoning = interruptedReasoning;
3832
- writeNotice("warn", "reasoning used the whole output budget — preserving it and widening the budget");
3833
- commitAssistantRetry(assistantText.visible);
3834
- interruptedReasoning = preservedBudgetReasoning;
3880
+ if (canonicalAssistantVisible.trim()) {
3881
+ visibleCommitted = true;
3882
+ pushAssistantHistory(assistantText.visible, retryReasoning);
3883
+ interruptedVisible = canonicalAssistantVisible;
3884
+ lowYieldResumptions = 0;
3885
+ }
3886
+ else {
3887
+ commitAssistantRetry(assistantText.visible);
3888
+ interruptedReasoning = preservedBudgetReasoning;
3889
+ }
3890
+ writeNotice("warn", retryWithoutThinking
3891
+ ? "response used the whole output budget — preserving it and continuing once with optional reasoning disabled"
3892
+ : "response used the whole output budget — preserving it and continuing once at the route limit");
3835
3893
  messages.push(recoveryUserMessage([
3836
- "Your previous response spent the entire output budget on reasoning and was cut off before any visible answer. " +
3837
- "Do not restart the analysis your conclusions so far are preserved below. " +
3838
- "Wrap up the reasoning now and emit the next tool call or the final answer directly.",
3894
+ canonicalAssistantVisible.trim()
3895
+ ? "Your previous response was cut off by the output token limit. Continue from the exact stopping point without repeating any prior text."
3896
+ : "Your previous response spent the output budget before producing a visible answer. Do not restart the analysis; use the preserved conclusions and answer now.",
3897
+ retryWithoutThinking
3898
+ ? "Optional reasoning is disabled for this continuation. Emit the next tool call or final answer directly and briefly."
3899
+ : "Finish the reasoning briefly, then emit the next tool call or final answer directly.",
3839
3900
  interruptedReasoningBrief(interruptedReasoning),
3840
3901
  ]
3841
3902
  .filter((part) => Boolean(part))
3842
3903
  .join("\n\n")));
3843
3904
  continue;
3844
3905
  }
3845
- const incompleteNativeStream = nativeToolCalls.length === 0 &&
3846
- streamedNativeCallNames.size > 0;
3906
+ writeNotice("warn", canonicalAssistantVisible.trim()
3907
+ ? "response reached the output limit again after its bounded continuation — returning the preserved partial answer"
3908
+ : "response reached the output limit again after its bounded continuation — stopping without restarting the reasoning");
3909
+ if (!canonicalAssistantVisible.trim()) {
3910
+ commitAssistantRetry(assistantText.visible);
3911
+ return finishTurn("The model exhausted its output budget again after one preserved continuation. No visible answer was produced.", step + 1, "partial", ["Retry at a lower reasoning effort or choose a model with a larger output limit."], "The model exhausted the route's output budget twice without a visible answer.");
3912
+ }
3913
+ }
3914
+ if (!canonicalAssistantVisible.trim() && !call) {
3847
3915
  if (incompleteNativeStream) {
3848
3916
  const reason = "The provider began this native tool call but never completed it. Nothing ran; reissue a complete call.";
3849
3917
  for (const deferred of deferredToolCalls) {
@@ -3904,6 +3972,7 @@ export async function runAgentTurn(prompt, options = {}) {
3904
3972
  // Reset the counter on any successful visible output or recovered call.
3905
3973
  emptyVisibleRetries = 0;
3906
3974
  truncatedBudgetRounds = 0;
3975
+ continuationBudgetFloor = 0;
3907
3976
  retryWithoutThinking = false;
3908
3977
  interruptedReasoning = "";
3909
3978
  }
@@ -4233,15 +4302,18 @@ export async function runAgentTurn(prompt, options = {}) {
4233
4302
  ? "One or more required plan tasks failed."
4234
4303
  : outcomeStatus === "partial"
4235
4304
  ? "Required outcome criteria remain unsupported by current evidence."
4236
- : undefined, displayCleaned);
4305
+ : undefined, interruptedVisible ? cleaned : displayCleaned);
4237
4306
  }
4238
4307
  // A valid primary tool call exists for this fresh model turn. Show any
4239
4308
  // prose / thinking that preceded it, record the assistant message ONCE.
4309
+ const toolDisplayText = interruptedVisible
4310
+ ? canonicalAssistantVisible
4311
+ : assistantText.visible;
4240
4312
  const beforeTool = recoveredFromBareJson
4241
4313
  ? ""
4242
4314
  : nativeToolCalls.length
4243
- ? assistantText.visible.trim()
4244
- : textBeforeToolCall(assistantText.visible);
4315
+ ? toolDisplayText.trim()
4316
+ : textBeforeToolCall(toolDisplayText);
4245
4317
  if (beforeTool) {
4246
4318
  writeAssistantMessage(beforeTool);
4247
4319
  }
@@ -4486,6 +4558,8 @@ export async function runAgentTurn(prompt, options = {}) {
4486
4558
  }
4487
4559
  const scopeForBatch = await loadScopeForSession(session.sessionId).catch(() => undefined);
4488
4560
  const isParallelSafe = (c) => {
4561
+ if (mcpRuntime?.isParallelSafe(c.name))
4562
+ return true;
4489
4563
  if (c.name === "pentest.recon" ||
4490
4564
  c.name === "net.context" ||
4491
4565
  c.name === "tool.batch" ||
@@ -5030,6 +5104,9 @@ export async function runAgentTurn(prompt, options = {}) {
5030
5104
  });
5031
5105
  throw error;
5032
5106
  }
5107
+ finally {
5108
+ mcpLease?.release();
5109
+ }
5033
5110
  }
5034
5111
  /** Compatibility boundary for callers that consume visible assistant text. */
5035
5112
  export async function runAgentLoop(prompt, options = {}) {