@moxxy/sdk 0.44.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 (242) 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 +1 -1
  26. package/dist/attachment-payload.js +1 -1
  27. package/dist/auto-approve.d.ts +9 -0
  28. package/dist/auto-approve.d.ts.map +1 -1
  29. package/dist/auto-approve.js +18 -0
  30. package/dist/auto-approve.js.map +1 -1
  31. package/dist/channel-auth.d.ts +3 -3
  32. package/dist/channel-auth.d.ts.map +1 -1
  33. package/dist/channel-auth.js +1 -1
  34. package/dist/channel.d.ts +1 -1
  35. package/dist/client-session.d.ts +9 -5
  36. package/dist/client-session.d.ts.map +1 -1
  37. package/dist/computer-control.d.ts +11 -11
  38. package/dist/computer-control.d.ts.map +1 -1
  39. package/dist/computer-control.js +1 -1
  40. package/dist/computer-control.js.map +1 -1
  41. package/dist/conversation-history.d.ts +50 -0
  42. package/dist/conversation-history.d.ts.map +1 -0
  43. package/dist/conversation-history.js +227 -0
  44. package/dist/conversation-history.js.map +1 -0
  45. package/dist/cron.d.ts +58 -0
  46. package/dist/cron.d.ts.map +1 -0
  47. package/dist/cron.js +320 -0
  48. package/dist/cron.js.map +1 -0
  49. package/dist/define.d.ts +2 -1
  50. package/dist/define.d.ts.map +1 -1
  51. package/dist/define.js +1 -0
  52. package/dist/define.js.map +1 -1
  53. package/dist/event-store.d.ts +1 -1
  54. package/dist/event-store.d.ts.map +1 -1
  55. package/dist/event-store.js +1 -0
  56. package/dist/event-store.js.map +1 -1
  57. package/dist/events.d.ts +17 -1
  58. package/dist/events.d.ts.map +1 -1
  59. package/dist/file-diff.d.ts +31 -0
  60. package/dist/file-diff.d.ts.map +1 -0
  61. package/dist/file-diff.js +196 -0
  62. package/dist/file-diff.js.map +1 -0
  63. package/dist/index.d.ts +22 -7
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +16 -4
  66. package/dist/index.js.map +1 -1
  67. package/dist/mode/agent-step-loop.d.ts +13 -0
  68. package/dist/mode/agent-step-loop.d.ts.map +1 -0
  69. package/dist/mode/agent-step-loop.js +578 -0
  70. package/dist/mode/agent-step-loop.js.map +1 -0
  71. package/dist/mode/checkpoint-gate.d.ts +25 -0
  72. package/dist/mode/checkpoint-gate.d.ts.map +1 -0
  73. package/dist/mode/checkpoint-gate.js +150 -0
  74. package/dist/mode/checkpoint-gate.js.map +1 -0
  75. package/dist/mode/checkpoint.d.ts +6 -0
  76. package/dist/mode/checkpoint.d.ts.map +1 -1
  77. package/dist/mode/collect-stream.d.ts +2 -0
  78. package/dist/mode/collect-stream.d.ts.map +1 -1
  79. package/dist/mode/collect-stream.js +1 -1
  80. package/dist/mode/collect-stream.js.map +1 -1
  81. package/dist/mode/project-messages.d.ts.map +1 -1
  82. package/dist/mode/project-messages.js +9 -67
  83. package/dist/mode/project-messages.js.map +1 -1
  84. package/dist/mode/react-loop-options.d.ts +113 -0
  85. package/dist/mode/react-loop-options.d.ts.map +1 -0
  86. package/dist/mode/react-loop-options.js +2 -0
  87. package/dist/mode/react-loop-options.js.map +1 -0
  88. package/dist/mode/react-loop.d.ts +2 -99
  89. package/dist/mode/react-loop.d.ts.map +1 -1
  90. package/dist/mode/react-loop.js +7 -145
  91. package/dist/mode/react-loop.js.map +1 -1
  92. package/dist/mode.d.ts +19 -7
  93. package/dist/mode.d.ts.map +1 -1
  94. package/dist/mode.js +1 -1
  95. package/dist/mode.js.map +1 -1
  96. package/dist/npm-registry.d.ts +22 -0
  97. package/dist/npm-registry.d.ts.map +1 -0
  98. package/dist/npm-registry.js +54 -0
  99. package/dist/npm-registry.js.map +1 -0
  100. package/dist/permission.d.ts +31 -0
  101. package/dist/permission.d.ts.map +1 -1
  102. package/dist/process-invocation.d.ts +2 -0
  103. package/dist/process-invocation.d.ts.map +1 -1
  104. package/dist/process-invocation.js +2 -2
  105. package/dist/process-invocation.js.map +1 -1
  106. package/dist/process-tree.d.ts +9 -0
  107. package/dist/process-tree.d.ts.map +1 -0
  108. package/dist/process-tree.js +40 -0
  109. package/dist/process-tree.js.map +1 -0
  110. package/dist/provider-login-bridge.js +1 -1
  111. package/dist/provider-login-bridge.js.map +1 -1
  112. package/dist/provider.d.ts +408 -0
  113. package/dist/provider.d.ts.map +1 -1
  114. package/dist/question-draft.d.ts +19 -0
  115. package/dist/question-draft.d.ts.map +1 -0
  116. package/dist/question-draft.js +30 -0
  117. package/dist/question-draft.js.map +1 -0
  118. package/dist/question.d.ts +204 -0
  119. package/dist/question.d.ts.map +1 -0
  120. package/dist/question.js +141 -0
  121. package/dist/question.js.map +1 -0
  122. package/dist/questions.d.ts +7 -0
  123. package/dist/questions.d.ts.map +1 -0
  124. package/dist/questions.js +7 -0
  125. package/dist/questions.js.map +1 -0
  126. package/dist/resolvers.d.ts +14 -1
  127. package/dist/resolvers.d.ts.map +1 -1
  128. package/dist/resolvers.js +33 -21
  129. package/dist/resolvers.js.map +1 -1
  130. package/dist/schemas.d.ts +53 -53
  131. package/dist/schemas.d.ts.map +1 -1
  132. package/dist/schemas.js +3 -2
  133. package/dist/schemas.js.map +1 -1
  134. package/dist/server.d.ts +2 -0
  135. package/dist/server.d.ts.map +1 -1
  136. package/dist/server.js +3 -0
  137. package/dist/server.js.map +1 -1
  138. package/dist/session-like.d.ts +19 -1
  139. package/dist/session-like.d.ts.map +1 -1
  140. package/dist/starting-model.d.ts +12 -0
  141. package/dist/starting-model.d.ts.map +1 -0
  142. package/dist/starting-model.js +9 -0
  143. package/dist/starting-model.js.map +1 -0
  144. package/dist/tool-dispatch.d.ts.map +1 -1
  145. package/dist/tool-dispatch.js +10 -4
  146. package/dist/tool-dispatch.js.map +1 -1
  147. package/dist/tool-display.d.ts +79 -3
  148. package/dist/tool-display.d.ts.map +1 -1
  149. package/dist/tool-display.js +117 -3
  150. package/dist/tool-display.js.map +1 -1
  151. package/dist/tool-modes.d.ts +1 -1
  152. package/dist/tool-modes.d.ts.map +1 -1
  153. package/dist/tool-modes.js +8 -2
  154. package/dist/tool-modes.js.map +1 -1
  155. package/dist/tool-output.d.ts +15 -0
  156. package/dist/tool-output.d.ts.map +1 -0
  157. package/dist/tool-output.js +39 -0
  158. package/dist/tool-output.js.map +1 -0
  159. package/dist/tool.d.ts +14 -1
  160. package/dist/tool.d.ts.map +1 -1
  161. package/dist/tunnel.d.ts +3 -3
  162. package/dist/tunnel.d.ts.map +1 -1
  163. package/dist/tunnel.js +2 -2
  164. package/dist/workflow-approval.d.ts +9 -9
  165. package/dist/workflow-approval.d.ts.map +1 -1
  166. package/dist/workflow-approval.js +1 -1
  167. package/dist/workflow-approval.js.map +1 -1
  168. package/package.json +33 -5
  169. package/src/agent-account-limit.test.ts +87 -0
  170. package/src/agent-account-limit.ts +89 -0
  171. package/src/agent-computer-use.test.ts +26 -0
  172. package/src/agent-computer-use.ts +29 -0
  173. package/src/agent-context-window.ts +51 -0
  174. package/src/agent-no-reply.ts +16 -0
  175. package/src/agent-step.ts +26 -0
  176. package/src/agent-work.ts +102 -0
  177. package/src/attachment-payload.ts +1 -1
  178. package/src/auto-approve.test.ts +13 -1
  179. package/src/auto-approve.ts +19 -0
  180. package/src/channel-auth.ts +3 -3
  181. package/src/channel.ts +1 -1
  182. package/src/client-session.ts +9 -2
  183. package/src/computer-control.ts +1 -1
  184. package/src/conversation-history.test.ts +184 -0
  185. package/src/conversation-history.ts +272 -0
  186. package/src/cron.test.ts +280 -0
  187. package/src/cron.ts +355 -0
  188. package/src/define.test.ts +1 -1
  189. package/src/define.ts +3 -1
  190. package/src/event-store.ts +2 -1
  191. package/src/events.ts +17 -1
  192. package/src/file-diff.test.ts +103 -0
  193. package/src/file-diff.ts +208 -0
  194. package/src/index.ts +121 -1
  195. package/src/json-file-store.test.ts +1 -1
  196. package/src/loop-helpers.test.ts +15 -0
  197. package/src/mode/agent-step-loop.ts +645 -0
  198. package/src/mode/checkpoint-gate.ts +198 -0
  199. package/src/mode/checkpoint.ts +6 -0
  200. package/src/mode/collect-stream.ts +1 -1
  201. package/src/mode/project-messages.test.ts +11 -0
  202. package/src/mode/project-messages.ts +8 -61
  203. package/src/mode/react-loop-options.ts +124 -0
  204. package/src/mode/react-loop.ts +10 -296
  205. package/src/mode.ts +19 -7
  206. package/src/npm-registry.test.ts +95 -0
  207. package/src/npm-registry.ts +57 -0
  208. package/src/permission.ts +30 -0
  209. package/src/process-invocation.test.ts +25 -0
  210. package/src/process-invocation.ts +8 -3
  211. package/src/process-tree.ts +37 -0
  212. package/src/provider-login-bridge.ts +1 -1
  213. package/src/provider-schema-constraints.test.ts +1 -1
  214. package/src/provider-utils.test.ts +1 -1
  215. package/src/provider.ts +346 -0
  216. package/src/question-draft.test.ts +57 -0
  217. package/src/question-draft.ts +47 -0
  218. package/src/question.test.ts +130 -0
  219. package/src/question.ts +229 -0
  220. package/src/questions.ts +6 -0
  221. package/src/resolvers.test.ts +84 -1
  222. package/src/resolvers.ts +40 -27
  223. package/src/schemas.test.ts +5 -0
  224. package/src/schemas.ts +3 -2
  225. package/src/server.ts +3 -0
  226. package/src/session-like.ts +19 -1
  227. package/src/skill-mentions.test.ts +1 -1
  228. package/src/starting-model.test.ts +19 -0
  229. package/src/starting-model.ts +11 -0
  230. package/src/token-efficiency.test.ts +1 -1
  231. package/src/tool-dispatch.ts +10 -4
  232. package/src/tool-display.test.ts +119 -0
  233. package/src/tool-display.ts +162 -4
  234. package/src/tool-gating.test.ts +1 -1
  235. package/src/tool-icon.test.ts +1 -1
  236. package/src/tool-modes.test.ts +18 -1
  237. package/src/tool-modes.ts +6 -2
  238. package/src/tool-output.test.ts +46 -0
  239. package/src/tool-output.ts +46 -0
  240. package/src/tool.ts +14 -1
  241. package/src/tunnel.ts +3 -3
  242. package/src/workflow-approval.ts +1 -1
@@ -0,0 +1,37 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
+ import * as path from 'node:path';
3
+
4
+ /**
5
+ * Signal the child's whole process group, not just the shell. The child must
6
+ * be spawned `detached` on POSIX so it leads its own group; a negative-pid kill
7
+ * then reaches every descendant (`sh -c 'sleep 1000 & wait'`, build workers,
8
+ * …) instead of orphaning them.
9
+ */
10
+ export function killTree(child: ChildProcess, signal: NodeJS.Signals): void {
11
+ if (child.pid === undefined) return;
12
+ if (process.platform === 'win32') {
13
+ // Windows has no process groups; taskkill /T walks the tree down from the
14
+ // shell. Always forced: a console program has no SIGTERM handler to run.
15
+ if (child.exitCode !== null || child.signalCode !== null) return;
16
+ const taskkill = path.win32.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe');
17
+ spawn(taskkill, ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }).on('error', () => {
18
+ child.kill(signal);
19
+ });
20
+ return;
21
+ }
22
+ try {
23
+ // Deliberately *no* "child already exited" guard here: the group outlives
24
+ // its leader (SIGTERM may kill the shell while a TERM-ignoring descendant
25
+ // lives on), and the pgid stays valid — and un-reusable — while any
26
+ // member survives. ESRCH below covers the fully-gone case.
27
+ process.kill(-child.pid, signal);
28
+ } catch (e) {
29
+ // ESRCH: the group is already gone — nothing to do. Anything else
30
+ // (e.g. EPERM, or the child somehow not leading a group): fall back to
31
+ // signalling the shell itself so we at least keep the old behavior.
32
+ if ((e as NodeJS.ErrnoException).code !== 'ESRCH') {
33
+ if (child.exitCode === null && child.signalCode === null) child.kill(signal);
34
+ }
35
+ }
36
+ }
37
+
@@ -17,7 +17,7 @@
17
17
  * depend on, so the two ends can never drift.
18
18
  */
19
19
 
20
- import { z } from 'zod';
20
+ import { z } from 'zod/v3';
21
21
 
22
22
  /** The byte that brackets a prompt marker. Absent from normal CLI output. */
23
23
  const NUL = '\u0000';
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { zodToJsonSchema } from './provider-utils.js';
4
4
 
5
5
  describe('model-visible validation constraints', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v3';
3
3
  import { zodToJsonSchema } from './provider-utils.js';
4
4
  import { resolveProviderTools } from './provider-tool-utils.js';
5
5
  import { defineTool } from './define.js';
package/src/provider.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { HostedTool, ToolDef } from './tool.js';
2
2
  import type { ReasoningReplayItem } from './events.js';
3
+ import type { StopReason } from './provider-utils.js';
4
+ import type { QuestionAskOptions, QuestionItem, QuestionOutcome } from './question.js';
5
+ import type { AgentAccountLimit } from './agent-account-limit.js';
6
+ import type { Skill } from './skill.js';
3
7
 
4
8
  export interface ProviderMessage {
5
9
  readonly role: 'system' | 'user' | 'assistant' | 'tool_result';
@@ -173,6 +177,14 @@ export interface ModelDescriptor {
173
177
  export interface LLMProvider {
174
178
  readonly name: string;
175
179
  readonly models: ReadonlyArray<ModelDescriptor>;
180
+ /**
181
+ * The model a new conversation starts on while the person chose no default
182
+ * for this provider: a light one, so an unset default does not spend their
183
+ * plan on the biggest model. Ignored when the provider does not list it.
184
+ */
185
+ readonly starterModel?: string;
186
+ /** For an agent connection, the agent's name as the person knows it ("Codex"), the same as its `ProviderDef.agentName`. */
187
+ readonly agentName?: string;
176
188
  /**
177
189
  * Optional provider-owned metadata preparation for a dynamically selected
178
190
  * model. The runtime awaits it before it calculates context budgets, so a
@@ -184,6 +196,41 @@ export interface LLMProvider {
184
196
  options?: { readonly signal?: AbortSignal },
185
197
  ): Promise<void>;
186
198
  stream(req: ProviderRequest): AsyncIterable<ProviderEvent>;
199
+ /**
200
+ * Present on an agent connection: run one turn of the vendor's own coding
201
+ * agent. The shared loop then calls this instead of {@link stream} for the
202
+ * conversation's turns; `stream` still serves one-shot text calls.
203
+ */
204
+ agentStep?(req: AgentStepRequest): AsyncIterable<AgentStepEvent>;
205
+ /**
206
+ * On an agent connection: the plan and goal the vendor's agent keeps itself
207
+ * (Claude, Codex), handed back as `plan` and `goal` events. Moxxy's plan and
208
+ * goal modes steer an agent without them like any model: through
209
+ * `plan_complete` / `goal_complete` it calls over MCP, and a nudge when it
210
+ * stops without one.
211
+ */
212
+ readonly agentOwns?: AgentOwnModes;
213
+ /**
214
+ * On an agent connection whose agent can start a turn by itself (Claude after
215
+ * a task it ran in the background finished): tell the listener when it does
216
+ * while no Moxxy turn is running, so the conversation opens a turn for it
217
+ * (`origin.kind: 'agent'`) whose `agentStep` reads the agent's turn
218
+ * (`continuesAgentTurn`). Returns the unsubscribe.
219
+ */
220
+ onAgentTurn?(listener: (start: AgentTurnStart) => void): () => void;
221
+ /**
222
+ * On an agent connection: what the agent's background work does while no
223
+ * Moxxy turn runs (its helpers' steps, `background_work` as it changes), for
224
+ * the conversation to record at once rather than at its next turn. Returns
225
+ * the unsubscribe.
226
+ */
227
+ onAgentActivity?(listener: (activity: AgentActivity) => void): () => void;
228
+ /**
229
+ * On an agent connection: stop one work the agent runs in the background in
230
+ * the conversation (`AgentBackgroundWork.id`). Resolves `false` when the
231
+ * agent has no such work running.
232
+ */
233
+ stopBackgroundWork?(sessionId: string, workId: string): Promise<boolean>;
187
234
  countTokens(req: Pick<ProviderRequest, 'model' | 'messages' | 'system' | 'tools'>): Promise<number>;
188
235
  }
189
236
 
@@ -337,9 +384,74 @@ export interface ProviderOAuthStatus {
337
384
  readonly message?: string;
338
385
  }
339
386
 
387
+ /**
388
+ * Where an agent connection's CLI stands on this computer:
389
+ * - `missing`: not found, so the connection cannot work until it is installed;
390
+ * - `update-required`: older than the oldest version Moxxy was checked against;
391
+ * - `update-available`: works, and the vendor has published a newer one;
392
+ * - `current`: works, and nothing newer is known;
393
+ * - `unknown`: found, but its version could not be read.
394
+ */
395
+ export type AgentCliState = 'missing' | 'update-required' | 'update-available' | 'current' | 'unknown';
396
+
397
+ /**
398
+ * Where an agent CLI came from, by where its file really is: the vendor's
399
+ * installer, Homebrew, a global npm install, inside an app (which updates it
400
+ * itself), or none of these.
401
+ */
402
+ export type AgentCliOrigin = 'installer' | 'homebrew' | 'npm' | 'app' | 'unknown';
403
+
404
+ /** The last check of an agent connection's CLI, as every surface shows it. */
405
+ export interface AgentCliReport {
406
+ readonly state: AgentCliState;
407
+ /** The CLI's command ("codex"), which also names it to `moxxy agents`. */
408
+ readonly command: string;
409
+ /** The version found on this computer; null when missing or unreadable. */
410
+ readonly installed: string | null;
411
+ /** The file that was checked, which a conversation also runs; absent when missing. */
412
+ readonly path?: string;
413
+ /** Where that file came from; absent when missing. */
414
+ readonly origin?: AgentCliOrigin;
415
+ /** The vendor's latest published version; null when the registry could not be asked. */
416
+ readonly latest: string | null;
417
+ /** The oldest version Moxxy works with. */
418
+ readonly minimum: string;
419
+ /** When the check ran, in epoch milliseconds. */
420
+ readonly checkedAt: number;
421
+ /** Whether the agent is signed in; absent when the CLI is missing or the connection does not say. */
422
+ readonly signedIn?: boolean;
423
+ /** Who it is signed in as, in the agent's words (Claude's e-mail, "ChatGPT" for Codex); null when it does not say. */
424
+ readonly account?: string | null;
425
+ }
426
+
427
+ /** How an agent connection's CLI is checked; the host runs it at start and every hour. */
428
+ export interface ProviderAgentCli {
429
+ /** `refresh` asks the vendor's registry again instead of trusting a recent answer. */
430
+ check(opts?: { readonly refresh?: boolean }): Promise<AgentCliReport>;
431
+ /**
432
+ * The models the agent offers the signed-in account now, as its own picker
433
+ * lists them. Asked after a check finds the agent installed and signed in;
434
+ * the connection's built-in `models` stay the answer until it succeeds.
435
+ */
436
+ models?(): Promise<ReadonlyArray<ModelDescriptor>>;
437
+ }
438
+
340
439
  export interface ProviderDef {
341
440
  readonly name: string;
342
441
  readonly models: ReadonlyArray<ModelDescriptor>;
442
+ /** What {@link LLMProvider.starterModel} says, known before the provider is built. */
443
+ readonly starterModel?: string;
444
+ /** For an agent connection, the agent's name as the person knows it ("Codex"), which surfaces use to say what it does. */
445
+ readonly agentName?: string;
446
+ /** For an agent connection, how its CLI is checked for presence and updates. */
447
+ readonly agentCli?: ProviderAgentCli;
448
+ /**
449
+ * For an agent connection, whether the agent as the person set it up acts
450
+ * without asking in `cwd` (`undefined` when it cannot tell). Read as a
451
+ * conversation starts on the connection, so its auto-approve is the truth
452
+ * from the first moment; a switch the conversation recorded always wins.
453
+ */
454
+ ownAutoApprove?(opts: { readonly cwd: string }): Promise<boolean | undefined>;
343
455
  /**
344
456
  * True when the provider can return its current model catalog at runtime.
345
457
  * Hosts use this capability signal to offer live discovery instead of
@@ -367,3 +479,237 @@ export interface ProviderDef {
367
479
  */
368
480
  readonly auth?: ProviderAuthDescriptor;
369
481
  }
482
+
483
+ /**
484
+ * Agent connections: the vendor's own coding agent (Claude, Codex, OpenCode)
485
+ * does the turn, unmodified. A provider that runs one implements
486
+ * `LLMProvider.agentStep`; the shared loop then runs one agent turn instead of
487
+ * "call the model, run the tools, repeat" and records what the agent did as
488
+ * ordinary Moxxy events. Everything the agent needs from Moxxy (its tools, the
489
+ * person's approval, a Question) goes through {@link AgentStepHost}.
490
+ */
491
+
492
+ /** One turn of the vendor's agent. */
493
+ export interface AgentStepRequest {
494
+ /** The Moxxy conversation, so an adapter keeps one live agent per conversation. */
495
+ readonly sessionId: string;
496
+ /** The model the person picked, exactly as the agent names it. */
497
+ readonly model: string;
498
+ /** The person's message for this turn (or the loop's follow-up), with its images and documents. */
499
+ readonly prompt: ReadonlyArray<ContentBlock>;
500
+ readonly cwd: string;
501
+ /** The active Moxxy mode (`default`, `plan`, `goal`, …), mapped by the adapter onto the agent's own. */
502
+ readonly mode: string;
503
+ /** Moxxy tools this mode offers; the adapter lends the ones the agent lacks over Moxxy's MCP server. */
504
+ readonly tools: ReadonlyArray<ToolDef>;
505
+ /**
506
+ * Every Moxxy tool of the conversation, in any mode. Agents read their tool
507
+ * list once (Codex as a thread opens, Claude as it starts), so the adapter
508
+ * lists these and refuses, with the reason, a call to one `tools` lacks.
509
+ */
510
+ readonly allTools?: ReadonlyArray<ToolDef>;
511
+ /** The conversation's Moxxy skills now; the adapter offers the agent those meant for it, through Moxxy's MCP server. */
512
+ readonly skills?: ReadonlyArray<Skill>;
513
+ /** The agent's own session from an earlier turn of this conversation, to resume. */
514
+ readonly resumeSessionId?: string;
515
+ /**
516
+ * The conversation before this turn, from Moxxy's record, for an agent that
517
+ * starts a new session of its own while the conversation already has turns
518
+ * (its earlier session is gone, or it joins partway). `undefined` when no
519
+ * turn came before. Built only when called.
520
+ */
521
+ readonly earlierConversation?: () => string | undefined;
522
+ /**
523
+ * The turns that ran on another connection since this agent's own last turn
524
+ * of the conversation, from Moxxy's record, for an agent that goes on in its
525
+ * own session: it was away for them. `undefined` when it missed none or
526
+ * never ran here. Built only when called.
527
+ */
528
+ readonly missedTurns?: () => string | undefined;
529
+ /** The conversation's auto-approve, one state with the agent's approval policy. */
530
+ readonly autoApprove: boolean;
531
+ /**
532
+ * The conversation recorded a switch: the adapter keeps `autoApprove` as it
533
+ * is, never the agent's own setup over it. Until one is recorded, the agent
534
+ * runs and shows the way the person set it up.
535
+ */
536
+ readonly autoApproveRecorded?: boolean;
537
+ /**
538
+ * The switch in force is the agent's own setup Moxxy took on, not the
539
+ * person's choice; an agent whose own setup still asks the person about
540
+ * something (OpenCode: work outside the project) keeps asking then.
541
+ */
542
+ readonly autoApproveFromAgent?: boolean;
543
+ readonly reasoning?: { readonly effort?: ReasoningEffort } | boolean;
544
+ /** The person asked for fast mode; the adapter runs the agent on its own faster tier, where it has one. */
545
+ readonly fast?: boolean;
546
+ readonly signal: AbortSignal;
547
+ readonly host: AgentStepHost;
548
+ /**
549
+ * The turn is one the agent started by itself (see `onAgentTurn`): read the
550
+ * agent's turn through to its end and send it nothing; `prompt` is only the
551
+ * marker Moxxy recorded for it.
552
+ */
553
+ readonly continuesAgentTurn?: true;
554
+ }
555
+
556
+ /** The agent started a turn by itself in a Moxxy conversation. */
557
+ export interface AgentTurnStart {
558
+ /** The Moxxy conversation (`AgentStepRequest.sessionId`). */
559
+ readonly sessionId: string;
560
+ /** What the agent took up, for the marker that opens the turn ("List files finished in the background"). */
561
+ readonly name: string;
562
+ /** The background work whose end it took up (`AgentBackgroundWork.id`), when the agent says. */
563
+ readonly workId?: string;
564
+ }
565
+
566
+ /** What Moxxy does for the agent during its turn. */
567
+ export interface AgentStepHost {
568
+ /** Run a Moxxy tool the agent called (over MCP) through Moxxy's hooks, permissions and log. */
569
+ runTool(call: AgentToolCall): Promise<AgentToolOutcome>;
570
+ /**
571
+ * Decide whether the agent may run one of its own tools. The person answers
572
+ * unless auto-approve or a standing rule does; with `policyOnly` only hooks
573
+ * and standing rules (`permissions.json`) are consulted, never the person;
574
+ * with `askPerson` the person answers even while auto-approve is on.
575
+ */
576
+ approve(request: AgentApprovalRequest): Promise<AgentApprovalDecision>;
577
+ /** Show the agent's Question in every Moxxy window of the conversation. */
578
+ ask(questions: ReadonlyArray<QuestionItem>, opts?: QuestionAskOptions): Promise<QuestionOutcome>;
579
+ }
580
+
581
+ export interface AgentToolCall {
582
+ readonly callId: string;
583
+ readonly name: string;
584
+ readonly input: unknown;
585
+ }
586
+
587
+ export type AgentToolOutcome =
588
+ | { readonly ok: true; readonly output: unknown }
589
+ | {
590
+ readonly ok: false;
591
+ readonly message: string;
592
+ /** The person refused a tool whose refusal ends the turn: stop the agent's turn. */
593
+ readonly endsTurn?: boolean;
594
+ };
595
+
596
+ export interface AgentApprovalRequest extends AgentToolCall {
597
+ readonly description?: string;
598
+ readonly policyOnly?: boolean;
599
+ /** The agent's own setting would have asked the person about this call, and auto-approve was not their choice. */
600
+ readonly askPerson?: boolean;
601
+ }
602
+
603
+ export type AgentApprovalDecision =
604
+ | { readonly allow: true; /** The person allowed it for the rest of the session. */ readonly always?: boolean }
605
+ | { readonly allow: false; readonly reason: string };
606
+
607
+ export type AgentGoalStatus = 'active' | 'complete';
608
+
609
+ export interface AgentOwnModes {
610
+ readonly plan?: boolean;
611
+ readonly goal?: boolean;
612
+ }
613
+
614
+ /**
615
+ * Work an agent connection's agent runs in the background: a command it left
616
+ * running, or a helper agent going on after the turn that started it.
617
+ */
618
+ export interface AgentBackgroundWork {
619
+ /** The agent's id for it; a helper's is the `id` of its `subagent_*` events. */
620
+ readonly id: string;
621
+ readonly kind: 'command' | 'agent';
622
+ /** What it does, as the agent names it ("npm run dev", "inspect folder"). */
623
+ readonly label: string;
624
+ }
625
+
626
+ /**
627
+ * A time the agent of an agent connection set itself to come back to the
628
+ * conversation by itself (Claude's ScheduleWakeup, `/loop`, CronCreate).
629
+ */
630
+ export interface AgentScheduledWakeup {
631
+ /** The agent's id for it. */
632
+ readonly id: string;
633
+ /** What the agent is told when it comes back. */
634
+ readonly prompt: string;
635
+ /** When it comes back next (epoch ms); null when it cannot be told. */
636
+ readonly dueAtMs: number | null;
637
+ /** Comes back on every match of its schedule, not once. */
638
+ readonly recurring: boolean;
639
+ }
640
+
641
+ /** How a background work ended. */
642
+ export type AgentWorkEnd = 'completed' | 'failed' | 'stopped';
643
+
644
+ /** What the agent's background work did while no Moxxy turn ran, for the conversation `sessionId`. */
645
+ export interface AgentActivity {
646
+ readonly sessionId: string;
647
+ readonly events: ReadonlyArray<AgentStepEvent>;
648
+ }
649
+
650
+ /** What the agent did, in the order it did it. */
651
+ export type AgentStepEvent =
652
+ | { readonly type: 'text_delta'; readonly delta: string }
653
+ | { readonly type: 'reasoning_delta'; readonly delta: string }
654
+ /** A finished assistant message; `tool_use` when the agent went on to call tools. */
655
+ | { readonly type: 'message'; readonly text: string; readonly stopReason?: StopReason }
656
+ | { readonly type: 'reasoning'; readonly text: string }
657
+ /** A tool the agent runs itself (its Bash, Edit, …). */
658
+ | { readonly type: 'tool_call'; readonly callId: string; readonly name: string; readonly input: unknown }
659
+ | { readonly type: 'tool_result'; readonly callId: string; readonly ok: boolean; readonly output?: unknown; readonly error?: string }
660
+ /** The agent's own session id, kept to resume it. */
661
+ | { readonly type: 'session'; readonly sessionId: string }
662
+ /**
663
+ * The agent's real approval policy; it becomes the conversation's auto-approve.
664
+ * `elsewhere` when it changed without Moxxy (a hook, another client, the agent itself).
665
+ */
666
+ | { readonly type: 'auto_approve'; readonly enabled: boolean; readonly elsewhere?: true }
667
+ /**
668
+ * Something outside Moxxy let the agent act without asking while auto-approve
669
+ * was off, and the connection set it back: only the person turns auto-approve on.
670
+ */
671
+ | { readonly type: 'auto_approve_restored' }
672
+ /** The state of the agent's own goal after its turn: still open, or met. */
673
+ | { readonly type: 'goal'; readonly status: AgentGoalStatus }
674
+ /** The agent's own finished plan (Markdown), waiting for the person to carry it out. */
675
+ | { readonly type: 'plan'; readonly text: string }
676
+ /**
677
+ * A helper agent the agent started itself (Claude's Agent tool, a Codex subagent, OpenCode's task),
678
+ * its tool calls, and its end. `id` ties them together; it need not be a Moxxy session.
679
+ */
680
+ /** `toolsUnseen` when the agent does not report the subagent's own tool calls (OpenCode). */
681
+ | { readonly type: 'subagent_started'; readonly id: string; readonly label: string; readonly agentType: string; readonly prompt?: string; readonly toolsUnseen?: true }
682
+ | { readonly type: 'subagent_tool_call'; readonly id: string; readonly name: string; readonly input: unknown }
683
+ /** What a subagent said as it worked, before its answer. */
684
+ | { readonly type: 'subagent_message'; readonly id: string; readonly text: string }
685
+ | {
686
+ readonly type: 'subagent_completed';
687
+ readonly id: string;
688
+ readonly text: string;
689
+ readonly tokensUsed?: number;
690
+ readonly error?: string;
691
+ /** It ended because it was stopped (by the person, or by the agent), not because it failed. */
692
+ readonly stopped?: true;
693
+ }
694
+ /** Everything the agent runs in the background now; it replaces the list before, so a missed end cannot leave work shown running. */
695
+ | { readonly type: 'background_work'; readonly work: ReadonlyArray<AgentBackgroundWork> }
696
+ /** Every time the agent set itself to come back; it replaces the list before. Not recorded in the log: it is what is planned now. */
697
+ | { readonly type: 'scheduled_wakeups'; readonly wakeups: ReadonlyArray<AgentScheduledWakeup> }
698
+ /** One background work ended; `output` is what a command printed last. */
699
+ | {
700
+ readonly type: 'background_work_ended';
701
+ readonly id: string;
702
+ readonly kind: AgentBackgroundWork['kind'];
703
+ readonly label: string;
704
+ readonly status: AgentWorkEnd;
705
+ readonly summary?: string;
706
+ readonly output?: string;
707
+ }
708
+ /** How much of the person's account the agent may still use; a reached limit ends the turn without a raw error. */
709
+ | { readonly type: 'account_limit'; readonly limit: AgentAccountLimit }
710
+ | { readonly type: 'usage'; readonly usage: TokenUsage }
711
+ /** How many tokens the agent's context holds now, when its usage is not per call (a whole turn's total). */
712
+ | { readonly type: 'context_used'; readonly tokens: number }
713
+ /** The context window the agent runs the model with, as it reports it after a response. */
714
+ | { readonly type: 'context_window'; readonly model: string; readonly tokens: number }
715
+ | { readonly type: 'error'; readonly message: string; readonly retryable: boolean };
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { QuestionItem } from './question.js';
3
+ import { chooseOption, draftAnswers, emptyDraft, typeOwnAnswer } from './question-draft.js';
4
+
5
+ const colour: QuestionItem = {
6
+ id: 'q1',
7
+ question: 'Which colour?',
8
+ options: [
9
+ { id: 'red', label: 'Red' },
10
+ { id: 'blue', label: 'Blue' },
11
+ ],
12
+ multiSelect: false,
13
+ allowOther: true,
14
+ };
15
+ const parts: QuestionItem = {
16
+ id: 'q2',
17
+ question: 'Which parts?',
18
+ options: [
19
+ { id: 'auth', label: 'Auth' },
20
+ { id: 'billing', label: 'Billing' },
21
+ ],
22
+ multiSelect: true,
23
+ allowOther: true,
24
+ };
25
+
26
+ describe('answering in the card', () => {
27
+ it('takes one choice where only one fits, replacing the earlier one and any typed answer', () => {
28
+ let draft = typeOwnAnswer(emptyDraft([colour]), colour, 'Teal');
29
+ draft = chooseOption(draft, colour, 'red');
30
+ draft = chooseOption(draft, colour, 'blue');
31
+
32
+ expect(draftAnswers(draft)).toEqual({ q1: { selected: ['blue'] } });
33
+ });
34
+
35
+ it('toggles choices where several fit, and keeps typed words beside them', () => {
36
+ let draft = emptyDraft([parts]);
37
+ draft = chooseOption(draft, parts, 'auth');
38
+ draft = chooseOption(draft, parts, 'billing');
39
+ draft = chooseOption(draft, parts, 'auth');
40
+ draft = typeOwnAnswer(draft, parts, 'Logs');
41
+
42
+ expect(draftAnswers(draft)).toEqual({ q2: { selected: ['billing'], other: 'Logs' } });
43
+ });
44
+
45
+ it('lets typed words stand alone as the one answer, clearing the choice', () => {
46
+ let draft = chooseOption(emptyDraft([colour]), colour, 'red');
47
+ draft = typeOwnAnswer(draft, colour, 'Teal');
48
+
49
+ expect(draftAnswers(draft)).toEqual({ q1: { selected: [], other: 'Teal' } });
50
+ });
51
+
52
+ it('sends no empty typed answer', () => {
53
+ const draft = typeOwnAnswer(chooseOption(emptyDraft([parts]), parts, 'auth'), parts, ' ');
54
+
55
+ expect(draftAnswers(draft)).toEqual({ q2: { selected: ['auth'] } });
56
+ });
57
+ });
@@ -0,0 +1,47 @@
1
+ import type { QuestionAnswers, QuestionItem } from './question.js';
2
+
3
+ interface ItemDraft {
4
+ readonly selected: ReadonlyArray<string>;
5
+ readonly other: string;
6
+ }
7
+
8
+ /**
9
+ * An answer being put together on a surface (the desktop card, the TUI, a chat
10
+ * bot's buttons): what the person has picked and typed so far, by question id.
11
+ */
12
+ export type QuestionDraft = Readonly<Record<string, ItemDraft>>;
13
+
14
+ export function emptyDraft(questions: ReadonlyArray<QuestionItem>): QuestionDraft {
15
+ return Object.fromEntries(questions.map((item) => [item.id, { selected: [], other: '' }]));
16
+ }
17
+
18
+ /** Pick an option: the one answer where one fits, a toggle where several do. */
19
+ export function chooseOption(draft: QuestionDraft, item: QuestionItem, optionId: string): QuestionDraft {
20
+ const current = itemDraft(draft, item);
21
+ if (!item.multiSelect) return { ...draft, [item.id]: { selected: [optionId], other: '' } };
22
+ const selected = current.selected.includes(optionId)
23
+ ? current.selected.filter((id) => id !== optionId)
24
+ : [...current.selected, optionId];
25
+ return { ...draft, [item.id]: { ...current, selected } };
26
+ }
27
+
28
+ /** Type an answer of the person's own; where one answer fits, it replaces the choice. */
29
+ export function typeOwnAnswer(draft: QuestionDraft, item: QuestionItem, other: string): QuestionDraft {
30
+ const current = itemDraft(draft, item);
31
+ const selected = !item.multiSelect && other.trim() !== '' ? [] : current.selected;
32
+ return { ...draft, [item.id]: { selected, other } };
33
+ }
34
+
35
+ /** The draft as the answer the session takes. */
36
+ export function draftAnswers(draft: QuestionDraft): QuestionAnswers {
37
+ return Object.fromEntries(
38
+ Object.entries(draft).map(([id, item]) => {
39
+ const other = item.other.trim();
40
+ return [id, other === '' ? { selected: item.selected } : { selected: item.selected, other }];
41
+ }),
42
+ );
43
+ }
44
+
45
+ function itemDraft(draft: QuestionDraft, item: QuestionItem): ItemDraft {
46
+ return draft[item.id] ?? { selected: [], other: '' };
47
+ }