@adecore/agents 0.38.0 → 0.40.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 (170) 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.d.ts +1 -0
  5. package/dist/chat/chat-core.js +9 -2
  6. package/dist/chat/chat-process.d.ts +1 -0
  7. package/dist/chat/chat-process.js +1 -1
  8. package/dist/chat/chat-session.d.ts +2 -0
  9. package/dist/chat/chat-session.js +9 -10
  10. package/dist/chat/claude-protocol.js +1 -6
  11. package/dist/chat/claude-transcript.js +1 -3
  12. package/dist/chat/codex-protocol.js +1 -6
  13. package/dist/chat/projector.js +1 -3
  14. package/dist/chat/request-summary.js +1 -3
  15. package/dist/coded-error.js +1 -5
  16. package/dist/context/argv.js +1 -2
  17. package/dist/context/verb.js +1 -3
  18. package/dist/error-text.d.ts +1 -0
  19. package/dist/error-text.js +4 -0
  20. package/dist/fields.d.ts +3 -0
  21. package/dist/fields.js +12 -0
  22. package/dist/fs.d.ts +1 -0
  23. package/dist/fs.js +8 -4
  24. package/dist/lineage.js +11 -5
  25. package/dist/messages/notice-store.js +3 -4
  26. package/dist/providers/accounts/acp-account.js +3 -20
  27. package/dist/providers/accounts/service.d.ts +1 -2
  28. package/dist/providers/accounts/service.js +5 -9
  29. package/dist/providers/accounts/status.js +1 -3
  30. package/dist/providers/acp-agent-state.js +1 -3
  31. package/dist/providers/claude.js +0 -1
  32. package/dist/providers/codex.js +0 -1
  33. package/dist/providers/detect.js +1 -1
  34. package/dist/providers/shell-commands.js +1 -2
  35. package/dist/run-process.js +1 -3
  36. package/dist/skills.d.ts +1 -1
  37. package/dist/skills.js +5 -8
  38. package/dist/tasks/background-limit.js +3 -2
  39. package/dist/tasks/task-coordinator.js +11 -17
  40. package/dist/testing/watch.js +0 -1
  41. package/dist/usage/limits/monitor.js +1 -2
  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 +218 -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 +414 -13
  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-rules.ts +3 -0
  126. package/src/providers/codex.ts +2 -1
  127. package/src/providers/detect.ts +6 -1
  128. package/src/providers/provider.ts +1 -1
  129. package/src/providers/registry.ts +18 -2
  130. package/src/providers/shell-commands.ts +37 -2
  131. package/src/record-directory.ts +19 -0
  132. package/src/run-process.ts +24 -3
  133. package/src/serializer.ts +3 -0
  134. package/src/skills.ts +73 -10
  135. package/src/tasks/background-limit.ts +9 -2
  136. package/src/tasks/end-children.ts +9 -0
  137. package/src/tasks/give-task.ts +8 -0
  138. package/src/tasks/task-coordinator.ts +81 -17
  139. package/src/tasks/task-store.ts +28 -0
  140. package/src/tasks/waiting-child.ts +21 -0
  141. package/src/tasks/wake-parent.ts +26 -0
  142. package/src/tasks/wiring.ts +22 -0
  143. package/src/testing/accounts.ts +2 -0
  144. package/src/testing/fake-acp-capture.ts +16 -0
  145. package/src/testing/fake-acp.ts +66 -0
  146. package/src/testing/fake-claude.ts +177 -0
  147. package/src/testing/fake-cli.ts +29 -0
  148. package/src/testing/fake-codex.ts +122 -0
  149. package/src/testing/manual-clock.ts +2 -0
  150. package/src/testing/watch.ts +7 -1
  151. package/src/title-file.ts +12 -0
  152. package/src/usage/accounts.ts +6 -0
  153. package/src/usage/aggregate.ts +19 -0
  154. package/src/usage/exchange.ts +12 -0
  155. package/src/usage/index-file.ts +27 -0
  156. package/src/usage/limits/monitor.ts +31 -3
  157. package/src/usage/limits/normalize.ts +30 -0
  158. package/src/usage/limits/probe.ts +17 -0
  159. package/src/usage/pricing.ts +35 -0
  160. package/src/usage/projects.ts +8 -0
  161. package/src/usage/readers/claude.ts +12 -9
  162. package/src/usage/readers/codex.ts +40 -8
  163. package/src/usage/readers/grok.ts +17 -9
  164. package/src/usage/readers/host-log.ts +9 -9
  165. package/src/usage/record.ts +22 -0
  166. package/src/usage/roots.ts +6 -0
  167. package/src/usage/scanner.ts +61 -0
  168. package/src/usage/usage-service.ts +14 -0
  169. package/src/watch-seam.ts +8 -0
  170. package/src/windows-command.ts +12 -0
@@ -13,9 +13,11 @@ const decoder = new TextDecoder();
13
13
  /* The first `bytes` of a text without cutting a character in half, and whether anything was left off. */
14
14
  function cutAt(text: string, bytes: number): { text: string; cut: boolean } {
15
15
  const encoded = encoder.encode(text);
16
+
16
17
  if (encoded.length <= bytes) {
17
18
  return { text, cut: false };
18
19
  }
20
+
19
21
  return { text: decoder.decode(encoded.slice(0, bytes)).replace(/\uFFFD$/, ''), cut: true };
20
22
  }
21
23
 
@@ -26,24 +28,30 @@ function cutAt(text: string, bytes: number): { text: string; cut: boolean } {
26
28
  */
27
29
  export function wakePrompt(tasks: readonly Task[], teamsOut = 0, restOf?: (childId: string) => string): string {
28
30
  const head = tasks.length === 1 ? 'A task you gave has settled. Its result:' : `${tasks.length} tasks you gave have settled. Their results:`;
31
+
29
32
  const sections = tasks.map((task) => {
30
33
  const { text, cut } = cutAt(task.result?.text ?? '', RESULT_PREVIEW_BYTES);
31
34
  const lines = [`## ${task.title} (node ${task.childId}, task ${task.id}): ${task.status}`, '', text === '' ? '(no result text)' : text];
35
+
32
36
  if (cut) {
33
37
  const rest = restOf === undefined ? '' : ` ${restOf(task.childId)}`;
34
38
  lines.push('', `[The result was cut at ${RESULT_PREVIEW_BYTES / 1024} KiB.${rest}]`);
35
39
  }
40
+
36
41
  return lines.join('\n');
37
42
  });
43
+
38
44
  const together = tasks.some((task) => task.batchId !== undefined)
39
45
  ? ['The tasks of one team call come in together, once every one of them has settled.']
40
46
  : [];
47
+
41
48
  const out =
42
49
  teamsOut === 0
43
50
  ? []
44
51
  : [
45
52
  `${teamsOut === 1 ? 'A team you gave is' : `${teamsOut} teams you gave are`} still out: you are woken with all of a team's results once its last task settles.`
46
53
  ];
54
+
47
55
  return [head, ...sections, ...together, ...out].join('\n\n');
48
56
  }
49
57
 
@@ -70,6 +78,7 @@ function deliveryBlocked(items: readonly ChatItem[], tasks: readonly Task[]): bo
70
78
  function deliveryPending(items: readonly ChatItem[], tasks: readonly Task[]): boolean {
71
79
  return tasks.some((task) => {
72
80
  const turn = items.findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
81
+
73
82
  return turn?.kind === 'turn' && turn.deliveryPending;
74
83
  });
75
84
  }
@@ -90,34 +99,47 @@ export function wakeParentHandler(deps: WakeParentDeps) {
90
99
  return async (entry: WakeParentEntry): Promise<OutboxOutcome> => {
91
100
  const parentId = entry.target;
92
101
  const pending = deps.tasks.pendingWake(parentId);
102
+
93
103
  if (pending.length === 0) {
94
104
  return;
95
105
  }
106
+
96
107
  const held = (): OutboxOutcome => (deps.tasks.readyWake(parentId).length < deps.tasks.pendingWake(parentId).length ? 'wait' : undefined);
108
+
97
109
  // Nothing to say yet, so the chat is not loaded from disk for it.
98
110
  if (deps.tasks.readyWake(parentId).length === 0) {
99
111
  return held();
100
112
  }
113
+
101
114
  const chat = await deps.chat(parentId);
115
+
102
116
  if (chat === null) {
103
117
  await deps.tasks.dropWake(parentId);
118
+
104
119
  return;
105
120
  }
121
+
106
122
  const items = chat.items();
107
123
  const named = new Set(items.flatMap((item) => (item.kind === 'turn' && !item.deliveryPending ? (item.taskIds ?? []) : [])));
108
124
  const already = deps.tasks.pendingWake(parentId).filter((task) => named.has(task.id));
125
+
109
126
  if (already.length > 0) {
110
127
  await chat.persist?.();
111
128
  await deps.tasks.markWoken(already.map((task) => task.id));
112
129
  }
130
+
113
131
  const tasks = deps.tasks.readyWake(parentId).filter((task) => !named.has(task.id));
132
+
114
133
  if (tasks.length === 0) {
115
134
  return held();
116
135
  }
136
+
117
137
  if (deliveryBlocked(items, tasks)) {
118
138
  return 'wait';
119
139
  }
140
+
120
141
  const teamsOut = deps.tasks.openBatches(parentId).length;
142
+
121
143
  if (
122
144
  !chat.wake({
123
145
  text: wakePrompt(tasks, teamsOut, deps.restOf),
@@ -128,11 +150,15 @@ export function wakeParentHandler(deps: WakeParentDeps) {
128
150
  ) {
129
151
  return 'wait';
130
152
  }
153
+
131
154
  await chat.persist?.();
155
+
132
156
  if (deliveryPending(chat.items(), tasks)) {
133
157
  return 'wait';
134
158
  }
159
+
135
160
  await deps.tasks.markWoken(tasks.map((task) => task.id));
161
+
136
162
  return held();
137
163
  };
138
164
  }
@@ -85,10 +85,14 @@ export interface TaskWiring {
85
85
  export function wireTasks(deps: TaskWiringDeps): TaskWiring {
86
86
  const now = deps.now ?? Date.now;
87
87
  const { chats, outbox } = deps;
88
+
88
89
  const enqueue = (projectId: string, target: string, work: TaskWork, notBefore?: number): Promise<void> =>
89
90
  outbox.enqueue(projectId, target, work, notBefore);
91
+
90
92
  const limit = backgroundLimits(outbox);
93
+
91
94
  const owesGiveTask = (taskId: string): boolean => outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === taskId);
95
+
92
96
  const coordinator = new TaskCoordinator({
93
97
  tasks: deps.tasks,
94
98
  now,
@@ -102,15 +106,18 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
102
106
  ...(deps.retryAt ? { retryAt: deps.retryAt } : {}),
103
107
  afterBackgroundWork: (chatId) => {
104
108
  const chat = chats.get(chatId);
109
+
105
110
  if (!chat?.running) {
106
111
  return 'gone';
107
112
  }
113
+
108
114
  return reportsOnBackgroundWork(chat.info.provider) ? 'reports' : 'silent';
109
115
  },
110
116
  commandsOf: (chatId) => chats.get(chatId)?.info.background ?? [],
111
117
  activeGoal: (chatId) => {
112
118
  const chat = chats.get(chatId);
113
119
  const goal = chat?.info.goal;
120
+
114
121
  return chat && goal?.status === 'active' ? { objective: goal.objective, running: chat.running } : null;
115
122
  },
116
123
  limit
@@ -118,10 +125,12 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
118
125
 
119
126
  deps.tasks.onChange((task: Task) => {
120
127
  deps.changed?.(task);
128
+
121
129
  // A cancelled task owes no wake of its own, but it may be the last of a team whose held results now go out.
122
130
  if (task.status === 'cancelled' && task.batchId !== undefined) {
123
131
  outbox.wake(task.parentId);
124
132
  }
133
+
125
134
  // However a task ended, the limit on its child's background commands has nothing left to settle.
126
135
  if (task.status !== 'open' && limit.owed(task.id)) {
127
136
  void limit.lapse(task.id).catch((e: unknown) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
@@ -129,10 +138,12 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
129
138
  });
130
139
 
131
140
  const requests = chatRequests(chats);
141
+
132
142
  const waiting = new WaitingObserver({
133
143
  // A child on another machine asks its questions there.
134
144
  openTask: (childId) => {
135
145
  const task = deps.tasks.openFor(childId);
146
+
136
147
  return task?.machine === undefined ? task : undefined;
137
148
  },
138
149
  enqueue,
@@ -142,6 +153,7 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
142
153
  chats.observe((event) => {
143
154
  coordinator.chatEvent(event);
144
155
  waiting.chatEvent(event);
156
+
145
157
  if (event.event === 'chat.event' && chats.get(event.payload.chatId)?.info.activeTurnId === null) {
146
158
  outbox.wake(event.payload.chatId);
147
159
  }
@@ -155,12 +167,15 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
155
167
  if (task.requiresTaskTurn === true && deps.placed(task.childId) && !owesGiveTask(task.id)) {
156
168
  await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
157
169
  }
170
+
158
171
  const stored = await chats.storedBackground(task.childId);
159
172
  const storedWork = stored.items.length > 0 || stored.commands.length > 0;
173
+
160
174
  // A goal still active went down with the CLI, which its child's last turn settles on.
161
175
  if (!deps.placed(task.childId) || (task.background === undefined && task.requiresTaskTurn !== true && !storedWork && !stored.goal)) {
162
176
  return;
163
177
  }
178
+
164
179
  if (task.background === undefined && stored.turnId !== null && storedWork) {
165
180
  await deps.tasks.holdBackground(task.id, {
166
181
  turnId: stored.turnId,
@@ -168,9 +183,12 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
168
183
  commands: stored.commands
169
184
  });
170
185
  }
186
+
171
187
  const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
188
+
172
189
  if (chat && chat.info.activeTurnId === null) {
173
190
  const last = chat.thread.list().findLast((item) => item.kind === 'turn');
191
+
174
192
  if (last?.kind === 'turn') {
175
193
  coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
176
194
  }
@@ -184,6 +202,7 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
184
202
  if (task.status !== 'open') {
185
203
  continue;
186
204
  }
205
+
187
206
  // One child whose record this version cannot read must not keep every later task from recovering.
188
207
  try {
189
208
  await recoverOpen(task);
@@ -191,7 +210,9 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
191
210
  console.error(`Recovering task ${task.id} failed:`, errorText(e));
192
211
  }
193
212
  }
213
+
194
214
  await coordinator.settled();
215
+
195
216
  for (const task of deps.tasks.all()) {
196
217
  if (
197
218
  task.status !== 'open' &&
@@ -209,6 +230,7 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
209
230
  // The record first: the handler reads what the task asks from it, and a restart in between still owes the turn.
210
231
  const task = await deps.tasks.open({ ...record, requiresTaskTurn: true }, now());
211
232
  await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
233
+
212
234
  return task;
213
235
  },
214
236
  chatState: (nodeId) => targets.chat(nodeId),
@@ -41,8 +41,10 @@ export async function testAccounts(options: {
41
41
  prepareShadow: async () => ({ linked: [], unshared: [], sharedLogin: false }),
42
42
  secrets: memorySecrets()
43
43
  });
44
+
44
45
  // Written and not saved, so an account whose folder is already gone is there the way a person left it.
45
46
  await writeAccounts(accountsPath(options.home), options.accounts);
46
47
  await service.load();
48
+
47
49
  return service;
48
50
  }
@@ -30,58 +30,74 @@ export function fakeAcpCapture(capture: AcpCapture, answers: unknown[] = []): Fa
30
30
  const send = (frame: Frame): void => {
31
31
  io.out({ jsonrpc: '2.0', ...frame });
32
32
  };
33
+
33
34
  const update = (fields: unknown): void => {
34
35
  const notification = (fields as Frame).update === undefined ? { update: fields } : (fields as Frame);
35
36
  send({ method: 'session/update', params: { sessionId: capture.sessionId, ...notification } });
36
37
  };
38
+
37
39
  let waiting: { rpcId: number; rest: unknown[]; promptId: unknown; text: unknown } | null = null;
38
40
  let nextRpcId = 0;
41
+
39
42
  const play = (updates: unknown[], promptId: unknown, text: unknown): void => {
40
43
  for (const [index, fields] of updates.entries()) {
41
44
  const permission = (fields as Frame).permission;
42
45
  const request = (fields as Frame).request as { method: string; params: unknown } | undefined;
46
+
43
47
  if (permission !== undefined || request !== undefined) {
44
48
  const rpcId = nextRpcId++;
45
49
  waiting = { rpcId, rest: updates.slice(index + 1), promptId, text };
46
50
  send({ id: rpcId, method: request?.method ?? 'session/request_permission', params: request?.params ?? permission });
51
+
47
52
  return;
48
53
  }
54
+
49
55
  const named = capture.titleFromPrompt === true && (fields as Frame).sessionUpdate === 'session_info_update';
50
56
  update(named ? { ...(fields as Frame), title: text } : fields);
51
57
  }
58
+
52
59
  send(capture.promptError === undefined ? { id: promptId, result: capture.promptResponse } : { id: promptId, error: capture.promptError });
53
60
  };
61
+
54
62
  return {
55
63
  onLine: (line) => {
56
64
  const frame = JSON.parse(line) as Frame;
57
65
  const id = frame.id;
66
+
58
67
  switch (frame.method) {
59
68
  case undefined: {
60
69
  const answered = waiting;
70
+
61
71
  if (answered !== null && id === answered.rpcId) {
62
72
  waiting = null;
63
73
  answers.push(frame.result ?? frame.error);
64
74
  play(answered.rest, answered.promptId, answered.text);
65
75
  }
76
+
66
77
  return;
67
78
  }
68
79
  case 'initialize':
69
80
  send({ id, result: capture.initialize });
81
+
70
82
  return;
71
83
  case 'session/new':
72
84
  update({ sessionUpdate: 'available_commands_update', availableCommands: capture.commands });
73
85
  send({ id, result: capture.sessionNew });
86
+
74
87
  return;
75
88
  case 'session/set_config_option':
76
89
  send({ id, result: capture.setConfigOption });
90
+
77
91
  return;
78
92
  case 'session/set_mode':
79
93
  case 'session/close':
80
94
  send({ id, result: {} });
95
+
81
96
  return;
82
97
  case 'session/prompt': {
83
98
  const blocks = Array.isArray((frame.params as Frame | undefined)?.prompt) ? ((frame.params as Frame).prompt as Frame[]) : [];
84
99
  play(capture.promptUpdates, id, blocks.find((block) => block.type === 'text')?.text);
100
+
85
101
  return;
86
102
  }
87
103
  case 'session/cancel':
@@ -74,6 +74,7 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
74
74
  if (prompt === null) {
75
75
  return;
76
76
  }
77
+
77
78
  update({ sessionUpdate: 'usage_update', used: 1200, size: 200000, cost: { amount: 0.01, currency: 'USD' } });
78
79
  respond(prompt.rpcId, { stopReason, ...(options.turnUsage === undefined ? {} : { usage: options.turnUsage }) });
79
80
  prompt = null;
@@ -81,19 +82,23 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
81
82
 
82
83
  const sessionState = (): Frame => {
83
84
  const meta = (totalContextTokens: number): Frame => (options.contextWindows ? { _meta: { totalContextTokens } } : {});
85
+
84
86
  const modelChoices = [
85
87
  { value: 'fake-small', name: 'Fake small', ...meta(128000) },
86
88
  { value: 'fake-large', name: 'Fake large', ...meta(1000000) }
87
89
  ];
90
+
88
91
  const effortChoices = [
89
92
  { value: 'low', name: 'Low', description: 'Answers quickly' },
90
93
  { value: 'high', name: 'High' },
91
94
  ...(model === 'fake-large' ? [{ value: 'xhigh', name: 'Extra high' }] : [])
92
95
  ];
96
+
93
97
  const effortOption =
94
98
  options.reasoningEffort === undefined
95
99
  ? []
96
100
  : [{ id: 'reasoning_effort', name: 'Reasoning effort', type: 'select', currentValue: effort, options: effortChoices }];
101
+
97
102
  return {
98
103
  ...(modes.length === 0 ? {} : { modes: { currentModeId: mode, availableModes: modes.map((id) => ({ id, name: id })) } }),
99
104
  ...(options.legacyModels
@@ -110,6 +115,7 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
110
115
  const askPermission = (toolCall: Frame, onAnswer: (outcome: Frame) => void): void => {
111
116
  const rpcId = serverRequestId++;
112
117
  permission = { rpcId, onAnswer };
118
+
113
119
  send({
114
120
  id: rpcId,
115
121
  method: 'session/request_permission',
@@ -127,33 +133,44 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
127
133
 
128
134
  const handlePrompt = (id: unknown, params: Frame): void => {
129
135
  const blocks = Array.isArray(params.prompt) ? (params.prompt as Frame[]) : [];
136
+
130
137
  const text = blocks
131
138
  .filter((block) => block.type === 'text')
132
139
  .map((block) => String(block.text))
133
140
  .join('\n');
141
+
134
142
  const images = blocks.filter((block) => block.type === 'image').length;
135
143
  const history = sessionHistory.get(sessionId) ?? [];
136
144
  sessionHistory.set(sessionId, [...history, text]);
137
145
  prompt = { rpcId: id as number | string, cancelled: false };
138
146
  const last = text.split('\n\n').at(-1) ?? text;
147
+
139
148
  if (last === 'history?') {
140
149
  say(`prompts before: ${history.length}`);
141
150
  endTurn('end_turn');
151
+
142
152
  return;
143
153
  }
154
+
144
155
  if (last === 'mode?') {
145
156
  say(`mode: ${mode}, model: ${model}`);
146
157
  endTurn('end_turn');
158
+
147
159
  return;
148
160
  }
161
+
149
162
  if (last === 'crash') {
150
163
  io.exit(1);
164
+
151
165
  return;
152
166
  }
167
+
153
168
  if (last === 'slow') {
154
169
  say('working');
170
+
155
171
  return;
156
172
  }
173
+
157
174
  if (last === 'plan') {
158
175
  update({
159
176
  sessionUpdate: 'plan',
@@ -162,37 +179,48 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
162
179
  { content: 'Write the fix', priority: 'medium', status: 'in_progress' }
163
180
  ]
164
181
  });
182
+
165
183
  endTurn('end_turn');
184
+
166
185
  return;
167
186
  }
187
+
168
188
  if (last === 'refuse' || last === 'max tokens') {
169
189
  say('no');
170
190
  endTurn(last === 'refuse' ? 'refusal' : 'max_tokens');
191
+
171
192
  return;
172
193
  }
194
+
173
195
  if (last.startsWith('edit:')) {
174
196
  const path = last.slice(5).trim();
175
197
  const toolCallId = `call-${++callCounter}`;
176
198
  const content = [{ type: 'diff', path, oldText: 'one\ntwo\n', newText: 'one\nthree\n' }];
177
199
  update({ sessionUpdate: 'tool_call', toolCallId, title: `Edit ${path}`, kind: 'edit', status: 'pending', locations: [{ path }] });
200
+
178
201
  askPermission({ toolCallId, title: `Edit ${path}`, kind: 'edit', content }, (outcome) => {
179
202
  const allowed = outcome.outcome === 'selected' && outcome.optionId !== 'no';
180
203
  update({ sessionUpdate: 'tool_call_update', toolCallId, status: allowed ? 'completed' : 'failed', content });
181
204
  endTurn(outcome.outcome === 'cancelled' ? 'cancelled' : 'end_turn');
182
205
  });
206
+
183
207
  return;
184
208
  }
209
+
185
210
  if (last.startsWith('tool:')) {
186
211
  const command = last.slice(5).trim();
187
212
  const toolCallId = `call-${++callCounter}`;
188
213
  update({ sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'I should run it.' } });
189
214
  update({ sessionUpdate: 'tool_call', toolCallId, title: `Run ${command}`, kind: 'execute', status: 'pending', rawInput: { command } });
215
+
190
216
  askPermission({ toolCallId }, (outcome) => {
191
217
  if (outcome.outcome === 'cancelled') {
192
218
  update({ sessionUpdate: 'tool_call_update', toolCallId, status: 'failed' });
193
219
  endTurn('cancelled');
220
+
194
221
  return;
195
222
  }
223
+
196
224
  if (outcome.optionId === 'no') {
197
225
  update({
198
226
  sessionUpdate: 'tool_call_update',
@@ -200,22 +228,29 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
200
228
  status: 'failed',
201
229
  content: [{ type: 'content', content: { type: 'text', text: 'rejected' } }]
202
230
  });
231
+
203
232
  say('not run');
204
233
  endTurn('end_turn');
234
+
205
235
  return;
206
236
  }
237
+
207
238
  update({ sessionUpdate: 'tool_call_update', toolCallId, status: 'in_progress' });
239
+
208
240
  update({
209
241
  sessionUpdate: 'tool_call_update',
210
242
  toolCallId,
211
243
  status: 'completed',
212
244
  content: [{ type: 'content', content: { type: 'text', text: `ran ${command} (${String(outcome.optionId)})` } }]
213
245
  });
246
+
214
247
  say('done');
215
248
  endTurn('end_turn');
216
249
  });
250
+
217
251
  return;
218
252
  }
253
+
219
254
  update({ sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'Thinking.' } });
220
255
  say(`echo: ${text}${images > 0 ? ` (${images} images)` : ''}`);
221
256
  endTurn('end_turn');
@@ -224,6 +259,7 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
224
259
  const handle = (frame: Frame): void => {
225
260
  const id = frame.id;
226
261
  const params = (typeof frame.params === 'object' && frame.params !== null ? frame.params : {}) as Frame;
262
+
227
263
  if (frame.method === undefined) {
228
264
  // The client's answer to a permission request.
229
265
  if (permission !== null && id === permission.rpcId) {
@@ -232,8 +268,10 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
232
268
  const result = frame.result as { outcome?: Frame } | undefined;
233
269
  answered.onAnswer(result?.outcome ?? { outcome: 'cancelled' });
234
270
  }
271
+
235
272
  return;
236
273
  }
274
+
237
275
  switch (frame.method) {
238
276
  case 'initialize':
239
277
  respond(id, {
@@ -246,87 +284,115 @@ export function fakeAcpWith(options: FakeAcpOptions): FakeCli {
246
284
  authMethods: options.authMethod === undefined ? [] : [{ id: options.authMethod, name: 'Sign in' }],
247
285
  agentInfo: { name: 'fake-acp', title: 'Fake ACP', version: '0.0.1' }
248
286
  });
287
+
249
288
  return;
250
289
  case 'authenticate':
251
290
  authenticated = params.methodId === options.authMethod;
252
291
  respond(id, {});
292
+
253
293
  return;
254
294
  case 'session/new':
255
295
  if (!authenticated) {
256
296
  refuse(id, -32000, 'Authentication required');
297
+
257
298
  return;
258
299
  }
300
+
259
301
  sessionId = `fake-session-${Math.random().toString(36).slice(2, 8)}`;
260
302
  sessionHistory.set(sessionId, []);
303
+
261
304
  if (options.commands !== undefined) {
262
305
  update({
263
306
  sessionUpdate: 'available_commands_update',
264
307
  availableCommands: options.commands.map((name) => ({ name, description: name }))
265
308
  });
266
309
  }
310
+
267
311
  respond(id, { sessionId, ...sessionState() });
312
+
268
313
  return;
269
314
  case 'session/load':
270
315
  case 'session/resume': {
271
316
  const history = sessionHistory.get(String(params.sessionId));
317
+
272
318
  if (!authenticated) {
273
319
  refuse(id, -32000, 'Authentication required');
320
+
274
321
  return;
275
322
  }
323
+
276
324
  if (history === undefined) {
277
325
  refuse(id, -32002, 'Session not found');
326
+
278
327
  return;
279
328
  }
329
+
280
330
  sessionId = String(params.sessionId);
331
+
281
332
  if (frame.method === 'session/load') {
282
333
  for (const earlier of history) {
283
334
  update({ sessionUpdate: 'user_message_chunk', content: { type: 'text', text: earlier } });
284
335
  update({ sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'replayed' } });
285
336
  }
286
337
  }
338
+
287
339
  respond(id, sessionState());
340
+
288
341
  return;
289
342
  }
290
343
  case 'session/close':
291
344
  if (!options.closeSession) {
292
345
  refuse(id, -32601, 'Method not found: session/close');
346
+
293
347
  return;
294
348
  }
349
+
295
350
  sessionHistory.delete(String(params.sessionId));
296
351
  respond(id, {});
352
+
297
353
  return;
298
354
  case 'session/set_mode':
299
355
  mode = String(params.modeId);
300
356
  respond(id, {});
301
357
  update({ sessionUpdate: 'current_mode_update', currentModeId: mode });
358
+
302
359
  return;
303
360
  case 'session/set_model':
304
361
  model = String(params.modelId);
305
362
  respond(id, {});
363
+
306
364
  return;
307
365
  case 'session/set_config_option':
308
366
  if (params.configId === 'model') {
309
367
  model = String(params.value);
310
368
  }
369
+
311
370
  if (params.configId === 'reasoning_effort') {
312
371
  if (options.reasoningEffort === 'reject') {
313
372
  refuse(id, -32602, 'Reasoning effort is locked');
373
+
314
374
  return;
315
375
  }
376
+
316
377
  effort = String(params.value);
317
378
  }
379
+
318
380
  respond(id, { configOptions: sessionState().configOptions ?? [] });
381
+
319
382
  return;
320
383
  case 'session/prompt':
321
384
  handlePrompt(id, params);
385
+
322
386
  return;
323
387
  case 'session/cancel':
324
388
  if (prompt !== null) {
325
389
  prompt.cancelled = true;
390
+
326
391
  if (permission === null) {
327
392
  endTurn('cancelled');
328
393
  }
329
394
  }
395
+
330
396
  return;
331
397
  default:
332
398
  if (id !== undefined) {