@adecore/agents 0.0.1 → 0.17.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +33 -0
- package/dist/async.d.ts +2 -0
- package/dist/async.js +31 -0
- package/dist/chat/attachment-store.d.ts +10 -0
- package/dist/chat/attachment-store.js +93 -0
- package/dist/chat/backend.d.ts +214 -0
- package/dist/chat/backend.js +1 -0
- package/dist/chat/background-work.d.ts +8 -0
- package/dist/chat/background-work.js +33 -0
- package/dist/chat/bookmark-store.d.ts +25 -0
- package/dist/chat/bookmark-store.js +141 -0
- package/dist/chat/chat-core.d.ts +311 -0
- package/dist/chat/chat-core.js +1025 -0
- package/dist/chat/chat-log.d.ts +33 -0
- package/dist/chat/chat-log.js +163 -0
- package/dist/chat/chat-process.d.ts +46 -0
- package/dist/chat/chat-process.js +190 -0
- package/dist/chat/chat-session.d.ts +209 -0
- package/dist/chat/chat-session.js +1511 -0
- package/dist/chat/chat-store.d.ts +37 -0
- package/dist/chat/chat-store.js +202 -0
- package/dist/chat/chat-title.d.ts +10 -0
- package/dist/chat/chat-title.js +74 -0
- package/dist/chat/claude-backend.d.ts +35 -0
- package/dist/chat/claude-backend.js +214 -0
- package/dist/chat/claude-protocol.d.ts +41 -0
- package/dist/chat/claude-protocol.js +642 -0
- package/dist/chat/claude-title.d.ts +11 -0
- package/dist/chat/claude-title.js +97 -0
- package/dist/chat/claude-transcript.d.ts +28 -0
- package/dist/chat/claude-transcript.js +211 -0
- package/dist/chat/codex-backend.d.ts +51 -0
- package/dist/chat/codex-backend.js +364 -0
- package/dist/chat/codex-protocol.d.ts +56 -0
- package/dist/chat/codex-protocol.js +767 -0
- package/dist/chat/codex-thread.d.ts +32 -0
- package/dist/chat/codex-thread.js +131 -0
- package/dist/chat/codex-transport.d.ts +42 -0
- package/dist/chat/codex-transport.js +188 -0
- package/dist/chat/composer-preferences.d.ts +15 -0
- package/dist/chat/composer-preferences.js +46 -0
- package/dist/chat/context-breakdown.d.ts +2 -0
- package/dist/chat/context-breakdown.js +74 -0
- package/dist/chat/delta-coalescer.d.ts +12 -0
- package/dist/chat/delta-coalescer.js +55 -0
- package/dist/chat/errors.d.ts +4 -0
- package/dist/chat/errors.js +4 -0
- package/dist/chat/fake-claude.d.ts +3 -0
- package/dist/chat/fake-claude.js +982 -0
- package/dist/chat/fake-cli.d.ts +25 -0
- package/dist/chat/fake-cli.js +157 -0
- package/dist/chat/fake-codex.d.ts +10 -0
- package/dist/chat/fake-codex.js +502 -0
- package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/dist/chat/input.d.ts +19 -0
- package/dist/chat/input.js +71 -0
- package/dist/chat/limit-resume.d.ts +11 -0
- package/dist/chat/limit-resume.js +68 -0
- package/dist/chat/projector.d.ts +60 -0
- package/dist/chat/projector.js +860 -0
- package/dist/chat/request-summary.d.ts +3 -0
- package/dist/chat/request-summary.js +143 -0
- package/dist/chat/subagent-projection.d.ts +8 -0
- package/dist/chat/subagent-projection.js +40 -0
- package/dist/chat/subagent-reader.d.ts +66 -0
- package/dist/chat/subagent-reader.js +396 -0
- package/dist/chat/subagent-settlement.d.ts +6 -0
- package/dist/chat/subagent-settlement.js +103 -0
- package/dist/chat/thread.d.ts +30 -0
- package/dist/chat/thread.js +147 -0
- package/dist/chat/wake-chat.d.ts +20 -0
- package/dist/chat/wake-chat.js +27 -0
- package/dist/client-sinks.d.ts +9 -0
- package/dist/client-sinks.js +34 -0
- package/dist/coded-error.d.ts +5 -0
- package/dist/coded-error.js +17 -0
- package/dist/context/argv.d.ts +11 -0
- package/dist/context/argv.js +58 -0
- package/dist/context/refusal.d.ts +9 -0
- package/dist/context/refusal.js +38 -0
- package/dist/context/verb.d.ts +85 -0
- package/dist/context/verb.js +233 -0
- package/dist/error-text.d.ts +5 -0
- package/dist/error-text.js +24 -0
- package/dist/events.d.ts +12 -0
- package/dist/events.js +1 -0
- package/dist/fs.d.ts +9 -0
- package/dist/fs.js +107 -0
- package/dist/host/agent-host.d.ts +28 -0
- package/dist/host/agent-host.js +105 -0
- package/dist/host/environment.d.ts +7 -0
- package/dist/host/environment.js +22 -0
- package/dist/host/handlers.d.ts +30 -0
- package/dist/host/handlers.js +111 -0
- package/dist/host/memory-port.d.ts +2 -0
- package/dist/host/memory-port.js +25 -0
- package/dist/host/wiring.d.ts +40 -0
- package/dist/host/wiring.js +120 -0
- package/dist/lineage.d.ts +44 -0
- package/dist/lineage.js +134 -0
- package/dist/messages/deliver-message.d.ts +21 -0
- package/dist/messages/deliver-message.js +42 -0
- package/dist/messages/deliver-notice.d.ts +25 -0
- package/dist/messages/deliver-notice.js +75 -0
- package/dist/messages/notice-notes.d.ts +15 -0
- package/dist/messages/notice-notes.js +26 -0
- package/dist/messages/notice-store.d.ts +30 -0
- package/dist/messages/notice-store.js +120 -0
- package/dist/modes.d.ts +11 -0
- package/dist/modes.js +28 -0
- package/dist/outbox/manual-clock.d.ts +10 -0
- package/dist/outbox/manual-clock.js +25 -0
- package/dist/outbox/outbox-worker.d.ts +44 -0
- package/dist/outbox/outbox-worker.js +173 -0
- package/dist/outbox/outbox.d.ts +37 -0
- package/dist/outbox/outbox.js +76 -0
- package/dist/providers/accounts/accounts.d.ts +20 -0
- package/dist/providers/accounts/accounts.js +158 -0
- package/dist/providers/accounts/launch.d.ts +17 -0
- package/dist/providers/accounts/launch.js +30 -0
- package/dist/providers/accounts/service.d.ts +87 -0
- package/dist/providers/accounts/service.js +538 -0
- package/dist/providers/accounts/shadow-home.d.ts +8 -0
- package/dist/providers/accounts/shadow-home.js +70 -0
- package/dist/providers/accounts/status.d.ts +12 -0
- package/dist/providers/accounts/status.js +85 -0
- package/dist/providers/accounts/store.d.ts +14 -0
- package/dist/providers/accounts/store.js +112 -0
- package/dist/providers/accounts/test-accounts.d.ts +12 -0
- package/dist/providers/accounts/test-accounts.js +38 -0
- package/dist/providers/accounts/variables.d.ts +27 -0
- package/dist/providers/accounts/variables.js +94 -0
- package/dist/providers/catalog.d.ts +17 -0
- package/dist/providers/catalog.js +76 -0
- package/dist/providers/claude-models.json +114 -0
- package/dist/providers/claude-provider.d.ts +4 -0
- package/dist/providers/claude-provider.js +29 -0
- package/dist/providers/claude.d.ts +15 -0
- package/dist/providers/claude.js +105 -0
- package/dist/providers/codex-models.json +135 -0
- package/dist/providers/codex-provider.d.ts +4 -0
- package/dist/providers/codex-provider.js +27 -0
- package/dist/providers/codex-rules.d.ts +9 -0
- package/dist/providers/codex-rules.js +35 -0
- package/dist/providers/codex.d.ts +14 -0
- package/dist/providers/codex.js +52 -0
- package/dist/providers/detect.d.ts +6 -0
- package/dist/providers/detect.js +30 -0
- package/dist/providers/provider.d.ts +25 -0
- package/dist/providers/provider.js +1 -0
- package/dist/providers/registry.d.ts +28 -0
- package/dist/providers/registry.js +118 -0
- package/dist/record-directory.d.ts +27 -0
- package/dist/record-directory.js +114 -0
- package/dist/run-process.d.ts +13 -0
- package/dist/run-process.js +42 -0
- package/dist/serializer.d.ts +9 -0
- package/dist/serializer.js +34 -0
- package/dist/skills.d.ts +21 -0
- package/dist/skills.js +174 -0
- package/dist/tasks/background-limit.d.ts +16 -0
- package/dist/tasks/background-limit.js +55 -0
- package/dist/tasks/end-children.d.ts +34 -0
- package/dist/tasks/end-children.js +53 -0
- package/dist/tasks/give-task.d.ts +14 -0
- package/dist/tasks/give-task.js +33 -0
- package/dist/tasks/task-coordinator.d.ts +52 -0
- package/dist/tasks/task-coordinator.js +323 -0
- package/dist/tasks/task-store.d.ts +49 -0
- package/dist/tasks/task-store.js +197 -0
- package/dist/tasks/task-work.d.ts +47 -0
- package/dist/tasks/task-work.js +28 -0
- package/dist/tasks/waiting-child.d.ts +51 -0
- package/dist/tasks/waiting-child.js +143 -0
- package/dist/tasks/wake-parent.d.ts +19 -0
- package/dist/tasks/wake-parent.js +109 -0
- package/dist/tasks/wiring.d.ts +51 -0
- package/dist/tasks/wiring.js +151 -0
- package/dist/title-file.d.ts +2 -0
- package/dist/title-file.js +55 -0
- package/dist/usage/accounts.d.ts +15 -0
- package/dist/usage/accounts.js +64 -0
- package/dist/usage/aggregate.d.ts +12 -0
- package/dist/usage/aggregate.js +154 -0
- package/dist/usage/exchange.d.ts +16 -0
- package/dist/usage/exchange.js +84 -0
- package/dist/usage/index-file.d.ts +15 -0
- package/dist/usage/index-file.js +123 -0
- package/dist/usage/limits/monitor.d.ts +50 -0
- package/dist/usage/limits/monitor.js +179 -0
- package/dist/usage/limits/normalize.d.ts +21 -0
- package/dist/usage/limits/normalize.js +160 -0
- package/dist/usage/limits/probe.d.ts +9 -0
- package/dist/usage/limits/probe.js +113 -0
- package/dist/usage/prices-snapshot.json +1071 -0
- package/dist/usage/pricing.d.ts +34 -0
- package/dist/usage/pricing.js +186 -0
- package/dist/usage/projects.d.ts +17 -0
- package/dist/usage/projects.js +49 -0
- package/dist/usage/readers/claude.d.ts +3 -0
- package/dist/usage/readers/claude.js +59 -0
- package/dist/usage/readers/codex.d.ts +23 -0
- package/dist/usage/readers/codex.js +155 -0
- package/dist/usage/record.d.ts +15 -0
- package/dist/usage/record.js +39 -0
- package/dist/usage/roots.d.ts +12 -0
- package/dist/usage/roots.js +29 -0
- package/dist/usage/scanner.d.ts +30 -0
- package/dist/usage/scanner.js +234 -0
- package/dist/usage/usage-service.d.ts +35 -0
- package/dist/usage/usage-service.js +130 -0
- package/dist/watch-seam.d.ts +29 -0
- package/dist/watch-seam.js +78 -0
- package/dist/watch-test-helpers.d.ts +28 -0
- package/dist/watch-test-helpers.js +86 -0
- package/package.json +42 -3
- package/src/async.ts +40 -0
- package/src/chat/attachment-store.ts +108 -0
- package/src/chat/backend.ts +199 -0
- package/src/chat/background-work.ts +42 -0
- package/src/chat/bookmark-store.ts +166 -0
- package/src/chat/chat-core.ts +1228 -0
- package/src/chat/chat-log.ts +187 -0
- package/src/chat/chat-process.ts +237 -0
- package/src/chat/chat-session.ts +1723 -0
- package/src/chat/chat-store.ts +261 -0
- package/src/chat/chat-title.ts +90 -0
- package/src/chat/claude-backend.ts +235 -0
- package/src/chat/claude-protocol.ts +684 -0
- package/src/chat/claude-title.ts +106 -0
- package/src/chat/claude-transcript.ts +233 -0
- package/src/chat/codex-backend.ts +397 -0
- package/src/chat/codex-protocol.ts +872 -0
- package/src/chat/codex-thread.ts +162 -0
- package/src/chat/codex-transport.ts +224 -0
- package/src/chat/composer-preferences.ts +67 -0
- package/src/chat/context-breakdown.ts +80 -0
- package/src/chat/delta-coalescer.ts +62 -0
- package/src/chat/errors.ts +25 -0
- package/src/chat/fake-claude.ts +1006 -0
- package/src/chat/fake-cli.ts +199 -0
- package/src/chat/fake-codex.ts +534 -0
- package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/src/chat/input.ts +89 -0
- package/src/chat/limit-resume.ts +74 -0
- package/src/chat/projector.ts +969 -0
- package/src/chat/request-summary.ts +168 -0
- package/src/chat/subagent-projection.ts +45 -0
- package/src/chat/subagent-reader.ts +469 -0
- package/src/chat/subagent-settlement.ts +111 -0
- package/src/chat/thread.ts +167 -0
- package/src/chat/wake-chat.ts +45 -0
- package/src/client-sinks.ts +39 -0
- package/src/coded-error.ts +18 -0
- package/src/context/argv.ts +61 -0
- package/src/context/refusal.ts +50 -0
- package/src/context/verb.ts +392 -0
- package/src/error-text.ts +28 -0
- package/src/events.ts +9 -0
- package/src/fs.ts +118 -0
- package/src/host/agent-host.ts +124 -0
- package/src/host/environment.ts +31 -0
- package/src/host/handlers.ts +142 -0
- package/src/host/memory-port.ts +27 -0
- package/src/host/wiring.ts +164 -0
- package/src/lineage.ts +159 -0
- package/src/messages/deliver-message.ts +61 -0
- package/src/messages/deliver-notice.ts +120 -0
- package/src/messages/notice-notes.ts +34 -0
- package/src/messages/notice-store.ts +142 -0
- package/src/modes.ts +38 -0
- package/src/outbox/manual-clock.ts +31 -0
- package/src/outbox/outbox-worker.ts +214 -0
- package/src/outbox/outbox.ts +106 -0
- package/src/providers/accounts/accounts.ts +192 -0
- package/src/providers/accounts/launch.ts +55 -0
- package/src/providers/accounts/service.ts +632 -0
- package/src/providers/accounts/shadow-home.ts +85 -0
- package/src/providers/accounts/status.ts +106 -0
- package/src/providers/accounts/store.ts +158 -0
- package/src/providers/accounts/test-accounts.ts +48 -0
- package/src/providers/accounts/variables.ts +132 -0
- package/src/providers/catalog.ts +90 -0
- package/src/providers/claude-models.json +114 -0
- package/src/providers/claude-provider.ts +32 -0
- package/src/providers/claude.ts +123 -0
- package/src/providers/codex-models.json +135 -0
- package/src/providers/codex-provider.ts +30 -0
- package/src/providers/codex-rules.ts +54 -0
- package/src/providers/codex.ts +69 -0
- package/src/providers/detect.ts +40 -0
- package/src/providers/provider.ts +46 -0
- package/src/providers/registry.ts +150 -0
- package/src/record-directory.ts +136 -0
- package/src/run-process.ts +61 -0
- package/src/serializer.ts +41 -0
- package/src/skills.ts +199 -0
- package/src/tasks/background-limit.ts +72 -0
- package/src/tasks/end-children.ts +100 -0
- package/src/tasks/give-task.ts +52 -0
- package/src/tasks/task-coordinator.ts +384 -0
- package/src/tasks/task-store.ts +236 -0
- package/src/tasks/task-work.ts +62 -0
- package/src/tasks/waiting-child.ts +203 -0
- package/src/tasks/wake-parent.ts +142 -0
- package/src/tasks/wiring.ts +231 -0
- package/src/title-file.ts +57 -0
- package/src/usage/accounts.ts +86 -0
- package/src/usage/aggregate.ts +220 -0
- package/src/usage/exchange.ts +93 -0
- package/src/usage/index-file.ts +178 -0
- package/src/usage/limits/monitor.ts +239 -0
- package/src/usage/limits/normalize.ts +188 -0
- package/src/usage/limits/probe.ts +120 -0
- package/src/usage/prices-snapshot.json +1071 -0
- package/src/usage/pricing.ts +236 -0
- package/src/usage/projects.ts +65 -0
- package/src/usage/readers/claude.ts +60 -0
- package/src/usage/readers/codex.ts +196 -0
- package/src/usage/record.ts +60 -0
- package/src/usage/roots.ts +39 -0
- package/src/usage/scanner.ts +265 -0
- package/src/usage/usage-service.ts +161 -0
- package/src/watch-seam.ts +111 -0
- package/src/watch-test-helpers.ts +102 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isNotFound, writeAtomic } from './fs.js';
|
|
4
|
+
import { KeyedSerializer } from './serializer.js';
|
|
5
|
+
// The id is a node id or a chat id a client chose, so it is encoded before it becomes a file name.
|
|
6
|
+
export function recordFileName(id) {
|
|
7
|
+
return `${encodeURIComponent(id)}.json`;
|
|
8
|
+
}
|
|
9
|
+
/*
|
|
10
|
+
* One kind of record under a host's data folder, a file per record and the whole of it in memory. A file
|
|
11
|
+
* that does not parse is left alone rather than deleted: it is a record of an older or newer host,
|
|
12
|
+
* and the one thing worse than not reading it is throwing it away.
|
|
13
|
+
*/
|
|
14
|
+
export class RecordDirectory {
|
|
15
|
+
dir;
|
|
16
|
+
schema;
|
|
17
|
+
idOf;
|
|
18
|
+
writeFile;
|
|
19
|
+
keep;
|
|
20
|
+
records = new Map();
|
|
21
|
+
// One write at a time per record, so an older one never lands after a newer one.
|
|
22
|
+
writes = new KeyedSerializer();
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.dir = options.dir;
|
|
25
|
+
this.schema = options.schema;
|
|
26
|
+
this.idOf = options.idOf;
|
|
27
|
+
this.writeFile = options.write ?? writeAtomic;
|
|
28
|
+
this.keep = options.keep ?? null;
|
|
29
|
+
}
|
|
30
|
+
/* Reads what an earlier run of the host wrote down. Call before anything can ask. */
|
|
31
|
+
async load() {
|
|
32
|
+
let names;
|
|
33
|
+
try {
|
|
34
|
+
names = await readdir(this.dir);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
if (isNotFound(e)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
if (!name.endsWith('.json')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const raw = await readFile(join(this.dir, name), 'utf8').catch(() => null);
|
|
47
|
+
if (raw === null) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = this.schema.safeParse(JSON.parse(raw));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const record = this.keep === null ? parsed.data : this.keep(parsed.data);
|
|
61
|
+
if (record === null) {
|
|
62
|
+
await rm(join(this.dir, name), { force: true });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
this.records.set(this.idOf(record), record);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
get(id) {
|
|
69
|
+
return this.records.get(id);
|
|
70
|
+
}
|
|
71
|
+
has(id) {
|
|
72
|
+
return this.records.has(id);
|
|
73
|
+
}
|
|
74
|
+
all() {
|
|
75
|
+
return [...this.records.values()];
|
|
76
|
+
}
|
|
77
|
+
/*
|
|
78
|
+
* Holds the record in memory at once, so the next question in the same tick already sees it, and
|
|
79
|
+
* answers whether it is still the one this directory holds by the time the file is written. A
|
|
80
|
+
* record removed while its file was being written does not come back: the rename is undone.
|
|
81
|
+
*/
|
|
82
|
+
write(record) {
|
|
83
|
+
const id = this.idOf(record);
|
|
84
|
+
this.records.set(id, record);
|
|
85
|
+
return this.writes.run(id, async () => {
|
|
86
|
+
if (this.records.get(id) !== record) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
90
|
+
await this.writeFile(this.pathOf(id), JSON.stringify(record));
|
|
91
|
+
if (!this.records.has(id)) {
|
|
92
|
+
await rm(this.pathOf(id), { force: true });
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/* Forgets a record here and now; its file follows. */
|
|
99
|
+
remove(id) {
|
|
100
|
+
this.records.delete(id);
|
|
101
|
+
return rm(this.pathOf(id), { force: true });
|
|
102
|
+
}
|
|
103
|
+
/* Drops every record the predicate says is gone. */
|
|
104
|
+
async prune(drop) {
|
|
105
|
+
for (const record of this.all()) {
|
|
106
|
+
if (drop(record)) {
|
|
107
|
+
await this.remove(this.idOf(record));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
pathOf(id) {
|
|
112
|
+
return join(this.dir, recordFileName(id));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ProcessResult {
|
|
2
|
+
exitCode: number | null;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RunProcessOptions {
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
8
|
+
cwd?: string;
|
|
9
|
+
stdin?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
timeoutMessage?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function runProcess(command: readonly string[], options?: RunProcessOptions): Promise<ProcessResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/* One short command to its end, with what it printed. A command that cannot start rejects. */
|
|
3
|
+
export function runProcess(command, options = {}) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const [executable, ...args] = command;
|
|
6
|
+
if (executable === undefined) {
|
|
7
|
+
reject(new Error('No command to run'));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const child = spawn(executable, args, {
|
|
11
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
12
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
13
|
+
stdio: [options.stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe']
|
|
14
|
+
});
|
|
15
|
+
const stdout = [];
|
|
16
|
+
const stderr = [];
|
|
17
|
+
child.stdout?.on('data', (chunk) => stdout.push(chunk));
|
|
18
|
+
child.stderr?.on('data', (chunk) => stderr.push(chunk));
|
|
19
|
+
const timer = options.timeoutMs === undefined
|
|
20
|
+
? null
|
|
21
|
+
: setTimeout(() => {
|
|
22
|
+
child.kill('SIGTERM');
|
|
23
|
+
reject(new Error(options.timeoutMessage ?? `${executable} did not finish in time`));
|
|
24
|
+
}, options.timeoutMs);
|
|
25
|
+
child.on('error', (error) => {
|
|
26
|
+
if (timer !== null) {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
reject(error);
|
|
30
|
+
});
|
|
31
|
+
child.on('close', (exitCode) => {
|
|
32
|
+
if (timer !== null) {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
resolve({ exitCode, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') });
|
|
36
|
+
});
|
|
37
|
+
if (options.stdin !== undefined && child.stdin) {
|
|
38
|
+
child.stdin.on('error', () => undefined);
|
|
39
|
+
child.stdin.end(options.stdin);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One writer at a time: the next piece of work starts once the one before it settled, whether that
|
|
3
|
+
* one kept its promise or not, and every caller hears its own result. Two changes in a row never
|
|
4
|
+
* read the same file and drop one of them.
|
|
5
|
+
*/
|
|
6
|
+
export class Serializer {
|
|
7
|
+
chain = Promise.resolve();
|
|
8
|
+
run(work) {
|
|
9
|
+
const next = this.chain.then(work, work);
|
|
10
|
+
this.chain = next.catch(() => undefined);
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
13
|
+
/* Resolves once what is queued has settled, without adding work of its own. */
|
|
14
|
+
idle() {
|
|
15
|
+
return this.chain.then(() => undefined, () => undefined);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/* The same per key, for a store whose records are written one by one and never wait on each other. */
|
|
19
|
+
export class KeyedSerializer {
|
|
20
|
+
chains = new Map();
|
|
21
|
+
run(key, work) {
|
|
22
|
+
const previous = this.chains.get(key) ?? Promise.resolve();
|
|
23
|
+
const next = previous.then(work, work);
|
|
24
|
+
const settled = next.catch(() => undefined);
|
|
25
|
+
this.chains.set(key, settled);
|
|
26
|
+
// A key whose chain ran out leaves nothing behind, or the map grows with every id ever written.
|
|
27
|
+
void settled.then(() => {
|
|
28
|
+
if (this.chains.get(key) === settled) {
|
|
29
|
+
this.chains.delete(key);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentKind, ChatSkill, ChatSkillSource } from '@adecore/agent-contracts';
|
|
2
|
+
export interface SkillRoot {
|
|
3
|
+
dir: string;
|
|
4
|
+
source: ChatSkillSource;
|
|
5
|
+
prefix?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SkillRootOptions {
|
|
8
|
+
home?: string;
|
|
9
|
+
claudeConfigDir?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseFrontmatter(text: string): Record<string, string>;
|
|
12
|
+
export declare function pluginSkillRoots(configDir: string): Promise<SkillRoot[]>;
|
|
13
|
+
export declare function skillRootsFor(kind: AgentKind, cwd: string, options?: SkillRootOptions): Promise<SkillRoot[]>;
|
|
14
|
+
export declare function scanSkillRoots(roots: SkillRoot[]): Promise<ChatSkill[]>;
|
|
15
|
+
export declare function discoverSkills(kind: AgentKind, cwd: string, options?: SkillRootOptions): Promise<ChatSkill[]>;
|
|
16
|
+
export declare class SkillIndex {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly cache;
|
|
19
|
+
constructor(options?: SkillRootOptions);
|
|
20
|
+
list(kind: AgentKind, cwd: string): Promise<ChatSkill[]>;
|
|
21
|
+
}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, parse } from 'node:path';
|
|
5
|
+
import { isNotFound } from './fs.js';
|
|
6
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_:-]*$/;
|
|
7
|
+
function unquote(value) {
|
|
8
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(value);
|
|
9
|
+
return quoted ? quoted[2] : value;
|
|
10
|
+
}
|
|
11
|
+
/*
|
|
12
|
+
* The `key: value` pairs of a SKILL.md front matter. Enough YAML for what a skill file holds: flat
|
|
13
|
+
* keys, quoted or bare values, and the block scalars (`>-`, `|`) a long description is written as.
|
|
14
|
+
*/
|
|
15
|
+
export function parseFrontmatter(text) {
|
|
16
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const fields = {};
|
|
21
|
+
const lines = match[1].split(/\r?\n/);
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const pair = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]);
|
|
24
|
+
if (!pair) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
let value = pair[2].trim();
|
|
28
|
+
if (value === '' || value === '>' || value === '>-' || value === '|' || value === '|-') {
|
|
29
|
+
// A block scalar puts the value on the indented lines under it; `|` keeps the newlines.
|
|
30
|
+
const keepNewlines = value.startsWith('|');
|
|
31
|
+
const parts = [];
|
|
32
|
+
while (i + 1 < lines.length && /^[ \t]+\S/.test(lines[i + 1])) {
|
|
33
|
+
parts.push(lines[++i].trim());
|
|
34
|
+
}
|
|
35
|
+
value = parts.join(keepNewlines ? '\n' : ' ');
|
|
36
|
+
}
|
|
37
|
+
fields[pair[1]] = unquote(value);
|
|
38
|
+
}
|
|
39
|
+
return fields;
|
|
40
|
+
}
|
|
41
|
+
/*
|
|
42
|
+
* The folders from `cwd` up to and including the repository root, closest first. The home folder
|
|
43
|
+
* itself is not one of them, since its `.claude/skills` is already the user root.
|
|
44
|
+
*/
|
|
45
|
+
function projectDirs(cwd, home) {
|
|
46
|
+
const dirs = [];
|
|
47
|
+
let current = cwd;
|
|
48
|
+
while (current !== '' && current !== parse(current).root && current !== home && current !== dirname(home) && dirs.length < 8) {
|
|
49
|
+
dirs.push(current);
|
|
50
|
+
// Anything above a repository root belongs to another project, so the walk ends there.
|
|
51
|
+
if (existsSync(join(current, '.git'))) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const parent = dirname(current);
|
|
55
|
+
if (parent === current) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
current = parent;
|
|
59
|
+
}
|
|
60
|
+
return dirs;
|
|
61
|
+
}
|
|
62
|
+
/* The plugin folders Claude Code installed, each named the way its skills are addressed. */
|
|
63
|
+
export async function pluginSkillRoots(configDir) {
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = await readFile(join(configDir, 'plugins', 'installed_plugins.json'), 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
if (isNotFound(e)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const plugins = typeof parsed === 'object' && parsed !== null ? parsed.plugins : undefined;
|
|
82
|
+
if (typeof plugins !== 'object' || plugins === null) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const roots = [];
|
|
86
|
+
for (const [key, installs] of Object.entries(plugins)) {
|
|
87
|
+
const name = key.split('@')[0] ?? key;
|
|
88
|
+
for (const install of Array.isArray(installs) ? installs : []) {
|
|
89
|
+
const path = typeof install === 'object' && install !== null ? install.installPath : undefined;
|
|
90
|
+
if (typeof path === 'string' && path !== '') {
|
|
91
|
+
roots.push({ dir: join(path, 'skills'), source: 'plugin', prefix: name });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return roots;
|
|
96
|
+
}
|
|
97
|
+
/* Where a provider's CLI looks for skills, in the order a name clash is decided. */
|
|
98
|
+
export async function skillRootsFor(kind, cwd, options = {}) {
|
|
99
|
+
const home = options.home ?? homedir();
|
|
100
|
+
if (kind === 'codex') {
|
|
101
|
+
return [
|
|
102
|
+
{ dir: join(home, '.agents', 'skills'), source: 'user' },
|
|
103
|
+
{ dir: join(home, '.codex', 'skills'), source: 'user' },
|
|
104
|
+
...projectDirs(cwd, home).map((dir) => ({ dir: join(dir, '.agents', 'skills'), source: 'project' }))
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
const configDir = options.claudeConfigDir ?? join(home, '.claude');
|
|
108
|
+
return [
|
|
109
|
+
{ dir: join(configDir, 'skills'), source: 'user' },
|
|
110
|
+
...projectDirs(cwd, home).map((dir) => ({ dir: join(dir, '.claude', 'skills'), source: 'project' })),
|
|
111
|
+
...(await pluginSkillRoots(configDir))
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
async function readSkill(root, folder) {
|
|
115
|
+
let text;
|
|
116
|
+
try {
|
|
117
|
+
text = await readFile(join(root.dir, folder, 'SKILL.md'), 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const fields = parseFrontmatter(text);
|
|
123
|
+
const own = fields.name && NAME.test(fields.name) ? fields.name : folder;
|
|
124
|
+
const name = root.prefix ? `${root.prefix}:${own}` : own;
|
|
125
|
+
if (!NAME.test(name)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { name, description: fields.description ?? '', source: root.source };
|
|
129
|
+
}
|
|
130
|
+
/* Every skill under these roots, sorted by name; the first root that carries a name wins. */
|
|
131
|
+
export async function scanSkillRoots(roots) {
|
|
132
|
+
const found = new Map();
|
|
133
|
+
for (const root of roots) {
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = await readdir(root.dir, { withFileTypes: true });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// A skill manager links each skill in from a shared folder; a link to anything but a skill folder reads as no skill.
|
|
142
|
+
const folders = entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink());
|
|
143
|
+
const skills = await Promise.all(folders.map((entry) => readSkill(root, entry.name)));
|
|
144
|
+
for (const skill of skills) {
|
|
145
|
+
if (skill && !found.has(skill.name)) {
|
|
146
|
+
found.set(skill.name, skill);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
151
|
+
}
|
|
152
|
+
export async function discoverSkills(kind, cwd, options = {}) {
|
|
153
|
+
return scanSkillRoots(await skillRootsFor(kind, cwd, options));
|
|
154
|
+
}
|
|
155
|
+
// How long a scan stays good; a skill written mid-session shows up on the next look.
|
|
156
|
+
const CACHE_TTL_MS = 30_000;
|
|
157
|
+
/* The skills of one CLI in one folder, scanned at most once per half minute. There is no watcher. */
|
|
158
|
+
export class SkillIndex {
|
|
159
|
+
options;
|
|
160
|
+
cache = new Map();
|
|
161
|
+
constructor(options = {}) {
|
|
162
|
+
this.options = options;
|
|
163
|
+
}
|
|
164
|
+
async list(kind, cwd) {
|
|
165
|
+
const key = `${kind}:${cwd}`;
|
|
166
|
+
const cached = this.cache.get(key);
|
|
167
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
168
|
+
return cached.skills;
|
|
169
|
+
}
|
|
170
|
+
const skills = await discoverSkills(kind, cwd, this.options);
|
|
171
|
+
this.cache.set(key, { at: Date.now(), skills });
|
|
172
|
+
return skills;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ChatOpenerDeps } from '../chat/wake-chat.ts';
|
|
2
|
+
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
3
|
+
import type { TaskCoordinator, TaskCoordinatorDeps } from './task-coordinator.ts';
|
|
4
|
+
import type { TaskStore } from './task-store.ts';
|
|
5
|
+
import { type AnyOutboxEntry, type BackgroundLimitEntry, type TaskOutbox } from './task-work.ts';
|
|
6
|
+
export interface BackgroundLimitDeps extends ChatOpenerDeps {
|
|
7
|
+
tasks: Pick<TaskStore, 'get'>;
|
|
8
|
+
coordinator: Pick<TaskCoordinator, 'outlasted'>;
|
|
9
|
+
}
|
|
10
|
+
export declare function backgroundLimitHandler(deps: BackgroundLimitDeps): (entry: BackgroundLimitEntry) => Promise<OutboxOutcome>;
|
|
11
|
+
export interface RestartedOutbox {
|
|
12
|
+
list(): readonly AnyOutboxEntry[];
|
|
13
|
+
update(entry: BackgroundLimitEntry): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function restartBackgroundLimits(outbox: RestartedOutbox, now: number): Promise<void>;
|
|
16
|
+
export declare function backgroundLimits(outbox: Pick<TaskOutbox, 'list' | 'enqueue' | 'remove'>): TaskCoordinatorDeps['limit'];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { commandLabel, runningInBackground } from '../chat/background-work.js';
|
|
2
|
+
import { loadChat } from '../chat/wake-chat.js';
|
|
3
|
+
import { isBackgroundLimit } from './task-work.js';
|
|
4
|
+
/*
|
|
5
|
+
* The limit on a child's background commands passed, or a restart took them down. Its task settles on
|
|
6
|
+
* the child's last turn (failed after a restart), unless something else settled it first or a subagent
|
|
7
|
+
* or a workflow now holds it, which has no limit; a turn in flight may still answer it, so the limit
|
|
8
|
+
* looks again once that turn ended.
|
|
9
|
+
*/
|
|
10
|
+
export function backgroundLimitHandler(deps) {
|
|
11
|
+
return async (entry) => {
|
|
12
|
+
if (deps.tasks.get(entry.payload.taskId)?.status !== 'open') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const chat = await loadChat(deps, entry.target);
|
|
16
|
+
if (chat === null) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (chat.info.activeTurnId !== null) {
|
|
20
|
+
return 'wait';
|
|
21
|
+
}
|
|
22
|
+
if (runningInBackground(chat.thread.list()).length > 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// A child whose process went since the limit started runs nothing any more; what it ran then is what the parent is told.
|
|
26
|
+
const running = (chat.info.background ?? []).map(commandLabel);
|
|
27
|
+
await deps.coordinator.outlasted(entry.target, running.length > 0 ? running : entry.payload.commands, entry.payload.restarted ? 'restart' : 'limit');
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/*
|
|
31
|
+
* Every limit still owed when the host starts was owed by an earlier run, and the CLI whose commands
|
|
32
|
+
* it waits on went down with that run, so nothing is left to wait for: each is due now and fails its
|
|
33
|
+
* task. Call after the outbox loaded and before its worker starts.
|
|
34
|
+
*/
|
|
35
|
+
export async function restartBackgroundLimits(outbox, now) {
|
|
36
|
+
for (const entry of outbox.list()) {
|
|
37
|
+
if (isBackgroundLimit(entry)) {
|
|
38
|
+
await outbox.update({ ...entry, notBefore: now, payload: { ...entry.payload, restarted: true } });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/* The `background-limit` entries of a host's outbox, one per task at most, as the coordinator owes and drops them. */
|
|
43
|
+
export function backgroundLimits(outbox) {
|
|
44
|
+
return {
|
|
45
|
+
owed: (taskId) => outbox.list().some((entry) => isBackgroundLimit(entry) && entry.payload.taskId === taskId),
|
|
46
|
+
owe: (task, commands, at) => outbox.enqueue(task.projectId, task.childId, { kind: 'background-limit', payload: { taskId: task.id, commands: [...commands] } }, at),
|
|
47
|
+
lapse: async (taskId) => {
|
|
48
|
+
for (const entry of outbox.list()) {
|
|
49
|
+
if (isBackgroundLimit(entry) && entry.payload.taskId === taskId) {
|
|
50
|
+
await outbox.remove(entry.id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { AgentLineageStore } from '../lineage.ts';
|
|
3
|
+
import type { OutboxEntryOf } from '../outbox/outbox.ts';
|
|
4
|
+
import type { TaskStore } from './task-store.ts';
|
|
5
|
+
import type { AnyOutboxEntry } from './task-work.ts';
|
|
6
|
+
export declare const EndChildrenWorkSchema: z.ZodObject<{
|
|
7
|
+
kind: z.ZodLiteral<"end-children">;
|
|
8
|
+
payload: z.ZodObject<{
|
|
9
|
+
nodeIds: z.ZodArray<z.ZodString>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export type EndChildrenWork = z.infer<typeof EndChildrenWorkSchema>;
|
|
13
|
+
export type EndChildrenEntry = OutboxEntryOf<EndChildrenWork>;
|
|
14
|
+
export interface EndChildrenOutbox {
|
|
15
|
+
list(): readonly AnyOutboxEntry[];
|
|
16
|
+
enqueue(projectId: string, target: string, work: EndChildrenWork): Promise<void>;
|
|
17
|
+
remove(id: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface EndChildrenDeps {
|
|
20
|
+
lineage: Pick<AgentLineageStore, 'descendants' | 'projectOf' | 'markEnded'>;
|
|
21
|
+
tasks: Pick<TaskStore, 'cancelOpen' | 'dropWake'>;
|
|
22
|
+
outbox: EndChildrenOutbox;
|
|
23
|
+
reviving?: readonly string[];
|
|
24
|
+
reason: string;
|
|
25
|
+
stop(nodeId: string, reason: string): Promise<void>;
|
|
26
|
+
ended?: (nodeIds: readonly string[]) => Promise<void>;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
}
|
|
29
|
+
export interface EndChildren {
|
|
30
|
+
owe(target: string, nodeIds?: readonly string[]): Promise<number>;
|
|
31
|
+
handler(entry: EndChildrenEntry): Promise<void>;
|
|
32
|
+
end(nodeIds: readonly string[], reason: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export declare function endChildren(deps: EndChildrenDeps): EndChildren;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const EndChildrenWorkSchema = z.object({
|
|
3
|
+
kind: z.literal('end-children'),
|
|
4
|
+
// The target is the node that was stopped or deleted; these are the agents it had opened when that was owed.
|
|
5
|
+
payload: z.object({ nodeIds: z.array(z.string().min(1)) })
|
|
6
|
+
});
|
|
7
|
+
/* The work of tasks that would start or wake an agent again; an agent that was ended has none of it left. */
|
|
8
|
+
const TASK_REVIVING = ['wake-parent', 'give-task'];
|
|
9
|
+
/*
|
|
10
|
+
* The ending of agents and the ones they opened. Marked first, so a restart halfway through finishes
|
|
11
|
+
* the same way; then the work that would bring one back goes, the open tasks are cancelled before any
|
|
12
|
+
* turn is aborted (so no parent is woken by an agent that was stopped), and the leaves stop before
|
|
13
|
+
* the nodes that opened them. Every step is safe to run twice.
|
|
14
|
+
*/
|
|
15
|
+
export function endChildren(deps) {
|
|
16
|
+
const { lineage, tasks, outbox } = deps;
|
|
17
|
+
const now = deps.now ?? Date.now;
|
|
18
|
+
const reviving = new Set([...TASK_REVIVING, ...(deps.reviving ?? [])]);
|
|
19
|
+
const end = async (nodeIds, reason) => {
|
|
20
|
+
await lineage.markEnded(nodeIds);
|
|
21
|
+
const ended = new Set(nodeIds);
|
|
22
|
+
for (const owed of outbox.list()) {
|
|
23
|
+
if (reviving.has(owed.kind) && ended.has(owed.target)) {
|
|
24
|
+
await outbox.remove(owed.id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
await tasks.cancelOpen(ended, reason, now());
|
|
28
|
+
// A stopped chat that gave tasks of its own is not woken about them either.
|
|
29
|
+
for (const nodeId of nodeIds) {
|
|
30
|
+
await tasks.dropWake(nodeId);
|
|
31
|
+
}
|
|
32
|
+
for (const nodeId of [...nodeIds].reverse()) {
|
|
33
|
+
await deps.stop(nodeId, reason);
|
|
34
|
+
}
|
|
35
|
+
await deps.ended?.(nodeIds);
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
owe: async (target, named) => {
|
|
39
|
+
const nodeIds = named === undefined ? lineage.descendants(target) : [...named];
|
|
40
|
+
const projectId = nodeIds.map((id) => lineage.projectOf(id)).find((id) => id !== null) ?? null;
|
|
41
|
+
if (nodeIds.length === 0 || projectId === null) {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
const owed = outbox.list().some((entry) => entry.kind === 'end-children' && entry.target === target);
|
|
45
|
+
if (!owed) {
|
|
46
|
+
await outbox.enqueue(projectId, target, { kind: 'end-children', payload: { nodeIds } });
|
|
47
|
+
}
|
|
48
|
+
return nodeIds.length;
|
|
49
|
+
},
|
|
50
|
+
handler: (entry) => end([...new Set([...entry.payload.nodeIds, ...lineage.descendants(entry.target)])], deps.reason),
|
|
51
|
+
end
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Task } from '@adecore/agent-contracts';
|
|
2
|
+
import type { WakeChat } from '../chat/wake-chat.ts';
|
|
3
|
+
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
4
|
+
import type { TaskStore } from './task-store.ts';
|
|
5
|
+
import type { GiveTaskEntry } from './task-work.ts';
|
|
6
|
+
export declare function taskNote(from: string): string;
|
|
7
|
+
export interface GiveTaskDeps {
|
|
8
|
+
tasks: Pick<TaskStore, 'get'>;
|
|
9
|
+
chat(chatId: string): Promise<WakeChat | null>;
|
|
10
|
+
titleFor(nodeId: string): string | null;
|
|
11
|
+
placed(nodeId: string): boolean;
|
|
12
|
+
assignment?: (task: Task) => string;
|
|
13
|
+
}
|
|
14
|
+
export declare function giveTaskHandler(deps: GiveTaskDeps): (entry: GiveTaskEntry) => Promise<OutboxOutcome>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* The note above the turn a task opens, so whoever reads the child sees who asked for it. */
|
|
2
|
+
export function taskNote(from) {
|
|
3
|
+
return `Task from ${from}`;
|
|
4
|
+
}
|
|
5
|
+
/*
|
|
6
|
+
* Opens the turn that carries a task given to an agent already running. A turn in the way makes this
|
|
7
|
+
* `wait`, the same as a person's own message, so nothing the host owes steps on what the agent is
|
|
8
|
+
* doing. One agent holds one task at a time, so the task is never queued twice.
|
|
9
|
+
*/
|
|
10
|
+
export function giveTaskHandler(deps) {
|
|
11
|
+
return async (entry) => {
|
|
12
|
+
const task = deps.tasks.get(entry.payload.taskId);
|
|
13
|
+
// Settled or cancelled before the turn could open, or a node the project no longer places.
|
|
14
|
+
if (!task || task.status !== 'open' || !deps.placed(task.childId)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const chat = await deps.chat(task.childId);
|
|
18
|
+
if (chat === null) {
|
|
19
|
+
throw new Error(`${task.childId} has no chat to open a turn in`);
|
|
20
|
+
}
|
|
21
|
+
// The turn names the task, so a restart between opening it and dropping this entry gives it once.
|
|
22
|
+
if (chat.items().some((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id))) {
|
|
23
|
+
await chat.persist?.();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const from = deps.titleFor(task.parentId) ?? task.parentId;
|
|
27
|
+
const text = deps.assignment?.(task) ?? task.prompt;
|
|
28
|
+
if (!chat.wake({ text, label: task.title, note: taskNote(from), taskIds: [task.id] })) {
|
|
29
|
+
return 'wait';
|
|
30
|
+
}
|
|
31
|
+
await chat.persist?.();
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ChatBackgroundTask, ChatItem, ChatTurnItem, Task, TaskResult } from '@adecore/agent-contracts';
|
|
2
|
+
import type { AgentEvent } from '../events.ts';
|
|
3
|
+
import type { TaskStore } from './task-store.ts';
|
|
4
|
+
export interface TaskCoordinatorDeps {
|
|
5
|
+
tasks: TaskStore;
|
|
6
|
+
now(): number;
|
|
7
|
+
chatItems(chatId: string): readonly ChatItem[] | null;
|
|
8
|
+
placed(nodeId: string): boolean;
|
|
9
|
+
loading?(chatId: string): boolean;
|
|
10
|
+
owedTurn(taskId: string): boolean;
|
|
11
|
+
oweWake(task: Task): Promise<void>;
|
|
12
|
+
alert(nodeId: string, title: string, body: string): void;
|
|
13
|
+
retryAt?(chatId: string, turn: ChatTurnItem, items: readonly ChatItem[]): number | null;
|
|
14
|
+
afterBackgroundWork(chatId: string): 'reports' | 'silent' | 'gone';
|
|
15
|
+
commandsOf(chatId: string): readonly ChatBackgroundTask[];
|
|
16
|
+
limit: {
|
|
17
|
+
owed(taskId: string): boolean;
|
|
18
|
+
owe(task: Task, commands: readonly string[], at: number): Promise<void>;
|
|
19
|
+
lapse(taskId: string): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function resultOfTurn(turn: ChatTurnItem, items: readonly ChatItem[], at: number): {
|
|
23
|
+
status: 'done' | 'failed';
|
|
24
|
+
result: TaskResult;
|
|
25
|
+
};
|
|
26
|
+
export declare class TaskCoordinator {
|
|
27
|
+
private readonly deps;
|
|
28
|
+
private stopped;
|
|
29
|
+
private readonly pending;
|
|
30
|
+
constructor(deps: TaskCoordinatorDeps);
|
|
31
|
+
stop(): void;
|
|
32
|
+
settled(): Promise<void>;
|
|
33
|
+
chatEvent(event: AgentEvent): void;
|
|
34
|
+
agentEnded(childId: string, text: string): void;
|
|
35
|
+
startFailed(childId: string, error: unknown): void;
|
|
36
|
+
giveFailed(childId: string, error: unknown): void;
|
|
37
|
+
done(childId: string, text: string): Promise<Task | null>;
|
|
38
|
+
outlasted(childId: string, commands: readonly string[], cause?: 'limit' | 'restart'): Promise<Task | null>;
|
|
39
|
+
cancelled(tasks: readonly Task[]): void;
|
|
40
|
+
private failed;
|
|
41
|
+
private answers;
|
|
42
|
+
private turnEnded;
|
|
43
|
+
private heldInBackground;
|
|
44
|
+
private pauseFor;
|
|
45
|
+
private turnStarted;
|
|
46
|
+
private hold;
|
|
47
|
+
private recheck;
|
|
48
|
+
private delegating;
|
|
49
|
+
private settle;
|
|
50
|
+
private track;
|
|
51
|
+
private settleNow;
|
|
52
|
+
}
|