@adecore/agents 0.39.0 → 0.40.1

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 (172) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex-models.json +3 -2
  31. package/dist/providers/codex.js +0 -1
  32. package/dist/providers/detect.js +1 -1
  33. package/dist/providers/shell-commands.js +1 -2
  34. package/dist/run-process.js +1 -3
  35. package/dist/skills.d.ts +1 -1
  36. package/dist/skills.js +5 -8
  37. package/dist/tasks/background-limit.js +3 -2
  38. package/dist/tasks/task-coordinator.js +11 -17
  39. package/dist/testing/watch.js +0 -1
  40. package/dist/usage/limits/monitor.js +1 -2
  41. package/dist/usage/prices-snapshot.json +8 -0
  42. package/dist/usage/readers/claude.js +3 -10
  43. package/dist/usage/readers/codex.js +2 -9
  44. package/dist/usage/readers/grok.js +3 -10
  45. package/dist/usage/readers/host-log.js +3 -10
  46. package/dist/usage/record.d.ts +2 -0
  47. package/dist/usage/record.js +13 -0
  48. package/package.json +6 -6
  49. package/src/async.ts +23 -5
  50. package/src/chat/acp-backend.ts +121 -0
  51. package/src/chat/acp-protocol.ts +172 -6
  52. package/src/chat/attachment-store.ts +21 -0
  53. package/src/chat/backend.ts +0 -2
  54. package/src/chat/bookmark-store.ts +23 -0
  55. package/src/chat/chat-core.ts +208 -7
  56. package/src/chat/chat-log.ts +19 -0
  57. package/src/chat/chat-process.ts +52 -1
  58. package/src/chat/chat-session.ts +401 -10
  59. package/src/chat/chat-store.ts +50 -0
  60. package/src/chat/chat-title.ts +9 -0
  61. package/src/chat/claude-backend.ts +50 -0
  62. package/src/chat/claude-protocol.ts +155 -8
  63. package/src/chat/claude-title.ts +10 -0
  64. package/src/chat/claude-transcript.ts +47 -4
  65. package/src/chat/codex-backend.ts +131 -0
  66. package/src/chat/codex-protocol.ts +180 -8
  67. package/src/chat/codex-thread.ts +23 -0
  68. package/src/chat/composer-preferences.ts +6 -0
  69. package/src/chat/context-breakdown.ts +11 -0
  70. package/src/chat/delta-coalescer.ts +8 -0
  71. package/src/chat/external-sessions.ts +28 -0
  72. package/src/chat/generated-image.ts +25 -0
  73. package/src/chat/index.ts +5 -0
  74. package/src/chat/input.ts +13 -0
  75. package/src/chat/json-rpc-transport.ts +28 -0
  76. package/src/chat/limit-resume.ts +13 -0
  77. package/src/chat/projector.ts +160 -4
  78. package/src/chat/readable-text.ts +3 -0
  79. package/src/chat/request-summary.ts +18 -4
  80. package/src/chat/subagent-projection.ts +3 -0
  81. package/src/chat/subagent-reader.ts +76 -0
  82. package/src/chat/subagent-settlement.ts +24 -0
  83. package/src/chat/thread-search.ts +13 -0
  84. package/src/chat/thread.ts +31 -0
  85. package/src/chat/ui-queries.ts +122 -0
  86. package/src/chat/ui-replies.ts +34 -0
  87. package/src/chat/visual-store.ts +49 -0
  88. package/src/chat/wake-chat.ts +3 -0
  89. package/src/client-sinks.ts +2 -0
  90. package/src/coded-error.ts +1 -5
  91. package/src/context/argv.ts +18 -2
  92. package/src/context/index.ts +1 -0
  93. package/src/context/refusal.ts +3 -0
  94. package/src/context/verb.ts +32 -4
  95. package/src/coordination/index.ts +10 -0
  96. package/src/error-text.ts +7 -0
  97. package/src/fields.ts +14 -0
  98. package/src/fs.ts +20 -4
  99. package/src/host/agent-host.ts +20 -0
  100. package/src/host/environment.ts +6 -0
  101. package/src/host/handlers.ts +18 -0
  102. package/src/host/memory-port.ts +4 -0
  103. package/src/host/wiring.ts +10 -0
  104. package/src/index.ts +1 -0
  105. package/src/lineage.ts +24 -5
  106. package/src/messages/deliver-message.ts +6 -0
  107. package/src/messages/deliver-notice.ts +13 -0
  108. package/src/messages/notice-store.ts +16 -4
  109. package/src/modes.ts +1 -0
  110. package/src/outbox/outbox-worker.ts +31 -0
  111. package/src/outbox/outbox.ts +5 -0
  112. package/src/providers/accounts/accounts.ts +36 -0
  113. package/src/providers/accounts/acp-account.ts +22 -27
  114. package/src/providers/accounts/launch.ts +4 -0
  115. package/src/providers/accounts/service.ts +110 -12
  116. package/src/providers/accounts/shadow-home.ts +10 -0
  117. package/src/providers/accounts/status.ts +13 -4
  118. package/src/providers/accounts/store.ts +25 -0
  119. package/src/providers/accounts/variables.ts +11 -1
  120. package/src/providers/acp-agent-state.ts +24 -3
  121. package/src/providers/acp-provider.ts +5 -0
  122. package/src/providers/acp-usage-log.ts +2 -0
  123. package/src/providers/catalog.ts +17 -0
  124. package/src/providers/claude.ts +10 -1
  125. package/src/providers/codex-models.json +3 -2
  126. package/src/providers/codex-rules.ts +3 -0
  127. package/src/providers/codex.ts +2 -1
  128. package/src/providers/detect.ts +6 -1
  129. package/src/providers/provider.ts +1 -1
  130. package/src/providers/registry.ts +18 -2
  131. package/src/providers/shell-commands.ts +37 -2
  132. package/src/record-directory.ts +19 -0
  133. package/src/run-process.ts +24 -3
  134. package/src/serializer.ts +3 -0
  135. package/src/skills.ts +73 -10
  136. package/src/tasks/background-limit.ts +9 -2
  137. package/src/tasks/end-children.ts +9 -0
  138. package/src/tasks/give-task.ts +8 -0
  139. package/src/tasks/task-coordinator.ts +81 -17
  140. package/src/tasks/task-store.ts +28 -0
  141. package/src/tasks/waiting-child.ts +21 -0
  142. package/src/tasks/wake-parent.ts +26 -0
  143. package/src/tasks/wiring.ts +22 -0
  144. package/src/testing/accounts.ts +2 -0
  145. package/src/testing/fake-acp-capture.ts +16 -0
  146. package/src/testing/fake-acp.ts +66 -0
  147. package/src/testing/fake-claude.ts +177 -0
  148. package/src/testing/fake-cli.ts +29 -0
  149. package/src/testing/fake-codex.ts +122 -0
  150. package/src/testing/manual-clock.ts +2 -0
  151. package/src/testing/watch.ts +7 -1
  152. package/src/title-file.ts +12 -0
  153. package/src/usage/accounts.ts +6 -0
  154. package/src/usage/aggregate.ts +19 -0
  155. package/src/usage/exchange.ts +12 -0
  156. package/src/usage/index-file.ts +27 -0
  157. package/src/usage/limits/monitor.ts +31 -3
  158. package/src/usage/limits/normalize.ts +30 -0
  159. package/src/usage/limits/probe.ts +17 -0
  160. package/src/usage/prices-snapshot.json +8 -0
  161. package/src/usage/pricing.ts +35 -0
  162. package/src/usage/projects.ts +8 -0
  163. package/src/usage/readers/claude.ts +12 -9
  164. package/src/usage/readers/codex.ts +40 -8
  165. package/src/usage/readers/grok.ts +17 -9
  166. package/src/usage/readers/host-log.ts +9 -9
  167. package/src/usage/record.ts +22 -0
  168. package/src/usage/roots.ts +6 -0
  169. package/src/usage/scanner.ts +61 -0
  170. package/src/usage/usage-service.ts +14 -0
  171. package/src/watch-seam.ts +8 -0
  172. package/src/windows-command.ts +12 -0
@@ -16,11 +16,13 @@ function textInput(text: string) {
16
16
  export function parseSkillsList(result: unknown): ChatSkill[] {
17
17
  const data = isRecord(result) && Array.isArray(result.data) ? result.data : [];
18
18
  const skills = new Map<string, ChatSkill>();
19
+
19
20
  for (const entry of data) {
20
21
  for (const skill of isRecord(entry) && Array.isArray(entry.skills) ? entry.skills : []) {
21
22
  if (!isRecord(skill) || typeof skill.name !== 'string' || skill.enabled === false || skills.has(skill.name)) {
22
23
  continue;
23
24
  }
25
+
24
26
  skills.set(skill.name, {
25
27
  name: skill.name,
26
28
  description: typeof skill.description === 'string' ? skill.description : '',
@@ -28,6 +30,7 @@ export function parseSkillsList(result: unknown): ChatSkill[] {
28
30
  });
29
31
  }
30
32
  }
33
+
31
34
  return [...skills.values()].sort((a, b) => a.name.localeCompare(b.name));
32
35
  }
33
36
 
@@ -39,20 +42,26 @@ const CLEAR_WORDS = new Set(['clear', 'stop', 'off', 'reset', 'none', 'cancel'])
39
42
  /* A `/goal` message the way the CLIs read one: a bare `/goal` shows the goal, a control word acts on it, and anything else is a new objective. Null for any other message. */
40
43
  export function parseGoalCommand(text: string): GoalCommand | null {
41
44
  const match = /^\/goal(?:\s+([\s\S]*))?$/.exec(text.trim());
45
+
42
46
  if (!match) {
43
47
  return null;
44
48
  }
49
+
45
50
  const argument = (match[1] ?? '').trim();
46
51
  const word = argument.toLowerCase();
52
+
47
53
  if (word === '') {
48
54
  return { kind: 'show' };
49
55
  }
56
+
50
57
  if (CLEAR_WORDS.has(word)) {
51
58
  return { kind: 'clear' };
52
59
  }
60
+
53
61
  if (word === 'pause' || word === 'resume') {
54
62
  return { kind: word };
55
63
  }
64
+
56
65
  return { kind: 'set', objective: argument };
57
66
  }
58
67
 
@@ -111,6 +120,7 @@ export class CodexBackend implements ChatBackend {
111
120
  if (this.transport) {
112
121
  return;
113
122
  }
123
+
114
124
  const transport = new CodexTransport({
115
125
  command: this.launch.command,
116
126
  cwd: this.launch.cwd,
@@ -120,7 +130,9 @@ export class CodexBackend implements ChatBackend {
120
130
  onFrame: (frame) => this.handleFrame(transport, frame),
121
131
  onExit: (exitCode, stderr) => this.handleExit(transport, exitCode, stderr)
122
132
  });
133
+
123
134
  this.transport = transport;
135
+
124
136
  try {
125
137
  await this.handshake(transport);
126
138
  } catch (error) {
@@ -129,6 +141,7 @@ export class CodexBackend implements ChatBackend {
129
141
  this.transport = null;
130
142
  void transport.dispose();
131
143
  }
144
+
132
145
  throw error;
133
146
  }
134
147
  }
@@ -137,14 +150,17 @@ export class CodexBackend implements ChatBackend {
137
150
  await transport.request('initialize', { clientInfo: this.client, capabilities: { experimentalApi: true, requestAttestation: false } });
138
151
  transport.notify('initialized', {});
139
152
  const tier = codexServiceTier(this.launch.selection);
153
+
140
154
  const params = {
141
155
  cwd: this.launch.cwd,
142
156
  model: this.launch.selection.model,
143
157
  ...(tier === null ? {} : { serviceTier: tier }),
144
158
  ...codexThreadOptions(this.launch.runtimeMode)
145
159
  };
160
+
146
161
  const instructions = this.launch.instructions === null ? {} : { developerInstructions: this.launch.instructions };
147
162
  let result: unknown;
163
+
148
164
  if (this.launch.resume) {
149
165
  try {
150
166
  result = await transport.request('thread/resume', { threadId: this.launch.resume, excludeTurns: true, ...params });
@@ -154,19 +170,24 @@ export class CodexBackend implements ChatBackend {
154
170
  if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
155
171
  throw error;
156
172
  }
173
+
157
174
  this.emit({ type: 'note', level: 'warning', text: `Codex could not resume its thread (${errorText(error)}). Started a new one.` });
158
175
  result = await transport.request('thread/start', { ...params, ...instructions });
159
176
  }
160
177
  } else {
161
178
  result = await transport.request('thread/start', { ...params, ...instructions });
162
179
  }
180
+
163
181
  this.imageInputSupported = await this.readImageSupport(transport);
182
+
164
183
  for (const event of this.protocol.threadReady(result)) {
165
184
  if (event.type === 'session' && event.agentSessionId) {
166
185
  this.threadId = event.agentSessionId;
167
186
  }
187
+
168
188
  this.emit(event);
169
189
  }
190
+
170
191
  await this.readGoal(transport).catch(() => {
171
192
  // An app-server without goals, or with the feature turned off, has none to report.
172
193
  });
@@ -178,6 +199,7 @@ export class CodexBackend implements ChatBackend {
178
199
  */
179
200
  private async resumeContext(transport: CodexTransport, threadId: string): Promise<string | null> {
180
201
  const instructions = this.launch.resumeInstructions ?? null;
202
+
181
203
  if (instructions !== null) {
182
204
  try {
183
205
  const items = [{ type: 'message', role: 'developer', content: [{ type: 'input_text', text: instructions }] }];
@@ -186,26 +208,32 @@ export class CodexBackend implements ChatBackend {
186
208
  if (!(error instanceof Error) || error.name !== 'CodexRpcError') {
187
209
  throw error;
188
210
  }
211
+
189
212
  return [instructions, this.launch.resumeNote].filter((part) => part !== null).join('\n\n');
190
213
  }
191
214
  }
215
+
192
216
  return this.launch.resumeNote;
193
217
  }
194
218
 
195
219
  private async readGoal(transport: CodexTransport): Promise<ChatGoal | null> {
196
220
  const result = await transport.request('thread/goal/get', { threadId: this.threadId });
197
221
  const goal = codexGoal(isRecord(result) ? result.goal : null);
222
+
198
223
  if (this.transport === transport) {
199
224
  this.emit({ type: 'goal', goal });
200
225
  }
226
+
201
227
  return goal;
202
228
  }
203
229
 
204
230
  private turnInput(input: TurnInput): unknown[] {
205
231
  const images = input.attachments.filter((attachment) => attachmentImageMime(attachment) !== null);
232
+
206
233
  if (images.length > 0 && this.imageInputSupported === false) {
207
234
  throw new Error(`${this.launch.modelName} does not support image input. Choose a model that accepts images.`);
208
235
  }
236
+
209
237
  for (const attachment of images) {
210
238
  try {
211
239
  accessSync(attachment.path, constants.R_OK);
@@ -213,28 +241,37 @@ export class CodexBackend implements ChatBackend {
213
241
  throw new Error(`Could not read attached image "${attachment.name}". Attach it again and retry.`);
214
242
  }
215
243
  }
244
+
216
245
  const parts: string[] = [];
246
+
217
247
  if (this.contextPending !== null) {
218
248
  parts.push(`${this.contextPending}\n\n`);
219
249
  this.contextPending = null;
220
250
  }
251
+
221
252
  if (input.preamble !== null) {
222
253
  parts.push(`${input.preamble}\n\n`);
223
254
  }
255
+
224
256
  parts.push(input.text);
225
257
  const note = attachmentNote(input.attachments);
258
+
226
259
  if (note !== '') {
227
260
  parts.push(`\n\n${note}`);
228
261
  }
262
+
229
263
  return [...textInput(parts.join('')), ...images.map((attachment) => ({ type: 'localImage', path: attachment.path }))];
230
264
  }
231
265
 
232
266
  sendTurn(input: TurnInput): void {
233
267
  const command = input.attachments.length === 0 ? parseGoalCommand(input.text) : null;
268
+
234
269
  if (command !== null) {
235
270
  void this.runGoalCommand(command, input);
271
+
236
272
  return;
237
273
  }
274
+
238
275
  const nativeInput = this.turnInput(input);
239
276
  this.turnUnnamed = true;
240
277
  this.promptId = input.promptId ?? null;
@@ -244,6 +281,7 @@ export class CodexBackend implements ChatBackend {
244
281
  private turnStartParams(nativeInput: unknown[]): Record<string, unknown> {
245
282
  const effort = this.launch.selection.options.effort;
246
283
  const tier = codexServiceTier(this.launch.selection);
284
+
247
285
  return {
248
286
  threadId: this.threadId,
249
287
  input: nativeInput,
@@ -261,52 +299,71 @@ export class CodexBackend implements ChatBackend {
261
299
  */
262
300
  private async runGoalCommand(command: GoalCommand, input: TurnInput): Promise<void> {
263
301
  const transport = this.transport;
302
+
264
303
  if (!transport) {
265
304
  this.emit({ type: 'failed', message: 'Codex is not running' });
305
+
266
306
  return;
267
307
  }
308
+
268
309
  const threadId = this.threadId;
310
+
269
311
  const setGoal = async (params: Record<string, unknown>): Promise<void> => {
270
312
  const result = await transport.request('thread/goal/set', { threadId, origin: 'user', ...params });
313
+
271
314
  if (this.transport === transport) {
272
315
  this.emit({ type: 'goal', goal: codexGoal(isRecord(result) ? result.goal : null) });
273
316
  }
274
317
  };
318
+
275
319
  try {
276
320
  const current = await this.readGoal(transport);
321
+
277
322
  if (command.kind === 'show') {
278
323
  this.reply(input.promptId, current === null ? 'No goal is set.' : goalLine(current));
324
+
279
325
  return;
280
326
  }
327
+
281
328
  if (command.kind === 'clear') {
282
329
  const result = await transport.request('thread/goal/clear', { threadId, origin: 'user' });
283
330
  this.emit({ type: 'goal', goal: null });
284
331
  this.reply(input.promptId, isRecord(result) && result.cleared === true ? 'Goal cleared.' : 'No goal to clear.');
332
+
285
333
  return;
286
334
  }
335
+
287
336
  if (current === null && command.kind !== 'set') {
288
337
  this.reply(input.promptId, 'No goal is set. Set one with /goal and the objective.');
338
+
289
339
  return;
290
340
  }
341
+
291
342
  if (command.kind === 'pause') {
292
343
  await setGoal({ status: 'paused' });
293
344
  this.reply(input.promptId, 'Goal paused. /goal resume takes it up again.');
345
+
294
346
  return;
295
347
  }
348
+
296
349
  if (command.kind === 'set') {
297
350
  // A new objective starts over, its tokens and time included.
298
351
  if (current !== null) {
299
352
  await transport.request('thread/goal/clear', { threadId, origin: 'user' });
300
353
  }
354
+
301
355
  await setGoal({ objective: command.objective, status: 'paused' });
302
356
  }
357
+
303
358
  const nativeInput = command.kind === 'set' ? this.turnInput({ ...input, text: command.objective }) : [];
304
359
  this.turnUnnamed = true;
305
360
  this.promptId = input.promptId ?? null;
306
361
  await transport.request('turn/start', this.turnStartParams(nativeInput));
362
+
307
363
  if (this.transport !== transport) {
308
364
  return;
309
365
  }
366
+
310
367
  this.acceptPrompt(input.promptId);
311
368
  await setGoal({ status: 'active' });
312
369
  } catch (error) {
@@ -321,27 +378,34 @@ export class CodexBackend implements ChatBackend {
321
378
  if (promptId !== undefined) {
322
379
  this.emit({ type: 'turn.accepted', promptId });
323
380
  }
381
+
324
382
  this.emit({ type: 'text.done', ref: `goal-${randomUUID()}`, text });
325
383
  this.emit({ type: 'turn.done', ...(promptId === undefined ? {} : { promptId }), state: 'done', costUsd: 0 });
326
384
  }
327
385
 
328
386
  async goal(action: ChatGoalAction): Promise<void> {
329
387
  const transport = this.transport;
388
+
330
389
  if (!transport) {
331
390
  throw new Error('Codex is not running');
332
391
  }
392
+
333
393
  if (action === 'clear') {
334
394
  await transport.request('thread/goal/clear', { threadId: this.threadId, origin: 'user' });
395
+
335
396
  if (this.transport === transport) {
336
397
  this.emit({ type: 'goal', goal: null });
337
398
  }
399
+
338
400
  return;
339
401
  }
402
+
340
403
  const result = await transport.request('thread/goal/set', {
341
404
  threadId: this.threadId,
342
405
  status: action === 'pause' ? 'paused' : 'active',
343
406
  origin: 'user'
344
407
  });
408
+
345
409
  if (this.transport === transport) {
346
410
  this.emit({ type: 'goal', goal: codexGoal(isRecord(result) ? result.goal : null) });
347
411
  }
@@ -350,18 +414,23 @@ export class CodexBackend implements ChatBackend {
350
414
  async steerTurn(input: TurnInput): Promise<boolean> {
351
415
  const transport = this.transport;
352
416
  const turnId = this.protocol.turnId;
417
+
353
418
  if (!transport || !turnId || this.turnUnnamed) {
354
419
  return false;
355
420
  }
421
+
356
422
  try {
357
423
  await transport.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: this.turnInput(input) });
424
+
358
425
  return true;
359
426
  } catch (error) {
360
427
  const rejected = error instanceof Error && error.name === 'CodexRpcError';
428
+
361
429
  // A compact or review turn takes no steer (codex 0.162: "cannot steer a compact turn"); the message waits for the next turn.
362
430
  if (rejected && /no active turn|not.*active|turn.*mismatch|expected.*turn|method not found|cannot steer|not steerable/i.test(error.message)) {
363
431
  return false;
364
432
  }
433
+
365
434
  throw error;
366
435
  }
367
436
  }
@@ -382,11 +451,14 @@ export class CodexBackend implements ChatBackend {
382
451
  if (!this.transport) {
383
452
  throw new Error('Codex is not running');
384
453
  }
454
+
385
455
  this.transport.respond(rpcId, result);
456
+
386
457
  return true;
387
458
  } catch (error) {
388
459
  this.emit({ type: 'request.withdrawn', requestId });
389
460
  this.fail(error);
461
+
390
462
  return false;
391
463
  }
392
464
  }
@@ -394,27 +466,35 @@ export class CodexBackend implements ChatBackend {
394
466
  private async readImageSupport(transport: CodexTransport): Promise<boolean | null> {
395
467
  let cursor: string | null = null;
396
468
  const seen = new Set<string>();
469
+
397
470
  try {
398
471
  do {
399
472
  const result = await transport.request('model/list', { includeHidden: true, ...(cursor === null ? {} : { cursor }) });
473
+
400
474
  if (!isRecord(result) || !Array.isArray(result.data)) {
401
475
  return null;
402
476
  }
477
+
403
478
  const model = result.data.find((entry) => isRecord(entry) && entry.model === this.launch.selection.model);
479
+
404
480
  if (isRecord(model)) {
405
481
  return Array.isArray(model.inputModalities) ? model.inputModalities.includes('image') : null;
406
482
  }
483
+
407
484
  cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
485
+
408
486
  if (cursor !== null) {
409
487
  if (seen.has(cursor)) {
410
488
  return null;
411
489
  }
490
+
412
491
  seen.add(cursor);
413
492
  }
414
493
  } while (cursor !== null);
415
494
  } catch {
416
495
  // Older app-servers may not report modalities; their turn response remains authoritative.
417
496
  }
497
+
418
498
  return null;
419
499
  }
420
500
 
@@ -428,43 +508,54 @@ export class CodexBackend implements ChatBackend {
428
508
  if (!this.transport || this.threadId === '') {
429
509
  return;
430
510
  }
511
+
431
512
  void this.transport.request('thread/name/set', { threadId: this.threadId, name: title }).catch(() => undefined);
432
513
  }
433
514
 
434
515
  /* Codex keeps its own skill index, so the running app-server is the authority on what it will run. */
435
516
  async listSkills(): Promise<ChatSkill[]> {
436
517
  const transport = this.transport;
518
+
437
519
  if (!transport) {
438
520
  return [];
439
521
  }
522
+
440
523
  return parseSkillsList(await transport.request('skills/list', { cwds: [this.launch.cwd] }));
441
524
  }
442
525
 
443
526
  listThreadItems(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown> {
444
527
  const transport = this.transport;
528
+
445
529
  if (!transport) {
446
530
  return Promise.reject(new Error('Codex is not running'));
447
531
  }
532
+
448
533
  return transport.request('thread/items/list', params);
449
534
  }
450
535
 
451
536
  interrupt(): void {
452
537
  const turnId = this.protocol.turnId;
538
+
453
539
  if (!this.transport) {
454
540
  return;
455
541
  }
542
+
456
543
  // As Codex's own TUI does: paused first, so no goal turn opens after the one this stops.
457
544
  if (this.goalNow?.status === 'active') {
458
545
  void this.transport.request('thread/goal/set', { threadId: this.threadId, status: 'paused', origin: 'user' }).catch(() => undefined);
459
546
  }
547
+
460
548
  if (!turnId) {
461
549
  this.interruptOwed = this.turnUnnamed;
550
+
462
551
  // A child request can open a host turn after the root's native turn already ended.
463
552
  if (!this.turnUnnamed) {
464
553
  this.emit({ type: 'turn.done', state: 'aborted', costUsd: 0 });
465
554
  }
555
+
466
556
  return;
467
557
  }
558
+
468
559
  void this.transport.request('turn/interrupt', { threadId: this.threadId, turnId }).catch(() => {
469
560
  // A turn that already ended has nothing to interrupt.
470
561
  });
@@ -474,6 +565,7 @@ export class CodexBackend implements ChatBackend {
474
565
  if (!this.transport) {
475
566
  return;
476
567
  }
568
+
477
569
  void this.transport.request('thread/backgroundTerminals/terminate', { threadId: this.threadId, processId: taskId }).catch(() => {
478
570
  // A process that already ended has nothing to terminate; its item says so on its own.
479
571
  });
@@ -483,10 +575,13 @@ export class CodexBackend implements ChatBackend {
483
575
  if (!this.transport) {
484
576
  return false;
485
577
  }
578
+
486
579
  const answer = this.protocol.approvalDecision(requestId, decision);
580
+
487
581
  if (!answer) {
488
582
  return false;
489
583
  }
584
+
490
585
  return this.respond(requestId, answer.rpcId, answer.result);
491
586
  }
492
587
 
@@ -494,27 +589,36 @@ export class CodexBackend implements ChatBackend {
494
589
  if (!this.transport) {
495
590
  return false;
496
591
  }
592
+
497
593
  const answer = this.protocol.questionAnswer(requestId, answers);
594
+
498
595
  if (!answer) {
499
596
  return false;
500
597
  }
598
+
501
599
  if (answer.kind === 'respond') {
502
600
  return this.respond(requestId, answer.rpcId, answer.result);
503
601
  }
602
+
504
603
  const turnId = this.protocol.turnId;
604
+
505
605
  if (!turnId) {
506
606
  // An optional question can outlive its turn; its answer resumes the same chat as plain text.
507
607
  this.turnUnnamed = true;
508
608
  this.promptId = null;
509
609
  this.request('turn/start', this.turnStartParams(textInput(answer.text)));
610
+
510
611
  return true;
511
612
  }
613
+
512
614
  this.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: textInput(answer.text) });
615
+
513
616
  return true;
514
617
  }
515
618
 
516
619
  dismissRequest(requestId: string): boolean {
517
620
  const answer = this.protocol.dismissQuestionAnswer(requestId);
621
+
518
622
  return answer?.kind === 'respond' ? this.respond(requestId, answer.rpcId, answer.result) : this.protocol.dismissQuestion(requestId);
519
623
  }
520
624
 
@@ -522,10 +626,13 @@ export class CodexBackend implements ChatBackend {
522
626
  if (this.protocol.dismissQuestion(requestId)) {
523
627
  return true;
524
628
  }
629
+
525
630
  const answer = this.protocol.declineRequest(requestId);
631
+
526
632
  if (!this.transport || !answer) {
527
633
  return false;
528
634
  }
635
+
529
636
  return this.respond(requestId, answer.rpcId, answer.result);
530
637
  }
531
638
 
@@ -536,16 +643,20 @@ export class CodexBackend implements ChatBackend {
536
643
  dispose(): Promise<void> {
537
644
  const transport = this.transport;
538
645
  this.transport = null;
646
+
539
647
  return Promise.all([transport?.dispose(), this.imageWrites]).then(() => undefined);
540
648
  }
541
649
 
542
650
  /* A request whose failure the person has to hear about, since it is the turn that cannot go on. */
543
651
  private request(method: string, params: unknown, accepted?: () => void): void {
544
652
  const transport = this.transport;
653
+
545
654
  if (!transport) {
546
655
  this.emit({ type: 'failed', message: 'Codex is not running' });
656
+
547
657
  return;
548
658
  }
659
+
549
660
  void transport
550
661
  .request(method, params)
551
662
  .then(() => {
@@ -564,18 +675,24 @@ export class CodexBackend implements ChatBackend {
564
675
  if (this.transport !== transport) {
565
676
  return;
566
677
  }
678
+
567
679
  const events = this.protocol.handle(frame);
680
+
568
681
  // A server request the protocol did not take still needs an answer, or Codex waits forever.
569
682
  if (frame.id !== undefined && events.length === 0 && (typeof frame.id === 'number' || typeof frame.id === 'string')) {
570
683
  transport.respondError(frame.id, -32601, `${this.client.title} does not handle ${String(frame.method)}`);
571
684
  }
685
+
572
686
  const nativeTurnId = frame.method === 'turn/started' && isRecord(frame.params) && frame.params.threadId === this.threadId ? this.protocol.turnId : null;
687
+
573
688
  if (nativeTurnId !== null) {
574
689
  if (this.turnUnnamed && this.promptId !== null) {
575
690
  this.nativePrompt = { turnId: nativeTurnId, promptId: this.promptId };
576
691
  }
692
+
577
693
  this.acceptPrompt(this.promptId ?? undefined);
578
694
  }
695
+
579
696
  for (const event of events) {
580
697
  if (event.type === 'turn.done' && this.nativePrompt !== null && event.native?.turnId === this.nativePrompt.turnId) {
581
698
  const promptId = this.nativePrompt.promptId;
@@ -585,8 +702,10 @@ export class CodexBackend implements ChatBackend {
585
702
  this.emit(event);
586
703
  }
587
704
  }
705
+
588
706
  if (this.turnUnnamed && nativeTurnId !== null) {
589
707
  this.turnUnnamed = false;
708
+
590
709
  if (this.interruptOwed) {
591
710
  this.interruptOwed = false;
592
711
  this.interrupt();
@@ -598,6 +717,7 @@ export class CodexBackend implements ChatBackend {
598
717
  if (this.transport !== transport) {
599
718
  return;
600
719
  }
720
+
601
721
  this.transport = null;
602
722
  this.protocol.forgetPending();
603
723
  this.emit({ type: 'exit', exitCode, ...(stderr === null ? {} : { stderr }) });
@@ -607,6 +727,7 @@ export class CodexBackend implements ChatBackend {
607
727
  if (event.type === 'goal') {
608
728
  this.goalNow = event.goal;
609
729
  }
730
+
610
731
  // An older autonomous turn can finish while the next prompt still waits for its own turn/started.
611
732
  if ((event.type === 'turn.done' && (!this.turnUnnamed || event.promptId !== undefined)) || event.type === 'failed' || event.type === 'exit') {
612
733
  this.turnUnnamed = false;
@@ -614,6 +735,7 @@ export class CodexBackend implements ChatBackend {
614
735
  this.promptId = null;
615
736
  this.nativePrompt = null;
616
737
  }
738
+
617
739
  if (event.type === 'image.generated' || this.imageWrites !== null) {
618
740
  const pending = (this.imageWrites ?? Promise.resolve()).then(async () => {
619
741
  if (event.type === 'image.generated') {
@@ -622,7 +744,9 @@ export class CodexBackend implements ChatBackend {
622
744
  this.host.onEvent(event);
623
745
  }
624
746
  });
747
+
625
748
  this.imageWrites = pending;
749
+
626
750
  void pending
627
751
  .finally(() => {
628
752
  if (this.imageWrites === pending) {
@@ -630,24 +754,30 @@ export class CodexBackend implements ChatBackend {
630
754
  }
631
755
  })
632
756
  .catch(() => undefined);
757
+
633
758
  return;
634
759
  }
760
+
635
761
  this.host.onEvent(event);
636
762
  }
637
763
 
638
764
  private async storeImage(event: Extract<BackendEvent, { type: 'image.generated' }>): Promise<void> {
639
765
  const input = { revisedPrompt: event.revisedPrompt, transparentBackground: event.transparentBackground };
640
766
  let attachment;
767
+
641
768
  try {
642
769
  if (!this.launch.saveGeneratedImage) {
643
770
  throw new Error('This machine cannot save generated images');
644
771
  }
772
+
645
773
  attachment = await this.launch.saveGeneratedImage(event.ref, event.upload);
646
774
  } catch (error) {
647
775
  this.host.onEvent({ type: 'tool.started', ref: event.ref, name: 'ImageGeneration', input, parentRef: null });
648
776
  this.host.onEvent({ type: 'tool.done', ref: event.ref, output: errorText(error), state: 'error' });
777
+
649
778
  return;
650
779
  }
780
+
651
781
  this.host.onEvent({
652
782
  type: 'tool.started',
653
783
  ref: event.ref,
@@ -662,6 +792,7 @@ export class CodexBackend implements ChatBackend {
662
792
  },
663
793
  parentRef: null
664
794
  });
795
+
665
796
  this.host.onEvent({ type: 'tool.done', ref: event.ref, output: null, state: 'done' });
666
797
  }
667
798
  }