@adecore/agents 0.21.1 → 0.22.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.js +3 -8
- package/dist/chat/backend.d.ts +4 -0
- package/dist/chat/chat-core.d.ts +5 -2
- package/dist/chat/chat-core.js +8 -7
- package/dist/chat/chat-log.js +2 -2
- package/dist/chat/chat-session.d.ts +11 -2
- package/dist/chat/chat-session.js +141 -13
- package/dist/chat/chat-store.js +4 -7
- package/dist/chat/chat-title.js +4 -8
- package/dist/chat/claude-backend.d.ts +1 -0
- package/dist/chat/claude-backend.js +8 -6
- package/dist/chat/claude-protocol.d.ts +2 -0
- package/dist/chat/claude-protocol.js +21 -1
- package/dist/chat/claude-title.js +1 -1
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +1 -1
- package/dist/chat/codex-backend.d.ts +2 -2
- package/dist/chat/codex-backend.js +5 -8
- package/dist/chat/codex-protocol.js +1 -1
- package/dist/chat/codex-thread.js +4 -5
- package/dist/chat/composer-preferences.js +2 -3
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.d.ts +26 -0
- package/dist/chat/external-sessions.js +136 -0
- package/dist/chat/fake-claude.js +73 -0
- package/dist/chat/limit-resume.js +2 -7
- package/dist/chat/thread-search.d.ts +14 -0
- package/dist/chat/thread-search.js +64 -0
- package/dist/chat/visual-store.js +8 -6
- package/dist/context/refusal.js +3 -7
- package/dist/context/verb.js +1 -5
- package/dist/host/agent-host.js +2 -2
- package/dist/host/handlers.js +1 -2
- package/dist/host/wiring.js +1 -1
- package/dist/lineage.js +2 -3
- package/dist/messages/deliver-message.js +6 -11
- package/dist/messages/deliver-notice.js +8 -12
- package/dist/messages/notice-store.js +3 -5
- package/dist/outbox/outbox-worker.js +5 -2
- package/dist/providers/accounts/service.d.ts +2 -1
- package/dist/providers/accounts/service.js +11 -13
- package/dist/providers/catalog.js +2 -1
- package/dist/providers/claude.js +0 -1
- package/dist/providers/detect.js +2 -10
- package/dist/record-directory.js +0 -1
- package/dist/tasks/task-coordinator.d.ts +1 -0
- package/dist/tasks/task-coordinator.js +34 -34
- package/dist/tasks/task-store.js +1 -1
- package/dist/tasks/task-work.js +2 -2
- package/dist/tasks/waiting-child.d.ts +1 -0
- package/dist/tasks/waiting-child.js +8 -6
- package/dist/tasks/wake-parent.js +15 -10
- package/dist/tasks/wiring.js +32 -37
- package/dist/usage/aggregate.js +62 -46
- package/dist/usage/exchange.js +1 -1
- package/dist/usage/limits/monitor.d.ts +2 -2
- package/dist/usage/limits/monitor.js +2 -2
- package/dist/usage/limits/probe.js +1 -1
- package/dist/usage/pricing.js +1 -1
- package/dist/usage/readers/codex.d.ts +1 -0
- package/dist/usage/readers/codex.js +1 -1
- package/dist/usage/roots.js +1 -2
- package/dist/usage/scanner.js +7 -9
- package/dist/usage/usage-service.d.ts +1 -2
- package/dist/usage/usage-service.js +3 -6
- package/package.json +3 -3
- package/src/async.ts +3 -9
- package/src/chat/backend.ts +2 -0
- package/src/chat/chat-core.ts +8 -7
- package/src/chat/chat-log.ts +2 -2
- package/src/chat/chat-session.ts +148 -13
- package/src/chat/chat-store.ts +4 -8
- package/src/chat/chat-title.ts +4 -8
- package/src/chat/claude-backend.ts +9 -6
- package/src/chat/claude-protocol.ts +23 -1
- package/src/chat/claude-title.ts +1 -1
- package/src/chat/claude-transcript.ts +2 -2
- package/src/chat/codex-backend.ts +5 -8
- package/src/chat/codex-protocol.ts +1 -1
- package/src/chat/codex-thread.ts +6 -5
- package/src/chat/composer-preferences.ts +2 -3
- package/src/chat/errors.ts +2 -0
- package/src/chat/external-sessions.ts +177 -0
- package/src/chat/fake-claude.ts +75 -1
- package/src/chat/limit-resume.ts +2 -7
- package/src/chat/thread-search.ts +87 -0
- package/src/chat/thread.ts +1 -1
- package/src/chat/visual-store.ts +9 -6
- package/src/chat/wake-chat.ts +4 -1
- package/src/context/refusal.ts +3 -7
- package/src/context/verb.ts +1 -5
- package/src/host/agent-host.ts +2 -2
- package/src/host/handlers.ts +1 -2
- package/src/host/wiring.ts +1 -1
- package/src/lineage.ts +2 -3
- package/src/messages/deliver-message.ts +6 -11
- package/src/messages/deliver-notice.ts +8 -12
- package/src/messages/notice-store.ts +3 -5
- package/src/outbox/outbox-worker.ts +5 -2
- package/src/providers/accounts/service.ts +12 -13
- package/src/providers/accounts/variables.ts +1 -0
- package/src/providers/catalog.ts +2 -1
- package/src/providers/claude.ts +0 -1
- package/src/providers/detect.ts +2 -11
- package/src/record-directory.ts +0 -1
- package/src/skills.ts +0 -1
- package/src/tasks/task-coordinator.ts +35 -33
- package/src/tasks/task-store.ts +1 -1
- package/src/tasks/task-work.ts +2 -2
- package/src/tasks/waiting-child.ts +10 -7
- package/src/tasks/wake-parent.ts +21 -16
- package/src/tasks/wiring.ts +33 -41
- package/src/usage/aggregate.ts +80 -50
- package/src/usage/exchange.ts +1 -1
- package/src/usage/limits/monitor.ts +4 -4
- package/src/usage/limits/probe.ts +2 -2
- package/src/usage/pricing.ts +1 -1
- package/src/usage/readers/codex.ts +1 -1
- package/src/usage/roots.ts +1 -2
- package/src/usage/scanner.ts +8 -10
- package/src/usage/usage-service.ts +5 -9
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Whether the turn a chat is running was itself opened by a message
|
|
3
|
-
*
|
|
4
|
-
* and nothing about that started with a person. It rides on the turn, so it survives a restart.
|
|
2
|
+
* Whether the turn a chat is running was itself opened by a message, which stops two agents that
|
|
3
|
+
* read each other from waking each other forever. It rides on the turn, so it survives a restart.
|
|
5
4
|
* Between turns the last one counts, since work it left running (a background subagent) may still
|
|
6
|
-
* write, and a turn the CLI opened by itself
|
|
5
|
+
* write, and a turn the CLI opened by itself takes the step of the turn before it.
|
|
7
6
|
*/
|
|
8
7
|
export function turnFromMessage(items, activeTurnId) {
|
|
9
8
|
const from = activeTurnId === null ? items.findLastIndex((item) => item.kind === 'turn') : items.findIndex((item) => item.id === activeTurnId);
|
|
@@ -40,10 +39,9 @@ export function chatNoticeTargets(chats) {
|
|
|
40
39
|
}
|
|
41
40
|
/*
|
|
42
41
|
* One message on its way to a chat, and whether the chat owes a turn on it. A chat between turns gets
|
|
43
|
-
* one,
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* host runs something else in, such as a terminal, is the host's to deliver to before this.
|
|
42
|
+
* one, as for a settled task, or the message sits there until someone happens to prompt that chat.
|
|
43
|
+
* One step deep: the turn a message opened wakes nobody with a message of its own. A node that runs
|
|
44
|
+
* something else, such as a terminal, is the host's to deliver to before this.
|
|
47
45
|
*/
|
|
48
46
|
export async function deliverToChat(store, targets, notice) {
|
|
49
47
|
const waiting = await store.put(notice);
|
|
@@ -60,10 +58,8 @@ export async function deliverToChat(store, targets, notice) {
|
|
|
60
58
|
return { outcome: 'wake', waiting };
|
|
61
59
|
}
|
|
62
60
|
/*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* An id no chat holds shows nothing and marks nothing, so the chat that opens on that id later still
|
|
66
|
-
* has all of it.
|
|
61
|
+
* Shows a person the message in the chat's thread the moment it lands, not when the model gets round
|
|
62
|
+
* to it. An id no chat holds shows and marks nothing, so a chat that opens on it later still has all of it.
|
|
67
63
|
*/
|
|
68
64
|
export async function showNotices(store, chat, words, targetId) {
|
|
69
65
|
if (!(await chat.has(targetId))) {
|
|
@@ -5,10 +5,8 @@ import { RecordDirectory } from '../record-directory.js';
|
|
|
5
5
|
// Per receiver. Past this the oldest goes: an agent in a loop must not fill a queue nobody reads.
|
|
6
6
|
export const MAX_NOTICES = 10;
|
|
7
7
|
/*
|
|
8
|
-
* A message
|
|
9
|
-
*
|
|
10
|
-
* something that has moved on. The cap alone would not do it: one message in a queue of one waits
|
|
11
|
-
* for ever.
|
|
8
|
+
* A message is news about work happening now ("the build is green"), and a model reading it six hours
|
|
9
|
+
* late would act on something that moved on. The cap alone would leave a queue of one waiting forever.
|
|
12
10
|
*/
|
|
13
11
|
export const NOTICE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
14
12
|
const NoticeSchema = z.object({
|
|
@@ -109,7 +107,7 @@ export class NoticeStore {
|
|
|
109
107
|
fresh(notice) {
|
|
110
108
|
return this.now() - notice.createdAt < NOTICE_MAX_AGE_MS;
|
|
111
109
|
}
|
|
112
|
-
/*
|
|
110
|
+
/* In memory and on disk both; an empty queue leaves no file behind. */
|
|
113
111
|
async persist(targetId, queue) {
|
|
114
112
|
if (queue.length === 0) {
|
|
115
113
|
await this.queues.remove(targetId);
|
|
@@ -80,6 +80,9 @@ export class OutboxWorker {
|
|
|
80
80
|
const now = this.clock.now();
|
|
81
81
|
const busy = new Set(this.running);
|
|
82
82
|
let nextDue = null;
|
|
83
|
+
const dueAt = (at) => {
|
|
84
|
+
nextDue = nextDue === null ? at : Math.min(nextDue, at);
|
|
85
|
+
};
|
|
83
86
|
for (const entry of this.store.list()) {
|
|
84
87
|
// A waiting entry holds no lane. The resume of a chat must not queue behind a wake that waits for it.
|
|
85
88
|
if (busy.has(entry.target) || this.waiting.has(entry.id)) {
|
|
@@ -87,7 +90,7 @@ export class OutboxWorker {
|
|
|
87
90
|
}
|
|
88
91
|
// Nor does work due at a later time, which nothing owed before it has to wait for.
|
|
89
92
|
if (entry.attempts === 0 && entry.notBefore > now) {
|
|
90
|
-
|
|
93
|
+
dueAt(entry.notBefore);
|
|
91
94
|
continue;
|
|
92
95
|
}
|
|
93
96
|
const lanes = this.store.lanesOf(entry);
|
|
@@ -100,7 +103,7 @@ export class OutboxWorker {
|
|
|
100
103
|
continue;
|
|
101
104
|
}
|
|
102
105
|
if (entry.notBefore > now) {
|
|
103
|
-
|
|
106
|
+
dueAt(entry.notBefore);
|
|
104
107
|
continue;
|
|
105
108
|
}
|
|
106
109
|
void this.run(entry);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AgentKind, type ProviderAccount, type ProviderAccountCreatePayload, type ProviderAccountCreateResult, type ProviderAccountMap, type ProviderAccounts, type ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
import { type CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
2
3
|
import type { AgentSink } from '../../events.ts';
|
|
3
4
|
import type { ChatProvider } from '../provider.ts';
|
|
4
5
|
import { type Env } from './accounts.ts';
|
|
@@ -6,7 +7,6 @@ import { type AccountLaunches } from './launch.ts';
|
|
|
6
7
|
import { type ShadowHomeReport } from './shadow-home.ts';
|
|
7
8
|
import { type AskAccount } from './status.ts';
|
|
8
9
|
import { type AccountsHost, type SecretStore } from './variables.ts';
|
|
9
|
-
import { type CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
10
10
|
export interface ProviderAccountsOptions {
|
|
11
11
|
home: string;
|
|
12
12
|
host?: AccountsHost;
|
|
@@ -68,6 +68,7 @@ export declare class ProviderAccountsService implements AccountLaunches {
|
|
|
68
68
|
create(payload: ProviderAccountCreatePayload): Promise<ProviderAccountCreateResult>;
|
|
69
69
|
watchLogin(id: string): Promise<void>;
|
|
70
70
|
refresh(): Promise<ProviderAccounts>;
|
|
71
|
+
private folderOf;
|
|
71
72
|
private runWatch;
|
|
72
73
|
private serial;
|
|
73
74
|
private commit;
|
|
@@ -2,6 +2,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { AgentKindSchema } from '@adecore/agent-contracts';
|
|
5
|
+
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
5
6
|
import { ClientSinks } from '../../client-sinks.js';
|
|
6
7
|
import { errorText } from '../../error-text.js';
|
|
7
8
|
import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isKnownKind, transcriptFolder } from './accounts.js';
|
|
@@ -10,7 +11,6 @@ import { prepareShadowHome } from './shadow-home.js';
|
|
|
10
11
|
import { askAccountAs } from './status.js';
|
|
11
12
|
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts } from './store.js';
|
|
12
13
|
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError } from './variables.js';
|
|
13
|
-
import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
|
|
14
14
|
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
15
15
|
const CHECK_INTERVAL_MS = 15 * 60_000;
|
|
16
16
|
const MAX_BACKOFF_MS = 60 * 60_000;
|
|
@@ -161,20 +161,10 @@ export class ProviderAccountsService {
|
|
|
161
161
|
return accountEnv(key, account, provider, baseEnv, variables(account));
|
|
162
162
|
}
|
|
163
163
|
transcriptFolder(kind, id) {
|
|
164
|
-
|
|
165
|
-
const account = this.accounts[key];
|
|
166
|
-
if (account === undefined || account.kind !== kind) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
return transcriptFolder(key, account, this.providers.get(kind), this.env);
|
|
164
|
+
return this.folderOf(kind, id, transcriptFolder);
|
|
170
165
|
}
|
|
171
166
|
homeFolder(kind, id) {
|
|
172
|
-
|
|
173
|
-
const account = this.accounts[key];
|
|
174
|
-
if (account === undefined || account.kind !== kind) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
return accountFolder(key, account, this.providers.get(kind), this.env);
|
|
167
|
+
return this.folderOf(kind, id, accountFolder);
|
|
178
168
|
}
|
|
179
169
|
canContinue(kind, from, to) {
|
|
180
170
|
const fromId = from ?? kind;
|
|
@@ -268,6 +258,14 @@ export class ProviderAccountsService {
|
|
|
268
258
|
await this.queue(true);
|
|
269
259
|
return this.snapshot();
|
|
270
260
|
}
|
|
261
|
+
folderOf(kind, id, folder) {
|
|
262
|
+
const key = id ?? kind;
|
|
263
|
+
const account = this.accounts[key];
|
|
264
|
+
if (account === undefined || account.kind !== kind) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return folder(key, account, this.providers.get(kind), this.env);
|
|
268
|
+
}
|
|
271
269
|
async runWatch(id, watch) {
|
|
272
270
|
while (this.now() < watch.deadline) {
|
|
273
271
|
await this.sleep(LOGIN_POLL_MS);
|
|
@@ -33,7 +33,8 @@ export class ModelCatalog {
|
|
|
33
33
|
badge: model.badge,
|
|
34
34
|
legacy: model.legacy === true,
|
|
35
35
|
isDefault: model.slug === this.data.defaultModel,
|
|
36
|
-
options: this.profileOf(model.slug)?.options ?? []
|
|
36
|
+
options: this.profileOf(model.slug)?.options ?? [],
|
|
37
|
+
...(model.minCliVersion === undefined ? {} : { minCliVersion: model.minCliVersion })
|
|
37
38
|
}));
|
|
38
39
|
}
|
|
39
40
|
/* What a person reads for a model, so an error in a thread names it the way the picker does. */
|
package/dist/providers/claude.js
CHANGED
|
@@ -25,7 +25,6 @@ export function claudeRuntimeMode(mode) {
|
|
|
25
25
|
}
|
|
26
26
|
return Object.keys(PERMISSION_MODE).find((key) => PERMISSION_MODE[key] === mode);
|
|
27
27
|
}
|
|
28
|
-
/* The argument list for one Claude Code chat process. */
|
|
29
28
|
export function claudeArgs(launch) {
|
|
30
29
|
// The `=` form matters, since the flag is variadic and would swallow a prompt argument after it (measured on Claude Code 2.1.274).
|
|
31
30
|
const args = [
|
package/dist/providers/detect.js
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { spawnChatProcess } from '../chat/chat-process.js';
|
|
2
|
-
|
|
3
|
-
const defined = {};
|
|
4
|
-
for (const [key, value] of Object.entries(env)) {
|
|
5
|
-
if (value !== undefined) {
|
|
6
|
-
defined[key] = value;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
return defined;
|
|
10
|
-
}
|
|
2
|
+
import { definedEnv } from './accounts/launch.js';
|
|
11
3
|
/* Asks a CLI for its version; a missing binary is a plain "not installed", never an error. */
|
|
12
4
|
export async function detectCli(command, env = process.env, spawn = spawnChatProcess) {
|
|
13
5
|
try {
|
|
@@ -15,7 +7,7 @@ export async function detectCli(command, env = process.env, spawn = spawnChatPro
|
|
|
15
7
|
const code = new Promise((resolve) => {
|
|
16
8
|
exited = resolve;
|
|
17
9
|
});
|
|
18
|
-
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env:
|
|
10
|
+
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
|
|
19
11
|
proc.stdin.end();
|
|
20
12
|
const output = await new Response(proc.stdout).text();
|
|
21
13
|
if ((await code) !== 0) {
|
package/dist/record-directory.js
CHANGED
|
@@ -100,7 +100,6 @@ export class RecordDirectory {
|
|
|
100
100
|
this.records.delete(id);
|
|
101
101
|
return rm(this.pathOf(id), { force: true });
|
|
102
102
|
}
|
|
103
|
-
/* Drops every record the predicate says is gone. */
|
|
104
103
|
async prune(drop) {
|
|
105
104
|
for (const record of this.all()) {
|
|
106
105
|
if (drop(record)) {
|
|
@@ -71,7 +71,6 @@ export class TaskCoordinator {
|
|
|
71
71
|
}
|
|
72
72
|
await this.deps.tasks.settled();
|
|
73
73
|
}
|
|
74
|
-
/* For `chats.observe()`. */
|
|
75
74
|
chatEvent(event) {
|
|
76
75
|
if (this.stopped || event.event !== 'chat.event') {
|
|
77
76
|
return;
|
|
@@ -201,38 +200,7 @@ export class TaskCoordinator {
|
|
|
201
200
|
return;
|
|
202
201
|
}
|
|
203
202
|
if (after === 'gone') {
|
|
204
|
-
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const lost = task.background.itemIds.filter((id) => {
|
|
208
|
-
const item = items.find((item) => item.id === id);
|
|
209
|
-
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
210
|
-
});
|
|
211
|
-
if (lost.length > 0 || task.background.commands.length > 0) {
|
|
212
|
-
this.settle(task, 'failed', {
|
|
213
|
-
text: `The agent ended before its background work completed: ${[
|
|
214
|
-
...lost.map((id) => {
|
|
215
|
-
const row = items.find((item) => item.id === id);
|
|
216
|
-
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
217
|
-
}),
|
|
218
|
-
...task.background.commands
|
|
219
|
-
].join(', ')}. This work will not send a result.`,
|
|
220
|
-
source: 'exit',
|
|
221
|
-
at: this.deps.now()
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
const reports = task.background.itemIds.flatMap((id) => {
|
|
226
|
-
const row = items.find((item) => item.id === id);
|
|
227
|
-
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
228
|
-
return report ? [report] : [];
|
|
229
|
-
});
|
|
230
|
-
this.settle(task, 'done', {
|
|
231
|
-
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
232
|
-
source: 'turn',
|
|
233
|
-
at: this.deps.now()
|
|
234
|
-
});
|
|
235
|
-
}
|
|
203
|
+
this.settleGone(task, task.background, turn, items);
|
|
236
204
|
return;
|
|
237
205
|
}
|
|
238
206
|
}
|
|
@@ -243,6 +211,38 @@ export class TaskCoordinator {
|
|
|
243
211
|
const { status, result } = resultOfTurn(turn, items, this.deps.now());
|
|
244
212
|
this.settle(task, status, result);
|
|
245
213
|
}
|
|
214
|
+
/* The child's process went after its turn was held for background work; what that work left decides the task. */
|
|
215
|
+
settleGone(task, background, turn, items) {
|
|
216
|
+
if (background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const lost = background.itemIds.filter((id) => {
|
|
220
|
+
const item = items.find((item) => item.id === id);
|
|
221
|
+
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
222
|
+
});
|
|
223
|
+
if (lost.length > 0 || background.commands.length > 0) {
|
|
224
|
+
const names = lost.map((id) => {
|
|
225
|
+
const row = items.find((item) => item.id === id);
|
|
226
|
+
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
227
|
+
});
|
|
228
|
+
this.settle(task, 'failed', {
|
|
229
|
+
text: `The agent ended before its background work completed: ${[...names, ...background.commands].join(', ')}. This work will not send a result.`,
|
|
230
|
+
source: 'exit',
|
|
231
|
+
at: this.deps.now()
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const reports = background.itemIds.flatMap((id) => {
|
|
236
|
+
const row = items.find((item) => item.id === id);
|
|
237
|
+
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
238
|
+
return report ? [report] : [];
|
|
239
|
+
});
|
|
240
|
+
this.settle(task, 'done', {
|
|
241
|
+
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
242
|
+
source: 'turn',
|
|
243
|
+
at: this.deps.now()
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
246
|
/*
|
|
247
247
|
* Whether work in the background keeps this task open past the turn. Commands and monitors alone hold
|
|
248
248
|
* it only until the limit, owed from the moment they are all that is left; a subagent or a workflow
|
|
@@ -297,7 +297,7 @@ export class TaskCoordinator {
|
|
|
297
297
|
if (!task || items === null || this.delegating(chatId)) {
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
|
-
const last =
|
|
300
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
301
301
|
if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
|
|
302
302
|
this.turnEnded(chatId, last);
|
|
303
303
|
}
|
package/dist/tasks/task-store.js
CHANGED
|
@@ -80,7 +80,7 @@ export class TaskStore {
|
|
|
80
80
|
return this.sorted().filter((task) => task.projectId === projectId);
|
|
81
81
|
}
|
|
82
82
|
/*
|
|
83
|
-
* Ends an open task with its result;
|
|
83
|
+
* Ends an open task with its result; null when it was not open any more, which is how the first of
|
|
84
84
|
* `done`, the end of a turn and an exit wins. A cancelled task wakes nobody.
|
|
85
85
|
*/
|
|
86
86
|
async settle(id, status, result, now) {
|
package/dist/tasks/task-work.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export const BackgroundLimitWorkSchema = z.object({
|
|
3
3
|
kind: z.literal('background-limit'),
|
|
4
|
-
// The target is the child
|
|
5
|
-
// `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
4
|
+
// The target is the child. `commands` are what held its task open when the limit started, for a child whose
|
|
5
|
+
// process went since; `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
6
6
|
payload: z.object({ taskId: z.string().min(1), commands: z.array(z.string()), restarted: z.literal(true).optional() })
|
|
7
7
|
});
|
|
8
8
|
export const WakeParentWorkSchema = z.object({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clipText } from '@adecore/agent-contracts';
|
|
1
|
+
import { clipText, WAITING_NOTE_PREFIX } from '@adecore/agent-contracts';
|
|
2
2
|
import { errorText } from '../error-text.js';
|
|
3
3
|
export function isPendingRequest(item) {
|
|
4
4
|
return (item?.kind === 'question' && item.state === 'pending') || (item?.kind === 'approval' && item.decision === 'pending');
|
|
@@ -26,7 +26,7 @@ export function chatRequests(chats) {
|
|
|
26
26
|
}
|
|
27
27
|
/* The id of the note one request of one child leaves in its parent, so delivering it twice leaves one. */
|
|
28
28
|
export function waitingNoteId(childId, requestId) {
|
|
29
|
-
return
|
|
29
|
+
return `${WAITING_NOTE_PREFIX}${childId}-${requestId}`;
|
|
30
30
|
}
|
|
31
31
|
/*
|
|
32
32
|
* How long a request may wait before its parent hears of it. A person watching the child settles one
|
|
@@ -51,7 +51,6 @@ export class WaitingObserver {
|
|
|
51
51
|
stop() {
|
|
52
52
|
this.stopped = true;
|
|
53
53
|
}
|
|
54
|
-
/* For `chats.observe()`. */
|
|
55
54
|
chatEvent(event) {
|
|
56
55
|
if (this.stopped || event.event !== 'chat.event') {
|
|
57
56
|
return;
|
|
@@ -71,13 +70,16 @@ export class WaitingObserver {
|
|
|
71
70
|
.catch((e) => (this.deps.log ?? console.error)(`Owing a note about ${chatId} failed: ${errorText(e)}`));
|
|
72
71
|
}
|
|
73
72
|
}
|
|
73
|
+
function oneLine(text) {
|
|
74
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
75
|
+
}
|
|
74
76
|
function quoted(text) {
|
|
75
|
-
return JSON.stringify(text
|
|
77
|
+
return JSON.stringify(oneLine(text));
|
|
76
78
|
}
|
|
77
79
|
/* The one line a note shows before "Show more": a question longer than that is read in the node. */
|
|
78
80
|
const NOTE_QUESTION_LENGTH = 200;
|
|
79
81
|
function quotedShort(text) {
|
|
80
|
-
const line = text
|
|
82
|
+
const line = oneLine(text);
|
|
81
83
|
return JSON.stringify(line.length > NOTE_QUESTION_LENGTH ? `${clipText(line, NOTE_QUESTION_LENGTH - 1)}…` : line);
|
|
82
84
|
}
|
|
83
85
|
/* One question as the parent's CLI reads it, with the choices it may pick from. */
|
|
@@ -138,6 +140,6 @@ export function deliverWaitingHandler(deps) {
|
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
140
142
|
const texts = waitingTexts({ id: childId, title: deps.titleFor(childId) ?? childId }, item, deps.words);
|
|
141
|
-
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), ...texts });
|
|
143
|
+
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), from: childId, ...texts });
|
|
142
144
|
};
|
|
143
145
|
}
|
|
@@ -43,6 +43,19 @@ export function wakeLabel(tasks) {
|
|
|
43
43
|
export function wakeNote(tasks) {
|
|
44
44
|
return `Woken by ${tasks.length} finished ${tasks.length === 1 ? 'task' : 'tasks'}`;
|
|
45
45
|
}
|
|
46
|
+
/* An unacknowledged attempt waits for a person's next turn, rather than retrying on every chat event. */
|
|
47
|
+
function deliveryBlocked(items, tasks) {
|
|
48
|
+
return items.some((item, index) => item.kind === 'turn' &&
|
|
49
|
+
item.deliveryPending &&
|
|
50
|
+
item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
|
|
51
|
+
(item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent')));
|
|
52
|
+
}
|
|
53
|
+
function deliveryPending(items, tasks) {
|
|
54
|
+
return tasks.some((task) => {
|
|
55
|
+
const turn = items.findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
|
|
56
|
+
return turn?.kind === 'turn' && turn.deliveryPending;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
46
59
|
/*
|
|
47
60
|
* CLI acceptance lives in the saved turn before the task store consumes its result, so a restart
|
|
48
61
|
* between those writes can finish delivery without opening another turn.
|
|
@@ -75,12 +88,7 @@ export function wakeParentHandler(deps) {
|
|
|
75
88
|
if (tasks.length === 0) {
|
|
76
89
|
return held();
|
|
77
90
|
}
|
|
78
|
-
|
|
79
|
-
const blocked = items.some((item, index) => item.kind === 'turn' &&
|
|
80
|
-
item.deliveryPending &&
|
|
81
|
-
item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
|
|
82
|
-
(item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent')));
|
|
83
|
-
if (blocked) {
|
|
91
|
+
if (deliveryBlocked(items, tasks)) {
|
|
84
92
|
return 'wait';
|
|
85
93
|
}
|
|
86
94
|
const teamsOut = deps.tasks.openBatches(parentId).length;
|
|
@@ -93,10 +101,7 @@ export function wakeParentHandler(deps) {
|
|
|
93
101
|
return 'wait';
|
|
94
102
|
}
|
|
95
103
|
await chat.persist?.();
|
|
96
|
-
if (
|
|
97
|
-
const turn = chat.items().findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
|
|
98
|
-
return turn?.kind === 'turn' && turn.deliveryPending;
|
|
99
|
-
})) {
|
|
104
|
+
if (deliveryPending(chat.items(), tasks)) {
|
|
100
105
|
return 'wait';
|
|
101
106
|
}
|
|
102
107
|
await deps.tasks.markWoken(tasks.map((task) => task.id));
|
package/dist/tasks/wiring.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { reportsOnBackgroundWork } from '../chat/background-work.js';
|
|
2
2
|
import { chatOpener, loadChat } from '../chat/wake-chat.js';
|
|
3
3
|
import { errorText } from '../error-text.js';
|
|
4
|
+
import { chatNoticeTargets } from '../messages/deliver-notice.js';
|
|
4
5
|
import { backgroundLimitHandler, backgroundLimits } from './background-limit.js';
|
|
5
6
|
import { giveTaskHandler } from './give-task.js';
|
|
6
7
|
import { TaskCoordinator } from './task-coordinator.js';
|
|
@@ -16,6 +17,7 @@ export function wireTasks(deps) {
|
|
|
16
17
|
const { chats, outbox } = deps;
|
|
17
18
|
const enqueue = (projectId, target, work, notBefore) => outbox.enqueue(projectId, target, work, notBefore);
|
|
18
19
|
const limit = backgroundLimits(outbox);
|
|
20
|
+
const owesGiveTask = (taskId) => outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === taskId);
|
|
19
21
|
const coordinator = new TaskCoordinator({
|
|
20
22
|
tasks: deps.tasks,
|
|
21
23
|
now,
|
|
@@ -23,7 +25,7 @@ export function wireTasks(deps) {
|
|
|
23
25
|
placed: (nodeId) => deps.placed(nodeId),
|
|
24
26
|
loading: (chatId) => chats.isLoading(chatId),
|
|
25
27
|
// Until the turn that carries a task ran, no turn of the child is its answer.
|
|
26
|
-
owedTurn:
|
|
28
|
+
owedTurn: owesGiveTask,
|
|
27
29
|
oweWake: (task) => oweWake({ enqueue }, task),
|
|
28
30
|
alert: (nodeId, title, body) => deps.alert(nodeId, title, body),
|
|
29
31
|
...(deps.retryAt ? { retryAt: deps.retryAt } : {}),
|
|
@@ -58,38 +60,38 @@ export function wireTasks(deps) {
|
|
|
58
60
|
}
|
|
59
61
|
});
|
|
60
62
|
const chatFor = chatOpener({ chats, placed: deps.placed });
|
|
63
|
+
const targets = chatNoticeTargets(chats);
|
|
64
|
+
/* Owes again what an earlier run left of an open task, and lets its child's last turn settle it. */
|
|
65
|
+
const recoverOpen = async (task) => {
|
|
66
|
+
if (task.requiresTaskTurn === true && deps.placed(task.childId) && !owesGiveTask(task.id)) {
|
|
67
|
+
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
68
|
+
}
|
|
69
|
+
const stored = await chats.storedBackground(task.childId);
|
|
70
|
+
const storedWork = stored.items.length > 0 || stored.commands.length > 0;
|
|
71
|
+
if (!deps.placed(task.childId) || (task.background === undefined && task.requiresTaskTurn !== true && !storedWork)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (task.background === undefined && stored.turnId !== null && storedWork) {
|
|
75
|
+
await deps.tasks.holdBackground(task.id, {
|
|
76
|
+
turnId: stored.turnId,
|
|
77
|
+
itemIds: stored.items.map((item) => item.id),
|
|
78
|
+
commands: stored.commands
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
|
|
82
|
+
if (chat && chat.info.activeTurnId === null) {
|
|
83
|
+
const last = chat.thread.list().findLast((item) => item.kind === 'turn');
|
|
84
|
+
if (last?.kind === 'turn') {
|
|
85
|
+
coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
61
89
|
return {
|
|
62
90
|
coordinator,
|
|
63
91
|
recover: async () => {
|
|
64
92
|
for (const task of deps.tasks.all()) {
|
|
65
|
-
if (task.status === 'open'
|
|
66
|
-
|
|
67
|
-
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const storedBackground = task.status === 'open' ? await chats.storedBackground(task.childId) : { turnId: null, items: [], commands: [] };
|
|
71
|
-
if (task.status === 'open' &&
|
|
72
|
-
deps.placed(task.childId) &&
|
|
73
|
-
(task.background !== undefined ||
|
|
74
|
-
task.requiresTaskTurn === true ||
|
|
75
|
-
storedBackground.items.length > 0 ||
|
|
76
|
-
storedBackground.commands.length > 0)) {
|
|
77
|
-
if (task.background === undefined &&
|
|
78
|
-
storedBackground.turnId !== null &&
|
|
79
|
-
(storedBackground.items.length > 0 || storedBackground.commands.length > 0)) {
|
|
80
|
-
await deps.tasks.holdBackground(task.id, {
|
|
81
|
-
turnId: storedBackground.turnId,
|
|
82
|
-
itemIds: storedBackground.items.map((item) => item.id),
|
|
83
|
-
commands: storedBackground.commands
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
|
|
87
|
-
if (chat && chat.info.activeTurnId === null) {
|
|
88
|
-
const last = chat.thread.list().findLast((item) => item.kind === 'turn');
|
|
89
|
-
if (last?.kind === 'turn') {
|
|
90
|
-
coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
+
if (task.status === 'open') {
|
|
94
|
+
await recoverOpen(task);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
await coordinator.settled();
|
|
@@ -110,14 +112,7 @@ export function wireTasks(deps) {
|
|
|
110
112
|
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
111
113
|
return task;
|
|
112
114
|
},
|
|
113
|
-
chatState:
|
|
114
|
-
const session = chats.get(nodeId);
|
|
115
|
-
if (session) {
|
|
116
|
-
return session.info.activeTurnId === null ? 'idle' : 'running';
|
|
117
|
-
}
|
|
118
|
-
// A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
|
|
119
|
-
return (await chats.hasStored(nodeId)) ? 'idle' : 'none';
|
|
120
|
-
},
|
|
115
|
+
chatState: (nodeId) => targets.chat(nodeId),
|
|
121
116
|
done: (childId, text) => coordinator.done(childId, text),
|
|
122
117
|
involving: (nodeId) => deps.tasks.involving(nodeId)
|
|
123
118
|
},
|