@qduc/term2 0.13.0 → 0.14.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 (284) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +1 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/commands/compact-command.d.ts +6 -0
  5. package/dist/commands/compact-command.d.ts.map +1 -0
  6. package/dist/commands/compact-command.js +12 -0
  7. package/dist/commands/compact-command.js.map +1 -0
  8. package/dist/components/input/ApplicationInputSurface.d.ts.map +1 -1
  9. package/dist/components/input/ApplicationInputSurface.js +2 -0
  10. package/dist/components/input/ApplicationInputSurface.js.map +1 -1
  11. package/dist/components/input/MentorPoolMenuSession.d.ts +9 -0
  12. package/dist/components/input/MentorPoolMenuSession.d.ts.map +1 -0
  13. package/dist/components/input/MentorPoolMenuSession.js +131 -0
  14. package/dist/components/input/MentorPoolMenuSession.js.map +1 -0
  15. package/dist/components/input/MenuSurface.d.ts.map +1 -1
  16. package/dist/components/input/MenuSurface.js +16 -3
  17. package/dist/components/input/MenuSurface.js.map +1 -1
  18. package/dist/components/input/SettingsMenuSession.d.ts.map +1 -1
  19. package/dist/components/input/SettingsMenuSession.js +35 -22
  20. package/dist/components/input/SettingsMenuSession.js.map +1 -1
  21. package/dist/components/input/menu-controller.d.ts.map +1 -1
  22. package/dist/components/input/menu-controller.js +18 -1
  23. package/dist/components/input/menu-controller.js.map +1 -1
  24. package/dist/components/input/menu-registry.d.ts.map +1 -1
  25. package/dist/components/input/menu-registry.js +2 -0
  26. package/dist/components/input/menu-registry.js.map +1 -1
  27. package/dist/components/input/menu-types.d.ts +5 -0
  28. package/dist/components/input/menu-types.d.ts.map +1 -1
  29. package/dist/components/input/triggers.d.ts.map +1 -1
  30. package/dist/components/input/triggers.js +33 -0
  31. package/dist/components/input/triggers.js.map +1 -1
  32. package/dist/components/layout/BackgroundTasksPanel.d.ts +2 -0
  33. package/dist/components/layout/BackgroundTasksPanel.d.ts.map +1 -1
  34. package/dist/components/layout/BackgroundTasksPanel.js +25 -4
  35. package/dist/components/layout/BackgroundTasksPanel.js.map +1 -1
  36. package/dist/components/menu/CopyMenu.d.ts +1 -0
  37. package/dist/components/menu/CopyMenu.d.ts.map +1 -1
  38. package/dist/components/menu/CopyMenu.js +15 -2
  39. package/dist/components/menu/CopyMenu.js.map +1 -1
  40. package/dist/components/menu/MentorPoolSelectionMenu.d.ts +13 -0
  41. package/dist/components/menu/MentorPoolSelectionMenu.d.ts.map +1 -0
  42. package/dist/components/menu/MentorPoolSelectionMenu.js +98 -0
  43. package/dist/components/menu/MentorPoolSelectionMenu.js.map +1 -0
  44. package/dist/components/menu/ModelSelectionMenu.d.ts +1 -0
  45. package/dist/components/menu/ModelSelectionMenu.d.ts.map +1 -1
  46. package/dist/components/menu/ModelSelectionMenu.js +4 -2
  47. package/dist/components/menu/ModelSelectionMenu.js.map +1 -1
  48. package/dist/components/menu/SlashCommandMenu.d.ts.map +1 -1
  49. package/dist/components/menu/SlashCommandMenu.js +12 -9
  50. package/dist/components/menu/SlashCommandMenu.js.map +1 -1
  51. package/dist/components/message/SubagentActivityMessage.d.ts +1 -0
  52. package/dist/components/message/SubagentActivityMessage.d.ts.map +1 -1
  53. package/dist/components/message/SubagentActivityMessage.js +14 -1
  54. package/dist/components/message/SubagentActivityMessage.js.map +1 -1
  55. package/dist/context/InputContext.d.ts +1 -1
  56. package/dist/context/InputContext.d.ts.map +1 -1
  57. package/dist/context/InputContext.js +2 -0
  58. package/dist/context/InputContext.js.map +1 -1
  59. package/dist/contracts/application-stream.d.ts +7 -0
  60. package/dist/contracts/application-stream.d.ts.map +1 -1
  61. package/dist/contracts/provider-input.d.ts +15 -0
  62. package/dist/contracts/provider-input.d.ts.map +1 -1
  63. package/dist/contracts/provider-input.js +6 -1
  64. package/dist/contracts/provider-input.js.map +1 -1
  65. package/dist/hooks/settings-completion-config.d.ts.map +1 -1
  66. package/dist/hooks/settings-completion-config.js +10 -2
  67. package/dist/hooks/settings-completion-config.js.map +1 -1
  68. package/dist/hooks/use-app-commands.d.ts +2 -1
  69. package/dist/hooks/use-app-commands.d.ts.map +1 -1
  70. package/dist/hooks/use-app-commands.js +4 -1
  71. package/dist/hooks/use-app-commands.js.map +1 -1
  72. package/dist/hooks/use-conversation.d.ts.map +1 -1
  73. package/dist/hooks/use-conversation.js +9 -2
  74. package/dist/hooks/use-conversation.js.map +1 -1
  75. package/dist/hooks/use-mentor-pool-selection.d.ts +103 -0
  76. package/dist/hooks/use-mentor-pool-selection.d.ts.map +1 -0
  77. package/dist/hooks/use-mentor-pool-selection.js +642 -0
  78. package/dist/hooks/use-mentor-pool-selection.js.map +1 -0
  79. package/dist/lib/agent-chat-service.d.ts +3 -6
  80. package/dist/lib/agent-chat-service.d.ts.map +1 -1
  81. package/dist/lib/agent-chat-service.js +11 -1
  82. package/dist/lib/agent-chat-service.js.map +1 -1
  83. package/dist/lib/agent-client.d.ts +13 -0
  84. package/dist/lib/agent-client.d.ts.map +1 -1
  85. package/dist/lib/agent-client.js +115 -0
  86. package/dist/lib/agent-client.js.map +1 -1
  87. package/dist/lib/agent-configuration.d.ts.map +1 -1
  88. package/dist/lib/agent-configuration.js +2 -0
  89. package/dist/lib/agent-configuration.js.map +1 -1
  90. package/dist/lib/agent-factory.d.ts.map +1 -1
  91. package/dist/lib/agent-factory.js +9 -1
  92. package/dist/lib/agent-factory.js.map +1 -1
  93. package/dist/prompts/context-compaction.d.ts +4 -0
  94. package/dist/prompts/context-compaction.d.ts.map +1 -0
  95. package/dist/prompts/context-compaction.js +18 -0
  96. package/dist/prompts/context-compaction.js.map +1 -0
  97. package/dist/prompts/context-compaction.ts +23 -0
  98. package/dist/prompts/explorer-delegation-contract.test.ts +22 -0
  99. package/dist/prompts/gpt-5.6.md +1 -1
  100. package/dist/prompts/kimi.md +1 -1
  101. package/dist/prompts/plan-mode-info.md +2 -2
  102. package/dist/prompts/subagent-delegation.d.ts.map +1 -1
  103. package/dist/prompts/subagent-delegation.js +4 -2
  104. package/dist/prompts/subagent-delegation.js.map +1 -1
  105. package/dist/prompts/subagent-delegation.test.ts +10 -0
  106. package/dist/prompts/subagent-delegation.ts +4 -2
  107. package/dist/prompts/subagents/explorer.md +10 -5
  108. package/dist/prompts/subagents/librarian.md +1 -1
  109. package/dist/prompts/subagents/worker.md +1 -1
  110. package/dist/providers/aborted-stream-recorder.d.ts +20 -0
  111. package/dist/providers/aborted-stream-recorder.d.ts.map +1 -0
  112. package/dist/providers/aborted-stream-recorder.js +63 -0
  113. package/dist/providers/aborted-stream-recorder.js.map +1 -0
  114. package/dist/providers/codex-responses-model.d.ts +7 -2
  115. package/dist/providers/codex-responses-model.d.ts.map +1 -1
  116. package/dist/providers/codex-responses-model.js +72 -41
  117. package/dist/providers/codex-responses-model.js.map +1 -1
  118. package/dist/providers/codex.provider.d.ts +3 -2
  119. package/dist/providers/codex.provider.d.ts.map +1 -1
  120. package/dist/providers/codex.provider.js +24 -11
  121. package/dist/providers/codex.provider.js.map +1 -1
  122. package/dist/providers/model-catalog/catalog.generated.d.ts +406 -166
  123. package/dist/providers/model-catalog/catalog.generated.d.ts.map +1 -1
  124. package/dist/providers/model-catalog/catalog.generated.js +406 -166
  125. package/dist/providers/model-catalog/catalog.generated.js.map +1 -1
  126. package/dist/providers/openai-responses-model.d.ts +9 -1
  127. package/dist/providers/openai-responses-model.d.ts.map +1 -1
  128. package/dist/providers/openai-responses-model.js +34 -12
  129. package/dist/providers/openai-responses-model.js.map +1 -1
  130. package/dist/scripts/eval-context-compaction/runner.d.ts +2 -0
  131. package/dist/scripts/eval-context-compaction/runner.d.ts.map +1 -0
  132. package/dist/scripts/eval-context-compaction/runner.js +159 -0
  133. package/dist/scripts/eval-context-compaction/runner.js.map +1 -0
  134. package/dist/services/agent-runtime/application-run-loop.d.ts +30 -0
  135. package/dist/services/agent-runtime/application-run-loop.d.ts.map +1 -1
  136. package/dist/services/agent-runtime/application-run-loop.js +221 -99
  137. package/dist/services/agent-runtime/application-run-loop.js.map +1 -1
  138. package/dist/services/agent-runtime/context-compaction/index.d.ts +56 -0
  139. package/dist/services/agent-runtime/context-compaction/index.d.ts.map +1 -0
  140. package/dist/services/agent-runtime/context-compaction/index.js +131 -0
  141. package/dist/services/agent-runtime/context-compaction/index.js.map +1 -0
  142. package/dist/services/agent-runtime/context-compaction/local-context-compactor.d.ts +76 -0
  143. package/dist/services/agent-runtime/context-compaction/local-context-compactor.d.ts.map +1 -0
  144. package/dist/services/agent-runtime/context-compaction/local-context-compactor.js +146 -0
  145. package/dist/services/agent-runtime/context-compaction/local-context-compactor.js.map +1 -0
  146. package/dist/services/agent-runtime/generation-guard.d.ts +15 -1
  147. package/dist/services/agent-runtime/generation-guard.d.ts.map +1 -1
  148. package/dist/services/agent-runtime/generation-guard.js +34 -2
  149. package/dist/services/agent-runtime/generation-guard.js.map +1 -1
  150. package/dist/services/conversation/conversation-events.d.ts +3 -0
  151. package/dist/services/conversation/conversation-events.d.ts.map +1 -1
  152. package/dist/services/conversation/conversation-message-projection.d.ts.map +1 -1
  153. package/dist/services/conversation/conversation-message-projection.js +7 -3
  154. package/dist/services/conversation/conversation-message-projection.js.map +1 -1
  155. package/dist/services/conversation/conversation-service.d.ts +1 -0
  156. package/dist/services/conversation/conversation-service.d.ts.map +1 -1
  157. package/dist/services/conversation/conversation-service.js +74 -0
  158. package/dist/services/conversation/conversation-service.js.map +1 -1
  159. package/dist/services/conversation/conversation-state-projector.d.ts +7 -1
  160. package/dist/services/conversation/conversation-state-projector.d.ts.map +1 -1
  161. package/dist/services/conversation/conversation-state-projector.js +20 -3
  162. package/dist/services/conversation/conversation-state-projector.js.map +1 -1
  163. package/dist/services/conversation/conversation-state-schema.d.ts +8 -4
  164. package/dist/services/conversation/conversation-state-schema.d.ts.map +1 -1
  165. package/dist/services/conversation/conversation-state-schema.js +24 -2
  166. package/dist/services/conversation/conversation-state-schema.js.map +1 -1
  167. package/dist/services/conversation/conversation-store.d.ts +2 -0
  168. package/dist/services/conversation/conversation-store.d.ts.map +1 -1
  169. package/dist/services/conversation/conversation-store.js +8 -0
  170. package/dist/services/conversation/conversation-store.js.map +1 -1
  171. package/dist/services/conversation-agent-client.d.ts +9 -0
  172. package/dist/services/conversation-agent-client.d.ts.map +1 -1
  173. package/dist/services/logging/conversation-log-events.d.ts +1 -1
  174. package/dist/services/logging/conversation-log-events.d.ts.map +1 -1
  175. package/dist/services/logging/provider-traffic.d.ts +1 -1
  176. package/dist/services/logging/provider-traffic.d.ts.map +1 -1
  177. package/dist/services/logging/provider-traffic.js +10 -3
  178. package/dist/services/logging/provider-traffic.js.map +1 -1
  179. package/dist/services/retry/recovery-executor.d.ts.map +1 -1
  180. package/dist/services/retry/recovery-executor.js +8 -9
  181. package/dist/services/retry/recovery-executor.js.map +1 -1
  182. package/dist/services/service-interfaces.d.ts +15 -0
  183. package/dist/services/service-interfaces.d.ts.map +1 -1
  184. package/dist/services/session/session-composition.d.ts +7 -0
  185. package/dist/services/session/session-composition.d.ts.map +1 -1
  186. package/dist/services/session/session-composition.js +79 -0
  187. package/dist/services/session/session-composition.js.map +1 -1
  188. package/dist/services/session/session-manager.d.ts +3 -0
  189. package/dist/services/session/session-manager.d.ts.map +1 -1
  190. package/dist/services/session/session-manager.js +6 -0
  191. package/dist/services/session/session-manager.js.map +1 -1
  192. package/dist/services/session/session-stream-processor.d.ts.map +1 -1
  193. package/dist/services/session/session-stream-processor.js +3 -1
  194. package/dist/services/session/session-stream-processor.js.map +1 -1
  195. package/dist/services/session/session-tool-tracker.d.ts +17 -6
  196. package/dist/services/session/session-tool-tracker.d.ts.map +1 -1
  197. package/dist/services/session/session-tool-tracker.js +19 -7
  198. package/dist/services/session/session-tool-tracker.js.map +1 -1
  199. package/dist/services/settings/settings-schema.d.ts +52 -0
  200. package/dist/services/settings/settings-schema.d.ts.map +1 -1
  201. package/dist/services/settings/settings-schema.js +31 -2
  202. package/dist/services/settings/settings-schema.js.map +1 -1
  203. package/dist/services/settings/settings-sources.d.ts.map +1 -1
  204. package/dist/services/settings/settings-sources.js +2 -0
  205. package/dist/services/settings/settings-sources.js.map +1 -1
  206. package/dist/services/stream-event-processor.d.ts.map +1 -1
  207. package/dist/services/stream-event-processor.js +27 -1
  208. package/dist/services/stream-event-processor.js.map +1 -1
  209. package/dist/services/subagents/execution-runner.d.ts.map +1 -1
  210. package/dist/services/subagents/execution-runner.js +26 -1
  211. package/dist/services/subagents/execution-runner.js.map +1 -1
  212. package/dist/services/subagents/mentor-runner.d.ts.map +1 -1
  213. package/dist/services/subagents/mentor-runner.js +111 -13
  214. package/dist/services/subagents/mentor-runner.js.map +1 -1
  215. package/dist/services/subagents/nested-runner.d.ts.map +1 -1
  216. package/dist/services/subagents/nested-runner.js +54 -25
  217. package/dist/services/subagents/nested-runner.js.map +1 -1
  218. package/dist/services/subagents/role-loader.d.ts +6 -0
  219. package/dist/services/subagents/role-loader.d.ts.map +1 -1
  220. package/dist/services/subagents/role-loader.js +6 -1
  221. package/dist/services/subagents/role-loader.js.map +1 -1
  222. package/dist/services/subagents/utils.d.ts +17 -0
  223. package/dist/services/subagents/utils.d.ts.map +1 -1
  224. package/dist/services/subagents/utils.js +36 -0
  225. package/dist/services/subagents/utils.js.map +1 -1
  226. package/dist/services/tool-execution-ledger.d.ts +26 -1
  227. package/dist/services/tool-execution-ledger.d.ts.map +1 -1
  228. package/dist/services/tool-execution-ledger.js +103 -33
  229. package/dist/services/tool-execution-ledger.js.map +1 -1
  230. package/dist/tools/agent/activate-skill.d.ts.map +1 -1
  231. package/dist/tools/agent/activate-skill.js +1 -0
  232. package/dist/tools/agent/activate-skill.js.map +1 -1
  233. package/dist/tools/agent/ask-mentor.d.ts.map +1 -1
  234. package/dist/tools/agent/ask-mentor.js +5 -1
  235. package/dist/tools/agent/ask-mentor.js.map +1 -1
  236. package/dist/tools/agent/run-subagent-async.d.ts.map +1 -1
  237. package/dist/tools/agent/run-subagent-async.js +2 -0
  238. package/dist/tools/agent/run-subagent-async.js.map +1 -1
  239. package/dist/tools/agent/run-subagent.d.ts.map +1 -1
  240. package/dist/tools/agent/run-subagent.js +15 -6
  241. package/dist/tools/agent/run-subagent.js.map +1 -1
  242. package/dist/tools/file/apply-patch.d.ts.map +1 -1
  243. package/dist/tools/file/apply-patch.js +6 -3
  244. package/dist/tools/file/apply-patch.js.map +1 -1
  245. package/dist/tools/file/code-context.d.ts.map +1 -1
  246. package/dist/tools/file/code-context.js +2 -0
  247. package/dist/tools/file/code-context.js.map +1 -1
  248. package/dist/tools/file/glob.d.ts.map +1 -1
  249. package/dist/tools/file/glob.js +1 -0
  250. package/dist/tools/file/glob.js.map +1 -1
  251. package/dist/tools/file/grep.d.ts.map +1 -1
  252. package/dist/tools/file/grep.js +1 -0
  253. package/dist/tools/file/grep.js.map +1 -1
  254. package/dist/tools/file/read-file.d.ts +2 -0
  255. package/dist/tools/file/read-file.d.ts.map +1 -1
  256. package/dist/tools/file/read-file.js +36 -15
  257. package/dist/tools/file/read-file.js.map +1 -1
  258. package/dist/tools/system/shell.d.ts.map +1 -1
  259. package/dist/tools/system/shell.js +1 -0
  260. package/dist/tools/system/shell.js.map +1 -1
  261. package/dist/tools/types.d.ts +5 -0
  262. package/dist/tools/types.d.ts.map +1 -1
  263. package/dist/tools/types.js.map +1 -1
  264. package/dist/tools/web/web-fetch.d.ts.map +1 -1
  265. package/dist/tools/web/web-fetch.js +10 -12
  266. package/dist/tools/web/web-fetch.js.map +1 -1
  267. package/dist/tools/web/web-search.d.ts.map +1 -1
  268. package/dist/tools/web/web-search.js +1 -0
  269. package/dist/tools/web/web-search.js.map +1 -1
  270. package/dist/utils/conversation/conversation-event-handler.d.ts.map +1 -1
  271. package/dist/utils/conversation/conversation-event-handler.js +13 -1
  272. package/dist/utils/conversation/conversation-event-handler.js.map +1 -1
  273. package/dist/utils/output/bound-tool-result.d.ts +42 -0
  274. package/dist/utils/output/bound-tool-result.d.ts.map +1 -0
  275. package/dist/utils/output/bound-tool-result.js +81 -0
  276. package/dist/utils/output/bound-tool-result.js.map +1 -0
  277. package/dist/utils/settings-command.d.ts.map +1 -1
  278. package/dist/utils/settings-command.js +17 -2
  279. package/dist/utils/settings-command.js.map +1 -1
  280. package/dist/utils/shell/shell-output.d.ts +11 -0
  281. package/dist/utils/shell/shell-output.d.ts.map +1 -1
  282. package/dist/utils/shell/shell-output.js +18 -8
  283. package/dist/utils/shell/shell-output.js.map +1 -1
  284. package/package.json +1 -1
@@ -65,6 +65,8 @@ class EventQueue {
65
65
  * turns and silently drop later turns' records.
66
66
  */
67
67
  let nextCostRequestSeq = 0;
68
+ let nextToolBatchSeq = 0;
69
+ const DEFAULT_MAX_PARALLEL_TOOL_CALLS = 4;
68
70
  /**
69
71
  * Small provider-neutral agent loop. It owns model-turn sequencing and tool
70
72
  * execution; providers only implement `StreamedModelTurn`.
@@ -72,8 +74,10 @@ let nextCostRequestSeq = 0;
72
74
  export class ApplicationRunLoop {
73
75
  #deps;
74
76
  #contextCompactionSessionState;
77
+ #maxParallelToolCalls;
75
78
  #activeAbortController = null;
76
79
  #runInFlight = false;
80
+ #segmentGeneration = 0;
77
81
  /**
78
82
  * Set when a segment ends holding approvals, meaning the turn is pausing
79
83
  * rather than finishing. Injections offered during the pause wait here for
@@ -96,6 +100,7 @@ export class ApplicationRunLoop {
96
100
  constructor(deps) {
97
101
  this.#deps = deps;
98
102
  this.#contextCompactionSessionState = deps.contextCompactionSessionState ?? { disabled: false };
103
+ this.#maxParallelToolCalls = Math.max(1, Math.floor(deps.maxParallelToolCalls ?? DEFAULT_MAX_PARALLEL_TOOL_CALLS));
99
104
  }
100
105
  /**
101
106
  * Declare a turn open, making it steerable before its first run exists.
@@ -267,6 +272,7 @@ export class ApplicationRunLoop {
267
272
  approvals: options.approvals ?? new ApprovalLedger(),
268
273
  turnCount: 0,
269
274
  maxTurns: options.maxTurns,
275
+ automaticCompactionsThisRun: 0,
270
276
  };
271
277
  state.approve = (interruption) => {
272
278
  state.approvalDecision = 'approved';
@@ -346,6 +352,7 @@ export class ApplicationRunLoop {
346
352
  }
347
353
  #run(state, options) {
348
354
  const queue = new EventQueue();
355
+ const segmentGeneration = ++this.#segmentGeneration;
349
356
  this.abortSegment();
350
357
  const controller = new AbortController();
351
358
  this.#activeAbortController = controller;
@@ -397,6 +404,8 @@ export class ApplicationRunLoop {
397
404
  throw error;
398
405
  })
399
406
  .finally(() => {
407
+ if (segmentGeneration !== this.#segmentGeneration)
408
+ return;
400
409
  if (this.#activeAbortController === controller)
401
410
  this.#activeAbortController = null;
402
411
  this.#runInFlight = false;
@@ -445,27 +454,20 @@ export class ApplicationRunLoop {
445
454
  else {
446
455
  state.approvals.rejectTool({ toolName: pending.toolName, callId: pending.callId }, { message: state.approvalMessage });
447
456
  }
448
- const rawResult = approved
449
- ? await this.#invokeTool(pending.definition, pending.params, toolContext, pending.callId, state)
450
- : state.approvalMessage ?? 'rejected';
451
- const result = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult);
452
- const resultItem = {
453
- type: 'function_call_result',
454
- callId: pending.callId,
455
- name: pending.toolName,
456
- output: result,
457
- };
458
- state.history.push(resultItem);
459
- state.input.push({ type: 'tool_result', id: pending.callId, output: result });
460
- outputPush(stream, queue, { type: 'item', item: resultItem });
457
+ pending.plan.status = 'ready';
458
+ if (!approved)
459
+ pending.plan.output = state.approvalMessage ?? 'rejected';
461
460
  state.pendingApprovals.splice(pendingIndex, 1);
462
461
  state.pendingApproval = state.pendingApprovals[0];
463
462
  state.approvalDecision = undefined;
464
463
  state.approvalDecisionCallId = undefined;
465
464
  state.approvalMessage = undefined;
465
+ await this.#settleToolPlan(state, stream, queue, toolContext);
466
466
  stream.interruptions = state.pendingApprovals.map((item) => item.interruption);
467
- if (state.pendingApprovals.length > 0)
467
+ if (state.pendingApprovals.length > 0) {
468
+ this.#turnPaused = true;
468
469
  return finish(stream, state, queue);
470
+ }
469
471
  // Nothing is outstanding, so this segment is terminal — see
470
472
  // `stopAfterApprovalResolution`. Stopping here rather than at the
471
473
  // request boundary below is what keeps the just-recorded tool result
@@ -477,12 +479,52 @@ export class ApplicationRunLoop {
477
479
  // history, and the next request has not been built. A user message
478
480
  // admitted here reaches the model in sequence, mid-turn.
479
481
  this.#admitPendingSteers(state, stream, queue);
482
+ if (options.boundaryCompaction) {
483
+ const compactionStartedAt = Date.now();
484
+ const compaction = await options.boundaryCompaction.compact({
485
+ history: state.history,
486
+ automaticCompactionsThisRun: state.automaticCompactionsThisRun ?? 0,
487
+ signal: options.signal,
488
+ onStarted: (provider) => outputPush(stream, queue, { type: 'context_compaction_started', provider, strategy: 'local' }),
489
+ });
490
+ if (compaction.kind === 'compacted') {
491
+ state.history.splice(0, state.history.length, ...compaction.history);
492
+ state.input.splice(0, state.input.length, ...normalizeApplicationInput(compaction.modelInput));
493
+ state.responseId = undefined;
494
+ state.responseProviderId = undefined;
495
+ if (compaction.costRecords?.length) {
496
+ state.costRecords ??= [];
497
+ state.costRecords.push(...compaction.costRecords);
498
+ }
499
+ state.automaticCompactionsThisRun = (state.automaticCompactionsThisRun ?? 0) + 1;
500
+ outputPush(stream, queue, {
501
+ type: 'context_compaction_completed',
502
+ provider: state.currentProviderId ?? 'unknown',
503
+ strategy: 'local',
504
+ durationMs: Math.max(0, Date.now() - compactionStartedAt),
505
+ });
506
+ }
507
+ else if (compaction.kind === 'failed') {
508
+ outputPush(stream, queue, {
509
+ type: 'context_compaction_failed',
510
+ provider: compaction.provider,
511
+ strategy: 'local',
512
+ durationMs: Math.max(0, Date.now() - compactionStartedAt),
513
+ });
514
+ }
515
+ // Summarization is an asynchronous part of this same request
516
+ // boundary. Admit anything that arrived while it was in flight after
517
+ // applying a replacement, so the steer cannot be overwritten by the
518
+ // checkpoint or miss a terminal model request.
519
+ this.#admitPendingSteers(state, stream, queue);
520
+ }
480
521
  state.turnCount += 1;
481
522
  if (state.maxTurns !== undefined && state.turnCount > state.maxTurns) {
482
523
  throw new MaxTurnsExceededError(state.maxTurns);
483
524
  }
484
525
  const model = await this.#deps.resolveModel(state.agent.model);
485
526
  let sawToolCall = false;
527
+ const streamedToolCalls = [];
486
528
  let completion;
487
529
  let pendingNativeReasoning;
488
530
  // Stable process-unique request id, allocated immediately before dispatch
@@ -527,7 +569,7 @@ export class ApplicationRunLoop {
527
569
  ...options.generationGuard,
528
570
  });
529
571
  const consume = async () => {
530
- const deadline = new GenerationDeadline(generationGuard.requestDeadlineMs, () => requestAbortController.abort());
572
+ const deadline = new GenerationDeadline(generationGuard.requestDeadlineMs, () => requestAbortController.abort(), () => generationGuard.progress);
531
573
  let iterator;
532
574
  let iteratorFinished = false;
533
575
  try {
@@ -572,7 +614,7 @@ export class ApplicationRunLoop {
572
614
  generationGuard.observeToolCall(event.arguments);
573
615
  pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
574
616
  sawToolCall = true;
575
- await this.#handleToolCall(state, stream, queue, event, toolContext);
617
+ streamedToolCalls.push(event);
576
618
  }
577
619
  }
578
620
  }
@@ -612,8 +654,22 @@ export class ApplicationRunLoop {
612
654
  queue.push({ type: 'cost_update', record });
613
655
  throw error;
614
656
  }
615
- if (!completion)
616
- throw new Error('Application model turn ended without completion');
657
+ if (!completion) {
658
+ // A few application-owned model fixtures (and legacy adapters) end a
659
+ // tool-call stream immediately after the call and use the next request
660
+ // as the continuation boundary. Preserve that contract while routing
661
+ // the calls through the same ordered dispatcher. An empty stream still
662
+ // fails closed as an incomplete model turn.
663
+ if (streamedToolCalls.length === 0)
664
+ throw new Error('Application model turn ended without completion');
665
+ await this.#dispatchToolCalls(state, stream, queue, streamedToolCalls, toolContext);
666
+ if (state.pendingApprovals && state.pendingApprovals.length > 0) {
667
+ stream.interruptions = state.pendingApprovals.map((item) => item.interruption);
668
+ this.#turnPaused = true;
669
+ return finish(stream, state, queue);
670
+ }
671
+ continue;
672
+ }
617
673
  // Commit the authoritative terminal state before handling terminal-only
618
674
  // tool calls. Approval may pause immediately after this point, and the
619
675
  // continuation must still carry the response that produced the calls.
@@ -680,6 +736,7 @@ export class ApplicationRunLoop {
680
736
  // Some provider adapters report function calls only in the terminal
681
737
  // completion rather than as separate stream events. Their reasoning may
682
738
  // likewise be terminal-only, so associate it before replaying calls.
739
+ const toolCalls = [...streamedToolCalls];
683
740
  if (!sawToolCall) {
684
741
  for (const item of completion.output) {
685
742
  if (item.type === 'reasoning')
@@ -690,9 +747,11 @@ export class ApplicationRunLoop {
690
747
  continue;
691
748
  pendingNativeReasoning = commitPendingNativeReasoning(state, stream, queue, pendingNativeReasoning);
692
749
  sawToolCall = true;
693
- await this.#handleToolCall(state, stream, queue, item, toolContext);
750
+ toolCalls.push(item);
694
751
  }
695
752
  }
753
+ if (toolCalls.length > 0)
754
+ await this.#dispatchToolCalls(state, stream, queue, toolCalls, toolContext);
696
755
  // A native reasoning item belongs to the completed assistant turn even
697
756
  // when no tool call follows it. Commit it before assistant text so both
698
757
  // stateless continuation and persisted canonical history retain the
@@ -732,105 +791,154 @@ export class ApplicationRunLoop {
732
791
  }
733
792
  if (state.pendingApprovals.length > 0) {
734
793
  stream.interruptions = state.pendingApprovals.map((item) => item.interruption);
794
+ this.#turnPaused = true;
735
795
  return finish(stream, state, queue);
736
796
  }
737
797
  if (!sawToolCall)
738
798
  return finish(stream, state, queue);
739
799
  }
740
800
  }
741
- async #handleToolCall(state, stream, queue, event, toolContext) {
742
- const definition = state.agent.tools.find((tool) => tool.name === event.name);
743
- const args = parseArguments(event.arguments);
744
- const callItem = {
745
- type: 'function_call',
746
- callId: event.id,
747
- name: event.name,
748
- arguments: event.arguments,
749
- };
750
- state.history.push(callItem);
751
- state.input.push({ type: 'tool_call', id: event.id, name: event.name, arguments: event.arguments });
752
- outputPush(stream, queue, { type: 'item', item: callItem });
753
- if (!definition) {
754
- const output = `Unknown tool: ${event.name}`;
755
- const resultItem = {
756
- type: 'function_call_result',
757
- callId: event.id,
758
- name: event.name,
759
- output,
760
- };
761
- state.history.push(resultItem);
762
- state.input.push({ type: 'tool_result', id: event.id, output });
763
- outputPush(stream, queue, { type: 'item', item: resultItem });
764
- return;
765
- }
766
- // Keep the raw-model invocation contract: normalization repairs the
767
- // accepted object shape but intentionally does not Zod-parse it, which
768
- // would apply schema defaults before execute. web_fetch relies on its
769
- // executor fallbacks on the strict JSON-schema path.
770
- const params = normalizeToolParameters(args, definition.parameters);
771
- // Consult this run's ledger before prompting: a decision already taken
772
- // (in the parent run and replayed in, or earlier in this run) must not
773
- // prompt again. This is what makes approval replay observable.
774
- const alreadyDecided = state.approvals.isToolApproved({ toolName: event.name, callId: event.id });
775
- if (alreadyDecided === false) {
776
- const message = state.approvals.getRejectionMessage(event.name, event.id) ?? 'Tool execution was not approved.';
777
- const resultItem = {
778
- type: 'function_call_result',
779
- callId: event.id,
780
- name: event.name,
781
- output: message,
782
- };
783
- state.history.push(resultItem);
784
- state.input.push({ type: 'tool_result', id: event.id, output: message });
785
- outputPush(stream, queue, { type: 'item', item: resultItem });
786
- return;
787
- }
788
- if (alreadyDecided === true) {
789
- const result = await this.#invokeTool(definition, params, toolContext, event.id, state);
790
- const output = typeof result === 'string' ? result : JSON.stringify(result);
791
- const resultItem = {
792
- type: 'function_call_result',
801
+ async #dispatchToolCalls(state, stream, queue, events, toolContext) {
802
+ const plan = events.map((event) => {
803
+ const definition = state.agent.tools.find((tool) => tool.name === event.name);
804
+ const callItem = {
805
+ type: 'function_call',
793
806
  callId: event.id,
794
807
  name: event.name,
795
- output,
808
+ arguments: event.arguments,
796
809
  };
797
- state.history.push(resultItem);
798
- state.input.push({ type: 'tool_result', id: event.id, output });
799
- outputPush(stream, queue, { type: 'item', item: resultItem });
800
- return;
801
- }
802
- if (await definition.needsApproval(params, toolContext)) {
803
- const pending = {
804
- callId: event.id,
805
- toolName: event.name,
806
- argumentsText: event.arguments,
807
- interruption: {
808
- type: 'tool_approval_item',
809
- rawItem: { type: 'function_call', callId: event.id, name: event.name, arguments: event.arguments },
810
- callId: event.id,
811
- name: event.name,
812
- arguments: event.arguments,
813
- },
810
+ state.history.push(callItem);
811
+ state.input.push({ type: 'tool_call', id: event.id, name: event.name, arguments: event.arguments });
812
+ outputPush(stream, queue, { type: 'item', item: callItem });
813
+ return {
814
+ event,
814
815
  definition,
815
- params,
816
+ parallelSafe: false,
817
+ status: 'ready',
816
818
  };
817
- state.pendingApprovals ??= [];
818
- if (!state.pendingApproval)
819
- state.pendingApproval = pending;
820
- state.pendingApprovals.push(pending);
821
- stream.interruptions = state.pendingApprovals.map((item) => item.interruption);
819
+ });
820
+ state.toolPlan = plan;
821
+ for (const entry of plan) {
822
+ const { event, definition } = entry;
823
+ if (!definition) {
824
+ entry.output = `Unknown tool: ${event.name}`;
825
+ continue;
826
+ }
827
+ // Keep the raw-model invocation contract: normalization repairs the
828
+ // accepted object shape but intentionally does not Zod-parse it, which
829
+ // would apply schema defaults before execute. web_fetch relies on its
830
+ // executor fallbacks on the strict JSON-schema path.
831
+ entry.params = normalizeToolParameters(parseArguments(event.arguments), definition.parameters);
832
+ // Consult this run's ledger before prompting: a decision already taken
833
+ // (in the parent run and replayed in, or earlier in this run) must not
834
+ // prompt again. This is what makes approval replay observable.
835
+ const alreadyDecided = state.approvals.isToolApproved({ toolName: event.name, callId: event.id });
836
+ if (alreadyDecided === false) {
837
+ entry.output = state.approvals.getRejectionMessage(event.name, event.id) ?? 'Tool execution was not approved.';
838
+ continue;
839
+ }
840
+ if (alreadyDecided !== true && (await definition.needsApproval(entry.params, toolContext))) {
841
+ entry.status = 'approval_pending';
842
+ const pending = {
843
+ callId: event.id,
844
+ toolName: event.name,
845
+ argumentsText: event.arguments,
846
+ interruption: {
847
+ type: 'tool_approval_item',
848
+ rawItem: { type: 'function_call', callId: event.id, name: event.name, arguments: event.arguments },
849
+ callId: event.id,
850
+ name: event.name,
851
+ arguments: event.arguments,
852
+ },
853
+ definition,
854
+ params: entry.params,
855
+ plan: entry,
856
+ };
857
+ state.pendingApprovals ??= [];
858
+ state.pendingApprovals.push(pending);
859
+ continue;
860
+ }
861
+ entry.parallelSafe = await isParallelSafe(definition, entry.params, toolContext);
862
+ }
863
+ state.pendingApproval = state.pendingApprovals?.[0];
864
+ stream.interruptions = (state.pendingApprovals ?? []).map((item) => item.interruption);
865
+ this.#deps.logDiagnostic?.('tool parallel eligibility', {
866
+ decisions: plan.map((entry) => ({
867
+ callId: entry.event.id,
868
+ toolName: entry.event.name,
869
+ parallelSafe: entry.parallelSafe,
870
+ approvalPending: entry.status === 'approval_pending',
871
+ })),
872
+ });
873
+ await this.#settleToolPlan(state, stream, queue, toolContext);
874
+ }
875
+ async #settleToolPlan(state, stream, queue, toolContext) {
876
+ const plan = state.toolPlan;
877
+ if (!plan)
822
878
  return;
879
+ while (true) {
880
+ const firstPending = plan.find((entry) => entry.status !== 'completed');
881
+ if (!firstPending) {
882
+ state.toolPlan = undefined;
883
+ return;
884
+ }
885
+ if (firstPending.status === 'approval_pending')
886
+ return;
887
+ const group = [];
888
+ for (const entry of plan) {
889
+ if (entry.status === 'completed')
890
+ continue;
891
+ if (entry.status === 'approval_pending')
892
+ break;
893
+ if (group.length > 0 &&
894
+ (!entry.parallelSafe || !group[0].parallelSafe || group.length >= this.#maxParallelToolCalls))
895
+ break;
896
+ group.push(entry);
897
+ if (!entry.parallelSafe)
898
+ break;
899
+ }
900
+ const batchId = `tool-batch-${++nextToolBatchSeq}`;
901
+ this.#deps.logDiagnostic?.('tool batch dispatched', {
902
+ batchId,
903
+ callIds: group.map((entry) => entry.event.id),
904
+ parallel: group.length > 1,
905
+ dispatchOrder: group.map((entry) => entry.event.id),
906
+ });
907
+ if (group.length > 1) {
908
+ const results = await Promise.allSettled(group.map((entry) => this.#invokePlannedTool(entry, toolContext, state)));
909
+ const rejected = results.find((result) => result.status === 'rejected');
910
+ if (rejected)
911
+ throw rejected.reason;
912
+ for (const [index, result] of results.entries()) {
913
+ this.#appendToolResult(state, stream, queue, group[index], result.value);
914
+ }
915
+ }
916
+ else {
917
+ const result = await this.#invokePlannedTool(group[0], toolContext, state);
918
+ this.#appendToolResult(state, stream, queue, group[0], result);
919
+ }
920
+ this.#deps.logDiagnostic?.('tool batch settled', {
921
+ batchId,
922
+ settlementOrder: group.map((entry) => entry.event.id),
923
+ });
823
924
  }
824
- const result = await this.#invokeTool(definition, params, toolContext, event.id, state);
925
+ }
926
+ async #invokePlannedTool(entry, toolContext, state) {
927
+ entry.status = 'completed';
928
+ if (entry.output !== undefined)
929
+ return entry.output;
930
+ return this.#invokeTool(entry.definition, entry.params, toolContext, entry.event.id, state);
931
+ }
932
+ #appendToolResult(state, stream, queue, entry, result) {
825
933
  const output = typeof result === 'string' ? result : JSON.stringify(result);
826
934
  const resultItem = {
827
935
  type: 'function_call_result',
828
- callId: event.id,
829
- name: event.name,
936
+ callId: entry.event.id,
937
+ name: entry.event.name,
830
938
  output,
831
939
  };
832
940
  state.history.push(resultItem);
833
- state.input.push({ type: 'tool_result', id: event.id, output });
941
+ state.input.push({ type: 'tool_result', id: entry.event.id, output });
834
942
  outputPush(stream, queue, { type: 'item', item: resultItem });
835
943
  }
836
944
  /**
@@ -864,6 +972,9 @@ export class ApplicationRunLoop {
864
972
  attempt,
865
973
  scope: state?.hookScope ?? 'root',
866
974
  };
975
+ // Dispatch mark must run before execute so a mid-tool stream failure can
976
+ // settle the ledger as unknown rather than aborted.
977
+ this.#deps.getOnToolDispatch?.()?.(callId);
867
978
  await this.#notifyToolLifecycle(() => this.#deps.toolLifecycle?.before(lifecycleContext));
868
979
  try {
869
980
  const result = await definition.execute(params, toolContext, { toolCall: { callId } });
@@ -1043,6 +1154,17 @@ function parseArguments(value) {
1043
1154
  return {};
1044
1155
  }
1045
1156
  }
1157
+ async function isParallelSafe(definition, params, context) {
1158
+ if (typeof definition.parallelSafe === 'function') {
1159
+ try {
1160
+ return await definition.parallelSafe(params, context);
1161
+ }
1162
+ catch {
1163
+ return false;
1164
+ }
1165
+ }
1166
+ return definition.parallelSafe === true;
1167
+ }
1046
1168
  function getInterruptionCallId(value) {
1047
1169
  if (typeof value !== 'object' || value === null)
1048
1170
  return undefined;