@adecore/agents 0.0.1 → 0.17.0-beta.2
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 +48 -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,392 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CodedError } from '../coded-error.ts';
|
|
3
|
+
import { parseArgv } from './argv.ts';
|
|
4
|
+
|
|
5
|
+
/* A verb said no. The code is for a script, the message for the agent, the lines for what it can pick instead. */
|
|
6
|
+
export class VerbRefusal extends CodedError {}
|
|
7
|
+
|
|
8
|
+
/* What every call carries, whatever else an app puts in it. */
|
|
9
|
+
export interface VerbCallBase {
|
|
10
|
+
/* What `--revision` named: the revision of the document the caller read before it decided on this write. */
|
|
11
|
+
expectedRevision?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface VerbHelp {
|
|
15
|
+
name: string;
|
|
16
|
+
usage: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
/* The tab-separated lines `help` prints for this one: everything the one-line summary has no room for. */
|
|
19
|
+
detail: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* A word that runs on its own, such as `help`: no noun in front of it. */
|
|
23
|
+
export interface Verb<Call extends VerbCallBase> extends VerbHelp {
|
|
24
|
+
served: 'verb';
|
|
25
|
+
/* The flags the parser takes, from the schema validation runs, so help and a test see the same list. */
|
|
26
|
+
flagNames: readonly string[];
|
|
27
|
+
/* Whether `--dry-run` means something here; only what makes something takes it. */
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
run(argv: readonly string[], call: Call): Promise<string[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The word after a noun, such as `rename` in `node rename`; `name` is both words, which is what a refusal and `help` say. */
|
|
33
|
+
export interface Action<Call extends VerbCallBase> extends Omit<Verb<Call>, 'served'> {
|
|
34
|
+
word: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* A first word that only says what the action after it works on: `node`, `link`, `view`. */
|
|
38
|
+
export interface Noun<Call extends VerbCallBase> {
|
|
39
|
+
served: 'noun';
|
|
40
|
+
name: string;
|
|
41
|
+
/* `<list|new|...> ...`, read off the actions, so it cannot name one that is not there. */
|
|
42
|
+
usage: string;
|
|
43
|
+
summary: string;
|
|
44
|
+
detail: readonly string[];
|
|
45
|
+
actions: readonly Action<Call>[];
|
|
46
|
+
run(argv: readonly string[], call: Call): Promise<string[]>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* A word the CLI answers on a route of its own, such as `list` and `read`; in the registry so `help` names it. */
|
|
50
|
+
export interface ContextVerb extends VerbHelp {
|
|
51
|
+
served: 'context';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type VerbEntry<Call extends VerbCallBase> = Verb<Call> | Noun<Call> | ContextVerb;
|
|
55
|
+
|
|
56
|
+
export const DRY_RUN_FLAG = 'dry-run';
|
|
57
|
+
|
|
58
|
+
export const REVISION_FLAG = 'revision';
|
|
59
|
+
|
|
60
|
+
/* Agents dry-run every call before the real one to be safe, which doubles what each costs them. */
|
|
61
|
+
export const DRY_RUN_PREVIEW = 'a refused call makes nothing either, so it is only a preview and never needed for safety';
|
|
62
|
+
|
|
63
|
+
export interface ArgsSpec<Positionals extends z.ZodType, Flags extends z.ZodObject, Call extends VerbCallBase> {
|
|
64
|
+
positionals: Positionals;
|
|
65
|
+
/* Every flag is a string that takes a value; the keys of this object are the flags the parser knows. */
|
|
66
|
+
flags: Flags;
|
|
67
|
+
/* The flags that are on by being written and take no value of their own. */
|
|
68
|
+
switches?: readonly string[];
|
|
69
|
+
/* Whether this makes something and can therefore be asked to validate and stop. */
|
|
70
|
+
dryRun?: boolean;
|
|
71
|
+
/* Whether this writes a document and so takes the revision it was decided on. */
|
|
72
|
+
revision?: boolean;
|
|
73
|
+
run(input: { positionals: z.infer<Positionals>; flags: z.infer<Flags>; switches: ReadonlySet<string>; dryRun: boolean }, call: Call): Promise<string[]>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface VerbSpec<Positionals extends z.ZodType, Flags extends z.ZodObject, Call extends VerbCallBase>
|
|
77
|
+
extends VerbHelp, ArgsSpec<Positionals, Flags, Call> {}
|
|
78
|
+
|
|
79
|
+
export interface NounSpec<Call extends VerbCallBase> {
|
|
80
|
+
name: string;
|
|
81
|
+
summary: string;
|
|
82
|
+
detail: readonly string[];
|
|
83
|
+
actions: readonly Action<Call>[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Guidance that belongs to no single verb, such as the API a verb's input is written against. */
|
|
87
|
+
export interface HelpTopic {
|
|
88
|
+
name: string;
|
|
89
|
+
/* The one line the root of `help` gives it. */
|
|
90
|
+
summary: string;
|
|
91
|
+
/* The lines `help <topic>` prints, as they are. */
|
|
92
|
+
body: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface HelpSpec<Call extends VerbCallBase> {
|
|
96
|
+
/* A function, since the help verb is itself one of the entries it lists. */
|
|
97
|
+
entries(): readonly VerbEntry<Call>[];
|
|
98
|
+
/* The lines the root of `help` prints between the list and its pointers to the rest. */
|
|
99
|
+
root(): readonly string[];
|
|
100
|
+
/* The one line about failure every help output ends with: how the CLI prints a refusal and what it exits with. */
|
|
101
|
+
refusal: string;
|
|
102
|
+
/* Printed by `help <topic>` and listed under the verbs; a verb or noun of the same name wins. */
|
|
103
|
+
topics?: readonly HelpTopic[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface VerbRegistry<Call extends VerbCallBase> {
|
|
107
|
+
/*
|
|
108
|
+
* One object per verb, holding its synopsis and the schemas its arguments are checked against, so
|
|
109
|
+
* `help` renders from what validation runs and the two cannot drift apart.
|
|
110
|
+
*/
|
|
111
|
+
defineVerb<Positionals extends z.ZodType, Flags extends z.ZodObject>(spec: VerbSpec<Positionals, Flags, Call>): Verb<Call>;
|
|
112
|
+
/* `spec.name` is only the action's own word; the noun goes in front of it. */
|
|
113
|
+
defineAction<Positionals extends z.ZodType, Flags extends z.ZodObject>(noun: string, spec: VerbSpec<Positionals, Flags, Call>): Action<Call>;
|
|
114
|
+
/*
|
|
115
|
+
* A noun and its actions. The table is the only source: the dispatch, the row in `help` and
|
|
116
|
+
* everything `help <noun>` prints come out of it, so an action is documented by being defined.
|
|
117
|
+
*/
|
|
118
|
+
defineNoun(spec: NounSpec<Call>): Noun<Call>;
|
|
119
|
+
/* The `help` verb over every entry, rendered from the same objects the dispatch runs. */
|
|
120
|
+
defineHelp(spec: HelpSpec<Call>): Verb<Call>;
|
|
121
|
+
/* What takes `--dry-run`, in the order each was defined. */
|
|
122
|
+
dryRunVerbNames(): readonly string[];
|
|
123
|
+
/* Said once under the list in `help`, since the flag is on some actions and refused by name on the rest. */
|
|
124
|
+
dryRunLine(): string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function flagsOf<Call extends VerbCallBase>(spec: ArgsSpec<z.ZodType, z.ZodObject, Call>): { values: string[]; switches: string[] } {
|
|
128
|
+
return {
|
|
129
|
+
values: [...Object.keys(spec.flags.shape), ...(spec.revision === true ? [REVISION_FLAG] : [])],
|
|
130
|
+
switches: [...(spec.switches ?? []), ...(spec.dryRun === true ? [DRY_RUN_FLAG] : [])]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function flagSpelled(usage: string, flag: string): boolean {
|
|
135
|
+
return new RegExp(`(^|[^\\w-])${flag}(?![\\w-])`).test(usage);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/*
|
|
139
|
+
* The synopsis with every flag the detail documents: what the hand-written one leaves out is added
|
|
140
|
+
* from its detail line, bracketed unless that line says it is required, so the two never drift.
|
|
141
|
+
*/
|
|
142
|
+
function usageWithFlags(usage: string, detail: readonly string[]): string {
|
|
143
|
+
const missing = detail
|
|
144
|
+
.filter((line) => line.startsWith('flag\t--'))
|
|
145
|
+
.map((line) => line.split('\t'))
|
|
146
|
+
.filter(([, syntax]) => !flagSpelled(usage, syntax!.split(' ')[0]!))
|
|
147
|
+
.map(([, syntax, need]) => (need === 'required' ? syntax! : `[${syntax}]`));
|
|
148
|
+
return [usage, ...missing].filter((part) => part !== '').join(' ');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* Every schema in the registry carries a sentence of its own, because zod's default ("Too small:
|
|
153
|
+
* expected array to have >=1 items") names neither what is missing nor what may go there.
|
|
154
|
+
*/
|
|
155
|
+
function issueMessage(error: z.ZodError, what: 'argument' | 'flag'): string {
|
|
156
|
+
return error.issues[0]?.message ?? `Bad ${what}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* Every row says what it is in its first field, so the lines under the list are never read as verbs.
|
|
161
|
+
* A noun names its actions and not their arguments, which keeps the root short; `help <noun>` has those.
|
|
162
|
+
*/
|
|
163
|
+
export function summaryLines<Call extends VerbCallBase>(entries: readonly VerbEntry<Call>[]): string[] {
|
|
164
|
+
return entries.map((entry) =>
|
|
165
|
+
entry.served === 'noun'
|
|
166
|
+
? `noun\t${entry.name}\t${entry.actions.map((action) => action.word).join('|')}\t${entry.summary}`
|
|
167
|
+
: `verb\t${entry.name}\t${entry.usage}\t${entry.summary}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* The signatures of every action of a noun, for `help <noun>` and a refusal about one. */
|
|
172
|
+
function actionLines<Call extends VerbCallBase>(noun: Noun<Call>): string[] {
|
|
173
|
+
return noun.actions.map((action) => `action\t${action.name}\t${action.usage}\t${action.summary}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* `cli` is the command an agent runs, such as `app-context`, which every pointer to `help` names. */
|
|
177
|
+
export function createVerbRegistry<Call extends VerbCallBase>(options: { cli: string }): VerbRegistry<Call> {
|
|
178
|
+
const { cli } = options;
|
|
179
|
+
/*
|
|
180
|
+
* What takes `--dry-run`, filled as each is defined. Everything else refuses the flag by name and
|
|
181
|
+
* points at these, so an agent never gets a silent nothing from a dry run that was never dry.
|
|
182
|
+
*/
|
|
183
|
+
const dryRunVerbs: string[] = [];
|
|
184
|
+
|
|
185
|
+
/* What a refusal over arguments points at; for an action `name` is both words, which is what `help` takes. */
|
|
186
|
+
const usageLinesOf = (name: string, usage: string): string[] => [`usage\t${name}\t${usage}`, `detail\t${cli} help ${name}`];
|
|
187
|
+
|
|
188
|
+
/* One set of arguments parsed and run. `name` is what a refusal calls it, so `view new` refuses under both its words. */
|
|
189
|
+
const runArgs = async <Positionals extends z.ZodType, Flags extends z.ZodObject>(
|
|
190
|
+
name: string,
|
|
191
|
+
usage: string,
|
|
192
|
+
spec: ArgsSpec<Positionals, Flags, Call>,
|
|
193
|
+
argv: readonly string[],
|
|
194
|
+
call: Call
|
|
195
|
+
): Promise<string[]> => {
|
|
196
|
+
const usageLines = usageLinesOf(name, usage);
|
|
197
|
+
const { values, switches } = flagsOf(spec);
|
|
198
|
+
const parsed = parseArgv(argv, values, switches);
|
|
199
|
+
if (!parsed.ok) {
|
|
200
|
+
const dryRunAsked = argv.some((word) => word === `--${DRY_RUN_FLAG}` || word.startsWith(`--${DRY_RUN_FLAG}=`));
|
|
201
|
+
if (parsed.code === 'unknown-flag' && spec.dryRun !== true && dryRunAsked) {
|
|
202
|
+
throw new VerbRefusal(
|
|
203
|
+
'no-dry-run',
|
|
204
|
+
`${name} takes no --${DRY_RUN_FLAG}; only the verbs that make something do`,
|
|
205
|
+
dryRunVerbs.map((verb) => `verb\t${verb}\ttakes --${DRY_RUN_FLAG}`)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
throw new VerbRefusal(parsed.code, parsed.message, usageLines);
|
|
209
|
+
}
|
|
210
|
+
const positionals = spec.positionals.safeParse(parsed.positionals);
|
|
211
|
+
if (!positionals.success) {
|
|
212
|
+
throw new VerbRefusal('bad-arguments', issueMessage(positionals.error, 'argument'), usageLines);
|
|
213
|
+
}
|
|
214
|
+
const { [REVISION_FLAG]: revision, ...given } = parsed.flags;
|
|
215
|
+
const flags = spec.flags.safeParse(given);
|
|
216
|
+
if (!flags.success) {
|
|
217
|
+
throw new VerbRefusal('bad-arguments', issueMessage(flags.error, 'flag'), usageLines);
|
|
218
|
+
}
|
|
219
|
+
if (revision !== undefined && !/^\d+$/.test(revision)) {
|
|
220
|
+
throw new VerbRefusal('bad-arguments', `--${REVISION_FLAG} takes the whole number a list printed as revision`, usageLines);
|
|
221
|
+
}
|
|
222
|
+
return spec.run(
|
|
223
|
+
{ positionals: positionals.data, flags: flags.data, switches: parsed.switches, dryRun: parsed.switches.has(DRY_RUN_FLAG) },
|
|
224
|
+
revision === undefined ? call : { ...call, expectedRevision: Number(revision) }
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/* What a verb and an action share: the synopsis beside the schemas its arguments are checked against. */
|
|
229
|
+
const defineArgs = <Positionals extends z.ZodType, Flags extends z.ZodObject>(
|
|
230
|
+
name: string,
|
|
231
|
+
spec: VerbSpec<Positionals, Flags, Call>
|
|
232
|
+
): Omit<Verb<Call>, 'served'> => {
|
|
233
|
+
const { values, switches } = flagsOf(spec);
|
|
234
|
+
if (spec.dryRun === true) {
|
|
235
|
+
dryRunVerbs.push(name);
|
|
236
|
+
}
|
|
237
|
+
const usage = usageWithFlags(spec.usage, spec.detail);
|
|
238
|
+
return {
|
|
239
|
+
name,
|
|
240
|
+
usage,
|
|
241
|
+
summary: spec.summary,
|
|
242
|
+
detail: spec.detail,
|
|
243
|
+
flagNames: [...new Set([...values, ...switches])],
|
|
244
|
+
dryRun: spec.dryRun === true,
|
|
245
|
+
run: (argv, call) => runArgs(name, usage, spec, argv, call)
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const defineVerb = <Positionals extends z.ZodType, Flags extends z.ZodObject>(spec: VerbSpec<Positionals, Flags, Call>): Verb<Call> => ({
|
|
250
|
+
served: 'verb',
|
|
251
|
+
...defineArgs(spec.name, spec)
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const defineAction = <Positionals extends z.ZodType, Flags extends z.ZodObject>(noun: string, spec: VerbSpec<Positionals, Flags, Call>): Action<Call> => ({
|
|
255
|
+
word: spec.name,
|
|
256
|
+
...defineArgs(`${noun} ${spec.name}`, spec)
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const defineNoun = (spec: NounSpec<Call>): Noun<Call> => {
|
|
260
|
+
const words = spec.actions.map((action) => action.word);
|
|
261
|
+
return {
|
|
262
|
+
served: 'noun',
|
|
263
|
+
name: spec.name,
|
|
264
|
+
usage: `<${words.join('|')}> ...`,
|
|
265
|
+
summary: spec.summary,
|
|
266
|
+
detail: spec.detail,
|
|
267
|
+
actions: spec.actions,
|
|
268
|
+
async run(argv, call) {
|
|
269
|
+
const [word, ...rest] = argv;
|
|
270
|
+
const action = spec.actions.find((candidate) => candidate.word === word);
|
|
271
|
+
if (!action) {
|
|
272
|
+
throw new VerbRefusal(
|
|
273
|
+
'unknown-action',
|
|
274
|
+
`${spec.name} needs one of ${words.join(', ')}${word === undefined ? '' : `, and ${word} is not one`}`,
|
|
275
|
+
[...spec.actions.map((candidate) => `usage\t${candidate.name}\t${candidate.usage}`), `detail\t${cli} help ${spec.name}`]
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return action.run(rest, call);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const defineHelp = (spec: HelpSpec<Call>): Verb<Call> => {
|
|
284
|
+
const detailLines = (entry: VerbHelp): string[] => [`usage\t${entry.name}\t${entry.usage}`, `about\t${entry.summary}`, ...entry.detail, spec.refusal];
|
|
285
|
+
const nounLines = (noun: Noun<Call>): string[] => [
|
|
286
|
+
`usage\t${noun.name}\t${noun.usage}`,
|
|
287
|
+
`about\t${noun.summary}`,
|
|
288
|
+
...noun.detail,
|
|
289
|
+
...actionLines(noun),
|
|
290
|
+
`detail\t${cli} help ${noun.name} <action>\tone action in full`,
|
|
291
|
+
spec.refusal
|
|
292
|
+
];
|
|
293
|
+
const topics = spec.topics ?? [];
|
|
294
|
+
const topicLines = topics.map((topic) => `topic\t${topic.name}\t${topic.summary}`);
|
|
295
|
+
const topicPointer = topics.length > 0 ? [`detail\t${cli} help <topic>\tone topic in full`] : [];
|
|
296
|
+
return defineVerb({
|
|
297
|
+
name: 'help',
|
|
298
|
+
usage: '[noun] [action]',
|
|
299
|
+
summary:
|
|
300
|
+
'Lists every verb and noun; with a noun the signature of each of its actions, with a noun and an action or with a verb everything that one takes',
|
|
301
|
+
detail: [
|
|
302
|
+
topics.length > 0
|
|
303
|
+
? 'argument\t<noun>\toptional\tThe noun, verb or topic to detail; without one every verb, noun and topic is listed'
|
|
304
|
+
: 'argument\t<noun>\toptional\tThe noun or verb to detail; without one every verb and noun is listed',
|
|
305
|
+
'argument\t<action>\toptional\tOne action of that noun, in full',
|
|
306
|
+
'prints\tverb\tname\targuments\tsummary\tone row per verb',
|
|
307
|
+
'prints\tnoun\tname\tactions\tsummary\tone row per noun, its actions separated by |; a row that starts with neither is not one',
|
|
308
|
+
...(topics.length > 0 ? ['prints\ttopic\tname\tsummary\tone row per topic, after the verbs and nouns'] : [])
|
|
309
|
+
],
|
|
310
|
+
positionals: z.array(z.string()).max(2, 'help takes a verb, or a noun and one of its actions, and nothing else'),
|
|
311
|
+
flags: z.object({}),
|
|
312
|
+
run: async ({ positionals: [name, actionWord] }) => {
|
|
313
|
+
const entries = spec.entries();
|
|
314
|
+
if (name === undefined) {
|
|
315
|
+
return [
|
|
316
|
+
...summaryLines(entries),
|
|
317
|
+
...topicLines,
|
|
318
|
+
...spec.root(),
|
|
319
|
+
`detail\t${cli} help <noun>\tthe signature of every action of a noun`,
|
|
320
|
+
`detail\t${cli} help <noun> <action>\tone action in full`,
|
|
321
|
+
`detail\t${cli} help <verb>\tone verb in full`,
|
|
322
|
+
...topicPointer,
|
|
323
|
+
spec.refusal
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
const entry = entries.find((candidate) => candidate.name === name);
|
|
327
|
+
const topic = entry ? undefined : topics.find((candidate) => candidate.name === name);
|
|
328
|
+
if (topic) {
|
|
329
|
+
if (actionWord !== undefined) {
|
|
330
|
+
throw new VerbRefusal('bad-arguments', `${name} is a topic and has no actions; ${cli} help ${name} prints it`, [
|
|
331
|
+
`detail\t${cli} help ${name}\tthe topic in full`
|
|
332
|
+
]);
|
|
333
|
+
}
|
|
334
|
+
return [`about\t${topic.summary}`, ...topic.body];
|
|
335
|
+
}
|
|
336
|
+
if (!entry) {
|
|
337
|
+
throw new VerbRefusal('unknown-verb', topics.length > 0 ? `${name} is not a verb, a noun or a topic` : `${name} is not a verb or a noun`, [
|
|
338
|
+
`detail\t${cli} help\tevery verb and noun, with what each takes`,
|
|
339
|
+
...summaryLines(entries),
|
|
340
|
+
...topicLines
|
|
341
|
+
]);
|
|
342
|
+
}
|
|
343
|
+
if (entry.served !== 'noun') {
|
|
344
|
+
if (actionWord !== undefined) {
|
|
345
|
+
throw new VerbRefusal('bad-arguments', `${name} is a verb and has no actions; ${cli} help ${name} details it`, [
|
|
346
|
+
`detail\t${cli} help ${name}\tone verb in full`
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
return detailLines(entry);
|
|
350
|
+
}
|
|
351
|
+
if (actionWord === undefined) {
|
|
352
|
+
return nounLines(entry);
|
|
353
|
+
}
|
|
354
|
+
const action = entry.actions.find((candidate) => candidate.word === actionWord);
|
|
355
|
+
if (!action) {
|
|
356
|
+
throw new VerbRefusal('unknown-action', `${actionWord} is not an action of ${name}`, actionLines(entry));
|
|
357
|
+
}
|
|
358
|
+
return detailLines(action);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const dryRunVerbNames = (): readonly string[] => dryRunVerbs;
|
|
364
|
+
|
|
365
|
+
const dryRunLine = (): string =>
|
|
366
|
+
`dry run\t--${DRY_RUN_FLAG}\t${dryRunVerbs.join(', ')}\tsame checks, nothing made; ${DRY_RUN_PREVIEW}; every other verb refuses the flag`;
|
|
367
|
+
|
|
368
|
+
return { defineVerb, defineAction, defineNoun, defineHelp, dryRunVerbNames, dryRunLine };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* How much came in, for a refusal that counts: zod hands its error function the input it rejected. */
|
|
372
|
+
export function lengthOf(input: unknown): number {
|
|
373
|
+
if (typeof input === 'string' || Array.isArray(input)) {
|
|
374
|
+
return input.length;
|
|
375
|
+
}
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* A flag a verb cannot do without: the same sentence whether it was left out or left empty, since
|
|
380
|
+
zod's own ("expected string, received undefined") names neither the flag nor what goes in it. */
|
|
381
|
+
export function requiredField(needs: string): z.ZodString {
|
|
382
|
+
return z.string({ error: needs }).min(1, needs);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/*
|
|
386
|
+
* What a refusal lists, or one line saying the set is empty. A refusal that promises the groups of a
|
|
387
|
+
* canvas and then prints nothing reads as a daemon that lost them, where the truth is that there are
|
|
388
|
+
* none, which is a different thing to do something about.
|
|
389
|
+
*/
|
|
390
|
+
export function orNote(lines: string[], note: string): string[] {
|
|
391
|
+
return lines.length === 0 ? [`note\t${note}`] : lines;
|
|
392
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* How an error reaches a log, a note or a reply. Bun prints an Error object handed to the console with a
|
|
3
|
+
* code frame of the bundle it came from, which in a compiled binary is a few lines of minified source,
|
|
4
|
+
* so an error is always turned into a string first.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The message of an error, or its whole stack with `debug` set. */
|
|
8
|
+
export function describeError(error: unknown, debug: boolean): string {
|
|
9
|
+
if (!(error instanceof Error)) {
|
|
10
|
+
return String(error);
|
|
11
|
+
}
|
|
12
|
+
if (debug) {
|
|
13
|
+
return error.stack ?? `${error.name}: ${error.message}`;
|
|
14
|
+
}
|
|
15
|
+
return error.message === '' ? error.name : error.message;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The host decides once whether a stack is worth showing; the package reads no variable of its own.
|
|
19
|
+
let showStacks = false;
|
|
20
|
+
|
|
21
|
+
export function setErrorStacks(on: boolean): void {
|
|
22
|
+
showStacks = on;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `describeError` as the host set it, for a log line or a note. */
|
|
26
|
+
export function errorText(error: unknown): string {
|
|
27
|
+
return describeError(error, showStacks);
|
|
28
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AGENT_EVENT_SCHEMAS, AgentEventType } from '@adecore/agent-contracts';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
export type AgentEventMap = { [E in AgentEventType]: z.infer<(typeof AGENT_EVENT_SCHEMAS)[E]> };
|
|
5
|
+
|
|
6
|
+
/* One event a chat host sends a client, before it is framed. */
|
|
7
|
+
export type AgentEvent = { [E in AgentEventType]: { event: E; payload: AgentEventMap[E] } }[AgentEventType];
|
|
8
|
+
|
|
9
|
+
export type AgentSink = (event: AgentEvent) => void;
|
package/src/fs.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { open, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Windows can refuse a rename while a watcher or an indexer holds the target; a few short retries cover it.
|
|
6
|
+
const RENAME_RETRIES = 5;
|
|
7
|
+
const RENAME_RETRY_MS = 40;
|
|
8
|
+
|
|
9
|
+
// Node has no synchronous sleep; a wait on a value nobody changes is one.
|
|
10
|
+
function sleepSync(ms: number): void {
|
|
11
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isTransient(e: unknown): boolean {
|
|
15
|
+
return typeof e === 'object' && e !== null && 'code' in e && (e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Two writes of the same file in flight at once (a fire-and-forget save next to an awaited one) must not share a temp name.
|
|
19
|
+
let nextTemp = 0;
|
|
20
|
+
|
|
21
|
+
/* Where a file waits while it is being written; the pid keeps two hosts over one home apart. */
|
|
22
|
+
export function tempNameFor(target: string): string {
|
|
23
|
+
return `${target}.${process.pid}-${nextTemp++}.tmp`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WriteAtomicOptions {
|
|
27
|
+
/* Flushed to the disk before and after the rename, for a file whose loss costs a person their work. */
|
|
28
|
+
durable?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function writeFlushed(path: string, content: string | Uint8Array, mode: number): Promise<void> {
|
|
32
|
+
const handle = await open(path, 'w', mode);
|
|
33
|
+
try {
|
|
34
|
+
await handle.writeFile(content);
|
|
35
|
+
await handle.sync();
|
|
36
|
+
} finally {
|
|
37
|
+
await handle.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Windows cannot open a directory to flush it, and NTFS journals the rename itself.
|
|
42
|
+
async function syncDirectory(path: string): Promise<void> {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const handle = await open(path, 'r');
|
|
47
|
+
try {
|
|
48
|
+
await handle.sync();
|
|
49
|
+
} finally {
|
|
50
|
+
await handle.close();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* A crash between the two steps leaves a stray temp file, never a half-written file. That holds for
|
|
56
|
+
* a process that dies; after a power cut APFS and ext4 can still hold an empty target, unless the
|
|
57
|
+
* write is durable.
|
|
58
|
+
*/
|
|
59
|
+
export async function writeAtomic(target: string, content: string | Uint8Array, mode = 0o600, options: WriteAtomicOptions = {}): Promise<void> {
|
|
60
|
+
const temp = tempNameFor(target);
|
|
61
|
+
if (options.durable) {
|
|
62
|
+
await writeFlushed(temp, content, mode);
|
|
63
|
+
} else {
|
|
64
|
+
await writeFile(temp, content, { mode });
|
|
65
|
+
}
|
|
66
|
+
for (let attempt = 0; ; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
await rename(temp, target);
|
|
69
|
+
break;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (!isTransient(e) || attempt >= RENAME_RETRIES) {
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, RENAME_RETRY_MS));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (options.durable) {
|
|
78
|
+
await syncDirectory(dirname(target));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* The same rename, without a turn of the event loop. A `bun --watch` reload restarts a module before
|
|
84
|
+
* an awaited write comes back, and an exit path is gone before one lands, so what has to be on disk
|
|
85
|
+
* before the process turns around goes through here.
|
|
86
|
+
*/
|
|
87
|
+
export function replaceSync(temp: string, target: string): void {
|
|
88
|
+
for (let attempt = 0; ; attempt++) {
|
|
89
|
+
try {
|
|
90
|
+
renameSync(temp, target);
|
|
91
|
+
return;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (!isTransient(e) || attempt >= RENAME_RETRIES) {
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
sleepSync(RENAME_RETRY_MS);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function writeAtomicSync(target: string, content: string | Uint8Array, mode = 0o600): void {
|
|
102
|
+
const temp = tempNameFor(target);
|
|
103
|
+
writeFileSync(temp, content, { mode });
|
|
104
|
+
replaceSync(temp, target);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isNotFound(e: unknown): boolean {
|
|
108
|
+
return typeof e === 'object' && e !== null && 'code' in e && e.code === 'ENOENT';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function fileExists(path: string): Promise<boolean> {
|
|
112
|
+
try {
|
|
113
|
+
await stat(path);
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AGENT_REQUEST_SCHEMAS, parseRequest, type AgentRequestType, type FramePort, type ReplyError } from '@adecore/agent-contracts';
|
|
2
|
+
import type { ChatCore } from '../chat/chat-core.ts';
|
|
3
|
+
import { CodedError } from '../coded-error.ts';
|
|
4
|
+
import { errorText } from '../error-text.ts';
|
|
5
|
+
import type { ProviderAccountsService } from '../providers/accounts/service.ts';
|
|
6
|
+
import type { ProviderRegistry } from '../providers/registry.ts';
|
|
7
|
+
import type { UsageMonitor } from '../usage/limits/monitor.ts';
|
|
8
|
+
import type { UsageService } from '../usage/usage-service.ts';
|
|
9
|
+
import type { AgentHandler } from './handlers.ts';
|
|
10
|
+
import { wireAgents, type AgentWiring, type AgentWiringOptions } from './wiring.ts';
|
|
11
|
+
|
|
12
|
+
export interface AgentHostOptions<Core extends ChatCore = ChatCore> extends AgentWiringOptions<Core> {
|
|
13
|
+
// Whether the host checks who is signed in and reads what is left of each plan on its own clock.
|
|
14
|
+
background?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function errorReply(id: string | null, code: string, message: string): ReplyError {
|
|
18
|
+
return { id, ok: false, error: { code, message } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isAgentRequest(type: string): type is AgentRequestType {
|
|
22
|
+
return Object.hasOwn(AGENT_REQUEST_SCHEMAS, type);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* The chats of an app that has no daemon: it answers the agent requests over any `FramePort` and
|
|
27
|
+
* sends the agent events back on it, frames checked on arrival. One port is one client. Closing the host writes every thread and ends every CLI; a chat
|
|
28
|
+
* goes on at the next start through its CLI's own session.
|
|
29
|
+
*/
|
|
30
|
+
export class AgentHost<Core extends ChatCore = ChatCore> {
|
|
31
|
+
readonly chats: Core;
|
|
32
|
+
readonly providers: ProviderRegistry;
|
|
33
|
+
readonly accounts: ProviderAccountsService;
|
|
34
|
+
readonly usage: UsageService;
|
|
35
|
+
readonly limits: UsageMonitor;
|
|
36
|
+
private readonly wiring: AgentWiring<Core>;
|
|
37
|
+
private readonly background: boolean;
|
|
38
|
+
private readonly connections = new Set<() => void>();
|
|
39
|
+
private nextClient = 1;
|
|
40
|
+
private closing: Promise<void> | null = null;
|
|
41
|
+
|
|
42
|
+
private constructor(options: AgentHostOptions<Core>) {
|
|
43
|
+
this.wiring = wireAgents(options);
|
|
44
|
+
this.chats = this.wiring.chats;
|
|
45
|
+
this.providers = this.wiring.providers;
|
|
46
|
+
this.accounts = this.wiring.accounts;
|
|
47
|
+
this.usage = this.wiring.usage;
|
|
48
|
+
this.limits = this.wiring.limits;
|
|
49
|
+
this.background = options.background ?? true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* A host with its accounts read; its clocks run from here unless `background` is off. */
|
|
53
|
+
static async open<Core extends ChatCore = ChatCore>(options: AgentHostOptions<Core>): Promise<AgentHost<Core>> {
|
|
54
|
+
const host = new AgentHost(options);
|
|
55
|
+
await host.accounts.load();
|
|
56
|
+
if (host.background) {
|
|
57
|
+
host.wiring.start();
|
|
58
|
+
}
|
|
59
|
+
return host;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Serves one client over `port` until the returned function or `close` lets go of it. */
|
|
63
|
+
connect(port: FramePort): () => void {
|
|
64
|
+
const clientId = `port-${this.nextClient++}`;
|
|
65
|
+
const releases = [
|
|
66
|
+
port.onFrame((frame) => void this.receive(port, clientId, frame)),
|
|
67
|
+
this.wiring.connect(clientId, (event) => port.send({ type: 'event', event: event.event, payload: event.payload }))
|
|
68
|
+
];
|
|
69
|
+
const disconnect = (): void => {
|
|
70
|
+
if (!this.connections.delete(disconnect)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const release of releases) {
|
|
74
|
+
release();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
this.connections.add(disconnect);
|
|
78
|
+
return disconnect;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Lets go of every client, writes every thread and ends every CLI; settles once they exited. */
|
|
82
|
+
close(): Promise<void> {
|
|
83
|
+
this.closing ??= this.shutDown();
|
|
84
|
+
return this.closing;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async shutDown(): Promise<void> {
|
|
88
|
+
for (const disconnect of [...this.connections]) {
|
|
89
|
+
disconnect();
|
|
90
|
+
}
|
|
91
|
+
await this.wiring.stop();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async receive(port: FramePort, clientId: string, frame: unknown): Promise<void> {
|
|
95
|
+
const parsed = parseRequest(frame);
|
|
96
|
+
if (!parsed.ok) {
|
|
97
|
+
// The id when the frame carried a usable one, so the client can settle its promise.
|
|
98
|
+
const id = typeof frame === 'object' && frame !== null && 'id' in frame && typeof frame.id === 'string' && frame.id !== '' ? frame.id : null;
|
|
99
|
+
port.send(errorReply(id, 'bad-request', parsed.message));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const { id, type, payload } = parsed.value;
|
|
103
|
+
if (!isAgentRequest(type)) {
|
|
104
|
+
port.send(errorReply(id, 'unknown-request', `Unknown request type: ${type}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const checked = AGENT_REQUEST_SCHEMAS[type].payload.safeParse(payload);
|
|
108
|
+
if (!checked.success) {
|
|
109
|
+
port.send(errorReply(id, 'bad-request', `Invalid payload for ${type}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const handler = this.wiring.handlers[type] as AgentHandler<AgentRequestType>;
|
|
114
|
+
port.send({ id, ok: true, result: await handler(checked.data as never, clientId) });
|
|
115
|
+
} catch (e) {
|
|
116
|
+
if (e instanceof CodedError) {
|
|
117
|
+
port.send(errorReply(id, e.code, e.message));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.error(`Handler for ${type} failed:`, errorText(e));
|
|
121
|
+
port.send(errorReply(id, 'internal', 'Request failed'));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|