@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,168 @@
|
|
|
1
|
+
import { CHAT_REQUEST_LIMITS, type ChatApprovalItem, type ChatQuestionItem, type ChatRequestApproval, type ChatRequestSummary } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function str(value: unknown): string | null {
|
|
8
|
+
return typeof value === 'string' ? value : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The keys a tool's input names its subject under, in the order a person would look for one.
|
|
12
|
+
const SUBJECT_KEYS = ['file_path', 'notebook_path', 'path', 'url', 'query', 'pattern', 'skill', 'description'];
|
|
13
|
+
|
|
14
|
+
/* Cuts text at `max` code units, never between the halves of a surrogate pair. */
|
|
15
|
+
function cut(text: string, max: number): string {
|
|
16
|
+
if (text.length <= max) {
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
const end = /[\uD800-\uDBFF]/.test(text[max - 1] ?? '') ? max - 1 : max;
|
|
20
|
+
return text.slice(0, end);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clip(text: string, max: number): string {
|
|
24
|
+
return text.length <= max ? text : `${cut(text, max - 1)}…`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Keeps the end of a path, where its file name is. */
|
|
28
|
+
function clipPath(path: string, max: number): string {
|
|
29
|
+
if (path.length <= max) {
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
const start = path.length - (max - 1);
|
|
33
|
+
return `…${path.slice(/[\uDC00-\uDFFF]/.test(path[start] ?? '') ? start + 1 : start)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function firstLine(text: string): string {
|
|
37
|
+
return text.trim().split('\n', 1)[0] ?? '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* At most `maxLines` lines and `maxLength` code units, cut at a line; a first line longer than that is cut in itself. */
|
|
41
|
+
function clipLines(lines: readonly string[], maxLines: number, maxLength: number): { text: string; truncated: boolean } {
|
|
42
|
+
const kept: string[] = [];
|
|
43
|
+
let length = 0;
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
if (kept.length === maxLines || length + line.length + (kept.length > 0 ? 1 : 0) > maxLength) {
|
|
46
|
+
if (kept.length === 0) {
|
|
47
|
+
return { text: cut(line, maxLength), truncated: true };
|
|
48
|
+
}
|
|
49
|
+
return { text: kept.join('\n'), truncated: true };
|
|
50
|
+
}
|
|
51
|
+
length += line.length + (kept.length > 0 ? 1 : 0);
|
|
52
|
+
kept.push(line);
|
|
53
|
+
}
|
|
54
|
+
return { text: kept.join('\n'), truncated: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function splitLines(text: string): string[] {
|
|
58
|
+
return text === '' ? [] : text.replace(/\n$/, '').split('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* The lines a replacement takes out and puts in; a provider's edit is a fragment, so there is no context to show. */
|
|
62
|
+
function replacementLines(before: string, after: string): string[] {
|
|
63
|
+
return [...splitLines(before).map((line) => `-${line}`), ...splitLines(after).map((line) => `+${line}`)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Only what a unified diff changes: its headers and context lines stay in the thread. */
|
|
67
|
+
function changedLines(diff: string): string[] {
|
|
68
|
+
return splitLines(diff).filter((line) => (line.startsWith('-') && !line.startsWith('---')) || (line.startsWith('+') && !line.startsWith('+++')));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface FileChange {
|
|
72
|
+
path: string;
|
|
73
|
+
files: number;
|
|
74
|
+
lines: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* The first file a call changes, as Codex's patch or Claude's edit and write tools carry it; null for any other call. */
|
|
78
|
+
function fileChange(toolName: string, input: Record<string, unknown>): FileChange | null {
|
|
79
|
+
const patches = Array.isArray(input.changes)
|
|
80
|
+
? input.changes.filter(
|
|
81
|
+
(change): change is { path: string; diff: string } => isRecord(change) && typeof change.path === 'string' && typeof change.diff === 'string'
|
|
82
|
+
)
|
|
83
|
+
: [];
|
|
84
|
+
if (patches.length > 0) {
|
|
85
|
+
return { path: patches[0]!.path, files: patches.length, lines: changedLines(patches[0]!.diff) };
|
|
86
|
+
}
|
|
87
|
+
const path = str(input.file_path);
|
|
88
|
+
if (path === null) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (toolName === 'Edit') {
|
|
92
|
+
return { path, files: 1, lines: replacementLines(str(input.old_string) ?? '', str(input.new_string) ?? '') };
|
|
93
|
+
}
|
|
94
|
+
if (toolName === 'Write') {
|
|
95
|
+
return { path, files: 1, lines: replacementLines('', str(input.content) ?? '') };
|
|
96
|
+
}
|
|
97
|
+
if (toolName === 'MultiEdit' && Array.isArray(input.edits)) {
|
|
98
|
+
return {
|
|
99
|
+
path,
|
|
100
|
+
files: 1,
|
|
101
|
+
lines: input.edits.filter(isRecord).flatMap((edit) => replacementLines(str(edit.old_string) ?? '', str(edit.new_string) ?? ''))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function subjectOf(input: Record<string, unknown>): string {
|
|
108
|
+
const named = SUBJECT_KEYS.map((key) => str(input[key])).find((value) => value !== null && value.trim() !== '');
|
|
109
|
+
const first = named ?? Object.values(input).find((value): value is string => typeof value === 'string' && value.trim() !== '') ?? '';
|
|
110
|
+
return clip(firstLine(first), CHAT_REQUEST_LIMITS.subject);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function approvalSummary(item: ChatApprovalItem): ChatRequestApproval {
|
|
114
|
+
const input = isRecord(item.input) ? item.input : {};
|
|
115
|
+
const change = fileChange(item.toolName, input);
|
|
116
|
+
const command = change === null ? str(input.command) : null;
|
|
117
|
+
const diff = change === null ? null : clipLines(change.lines, CHAT_REQUEST_LIMITS.diffLines, CHAT_REQUEST_LIMITS.diff);
|
|
118
|
+
const shell = command === null ? null : clipLines(splitLines(command), CHAT_REQUEST_LIMITS.commandLines, CHAT_REQUEST_LIMITS.command);
|
|
119
|
+
const truncated = diff?.truncated === true || shell?.truncated === true;
|
|
120
|
+
return {
|
|
121
|
+
toolName: clip(item.toolName, CHAT_REQUEST_LIMITS.subject),
|
|
122
|
+
subject:
|
|
123
|
+
change !== null
|
|
124
|
+
? clipPath(change.path, CHAT_REQUEST_LIMITS.subject)
|
|
125
|
+
: command !== null
|
|
126
|
+
? clip(firstLine(command), CHAT_REQUEST_LIMITS.subject)
|
|
127
|
+
: subjectOf(input),
|
|
128
|
+
description: item.description === null ? null : clip(item.description, CHAT_REQUEST_LIMITS.description),
|
|
129
|
+
...(change === null ? {} : { path: clipPath(change.path, CHAT_REQUEST_LIMITS.subject) }),
|
|
130
|
+
...(change !== null && change.files > 1 ? { files: change.files } : {}),
|
|
131
|
+
...(diff === null || diff.text === '' ? {} : { diff: diff.text }),
|
|
132
|
+
...(shell === null ? {} : { command: shell.text }),
|
|
133
|
+
...(truncated ? { truncated: true } : {}),
|
|
134
|
+
canAllowAlways: item.canAllowAlways,
|
|
135
|
+
...(item.allowAlways
|
|
136
|
+
? {
|
|
137
|
+
allowAlways: {
|
|
138
|
+
label: clip(item.allowAlways.label, CHAT_REQUEST_LIMITS.header),
|
|
139
|
+
description: clip(item.allowAlways.description, CHAT_REQUEST_LIMITS.description)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
: {})
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function questionSummary(item: ChatQuestionItem): NonNullable<ChatRequestSummary['question']> {
|
|
147
|
+
return {
|
|
148
|
+
questions: item.questions.map((question) => ({
|
|
149
|
+
id: question.id,
|
|
150
|
+
header: clip(question.header, CHAT_REQUEST_LIMITS.header),
|
|
151
|
+
question: clip(question.question, CHAT_REQUEST_LIMITS.question),
|
|
152
|
+
choices: question.choices.map((choice) => ({ label: choice.label, description: clip(choice.description, CHAT_REQUEST_LIMITS.choiceDescription) })),
|
|
153
|
+
multiSelect: question.multiSelect
|
|
154
|
+
})),
|
|
155
|
+
...(item.async === undefined ? {} : { async: item.async })
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* What a chat waits on, oldest first, as `ChatInfo.requests` carries it. */
|
|
160
|
+
export function requestSummaries(pending: ReadonlyArray<ChatApprovalItem | ChatQuestionItem>): ChatRequestSummary[] {
|
|
161
|
+
return pending.slice(0, CHAT_REQUEST_LIMITS.perChat).map((item) => ({
|
|
162
|
+
requestId: item.requestId,
|
|
163
|
+
itemId: item.id,
|
|
164
|
+
kind: item.kind,
|
|
165
|
+
createdAt: item.createdAt,
|
|
166
|
+
...(item.kind === 'approval' ? { approval: approvalSummary(item) } : { question: questionSummary(item) })
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChatInfo, ChatItem } from '@adecore/agent-contracts';
|
|
2
|
+
import { ThreadProjector } from './projector.ts';
|
|
3
|
+
import { ChatThread } from './thread.ts';
|
|
4
|
+
|
|
5
|
+
// A subagent's conversation has no turns of its own; the projector is kept inside this one so it never opens any.
|
|
6
|
+
const READING_TURN = 'subagent-reading';
|
|
7
|
+
|
|
8
|
+
/* The info a thread needs to exist. Nothing reads it: a subagent's conversation is only ever its items. */
|
|
9
|
+
function readingInfo(): ChatInfo {
|
|
10
|
+
return {
|
|
11
|
+
chatId: 'subagent',
|
|
12
|
+
provider: 'claude',
|
|
13
|
+
cwd: '',
|
|
14
|
+
agentSessionId: null,
|
|
15
|
+
model: null,
|
|
16
|
+
selection: { model: '', options: {} },
|
|
17
|
+
runtimeMode: 'full-access',
|
|
18
|
+
status: 'running',
|
|
19
|
+
running: true,
|
|
20
|
+
activeTurnId: READING_TURN,
|
|
21
|
+
slashCommands: [],
|
|
22
|
+
usage: { contextTokens: 0, contextWindow: 0, costUsd: 0, turns: 0 },
|
|
23
|
+
createdAt: 0
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* A thread and the projector that writes it, for a conversation read back rather than streamed. */
|
|
28
|
+
export function readingThread(providerName: string, now: () => number): { thread: ChatThread; projector: ThreadProjector } {
|
|
29
|
+
const thread = new ChatThread(readingInfo());
|
|
30
|
+
return { thread, projector: new ThreadProjector(thread, { providerName, now }) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* An item as a reader of a finished record sees it: nothing streams any more and nothing belongs to a
|
|
35
|
+
* turn, so the timeline draws the whole conversation flat instead of folding it behind a turn it never had.
|
|
36
|
+
*/
|
|
37
|
+
export function settledReading(item: ChatItem): ChatItem {
|
|
38
|
+
if (item.kind === 'assistant') {
|
|
39
|
+
return { ...item, turnId: null, streaming: false };
|
|
40
|
+
}
|
|
41
|
+
if (item.kind === 'thinking') {
|
|
42
|
+
return { ...item, turnId: null, streaming: false, endedAt: item.endedAt ?? item.createdAt };
|
|
43
|
+
}
|
|
44
|
+
return { ...item, turnId: null };
|
|
45
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { workflowAgentIdOf, type ChatInfo, type ChatItem, type ChatSubagentChangedEvent, type ChatSubagentResult } from '@adecore/agent-contracts';
|
|
3
|
+
import { SYSTEM_WATCH, type DirectoryWatcher, type WatchSeams } from '../watch-seam.ts';
|
|
4
|
+
import { findSubagentsDir, findWorkflowAgent, readSubagentMetas, TranscriptProjection, type SubagentMeta } from './claude-transcript.ts';
|
|
5
|
+
import { listThreadItemsOnce, parseThreadItemsPage, projectCodexItems, type CodexProcessSpec, type ThreadItemsParams } from './codex-thread.ts';
|
|
6
|
+
import { ChatError } from './errors.ts';
|
|
7
|
+
import { readSubagentSettlement, type SubagentSettlement } from './subagent-settlement.ts';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PAGE = 60;
|
|
10
|
+
// How often an open panel asks a running Codex for the newest items of the thread it follows.
|
|
11
|
+
const CODEX_POLL_MS = 2000;
|
|
12
|
+
// A page asked of a process started for the question is kept this long, so paging back costs one process.
|
|
13
|
+
const CODEX_CACHE_MS = 5000;
|
|
14
|
+
// What a poll compares: enough of the newest end to see an item land or settle.
|
|
15
|
+
const CODEX_POLL_ITEMS = 10;
|
|
16
|
+
// Lines land in bursts; a panel is told once per burst, not once per line.
|
|
17
|
+
const CHANGE_SETTLE_MS = 150;
|
|
18
|
+
// Transcripts kept read, beyond the ones a client holds open.
|
|
19
|
+
const MAX_IDLE_PROJECTIONS = 8;
|
|
20
|
+
const CODEX_CURSOR = 'codex:';
|
|
21
|
+
|
|
22
|
+
/* What the reader needs of one chat, so a test can hand in a thread without a CLI. */
|
|
23
|
+
export interface SubagentChat {
|
|
24
|
+
info: ChatInfo;
|
|
25
|
+
/* Whether the chat's CLI runs now; a Codex child only works while the app-server of its parent does. */
|
|
26
|
+
running: boolean;
|
|
27
|
+
items(): ChatItem[];
|
|
28
|
+
/* A page through the chat's own running app-server, or null when none runs. */
|
|
29
|
+
listThreadItems(params: ThreadItemsParams): Promise<unknown> | null;
|
|
30
|
+
noteNative(toolUseId: string, native: { agentId?: string; threadId?: string }): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SubagentReaderOptions {
|
|
34
|
+
chat(chatId: string): SubagentChat | null;
|
|
35
|
+
/* What a chat is, loaded or not, for the step from a fork to the chat it was forked from. */
|
|
36
|
+
chatInfo?(chatId: string): Promise<ChatInfo | null>;
|
|
37
|
+
/* Where Claude Code keeps its projects; empty when it has none on this machine. */
|
|
38
|
+
claudeProjectsDir: string;
|
|
39
|
+
/* Where it keeps those of a chat's account, when that is not the default one's. */
|
|
40
|
+
claudeProjectsDirOf?(info: ChatInfo): string;
|
|
41
|
+
/* How a Codex app-server is started for a chat whose own is not running. */
|
|
42
|
+
codexProcess(info: ChatInfo): CodexProcessSpec;
|
|
43
|
+
/* Tells one client that a conversation it holds has more in it. */
|
|
44
|
+
notify(clientId: string, event: ChatSubagentChangedEvent): void;
|
|
45
|
+
seams?: WatchSeams;
|
|
46
|
+
now?(): number;
|
|
47
|
+
listOnce?(spec: CodexProcessSpec, params: ThreadItemsParams): Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ClaudeFile {
|
|
51
|
+
dir: string;
|
|
52
|
+
path: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Hold {
|
|
56
|
+
chatId: string;
|
|
57
|
+
toolUseId: string;
|
|
58
|
+
clients: Set<string>;
|
|
59
|
+
stop: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function holdKey(chatId: string, toolUseId: string): string {
|
|
63
|
+
return `${chatId}\n${toolUseId}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findSubagent(items: ChatItem[], toolUseId: string): ChatItem | undefined {
|
|
67
|
+
return items.find((item) => item.kind === 'subagent' && item.toolUseId === toolUseId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A workflow's agent works while its Workflow row runs and its last report has it running.
|
|
71
|
+
function workflowAgentRunning(items: ChatItem[], agentId: string): boolean {
|
|
72
|
+
return items.some(
|
|
73
|
+
(item) =>
|
|
74
|
+
item.kind === 'tool' &&
|
|
75
|
+
item.state === 'running' &&
|
|
76
|
+
item.workflow?.agents.some((agent) => agent.agentId === agentId && agent.status === 'running') === true
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* The whole conversation of a subagent, which the chat's own thread only keeps the beginning of:
|
|
82
|
+
* Claude's transcript beside the session, or the thread Codex keeps per agent. A client that holds a
|
|
83
|
+
* conversation is told when it grows (a counted watch on Claude's folder, a poll of the running Codex)
|
|
84
|
+
* for as long as it holds it, and the event carries nothing but that there is more.
|
|
85
|
+
*/
|
|
86
|
+
export class SubagentReader {
|
|
87
|
+
private readonly options: SubagentReaderOptions;
|
|
88
|
+
private readonly seams: WatchSeams;
|
|
89
|
+
private readonly now: () => number;
|
|
90
|
+
private readonly listOnce: (spec: CodexProcessSpec, params: ThreadItemsParams) => Promise<unknown>;
|
|
91
|
+
private readonly claudeFiles = new Map<string, ClaudeFile>();
|
|
92
|
+
// Keyed on the transcript path, with the chat it was read for so a grandchild's status can be found.
|
|
93
|
+
private readonly projections = new Map<string, { chatId: string; projection: TranscriptProjection }>();
|
|
94
|
+
// The subagents seen inside a Codex conversation, so a row of a grandchild knows its thread.
|
|
95
|
+
private readonly codexRows = new Map<string, ChatItem>();
|
|
96
|
+
private readonly codexCache = new Map<string, { at: number; result: unknown }>();
|
|
97
|
+
// The newest end of every Codex thread as a reader last saw it, which is what a poll compares against.
|
|
98
|
+
private readonly codexSeen = new Map<string, string>();
|
|
99
|
+
private readonly holds = new Map<string, Hold>();
|
|
100
|
+
private readonly dirWatches = new Map<string, { watcher: DirectoryWatcher; keys: Set<string> }>();
|
|
101
|
+
private readonly changed = new Set<string>();
|
|
102
|
+
private cancelSettle: (() => void) | null = null;
|
|
103
|
+
|
|
104
|
+
constructor(options: SubagentReaderOptions) {
|
|
105
|
+
this.options = options;
|
|
106
|
+
this.seams = options.seams ?? SYSTEM_WATCH;
|
|
107
|
+
this.now = options.now ?? Date.now;
|
|
108
|
+
this.listOnce = options.listOnce ?? listThreadItemsOnce;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async read(chatId: string, toolUseId: string, cursor?: string, limit = DEFAULT_PAGE): Promise<ChatSubagentResult> {
|
|
112
|
+
const chat = this.require(chatId);
|
|
113
|
+
return chat.info.provider === 'codex' ? this.readCodex(chat, toolUseId, cursor, limit) : this.readClaude(chat, toolUseId, cursor, limit);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Every item, for an agent reading the conversation as text; a Codex thread is paged through to its start. */
|
|
117
|
+
async readAll(chatId: string, toolUseId: string): Promise<ChatItem[]> {
|
|
118
|
+
const chat = this.require(chatId);
|
|
119
|
+
if (chat.info.provider !== 'codex') {
|
|
120
|
+
return (await this.claudeProjection(chat, toolUseId)).items();
|
|
121
|
+
}
|
|
122
|
+
const pages: ChatItem[][] = [];
|
|
123
|
+
let cursor: string | undefined;
|
|
124
|
+
do {
|
|
125
|
+
const page = await this.readCodex(chat, toolUseId, cursor, 100);
|
|
126
|
+
pages.unshift(page.items);
|
|
127
|
+
cursor = page.history.cursor ?? undefined;
|
|
128
|
+
} while (cursor !== undefined);
|
|
129
|
+
return pages.flat();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* What a Claude subagent's own transcript says about how it ended, for a row whose CLI is gone and
|
|
134
|
+
* so will never send the notification; null when there is no transcript or it does not show an end.
|
|
135
|
+
*/
|
|
136
|
+
async claudeSettlement(chatId: string, toolUseId: string): Promise<SubagentSettlement | null> {
|
|
137
|
+
const chat = this.options.chat(chatId);
|
|
138
|
+
if (!chat || chat.info.provider !== 'claude') {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return await readSubagentSettlement((await this.claudeFile(chat, toolUseId)).path);
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Keeps `clientId` told about this conversation until it lets go, closes its socket or the chat goes. */
|
|
149
|
+
hold(clientId: string, chatId: string, toolUseId: string): void {
|
|
150
|
+
const key = holdKey(chatId, toolUseId);
|
|
151
|
+
const existing = this.holds.get(key);
|
|
152
|
+
if (existing) {
|
|
153
|
+
existing.clients.add(clientId);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const chat = this.options.chat(chatId);
|
|
157
|
+
if (!chat) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const stop = chat.info.provider === 'codex' ? this.pollCodex(key, chatId, toolUseId) : this.watchClaude(key);
|
|
161
|
+
this.holds.set(key, { chatId, toolUseId, clients: new Set([clientId]), stop });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
release(clientId: string, chatId: string, toolUseId: string): void {
|
|
165
|
+
const key = holdKey(chatId, toolUseId);
|
|
166
|
+
const hold = this.holds.get(key);
|
|
167
|
+
if (!hold) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
hold.clients.delete(clientId);
|
|
171
|
+
if (hold.clients.size === 0) {
|
|
172
|
+
this.drop(key, hold);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
releaseClient(clientId: string): void {
|
|
177
|
+
for (const hold of [...this.holds.values()]) {
|
|
178
|
+
this.release(clientId, hold.chatId, hold.toolUseId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Forgets everything about a chat that is gone. */
|
|
183
|
+
releaseChat(chatId: string): void {
|
|
184
|
+
const ofChat = (key: string): boolean => key.startsWith(`${chatId}\n`);
|
|
185
|
+
for (const [key, hold] of [...this.holds]) {
|
|
186
|
+
if (ofChat(key)) {
|
|
187
|
+
this.drop(key, hold);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const [key, file] of [...this.claudeFiles]) {
|
|
191
|
+
if (ofChat(key)) {
|
|
192
|
+
this.claudeFiles.delete(key);
|
|
193
|
+
this.projections.delete(file.path);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const map of [this.codexRows, this.codexSeen]) {
|
|
197
|
+
for (const key of [...map.keys()]) {
|
|
198
|
+
if (ofChat(key)) {
|
|
199
|
+
map.delete(key);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* How many clients hold something, for a test that checks a socket let go of everything. */
|
|
206
|
+
get holdCount(): number {
|
|
207
|
+
return this.holds.size;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private require(chatId: string): SubagentChat {
|
|
211
|
+
const chat = this.options.chat(chatId);
|
|
212
|
+
if (!chat) {
|
|
213
|
+
throw new ChatError('chat-not-found', `No chat ${chatId}`);
|
|
214
|
+
}
|
|
215
|
+
return chat;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private drop(key: string, hold: Hold): void {
|
|
219
|
+
this.holds.delete(key);
|
|
220
|
+
this.changed.delete(key);
|
|
221
|
+
hold.stop();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Running while its row says so, whether that row is in the chat's thread or in a conversation read before. */
|
|
225
|
+
private live(chat: SubagentChat, toolUseId: string, inside: ChatItem[]): boolean {
|
|
226
|
+
const workflowAgent = workflowAgentIdOf(toolUseId);
|
|
227
|
+
if (workflowAgent !== null) {
|
|
228
|
+
return workflowAgentRunning(chat.items(), workflowAgent);
|
|
229
|
+
}
|
|
230
|
+
const row = findSubagent(chat.items(), toolUseId) ?? findSubagent(inside, toolUseId);
|
|
231
|
+
return row?.kind === 'subagent' && row.status === 'running';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async readClaude(chat: SubagentChat, toolUseId: string, cursor: string | undefined, limit: number): Promise<ChatSubagentResult> {
|
|
235
|
+
const projection = await this.claudeProjection(chat, toolUseId);
|
|
236
|
+
const page = projection.page(limit, cursor);
|
|
237
|
+
const seen = [...this.projections.values()].filter((entry) => entry.chatId === chat.info.chatId).flatMap((entry) => entry.projection.items());
|
|
238
|
+
return {
|
|
239
|
+
items: page.items,
|
|
240
|
+
history: page.history,
|
|
241
|
+
source: 'claude-transcript',
|
|
242
|
+
context: { provider: 'claude', cwd: chat.info.cwd },
|
|
243
|
+
live: this.live(chat, toolUseId, seen)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async claudeProjection(chat: SubagentChat, toolUseId: string): Promise<TranscriptProjection> {
|
|
248
|
+
const file = await this.claudeFile(chat, toolUseId);
|
|
249
|
+
let entry = this.projections.get(file.path);
|
|
250
|
+
if (!entry) {
|
|
251
|
+
entry = { chatId: chat.info.chatId, projection: new TranscriptProjection(file.path) };
|
|
252
|
+
this.projections.set(file.path, entry);
|
|
253
|
+
this.trimProjections();
|
|
254
|
+
}
|
|
255
|
+
if (!(await entry.projection.refresh())) {
|
|
256
|
+
throw new ChatError('subagent-not-found', 'Claude has not written a transcript for this subagent');
|
|
257
|
+
}
|
|
258
|
+
return entry.projection;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async claudeFile(chat: SubagentChat, toolUseId: string): Promise<ClaudeFile> {
|
|
262
|
+
const key = holdKey(chat.info.chatId, toolUseId);
|
|
263
|
+
const known = this.claudeFiles.get(key);
|
|
264
|
+
if (known) {
|
|
265
|
+
return known;
|
|
266
|
+
}
|
|
267
|
+
const found = await this.claudeSubagentOf(chat.info, toolUseId);
|
|
268
|
+
if (found === null) {
|
|
269
|
+
throw new ChatError('subagent-not-found', 'Claude has not written a transcript for this subagent');
|
|
270
|
+
}
|
|
271
|
+
const { dir, meta } = found;
|
|
272
|
+
const file = { dir, path: meta.transcript };
|
|
273
|
+
this.claudeFiles.set(key, file);
|
|
274
|
+
chat.noteNative(toolUseId, { agentId: meta.agentId });
|
|
275
|
+
return file;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* The transcripts nobody holds are only a cache: past a handful, the oldest is read again when asked. */
|
|
279
|
+
private trimProjections(): void {
|
|
280
|
+
const held = new Set([...this.holds.values()].map((hold) => this.claudeFiles.get(holdKey(hold.chatId, hold.toolUseId))?.path));
|
|
281
|
+
const idle = [...this.projections.keys()].filter((path) => !held.has(path));
|
|
282
|
+
for (const path of idle.slice(0, Math.max(0, idle.length - MAX_IDLE_PROJECTIONS))) {
|
|
283
|
+
this.projections.delete(path);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* The folder and meta of a subagent, in the chat's own session or, for a row a fork copied, in the
|
|
289
|
+
* session of the chat it was forked from: the CLI keeps subagents beside the session that ran them,
|
|
290
|
+
* and a fork's transcript is a new session with none. One step per fork, so a fork of a fork works too.
|
|
291
|
+
*/
|
|
292
|
+
private async claudeSubagentOf(info: ChatInfo, toolUseId: string): Promise<{ dir: string; meta: SubagentMeta } | null> {
|
|
293
|
+
const seen = new Set<string>();
|
|
294
|
+
let current: ChatInfo | null = info;
|
|
295
|
+
while (current !== null && !seen.has(current.chatId)) {
|
|
296
|
+
seen.add(current.chatId);
|
|
297
|
+
const sessionId = current.agentSessionId;
|
|
298
|
+
const dir =
|
|
299
|
+
sessionId === null
|
|
300
|
+
? null
|
|
301
|
+
: await findSubagentsDir(this.options.claudeProjectsDirOf?.(current) ?? this.options.claudeProjectsDir, current.cwd, sessionId);
|
|
302
|
+
const workflowAgent = workflowAgentIdOf(toolUseId);
|
|
303
|
+
if (dir !== null && workflowAgent !== null) {
|
|
304
|
+
const meta = await findWorkflowAgent(dir, workflowAgent);
|
|
305
|
+
if (meta !== null) {
|
|
306
|
+
return { dir: dirname(meta.transcript), meta };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// A grandchild has a file of its own in the same folder, found by the call that opened it like any other.
|
|
310
|
+
const meta = dir === null ? undefined : (await readSubagentMetas(dir)).find((candidate) => candidate.toolUseId === toolUseId);
|
|
311
|
+
if (dir !== null && meta !== undefined) {
|
|
312
|
+
return { dir, meta };
|
|
313
|
+
}
|
|
314
|
+
const origin: string | undefined = current.forkOf?.chatId;
|
|
315
|
+
current = origin === undefined || !this.options.chatInfo ? null : await this.options.chatInfo(origin);
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async readCodex(chat: SubagentChat, toolUseId: string, cursor: string | undefined, limit: number): Promise<ChatSubagentResult> {
|
|
321
|
+
const threadId = this.codexThread(chat, toolUseId);
|
|
322
|
+
if (cursor !== undefined && !cursor.startsWith(CODEX_CURSOR)) {
|
|
323
|
+
throw new ChatError('history-expired', 'The conversation changed. Reload its history.');
|
|
324
|
+
}
|
|
325
|
+
const params: ThreadItemsParams = {
|
|
326
|
+
threadId,
|
|
327
|
+
limit,
|
|
328
|
+
sortDirection: 'desc',
|
|
329
|
+
...(cursor === undefined ? {} : { cursor: cursor.slice(CODEX_CURSOR.length) })
|
|
330
|
+
};
|
|
331
|
+
const { entries, nextCursor } = parseThreadItemsPage(await this.codexPage(chat, params));
|
|
332
|
+
if (cursor === undefined) {
|
|
333
|
+
// What a poll that starts after this read compares against, so a change in between is not lost.
|
|
334
|
+
this.codexSeen.set(holdKey(chat.info.chatId, toolUseId), JSON.stringify(entries.slice(0, CODEX_POLL_ITEMS)));
|
|
335
|
+
}
|
|
336
|
+
const items = projectCodexItems([...entries].reverse(), this.now());
|
|
337
|
+
for (const item of items) {
|
|
338
|
+
if (item.kind === 'subagent') {
|
|
339
|
+
this.codexRows.set(holdKey(chat.info.chatId, item.toolUseId), item);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
items,
|
|
344
|
+
history: { cursor: nextCursor === null ? null : `${CODEX_CURSOR}${nextCursor}` },
|
|
345
|
+
source: 'codex-thread',
|
|
346
|
+
context: { provider: 'codex', cwd: chat.info.cwd },
|
|
347
|
+
live: chat.running && this.live(chat, toolUseId, [...this.codexRows.values()])
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private codexThreadOf(chat: SubagentChat, toolUseId: string): string | null {
|
|
352
|
+
const row = findSubagent(chat.items(), toolUseId) ?? this.codexRows.get(holdKey(chat.info.chatId, toolUseId));
|
|
353
|
+
return row?.kind === 'subagent' ? (row.native?.threadId ?? null) : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private codexThread(chat: SubagentChat, toolUseId: string): string {
|
|
357
|
+
const threadId = this.codexThreadOf(chat, toolUseId);
|
|
358
|
+
if (threadId === null) {
|
|
359
|
+
throw new ChatError('subagent-not-found', 'Codex has not said which thread this subagent works in');
|
|
360
|
+
}
|
|
361
|
+
return threadId;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async codexPage(chat: SubagentChat, params: ThreadItemsParams): Promise<unknown> {
|
|
365
|
+
const running = chat.listThreadItems(params);
|
|
366
|
+
if (running !== null) {
|
|
367
|
+
return running;
|
|
368
|
+
}
|
|
369
|
+
const key = JSON.stringify(params);
|
|
370
|
+
const now = this.now();
|
|
371
|
+
const cached = this.codexCache.get(key);
|
|
372
|
+
if (cached && now - cached.at < CODEX_CACHE_MS) {
|
|
373
|
+
return cached.result;
|
|
374
|
+
}
|
|
375
|
+
const result = await this.listOnce(this.options.codexProcess(chat.info), params);
|
|
376
|
+
for (const [stale, entry] of [...this.codexCache]) {
|
|
377
|
+
if (now - entry.at >= CODEX_CACHE_MS) {
|
|
378
|
+
this.codexCache.delete(stale);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
this.codexCache.set(key, { at: now, result });
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/* One watch per subagents folder, however many of its transcripts are held. */
|
|
386
|
+
private watchClaude(key: string): () => void {
|
|
387
|
+
const file = this.claudeFiles.get(key);
|
|
388
|
+
if (!file) {
|
|
389
|
+
return () => undefined;
|
|
390
|
+
}
|
|
391
|
+
let shared = this.dirWatches.get(file.dir);
|
|
392
|
+
if (!shared) {
|
|
393
|
+
const watcher = this.seams.watch(file.dir, { recursive: false }, (_event, filename) => this.onClaudeChange(file.dir, filename));
|
|
394
|
+
// A folder that disappears ends the growing, not the host.
|
|
395
|
+
watcher.on('error', () => undefined);
|
|
396
|
+
shared = { watcher, keys: new Set() };
|
|
397
|
+
this.dirWatches.set(file.dir, shared);
|
|
398
|
+
}
|
|
399
|
+
shared.keys.add(key);
|
|
400
|
+
return () => {
|
|
401
|
+
const current = this.dirWatches.get(file.dir);
|
|
402
|
+
current?.keys.delete(key);
|
|
403
|
+
if (current && current.keys.size === 0) {
|
|
404
|
+
current.watcher.close();
|
|
405
|
+
this.dirWatches.delete(file.dir);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private onClaudeChange(dir: string, filename: string | null): void {
|
|
411
|
+
for (const key of this.dirWatches.get(dir)?.keys ?? []) {
|
|
412
|
+
const path = this.claudeFiles.get(key)?.path;
|
|
413
|
+
// A platform that names no file could mean any of them.
|
|
414
|
+
if (filename === null || (path !== undefined && path.endsWith(`/${filename}`))) {
|
|
415
|
+
this.markChanged(key);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private pollCodex(key: string, chatId: string, toolUseId: string): () => void {
|
|
421
|
+
let cancel: (() => void) | null = null;
|
|
422
|
+
let stopped = false;
|
|
423
|
+
const tick = async (): Promise<void> => {
|
|
424
|
+
cancel = null;
|
|
425
|
+
const chat = this.options.chat(chatId);
|
|
426
|
+
const threadId = chat ? this.codexThreadOf(chat, toolUseId) : null;
|
|
427
|
+
const asked = chat && threadId ? chat.listThreadItems({ threadId, limit: CODEX_POLL_ITEMS, sortDirection: 'desc' }) : null;
|
|
428
|
+
if (asked !== null) {
|
|
429
|
+
try {
|
|
430
|
+
const seen = JSON.stringify(parseThreadItemsPage(await asked).entries);
|
|
431
|
+
if (!stopped && this.codexSeen.get(key) !== seen) {
|
|
432
|
+
this.codexSeen.set(key, seen);
|
|
433
|
+
this.markChanged(key);
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
// The process went between the question and the answer; the next tick asks again.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!stopped) {
|
|
440
|
+
cancel = this.seams.schedule(() => tick(), CODEX_POLL_MS);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
cancel = this.seams.schedule(() => tick(), CODEX_POLL_MS);
|
|
444
|
+
return () => {
|
|
445
|
+
stopped = true;
|
|
446
|
+
cancel?.();
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private markChanged(key: string): void {
|
|
451
|
+
this.changed.add(key);
|
|
452
|
+
if (this.cancelSettle !== null) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.cancelSettle = this.seams.schedule(() => this.flushChanged(), CHANGE_SETTLE_MS);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private flushChanged(): void {
|
|
459
|
+
this.cancelSettle = null;
|
|
460
|
+
const keys = [...this.changed];
|
|
461
|
+
this.changed.clear();
|
|
462
|
+
for (const key of keys) {
|
|
463
|
+
const hold = this.holds.get(key);
|
|
464
|
+
for (const clientId of hold?.clients ?? []) {
|
|
465
|
+
this.options.notify(clientId, { chatId: hold!.chatId, toolUseId: hold!.toolUseId });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|