@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,111 @@
|
|
|
1
|
+
import { open, stat } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
// The end of a transcript is all the question needs; a final line longer than this reads as undecided.
|
|
4
|
+
const TAIL_BYTES = 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* How a subagent's own transcript says it ended: when, and the text of its last message. */
|
|
11
|
+
export interface SubagentSettlement {
|
|
12
|
+
finishedAt: number | null;
|
|
13
|
+
report: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* The report an agent handed back with `SubagentHandback` before its last message, which then only points at it. */
|
|
17
|
+
function handbackBefore(lines: string[], end: number): string | null {
|
|
18
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
19
|
+
let entry: unknown;
|
|
20
|
+
try {
|
|
21
|
+
entry = JSON.parse(lines[i]!);
|
|
22
|
+
} catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const message = isRecord(entry) && entry.type === 'assistant' && isRecord(entry.message) ? entry.message : null;
|
|
26
|
+
const content = message && Array.isArray(message.content) ? message.content.filter(isRecord) : [];
|
|
27
|
+
for (const block of content) {
|
|
28
|
+
const input = block.type === 'tool_use' && block.name === 'SubagentHandback' && isRecord(block.input) ? block.input : null;
|
|
29
|
+
if (input && typeof input.message === 'string' && input.message.trim() !== '') {
|
|
30
|
+
return input.message;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* Whether the end of a Claude subagent transcript shows an agent that finished: the last line that
|
|
39
|
+
* is a message is the assistant's, it ended its turn (`stop_reason: end_turn`) and it asked for no
|
|
40
|
+
* tool. Anything else (a tool call still out, a tool result the model has not answered yet, a line
|
|
41
|
+
* still being written, a file cut off in the middle of a line) is not an answer, and null leaves the
|
|
42
|
+
* row as it was. `text` starts on a line boundary.
|
|
43
|
+
*/
|
|
44
|
+
export function settlementOf(text: string): SubagentSettlement | null {
|
|
45
|
+
if (!text.endsWith('\n')) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const lines = text.split('\n');
|
|
49
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
50
|
+
const line = lines[i]!.trim();
|
|
51
|
+
if (line === '') {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let entry: unknown;
|
|
55
|
+
try {
|
|
56
|
+
entry = JSON.parse(line);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(entry) || (entry.type !== 'user' && entry.type !== 'assistant')) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const message = isRecord(entry.message) ? entry.message : {};
|
|
64
|
+
const content = Array.isArray(message.content) ? message.content.filter(isRecord) : [];
|
|
65
|
+
if (entry.type !== 'assistant' || message.stop_reason !== 'end_turn' || content.some((block) => block.type === 'tool_use')) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const time = typeof entry.timestamp === 'string' ? Date.parse(entry.timestamp) : Number.NaN;
|
|
69
|
+
const handedBack = handbackBefore(lines, i);
|
|
70
|
+
const report =
|
|
71
|
+
handedBack ??
|
|
72
|
+
content
|
|
73
|
+
.map((block) => (block.type === 'text' && typeof block.text === 'string' ? block.text : ''))
|
|
74
|
+
.filter((part) => part.trim() !== '')
|
|
75
|
+
.join('\n');
|
|
76
|
+
return { finishedAt: Number.isFinite(time) ? time : null, report: report === '' ? null : report };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* Reads the end of a transcript and asks `settlementOf`. The file has to be the same size and age
|
|
83
|
+
* after the read as before it: a transcript that grew in between belongs to an agent still writing.
|
|
84
|
+
*/
|
|
85
|
+
export async function readSubagentSettlement(path: string): Promise<SubagentSettlement | null> {
|
|
86
|
+
let before: { size: number; mtimeMs: number };
|
|
87
|
+
try {
|
|
88
|
+
before = await stat(path);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const from = Math.max(0, before.size - TAIL_BYTES);
|
|
93
|
+
const handle = await open(path, 'r');
|
|
94
|
+
let text: string;
|
|
95
|
+
try {
|
|
96
|
+
const bytes = Buffer.alloc(before.size - from);
|
|
97
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, from);
|
|
98
|
+
text = bytes.subarray(0, bytesRead).toString('utf8');
|
|
99
|
+
} finally {
|
|
100
|
+
await handle.close();
|
|
101
|
+
}
|
|
102
|
+
const after = await stat(path).catch(() => null);
|
|
103
|
+
if (after === null || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (from > 0) {
|
|
107
|
+
const start = text.indexOf('\n');
|
|
108
|
+
text = start < 0 ? '' : text.slice(start + 1);
|
|
109
|
+
}
|
|
110
|
+
return text === '' ? null : settlementOf(text);
|
|
111
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ChatError } from './errors.ts';
|
|
3
|
+
import { requestSummaries } from './request-summary.ts';
|
|
4
|
+
import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The state of one chat as the client sees it. Every mutation answers the event that describes
|
|
8
|
+
* it, so the host broadcasts exactly what it applied and a reload can rebuild from `snapshot`.
|
|
9
|
+
*/
|
|
10
|
+
export class ChatThread {
|
|
11
|
+
info: ChatInfo;
|
|
12
|
+
private readonly items = new Map<string, ChatItem>();
|
|
13
|
+
private readonly order: string[] = [];
|
|
14
|
+
private historyGeneration = randomUUID();
|
|
15
|
+
private readonly indices = new Map<string, number>();
|
|
16
|
+
|
|
17
|
+
constructor(info: ChatInfo, items: ChatItem[] = []) {
|
|
18
|
+
this.info = info;
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
this.items.set(item.id, item);
|
|
21
|
+
this.indices.set(item.id, this.order.length);
|
|
22
|
+
this.order.push(item.id);
|
|
23
|
+
}
|
|
24
|
+
this.syncRequests();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get(id: string): ChatItem | undefined {
|
|
28
|
+
return this.items.get(id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
list(): ChatItem[] {
|
|
32
|
+
return this.order.map((id) => this.items.get(id)!);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
find<K extends ChatItem['kind']>(kind: K, match: (item: Extract<ChatItem, { kind: K }>) => boolean): Extract<ChatItem, { kind: K }> | undefined {
|
|
36
|
+
for (let i = this.order.length - 1; i >= 0; i--) {
|
|
37
|
+
const item = this.items.get(this.order[i]!);
|
|
38
|
+
if (item?.kind === kind && match(item as Extract<ChatItem, { kind: K }>)) {
|
|
39
|
+
return item as Extract<ChatItem, { kind: K }>;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
upsert(item: ChatItem): ChatEvent {
|
|
46
|
+
if (!this.items.has(item.id)) {
|
|
47
|
+
this.indices.set(item.id, this.order.length);
|
|
48
|
+
this.order.push(item.id);
|
|
49
|
+
}
|
|
50
|
+
this.items.set(item.id, item);
|
|
51
|
+
if (item.kind === 'approval' || item.kind === 'question') {
|
|
52
|
+
this.syncRequests();
|
|
53
|
+
}
|
|
54
|
+
return { type: 'item', item, historyIndex: this.indices.get(item.id)! };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Streams text into an assistant or thinking item, or partial output into a tool call that is still running. */
|
|
58
|
+
appendText(itemId: string, text: string): ChatEvent | null {
|
|
59
|
+
const item = this.items.get(itemId);
|
|
60
|
+
if (item?.kind === 'assistant' || item?.kind === 'thinking') {
|
|
61
|
+
this.items.set(itemId, { ...item, text: item.text + text });
|
|
62
|
+
return { type: 'delta', itemId, text };
|
|
63
|
+
}
|
|
64
|
+
if (item?.kind === 'tool' && item.state === 'running') {
|
|
65
|
+
const progress = item.progress ?? { startedAt: null, description: null, output: null };
|
|
66
|
+
this.items.set(itemId, { ...item, progress: { ...progress, output: (progress.output ?? '') + text } });
|
|
67
|
+
return { type: 'delta', itemId, text };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
patchInfo(patch: Partial<ChatInfo>): ChatEvent {
|
|
73
|
+
this.info = { ...this.info, ...patch };
|
|
74
|
+
return { type: 'info', info: this.info };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Empties the thread; the info carries on with the patch on top. */
|
|
78
|
+
reset(patch: Partial<ChatInfo>): ChatEvent {
|
|
79
|
+
this.items.clear();
|
|
80
|
+
this.indices.clear();
|
|
81
|
+
this.historyGeneration = randomUUID();
|
|
82
|
+
this.order.length = 0;
|
|
83
|
+
this.info = { ...this.info, ...patch };
|
|
84
|
+
this.syncRequests();
|
|
85
|
+
return { type: 'reset', info: this.info, items: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Applies an event this thread answered before, as a log read back after a restart hands it in. */
|
|
89
|
+
apply(event: ChatEvent): void {
|
|
90
|
+
switch (event.type) {
|
|
91
|
+
case 'item':
|
|
92
|
+
this.upsert(event.item);
|
|
93
|
+
break;
|
|
94
|
+
case 'delta':
|
|
95
|
+
this.appendText(event.itemId, event.text);
|
|
96
|
+
break;
|
|
97
|
+
case 'info':
|
|
98
|
+
this.info = event.info;
|
|
99
|
+
break;
|
|
100
|
+
case 'reset':
|
|
101
|
+
this.reset(event.info);
|
|
102
|
+
for (const item of event.items) {
|
|
103
|
+
this.upsert(item);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setStatus(status: AgentStatus): ChatEvent {
|
|
110
|
+
return this.patchInfo({ status });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* A request can outlive the turn it came in, when work the CLI runs beside its turns asked it. */
|
|
114
|
+
statusFor(activeTurnId: string | null): AgentStatus {
|
|
115
|
+
if (this.pending().length > 0) {
|
|
116
|
+
return 'needs-you';
|
|
117
|
+
}
|
|
118
|
+
return activeTurnId === null ? 'idle' : 'running';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pending(): Array<ChatApprovalItem | ChatQuestionItem> {
|
|
122
|
+
return this.list().filter(
|
|
123
|
+
(item): item is ChatApprovalItem | ChatQuestionItem =>
|
|
124
|
+
(item.kind === 'approval' && item.decision === 'pending') || (item.kind === 'question' && item.state === 'pending')
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* `info.requests` follows the request items, so whatever event comes next carries what the chat waits on,
|
|
130
|
+
* and a log read back rebuilds it the same way.
|
|
131
|
+
*/
|
|
132
|
+
private syncRequests(): void {
|
|
133
|
+
const { requests: _requests, ...rest } = this.info;
|
|
134
|
+
const pending = this.pending();
|
|
135
|
+
this.info = pending.length === 0 ? rest : { ...rest, requests: requestSummaries(pending) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
history(limit = 60, cursor?: string): ChatHistoryResult {
|
|
139
|
+
let end = this.order.length;
|
|
140
|
+
if (cursor) {
|
|
141
|
+
const [generation, offset] = cursor.split(':');
|
|
142
|
+
end = Number(offset);
|
|
143
|
+
if (generation !== this.historyGeneration || !/^\d+$/.test(offset ?? '') || !Number.isSafeInteger(end) || end < 0 || end > this.order.length) {
|
|
144
|
+
throw new ChatError('history-expired', 'The conversation changed. Reload its history.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const items: ChatItem[] = [];
|
|
148
|
+
let bytes = 0;
|
|
149
|
+
let start = end;
|
|
150
|
+
while (start > 0 && items.length < Math.min(100, Math.max(1, limit))) {
|
|
151
|
+
const item = this.items.get(this.order[start - 1]!)!;
|
|
152
|
+
const size = Buffer.byteLength(JSON.stringify(item));
|
|
153
|
+
// An atomic item is never cut; an oversized item travels alone.
|
|
154
|
+
if (items.length && bytes + size > 512 * 1024) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
items.unshift(item);
|
|
158
|
+
bytes += size;
|
|
159
|
+
start--;
|
|
160
|
+
}
|
|
161
|
+
return { items, history: { start, cursor: start ? `${this.historyGeneration}:${start}` : null } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
snapshot(): { info: ChatInfo; items: ChatItem[] } {
|
|
165
|
+
return { info: this.info, items: this.list() };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import type { ChatCore } from './chat-core.ts';
|
|
3
|
+
import type { ChatSession } from './chat-session.ts';
|
|
4
|
+
|
|
5
|
+
/* A chat the host may open a turn in, as the outbox handlers see it: what its thread holds, and the one call that opens one, false while a turn or an owed resume is in the way. */
|
|
6
|
+
export interface WakeChat {
|
|
7
|
+
items(): ChatItem[];
|
|
8
|
+
persist?(): Promise<void>;
|
|
9
|
+
wake(wake: { text: string; label: string; note?: string; taskIds: string[]; messageFrom?: string[] }): boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ChatOpenerDeps {
|
|
13
|
+
chats: Pick<ChatCore, 'get' | 'hasStored' | 'create' | 'save'>;
|
|
14
|
+
/* Whether a project still places the node; one that left the document is nobody's to open a turn in. */
|
|
15
|
+
placed(nodeId: string): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* A chat, loaded from disk when nobody has it, so work owed to a chat nobody mounted since the restart
|
|
20
|
+
* still lands. Null for a node with no thread at all.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadChat(deps: ChatOpenerDeps, chatId: string): Promise<ChatSession | null> {
|
|
23
|
+
if (!deps.chats.get(chatId)) {
|
|
24
|
+
if (!deps.placed(chatId) || !(await deps.chats.hasStored(chatId))) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
await deps.chats.create({ chatId });
|
|
28
|
+
}
|
|
29
|
+
return deps.chats.get(chatId) ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The chat a turn is to be opened in. */
|
|
33
|
+
export function chatOpener(deps: ChatOpenerDeps) {
|
|
34
|
+
return async (chatId: string): Promise<WakeChat | null> => {
|
|
35
|
+
const session = await loadChat(deps, chatId);
|
|
36
|
+
return session
|
|
37
|
+
? {
|
|
38
|
+
items: () => session.thread.list(),
|
|
39
|
+
persist: () => deps.chats.save(chatId),
|
|
40
|
+
// A turn before the reset would stop on the same limit and spend one of the resume's tries, so it waits for the resume turn.
|
|
41
|
+
wake: (wake) => session.info.resumeAt === undefined && session.wake(wake) !== null
|
|
42
|
+
}
|
|
43
|
+
: null;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The clients listening to one part of a host. A client is one sink at a time; a client that
|
|
3
|
+
* connects again replaces the one before it.
|
|
4
|
+
*/
|
|
5
|
+
export class ClientSinks<TEvent> {
|
|
6
|
+
private readonly sinks = new Map<string, (event: TEvent) => void>();
|
|
7
|
+
private readonly onRelease: ((clientId: string) => void) | null;
|
|
8
|
+
|
|
9
|
+
/* What else a client leaves behind, for the parts that keep more than a sink per client. */
|
|
10
|
+
constructor(onRelease: ((clientId: string) => void) | null = null) {
|
|
11
|
+
this.onRelease = onRelease;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
subscribe(clientId: string, sink: (event: TEvent) => void): () => void {
|
|
15
|
+
this.sinks.set(clientId, sink);
|
|
16
|
+
return () => {
|
|
17
|
+
// A client that subscribes again before it unsubscribes keeps its newer sink.
|
|
18
|
+
if (this.sinks.get(clientId) !== sink) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.sinks.delete(clientId);
|
|
22
|
+
this.onRelease?.(clientId);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
emit(event: TEvent): void {
|
|
27
|
+
for (const sink of this.sinks.values()) {
|
|
28
|
+
sink(event);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
to(clientId: string, event: TEvent): void {
|
|
33
|
+
this.sinks.get(clientId)?.(event);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clientIds(): string[] {
|
|
37
|
+
return [...this.sinks.keys()];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The shape every failure on the wire has: a code a client can branch on and a message a person reads.
|
|
3
|
+
* A domain gives it a union of its own codes, so a typo in a code is a compile error instead of a
|
|
4
|
+
* string the other end never matches.
|
|
5
|
+
*/
|
|
6
|
+
export class CodedError<TCode extends string = string> extends Error {
|
|
7
|
+
readonly code: TCode;
|
|
8
|
+
/* What the actor can do instead. Only a refusal fills this; the rest carry an empty list. */
|
|
9
|
+
readonly lines: string[];
|
|
10
|
+
|
|
11
|
+
constructor(code: TCode, message: string, lines: string[] = []) {
|
|
12
|
+
super(message);
|
|
13
|
+
// The subclass names itself, so a stack reads `GitError:` and not `Error:`.
|
|
14
|
+
this.name = new.target.name;
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.lines = lines;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type ParsedArgv =
|
|
2
|
+
| { ok: true; positionals: string[]; flags: Record<string, string>; switches: Set<string> }
|
|
3
|
+
| { ok: false; code: 'unknown-flag' | 'missing-value' | 'duplicate-flag' | 'unexpected-value'; message: string };
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Splits the words after a verb into positionals, `--flag value` pairs (`--flag=value` too, for a
|
|
7
|
+
* value that itself starts with `--`) and the switches the verb declared. Anything not declared a
|
|
8
|
+
* switch takes a value, so a flag that lost its argument is refused instead of swallowing the next
|
|
9
|
+
* word, and a flag given twice is refused rather than one of the two silently winning. A name that is
|
|
10
|
+
* both a switch and a flag is on when written bare and takes a value only after `=` (`--state=full`).
|
|
11
|
+
*/
|
|
12
|
+
export function parseArgv(argv: readonly string[], known: readonly string[], switches: readonly string[] = []): ParsedArgv {
|
|
13
|
+
const positionals: string[] = [];
|
|
14
|
+
const flags: Record<string, string> = {};
|
|
15
|
+
const given = new Set<string>();
|
|
16
|
+
const all = [...new Set([...known, ...switches])];
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const word = argv[i]!;
|
|
19
|
+
if (!word.startsWith('--')) {
|
|
20
|
+
positionals.push(word);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const equals = word.indexOf('=');
|
|
24
|
+
const name = word.slice(2, equals === -1 ? undefined : equals);
|
|
25
|
+
if (!all.includes(name)) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
code: 'unknown-flag',
|
|
29
|
+
message: all.length === 0 ? `--${name} is not a flag here; this verb takes none` : `--${name} is not one of --${all.join(', --')}`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (Object.hasOwn(flags, name) || given.has(name)) {
|
|
33
|
+
return { ok: false, code: 'duplicate-flag', message: `--${name} is given twice` };
|
|
34
|
+
}
|
|
35
|
+
if (switches.includes(name)) {
|
|
36
|
+
if (equals !== -1 && known.includes(name)) {
|
|
37
|
+
flags[name] = word.slice(equals + 1);
|
|
38
|
+
given.add(name);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (equals !== -1) {
|
|
42
|
+
return { ok: false, code: 'unexpected-value', message: `--${name} takes no value; it is on when you write it and off when you do not` };
|
|
43
|
+
}
|
|
44
|
+
given.add(name);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
let value: string;
|
|
48
|
+
if (equals !== -1) {
|
|
49
|
+
value = word.slice(equals + 1);
|
|
50
|
+
} else {
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
if (next === undefined || next.startsWith('--')) {
|
|
53
|
+
return { ok: false, code: 'missing-value', message: `--${name} needs a value (write --${name}=<value> for one that starts with --)` };
|
|
54
|
+
}
|
|
55
|
+
value = next;
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
flags[name] = value;
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, positionals, flags, switches: given };
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* How every no reads: `refused`, the code a script branches on and the sentence a model reads, and
|
|
3
|
+
* under it a line per thing the actor can do instead. One row per line, and a line is a row of
|
|
4
|
+
* tab-separated fields of its own, so only a newline is taken out of it: that would split the row in
|
|
5
|
+
* two, and everything under the first row is read as advice.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* A field that goes into a tab-separated line; a tab or a newline in a title would split the row. */
|
|
9
|
+
export function field(value: string): string {
|
|
10
|
+
return value.replace(/[\t\r\n]+/g, ' ');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function row(line: string): string {
|
|
14
|
+
return line.replace(/[\r\n]+/g, ' ');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function rows(code: string, message: string, lines: readonly string[]): string[] {
|
|
18
|
+
return [`${field(code)}\t${field(message)}`, ...lines.map(row)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* What a verb is refused with, whole: the `refused` row and what to do instead under it. */
|
|
22
|
+
export function refusalBody(code: string, message: string, lines: readonly string[] = []): string {
|
|
23
|
+
const [first, ...rest] = rows(code, message, lines);
|
|
24
|
+
return [`refused\t${first}`, ...rest].join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The same without the word that names it, for a host that adds the prefix in its CLI:
|
|
29
|
+
* The context CLI puts the prefix on, so a CLI of an older build still prints one refusal.
|
|
30
|
+
*/
|
|
31
|
+
export function refusalRows(code: string, message: string, lines: readonly string[] = []): string {
|
|
32
|
+
return rows(code, message, lines).join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedRefusal {
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
lines: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Reads either form back. Null when the text is no refusal at all, which is not the same as one without advice. */
|
|
42
|
+
export function parseRefusalBody(text: string): ParsedRefusal | null {
|
|
43
|
+
const [first = '', ...lines] = text.split('\n').filter((line) => line.trim() !== '');
|
|
44
|
+
const parts = first.split('\t');
|
|
45
|
+
const [code, message] = parts[0] === 'refused' ? parts.slice(1) : parts;
|
|
46
|
+
if (!code || !message) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return { code, message, lines };
|
|
50
|
+
}
|