@adecore/agents 0.39.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.
- 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.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/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-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/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
|
@@ -46,23 +46,28 @@ export interface TaskCoordinatorDeps {
|
|
|
46
46
|
function answerOf(items: readonly ChatItem[], turnId: string): string | null {
|
|
47
47
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
48
48
|
const item = items[i]!;
|
|
49
|
+
|
|
49
50
|
if (item.kind === 'assistant' && item.turnId === turnId && !item.parentToolUseId) {
|
|
50
51
|
const text = readableAssistantText(item).trim();
|
|
52
|
+
|
|
51
53
|
if (text !== '') {
|
|
52
54
|
return text;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function lastNote(items: readonly ChatItem[], turnId: string, levels: readonly string[]): string | null {
|
|
60
63
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
61
64
|
const item = items[i]!;
|
|
65
|
+
|
|
62
66
|
if (item.kind === 'note' && item.turnId === turnId && levels.includes(item.level)) {
|
|
63
67
|
return item.text;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
70
|
+
|
|
66
71
|
return null;
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -87,23 +92,24 @@ export function resultOfTurn(turn: ChatTurnItem, items: readonly ChatItem[], at:
|
|
|
87
92
|
if (turn.state === 'done') {
|
|
88
93
|
return { status: 'done', result: { text: answerOf(items, turn.id) ?? 'It ended its turn without an answer.', source: 'turn', at } };
|
|
89
94
|
}
|
|
95
|
+
|
|
90
96
|
if (turn.state === 'aborted') {
|
|
91
97
|
const text = abortedByMachine(turn, items)
|
|
92
98
|
? (lastNote(items, turn.id, ['warning']) ?? 'The machine stopped it.')
|
|
93
99
|
: 'It was stopped before it finished.';
|
|
100
|
+
|
|
94
101
|
return { status: 'failed', result: { text, source: 'turn', at } };
|
|
95
102
|
}
|
|
103
|
+
|
|
96
104
|
return { status: 'failed', result: { text: lastNote(items, turn.id, ['error']) ?? 'Its turn ended in an error.', source: 'turn', at } };
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
/*
|
|
100
108
|
* Settles tasks from what children do and does nothing else: it writes the task store and owes wakes
|
|
101
109
|
* in the outbox, never starts a turn, since it runs inside the broadcast of the child it heard. A chat
|
|
102
|
-
* child settles when a turn ends with
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* an agent that is not a chat, which the host tells it about, settles only on `done`, or fails when it
|
|
106
|
-
* ends without one.
|
|
110
|
+
* child settles when a turn ends with no task of its own open or waiting to wake it, no active goal and
|
|
111
|
+
* no background work running, or once its background commands outlast their limit. An agent that is
|
|
112
|
+
* not a chat settles only on `done`, or fails when it ends without one.
|
|
107
113
|
*/
|
|
108
114
|
export class TaskCoordinator {
|
|
109
115
|
private readonly deps: TaskCoordinatorDeps;
|
|
@@ -123,6 +129,7 @@ export class TaskCoordinator {
|
|
|
123
129
|
while (this.pending.size > 0) {
|
|
124
130
|
await Promise.allSettled([...this.pending]);
|
|
125
131
|
}
|
|
132
|
+
|
|
126
133
|
await this.deps.tasks.settled();
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -133,9 +140,11 @@ export class TaskCoordinator {
|
|
|
133
140
|
*/
|
|
134
141
|
async settleFromElsewhere(taskId: string, result: TaskResult, status: 'done' | 'failed' = 'done'): Promise<Task | null> {
|
|
135
142
|
const task = this.deps.tasks.get(taskId);
|
|
143
|
+
|
|
136
144
|
if (task?.machine === undefined || task.status !== 'open') {
|
|
137
145
|
return null;
|
|
138
146
|
}
|
|
147
|
+
|
|
139
148
|
return this.settleNow(task, status, result);
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -143,10 +152,13 @@ export class TaskCoordinator {
|
|
|
143
152
|
if (this.stopped || event.event !== 'chat.event') {
|
|
144
153
|
return;
|
|
145
154
|
}
|
|
155
|
+
|
|
146
156
|
const { chatId, event: chatEvent } = event.payload;
|
|
157
|
+
|
|
147
158
|
if (this.deps.loading?.(chatId)) {
|
|
148
159
|
return;
|
|
149
160
|
}
|
|
161
|
+
|
|
150
162
|
if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn' && chatEvent.item.state === 'running') {
|
|
151
163
|
this.turnStarted(chatId);
|
|
152
164
|
} else if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn') {
|
|
@@ -164,14 +176,9 @@ export class TaskCoordinator {
|
|
|
164
176
|
* which says how that kind of agent reports back.
|
|
165
177
|
*/
|
|
166
178
|
agentEnded(childId: string, text: string): void {
|
|
167
|
-
if (this.
|
|
168
|
-
|
|
179
|
+
if (this.deps.placed(childId)) {
|
|
180
|
+
this.failed(childId, text);
|
|
169
181
|
}
|
|
170
|
-
const task = this.localTask(childId);
|
|
171
|
-
if (!task || !this.deps.placed(childId)) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
startFailed(childId: string, error: unknown): void {
|
|
@@ -186,9 +193,11 @@ export class TaskCoordinator {
|
|
|
186
193
|
/* `done` from the child itself, which wins over whatever else was about to settle it. */
|
|
187
194
|
async done(childId: string, text: string): Promise<Task | null> {
|
|
188
195
|
const task = this.localTask(childId);
|
|
196
|
+
|
|
189
197
|
if (!task) {
|
|
190
198
|
return null;
|
|
191
199
|
}
|
|
200
|
+
|
|
192
201
|
return this.settleNow(task, 'done', { text, source: 'done', at: this.deps.now() });
|
|
193
202
|
}
|
|
194
203
|
|
|
@@ -200,17 +209,23 @@ export class TaskCoordinator {
|
|
|
200
209
|
async outlasted(childId: string, commands: readonly string[], cause: 'limit' | 'restart' = 'limit'): Promise<Task | null> {
|
|
201
210
|
const task = this.localTask(childId);
|
|
202
211
|
const items = this.deps.chatItems(childId);
|
|
212
|
+
|
|
203
213
|
if (this.stopped || !task || items === null || this.delegating(childId)) {
|
|
204
214
|
return null;
|
|
205
215
|
}
|
|
216
|
+
|
|
206
217
|
const last = items.findLast((item): item is ChatTurnItem => item.kind === 'turn');
|
|
218
|
+
|
|
207
219
|
if (!last || last.state === 'running' || !this.answers(items, last, task)) {
|
|
208
220
|
return null;
|
|
209
221
|
}
|
|
222
|
+
|
|
210
223
|
const { status, result } = resultOfTurn(last, items, this.deps.now());
|
|
224
|
+
|
|
211
225
|
if (cause === 'restart') {
|
|
212
226
|
return this.settleNow(task, 'failed', { ...result, text: `${result.text}\n\n${restartedNote(commands)}`, source: 'exit' });
|
|
213
227
|
}
|
|
228
|
+
|
|
214
229
|
return this.settleNow(task, status, { ...result, text: `${result.text}\n\n${outlastedNote(commands)}` });
|
|
215
230
|
}
|
|
216
231
|
|
|
@@ -224,15 +239,18 @@ export class TaskCoordinator {
|
|
|
224
239
|
/* The open task a chat here works on; one whose child runs on another machine is settled from there. */
|
|
225
240
|
private localTask(childId: string): Task | undefined {
|
|
226
241
|
const task = this.deps.tasks.openFor(childId);
|
|
242
|
+
|
|
227
243
|
return task?.machine === undefined ? task : undefined;
|
|
228
244
|
}
|
|
229
245
|
|
|
230
246
|
/* An open task of this child that nothing can carry any more. */
|
|
231
247
|
private failed(childId: string, text: string): void {
|
|
232
248
|
const task = this.localTask(childId);
|
|
249
|
+
|
|
233
250
|
if (!task || this.stopped) {
|
|
234
251
|
return;
|
|
235
252
|
}
|
|
253
|
+
|
|
236
254
|
this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
|
|
237
255
|
}
|
|
238
256
|
|
|
@@ -241,41 +259,53 @@ export class TaskCoordinator {
|
|
|
241
259
|
* gets a turn of its own that names it; the turn that was in its way answers the person who sent it,
|
|
242
260
|
* not the task, and nothing settles while that turn is still owed. A turn after it answers too, as the
|
|
243
261
|
* one the CLI opens about work that turn left running. A child opened with its task has no turn of that
|
|
244
|
-
* kind, so its first turn settles it
|
|
262
|
+
* kind, so its first turn settles it.
|
|
245
263
|
*/
|
|
246
264
|
private answers(items: readonly ChatItem[], turn: ChatTurnItem, task: Task): boolean {
|
|
247
265
|
if ((turn.taskIds ?? []).includes(task.id)) {
|
|
248
266
|
return true;
|
|
249
267
|
}
|
|
268
|
+
|
|
250
269
|
if (this.deps.owedTurn(task.id)) {
|
|
251
270
|
return false;
|
|
252
271
|
}
|
|
272
|
+
|
|
253
273
|
const own = items.findIndex((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id));
|
|
274
|
+
|
|
254
275
|
return own === -1 ? task.requiresTaskTurn !== true && turn.createdAt >= task.createdAt : own < items.findIndex((item) => item.id === turn.id);
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
private turnEnded(chatId: string, turn: ChatTurnItem): void {
|
|
258
279
|
const task = this.localTask(chatId);
|
|
259
280
|
const items = this.deps.chatItems(chatId);
|
|
281
|
+
|
|
260
282
|
if (!task || items === null || !this.answers(items, turn, task) || this.delegating(chatId)) {
|
|
261
283
|
return;
|
|
262
284
|
}
|
|
285
|
+
|
|
263
286
|
const paused = this.pauseFor(chatId, turn, items);
|
|
287
|
+
|
|
264
288
|
if (paused !== null) {
|
|
265
289
|
this.hold(task, paused);
|
|
290
|
+
|
|
266
291
|
return;
|
|
267
292
|
}
|
|
293
|
+
|
|
268
294
|
// A goal holds the task with no limit, and the parent sleeps on through the turns its CLI opens toward it.
|
|
269
295
|
const goal = turn.state === 'done' ? (this.deps.activeGoal?.(chatId) ?? null) : null;
|
|
296
|
+
|
|
270
297
|
if (goal?.running === true) {
|
|
271
298
|
return;
|
|
272
299
|
}
|
|
300
|
+
|
|
273
301
|
if (goal !== null) {
|
|
274
302
|
const { result } = resultOfTurn(turn, items, this.deps.now());
|
|
275
303
|
this.settle(task, 'failed', { ...result, text: `${result.text}\n\n${goalLostNote(goal.objective)}`, source: 'exit' });
|
|
304
|
+
|
|
276
305
|
return;
|
|
277
306
|
}
|
|
278
|
-
|
|
307
|
+
|
|
308
|
+
// A turn a person stopped or that failed ends the task, whatever still runs beside it.
|
|
279
309
|
if (turn.state === 'done' && this.heldInBackground(chatId, task, items)) {
|
|
280
310
|
this.track(
|
|
281
311
|
this.deps.tasks.holdBackground(task.id, {
|
|
@@ -285,21 +315,27 @@ export class TaskCoordinator {
|
|
|
285
315
|
}),
|
|
286
316
|
`Saving the background work of task ${task.id} failed`
|
|
287
317
|
);
|
|
318
|
+
|
|
288
319
|
return;
|
|
289
320
|
}
|
|
321
|
+
|
|
290
322
|
if (task.background?.turnId === turn.id) {
|
|
291
323
|
const after = this.deps.afterBackgroundWork(chatId);
|
|
324
|
+
|
|
292
325
|
if (after === 'reports') {
|
|
293
326
|
return;
|
|
294
327
|
}
|
|
328
|
+
|
|
295
329
|
if (after === 'gone') {
|
|
296
330
|
this.settleGone(task, task.background, turn, items);
|
|
331
|
+
|
|
297
332
|
return;
|
|
298
333
|
}
|
|
299
334
|
} else if (turn.state === 'done' && this.deps.limit.owed(task.id) && this.deps.afterBackgroundWork(chatId) === 'gone') {
|
|
300
335
|
// After a restart nothing here remembers the turn was held, but the limit it owes does, and that settles it.
|
|
301
336
|
return;
|
|
302
337
|
}
|
|
338
|
+
|
|
303
339
|
const { status, result } = resultOfTurn(turn, items, this.deps.now());
|
|
304
340
|
this.settle(task, status, result);
|
|
305
341
|
}
|
|
@@ -309,27 +345,38 @@ export class TaskCoordinator {
|
|
|
309
345
|
if (background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
|
|
310
346
|
return;
|
|
311
347
|
}
|
|
348
|
+
|
|
349
|
+
const rowOf = (id: string): ChatItem | undefined => items.find((item) => item.id === id);
|
|
350
|
+
|
|
312
351
|
const lost = background.itemIds.filter((id) => {
|
|
313
|
-
const item =
|
|
352
|
+
const item = rowOf(id);
|
|
353
|
+
|
|
314
354
|
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
315
355
|
});
|
|
356
|
+
|
|
316
357
|
if (lost.length > 0 || background.commands.length > 0) {
|
|
317
358
|
const names = lost.map((id) => {
|
|
318
|
-
const row =
|
|
359
|
+
const row = rowOf(id);
|
|
360
|
+
|
|
319
361
|
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
320
362
|
});
|
|
363
|
+
|
|
321
364
|
this.settle(task, 'failed', {
|
|
322
365
|
text: `The agent ended before its background work completed: ${[...names, ...background.commands].join(', ')}. This work will not send a result.`,
|
|
323
366
|
source: 'exit',
|
|
324
367
|
at: this.deps.now()
|
|
325
368
|
});
|
|
369
|
+
|
|
326
370
|
return;
|
|
327
371
|
}
|
|
372
|
+
|
|
328
373
|
const reports = background.itemIds.flatMap((id) => {
|
|
329
|
-
const row =
|
|
374
|
+
const row = rowOf(id);
|
|
330
375
|
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
376
|
+
|
|
331
377
|
return report ? [report] : [];
|
|
332
378
|
});
|
|
379
|
+
|
|
333
380
|
this.settle(task, 'done', {
|
|
334
381
|
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
335
382
|
source: 'turn',
|
|
@@ -347,17 +394,22 @@ export class TaskCoordinator {
|
|
|
347
394
|
if (this.deps.limit.owed(task.id)) {
|
|
348
395
|
void this.deps.limit.lapse(task.id).catch((e: unknown) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
|
|
349
396
|
}
|
|
397
|
+
|
|
350
398
|
return true;
|
|
351
399
|
}
|
|
400
|
+
|
|
352
401
|
const commands = this.deps.commandsOf(chatId);
|
|
402
|
+
|
|
353
403
|
if (commands.length === 0) {
|
|
354
404
|
return false;
|
|
355
405
|
}
|
|
406
|
+
|
|
356
407
|
if (!this.deps.limit.owed(task.id)) {
|
|
357
408
|
void this.deps.limit
|
|
358
409
|
.owe(task, commands.map(commandLabel), this.deps.now() + BACKGROUND_COMMAND_LIMIT_MS)
|
|
359
410
|
.catch((e: unknown) => console.error(`Owing the limit of task ${task.id} failed:`, errorText(e)));
|
|
360
411
|
}
|
|
412
|
+
|
|
361
413
|
return true;
|
|
362
414
|
}
|
|
363
415
|
|
|
@@ -367,19 +419,24 @@ export class TaskCoordinator {
|
|
|
367
419
|
*/
|
|
368
420
|
private pauseFor(chatId: string, turn: ChatTurnItem, items: readonly ChatItem[]): NonNullable<Task['paused']> | null {
|
|
369
421
|
const limit = turn.state === 'error' ? turn.limit : undefined;
|
|
422
|
+
|
|
370
423
|
if (limit?.kind === 'usage') {
|
|
371
424
|
return { kind: 'usage', ...(limit.resetsAt === undefined ? {} : { until: limit.resetsAt }) };
|
|
372
425
|
}
|
|
426
|
+
|
|
373
427
|
if (limit?.kind === 'overload') {
|
|
374
428
|
const at = this.deps.retryAt?.(chatId, turn, items) ?? null;
|
|
429
|
+
|
|
375
430
|
return at === null ? null : { kind: 'overload', until: at };
|
|
376
431
|
}
|
|
432
|
+
|
|
377
433
|
return null;
|
|
378
434
|
}
|
|
379
435
|
|
|
380
436
|
/* A child that took up work again is no longer waiting out a limit. */
|
|
381
437
|
private turnStarted(chatId: string): void {
|
|
382
438
|
const task = this.localTask(chatId);
|
|
439
|
+
|
|
383
440
|
if (task?.paused !== undefined) {
|
|
384
441
|
this.hold(task, null);
|
|
385
442
|
}
|
|
@@ -392,10 +449,13 @@ export class TaskCoordinator {
|
|
|
392
449
|
private recheck(chatId: string): void {
|
|
393
450
|
const task = this.localTask(chatId);
|
|
394
451
|
const items = this.deps.chatItems(chatId);
|
|
452
|
+
|
|
395
453
|
if (!task || items === null || this.delegating(chatId)) {
|
|
396
454
|
return;
|
|
397
455
|
}
|
|
456
|
+
|
|
398
457
|
const last = items.findLast((item): item is ChatTurnItem => item.kind === 'turn');
|
|
458
|
+
|
|
399
459
|
if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
|
|
400
460
|
this.turnEnded(chatId, last);
|
|
401
461
|
}
|
|
@@ -417,13 +477,17 @@ export class TaskCoordinator {
|
|
|
417
477
|
|
|
418
478
|
private async settleNow(task: Task, status: 'done' | 'failed', result: TaskResult): Promise<Task | null> {
|
|
419
479
|
const settled = await this.deps.tasks.settle(task.id, status, result, this.deps.now());
|
|
480
|
+
|
|
420
481
|
if (!settled) {
|
|
421
482
|
return null;
|
|
422
483
|
}
|
|
484
|
+
|
|
423
485
|
await this.deps.oweWake(settled);
|
|
486
|
+
|
|
424
487
|
if (status === 'failed') {
|
|
425
488
|
this.deps.alert(settled.childId, `Task failed: ${settled.title}`, clipText(result.text, 500));
|
|
426
489
|
}
|
|
490
|
+
|
|
427
491
|
return settled;
|
|
428
492
|
}
|
|
429
493
|
}
|
package/src/tasks/task-store.ts
CHANGED
|
@@ -40,6 +40,7 @@ export class TaskStore {
|
|
|
40
40
|
/* Told about every task that is written, after it is on disk. */
|
|
41
41
|
onChange(listener: TaskListener): () => void {
|
|
42
42
|
this.listeners.add(listener);
|
|
43
|
+
|
|
43
44
|
return () => {
|
|
44
45
|
this.listeners.delete(listener);
|
|
45
46
|
};
|
|
@@ -72,7 +73,9 @@ export class TaskStore {
|
|
|
72
73
|
settledAt: null,
|
|
73
74
|
wake: 'pending'
|
|
74
75
|
};
|
|
76
|
+
|
|
75
77
|
await this.write(task);
|
|
78
|
+
|
|
76
79
|
return task;
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -88,9 +91,11 @@ export class TaskStore {
|
|
|
88
91
|
|
|
89
92
|
async holdBackground(id: string, background: NonNullable<Task['background']>): Promise<void> {
|
|
90
93
|
const task = this.get(id);
|
|
94
|
+
|
|
91
95
|
if (!task || task.status !== 'open' || JSON.stringify(task.background) === JSON.stringify(background)) {
|
|
92
96
|
return;
|
|
93
97
|
}
|
|
98
|
+
|
|
94
99
|
await this.write({ ...task, background });
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -122,46 +127,57 @@ export class TaskStore {
|
|
|
122
127
|
*/
|
|
123
128
|
async settle(id: string, status: 'done' | 'failed' | 'cancelled', result: TaskResult | null, now: number): Promise<Task | null> {
|
|
124
129
|
const task = this.tasks.get(id);
|
|
130
|
+
|
|
125
131
|
if (!task || task.status !== 'open') {
|
|
126
132
|
return null;
|
|
127
133
|
}
|
|
134
|
+
|
|
128
135
|
const { paused: _paused, ...open } = task;
|
|
129
136
|
const settled: Task = { ...open, status, result, settledAt: now, wake: status === 'cancelled' ? 'none' : task.wake };
|
|
130
137
|
await this.write(settled);
|
|
138
|
+
|
|
131
139
|
return settled;
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
/* Holds an open task while its child waits out a limit, or lets it go on with null; false when it was not open. */
|
|
135
143
|
async pause(id: string, paused: NonNullable<Task['paused']> | null): Promise<boolean> {
|
|
136
144
|
const task = this.tasks.get(id);
|
|
145
|
+
|
|
137
146
|
if (!task || task.status !== 'open') {
|
|
138
147
|
return false;
|
|
139
148
|
}
|
|
149
|
+
|
|
140
150
|
if (JSON.stringify(task.paused ?? null) === JSON.stringify(paused)) {
|
|
141
151
|
return true;
|
|
142
152
|
}
|
|
153
|
+
|
|
143
154
|
const { paused: _was, ...rest } = task;
|
|
144
155
|
await this.write(paused === null ? rest : { ...rest, paused });
|
|
156
|
+
|
|
145
157
|
return true;
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
/* Cancels the open tasks of these children, with the reason as the result; a cancelled task wakes nobody. */
|
|
149
161
|
async cancelOpen(childIds: ReadonlySet<string>, reason: string, now: number): Promise<Task[]> {
|
|
150
162
|
const cancelled: Task[] = [];
|
|
163
|
+
|
|
151
164
|
for (const task of this.sorted()) {
|
|
152
165
|
if (task.status === 'open' && childIds.has(task.childId)) {
|
|
153
166
|
const settled = await this.settle(task.id, 'cancelled', { text: reason, source: 'exit', at: now }, now);
|
|
167
|
+
|
|
154
168
|
if (settled) {
|
|
155
169
|
cancelled.push(settled);
|
|
156
170
|
}
|
|
157
171
|
}
|
|
158
172
|
}
|
|
173
|
+
|
|
159
174
|
return cancelled;
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
async markWoken(ids: readonly string[]): Promise<void> {
|
|
163
178
|
for (const id of ids) {
|
|
164
179
|
const task = this.tasks.get(id);
|
|
180
|
+
|
|
165
181
|
if (task && task.wake === 'pending') {
|
|
166
182
|
await this.write({ ...task, wake: 'sent' });
|
|
167
183
|
}
|
|
@@ -178,12 +194,14 @@ export class TaskStore {
|
|
|
178
194
|
*/
|
|
179
195
|
readyWake(parentId: string): Task[] {
|
|
180
196
|
const open = new Set(this.openBatches(parentId));
|
|
197
|
+
|
|
181
198
|
return this.pendingWake(parentId).filter((task) => task.batchId === undefined || !open.has(task.batchId));
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
/* The batches of this parent with a task still open, oldest first. */
|
|
185
202
|
openBatches(parentId: string): string[] {
|
|
186
203
|
const ids = this.ofParent(parentId).flatMap((task) => (task.status === 'open' && task.batchId !== undefined ? [task.batchId] : []));
|
|
204
|
+
|
|
187
205
|
return [...new Set(ids)];
|
|
188
206
|
}
|
|
189
207
|
|
|
@@ -194,6 +212,7 @@ export class TaskStore {
|
|
|
194
212
|
async dropWake(parentId: string): Promise<void> {
|
|
195
213
|
for (const { id } of this.ofParent(parentId)) {
|
|
196
214
|
const task = this.tasks.get(id);
|
|
215
|
+
|
|
197
216
|
if (task && task.wake === 'pending') {
|
|
198
217
|
await this.write({ ...task, wake: 'none' });
|
|
199
218
|
}
|
|
@@ -206,21 +225,27 @@ export class TaskStore {
|
|
|
206
225
|
*/
|
|
207
226
|
async prune(projectId: string, ids: ReadonlySet<string>, now: number): Promise<Task[]> {
|
|
208
227
|
const cancelled: Task[] = [];
|
|
228
|
+
|
|
209
229
|
for (const task of this.sorted()) {
|
|
210
230
|
if (task.projectId !== projectId) {
|
|
211
231
|
continue;
|
|
212
232
|
}
|
|
233
|
+
|
|
213
234
|
if (!ids.has(task.parentId)) {
|
|
214
235
|
await this.tasks.remove(task.id);
|
|
236
|
+
|
|
215
237
|
continue;
|
|
216
238
|
}
|
|
239
|
+
|
|
217
240
|
if (!ids.has(task.childId) && task.status === 'open') {
|
|
218
241
|
const settled = await this.settle(task.id, 'cancelled', null, now);
|
|
242
|
+
|
|
219
243
|
if (settled) {
|
|
220
244
|
cancelled.push(settled);
|
|
221
245
|
}
|
|
222
246
|
}
|
|
223
247
|
}
|
|
248
|
+
|
|
224
249
|
return cancelled;
|
|
225
250
|
}
|
|
226
251
|
|
|
@@ -233,6 +258,7 @@ export class TaskStore {
|
|
|
233
258
|
const write = this.writeNow(task);
|
|
234
259
|
this.writes.add(write);
|
|
235
260
|
void write.finally(() => this.writes.delete(write)).catch(() => undefined);
|
|
261
|
+
|
|
236
262
|
return write;
|
|
237
263
|
}
|
|
238
264
|
|
|
@@ -241,9 +267,11 @@ export class TaskStore {
|
|
|
241
267
|
if (!(await this.tasks.write(task))) {
|
|
242
268
|
return;
|
|
243
269
|
}
|
|
270
|
+
|
|
244
271
|
for (const listener of this.listeners) {
|
|
245
272
|
listener(task);
|
|
246
273
|
}
|
|
274
|
+
|
|
247
275
|
this.sinks.emit({ event: 'task.changed', payload: { task } });
|
|
248
276
|
}
|
|
249
277
|
}
|
|
@@ -29,12 +29,14 @@ export function chatRequests(chats: Pick<ChatCore, 'get' | 'answer'>): ChatReque
|
|
|
29
29
|
// The projector writes each under the kind it is.
|
|
30
30
|
const thread = chats.get(nodeId)?.thread;
|
|
31
31
|
const item = thread?.get(`question-${requestId}`) ?? thread?.get(`approval-${requestId}`);
|
|
32
|
+
|
|
32
33
|
return item?.kind === 'question' || item?.kind === 'approval' ? item : null;
|
|
33
34
|
},
|
|
34
35
|
waiting: (nodeId) => (chats.get(nodeId)?.thread.list() ?? []).filter(isPendingRequest),
|
|
35
36
|
answer: (nodeId, requestId, answers) => {
|
|
36
37
|
try {
|
|
37
38
|
chats.answer(nodeId, requestId, answers);
|
|
39
|
+
|
|
38
40
|
return true;
|
|
39
41
|
} catch {
|
|
40
42
|
return false;
|
|
@@ -86,16 +88,22 @@ export class WaitingObserver {
|
|
|
86
88
|
if (this.stopped || event.event !== 'chat.event') {
|
|
87
89
|
return;
|
|
88
90
|
}
|
|
91
|
+
|
|
89
92
|
const { chatId, event: chatEvent } = event.payload;
|
|
93
|
+
|
|
90
94
|
if (chatEvent.type !== 'item' || !isPendingRequest(chatEvent.item)) {
|
|
91
95
|
return;
|
|
92
96
|
}
|
|
97
|
+
|
|
93
98
|
const task = this.deps.openTask(chatId);
|
|
94
99
|
const key = `${chatId}\n${chatEvent.item.requestId}`;
|
|
100
|
+
|
|
95
101
|
if (task === undefined || this.owed.has(key)) {
|
|
96
102
|
return;
|
|
97
103
|
}
|
|
104
|
+
|
|
98
105
|
this.owed.add(key);
|
|
106
|
+
|
|
99
107
|
void this.deps
|
|
100
108
|
.enqueue(
|
|
101
109
|
task.projectId,
|
|
@@ -120,16 +128,19 @@ const NOTE_QUESTION_LENGTH = 200;
|
|
|
120
128
|
|
|
121
129
|
function quotedShort(text: string): string {
|
|
122
130
|
const line = oneLine(text);
|
|
131
|
+
|
|
123
132
|
return JSON.stringify(line.length > NOTE_QUESTION_LENGTH ? `${clipText(line, NOTE_QUESTION_LENGTH - 1)}…` : line);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
/* One question as the parent's CLI reads it, with the choices it may pick from. */
|
|
127
136
|
function questionLine(question: ChatQuestionItem['questions'][number]): string {
|
|
128
137
|
const choices = question.choices.map((choice) => (choice.description === '' ? choice.label : `${choice.label} (${choice.description})`));
|
|
138
|
+
|
|
129
139
|
const how =
|
|
130
140
|
choices.length === 0
|
|
131
141
|
? 'answer in your own words'
|
|
132
142
|
: `choices: ${choices.join(' | ')}; ${question.multiSelect ? 'pick one or more' : 'pick one'}, or answer in your own words`;
|
|
143
|
+
|
|
133
144
|
return `- question ${question.id}: ${quoted(question.question)} (${how})`;
|
|
134
145
|
}
|
|
135
146
|
|
|
@@ -146,9 +157,12 @@ export interface WaitingWords {
|
|
|
146
157
|
function answerCall(cli: string, childId: string, item: ChatQuestionItem): string {
|
|
147
158
|
if (item.questions.length === 1) {
|
|
148
159
|
const multi = item.questions[0]!.multiSelect ? ', several labels in one --answer separated by a comma' : '';
|
|
160
|
+
|
|
149
161
|
return `${cli} answer ${childId} ${item.requestId} --answer '<a choice label as written, or your own words>'${multi}`;
|
|
150
162
|
}
|
|
163
|
+
|
|
151
164
|
const shape = Object.fromEntries(item.questions.map((question) => [question.id, '...']));
|
|
165
|
+
|
|
152
166
|
return `${cli} answer ${childId} ${item.requestId} --answers '${JSON.stringify(shape)}', one answer per question id`;
|
|
153
167
|
}
|
|
154
168
|
|
|
@@ -158,8 +172,10 @@ function answerCall(cli: string, childId: string, item: ChatQuestionItem): strin
|
|
|
158
172
|
*/
|
|
159
173
|
export function waitingTexts(child: { id: string; title: string }, item: WaitingRequest, words: WaitingWords): { note: string; preamble: string } {
|
|
160
174
|
const who = `${child.title} (node ${child.id})`;
|
|
175
|
+
|
|
161
176
|
if (item.kind === 'approval') {
|
|
162
177
|
const what = item.description === null ? item.toolName : `${item.toolName}: ${item.description}`;
|
|
178
|
+
|
|
163
179
|
return {
|
|
164
180
|
note: `${who} waits for a person to approve ${what}`,
|
|
165
181
|
preamble: [
|
|
@@ -168,11 +184,14 @@ export function waitingTexts(child: { id: string; title: string }, item: Waiting
|
|
|
168
184
|
].join('\n')
|
|
169
185
|
};
|
|
170
186
|
}
|
|
187
|
+
|
|
171
188
|
const asked = item.questions.length === 1 ? quotedShort(item.questions[0]!.question) : `${item.questions.length} questions`;
|
|
189
|
+
|
|
172
190
|
const how = [
|
|
173
191
|
...item.questions.map(questionLine),
|
|
174
192
|
`A person can answer it in that node, and this chat with ${answerCall(words.cli, child.id, item)}. Whoever answers first wins; the verb refuses a question that was answered already.`
|
|
175
193
|
];
|
|
194
|
+
|
|
176
195
|
return {
|
|
177
196
|
note: [`${who} waits for an answer to ${asked}`, '', ...how].join('\n'),
|
|
178
197
|
preamble: [
|
|
@@ -197,9 +216,11 @@ export function deliverWaitingHandler(deps: DeliverWaitingDeps) {
|
|
|
197
216
|
return async (entry: DeliverWaitingEntry): Promise<OutboxOutcome> => {
|
|
198
217
|
const { childId, requestId } = entry.payload;
|
|
199
218
|
const item = deps.request(childId, requestId);
|
|
219
|
+
|
|
200
220
|
if (!isPendingRequest(item)) {
|
|
201
221
|
return;
|
|
202
222
|
}
|
|
223
|
+
|
|
203
224
|
const texts = waitingTexts({ id: childId, title: deps.titleFor(childId) ?? childId }, item, deps.words);
|
|
204
225
|
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), from: childId, ...texts });
|
|
205
226
|
};
|