@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,162 @@
|
|
|
1
|
+
import type { ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import type { SpawnChatProcess } from './chat-process.ts';
|
|
3
|
+
import { CodexProtocol } from './codex-protocol.ts';
|
|
4
|
+
import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo } from './codex-transport.ts';
|
|
5
|
+
import { ChatError } from './errors.ts';
|
|
6
|
+
import { readingThread, settledReading } from './subagent-projection.ts';
|
|
7
|
+
import { errorText } from '../error-text.ts';
|
|
8
|
+
|
|
9
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ThreadItemsParams {
|
|
14
|
+
threadId: string;
|
|
15
|
+
cursor?: string;
|
|
16
|
+
limit: number;
|
|
17
|
+
sortDirection: 'asc' | 'desc';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* One `thread/items/list` answer as far as a reader needs it: the entries newest first, and the cursor to older ones. */
|
|
21
|
+
export function parseThreadItemsPage(result: unknown): { entries: Record<string, unknown>[]; nextCursor: string | null } {
|
|
22
|
+
const data = isRecord(result) && Array.isArray(result.data) ? result.data.filter(isRecord) : [];
|
|
23
|
+
const nextCursor = isRecord(result) && typeof result.nextCursor === 'string' && result.nextCursor !== '' ? result.nextCursor : null;
|
|
24
|
+
return { entries: data, nextCursor };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function userText(content: unknown): string {
|
|
28
|
+
return Array.isArray(content)
|
|
29
|
+
? content
|
|
30
|
+
.filter(isRecord)
|
|
31
|
+
.map((block) => (typeof block.text === 'string' ? block.text : ''))
|
|
32
|
+
.filter((text) => text !== '')
|
|
33
|
+
.join('\n')
|
|
34
|
+
: '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* Items of a Codex thread, oldest first, through the mapping the live stream uses. Every entry is
|
|
39
|
+
* projected on its own: consecutive reasoning would otherwise become one thinking item whose id
|
|
40
|
+
* depends on where a page happened to start, and a client merging two reads by id would show that
|
|
41
|
+
* thought twice.
|
|
42
|
+
*/
|
|
43
|
+
export function projectCodexItems(entriesOldestFirst: Record<string, unknown>[], now: number): ChatItem[] {
|
|
44
|
+
const { thread, projector } = readingThread('Codex', () => now);
|
|
45
|
+
const protocol = new CodexProtocol(0);
|
|
46
|
+
for (const entry of entriesOldestFirst) {
|
|
47
|
+
const item = isRecord(entry.item) ? entry.item : null;
|
|
48
|
+
if (!item || typeof item.id !== 'string') {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
projector.reset();
|
|
52
|
+
if (item.type === 'userMessage') {
|
|
53
|
+
const text = userText(item.content);
|
|
54
|
+
if (text !== '') {
|
|
55
|
+
thread.upsert({ id: `user-${item.id}`, kind: 'user', createdAt: now, turnId: null, text });
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
for (const event of protocol.handle({ method: 'item/completed', params: { item } })) {
|
|
60
|
+
projector.project(0, event);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return thread.list().map(settledReading);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CodexProcessSpec {
|
|
67
|
+
command: string[];
|
|
68
|
+
cwd: string;
|
|
69
|
+
env: Record<string, string>;
|
|
70
|
+
spawn?: SpawnChatProcess;
|
|
71
|
+
client?: CodexClientInfo;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* An app-server started for the questions `work` asks and ended after them, for a chat whose own process may not run. */
|
|
75
|
+
async function withAppServer<T>(spec: CodexProcessSpec, work: (transport: CodexTransport) => Promise<T>): Promise<T> {
|
|
76
|
+
const transport = new CodexTransport({
|
|
77
|
+
command: spec.command,
|
|
78
|
+
cwd: spec.cwd,
|
|
79
|
+
env: spec.env,
|
|
80
|
+
...(spec.spawn ? { spawn: spec.spawn } : {}),
|
|
81
|
+
onFrame: () => undefined,
|
|
82
|
+
onExit: () => undefined
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
await transport.request('initialize', {
|
|
86
|
+
clientInfo: spec.client ?? DEFAULT_CODEX_CLIENT,
|
|
87
|
+
capabilities: { experimentalApi: true, requestAttestation: false }
|
|
88
|
+
});
|
|
89
|
+
transport.notify('initialized', {});
|
|
90
|
+
return await work(transport);
|
|
91
|
+
} finally {
|
|
92
|
+
transport.end();
|
|
93
|
+
transport.kill('SIGTERM');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* A page asked of an app-server started for this one question, for a chat whose own process is not
|
|
99
|
+
* running. The app-server reads a thread it never loaded from disk (checked against codex-cli 0.154.0),
|
|
100
|
+
* so nothing has to be resumed first.
|
|
101
|
+
*/
|
|
102
|
+
export function listThreadItemsOnce(spec: CodexProcessSpec, params: ThreadItemsParams): Promise<unknown> {
|
|
103
|
+
return withAppServer(spec, (transport) => transport.request('thread/items/list', params));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ThreadForkParams {
|
|
107
|
+
threadId: string;
|
|
108
|
+
/* The turn the fork goes on after; null for all of it. */
|
|
109
|
+
at: { turnId: string } | { turns: number } | null;
|
|
110
|
+
/* The thread options the fork runs with: `cwd`, `model` and what the runtime mode says. */
|
|
111
|
+
options: Record<string, unknown>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* The id of the `turns`-th turn of a thread, oldest first, or null when it has fewer. */
|
|
115
|
+
async function nthTurnId(transport: CodexTransport, threadId: string, turns: number): Promise<string | null> {
|
|
116
|
+
let seen = 0;
|
|
117
|
+
let cursor: string | null = null;
|
|
118
|
+
do {
|
|
119
|
+
const page = await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) });
|
|
120
|
+
const data = isRecord(page) && Array.isArray(page.data) ? page.data.filter(isRecord) : [];
|
|
121
|
+
for (const turn of data) {
|
|
122
|
+
if (++seen === turns) {
|
|
123
|
+
return typeof turn.id === 'string' ? turn.id : null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
cursor = isRecord(page) && typeof page.nextCursor === 'string' && page.nextCursor !== '' ? page.nextCursor : null;
|
|
127
|
+
} while (cursor !== null);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* A new thread with the turns of `threadId` up to the one named, through `thread/fork`, and its id.
|
|
133
|
+
* Asked of a process of its own even while the chat's backend holds the thread: measured against
|
|
134
|
+
* codex-cli 0.154.0, with the original answering on in its own process afterwards.
|
|
135
|
+
*/
|
|
136
|
+
export function forkThreadOnce(spec: CodexProcessSpec, params: ThreadForkParams): Promise<string> {
|
|
137
|
+
return withAppServer(spec, async (transport) => {
|
|
138
|
+
let lastTurnId: string | null = null;
|
|
139
|
+
if (params.at !== null) {
|
|
140
|
+
lastTurnId = 'turnId' in params.at ? params.at.turnId : await nthTurnId(transport, params.threadId, params.at.turns);
|
|
141
|
+
if (lastTurnId === null) {
|
|
142
|
+
throw new ChatError('turn-not-found', 'Codex keeps fewer turns in this thread than the chat shows, so the turn could not be found');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let result: unknown;
|
|
146
|
+
try {
|
|
147
|
+
result = await transport.request('thread/fork', {
|
|
148
|
+
threadId: params.threadId,
|
|
149
|
+
excludeTurns: true,
|
|
150
|
+
...params.options,
|
|
151
|
+
...(lastTurnId === null ? {} : { lastTurnId })
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new ChatError('fork-failed', errorText(error));
|
|
155
|
+
}
|
|
156
|
+
const thread = isRecord(result) && isRecord(result.thread) ? result.thread : null;
|
|
157
|
+
if (thread === null || typeof thread.id !== 'string' || thread.id === '') {
|
|
158
|
+
throw new ChatError('fork-failed', 'Codex answered the fork without a thread');
|
|
159
|
+
}
|
|
160
|
+
return thread.id;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { errorText } from '../error-text.ts';
|
|
2
|
+
import { ChatChild, type SpawnChatProcess } from './chat-process.ts';
|
|
3
|
+
|
|
4
|
+
export type CodexFrame = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/* How the host names itself in the `initialize` every app-server is opened with. */
|
|
7
|
+
export interface CodexClientInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
title: string;
|
|
10
|
+
version: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_CODEX_CLIENT: CodexClientInfo = { name: 'agents', title: 'Agents', version: '0.1.0' };
|
|
14
|
+
|
|
15
|
+
interface CodexTransportOptions {
|
|
16
|
+
command: string[];
|
|
17
|
+
cwd: string;
|
|
18
|
+
env: Record<string, string>;
|
|
19
|
+
spawn?: SpawnChatProcess;
|
|
20
|
+
// Notifications and server requests; responses to our own requests settle their promise instead.
|
|
21
|
+
onFrame(frame: CodexFrame): void;
|
|
22
|
+
onError?(error: Error): void;
|
|
23
|
+
// `stderr` is the tail of what the app-server wrote there, for an exit with an error code only.
|
|
24
|
+
onExit(exitCode: number | null, stderr: string | null): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class CodexRpcError extends Error {
|
|
28
|
+
readonly code: number | null;
|
|
29
|
+
|
|
30
|
+
constructor(method: string, error: unknown) {
|
|
31
|
+
const detail = typeof error === 'object' && error !== null ? (error as { message?: unknown; code?: unknown }) : {};
|
|
32
|
+
super(`${method} failed: ${typeof detail.message === 'string' ? detail.message : JSON.stringify(error)}`);
|
|
33
|
+
this.name = 'CodexRpcError';
|
|
34
|
+
this.code = typeof detail.code === 'number' ? detail.code : null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Settle = { resolve(value: unknown): void; reject(reason: Error): void; method: string };
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* One `codex app-server` process and the JSON-RPC framing on its stdio: newline-delimited JSON,
|
|
42
|
+
* a request carries an id and gets a result or an error back under the same id, a notification
|
|
43
|
+
* has no id. The app-server also sends requests of its own (approvals), answered with `respond`.
|
|
44
|
+
*/
|
|
45
|
+
export class CodexTransport {
|
|
46
|
+
private readonly child: ChatChild;
|
|
47
|
+
private readonly options: CodexTransportOptions;
|
|
48
|
+
private readonly pending = new Map<number, Settle>();
|
|
49
|
+
private nextId = 1;
|
|
50
|
+
private closed = false;
|
|
51
|
+
|
|
52
|
+
constructor(options: CodexTransportOptions) {
|
|
53
|
+
this.options = options;
|
|
54
|
+
this.child = new ChatChild({
|
|
55
|
+
command: options.command,
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
env: options.env,
|
|
58
|
+
...(options.spawn ? { spawn: options.spawn } : {}),
|
|
59
|
+
onError: (error) => this.failed(error),
|
|
60
|
+
onExit: (exitCode, stderr) => this.handleExit(exitCode, stderr)
|
|
61
|
+
});
|
|
62
|
+
void this.readLines();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get pid(): number {
|
|
66
|
+
return this.child.pid;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get alive(): boolean {
|
|
70
|
+
return !this.closed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
request(method: string, params: unknown): Promise<unknown> {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (this.closed) {
|
|
76
|
+
reject(new Error(`${method} failed: Codex is not running`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const id = this.nextId++;
|
|
80
|
+
this.pending.set(id, { resolve, reject, method });
|
|
81
|
+
try {
|
|
82
|
+
this.write({ id, method, params });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.pending.delete(id);
|
|
85
|
+
reject(error);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
notify(method: string, params: unknown): void {
|
|
91
|
+
this.write({ method, params });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
respond(id: number | string, result: unknown): void {
|
|
95
|
+
this.write({ id, result });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
respondError(id: number | string, code: number, message: string): void {
|
|
99
|
+
this.write({ id, error: { code, message } });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Closes stdin; the app-server ends on its own once it has nothing more to say. */
|
|
103
|
+
end(): void {
|
|
104
|
+
try {
|
|
105
|
+
this.child.process.stdin.end();
|
|
106
|
+
} catch {
|
|
107
|
+
// Already closed by the other side.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
kill(signal: 'SIGTERM' | 'SIGKILL' = 'SIGTERM'): void {
|
|
112
|
+
this.child.process.kill(signal);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stop(): void {
|
|
116
|
+
this.end();
|
|
117
|
+
this.child.endAfterGrace();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
dispose(): Promise<void> {
|
|
121
|
+
return this.child.end();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private write(frame: CodexFrame): void {
|
|
125
|
+
if (this.closed) {
|
|
126
|
+
throw new Error('Codex stdin is closed');
|
|
127
|
+
}
|
|
128
|
+
this.child.process.stdin.write(`${JSON.stringify(frame)}\n`);
|
|
129
|
+
this.child.process.stdin.flush();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private failed(error: unknown): void {
|
|
133
|
+
const failure = new Error(`Codex transport failed: ${errorText(error)}`);
|
|
134
|
+
for (const settle of this.pending.values()) {
|
|
135
|
+
settle.reject(failure);
|
|
136
|
+
}
|
|
137
|
+
this.pending.clear();
|
|
138
|
+
try {
|
|
139
|
+
this.options.onError?.(failure);
|
|
140
|
+
} catch (reportError) {
|
|
141
|
+
console.error('Reporting a Codex transport failure failed:', errorText(reportError));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private readLine(line: string): void {
|
|
146
|
+
try {
|
|
147
|
+
this.handleLine(line);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
this.failed(error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async readLines(): Promise<void> {
|
|
154
|
+
const reader = this.child.process.stdout.getReader();
|
|
155
|
+
const decoder = new TextDecoder();
|
|
156
|
+
let buffered = '';
|
|
157
|
+
try {
|
|
158
|
+
while (true) {
|
|
159
|
+
const { value, done } = await reader.read();
|
|
160
|
+
if (done) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
buffered += decoder.decode(value, { stream: true });
|
|
164
|
+
let newline = buffered.indexOf('\n');
|
|
165
|
+
while (newline >= 0) {
|
|
166
|
+
this.readLine(buffered.slice(0, newline));
|
|
167
|
+
buffered = buffered.slice(newline + 1);
|
|
168
|
+
newline = buffered.indexOf('\n');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (buffered.trim() !== '') {
|
|
172
|
+
this.readLine(buffered);
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (!this.closed) {
|
|
176
|
+
this.failed(error);
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
reader.releaseLock();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private handleLine(line: string): void {
|
|
184
|
+
if (this.closed || line.trim() === '') {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let frame: unknown;
|
|
188
|
+
try {
|
|
189
|
+
frame = JSON.parse(line);
|
|
190
|
+
} catch {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (typeof frame !== 'object' || frame === null || Array.isArray(frame)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const message = frame as CodexFrame;
|
|
197
|
+
if (typeof message.method === 'string') {
|
|
198
|
+
this.options.onFrame(message);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const settle = typeof message.id === 'number' ? this.pending.get(message.id) : undefined;
|
|
202
|
+
if (!settle) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.pending.delete(message.id as number);
|
|
206
|
+
if (message.error !== undefined) {
|
|
207
|
+
settle.reject(new CodexRpcError(settle.method, message.error));
|
|
208
|
+
} else {
|
|
209
|
+
settle.resolve(message.result);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private handleExit(exitCode: number | null, stderr: string | null): void {
|
|
214
|
+
if (this.closed) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.closed = true;
|
|
218
|
+
for (const settle of this.pending.values()) {
|
|
219
|
+
settle.reject(new Error(`${settle.method} failed: Codex exited`));
|
|
220
|
+
}
|
|
221
|
+
this.pending.clear();
|
|
222
|
+
this.options.onExit(exitCode, stderr);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AgentKind, ChatPreferencesPayload, ModelSelection, RuntimeMode } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
export interface ComposerPreference {
|
|
4
|
+
runtimeMode?: RuntimeMode;
|
|
5
|
+
selection?: ModelSelection;
|
|
6
|
+
// The account a new chat or agent of this CLI starts under when nothing names one.
|
|
7
|
+
account?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Held {
|
|
11
|
+
preference: ChatPreferencesPayload;
|
|
12
|
+
// The order it was told in, which settles two picks with the same `changedAt`.
|
|
13
|
+
told: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* What each connected client says a new chat starts with, for the chats the host starts with no
|
|
18
|
+
* client mounting them. Held per socket like the approval switch: a machine never remembers a
|
|
19
|
+
* person's pick after that person's client is gone, so with nobody connected a chat gets the
|
|
20
|
+
* host's own defaults.
|
|
21
|
+
*/
|
|
22
|
+
export class ComposerPreferences {
|
|
23
|
+
private readonly byClient = new Map<string, Held>();
|
|
24
|
+
private told = 0;
|
|
25
|
+
|
|
26
|
+
set(clientId: string, preference: ChatPreferencesPayload): void {
|
|
27
|
+
this.told += 1;
|
|
28
|
+
this.byClient.set(clientId, { preference, told: this.told });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
forget(clientId: string): void {
|
|
32
|
+
this.byClient.delete(clientId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* The mode the newest pick gives a terminal agent; undefined with no client or none said. */
|
|
36
|
+
terminalMode(): RuntimeMode | undefined {
|
|
37
|
+
return this.newest()?.preference.terminalRuntimeMode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* The newest pick among the connected clients, narrowed to this provider; empty with none. */
|
|
41
|
+
for(provider: AgentKind): ComposerPreference {
|
|
42
|
+
const newest = this.newest();
|
|
43
|
+
if (newest === null) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
const { runtimeMode, selections, accounts } = newest.preference;
|
|
47
|
+
const selection = selections?.[provider];
|
|
48
|
+
const account = accounts?.[provider];
|
|
49
|
+
return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {}), ...(account ? { account } : {}) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private newest(): Held | null {
|
|
53
|
+
let newest: Held | null = null;
|
|
54
|
+
for (const held of this.byClient.values()) {
|
|
55
|
+
if (newest === null || isNewer(held, newest)) {
|
|
56
|
+
newest = held;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return newest;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isNewer(held: Held, than: Held): boolean {
|
|
64
|
+
const changedAt = held.preference.changedAt ?? 0;
|
|
65
|
+
const otherChangedAt = than.preference.changedAt ?? 0;
|
|
66
|
+
return changedAt === otherChangedAt ? held.told > than.told : changedAt > otherChangedAt;
|
|
67
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { attachmentImageMime, type ChatContextBreakdown, type ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
// A rough rate for prose and code alike; scaling to the CLI's own total absorbs the error.
|
|
4
|
+
const CHARS_PER_TOKEN = 4;
|
|
5
|
+
|
|
6
|
+
// What a model is charged for a picture at the size a CLI sends it, whatever its bytes on disk.
|
|
7
|
+
const IMAGE_TOKENS = 1_600;
|
|
8
|
+
|
|
9
|
+
// Tools whose result is a file's contents. An MCP tool is named `<server>/<tool>`, so only the last part counts.
|
|
10
|
+
const READ_TOOLS = new Set(['Read', 'NotebookRead', 'View', 'read_file', 'read_text_file', 'read_multiple_files']);
|
|
11
|
+
|
|
12
|
+
function isReadTool(name: string): boolean {
|
|
13
|
+
return READ_TOOLS.has(name.slice(name.lastIndexOf('/') + 1));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function textTokens(text: string): number {
|
|
17
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function inputTokens(input: unknown): number {
|
|
21
|
+
if (input === undefined || input === null) {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
return textTokens(typeof input === 'string' ? input : JSON.stringify(input));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* What the context of the last request is made of, estimated from the thread and scaled to the
|
|
29
|
+
* total the CLI reported. A subagent's own work never reached the main context, only its prompt and
|
|
30
|
+
* its report did, and nothing before the last compaction survived it. Thinking is left out: a CLI
|
|
31
|
+
* does not carry it into the next request.
|
|
32
|
+
*/
|
|
33
|
+
export function estimateContextBreakdown(items: readonly ChatItem[], contextTokens: number): ChatContextBreakdown {
|
|
34
|
+
let toolOutput = 0;
|
|
35
|
+
let filesRead = 0;
|
|
36
|
+
let conversation = 0;
|
|
37
|
+
const start = items.findLastIndex((item) => item.kind === 'compaction') + 1;
|
|
38
|
+
for (const item of items.slice(start)) {
|
|
39
|
+
switch (item.kind) {
|
|
40
|
+
case 'user':
|
|
41
|
+
conversation += textTokens(item.text);
|
|
42
|
+
filesRead += (item.attachments ?? []).filter((attachment) => attachmentImageMime(attachment) !== null).length * IMAGE_TOKENS;
|
|
43
|
+
break;
|
|
44
|
+
case 'assistant':
|
|
45
|
+
if (!item.parentToolUseId) {
|
|
46
|
+
conversation += textTokens(item.text);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case 'tool': {
|
|
50
|
+
if (item.parentToolUseId !== null) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const tokens = inputTokens(item.input) + textTokens(item.output ?? item.progress?.output ?? '');
|
|
54
|
+
if (isReadTool(item.name)) {
|
|
55
|
+
filesRead += tokens;
|
|
56
|
+
} else {
|
|
57
|
+
toolOutput += tokens;
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'subagent':
|
|
62
|
+
// A row the host drew for work it delegated stands for that work; the call that made it is a tool row of its own.
|
|
63
|
+
if (item.origin !== 'ruimte') {
|
|
64
|
+
toolOutput += textTokens(item.prompt ?? '') + textTokens(item.result ?? '');
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const total = Math.max(0, Math.round(contextTokens));
|
|
72
|
+
const estimated = toolOutput + filesRead + conversation;
|
|
73
|
+
const scale = estimated > total ? total / estimated : 1;
|
|
74
|
+
const parts = {
|
|
75
|
+
toolOutput: Math.floor(toolOutput * scale),
|
|
76
|
+
filesRead: Math.floor(filesRead * scale),
|
|
77
|
+
conversation: Math.floor(conversation * scale)
|
|
78
|
+
};
|
|
79
|
+
return { ...parts, system: Math.max(0, total - parts.toolOutput - parts.filesRead - parts.conversation) };
|
|
80
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ChatEvent } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
/* The same rhythm as terminal output: a frame at 60 Hz. */
|
|
4
|
+
export const DELTA_TICK_MS = 16;
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Holds the deltas of one chat back for a frame and sends the run on one item as a single delta. A
|
|
8
|
+
* CLI writes tens of deltas a second and every one was a frame to every client and a state update
|
|
9
|
+
* there, while the reveal on the client only moves once per frame anyway. Any other event sends
|
|
10
|
+
* what is held first, so the order a client sees is the order the thread changed in.
|
|
11
|
+
*/
|
|
12
|
+
export class DeltaCoalescer {
|
|
13
|
+
private pending: { itemId: string; text: string } | null = null;
|
|
14
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
15
|
+
private readonly send: (event: ChatEvent) => void;
|
|
16
|
+
private readonly tickMs: number;
|
|
17
|
+
|
|
18
|
+
constructor(send: (event: ChatEvent) => void, tickMs: number = DELTA_TICK_MS) {
|
|
19
|
+
this.send = send;
|
|
20
|
+
this.tickMs = tickMs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
push(event: ChatEvent): void {
|
|
24
|
+
if (event.type !== 'delta') {
|
|
25
|
+
this.flush();
|
|
26
|
+
this.send(event);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (this.pending !== null && this.pending.itemId === event.itemId) {
|
|
30
|
+
this.pending.text += event.text;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.flush();
|
|
34
|
+
this.pending = { itemId: event.itemId, text: event.text };
|
|
35
|
+
this.timer = setTimeout(() => this.flush(), this.tickMs);
|
|
36
|
+
// A frame of text waiting must never be the reason the process stays up.
|
|
37
|
+
this.timer.unref?.();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Sends what is held now, before a client attaches or detaches or the tick runs out. */
|
|
41
|
+
flush(): void {
|
|
42
|
+
if (this.timer !== null) {
|
|
43
|
+
clearTimeout(this.timer);
|
|
44
|
+
this.timer = null;
|
|
45
|
+
}
|
|
46
|
+
const pending = this.pending;
|
|
47
|
+
if (pending === null) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.pending = null;
|
|
51
|
+
this.send({ type: 'delta', itemId: pending.itemId, text: pending.text });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Drops what is held without sending it, for a chat that is gone. */
|
|
55
|
+
dispose(): void {
|
|
56
|
+
if (this.timer !== null) {
|
|
57
|
+
clearTimeout(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
this.pending = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CodedError } from '../coded-error.ts';
|
|
2
|
+
|
|
3
|
+
export type ChatErrorCode =
|
|
4
|
+
| 'history-expired'
|
|
5
|
+
| 'chat-not-found'
|
|
6
|
+
// A record on disk this version cannot read, which is never written over.
|
|
7
|
+
| 'chat-unreadable'
|
|
8
|
+
| 'chat-busy'
|
|
9
|
+
| 'request-not-found'
|
|
10
|
+
| 'chat-unsupported'
|
|
11
|
+
| 'invalid-attachments'
|
|
12
|
+
| 'subagent-not-found'
|
|
13
|
+
| 'task-not-found'
|
|
14
|
+
// What reading or forking a CLI's own conversation is refused with.
|
|
15
|
+
| 'transcript-missing'
|
|
16
|
+
| 'transcript-format'
|
|
17
|
+
| 'fork-failed'
|
|
18
|
+
| 'turn-not-found'
|
|
19
|
+
// What a bookmark is refused with.
|
|
20
|
+
| 'item-not-found'
|
|
21
|
+
| 'bookmark-not-found'
|
|
22
|
+
| 'too-many-bookmarks';
|
|
23
|
+
|
|
24
|
+
/* A host that refuses more than a chat does widens the codes with its own. */
|
|
25
|
+
export class ChatError<TCode extends string = ChatErrorCode> extends CodedError<TCode> {}
|