@moxxy/sdk 0.43.0 → 0.45.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 (246) hide show
  1. package/dist/agent-account-limit.d.ts +41 -0
  2. package/dist/agent-account-limit.d.ts.map +1 -0
  3. package/dist/agent-account-limit.js +64 -0
  4. package/dist/agent-account-limit.js.map +1 -0
  5. package/dist/agent-computer-use.d.ts +10 -0
  6. package/dist/agent-computer-use.d.ts.map +1 -0
  7. package/dist/agent-computer-use.js +24 -0
  8. package/dist/agent-computer-use.js.map +1 -0
  9. package/dist/agent-context-window.d.ts +22 -0
  10. package/dist/agent-context-window.d.ts.map +1 -0
  11. package/dist/agent-context-window.js +40 -0
  12. package/dist/agent-context-window.js.map +1 -0
  13. package/dist/agent-no-reply.d.ts +11 -0
  14. package/dist/agent-no-reply.d.ts.map +1 -0
  15. package/dist/agent-no-reply.js +13 -0
  16. package/dist/agent-no-reply.js.map +1 -0
  17. package/dist/agent-step.d.ts +14 -0
  18. package/dist/agent-step.d.ts.map +1 -0
  19. package/dist/agent-step.js +26 -0
  20. package/dist/agent-step.js.map +1 -0
  21. package/dist/agent-work.d.ts +34 -0
  22. package/dist/agent-work.d.ts.map +1 -0
  23. package/dist/agent-work.js +80 -0
  24. package/dist/agent-work.js.map +1 -0
  25. package/dist/attachment-payload.d.ts +95 -0
  26. package/dist/attachment-payload.d.ts.map +1 -0
  27. package/dist/attachment-payload.js +155 -0
  28. package/dist/attachment-payload.js.map +1 -0
  29. package/dist/auto-approve.d.ts +9 -0
  30. package/dist/auto-approve.d.ts.map +1 -1
  31. package/dist/auto-approve.js +18 -0
  32. package/dist/auto-approve.js.map +1 -1
  33. package/dist/channel-auth.d.ts +3 -3
  34. package/dist/channel-auth.d.ts.map +1 -1
  35. package/dist/channel-auth.js +1 -1
  36. package/dist/channel.d.ts +1 -1
  37. package/dist/client-session.d.ts +9 -5
  38. package/dist/client-session.d.ts.map +1 -1
  39. package/dist/computer-control.d.ts +11 -11
  40. package/dist/computer-control.d.ts.map +1 -1
  41. package/dist/computer-control.js +1 -1
  42. package/dist/computer-control.js.map +1 -1
  43. package/dist/conversation-history.d.ts +50 -0
  44. package/dist/conversation-history.d.ts.map +1 -0
  45. package/dist/conversation-history.js +227 -0
  46. package/dist/conversation-history.js.map +1 -0
  47. package/dist/cron.d.ts +58 -0
  48. package/dist/cron.d.ts.map +1 -0
  49. package/dist/cron.js +320 -0
  50. package/dist/cron.js.map +1 -0
  51. package/dist/define.d.ts +2 -1
  52. package/dist/define.d.ts.map +1 -1
  53. package/dist/define.js +1 -0
  54. package/dist/define.js.map +1 -1
  55. package/dist/event-store.d.ts +1 -1
  56. package/dist/event-store.d.ts.map +1 -1
  57. package/dist/event-store.js +1 -0
  58. package/dist/event-store.js.map +1 -1
  59. package/dist/events.d.ts +23 -1
  60. package/dist/events.d.ts.map +1 -1
  61. package/dist/file-diff.d.ts +31 -0
  62. package/dist/file-diff.d.ts.map +1 -0
  63. package/dist/file-diff.js +196 -0
  64. package/dist/file-diff.js.map +1 -0
  65. package/dist/index.d.ts +23 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +17 -4
  68. package/dist/index.js.map +1 -1
  69. package/dist/mode/agent-step-loop.d.ts +13 -0
  70. package/dist/mode/agent-step-loop.d.ts.map +1 -0
  71. package/dist/mode/agent-step-loop.js +578 -0
  72. package/dist/mode/agent-step-loop.js.map +1 -0
  73. package/dist/mode/checkpoint-gate.d.ts +25 -0
  74. package/dist/mode/checkpoint-gate.d.ts.map +1 -0
  75. package/dist/mode/checkpoint-gate.js +150 -0
  76. package/dist/mode/checkpoint-gate.js.map +1 -0
  77. package/dist/mode/checkpoint.d.ts +6 -0
  78. package/dist/mode/checkpoint.d.ts.map +1 -1
  79. package/dist/mode/collect-stream.d.ts +2 -0
  80. package/dist/mode/collect-stream.d.ts.map +1 -1
  81. package/dist/mode/collect-stream.js +1 -1
  82. package/dist/mode/collect-stream.js.map +1 -1
  83. package/dist/mode/project-messages.d.ts +3 -1
  84. package/dist/mode/project-messages.d.ts.map +1 -1
  85. package/dist/mode/project-messages.js +122 -81
  86. package/dist/mode/project-messages.js.map +1 -1
  87. package/dist/mode/react-loop-options.d.ts +113 -0
  88. package/dist/mode/react-loop-options.d.ts.map +1 -0
  89. package/dist/mode/react-loop-options.js +2 -0
  90. package/dist/mode/react-loop-options.js.map +1 -0
  91. package/dist/mode/react-loop.d.ts +2 -99
  92. package/dist/mode/react-loop.d.ts.map +1 -1
  93. package/dist/mode/react-loop.js +7 -145
  94. package/dist/mode/react-loop.js.map +1 -1
  95. package/dist/mode.d.ts +19 -7
  96. package/dist/mode.d.ts.map +1 -1
  97. package/dist/mode.js +1 -1
  98. package/dist/mode.js.map +1 -1
  99. package/dist/npm-registry.d.ts +22 -0
  100. package/dist/npm-registry.d.ts.map +1 -0
  101. package/dist/npm-registry.js +54 -0
  102. package/dist/npm-registry.js.map +1 -0
  103. package/dist/permission.d.ts +31 -0
  104. package/dist/permission.d.ts.map +1 -1
  105. package/dist/process-invocation.d.ts +2 -0
  106. package/dist/process-invocation.d.ts.map +1 -1
  107. package/dist/process-invocation.js +2 -2
  108. package/dist/process-invocation.js.map +1 -1
  109. package/dist/process-tree.d.ts +9 -0
  110. package/dist/process-tree.d.ts.map +1 -0
  111. package/dist/process-tree.js +40 -0
  112. package/dist/process-tree.js.map +1 -0
  113. package/dist/provider-login-bridge.js +1 -1
  114. package/dist/provider-login-bridge.js.map +1 -1
  115. package/dist/provider.d.ts +408 -0
  116. package/dist/provider.d.ts.map +1 -1
  117. package/dist/question-draft.d.ts +19 -0
  118. package/dist/question-draft.d.ts.map +1 -0
  119. package/dist/question-draft.js +30 -0
  120. package/dist/question-draft.js.map +1 -0
  121. package/dist/question.d.ts +204 -0
  122. package/dist/question.d.ts.map +1 -0
  123. package/dist/question.js +141 -0
  124. package/dist/question.js.map +1 -0
  125. package/dist/questions.d.ts +7 -0
  126. package/dist/questions.d.ts.map +1 -0
  127. package/dist/questions.js +7 -0
  128. package/dist/questions.js.map +1 -0
  129. package/dist/resolvers.d.ts +14 -1
  130. package/dist/resolvers.d.ts.map +1 -1
  131. package/dist/resolvers.js +33 -21
  132. package/dist/resolvers.js.map +1 -1
  133. package/dist/schemas.d.ts +53 -53
  134. package/dist/schemas.d.ts.map +1 -1
  135. package/dist/schemas.js +3 -2
  136. package/dist/schemas.js.map +1 -1
  137. package/dist/server.d.ts +2 -0
  138. package/dist/server.d.ts.map +1 -1
  139. package/dist/server.js +3 -0
  140. package/dist/server.js.map +1 -1
  141. package/dist/session-like.d.ts +19 -1
  142. package/dist/session-like.d.ts.map +1 -1
  143. package/dist/starting-model.d.ts +12 -0
  144. package/dist/starting-model.d.ts.map +1 -0
  145. package/dist/starting-model.js +9 -0
  146. package/dist/starting-model.js.map +1 -0
  147. package/dist/tool-dispatch.d.ts.map +1 -1
  148. package/dist/tool-dispatch.js +10 -4
  149. package/dist/tool-dispatch.js.map +1 -1
  150. package/dist/tool-display.d.ts +79 -3
  151. package/dist/tool-display.d.ts.map +1 -1
  152. package/dist/tool-display.js +117 -3
  153. package/dist/tool-display.js.map +1 -1
  154. package/dist/tool-modes.d.ts +1 -1
  155. package/dist/tool-modes.d.ts.map +1 -1
  156. package/dist/tool-modes.js +8 -2
  157. package/dist/tool-modes.js.map +1 -1
  158. package/dist/tool-output.d.ts +15 -0
  159. package/dist/tool-output.d.ts.map +1 -0
  160. package/dist/tool-output.js +39 -0
  161. package/dist/tool-output.js.map +1 -0
  162. package/dist/tool.d.ts +14 -1
  163. package/dist/tool.d.ts.map +1 -1
  164. package/dist/tunnel.d.ts +3 -3
  165. package/dist/tunnel.d.ts.map +1 -1
  166. package/dist/tunnel.js +2 -2
  167. package/dist/workflow-approval.d.ts +9 -9
  168. package/dist/workflow-approval.d.ts.map +1 -1
  169. package/dist/workflow-approval.js +1 -1
  170. package/dist/workflow-approval.js.map +1 -1
  171. package/package.json +37 -5
  172. package/src/agent-account-limit.test.ts +87 -0
  173. package/src/agent-account-limit.ts +89 -0
  174. package/src/agent-computer-use.test.ts +26 -0
  175. package/src/agent-computer-use.ts +29 -0
  176. package/src/agent-context-window.ts +51 -0
  177. package/src/agent-no-reply.ts +16 -0
  178. package/src/agent-step.ts +26 -0
  179. package/src/agent-work.ts +102 -0
  180. package/src/attachment-payload.test.ts +165 -0
  181. package/src/attachment-payload.ts +187 -0
  182. package/src/auto-approve.test.ts +13 -1
  183. package/src/auto-approve.ts +19 -0
  184. package/src/channel-auth.ts +3 -3
  185. package/src/channel.ts +1 -1
  186. package/src/client-session.ts +9 -2
  187. package/src/computer-control.ts +1 -1
  188. package/src/conversation-history.test.ts +184 -0
  189. package/src/conversation-history.ts +272 -0
  190. package/src/cron.test.ts +280 -0
  191. package/src/cron.ts +355 -0
  192. package/src/define.test.ts +1 -1
  193. package/src/define.ts +3 -1
  194. package/src/event-store.ts +2 -1
  195. package/src/events.ts +23 -1
  196. package/src/file-diff.test.ts +103 -0
  197. package/src/file-diff.ts +208 -0
  198. package/src/index.ts +140 -1
  199. package/src/json-file-store.test.ts +1 -1
  200. package/src/loop-helpers.test.ts +15 -0
  201. package/src/mode/agent-step-loop.ts +645 -0
  202. package/src/mode/checkpoint-gate.ts +198 -0
  203. package/src/mode/checkpoint.ts +6 -0
  204. package/src/mode/collect-stream.ts +1 -1
  205. package/src/mode/project-messages.test.ts +118 -1
  206. package/src/mode/project-messages.ts +136 -77
  207. package/src/mode/react-loop-options.ts +124 -0
  208. package/src/mode/react-loop.ts +10 -296
  209. package/src/mode.ts +19 -7
  210. package/src/npm-registry.test.ts +95 -0
  211. package/src/npm-registry.ts +57 -0
  212. package/src/permission.ts +30 -0
  213. package/src/process-invocation.test.ts +25 -0
  214. package/src/process-invocation.ts +8 -3
  215. package/src/process-tree.ts +37 -0
  216. package/src/provider-login-bridge.ts +1 -1
  217. package/src/provider-schema-constraints.test.ts +1 -1
  218. package/src/provider-utils.test.ts +1 -1
  219. package/src/provider.ts +346 -0
  220. package/src/question-draft.test.ts +57 -0
  221. package/src/question-draft.ts +47 -0
  222. package/src/question.test.ts +130 -0
  223. package/src/question.ts +229 -0
  224. package/src/questions.ts +6 -0
  225. package/src/resolvers.test.ts +84 -1
  226. package/src/resolvers.ts +40 -27
  227. package/src/schemas.test.ts +5 -0
  228. package/src/schemas.ts +3 -2
  229. package/src/server.ts +3 -0
  230. package/src/session-like.ts +19 -1
  231. package/src/skill-mentions.test.ts +1 -1
  232. package/src/starting-model.test.ts +19 -0
  233. package/src/starting-model.ts +11 -0
  234. package/src/token-efficiency.test.ts +1 -1
  235. package/src/tool-dispatch.ts +10 -4
  236. package/src/tool-display.test.ts +119 -0
  237. package/src/tool-display.ts +162 -4
  238. package/src/tool-gating.test.ts +1 -1
  239. package/src/tool-icon.test.ts +1 -1
  240. package/src/tool-modes.test.ts +18 -1
  241. package/src/tool-modes.ts +6 -2
  242. package/src/tool-output.test.ts +46 -0
  243. package/src/tool-output.ts +46 -0
  244. package/src/tool.ts +14 -1
  245. package/src/tunnel.ts +3 -3
  246. package/src/workflow-approval.ts +1 -1
@@ -0,0 +1,645 @@
1
+ import { AGENT_ACCOUNT_LIMIT_SUBTYPE, recordedAccountLimit, sameAccountLimit, withKnownReset } from '../agent-account-limit.js';
2
+ import { AGENT_CONTEXT_WINDOW_SUBTYPE, isContextWindowSize, recordedContextWindows } from '../agent-context-window.js';
3
+ import { AGENT_NO_REPLY_SUBTYPE } from '../agent-no-reply.js';
4
+ import { agentActivityRecord, type SubagentNames } from '../agent-work.js';
5
+ import { AGENT_CONNECTION_PLUGIN_ID, AGENT_GOAL_SUBTYPE, AGENT_SESSION_SUBTYPE, SUBAGENTS_PLUGIN_ID, agentSessionFromEvents } from '../agent-step.js';
6
+ import {
7
+ AUTO_APPROVE_PLUGIN_ID,
8
+ AUTO_APPROVE_RESTORED_SUBTYPE,
9
+ AUTO_APPROVE_SUBTYPE,
10
+ autoApproveFromAgent,
11
+ autoApproveFromEvents,
12
+ autoApproveRecorded,
13
+ } from '../auto-approve.js';
14
+ import { renderEarlierConversation, renderMissedTurns } from '../conversation-history.js';
15
+ import { computeElisionState } from '../elision-state.js';
16
+ import type { MoxxyEvent, UserPromptEvent } from '../events.js';
17
+ import { asToolCallId } from '../ids.js';
18
+ import type { ModeContext } from '../mode.js';
19
+ import type {
20
+ AgentGoalStatus,
21
+ AgentApprovalDecision,
22
+ AgentApprovalRequest,
23
+ AgentStepHost,
24
+ AgentToolCall,
25
+ AgentToolOutcome,
26
+ ContentBlock,
27
+ TokenUsage,
28
+ } from '../provider.js';
29
+ import type { ToolDef } from '../tool.js';
30
+ import type { StopReason } from '../provider-utils.js';
31
+ import { usageEventFields } from '../token-accounting.js';
32
+ import { dispatchToolCall } from '../tool-dispatch.js';
33
+ import { addUsage, type CollectedToolUse } from './collect-stream.js';
34
+ import { emitAbort, emitError, runCheckpointGate } from './checkpoint-gate.js';
35
+ import { projectUserPrompt } from './project-messages.js';
36
+ import type { ReactLoopOptions } from './react-loop-options.js';
37
+
38
+ const DEFAULT_MAX_ITERATIONS = 500;
39
+ const DEFAULT_MAX_INJECTIONS = 3;
40
+ const DEFAULT_MAX_INJECT_CHARS = 16_384;
41
+
42
+ /**
43
+ * The loop for an agent connection: each round is one turn of the vendor's
44
+ * agent (`provider.agentStep`), recorded as ordinary events. The mode's policy
45
+ * still applies: `onProviderSuccess` after the turn, `onToolBatchEnd` after the
46
+ * Moxxy tools it called, and the checkpoints that opted into agent
47
+ * connections at the turn-end. Moxxy's own end-of-turn reminders stay out:
48
+ * the agent decides when its turn is done.
49
+ */
50
+ export async function* runAgentStepLoop(ctx: ModeContext, opts: ReactLoopOptions): AsyncGenerator<MoxxyEvent, void> {
51
+ if (ctx.isSubagent || turnOrigin(ctx) !== 'agent') {
52
+ yield* runAgentStepRounds(ctx, opts, {});
53
+ return;
54
+ }
55
+ // A turn the agent took up by itself that shows nothing would leave its "Agent continued" marker alone.
56
+ const end: TurnEnd = {};
57
+ let shown = false;
58
+ for await (const event of runAgentStepRounds(ctx, opts, end)) {
59
+ shown ||= showsInChat(event);
60
+ yield event;
61
+ }
62
+ if (shown || end.limitReached) return;
63
+ yield await ctx.emit({
64
+ type: 'plugin_event',
65
+ sessionId: ctx.sessionId,
66
+ turnId: ctx.turnId,
67
+ source: 'system',
68
+ pluginId: AGENT_CONNECTION_PLUGIN_ID,
69
+ subtype: AGENT_NO_REPLY_SUBTYPE,
70
+ payload: {},
71
+ });
72
+ }
73
+
74
+ interface TurnEnd {
75
+ /** The turn stopped on a used-up account limit, which the chat already shows. */
76
+ limitReached?: true;
77
+ }
78
+
79
+ /** Whether the chat shows the event as something the turn did: its words, a tool or helper it ran, or why it stopped. */
80
+ function showsInChat(event: MoxxyEvent): boolean {
81
+ switch (event.type) {
82
+ case 'assistant_chunk':
83
+ return event.delta.trim() !== '';
84
+ case 'assistant_message':
85
+ return event.content.trim() !== '';
86
+ case 'tool_call_requested':
87
+ case 'error':
88
+ case 'abort':
89
+ return true;
90
+ case 'plugin_event':
91
+ return event.pluginId === SUBAGENTS_PLUGIN_ID;
92
+ default:
93
+ return false;
94
+ }
95
+ }
96
+
97
+ async function* runAgentStepRounds(ctx: ModeContext, opts: ReactLoopOptions, end: TurnEnd): AsyncGenerator<MoxxyEvent, void> {
98
+ const agentStep = ctx.provider.agentStep;
99
+ if (!agentStep) return;
100
+ if (opts.preflightAbortReason && ctx.signal.aborted) {
101
+ yield await emitAbort(ctx, opts.preflightAbortReason);
102
+ return;
103
+ }
104
+ const checkpoints = ctx.isSubagent ? [] : (opts.checkpoints ?? []).filter((cp) => cp.agentConnections === true);
105
+ const injectionBudget = Math.max(0, Math.floor(opts.maxInjections ?? DEFAULT_MAX_INJECTIONS));
106
+ const maxInjectChars = Math.max(1024, Math.floor(opts.maxInjectChars ?? DEFAULT_MAX_INJECT_CHARS));
107
+ const maxIterations =
108
+ typeof ctx.maxIterations === 'number' && Number.isFinite(ctx.maxIterations)
109
+ ? Math.max(1, Math.floor(ctx.maxIterations))
110
+ : (opts.defaultMaxIterations ?? DEFAULT_MAX_ITERATIONS);
111
+ let prompt = turnPrompt(ctx);
112
+ // A turn the agent started by itself: its first round reads the agent's turn, later rounds send as usual.
113
+ const agentsOwnTurn = turnOrigin(ctx) === 'agent';
114
+ let injectionsUsed = 0;
115
+ let consecutiveIdle = 0;
116
+
117
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
118
+ if (ctx.signal.aborted) {
119
+ yield await emitAbort(ctx, 'signal aborted');
120
+ return;
121
+ }
122
+ yield await ctx.emit({
123
+ type: 'mode_iteration',
124
+ sessionId: ctx.sessionId,
125
+ turnId: ctx.turnId,
126
+ source: 'system',
127
+ strategy: opts.strategyName,
128
+ iteration,
129
+ });
130
+ yield await ctx.emit({
131
+ type: 'provider_request',
132
+ sessionId: ctx.sessionId,
133
+ turnId: ctx.turnId,
134
+ source: 'system',
135
+ provider: ctx.provider.name,
136
+ model: ctx.model,
137
+ });
138
+
139
+ const round = await runRound(ctx, agentStep.bind(ctx.provider), prompt, iteration, opts.strategyName, agentsOwnTurn && iteration === 1);
140
+ yield* round.events;
141
+ if (round.limitReached) end.limitReached = true;
142
+
143
+ if (round.endsTurn !== undefined) {
144
+ yield await ctx.emit({
145
+ type: 'assistant_message',
146
+ sessionId: ctx.sessionId,
147
+ turnId: ctx.turnId,
148
+ source: 'system',
149
+ stopReason: 'end_turn',
150
+ content: round.endsTurn,
151
+ });
152
+ return;
153
+ }
154
+ if (ctx.signal.aborted) {
155
+ yield await emitAbort(ctx, 'signal aborted during the agent turn');
156
+ return;
157
+ }
158
+ if (round.error) {
159
+ // A reached account limit is already recorded and says why the agent stopped.
160
+ if (!round.limitReached) yield await emitError(ctx, 'fatal', `${opts.errorPrefix ?? ''}${round.error}`);
161
+ return;
162
+ }
163
+
164
+ if (opts.onProviderSuccess) {
165
+ const directive = await opts.onProviderSuccess(ctx, {
166
+ text: round.lastText,
167
+ stopReason: round.stopReason,
168
+ toolUses: [],
169
+ ...(round.usage ? { usage: round.usage } : {}),
170
+ iteration,
171
+ });
172
+ if (directive?.action === 'stop') return;
173
+ }
174
+ if (round.goal && opts.onAgentGoal) {
175
+ const directive = await opts.onAgentGoal(ctx, round.goal);
176
+ if (directive?.action === 'stop') return;
177
+ }
178
+ if (round.plan !== undefined) {
179
+ if (opts.onAgentPlan) {
180
+ const directive = await opts.onAgentPlan(ctx, round.plan);
181
+ if (directive?.action === 'stop') return;
182
+ } else {
183
+ yield await ctx.emit({
184
+ type: 'assistant_message',
185
+ sessionId: ctx.sessionId,
186
+ turnId: ctx.turnId,
187
+ source: 'model',
188
+ content: round.plan,
189
+ stopReason: 'end_turn',
190
+ });
191
+ }
192
+ }
193
+ if (round.moxxyTools.length > 0 && opts.onToolBatchEnd) {
194
+ const directive = await opts.onToolBatchEnd(ctx, { toolUses: round.moxxyTools, iteration });
195
+ if (directive?.action === 'stop') return;
196
+ }
197
+ // A round where the agent ran tools, its own or Moxxy's, is work, not idling.
198
+ if (round.moxxyTools.length > 0 || round.ranTools) {
199
+ consecutiveIdle = 0;
200
+ injectionsUsed = 0;
201
+ } else {
202
+ consecutiveIdle += 1;
203
+ }
204
+
205
+ const promptsBefore = ctx.log.ofType('user_prompt').length;
206
+ const verdict = yield* runCheckpointGate(ctx, checkpoints, {
207
+ candidateText: round.lastText,
208
+ stopReason: round.stopReason,
209
+ iteration,
210
+ consecutiveIdle,
211
+ injectionsUsed,
212
+ injectionBudget,
213
+ maxInjectChars,
214
+ });
215
+ if (verdict.kind === 'end') return;
216
+ injectionsUsed += 1;
217
+ // A persistent injection is a checkpoint user_prompt in the log; a volatile one rides this call only.
218
+ const injected = ctx.log.ofType('user_prompt').slice(promptsBefore).at(-1);
219
+ const follow = verdict.volatileText ?? (injected ? injected.text : undefined);
220
+ prompt = follow ? [{ type: 'text', text: follow }] : [{ type: 'text', text: 'Continue.' }];
221
+ }
222
+
223
+ if (opts.onMaxIterations) {
224
+ await opts.onMaxIterations(ctx, maxIterations);
225
+ return;
226
+ }
227
+ yield await emitError(ctx, 'fatal', `${opts.strategyName} mode loop exceeded maxIterations (${maxIterations})`);
228
+ }
229
+
230
+ interface Round {
231
+ readonly events: ReadonlyArray<MoxxyEvent>;
232
+ readonly lastText: string;
233
+ readonly stopReason: StopReason;
234
+ readonly usage?: TokenUsage;
235
+ readonly error?: string;
236
+ /** Moxxy tools the agent called over MCP, for the mode's `onToolBatchEnd`. */
237
+ readonly moxxyTools: ReadonlyArray<CollectedToolUse>;
238
+ /** The agent ran tools of its own. */
239
+ readonly ranTools: boolean;
240
+ /** Set when the person refused a tool whose refusal ends the turn: its text. */
241
+ readonly endsTurn?: string;
242
+ readonly goal?: AgentGoalStatus;
243
+ readonly plan?: string;
244
+ /** The agent reported its account limit reached: the turn ends on that, not on the error it came with. */
245
+ readonly limitReached?: true;
246
+ }
247
+
248
+ async function runRound(
249
+ ctx: ModeContext,
250
+ agentStep: NonNullable<ModeContext['provider']['agentStep']>,
251
+ prompt: ReadonlyArray<ContentBlock>,
252
+ iteration: number,
253
+ mode: string,
254
+ continuesAgentTurn: boolean,
255
+ ): Promise<Round> {
256
+ const events: MoxxyEvent[] = [];
257
+ const emit = async (event: Parameters<ModeContext['emit']>[0]) => {
258
+ events.push(await ctx.emit(event));
259
+ };
260
+ const base = { sessionId: ctx.sessionId, turnId: ctx.turnId };
261
+ const moxxyTools: CollectedToolUse[] = [];
262
+ const open = new Set<string>();
263
+ // Ending the turn on a refusal stops the agent without it counting as the person's Stop.
264
+ const stopAgent = new AbortController();
265
+ const signal = AbortSignal.any([ctx.signal, stopAgent.signal]);
266
+ let endsTurn: string | undefined;
267
+
268
+ const host: AgentStepHost = {
269
+ runTool: async (call) => {
270
+ moxxyTools.push({ id: call.callId, name: call.name, input: call.input });
271
+ await emit({ ...base, type: 'tool_call_requested', source: 'model', callId: asToolCallId(call.callId), name: call.name, input: call.input });
272
+ const outcome = await runMoxxyTool(ctx, call, iteration, mode, (event) => events.push(event));
273
+ if (!outcome.ok && outcome.endsTurn) {
274
+ endsTurn = ctx.tools.get(call.name)?.refusalEndsTurn;
275
+ stopAgent.abort();
276
+ }
277
+ return outcome;
278
+ },
279
+ approve: (request) => approveAgentTool(ctx, request, iteration, (event) => events.push(event)),
280
+ ask: (questions, opts) => (ctx.ask ? ctx.ask(questions, opts) : Promise.resolve({ kind: 'unanswerable' as const })),
281
+ };
282
+
283
+ const resumeSessionId = agentSessionFromEvents(ctx.log.ofType('plugin_event'), ctx.provider.name);
284
+ let autoApprove = autoApproveFromEvents(ctx.log.ofType('plugin_event'));
285
+ let lastText = '';
286
+ let stopReason: StopReason = 'end_turn';
287
+ let usage: TokenUsage | undefined;
288
+ // How full the agent's context is: what it says, else the prompt of its last call.
289
+ let contextTokens: number | undefined;
290
+ let reportsContext = false;
291
+ let error: string | undefined;
292
+ let sessionId = resumeSessionId;
293
+ let goal: AgentGoalStatus | undefined;
294
+ let plan: string | undefined;
295
+ let limit = recordedAccountLimit(ctx.log.ofType('plugin_event'), ctx.provider.name)?.limit;
296
+ let limitReached = false;
297
+ const windows = new Map(recordedContextWindows(ctx.log.ofType('plugin_event'), ctx.provider.name));
298
+ /** The agent's own subagents started in this step, to name them in their later events (one started in an earlier turn goes unnamed). */
299
+ const subagents: SubagentNames = new Map();
300
+ let ranTools = false;
301
+
302
+ try {
303
+ const stream = agentStep({
304
+ sessionId: String(ctx.sessionId),
305
+ model: ctx.model,
306
+ prompt,
307
+ cwd: ctx.cwd,
308
+ mode,
309
+ tools: ctx.tools.list(),
310
+ allTools: everyTool(ctx),
311
+ skills: ctx.skills.list(),
312
+ ...(resumeSessionId ? { resumeSessionId } : {}),
313
+ earlierConversation: () => renderEarlierConversation(ctx.log, ctx.turnId),
314
+ missedTurns: () => renderMissedTurns(ctx.log, ctx.provider.name, ctx.turnId),
315
+ autoApprove,
316
+ autoApproveRecorded: autoApproveRecorded(ctx.log.ofType('plugin_event')),
317
+ autoApproveFromAgent: autoApproveFromAgent(ctx.log.ofType('plugin_event')),
318
+ ...(ctx.reasoning ? { reasoning: ctx.reasoning } : {}),
319
+ ...(ctx.fast ? { fast: true } : {}),
320
+ signal,
321
+ host,
322
+ ...(continuesAgentTurn ? { continuesAgentTurn: true as const } : {}),
323
+ });
324
+ for await (const event of stream) {
325
+ switch (event.type) {
326
+ case 'text_delta':
327
+ await emit({ ...base, type: 'assistant_chunk', source: 'model', delta: event.delta });
328
+ break;
329
+ case 'reasoning_delta':
330
+ await emit({ ...base, type: 'reasoning_chunk', source: 'model', delta: event.delta });
331
+ break;
332
+ case 'message':
333
+ lastText = event.text;
334
+ stopReason = event.stopReason ?? 'end_turn';
335
+ await emit({ ...base, type: 'assistant_message', source: 'model', content: event.text, stopReason });
336
+ break;
337
+ case 'reasoning':
338
+ await emit({ ...base, type: 'reasoning_message', source: 'model', content: event.text });
339
+ break;
340
+ case 'tool_call':
341
+ open.add(event.callId);
342
+ ranTools = true;
343
+ await emit({
344
+ ...base,
345
+ type: 'tool_call_requested',
346
+ source: 'model',
347
+ callId: asToolCallId(event.callId),
348
+ name: event.name,
349
+ input: event.input,
350
+ executedBy: 'provider',
351
+ });
352
+ break;
353
+ case 'tool_result':
354
+ open.delete(event.callId);
355
+ await emit({
356
+ ...base,
357
+ type: 'tool_result',
358
+ source: 'tool',
359
+ callId: asToolCallId(event.callId),
360
+ ok: event.ok,
361
+ ...(event.ok ? { output: event.output } : { error: { kind: 'threw' as const, message: event.error ?? 'the tool failed' } }),
362
+ });
363
+ break;
364
+ case 'session':
365
+ if (event.sessionId !== sessionId) {
366
+ sessionId = event.sessionId;
367
+ await emit({
368
+ ...base,
369
+ type: 'plugin_event',
370
+ source: 'system',
371
+ pluginId: AGENT_CONNECTION_PLUGIN_ID,
372
+ subtype: AGENT_SESSION_SUBTYPE,
373
+ payload: { provider: ctx.provider.name, sessionId: event.sessionId },
374
+ });
375
+ }
376
+ break;
377
+ case 'auto_approve':
378
+ if (event.enabled !== autoApprove) {
379
+ autoApprove = event.enabled;
380
+ await emit({
381
+ ...base,
382
+ type: 'plugin_event',
383
+ source: 'system',
384
+ pluginId: AUTO_APPROVE_PLUGIN_ID,
385
+ subtype: AUTO_APPROVE_SUBTYPE,
386
+ payload: { enabled: event.enabled, ...(event.elsewhere ? { elsewhere: true } : {}) },
387
+ });
388
+ }
389
+ break;
390
+ case 'auto_approve_restored':
391
+ await emit({
392
+ ...base,
393
+ type: 'plugin_event',
394
+ source: 'system',
395
+ pluginId: AUTO_APPROVE_PLUGIN_ID,
396
+ subtype: AUTO_APPROVE_RESTORED_SUBTYPE,
397
+ payload: { provider: ctx.provider.name },
398
+ });
399
+ break;
400
+ case 'goal':
401
+ goal = event.status;
402
+ await emit({
403
+ ...base,
404
+ type: 'plugin_event',
405
+ source: 'system',
406
+ pluginId: AGENT_CONNECTION_PLUGIN_ID,
407
+ subtype: AGENT_GOAL_SUBTYPE,
408
+ payload: { provider: ctx.provider.name, status: event.status },
409
+ });
410
+ break;
411
+ case 'plan':
412
+ plan = event.text;
413
+ break;
414
+ case 'account_limit': {
415
+ if (event.limit.state === 'reached') limitReached = true;
416
+ const reading = withKnownReset(event.limit, limit, Date.now());
417
+ if (sameAccountLimit(limit, reading)) break;
418
+ limit = reading;
419
+ await emit({
420
+ ...base,
421
+ type: 'plugin_event',
422
+ source: 'system',
423
+ pluginId: AGENT_CONNECTION_PLUGIN_ID,
424
+ subtype: AGENT_ACCOUNT_LIMIT_SUBTYPE,
425
+ payload: { provider: ctx.provider.name, ...(ctx.provider.agentName ? { agent: ctx.provider.agentName } : {}), limit: reading },
426
+ });
427
+ break;
428
+ }
429
+ case 'context_window':
430
+ if (!isContextWindowSize(event.tokens) || windows.get(event.model) === event.tokens) break;
431
+ windows.set(event.model, event.tokens);
432
+ await emit({
433
+ ...base,
434
+ type: 'plugin_event',
435
+ source: 'system',
436
+ pluginId: AGENT_CONNECTION_PLUGIN_ID,
437
+ subtype: AGENT_CONTEXT_WINDOW_SUBTYPE,
438
+ payload: { provider: ctx.provider.name, model: event.model, tokens: event.tokens },
439
+ });
440
+ break;
441
+ case 'subagent_started':
442
+ case 'subagent_tool_call':
443
+ case 'subagent_message':
444
+ case 'subagent_completed':
445
+ case 'background_work':
446
+ case 'background_work_ended': {
447
+ const record = agentActivityRecord(event, ctx.provider.name, subagents);
448
+ if (record) await emit({ ...base, type: 'plugin_event', ...record });
449
+ break;
450
+ }
451
+ case 'scheduled_wakeups':
452
+ // What the agent plans now: the session holds it from the agent's activity, the log keeps nothing.
453
+ break;
454
+ case 'usage':
455
+ usage = usage ? addUsage(usage, event.usage) : event.usage;
456
+ if (!reportsContext) contextTokens = promptTokens(event.usage);
457
+ break;
458
+ case 'context_used':
459
+ reportsContext = true;
460
+ contextTokens = event.tokens;
461
+ break;
462
+ case 'error':
463
+ error = event.message;
464
+ break;
465
+ }
466
+ }
467
+ } catch (err) {
468
+ if (!signal.aborted) error = err instanceof Error ? err.message : String(err);
469
+ }
470
+
471
+ // The log never ends on a tool call without its result.
472
+ for (const callId of open) {
473
+ await emit({
474
+ ...base,
475
+ type: 'tool_result',
476
+ source: 'tool',
477
+ callId: asToolCallId(callId),
478
+ ok: false,
479
+ error: signal.aborted
480
+ ? { kind: 'aborted', message: 'turn stopped before the agent finished this tool' }
481
+ : { kind: 'threw', message: 'the agent did not report how this tool ended' },
482
+ });
483
+ }
484
+ await emit({
485
+ ...base,
486
+ type: 'provider_response',
487
+ source: 'system',
488
+ provider: ctx.provider.name,
489
+ model: ctx.model,
490
+ ...usageEventFields(usage),
491
+ ...(usage && contextTokens !== undefined ? { contextTokens } : {}),
492
+ });
493
+
494
+ return {
495
+ events,
496
+ lastText,
497
+ stopReason,
498
+ ...(usage ? { usage } : {}),
499
+ ...(error !== undefined ? { error } : {}),
500
+ moxxyTools,
501
+ ranTools,
502
+ ...(endsTurn !== undefined ? { endsTurn } : {}),
503
+ ...(goal ? { goal } : {}),
504
+ ...(plan !== undefined ? { plan } : {}),
505
+ ...(limitReached ? { limitReached: true as const } : {}),
506
+ };
507
+ }
508
+
509
+ function notOfferedHere(name: string, mode: string): string {
510
+ const question = name === 'ask_user' ? ' Nobody is there to answer questions: pick the sensible choice yourself and say which one you picked.' : '';
511
+ return `${name} is not available in ${mode} mode, so Moxxy did not run it. Carry on without it.${question}`;
512
+ }
513
+
514
+ /** The session's tools in any mode, with those this turn's mode adds for itself (plan mode's `plan_complete`). */
515
+ function everyTool(ctx: ModeContext): ToolDef[] {
516
+ const turn = ctx.tools.list();
517
+ if (!ctx.allTools) return [...turn];
518
+ const names = new Set(turn.map((tool) => tool.name));
519
+ return [...ctx.allTools.list().filter((tool) => !names.has(tool.name)), ...turn];
520
+ }
521
+
522
+ /** A Moxxy tool the agent called: the same hooks, permissions, run and log as any tool call. */
523
+ async function runMoxxyTool(
524
+ ctx: ModeContext,
525
+ call: AgentToolCall,
526
+ iteration: number,
527
+ mode: string,
528
+ record: (event: MoxxyEvent) => void,
529
+ ): Promise<AgentToolOutcome> {
530
+ // The agent sees every tool of the conversation (it reads the list once); this mode may not offer this one.
531
+ if (ctx.allTools?.get(call.name) && !ctx.tools.get(call.name)) {
532
+ const message = notOfferedHere(call.name, mode);
533
+ record(
534
+ await ctx.emit({
535
+ sessionId: ctx.sessionId,
536
+ turnId: ctx.turnId,
537
+ type: 'tool_result',
538
+ source: 'tool',
539
+ callId: asToolCallId(call.callId),
540
+ ok: false,
541
+ error: { kind: 'denied', message },
542
+ }),
543
+ );
544
+ return { ok: false, message };
545
+ }
546
+ let result: MoxxyEvent | undefined;
547
+ let refused = false;
548
+ for await (const event of dispatchToolCall(ctx, { id: call.callId, name: call.name, input: call.input }, iteration)) {
549
+ record(event);
550
+ if (event.type === 'tool_call_denied' && event.decidedBy === 'resolver') refused = true;
551
+ if (event.type === 'tool_result') result = event;
552
+ }
553
+ if (result && result.type === 'tool_result' && result.ok) return { ok: true, output: result.output };
554
+ const message = result && result.type === 'tool_result' && result.error ? result.error.message : 'the tool failed';
555
+ const endsTurn = refused && ctx.tools.get(call.name)?.refusalEndsTurn !== undefined;
556
+ return { ok: false, message, ...(endsTurn ? { endsTurn: true } : {}) };
557
+ }
558
+
559
+ /** May the agent run one of its own tools? Hooks, then the person (unless policy only), then standing rules. */
560
+ async function approveAgentTool(
561
+ ctx: ModeContext,
562
+ request: AgentApprovalRequest,
563
+ iteration: number,
564
+ record: (event: MoxxyEvent) => void,
565
+ ): Promise<AgentApprovalDecision> {
566
+ const call = { callId: asToolCallId(request.callId), name: request.name, input: request.input };
567
+ const scope = { sessionId: String(ctx.sessionId), turnId: String(ctx.turnId) };
568
+ const deny = async (reason: string, decidedBy: 'hook' | 'resolver' | 'policy'): Promise<AgentApprovalDecision> => {
569
+ record(
570
+ await ctx.emit({
571
+ sessionId: ctx.sessionId,
572
+ turnId: ctx.turnId,
573
+ type: 'tool_call_denied',
574
+ source: 'system',
575
+ callId: call.callId,
576
+ decidedBy,
577
+ reason,
578
+ }),
579
+ );
580
+ return { allow: false, reason };
581
+ };
582
+
583
+ const verdict = await ctx.hooks.dispatchToolCall({
584
+ sessionId: ctx.sessionId,
585
+ cwd: ctx.cwd,
586
+ log: ctx.log,
587
+ env: ctx.env,
588
+ services: ctx.services,
589
+ turnId: ctx.turnId,
590
+ iteration,
591
+ call,
592
+ });
593
+ if (verdict.action === 'deny') return deny(verdict.reason, 'hook');
594
+
595
+ if (!request.policyOnly) {
596
+ const decision = await ctx.permissions.check(call, {
597
+ ...scope,
598
+ toolDescription: request.description ?? ctx.tools.get(request.name)?.description,
599
+ ...(request.askPerson ? { askPerson: true } : {}),
600
+ });
601
+ if (decision.mode === 'deny') {
602
+ const standing = await ctx.permissions.policyCheck?.(call, scope);
603
+ return deny(decision.reason ?? 'denied by resolver', standing?.mode === 'deny' ? 'policy' : 'resolver');
604
+ }
605
+ const always = decision.mode !== 'allow';
606
+ const mandatory = await ctx.permissions.mandatoryCheck?.(call, scope);
607
+ if (mandatory?.mode === 'deny') return deny(mandatory.reason ?? 'workflow approval denied', 'resolver');
608
+ const policy = await ctx.permissions.policyCheck?.(call, scope);
609
+ if (policy?.mode === 'deny') return deny(policy.reason ?? 'denied by policy', 'policy');
610
+ record(
611
+ await ctx.emit({
612
+ sessionId: ctx.sessionId,
613
+ turnId: ctx.turnId,
614
+ type: 'tool_call_approved',
615
+ source: 'system',
616
+ callId: call.callId,
617
+ decidedBy: 'resolver',
618
+ mode: decision.mode,
619
+ ...(decision.decidedNow ? { decidedNow: true } : {}),
620
+ }),
621
+ );
622
+ return always ? { allow: true, always: true } : { allow: true };
623
+ }
624
+ const policy = await ctx.permissions.policyCheck?.(call, scope);
625
+ if (policy?.mode === 'deny') return deny(policy.reason ?? 'denied by policy', 'policy');
626
+ return { allow: true };
627
+ }
628
+
629
+ function turnOrigin(ctx: ModeContext): string | undefined {
630
+ const prompts = ctx.log.ofType('user_prompt') as ReadonlyArray<UserPromptEvent>;
631
+ const mine = prompts.filter((p) => p.turnId === ctx.turnId && p.origin?.kind !== 'checkpoint').at(-1);
632
+ return mine && mine.origin ? mine.origin.kind : undefined;
633
+ }
634
+
635
+ /** The person's message for this turn, as the agent should get it (text, images, documents). */
636
+ function turnPrompt(ctx: ModeContext): ReadonlyArray<ContentBlock> {
637
+ const prompts = ctx.log.ofType('user_prompt') as ReadonlyArray<UserPromptEvent>;
638
+ const mine = prompts.filter((p) => p.turnId === ctx.turnId).at(-1);
639
+ if (!mine) return [{ type: 'text', text: '' }];
640
+ return projectUserPrompt(mine, computeElisionState([]));
641
+ }
642
+
643
+ function promptTokens(usage: TokenUsage): number {
644
+ return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheCreationTokens ?? 0);
645
+ }