@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,31 @@
|
|
|
1
|
+
// These inherited hooks must remain blocked while older hosts are still in use.
|
|
2
|
+
export const RUIMTE_SESSION_VARIABLES = ['RUIMTE_HOOK_URL', 'RUIMTE_HOOK_TOKEN', 'RUIMTE_CONTEXT_URL', 'RUIMTE_CONTEXT_TOKEN', 'RUIMTE_SESSION_ID'] as const;
|
|
3
|
+
|
|
4
|
+
export interface CliEnvironmentPolicy {
|
|
5
|
+
variables?: readonly string[];
|
|
6
|
+
prefixes?: readonly string[];
|
|
7
|
+
/* A host prefix such as APP strips its hook/context variables and session id, leaving APP_HOME intact. */
|
|
8
|
+
sessionPrefixes?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const COMPATIBILITY_PREFIXES = ['RUIMTE_HOOK_', 'RUIMTE_CONTEXT_'];
|
|
12
|
+
|
|
13
|
+
export function cliEnvironment(env: Record<string, string | undefined> = process.env, policy: CliEnvironmentPolicy = {}): Record<string, string> {
|
|
14
|
+
const variables = new Set<string>([...RUIMTE_SESSION_VARIABLES, ...(policy.variables ?? [])]);
|
|
15
|
+
const prefixes = [...COMPATIBILITY_PREFIXES, ...(policy.prefixes ?? [])];
|
|
16
|
+
for (const host of policy.sessionPrefixes ?? []) {
|
|
17
|
+
const prefix = host.replace(/_+$/, '');
|
|
18
|
+
if (prefix === '') {
|
|
19
|
+
throw new Error('A session environment prefix must not be empty');
|
|
20
|
+
}
|
|
21
|
+
variables.add(`${prefix}_SESSION_ID`);
|
|
22
|
+
prefixes.push(`${prefix}_HOOK_`, `${prefix}_CONTEXT_`);
|
|
23
|
+
}
|
|
24
|
+
const clean: Record<string, string> = {};
|
|
25
|
+
for (const [name, value] of Object.entries(env)) {
|
|
26
|
+
if (value !== undefined && !variables.has(name) && !prefixes.some((prefix) => name.startsWith(prefix))) {
|
|
27
|
+
clean[name] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return clean;
|
|
31
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { AGENT_REQUEST_SCHEMAS, AgentRequestType } from '@adecore/agent-contracts';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { ChatCore } from '../chat/chat-core.ts';
|
|
4
|
+
import { ChatError } from '../chat/errors.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
|
+
|
|
10
|
+
export type AgentRequestMap = {
|
|
11
|
+
[T in AgentRequestType]: {
|
|
12
|
+
payload: z.infer<(typeof AGENT_REQUEST_SCHEMAS)[T]['payload']>;
|
|
13
|
+
result: z.infer<(typeof AGENT_REQUEST_SCHEMAS)[T]['result']>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/* One request as a host answers it, for the client that asked. */
|
|
18
|
+
export type AgentHandler<T extends AgentRequestType> = (
|
|
19
|
+
payload: AgentRequestMap[T]['payload'],
|
|
20
|
+
clientId: string
|
|
21
|
+
) => AgentRequestMap[T]['result'] | Promise<AgentRequestMap[T]['result']>;
|
|
22
|
+
|
|
23
|
+
export type AgentHandlers = { [T in AgentRequestType]: AgentHandler<T> };
|
|
24
|
+
|
|
25
|
+
type AccountRequestType = Extract<AgentRequestType, `accounts.${string}`>;
|
|
26
|
+
type UsageRequestType = Extract<AgentRequestType, `usage.${string}`>;
|
|
27
|
+
export type ChatRequestType = Exclude<AgentRequestType, AccountRequestType | UsageRequestType>;
|
|
28
|
+
|
|
29
|
+
// A fork lands on a canvas and a summary goes back along its line, which a host of chats alone does not have.
|
|
30
|
+
function unsupported(what: string) {
|
|
31
|
+
return (): never => {
|
|
32
|
+
throw new ChatError('chat-unsupported', `This host does not ${what}`);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The chat requests over a core, including the ones only a host with more than chats can answer, which say so. */
|
|
37
|
+
export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T in ChatRequestType]: AgentHandler<T> } {
|
|
38
|
+
return {
|
|
39
|
+
'provider.list': async () => ({ providers: await providers.list() }),
|
|
40
|
+
'chat.create': (payload) => chats.create(payload),
|
|
41
|
+
'chat.configure': (payload) => chats.configure(payload),
|
|
42
|
+
// What a chat the host starts on its own is made with, while this client is connected.
|
|
43
|
+
'chat.setPreferences': (payload, clientId) => {
|
|
44
|
+
chats.composerPreferences.set(clientId, payload);
|
|
45
|
+
return {};
|
|
46
|
+
},
|
|
47
|
+
'chat.attach': (payload, clientId) => chats.attachWithBookmarks(payload.chatId, clientId, payload.historyLimit, payload.since),
|
|
48
|
+
'chat.history': (payload) => chats.history(payload.chatId, payload.cursor, payload.limit),
|
|
49
|
+
'chat.subagent': (payload, clientId) => chats.subagent(clientId, payload),
|
|
50
|
+
'chat.stopSubagent': async (payload) => {
|
|
51
|
+
await chats.stopSubagent(payload.chatId, payload.toolUseId);
|
|
52
|
+
return {};
|
|
53
|
+
},
|
|
54
|
+
'chat.stopTask': async (payload) => {
|
|
55
|
+
await chats.stopTask(payload.chatId, payload.taskId);
|
|
56
|
+
return {};
|
|
57
|
+
},
|
|
58
|
+
'chat.detach': (payload, clientId) => {
|
|
59
|
+
chats.detach(payload.chatId, clientId);
|
|
60
|
+
return {};
|
|
61
|
+
},
|
|
62
|
+
'chat.send': (payload) =>
|
|
63
|
+
chats.send(payload.chatId, payload.text, { mentions: payload.mentions, skills: payload.skills, chats: payload.chats }, payload.attachments),
|
|
64
|
+
'chat.unqueue': (payload) => ({ message: chats.unqueue(payload.chatId, payload.messageId) }),
|
|
65
|
+
'chat.sendNow': (payload) => {
|
|
66
|
+
chats.sendNow(payload.chatId, payload.messageId);
|
|
67
|
+
return {};
|
|
68
|
+
},
|
|
69
|
+
'skills.list': async (payload) => ({ skills: await chats.skills(payload.chatId) }),
|
|
70
|
+
'chat.compact': (payload) => {
|
|
71
|
+
chats.compact(payload.chatId);
|
|
72
|
+
return {};
|
|
73
|
+
},
|
|
74
|
+
'chat.clear': async (payload) => {
|
|
75
|
+
await chats.clear(payload.chatId, payload.force === true);
|
|
76
|
+
return {};
|
|
77
|
+
},
|
|
78
|
+
'chat.turnDiff': async (payload) => ({ diff: await chats.turnDiff(payload.chatId, payload.turnId) }),
|
|
79
|
+
'chat.fork': unsupported('fork chats'),
|
|
80
|
+
'chat.forkInfo': unsupported('fork chats'),
|
|
81
|
+
'chat.summarize': unsupported('fork chats'),
|
|
82
|
+
'chat.continueOn': unsupported('continue a chat under another account'),
|
|
83
|
+
'chat.cancel': (payload) => {
|
|
84
|
+
chats.cancel(payload.chatId, payload.subagents === true);
|
|
85
|
+
return {};
|
|
86
|
+
},
|
|
87
|
+
'chat.approve': (payload) => {
|
|
88
|
+
chats.approve(payload.chatId, payload.requestId, payload.decision, payload.message);
|
|
89
|
+
return {};
|
|
90
|
+
},
|
|
91
|
+
'chat.answer': (payload) => {
|
|
92
|
+
chats.answer(payload.chatId, payload.requestId, payload.answers);
|
|
93
|
+
return {};
|
|
94
|
+
},
|
|
95
|
+
'chat.dismiss': (payload) => {
|
|
96
|
+
chats.dismiss(payload.chatId, payload.itemId);
|
|
97
|
+
return {};
|
|
98
|
+
},
|
|
99
|
+
'chat.addBookmark': async (payload) => ({ bookmarks: await chats.addBookmark(payload.chatId, payload.itemId, payload.name) }),
|
|
100
|
+
'chat.renameBookmark': async (payload) => ({ bookmarks: await chats.renameBookmark(payload.chatId, payload.itemId, payload.name) }),
|
|
101
|
+
'chat.removeBookmark': async (payload) => ({ bookmarks: await chats.removeBookmark(payload.chatId, payload.itemId) }),
|
|
102
|
+
'chat.kill': async (payload) => {
|
|
103
|
+
await chats.kill(payload.chatId);
|
|
104
|
+
return {};
|
|
105
|
+
},
|
|
106
|
+
'chat.list': () => ({ chats: chats.list() })
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* The accounts are the person's settings of this host. */
|
|
111
|
+
export function accountHandlers(accounts: ProviderAccountsService): { [T in AccountRequestType]: AgentHandler<T> } {
|
|
112
|
+
return {
|
|
113
|
+
'accounts.list': () => accounts.snapshot(),
|
|
114
|
+
'accounts.save': (payload) => accounts.save(payload.accounts),
|
|
115
|
+
'accounts.refresh': () => accounts.refresh(),
|
|
116
|
+
'accounts.create': (payload) => accounts.create(payload),
|
|
117
|
+
'accounts.watchLogin': (payload) => {
|
|
118
|
+
void accounts.watchLogin(payload.id);
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function usageHandlers(usage: UsageService, limits: UsageMonitor): { [T in UsageRequestType]: AgentHandler<T> } {
|
|
125
|
+
return {
|
|
126
|
+
'usage.summary': (payload) => usage.summary(payload),
|
|
127
|
+
// While a client has the page open the host keeps scanning; every other client gets the event too.
|
|
128
|
+
'usage.subscribe': (_payload, clientId) => {
|
|
129
|
+
usage.follow(clientId);
|
|
130
|
+
return {};
|
|
131
|
+
},
|
|
132
|
+
'usage.unsubscribe': (_payload, clientId) => {
|
|
133
|
+
usage.unfollow(clientId);
|
|
134
|
+
return {};
|
|
135
|
+
},
|
|
136
|
+
'usage.limits': () => limits.snapshot(),
|
|
137
|
+
'usage.refreshLimits': async () => {
|
|
138
|
+
await limits.refresh(true);
|
|
139
|
+
return limits.snapshot();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FramePort } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Two ends of a port in one process, for a test or a host and a client that live side by side. A
|
|
5
|
+
* frame crosses as a copy and only once the sender's own code ran, the way a MessagePort delivers it,
|
|
6
|
+
* so neither end ever holds the other's object.
|
|
7
|
+
*/
|
|
8
|
+
export function memoryPortPair(): [FramePort, FramePort] {
|
|
9
|
+
const listeners: [Set<(frame: unknown) => void>, Set<(frame: unknown) => void>] = [new Set(), new Set()];
|
|
10
|
+
const end = (own: number): FramePort => ({
|
|
11
|
+
send: (frame) => {
|
|
12
|
+
const copy = structuredClone(frame);
|
|
13
|
+
queueMicrotask(() => {
|
|
14
|
+
for (const listener of listeners[1 - own]!) {
|
|
15
|
+
listener(copy);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
onFrame: (listener) => {
|
|
20
|
+
listeners[own]!.add(listener);
|
|
21
|
+
return () => {
|
|
22
|
+
listeners[own]!.delete(listener);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return [end(0), end(1)];
|
|
27
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { AgentKind, UsageProvider } from '@adecore/agent-contracts';
|
|
2
|
+
import { AttachmentStore } from '../chat/attachment-store.ts';
|
|
3
|
+
import { BookmarkStore } from '../chat/bookmark-store.ts';
|
|
4
|
+
import { ChatCore, type ChatCoreOptions } from '../chat/chat-core.ts';
|
|
5
|
+
import type { SpawnChatProcess } from '../chat/chat-process.ts';
|
|
6
|
+
import { ChatStore } from '../chat/chat-store.ts';
|
|
7
|
+
import { suggestChatTitle } from '../chat/chat-title.ts';
|
|
8
|
+
import type { ClaudeBackendOptions } from '../chat/claude-backend.ts';
|
|
9
|
+
import { ClaudeTitleReader } from '../chat/claude-title.ts';
|
|
10
|
+
import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../chat/codex-transport.ts';
|
|
11
|
+
import type { AgentEvent } from '../events.ts';
|
|
12
|
+
import { definedEnv } from '../providers/accounts/launch.ts';
|
|
13
|
+
import { ProviderAccountsService } from '../providers/accounts/service.ts';
|
|
14
|
+
import type { AccountsHost } from '../providers/accounts/variables.ts';
|
|
15
|
+
import { createClaudeProvider } from '../providers/claude-provider.ts';
|
|
16
|
+
import { errorText } from '../error-text.ts';
|
|
17
|
+
import { createCodexProvider } from '../providers/codex-provider.ts';
|
|
18
|
+
import { codexRulesPathIn, defaultCodexHome, installCodexRules, type CodexRules } from '../providers/codex-rules.ts';
|
|
19
|
+
import { ProviderRegistry, type ProviderRegistryOptions } from '../providers/registry.ts';
|
|
20
|
+
import { chatSessionAccounts, limitAccountsOf, usageAccountsOf, usageRootsOf } from '../usage/accounts.ts';
|
|
21
|
+
import { UsageMonitor } from '../usage/limits/monitor.ts';
|
|
22
|
+
import { UsageService } from '../usage/usage-service.ts';
|
|
23
|
+
import { cliEnvironment, type CliEnvironmentPolicy } from './environment.ts';
|
|
24
|
+
import { accountHandlers, chatHandlers, usageHandlers, type AgentHandlers } from './handlers.ts';
|
|
25
|
+
|
|
26
|
+
export interface AgentWiringOptions<Core extends ChatCore = ChatCore> {
|
|
27
|
+
// Where the chats, their attachments and bookmarks, the accounts and the usage index are kept.
|
|
28
|
+
dataDir: string;
|
|
29
|
+
// The environment the CLIs start in; absent, this process's own through `cliEnvironment`.
|
|
30
|
+
env?: Record<string, string | undefined>;
|
|
31
|
+
environmentPolicy?: CliEnvironmentPolicy;
|
|
32
|
+
// What every agent is told once, at the start of its process.
|
|
33
|
+
systemNote?: string;
|
|
34
|
+
// How the host names itself: to Codex, in a refusal about an account, and in the keychain.
|
|
35
|
+
client?: CodexClientInfo;
|
|
36
|
+
accountsHost?: AccountsHost;
|
|
37
|
+
claude?: ClaudeBackendOptions;
|
|
38
|
+
// The commands Codex runs outside its sandbox without asking, such as the host's context CLI; written in its home and every account's.
|
|
39
|
+
codexRules?: CodexRules;
|
|
40
|
+
// The core of an app that extends `ChatCore`, made from what a plain core would get; absent, a plain core.
|
|
41
|
+
core?: (options: ChatCoreOptions) => Core;
|
|
42
|
+
// A test runs fake CLIs: the commands to start and how.
|
|
43
|
+
command?: string[];
|
|
44
|
+
codexCommand?: string[];
|
|
45
|
+
spawn?: SpawnChatProcess;
|
|
46
|
+
detect?: ProviderRegistryOptions['detect'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AgentWiring<Core extends ChatCore = ChatCore> {
|
|
50
|
+
providers: ProviderRegistry;
|
|
51
|
+
accounts: ProviderAccountsService;
|
|
52
|
+
limits: UsageMonitor;
|
|
53
|
+
usage: UsageService;
|
|
54
|
+
chats: Core;
|
|
55
|
+
handlers: AgentHandlers;
|
|
56
|
+
/* Sends one client its chat, account and usage events until the returned function lets go of it. */
|
|
57
|
+
connect(clientId: string, send: (event: AgentEvent) => void): () => void;
|
|
58
|
+
/* Checks who is signed in and reads what is left of each plan on their own clocks, until `stop`. */
|
|
59
|
+
start(): void;
|
|
60
|
+
/* Stops the clocks, writes every thread and ends every CLI; settles once they exited. */
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A folder the rule cannot be written to only costs an approval per call, so it never stops the host.
|
|
65
|
+
async function installRulesIn(kind: AgentKind, folder: string, rules: CodexRules): Promise<void> {
|
|
66
|
+
if (kind !== 'codex') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const path = codexRulesPathIn(folder, rules);
|
|
70
|
+
await installCodexRules(path, rules).catch((e: unknown) => console.error(`Could not write the codex rules in ${path}:`, errorText(e)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* The chats of a host with the providers, accounts and usage around them, and the handlers of
|
|
75
|
+
* every agent request. `AgentHost` serves them over a port; an app that answers requests on a wire
|
|
76
|
+
* of its own takes the handlers and `connect` from here. The accounts are read with
|
|
77
|
+
* `accounts.load()` before the first request.
|
|
78
|
+
*/
|
|
79
|
+
export function wireAgents<Core extends ChatCore = ChatCore>(options: AgentWiringOptions<Core>): AgentWiring<Core> {
|
|
80
|
+
const env = options.env === undefined || options.environmentPolicy !== undefined ? cliEnvironment(options.env, options.environmentPolicy) : options.env;
|
|
81
|
+
const client = options.client ?? DEFAULT_CODEX_CLIENT;
|
|
82
|
+
const codexRules = options.codexRules;
|
|
83
|
+
const providers = new ProviderRegistry({ providers: [createClaudeProvider(options.claude), createCodexProvider({ client })], env, detect: options.detect });
|
|
84
|
+
const accounts = new ProviderAccountsService({
|
|
85
|
+
home: options.dataDir,
|
|
86
|
+
providers,
|
|
87
|
+
env,
|
|
88
|
+
client,
|
|
89
|
+
...(options.accountsHost ? { host: options.accountsHost } : {}),
|
|
90
|
+
...(codexRules ? { install: (kind: AgentKind, folder: string) => installRulesIn(kind, folder, codexRules) } : {})
|
|
91
|
+
});
|
|
92
|
+
if (codexRules) {
|
|
93
|
+
void installRulesIn('codex', defaultCodexHome(env), codexRules);
|
|
94
|
+
}
|
|
95
|
+
const nameOf = (kind: AgentKind | UsageProvider): string => providers.get(kind).name;
|
|
96
|
+
const limits = new UsageMonitor({ providers, accounts: limitAccountsOf(accounts, nameOf, env), client });
|
|
97
|
+
accounts.listen(() => limits.accountsChanged());
|
|
98
|
+
const attachments = new AttachmentStore(options.dataDir);
|
|
99
|
+
// Only without `core`, where `Core` is `ChatCore` itself.
|
|
100
|
+
const core = options.core ?? ((coreOptions: ChatCoreOptions) => new ChatCore(coreOptions) as Core);
|
|
101
|
+
const chats = core({
|
|
102
|
+
providers,
|
|
103
|
+
store: new ChatStore(options.dataDir, { attachments }),
|
|
104
|
+
attachments,
|
|
105
|
+
bookmarks: new BookmarkStore(options.dataDir),
|
|
106
|
+
env,
|
|
107
|
+
...(options.systemNote === undefined ? {} : { instructions: options.systemNote }),
|
|
108
|
+
...(options.command ? { command: options.command } : {}),
|
|
109
|
+
...(options.codexCommand ? { codexCommand: options.codexCommand } : {}),
|
|
110
|
+
...(options.spawn ? { spawn: options.spawn } : {}),
|
|
111
|
+
codexClient: client,
|
|
112
|
+
accounts,
|
|
113
|
+
onLimits: (update) => limits.applyLive(update),
|
|
114
|
+
claudeTitles: new ClaudeTitleReader(),
|
|
115
|
+
nameChat: (kind, input) => suggestChatTitle(providers, kind, input, definedEnv(env))
|
|
116
|
+
});
|
|
117
|
+
const usage = new UsageService({
|
|
118
|
+
home: options.dataDir,
|
|
119
|
+
knownProjects: () => Promise.resolve([]),
|
|
120
|
+
roots: () => usageRootsOf(accounts),
|
|
121
|
+
sessionAccounts: () => chatSessionAccounts(chats.list()),
|
|
122
|
+
accounts: () => usageAccountsOf(accounts, nameOf)
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
providers,
|
|
126
|
+
accounts,
|
|
127
|
+
limits,
|
|
128
|
+
usage,
|
|
129
|
+
chats,
|
|
130
|
+
handlers: {
|
|
131
|
+
...chatHandlers(chats, providers),
|
|
132
|
+
...accountHandlers(accounts),
|
|
133
|
+
...usageHandlers(usage, limits)
|
|
134
|
+
},
|
|
135
|
+
connect: (clientId, send) => {
|
|
136
|
+
const releases = [
|
|
137
|
+
chats.subscribe(clientId, send),
|
|
138
|
+
accounts.subscribe(clientId, send),
|
|
139
|
+
usage.subscribe(clientId, send),
|
|
140
|
+
limits.subscribe(clientId, send)
|
|
141
|
+
];
|
|
142
|
+
return () => {
|
|
143
|
+
for (const release of releases) {
|
|
144
|
+
release();
|
|
145
|
+
}
|
|
146
|
+
chats.detachAll(clientId);
|
|
147
|
+
usage.unfollow(clientId);
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
start: () => {
|
|
151
|
+
limits.start();
|
|
152
|
+
accounts.start();
|
|
153
|
+
},
|
|
154
|
+
stop: async () => {
|
|
155
|
+
try {
|
|
156
|
+
usage.stop();
|
|
157
|
+
limits.stop();
|
|
158
|
+
accounts.stop();
|
|
159
|
+
} finally {
|
|
160
|
+
await chats.shutdown();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/lineage.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { RuntimeModeSchema, type RuntimeMode } from '@adecore/agent-contracts';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { RecordDirectory } from './record-directory.ts';
|
|
5
|
+
|
|
6
|
+
const LineageSchema = z.object({
|
|
7
|
+
projectId: z.string().min(1),
|
|
8
|
+
nodeId: z.string().min(1),
|
|
9
|
+
/* The node that ran the verb; a caller is counted against this to cap one runaway agent. */
|
|
10
|
+
openedBy: z.string().min(1),
|
|
11
|
+
depth: z.number().int().nonnegative(),
|
|
12
|
+
/* Whether this is an agent node, which is what the depth and the per-caller cap are about. A
|
|
13
|
+
record written before a plain node was written down at all is one of those, hence the default. */
|
|
14
|
+
agent: z.boolean().default(true),
|
|
15
|
+
/* A fork is a sibling a person made beside `openedBy` rather than a node it opened; absent is opened by a verb. */
|
|
16
|
+
relation: z.literal('fork').optional(),
|
|
17
|
+
/* The widest mode the agent may start in, the mode of its opener when it was made. Absent on a
|
|
18
|
+
record from before, and on a node nobody may widen anyway since no agent started it. */
|
|
19
|
+
ceiling: RuntimeModeSchema.optional(),
|
|
20
|
+
createdAt: z.number(),
|
|
21
|
+
/* When stopping or deleting the node that opened it ended this one too; a cascade never ends a node twice. */
|
|
22
|
+
endedAt: z.number().optional()
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type Lineage = z.infer<typeof LineageSchema>;
|
|
26
|
+
|
|
27
|
+
/* Opened by the agent it names, which is what every rule about children is about; a fork lives on its own. */
|
|
28
|
+
function openedByAgent(entry: Lineage): boolean {
|
|
29
|
+
return entry.agent && entry.relation !== 'fork';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Lineage lives in `lineage` under the host's data folder, outside the agent-writable project, so agents
|
|
34
|
+
* cannot reset their own depth. Persisting it also prevents a restart of the host from resetting recursion limits.
|
|
35
|
+
*/
|
|
36
|
+
export class AgentLineageStore {
|
|
37
|
+
readonly dir: string;
|
|
38
|
+
private readonly opened: RecordDirectory<Lineage>;
|
|
39
|
+
|
|
40
|
+
constructor(dataDir: string) {
|
|
41
|
+
this.dir = join(dataDir, 'lineage');
|
|
42
|
+
this.opened = new RecordDirectory({ dir: this.dir, schema: LineageSchema, idOf: (entry) => entry.nodeId });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Reads what an earlier run of the host wrote down. Call before any verb can ask. */
|
|
46
|
+
load(): Promise<void> {
|
|
47
|
+
return this.opened.load();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async put(record: Omit<Lineage, 'createdAt' | 'endedAt'>): Promise<void> {
|
|
51
|
+
await this.opened.write({ ...record, createdAt: Date.now() });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* The agent nodes this one opened and the ones those opened in turn that no cascade ended yet,
|
|
56
|
+
* nearest first, so whoever ends them can go from the leaves back up.
|
|
57
|
+
*/
|
|
58
|
+
descendants(nodeId: string): string[] {
|
|
59
|
+
const found: string[] = [];
|
|
60
|
+
const seen = new Set([nodeId]);
|
|
61
|
+
for (let i = -1; i < found.length; i++) {
|
|
62
|
+
const parent = i === -1 ? nodeId : found[i]!;
|
|
63
|
+
for (const entry of this.opened.all()) {
|
|
64
|
+
if (entry.openedBy === parent && openedByAgent(entry) && entry.endedAt === undefined && !seen.has(entry.nodeId)) {
|
|
65
|
+
seen.add(entry.nodeId);
|
|
66
|
+
found.push(entry.nodeId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* The project a node was made in, for work owed about it after the node that opened it is gone. */
|
|
74
|
+
projectOf(nodeId: string): string | null {
|
|
75
|
+
return this.opened.get(nodeId)?.projectId ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
endedAt(nodeId: string): number | null {
|
|
79
|
+
return this.opened.get(nodeId)?.endedAt ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* On the wall clock, like `createdAt`: a chat holds the mark against the time its turn started. */
|
|
83
|
+
async markEnded(nodeIds: readonly string[]): Promise<void> {
|
|
84
|
+
const at = Date.now();
|
|
85
|
+
for (const nodeId of nodeIds) {
|
|
86
|
+
const entry = this.opened.get(nodeId);
|
|
87
|
+
if (entry && entry.endedAt === undefined) {
|
|
88
|
+
await this.opened.write({ ...entry, endedAt: at });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* The nodes of this project whose opener it no longer places, and that no cascade ended yet. */
|
|
94
|
+
orphans(projectId: string, ids: ReadonlySet<string>): Array<{ nodeId: string; openedBy: string }> {
|
|
95
|
+
return [...this.opened.all()]
|
|
96
|
+
.filter(
|
|
97
|
+
(entry) =>
|
|
98
|
+
entry.projectId === projectId && openedByAgent(entry) && entry.endedAt === undefined && ids.has(entry.nodeId) && !ids.has(entry.openedBy)
|
|
99
|
+
)
|
|
100
|
+
.map((entry) => ({ nodeId: entry.nodeId, openedBy: entry.openedBy }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* The widest mode this agent node may start in, every time it starts; null for one no agent opened. */
|
|
104
|
+
ceilingOf(nodeId: string): RuntimeMode | null {
|
|
105
|
+
const entry = this.opened.get(nodeId);
|
|
106
|
+
return entry !== undefined && openedByAgent(entry) ? (entry.ceiling ?? null) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* How deep a node sits. A node nobody wrote down is one a person made, which is where a chain starts. */
|
|
110
|
+
depthOf(nodeId: string): number {
|
|
111
|
+
return this.opened.get(nodeId)?.depth ?? 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A host may exclude completed hidden agents while keeping their lineage for later reads and tasks.
|
|
115
|
+
openedCount(callerId: string, include: (nodeId: string) => boolean = () => true): number {
|
|
116
|
+
let count = 0;
|
|
117
|
+
for (const entry of this.opened.all()) {
|
|
118
|
+
if (entry.openedBy === callerId && openedByAgent(entry) && include(entry.nodeId)) {
|
|
119
|
+
count += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Who made this node, or null for one a person made: the whole of the rule node delete follows. */
|
|
126
|
+
madeBy(nodeId: string): string | null {
|
|
127
|
+
const entry = this.opened.get(nodeId);
|
|
128
|
+
return entry === undefined || entry.relation === 'fork' ? null : entry.openedBy;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* The node that opened this one as an agent, or null for a node no agent or team started. */
|
|
132
|
+
startedBy(nodeId: string): string | null {
|
|
133
|
+
const entry = this.opened.get(nodeId);
|
|
134
|
+
return entry !== undefined && openedByAgent(entry) ? entry.openedBy : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* The chat a fork was made from, or null for a node that is no fork. */
|
|
138
|
+
forkedFrom(nodeId: string): string | null {
|
|
139
|
+
const entry = this.opened.get(nodeId);
|
|
140
|
+
return entry?.relation === 'fork' ? entry.openedBy : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* The forks made from a chat. */
|
|
144
|
+
forksOf(nodeId: string): string[] {
|
|
145
|
+
return [...this.opened.all()].filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId).map((entry) => entry.nodeId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* The forks of this project whose node is gone, read before `prune` forgets them. */
|
|
149
|
+
forksLeaving(projectId: string, ids: ReadonlySet<string>): string[] {
|
|
150
|
+
return [...this.opened.all()]
|
|
151
|
+
.filter((entry) => entry.projectId === projectId && entry.relation === 'fork' && !ids.has(entry.nodeId))
|
|
152
|
+
.map((entry) => entry.nodeId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Drops what this project wrote down for ids it no longer has: the node was deleted. */
|
|
156
|
+
prune(projectId: string, ids: ReadonlySet<string>): Promise<void> {
|
|
157
|
+
return this.opened.prune((entry) => entry.projectId === projectId && !ids.has(entry.nodeId));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { WakeChat } from '../chat/wake-chat.ts';
|
|
3
|
+
import type { OutboxEntryOf } from '../outbox/outbox.ts';
|
|
4
|
+
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
5
|
+
import type { MessageWords } from './deliver-notice.ts';
|
|
6
|
+
import type { NoticeStore } from './notice-store.ts';
|
|
7
|
+
|
|
8
|
+
export const DeliverMessageWorkSchema = z.object({
|
|
9
|
+
kind: z.literal('deliver-message'),
|
|
10
|
+
// The target is the chat the message was left for; the message itself waits in the notice store, with any that came in beside it.
|
|
11
|
+
payload: z.object({ from: z.string().min(1) })
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type DeliverMessageWork = z.infer<typeof DeliverMessageWorkSchema>;
|
|
15
|
+
export type DeliverMessageEntry = OutboxEntryOf<DeliverMessageWork>;
|
|
16
|
+
|
|
17
|
+
export interface DeliverMessageDeps {
|
|
18
|
+
notices: Pick<NoticeStore, 'waiting'>;
|
|
19
|
+
/* The chat the message was left for, loaded from disk when nobody has it; null for a node with no thread. */
|
|
20
|
+
chat(chatId: string): Promise<WakeChat | null>;
|
|
21
|
+
/* Whether a project still places the node; one that is gone reads nothing any more. */
|
|
22
|
+
placed(nodeId: string): boolean;
|
|
23
|
+
words: Pick<MessageWords, 'prompt' | 'label'>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Opens the turn a message earns a chat. The messages themselves are not carried here: they wait in
|
|
28
|
+
* the notice store and the turn's preamble takes them (`NoticeNotes`), the same channel a person's
|
|
29
|
+
* own prompt reads them through, so one message is heard once whichever of the two opens the turn.
|
|
30
|
+
*
|
|
31
|
+
* One attempt, never `wait`. A chat that turned out to be in a turn reads the message in front of its
|
|
32
|
+
* next one, which is what a message to a busy chat has always done; holding the entry instead would
|
|
33
|
+
* make the host start turns nobody asked for long after the news was worth anything.
|
|
34
|
+
*/
|
|
35
|
+
export function deliverMessageHandler(deps: DeliverMessageDeps) {
|
|
36
|
+
return async (entry: DeliverMessageEntry): Promise<OutboxOutcome> => {
|
|
37
|
+
if (!deps.placed(entry.target)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const waiting = deps.notices.waiting(entry.target);
|
|
41
|
+
// A turn opened for an earlier message took these along, or they went stale: nothing to wake anyone for.
|
|
42
|
+
if (waiting.length === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const chat = await deps.chat(entry.target);
|
|
46
|
+
if (chat === null) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
/*
|
|
50
|
+
* No note of its own, and the preamble stays off the thread. A person read the message as it
|
|
51
|
+
* landed (`showNotices`), so anything the turn adds about it only says the same thing again;
|
|
52
|
+
* the label on the turn already names who it came from.
|
|
53
|
+
*/
|
|
54
|
+
chat.wake({
|
|
55
|
+
text: deps.words.prompt(waiting.length),
|
|
56
|
+
label: deps.words.label(waiting),
|
|
57
|
+
taskIds: [],
|
|
58
|
+
messageFrom: [...new Set(waiting.map((notice) => notice.from))]
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
}
|