@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.
- package/dist/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex-models.json +3 -2
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/prices-snapshot.json +8 -0
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-models.json +3 -2
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/prices-snapshot.json +8 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/tasks/wake-parent.ts
CHANGED
|
@@ -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
|
}
|
package/src/tasks/wiring.ts
CHANGED
|
@@ -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),
|
package/src/testing/accounts.ts
CHANGED
|
@@ -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':
|
package/src/testing/fake-acp.ts
CHANGED
|
@@ -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) {
|