@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
|
@@ -115,6 +115,9 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
115
115
|
const now = this.clock.now();
|
|
116
116
|
const busy = new Set(this.running);
|
|
117
117
|
let nextDue: number | null = null;
|
|
118
|
+
const dueAt = (at: number): void => {
|
|
119
|
+
nextDue = nextDue === null ? at : Math.min(nextDue, at);
|
|
120
|
+
};
|
|
118
121
|
for (const entry of this.store.list()) {
|
|
119
122
|
// A waiting entry holds no lane. The resume of a chat must not queue behind a wake that waits for it.
|
|
120
123
|
if (busy.has(entry.target) || this.waiting.has(entry.id)) {
|
|
@@ -122,7 +125,7 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
122
125
|
}
|
|
123
126
|
// Nor does work due at a later time, which nothing owed before it has to wait for.
|
|
124
127
|
if (entry.attempts === 0 && entry.notBefore > now) {
|
|
125
|
-
|
|
128
|
+
dueAt(entry.notBefore);
|
|
126
129
|
continue;
|
|
127
130
|
}
|
|
128
131
|
const lanes = this.store.lanesOf(entry);
|
|
@@ -135,7 +138,7 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
135
138
|
continue;
|
|
136
139
|
}
|
|
137
140
|
if (entry.notBefore > now) {
|
|
138
|
-
|
|
141
|
+
dueAt(entry.notBefore);
|
|
139
142
|
continue;
|
|
140
143
|
}
|
|
141
144
|
void this.run(entry);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ProviderAccountVariable,
|
|
14
14
|
type ProviderInfo
|
|
15
15
|
} from '@adecore/agent-contracts';
|
|
16
|
+
import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
16
17
|
import { ClientSinks } from '../../client-sinks.ts';
|
|
17
18
|
import { errorText } from '../../error-text.ts';
|
|
18
19
|
import type { AgentEvent, AgentSink } from '../../events.ts';
|
|
@@ -23,7 +24,6 @@ import { prepareShadowHome, type ShadowHomeReport } from './shadow-home.ts';
|
|
|
23
24
|
import { askAccountAs, type AskAccount } from './status.ts';
|
|
24
25
|
import { acceptAccounts, accountsPath, InvalidAccountError, readAccounts, wireAccounts, writeAccounts, type StoredAccounts } from './store.ts';
|
|
25
26
|
import { DEFAULT_ACCOUNTS_HOST, platformSecrets, secretKey, SecretsUnavailableError, type AccountsHost, type SecretStore } from './variables.ts';
|
|
26
|
-
import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../../chat/codex-transport.ts';
|
|
27
27
|
|
|
28
28
|
/* Who is signed in changes by hand and rarely; often enough to notice, rarely enough not to start CLIs all day. */
|
|
29
29
|
const CHECK_INTERVAL_MS = 15 * 60_000;
|
|
@@ -223,21 +223,11 @@ export class ProviderAccountsService implements AccountLaunches {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
transcriptFolder(kind: AgentKind, id: string | undefined): string | null {
|
|
226
|
-
|
|
227
|
-
const account = this.accounts[key];
|
|
228
|
-
if (account === undefined || account.kind !== kind) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return transcriptFolder(key, account, this.providers.get(kind), this.env);
|
|
226
|
+
return this.folderOf(kind, id, transcriptFolder);
|
|
232
227
|
}
|
|
233
228
|
|
|
234
229
|
homeFolder(kind: AgentKind, id: string | undefined): string | null {
|
|
235
|
-
|
|
236
|
-
const account = this.accounts[key];
|
|
237
|
-
if (account === undefined || account.kind !== kind) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
return accountFolder(key, account, this.providers.get(kind), this.env);
|
|
230
|
+
return this.folderOf(kind, id, accountFolder);
|
|
241
231
|
}
|
|
242
232
|
|
|
243
233
|
canContinue(kind: AgentKind, from: string | undefined, to: string | undefined): boolean {
|
|
@@ -343,6 +333,15 @@ export class ProviderAccountsService implements AccountLaunches {
|
|
|
343
333
|
return this.snapshot();
|
|
344
334
|
}
|
|
345
335
|
|
|
336
|
+
private folderOf(kind: AgentKind, id: string | undefined, folder: typeof accountFolder): string | null {
|
|
337
|
+
const key = id ?? kind;
|
|
338
|
+
const account = this.accounts[key];
|
|
339
|
+
if (account === undefined || account.kind !== kind) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
return folder(key, account, this.providers.get(kind), this.env);
|
|
343
|
+
}
|
|
344
|
+
|
|
346
345
|
private async runWatch(id: string, watch: LoginWatch): Promise<void> {
|
|
347
346
|
while (this.now() < watch.deadline) {
|
|
348
347
|
await this.sleep(LOGIN_POLL_MS);
|
|
@@ -84,6 +84,7 @@ export function redactVariables(variables: readonly ProviderAccountVariable[]):
|
|
|
84
84
|
export function keychainService(host: AccountsHost, home: string): string {
|
|
85
85
|
return `${host.keychainPrefix}-provider-env-${createHash('sha256').update(resolve(home)).digest('hex').slice(0, 12)}`;
|
|
86
86
|
}
|
|
87
|
+
|
|
87
88
|
// `security` exits with this when no item matches.
|
|
88
89
|
const NOT_FOUND = 44;
|
|
89
90
|
|
package/src/providers/catalog.ts
CHANGED
|
@@ -43,7 +43,8 @@ export class ModelCatalog {
|
|
|
43
43
|
badge: model.badge,
|
|
44
44
|
legacy: model.legacy === true,
|
|
45
45
|
isDefault: model.slug === this.data.defaultModel,
|
|
46
|
-
options: this.profileOf(model.slug)?.options ?? []
|
|
46
|
+
options: this.profileOf(model.slug)?.options ?? [],
|
|
47
|
+
...(model.minCliVersion === undefined ? {} : { minCliVersion: model.minCliVersion })
|
|
47
48
|
}));
|
|
48
49
|
}
|
|
49
50
|
|
package/src/providers/claude.ts
CHANGED
|
@@ -39,7 +39,6 @@ interface ClaudeLaunch {
|
|
|
39
39
|
folders?: readonly string[];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/* The argument list for one Claude Code chat process. */
|
|
43
42
|
export function claudeArgs(launch: ClaudeLaunch): string[] {
|
|
44
43
|
// The `=` form matters, since the flag is variadic and would swallow a prompt argument after it (measured on Claude Code 2.1.274).
|
|
45
44
|
const args = [
|
package/src/providers/detect.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import { spawnChatProcess, type SpawnChatProcess } from '../chat/chat-process.ts';
|
|
2
|
+
import { definedEnv } from './accounts/launch.ts';
|
|
2
3
|
|
|
3
4
|
export interface CliDetection {
|
|
4
5
|
installed: boolean;
|
|
5
6
|
version: string | null;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
function definedOnly(env: Record<string, string | undefined>): Record<string, string> {
|
|
9
|
-
const defined: Record<string, string> = {};
|
|
10
|
-
for (const [key, value] of Object.entries(env)) {
|
|
11
|
-
if (value !== undefined) {
|
|
12
|
-
defined[key] = value;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return defined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
/* Asks a CLI for its version; a missing binary is a plain "not installed", never an error. */
|
|
19
10
|
export async function detectCli(
|
|
20
11
|
command: string,
|
|
@@ -26,7 +17,7 @@ export async function detectCli(
|
|
|
26
17
|
const code = new Promise<number | null>((resolve) => {
|
|
27
18
|
exited = resolve;
|
|
28
19
|
});
|
|
29
|
-
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env:
|
|
20
|
+
const proc = spawn({ command: [command, '--version'], cwd: process.cwd(), env: definedEnv(env), onExit: (exitCode) => exited(exitCode) });
|
|
30
21
|
proc.stdin.end();
|
|
31
22
|
const output = await new Response(proc.stdout).text();
|
|
32
23
|
if ((await code) !== 0) {
|
package/src/record-directory.ts
CHANGED
|
@@ -121,7 +121,6 @@ export class RecordDirectory<T> {
|
|
|
121
121
|
return rm(this.pathOf(id), { force: true });
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/* Drops every record the predicate says is gone. */
|
|
125
124
|
async prune(drop: (record: T) => boolean): Promise<void> {
|
|
126
125
|
for (const record of this.all()) {
|
|
127
126
|
if (drop(record)) {
|
package/src/skills.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { dirname, join, parse } from 'node:path';
|
|
|
5
5
|
import type { AgentKind, ChatSkill, ChatSkillSource } from '@adecore/agent-contracts';
|
|
6
6
|
import { isNotFound } from './fs.ts';
|
|
7
7
|
|
|
8
|
-
// One folder full of skill folders, and what the CLI calls the skills it finds there.
|
|
9
8
|
export interface SkillRoot {
|
|
10
9
|
dir: string;
|
|
11
10
|
source: ChatSkillSource;
|
|
@@ -116,7 +116,6 @@ export class TaskCoordinator {
|
|
|
116
116
|
await this.deps.tasks.settled();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/* For `chats.observe()`. */
|
|
120
119
|
chatEvent(event: AgentEvent): void {
|
|
121
120
|
if (this.stopped || event.event !== 'chat.event') {
|
|
122
121
|
return;
|
|
@@ -255,37 +254,7 @@ export class TaskCoordinator {
|
|
|
255
254
|
return;
|
|
256
255
|
}
|
|
257
256
|
if (after === 'gone') {
|
|
258
|
-
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const lost = task.background.itemIds.filter((id) => {
|
|
262
|
-
const item = items.find((item) => item.id === id);
|
|
263
|
-
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
264
|
-
});
|
|
265
|
-
if (lost.length > 0 || task.background.commands.length > 0) {
|
|
266
|
-
this.settle(task, 'failed', {
|
|
267
|
-
text: `The agent ended before its background work completed: ${[
|
|
268
|
-
...lost.map((id) => {
|
|
269
|
-
const row = items.find((item) => item.id === id);
|
|
270
|
-
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
271
|
-
}),
|
|
272
|
-
...task.background.commands
|
|
273
|
-
].join(', ')}. This work will not send a result.`,
|
|
274
|
-
source: 'exit',
|
|
275
|
-
at: this.deps.now()
|
|
276
|
-
});
|
|
277
|
-
} else {
|
|
278
|
-
const reports = task.background.itemIds.flatMap((id) => {
|
|
279
|
-
const row = items.find((item) => item.id === id);
|
|
280
|
-
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
281
|
-
return report ? [report] : [];
|
|
282
|
-
});
|
|
283
|
-
this.settle(task, 'done', {
|
|
284
|
-
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
285
|
-
source: 'turn',
|
|
286
|
-
at: this.deps.now()
|
|
287
|
-
});
|
|
288
|
-
}
|
|
257
|
+
this.settleGone(task, task.background, turn, items);
|
|
289
258
|
return;
|
|
290
259
|
}
|
|
291
260
|
} else if (turn.state === 'done' && this.deps.limit.owed(task.id) && this.deps.afterBackgroundWork(chatId) === 'gone') {
|
|
@@ -296,6 +265,39 @@ export class TaskCoordinator {
|
|
|
296
265
|
this.settle(task, status, result);
|
|
297
266
|
}
|
|
298
267
|
|
|
268
|
+
/* The child's process went after its turn was held for background work; what that work left decides the task. */
|
|
269
|
+
private settleGone(task: Task, background: NonNullable<Task['background']>, turn: ChatTurnItem, items: readonly ChatItem[]): void {
|
|
270
|
+
if (background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const lost = background.itemIds.filter((id) => {
|
|
274
|
+
const item = items.find((item) => item.id === id);
|
|
275
|
+
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
276
|
+
});
|
|
277
|
+
if (lost.length > 0 || background.commands.length > 0) {
|
|
278
|
+
const names = lost.map((id) => {
|
|
279
|
+
const row = items.find((item) => item.id === id);
|
|
280
|
+
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
281
|
+
});
|
|
282
|
+
this.settle(task, 'failed', {
|
|
283
|
+
text: `The agent ended before its background work completed: ${[...names, ...background.commands].join(', ')}. This work will not send a result.`,
|
|
284
|
+
source: 'exit',
|
|
285
|
+
at: this.deps.now()
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const reports = background.itemIds.flatMap((id) => {
|
|
290
|
+
const row = items.find((item) => item.id === id);
|
|
291
|
+
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
292
|
+
return report ? [report] : [];
|
|
293
|
+
});
|
|
294
|
+
this.settle(task, 'done', {
|
|
295
|
+
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
296
|
+
source: 'turn',
|
|
297
|
+
at: this.deps.now()
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
299
301
|
/*
|
|
300
302
|
* Whether work in the background keeps this task open past the turn. Commands and monitors alone hold
|
|
301
303
|
* it only until the limit, owed from the moment they are all that is left; a subagent or a workflow
|
|
@@ -354,7 +356,7 @@ export class TaskCoordinator {
|
|
|
354
356
|
if (!task || items === null || this.delegating(chatId)) {
|
|
355
357
|
return;
|
|
356
358
|
}
|
|
357
|
-
const last =
|
|
359
|
+
const last = items.findLast((item): item is ChatTurnItem => item.kind === 'turn');
|
|
358
360
|
if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
|
|
359
361
|
this.turnEnded(chatId, last);
|
|
360
362
|
}
|
package/src/tasks/task-store.ts
CHANGED
|
@@ -108,7 +108,7 @@ export class TaskStore {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/*
|
|
111
|
-
* Ends an open task with its result;
|
|
111
|
+
* Ends an open task with its result; null when it was not open any more, which is how the first of
|
|
112
112
|
* `done`, the end of a turn and an exit wins. A cancelled task wakes nobody.
|
|
113
113
|
*/
|
|
114
114
|
async settle(id: string, status: 'done' | 'failed' | 'cancelled', result: TaskResult | null, now: number): Promise<Task | null> {
|
package/src/tasks/task-work.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { OutboxEntryOf, OutboxWorkShape } from '../outbox/outbox.ts';
|
|
|
3
3
|
|
|
4
4
|
export const BackgroundLimitWorkSchema = z.object({
|
|
5
5
|
kind: z.literal('background-limit'),
|
|
6
|
-
// The target is the child
|
|
7
|
-
// `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
6
|
+
// The target is the child. `commands` are what held its task open when the limit started, for a child whose
|
|
7
|
+
// process went since; `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
8
8
|
payload: z.object({ taskId: z.string().min(1), commands: z.array(z.string()), restarted: z.literal(true).optional() })
|
|
9
9
|
});
|
|
10
10
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clipText, type ChatApprovalItem, type ChatItem, type ChatQuestionItem, type Task } from '@adecore/agent-contracts';
|
|
1
|
+
import { clipText, WAITING_NOTE_PREFIX, type ChatApprovalItem, type ChatItem, type ChatQuestionItem, type Task } from '@adecore/agent-contracts';
|
|
2
2
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
3
|
import { errorText } from '../error-text.ts';
|
|
4
4
|
import type { AgentEvent } from '../events.ts';
|
|
@@ -45,7 +45,7 @@ export function chatRequests(chats: Pick<ChatCore, 'get' | 'answer'>): ChatReque
|
|
|
45
45
|
|
|
46
46
|
/* The id of the note one request of one child leaves in its parent, so delivering it twice leaves one. */
|
|
47
47
|
export function waitingNoteId(childId: string, requestId: string): string {
|
|
48
|
-
return
|
|
48
|
+
return `${WAITING_NOTE_PREFIX}${childId}-${requestId}`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/*
|
|
@@ -82,7 +82,6 @@ export class WaitingObserver {
|
|
|
82
82
|
this.stopped = true;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/* For `chats.observe()`. */
|
|
86
85
|
chatEvent(event: AgentEvent): void {
|
|
87
86
|
if (this.stopped || event.event !== 'chat.event') {
|
|
88
87
|
return;
|
|
@@ -108,15 +107,19 @@ export class WaitingObserver {
|
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
|
|
110
|
+
function oneLine(text: string): string {
|
|
111
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
111
114
|
function quoted(text: string): string {
|
|
112
|
-
return JSON.stringify(text
|
|
115
|
+
return JSON.stringify(oneLine(text));
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
/* The one line a note shows before "Show more": a question longer than that is read in the node. */
|
|
116
119
|
const NOTE_QUESTION_LENGTH = 200;
|
|
117
120
|
|
|
118
121
|
function quotedShort(text: string): string {
|
|
119
|
-
const line = text
|
|
122
|
+
const line = oneLine(text);
|
|
120
123
|
return JSON.stringify(line.length > NOTE_QUESTION_LENGTH ? `${clipText(line, NOTE_QUESTION_LENGTH - 1)}…` : line);
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -182,7 +185,7 @@ export function waitingTexts(child: { id: string; title: string }, item: Waiting
|
|
|
182
185
|
export interface DeliverWaitingDeps {
|
|
183
186
|
request: ChatRequests['request'];
|
|
184
187
|
titleFor(nodeId: string): string | null;
|
|
185
|
-
deliver(chatId: string, delivery: { noteId: string; note: string; preamble: string }): Promise<boolean>;
|
|
188
|
+
deliver(chatId: string, delivery: { noteId: string; note: string; from?: string; preamble: string }): Promise<boolean>;
|
|
186
189
|
words: WaitingWords;
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -198,6 +201,6 @@ export function deliverWaitingHandler(deps: DeliverWaitingDeps) {
|
|
|
198
201
|
return;
|
|
199
202
|
}
|
|
200
203
|
const texts = waitingTexts({ id: childId, title: deps.titleFor(childId) ?? childId }, item, deps.words);
|
|
201
|
-
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), ...texts });
|
|
204
|
+
await deps.deliver(entry.target, { noteId: waitingNoteId(childId, requestId), from: childId, ...texts });
|
|
202
205
|
};
|
|
203
206
|
}
|
package/src/tasks/wake-parent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Task } from '@adecore/agent-contracts';
|
|
1
|
+
import type { ChatItem, Task } from '@adecore/agent-contracts';
|
|
2
2
|
import type { WakeChat } from '../chat/wake-chat.ts';
|
|
3
3
|
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
4
4
|
import type { TaskStore } from './task-store.ts';
|
|
@@ -56,6 +56,24 @@ export function wakeNote(tasks: readonly Task[]): string {
|
|
|
56
56
|
return `Woken by ${tasks.length} finished ${tasks.length === 1 ? 'task' : 'tasks'}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/* An unacknowledged attempt waits for a person's next turn, rather than retrying on every chat event. */
|
|
60
|
+
function deliveryBlocked(items: readonly ChatItem[], tasks: readonly Task[]): boolean {
|
|
61
|
+
return items.some(
|
|
62
|
+
(item, index) =>
|
|
63
|
+
item.kind === 'turn' &&
|
|
64
|
+
item.deliveryPending &&
|
|
65
|
+
item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
|
|
66
|
+
(item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent'))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function deliveryPending(items: readonly ChatItem[], tasks: readonly Task[]): boolean {
|
|
71
|
+
return tasks.some((task) => {
|
|
72
|
+
const turn = items.findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
|
|
73
|
+
return turn?.kind === 'turn' && turn.deliveryPending;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
export interface WakeParentDeps {
|
|
60
78
|
tasks: Pick<TaskStore, 'pendingWake' | 'readyWake' | 'openBatches' | 'markWoken' | 'dropWake'>;
|
|
61
79
|
/* The chat as it is now, loading it from disk when nobody has; null for a chat that has no thread any more. */
|
|
@@ -96,15 +114,7 @@ export function wakeParentHandler(deps: WakeParentDeps) {
|
|
|
96
114
|
if (tasks.length === 0) {
|
|
97
115
|
return held();
|
|
98
116
|
}
|
|
99
|
-
|
|
100
|
-
const blocked = items.some(
|
|
101
|
-
(item, index) =>
|
|
102
|
-
item.kind === 'turn' &&
|
|
103
|
-
item.deliveryPending &&
|
|
104
|
-
item.taskIds?.some((id) => tasks.some((task) => task.id === id)) &&
|
|
105
|
-
(item.state === 'running' || !items.slice(index + 1).some((later) => later.kind === 'turn' && later.origin !== 'agent'))
|
|
106
|
-
);
|
|
107
|
-
if (blocked) {
|
|
117
|
+
if (deliveryBlocked(items, tasks)) {
|
|
108
118
|
return 'wait';
|
|
109
119
|
}
|
|
110
120
|
const teamsOut = deps.tasks.openBatches(parentId).length;
|
|
@@ -119,12 +129,7 @@ export function wakeParentHandler(deps: WakeParentDeps) {
|
|
|
119
129
|
return 'wait';
|
|
120
130
|
}
|
|
121
131
|
await chat.persist?.();
|
|
122
|
-
if (
|
|
123
|
-
tasks.some((task) => {
|
|
124
|
-
const turn = chat.items().findLast((item) => item.kind === 'turn' && item.taskIds?.includes(task.id));
|
|
125
|
-
return turn?.kind === 'turn' && turn.deliveryPending;
|
|
126
|
-
})
|
|
127
|
-
) {
|
|
132
|
+
if (deliveryPending(chat.items(), tasks)) {
|
|
128
133
|
return 'wait';
|
|
129
134
|
}
|
|
130
135
|
await deps.tasks.markWoken(tasks.map((task) => task.id));
|
package/src/tasks/wiring.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { reportsOnBackgroundWork } from '../chat/background-work.ts';
|
|
|
3
3
|
import type { ChatCore } from '../chat/chat-core.ts';
|
|
4
4
|
import { chatOpener, loadChat } from '../chat/wake-chat.ts';
|
|
5
5
|
import { errorText } from '../error-text.ts';
|
|
6
|
+
import { chatNoticeTargets } from '../messages/deliver-notice.ts';
|
|
6
7
|
import type { OutboxHandlers } from '../outbox/outbox-worker.ts';
|
|
7
8
|
import { backgroundLimitHandler, backgroundLimits } from './background-limit.ts';
|
|
8
9
|
import { giveTaskHandler } from './give-task.ts';
|
|
@@ -85,6 +86,7 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
|
|
|
85
86
|
const enqueue = (projectId: string, target: string, work: TaskWork, notBefore?: number): Promise<void> =>
|
|
86
87
|
outbox.enqueue(projectId, target, work, notBefore);
|
|
87
88
|
const limit = backgroundLimits(outbox);
|
|
89
|
+
const owesGiveTask = (taskId: string): boolean => outbox.list().some((entry) => isGiveTask(entry) && entry.payload.taskId === taskId);
|
|
88
90
|
const coordinator = new TaskCoordinator({
|
|
89
91
|
tasks: deps.tasks,
|
|
90
92
|
now,
|
|
@@ -92,7 +94,7 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
|
|
|
92
94
|
placed: (nodeId) => deps.placed(nodeId),
|
|
93
95
|
loading: (chatId) => chats.isLoading(chatId),
|
|
94
96
|
// Until the turn that carries a task ran, no turn of the child is its answer.
|
|
95
|
-
owedTurn:
|
|
97
|
+
owedTurn: owesGiveTask,
|
|
96
98
|
oweWake: (task) => oweWake({ enqueue }, task),
|
|
97
99
|
alert: (nodeId, title, body) => deps.alert(nodeId, title, body),
|
|
98
100
|
...(deps.retryAt ? { retryAt: deps.retryAt } : {}),
|
|
@@ -131,43 +133,40 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
|
|
|
131
133
|
});
|
|
132
134
|
|
|
133
135
|
const chatFor = chatOpener({ chats, placed: deps.placed });
|
|
136
|
+
const targets = chatNoticeTargets(chats);
|
|
137
|
+
|
|
138
|
+
/* Owes again what an earlier run left of an open task, and lets its child's last turn settle it. */
|
|
139
|
+
const recoverOpen = async (task: Task): Promise<void> => {
|
|
140
|
+
if (task.requiresTaskTurn === true && deps.placed(task.childId) && !owesGiveTask(task.id)) {
|
|
141
|
+
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
142
|
+
}
|
|
143
|
+
const stored = await chats.storedBackground(task.childId);
|
|
144
|
+
const storedWork = stored.items.length > 0 || stored.commands.length > 0;
|
|
145
|
+
if (!deps.placed(task.childId) || (task.background === undefined && task.requiresTaskTurn !== true && !storedWork)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (task.background === undefined && stored.turnId !== null && storedWork) {
|
|
149
|
+
await deps.tasks.holdBackground(task.id, {
|
|
150
|
+
turnId: stored.turnId,
|
|
151
|
+
itemIds: stored.items.map((item) => item.id),
|
|
152
|
+
commands: stored.commands
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
|
|
156
|
+
if (chat && chat.info.activeTurnId === null) {
|
|
157
|
+
const last = chat.thread.list().findLast((item) => item.kind === 'turn');
|
|
158
|
+
if (last?.kind === 'turn') {
|
|
159
|
+
coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
134
163
|
|
|
135
164
|
return {
|
|
136
165
|
coordinator,
|
|
137
166
|
recover: async () => {
|
|
138
167
|
for (const task of deps.tasks.all()) {
|
|
139
|
-
if (task.status === 'open'
|
|
140
|
-
|
|
141
|
-
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const storedBackground = task.status === 'open' ? await chats.storedBackground(task.childId) : { turnId: null, items: [], commands: [] };
|
|
145
|
-
if (
|
|
146
|
-
task.status === 'open' &&
|
|
147
|
-
deps.placed(task.childId) &&
|
|
148
|
-
(task.background !== undefined ||
|
|
149
|
-
task.requiresTaskTurn === true ||
|
|
150
|
-
storedBackground.items.length > 0 ||
|
|
151
|
-
storedBackground.commands.length > 0)
|
|
152
|
-
) {
|
|
153
|
-
if (
|
|
154
|
-
task.background === undefined &&
|
|
155
|
-
storedBackground.turnId !== null &&
|
|
156
|
-
(storedBackground.items.length > 0 || storedBackground.commands.length > 0)
|
|
157
|
-
) {
|
|
158
|
-
await deps.tasks.holdBackground(task.id, {
|
|
159
|
-
turnId: storedBackground.turnId,
|
|
160
|
-
itemIds: storedBackground.items.map((item) => item.id),
|
|
161
|
-
commands: storedBackground.commands
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
const chat = await loadChat({ chats, placed: deps.placed }, task.childId);
|
|
165
|
-
if (chat && chat.info.activeTurnId === null) {
|
|
166
|
-
const last = chat.thread.list().findLast((item) => item.kind === 'turn');
|
|
167
|
-
if (last?.kind === 'turn') {
|
|
168
|
-
coordinator.chatEvent({ event: 'chat.event', payload: { chatId: task.childId, event: { type: 'item', item: last } } });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
168
|
+
if (task.status === 'open') {
|
|
169
|
+
await recoverOpen(task);
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
await coordinator.settled();
|
|
@@ -190,14 +189,7 @@ export function wireTasks(deps: TaskWiringDeps): TaskWiring {
|
|
|
190
189
|
await enqueue(task.projectId, task.childId, { kind: 'give-task', payload: { taskId: task.id } });
|
|
191
190
|
return task;
|
|
192
191
|
},
|
|
193
|
-
chatState:
|
|
194
|
-
const session = chats.get(nodeId);
|
|
195
|
-
if (session) {
|
|
196
|
-
return session.info.activeTurnId === null ? 'idle' : 'running';
|
|
197
|
-
}
|
|
198
|
-
// A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
|
|
199
|
-
return (await chats.hasStored(nodeId)) ? 'idle' : 'none';
|
|
200
|
-
},
|
|
192
|
+
chatState: (nodeId) => targets.chat(nodeId),
|
|
201
193
|
done: (childId, text) => coordinator.done(childId, text),
|
|
202
194
|
involving: (nodeId) => deps.tasks.involving(nodeId)
|
|
203
195
|
},
|