@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,323 @@
|
|
|
1
|
+
import { abortedByMachine, clipText } from '@adecore/agent-contracts';
|
|
2
|
+
import { BACKGROUND_COMMAND_LIMIT_MS, commandLabel, isBackgroundWork, runningInBackground, runsInBackground } from '../chat/background-work.js';
|
|
3
|
+
import { errorText } from '../error-text.js';
|
|
4
|
+
// A child's answer is its last word of the turn; the thinking and the tools before it are not the result.
|
|
5
|
+
function answerOf(items, turnId) {
|
|
6
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
7
|
+
const item = items[i];
|
|
8
|
+
if (item.kind === 'assistant' && item.turnId === turnId && !item.parentToolUseId && item.text.trim() !== '') {
|
|
9
|
+
return item.text.trim();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function lastNote(items, turnId, levels) {
|
|
15
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
16
|
+
const item = items[i];
|
|
17
|
+
if (item.kind === 'note' && item.turnId === turnId && levels.includes(item.level)) {
|
|
18
|
+
return item.text;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function commandList(commands) {
|
|
24
|
+
return commands.map((command) => `\`${command}\``).join(', ');
|
|
25
|
+
}
|
|
26
|
+
function outlastedNote(commands) {
|
|
27
|
+
return `The task settled after ${BACKGROUND_COMMAND_LIMIT_MS / 60_000} minutes while this still ran in the background: ${commandList(commands)}.`;
|
|
28
|
+
}
|
|
29
|
+
function restartedNote(commands) {
|
|
30
|
+
return `The machine restarted while this still ran in the background: ${commandList(commands)}.`;
|
|
31
|
+
}
|
|
32
|
+
/* What a settled turn of a child makes of its task. */
|
|
33
|
+
export function resultOfTurn(turn, items, at) {
|
|
34
|
+
if (turn.state === 'done') {
|
|
35
|
+
return { status: 'done', result: { text: answerOf(items, turn.id) ?? 'It ended its turn without an answer.', source: 'turn', at } };
|
|
36
|
+
}
|
|
37
|
+
if (turn.state === 'aborted') {
|
|
38
|
+
const text = abortedByMachine(turn, items)
|
|
39
|
+
? (lastNote(items, turn.id, ['warning']) ?? 'The machine stopped it.')
|
|
40
|
+
: 'It was stopped before it finished.';
|
|
41
|
+
return { status: 'failed', result: { text, source: 'turn', at } };
|
|
42
|
+
}
|
|
43
|
+
return { status: 'failed', result: { text: lastNote(items, turn.id, ['error']) ?? 'Its turn ended in an error.', source: 'turn', at } };
|
|
44
|
+
}
|
|
45
|
+
/*
|
|
46
|
+
* Settles tasks from what children do and does nothing else: it writes the task store and owes wakes
|
|
47
|
+
* in the outbox, never starts a turn, since it runs inside the broadcast of the child it heard. A chat
|
|
48
|
+
* child settles when a turn ends with none of its own tasks still open or waiting to wake it and no work
|
|
49
|
+
* of its CLI's still running in the background, or once the commands it runs there outlast their limit;
|
|
50
|
+
* an agent that is not a chat, which the host tells it about, settles only on `done`, or fails when it
|
|
51
|
+
* ends without one.
|
|
52
|
+
*/
|
|
53
|
+
export class TaskCoordinator {
|
|
54
|
+
deps;
|
|
55
|
+
stopped = false;
|
|
56
|
+
pending = new Set();
|
|
57
|
+
constructor(deps) {
|
|
58
|
+
this.deps = deps;
|
|
59
|
+
}
|
|
60
|
+
/* The host is going down: what dies with it did not end its task. */
|
|
61
|
+
stop() {
|
|
62
|
+
this.stopped = true;
|
|
63
|
+
}
|
|
64
|
+
async settled() {
|
|
65
|
+
while (this.pending.size > 0) {
|
|
66
|
+
await Promise.allSettled([...this.pending]);
|
|
67
|
+
}
|
|
68
|
+
await this.deps.tasks.settled();
|
|
69
|
+
}
|
|
70
|
+
/* For `chats.observe()`. */
|
|
71
|
+
chatEvent(event) {
|
|
72
|
+
if (this.stopped || event.event !== 'chat.event') {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { chatId, event: chatEvent } = event.payload;
|
|
76
|
+
if (this.deps.loading?.(chatId)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn' && chatEvent.item.state === 'running') {
|
|
80
|
+
this.turnStarted(chatId);
|
|
81
|
+
}
|
|
82
|
+
else if (chatEvent.type === 'item' && chatEvent.item.kind === 'turn') {
|
|
83
|
+
this.turnEnded(chatId, chatEvent.item);
|
|
84
|
+
}
|
|
85
|
+
else if (chatEvent.type === 'item' && isBackgroundWork(chatEvent.item) && !runsInBackground(chatEvent.item)) {
|
|
86
|
+
this.recheck(chatId);
|
|
87
|
+
}
|
|
88
|
+
else if (chatEvent.type === 'info' && chatEvent.info.activeTurnId === null) {
|
|
89
|
+
// A child that waited on tasks of its own may be done now that its last wake turn went.
|
|
90
|
+
this.recheck(chatId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/*
|
|
94
|
+
* An agent that is not a chat ended, so no turn of it will answer its task: it fails with `text`,
|
|
95
|
+
* which says how that kind of agent reports back.
|
|
96
|
+
*/
|
|
97
|
+
agentEnded(childId, text) {
|
|
98
|
+
if (this.stopped) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const task = this.deps.tasks.openFor(childId);
|
|
102
|
+
if (!task || !this.deps.placed(childId)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
|
|
106
|
+
}
|
|
107
|
+
startFailed(childId, error) {
|
|
108
|
+
this.failed(childId, `The agent could not be started: ${errorText(error)}`);
|
|
109
|
+
}
|
|
110
|
+
/* The turn a task given to a running agent was to open never opened, so nobody is working on it. */
|
|
111
|
+
giveFailed(childId, error) {
|
|
112
|
+
this.failed(childId, `The task could not be given: ${errorText(error)}`);
|
|
113
|
+
}
|
|
114
|
+
/* `done` from the child itself, which wins over whatever else was about to settle it. */
|
|
115
|
+
async done(childId, text) {
|
|
116
|
+
const task = this.deps.tasks.openFor(childId);
|
|
117
|
+
if (!task) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return this.settleNow(task, 'done', { text, source: 'done', at: this.deps.now() });
|
|
121
|
+
}
|
|
122
|
+
/*
|
|
123
|
+
* The limit on the commands a child ran in the background passed, or a restart took them down with
|
|
124
|
+
* its CLI: its task settles on its last turn, saying what still ran, and fails after a restart as it
|
|
125
|
+
* does when the CLI goes within one run.
|
|
126
|
+
*/
|
|
127
|
+
async outlasted(childId, commands, cause = 'limit') {
|
|
128
|
+
const task = this.deps.tasks.openFor(childId);
|
|
129
|
+
const items = this.deps.chatItems(childId);
|
|
130
|
+
if (this.stopped || !task || items === null || this.delegating(childId)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const last = items.findLast((item) => item.kind === 'turn');
|
|
134
|
+
if (!last || last.state === 'running' || !this.answers(items, last, task)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const { status, result } = resultOfTurn(last, items, this.deps.now());
|
|
138
|
+
if (cause === 'restart') {
|
|
139
|
+
return this.settleNow(task, 'failed', { ...result, text: `${result.text}\n\n${restartedNote(commands)}`, source: 'exit' });
|
|
140
|
+
}
|
|
141
|
+
return this.settleNow(task, status, { ...result, text: `${result.text}\n\n${outlastedNote(commands)}` });
|
|
142
|
+
}
|
|
143
|
+
/* Tasks of children a person removed; their parent may have been waiting on nothing else. */
|
|
144
|
+
cancelled(tasks) {
|
|
145
|
+
for (const task of tasks) {
|
|
146
|
+
this.recheck(task.parentId);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/* An open task of this child that nothing can carry any more. */
|
|
150
|
+
failed(childId, text) {
|
|
151
|
+
const task = this.deps.tasks.openFor(childId);
|
|
152
|
+
if (!task || this.stopped) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.settle(task, 'failed', { text, source: 'exit', at: this.deps.now() });
|
|
156
|
+
}
|
|
157
|
+
/*
|
|
158
|
+
* Whether this turn is the one the task's result comes from. A task given to an already-working agent
|
|
159
|
+
* gets a turn of its own that names it; the turn that was in its way answers the person who sent it,
|
|
160
|
+
* not the task, and nothing settles while that turn is still owed. A turn after it answers too, as the
|
|
161
|
+
* one the CLI opens about work that turn left running. A child opened with its task has no turn of that
|
|
162
|
+
* kind, so its first turn settles it, as it always did.
|
|
163
|
+
*/
|
|
164
|
+
answers(items, turn, task) {
|
|
165
|
+
if ((turn.taskIds ?? []).includes(task.id)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (this.deps.owedTurn(task.id)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const own = items.findIndex((item) => item.kind === 'turn' && (item.taskIds ?? []).includes(task.id));
|
|
172
|
+
return own === -1 ? task.requiresTaskTurn !== true && turn.createdAt >= task.createdAt : own < items.findIndex((item) => item.id === turn.id);
|
|
173
|
+
}
|
|
174
|
+
turnEnded(chatId, turn) {
|
|
175
|
+
const task = this.deps.tasks.openFor(chatId);
|
|
176
|
+
const items = this.deps.chatItems(chatId);
|
|
177
|
+
if (!task || items === null || !this.answers(items, turn, task) || this.delegating(chatId)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const paused = this.pauseFor(chatId, turn, items);
|
|
181
|
+
if (paused !== null) {
|
|
182
|
+
this.hold(task, paused);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// A turn a person stopped or that failed ends the task as it always did, whatever still runs beside it.
|
|
186
|
+
if (turn.state === 'done' && this.heldInBackground(chatId, task, items)) {
|
|
187
|
+
this.track(this.deps.tasks.holdBackground(task.id, {
|
|
188
|
+
turnId: turn.id,
|
|
189
|
+
itemIds: runningInBackground(items).map((item) => item.id),
|
|
190
|
+
commands: this.deps.commandsOf(chatId).map(commandLabel)
|
|
191
|
+
}), `Saving the background work of task ${task.id} failed`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (task.background?.turnId === turn.id) {
|
|
195
|
+
const after = this.deps.afterBackgroundWork(chatId);
|
|
196
|
+
if (after === 'reports') {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (after === 'gone') {
|
|
200
|
+
if (task.background.itemIds.length === 0 && this.deps.limit.owed(task.id)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const lost = task.background.itemIds.filter((id) => {
|
|
204
|
+
const item = items.find((item) => item.id === id);
|
|
205
|
+
return !item || (item.kind === 'subagent' ? item.status !== 'done' : item.kind !== 'tool' || item.state !== 'done');
|
|
206
|
+
});
|
|
207
|
+
if (lost.length > 0 || task.background.commands.length > 0) {
|
|
208
|
+
this.settle(task, 'failed', {
|
|
209
|
+
text: `The agent ended before its background work completed: ${[
|
|
210
|
+
...lost.map((id) => {
|
|
211
|
+
const row = items.find((item) => item.id === id);
|
|
212
|
+
return row?.kind === 'subagent' ? row.description || row.toolUseId : row?.kind === 'tool' ? row.name : id;
|
|
213
|
+
}),
|
|
214
|
+
...task.background.commands
|
|
215
|
+
].join(', ')}. This work will not send a result.`,
|
|
216
|
+
source: 'exit',
|
|
217
|
+
at: this.deps.now()
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const reports = task.background.itemIds.flatMap((id) => {
|
|
222
|
+
const row = items.find((item) => item.id === id);
|
|
223
|
+
const report = row?.kind === 'subagent' ? row.result : row?.kind === 'tool' ? row.output : null;
|
|
224
|
+
return report ? [report] : [];
|
|
225
|
+
});
|
|
226
|
+
this.settle(task, 'done', {
|
|
227
|
+
text: reports.join('\n\n') || resultOfTurn(turn, items, this.deps.now()).result.text,
|
|
228
|
+
source: 'turn',
|
|
229
|
+
at: this.deps.now()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (turn.state === 'done' && this.deps.limit.owed(task.id) && this.deps.afterBackgroundWork(chatId) === 'gone') {
|
|
236
|
+
// After a restart nothing here remembers the turn was held, but the limit it owes does, and that settles it.
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const { status, result } = resultOfTurn(turn, items, this.deps.now());
|
|
240
|
+
this.settle(task, status, result);
|
|
241
|
+
}
|
|
242
|
+
/*
|
|
243
|
+
* Whether work in the background keeps this task open past the turn. Commands and monitors alone hold
|
|
244
|
+
* it only until the limit, owed from the moment they are all that is left; a subagent or a workflow
|
|
245
|
+
* running beside them holds it with no limit at all.
|
|
246
|
+
*/
|
|
247
|
+
heldInBackground(chatId, task, items) {
|
|
248
|
+
if (runningInBackground(items).length > 0) {
|
|
249
|
+
if (this.deps.limit.owed(task.id)) {
|
|
250
|
+
void this.deps.limit.lapse(task.id).catch((e) => console.error(`Dropping the limit of task ${task.id} failed:`, errorText(e)));
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
const commands = this.deps.commandsOf(chatId);
|
|
255
|
+
if (commands.length === 0) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (!this.deps.limit.owed(task.id)) {
|
|
259
|
+
void this.deps.limit
|
|
260
|
+
.owe(task, commands.map(commandLabel), this.deps.now() + BACKGROUND_COMMAND_LIMIT_MS)
|
|
261
|
+
.catch((e) => console.error(`Owing the limit of task ${task.id} failed:`, errorText(e)));
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
/*
|
|
266
|
+
* A limit is work waiting, not work that failed: the plan's limit always holds the task until the child
|
|
267
|
+
* finishes after it, an overload only while the host still owes the turn another try.
|
|
268
|
+
*/
|
|
269
|
+
pauseFor(chatId, turn, items) {
|
|
270
|
+
const limit = turn.state === 'error' ? turn.limit : undefined;
|
|
271
|
+
if (limit?.kind === 'usage') {
|
|
272
|
+
return { kind: 'usage', ...(limit.resetsAt === undefined ? {} : { until: limit.resetsAt }) };
|
|
273
|
+
}
|
|
274
|
+
if (limit?.kind === 'overload') {
|
|
275
|
+
const at = this.deps.retryAt?.(chatId, turn, items) ?? null;
|
|
276
|
+
return at === null ? null : { kind: 'overload', until: at };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
/* A child that took up work again is no longer waiting out a limit. */
|
|
281
|
+
turnStarted(chatId) {
|
|
282
|
+
const task = this.deps.tasks.openFor(chatId);
|
|
283
|
+
if (task?.paused !== undefined) {
|
|
284
|
+
this.hold(task, null);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
hold(task, paused) {
|
|
288
|
+
void this.deps.tasks.pause(task.id, paused).catch((e) => console.error(`Pausing task ${task.id} failed:`, errorText(e)));
|
|
289
|
+
}
|
|
290
|
+
recheck(chatId) {
|
|
291
|
+
const task = this.deps.tasks.openFor(chatId);
|
|
292
|
+
const items = this.deps.chatItems(chatId);
|
|
293
|
+
if (!task || items === null || this.delegating(chatId)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const last = [...items].reverse().find((item) => item.kind === 'turn');
|
|
297
|
+
if (last && last.state !== 'running' && last.createdAt >= task.createdAt) {
|
|
298
|
+
this.turnEnded(chatId, last);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/* A child that gave tasks of its own is not finished while one is open or has yet to wake it. */
|
|
302
|
+
delegating(chatId) {
|
|
303
|
+
return this.deps.tasks.ofParent(chatId).some((task) => task.status === 'open' || task.wake === 'pending');
|
|
304
|
+
}
|
|
305
|
+
settle(task, status, result) {
|
|
306
|
+
this.track(this.settleNow(task, status, result), `Settling task ${task.id} failed`);
|
|
307
|
+
}
|
|
308
|
+
track(work, message) {
|
|
309
|
+
this.pending.add(work);
|
|
310
|
+
void work.catch((e) => console.error(`${message}:`, errorText(e))).finally(() => this.pending.delete(work));
|
|
311
|
+
}
|
|
312
|
+
async settleNow(task, status, result) {
|
|
313
|
+
const settled = await this.deps.tasks.settle(task.id, status, result, this.deps.now());
|
|
314
|
+
if (!settled) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
await this.deps.oweWake(settled);
|
|
318
|
+
if (status === 'failed') {
|
|
319
|
+
this.deps.alert(settled.childId, `Task failed: ${settled.title}`, clipText(result.text, 500));
|
|
320
|
+
}
|
|
321
|
+
return settled;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Task, type TaskChangedEvent, type TaskResult } from '@adecore/agent-contracts';
|
|
2
|
+
export type TaskListener = (task: Task) => void;
|
|
3
|
+
export interface TaskEvent {
|
|
4
|
+
event: 'task.changed';
|
|
5
|
+
payload: TaskChangedEvent;
|
|
6
|
+
}
|
|
7
|
+
export type TaskSink = (event: TaskEvent) => void;
|
|
8
|
+
export declare class TaskStore {
|
|
9
|
+
readonly dir: string;
|
|
10
|
+
private readonly tasks;
|
|
11
|
+
private readonly listeners;
|
|
12
|
+
private readonly sinks;
|
|
13
|
+
private readonly writes;
|
|
14
|
+
constructor(dataDir: string, options?: {
|
|
15
|
+
write?: (path: string, text: string) => Promise<void>;
|
|
16
|
+
});
|
|
17
|
+
load(): Promise<void>;
|
|
18
|
+
onChange(listener: TaskListener): () => void;
|
|
19
|
+
subscribe(clientId: string, sink: TaskSink): () => void;
|
|
20
|
+
open(record: {
|
|
21
|
+
projectId: string;
|
|
22
|
+
parentId: string;
|
|
23
|
+
childId: string;
|
|
24
|
+
title: string;
|
|
25
|
+
prompt: string;
|
|
26
|
+
batchId?: string;
|
|
27
|
+
requiresTaskTurn?: boolean;
|
|
28
|
+
}, now: number): Promise<Task>;
|
|
29
|
+
all(): Task[];
|
|
30
|
+
settled(): Promise<void>;
|
|
31
|
+
holdBackground(id: string, background: NonNullable<Task['background']>): Promise<void>;
|
|
32
|
+
get(id: string): Task | undefined;
|
|
33
|
+
openFor(childId: string): Task | undefined;
|
|
34
|
+
involving(nodeId: string): Task[];
|
|
35
|
+
ofParent(parentId: string): Task[];
|
|
36
|
+
ofProject(projectId: string): Task[];
|
|
37
|
+
settle(id: string, status: 'done' | 'failed' | 'cancelled', result: TaskResult | null, now: number): Promise<Task | null>;
|
|
38
|
+
pause(id: string, paused: NonNullable<Task['paused']> | null): Promise<boolean>;
|
|
39
|
+
cancelOpen(childIds: ReadonlySet<string>, reason: string, now: number): Promise<Task[]>;
|
|
40
|
+
markWoken(ids: readonly string[]): Promise<void>;
|
|
41
|
+
pendingWake(parentId: string): Task[];
|
|
42
|
+
readyWake(parentId: string): Task[];
|
|
43
|
+
openBatches(parentId: string): string[];
|
|
44
|
+
dropWake(parentId: string): Promise<void>;
|
|
45
|
+
prune(projectId: string, ids: ReadonlySet<string>, now: number): Promise<Task[]>;
|
|
46
|
+
private sorted;
|
|
47
|
+
private write;
|
|
48
|
+
private writeNow;
|
|
49
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { TaskSchema } from '@adecore/agent-contracts';
|
|
4
|
+
import { ClientSinks } from '../client-sinks.js';
|
|
5
|
+
import { RecordDirectory } from '../record-directory.js';
|
|
6
|
+
/*
|
|
7
|
+
* What a chat asked of the nodes it opened with `--task`, one file per task under `<dataDir>/tasks`,
|
|
8
|
+
* beside the lineage and for the same reason: an agent with a shell in the project folder can rewrite
|
|
9
|
+
* the project's files, and waking the parent is a promise the host keeps. A settled task stays until
|
|
10
|
+
* its parent leaves the project, so the parent can still list what came of it.
|
|
11
|
+
*/
|
|
12
|
+
export class TaskStore {
|
|
13
|
+
dir;
|
|
14
|
+
tasks;
|
|
15
|
+
listeners = new Set();
|
|
16
|
+
sinks = new ClientSinks();
|
|
17
|
+
writes = new Set();
|
|
18
|
+
constructor(dataDir, options = {}) {
|
|
19
|
+
this.dir = join(dataDir, 'tasks');
|
|
20
|
+
this.tasks = new RecordDirectory({ dir: this.dir, schema: TaskSchema, idOf: (task) => task.id, ...options });
|
|
21
|
+
}
|
|
22
|
+
/* Reads what an earlier run of the host wrote down. Call before any verb or observer can ask. */
|
|
23
|
+
load() {
|
|
24
|
+
return this.tasks.load();
|
|
25
|
+
}
|
|
26
|
+
/* Told about every task that is written, after it is on disk. */
|
|
27
|
+
onChange(listener) {
|
|
28
|
+
this.listeners.add(listener);
|
|
29
|
+
return () => {
|
|
30
|
+
this.listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/* Every socket hears every task: the edge and the header of a child are drawn from it. */
|
|
34
|
+
subscribe(clientId, sink) {
|
|
35
|
+
return this.sinks.subscribe(clientId, sink);
|
|
36
|
+
}
|
|
37
|
+
async open(record, now) {
|
|
38
|
+
const task = {
|
|
39
|
+
...record,
|
|
40
|
+
id: `task-${randomBytes(6).toString('hex')}`,
|
|
41
|
+
status: 'open',
|
|
42
|
+
result: null,
|
|
43
|
+
createdAt: now,
|
|
44
|
+
settledAt: null,
|
|
45
|
+
wake: 'pending'
|
|
46
|
+
};
|
|
47
|
+
await this.write(task);
|
|
48
|
+
return task;
|
|
49
|
+
}
|
|
50
|
+
all() {
|
|
51
|
+
return this.sorted();
|
|
52
|
+
}
|
|
53
|
+
async settled() {
|
|
54
|
+
while (this.writes.size > 0) {
|
|
55
|
+
await Promise.all([...this.writes]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async holdBackground(id, background) {
|
|
59
|
+
const task = this.get(id);
|
|
60
|
+
if (!task || task.status !== 'open' || JSON.stringify(task.background) === JSON.stringify(background)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await this.write({ ...task, background });
|
|
64
|
+
}
|
|
65
|
+
get(id) {
|
|
66
|
+
return this.tasks.get(id);
|
|
67
|
+
}
|
|
68
|
+
/* The task a node is working on; a node has at most one, the one it was opened with. */
|
|
69
|
+
openFor(childId) {
|
|
70
|
+
return this.tasks.all().find((task) => task.childId === childId && task.status === 'open');
|
|
71
|
+
}
|
|
72
|
+
/* Every task this node gave or was given, oldest first. */
|
|
73
|
+
involving(nodeId) {
|
|
74
|
+
return this.sorted().filter((task) => task.parentId === nodeId || task.childId === nodeId);
|
|
75
|
+
}
|
|
76
|
+
ofParent(parentId) {
|
|
77
|
+
return this.sorted().filter((task) => task.parentId === parentId);
|
|
78
|
+
}
|
|
79
|
+
ofProject(projectId) {
|
|
80
|
+
return this.sorted().filter((task) => task.projectId === projectId);
|
|
81
|
+
}
|
|
82
|
+
/*
|
|
83
|
+
* Ends an open task with its result; false when it was not open any more, which is how the first of
|
|
84
|
+
* `done`, the end of a turn and an exit wins. A cancelled task wakes nobody.
|
|
85
|
+
*/
|
|
86
|
+
async settle(id, status, result, now) {
|
|
87
|
+
const task = this.tasks.get(id);
|
|
88
|
+
if (!task || task.status !== 'open') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const { paused: _paused, ...open } = task;
|
|
92
|
+
const settled = { ...open, status, result, settledAt: now, wake: status === 'cancelled' ? 'none' : task.wake };
|
|
93
|
+
await this.write(settled);
|
|
94
|
+
return settled;
|
|
95
|
+
}
|
|
96
|
+
/* Holds an open task while its child waits out a limit, or lets it go on with null; false when it was not open. */
|
|
97
|
+
async pause(id, paused) {
|
|
98
|
+
const task = this.tasks.get(id);
|
|
99
|
+
if (!task || task.status !== 'open') {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (JSON.stringify(task.paused ?? null) === JSON.stringify(paused)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
const { paused: _was, ...rest } = task;
|
|
106
|
+
await this.write(paused === null ? rest : { ...rest, paused });
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/* Cancels the open tasks of these children, with the reason as the result; a cancelled task wakes nobody. */
|
|
110
|
+
async cancelOpen(childIds, reason, now) {
|
|
111
|
+
const cancelled = [];
|
|
112
|
+
for (const task of this.sorted()) {
|
|
113
|
+
if (task.status === 'open' && childIds.has(task.childId)) {
|
|
114
|
+
const settled = await this.settle(task.id, 'cancelled', { text: reason, source: 'exit', at: now }, now);
|
|
115
|
+
if (settled) {
|
|
116
|
+
cancelled.push(settled);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return cancelled;
|
|
121
|
+
}
|
|
122
|
+
async markWoken(ids) {
|
|
123
|
+
for (const id of ids) {
|
|
124
|
+
const task = this.tasks.get(id);
|
|
125
|
+
if (task && task.wake === 'pending') {
|
|
126
|
+
await this.write({ ...task, wake: 'sent' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
pendingWake(parentId) {
|
|
131
|
+
return this.ofParent(parentId).filter((task) => task.status !== 'open' && task.wake === 'pending');
|
|
132
|
+
}
|
|
133
|
+
/*
|
|
134
|
+
* The part of `pendingWake` that may wake the parent now. A task of a `team --task` call is held back
|
|
135
|
+
* until every task of that call settled, so one team is one answer however far apart its roles finish.
|
|
136
|
+
*/
|
|
137
|
+
readyWake(parentId) {
|
|
138
|
+
const open = new Set(this.openBatches(parentId));
|
|
139
|
+
return this.pendingWake(parentId).filter((task) => task.batchId === undefined || !open.has(task.batchId));
|
|
140
|
+
}
|
|
141
|
+
/* The batches of this parent with a task still open, oldest first. */
|
|
142
|
+
openBatches(parentId) {
|
|
143
|
+
const ids = this.ofParent(parentId).flatMap((task) => (task.status === 'open' && task.batchId !== undefined ? [task.batchId] : []));
|
|
144
|
+
return [...new Set(ids)];
|
|
145
|
+
}
|
|
146
|
+
/* A parent nobody can wake any more: its settled tasks stop waiting for it. */
|
|
147
|
+
async dropWake(parentId) {
|
|
148
|
+
for (const task of this.ofParent(parentId)) {
|
|
149
|
+
if (task.wake === 'pending') {
|
|
150
|
+
await this.write({ ...task, wake: 'none' });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/*
|
|
155
|
+
* What this project wrote down for ids it no longer has. A task whose parent is gone has nobody to
|
|
156
|
+
* report to and goes; an open task whose child is gone is cancelled and stays, so its parent sees why.
|
|
157
|
+
*/
|
|
158
|
+
async prune(projectId, ids, now) {
|
|
159
|
+
const cancelled = [];
|
|
160
|
+
for (const task of this.sorted()) {
|
|
161
|
+
if (task.projectId !== projectId) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!ids.has(task.parentId)) {
|
|
165
|
+
await this.tasks.remove(task.id);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!ids.has(task.childId) && task.status === 'open') {
|
|
169
|
+
const settled = await this.settle(task.id, 'cancelled', null, now);
|
|
170
|
+
if (settled) {
|
|
171
|
+
cancelled.push(settled);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return cancelled;
|
|
176
|
+
}
|
|
177
|
+
sorted() {
|
|
178
|
+
// A stable sort: tasks made in the same millisecond keep the order they were made in.
|
|
179
|
+
return this.tasks.all().sort((a, b) => a.createdAt - b.createdAt);
|
|
180
|
+
}
|
|
181
|
+
write(task) {
|
|
182
|
+
const write = this.writeNow(task);
|
|
183
|
+
this.writes.add(write);
|
|
184
|
+
void write.finally(() => this.writes.delete(write)).catch(() => undefined);
|
|
185
|
+
return write;
|
|
186
|
+
}
|
|
187
|
+
async writeNow(task) {
|
|
188
|
+
// Only the write that is still the latest tells anyone: an older record never lands after it.
|
|
189
|
+
if (!(await this.tasks.write(task))) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const listener of this.listeners) {
|
|
193
|
+
listener(task);
|
|
194
|
+
}
|
|
195
|
+
this.sinks.emit({ event: 'task.changed', payload: { task } });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { OutboxEntryOf, OutboxWorkShape } from '../outbox/outbox.ts';
|
|
3
|
+
export declare const BackgroundLimitWorkSchema: z.ZodObject<{
|
|
4
|
+
kind: z.ZodLiteral<"background-limit">;
|
|
5
|
+
payload: z.ZodObject<{
|
|
6
|
+
taskId: z.ZodString;
|
|
7
|
+
commands: z.ZodArray<z.ZodString>;
|
|
8
|
+
restarted: z.ZodOptional<z.ZodLiteral<true>>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare const WakeParentWorkSchema: z.ZodObject<{
|
|
12
|
+
kind: z.ZodLiteral<"wake-parent">;
|
|
13
|
+
payload: z.ZodObject<{
|
|
14
|
+
taskId: z.ZodString;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export declare const GiveTaskWorkSchema: z.ZodObject<{
|
|
18
|
+
kind: z.ZodLiteral<"give-task">;
|
|
19
|
+
payload: z.ZodObject<{
|
|
20
|
+
taskId: z.ZodString;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export declare const DeliverWaitingWorkSchema: z.ZodObject<{
|
|
24
|
+
kind: z.ZodLiteral<"deliver-waiting">;
|
|
25
|
+
payload: z.ZodObject<{
|
|
26
|
+
childId: z.ZodString;
|
|
27
|
+
requestId: z.ZodString;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
export type BackgroundLimitWork = z.infer<typeof BackgroundLimitWorkSchema>;
|
|
31
|
+
export type WakeParentWork = z.infer<typeof WakeParentWorkSchema>;
|
|
32
|
+
export type GiveTaskWork = z.infer<typeof GiveTaskWorkSchema>;
|
|
33
|
+
export type DeliverWaitingWork = z.infer<typeof DeliverWaitingWorkSchema>;
|
|
34
|
+
export type TaskWork = BackgroundLimitWork | WakeParentWork | GiveTaskWork | DeliverWaitingWork;
|
|
35
|
+
export type BackgroundLimitEntry = OutboxEntryOf<BackgroundLimitWork>;
|
|
36
|
+
export type WakeParentEntry = OutboxEntryOf<WakeParentWork>;
|
|
37
|
+
export type GiveTaskEntry = OutboxEntryOf<GiveTaskWork>;
|
|
38
|
+
export type DeliverWaitingEntry = OutboxEntryOf<DeliverWaitingWork>;
|
|
39
|
+
export type AnyOutboxEntry = OutboxEntryOf<OutboxWorkShape>;
|
|
40
|
+
export interface TaskOutbox {
|
|
41
|
+
list(): readonly AnyOutboxEntry[];
|
|
42
|
+
enqueue(projectId: string, target: string, work: TaskWork, notBefore?: number): Promise<void>;
|
|
43
|
+
remove(id: string): Promise<void>;
|
|
44
|
+
wake(target: string): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function isBackgroundLimit(entry: AnyOutboxEntry): entry is BackgroundLimitEntry;
|
|
47
|
+
export declare function isGiveTask(entry: AnyOutboxEntry): entry is GiveTaskEntry;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const BackgroundLimitWorkSchema = z.object({
|
|
3
|
+
kind: z.literal('background-limit'),
|
|
4
|
+
// The target is the child; the task its background commands hold open, and what they were when the limit started, for a child whose process went since.
|
|
5
|
+
// `restarted` marks an entry an earlier run of the host owed, whose commands went down with it.
|
|
6
|
+
payload: z.object({ taskId: z.string().min(1), commands: z.array(z.string()), restarted: z.literal(true).optional() })
|
|
7
|
+
});
|
|
8
|
+
export const WakeParentWorkSchema = z.object({
|
|
9
|
+
kind: z.literal('wake-parent'),
|
|
10
|
+
// The target is the chat to wake; the task that settled is only what owed it, since a wake takes every settled task.
|
|
11
|
+
payload: z.object({ taskId: z.string().min(1) })
|
|
12
|
+
});
|
|
13
|
+
export const GiveTaskWorkSchema = z.object({
|
|
14
|
+
kind: z.literal('give-task'),
|
|
15
|
+
// The target is the agent the task went to; the record itself holds what it asks and who asked.
|
|
16
|
+
payload: z.object({ taskId: z.string().min(1) })
|
|
17
|
+
});
|
|
18
|
+
export const DeliverWaitingWorkSchema = z.object({
|
|
19
|
+
kind: z.literal('deliver-waiting'),
|
|
20
|
+
// The target is the chat that gave the child its task; the child waits on this request of its own.
|
|
21
|
+
payload: z.object({ childId: z.string().min(1), requestId: z.string().min(1) })
|
|
22
|
+
});
|
|
23
|
+
export function isBackgroundLimit(entry) {
|
|
24
|
+
return entry.kind === 'background-limit';
|
|
25
|
+
}
|
|
26
|
+
export function isGiveTask(entry) {
|
|
27
|
+
return entry.kind === 'give-task';
|
|
28
|
+
}
|