@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,684 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChatQuestion,
|
|
3
|
+
ChatSubagentStatus,
|
|
4
|
+
ChatSubagentUsage,
|
|
5
|
+
ChatTurnLimit,
|
|
6
|
+
ChatWorkflow,
|
|
7
|
+
ChatWorkflowAgent,
|
|
8
|
+
ChatWorkflowPhase
|
|
9
|
+
} from '@adecore/agent-contracts';
|
|
10
|
+
import { readClaudeEvent } from '../usage/limits/normalize.ts';
|
|
11
|
+
import { claudeRuntimeMode } from '../providers/claude.ts';
|
|
12
|
+
import type { ApprovalDecision, BackendEvent } from './backend.ts';
|
|
13
|
+
|
|
14
|
+
type Frame = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Frame {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function str(value: unknown): string | null {
|
|
21
|
+
return typeof value === 'string' ? value : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function num(value: unknown): number {
|
|
25
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A tool result is a string or a list of content blocks; only the text is worth showing.
|
|
29
|
+
function resultText(content: unknown): string {
|
|
30
|
+
if (typeof content === 'string') {
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(content)) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
return content
|
|
37
|
+
.map((block) => (isRecord(block) && block.type === 'text' ? str(block.text) : null))
|
|
38
|
+
.filter((text): text is string => text !== null)
|
|
39
|
+
.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// What a subagent spent, as `task_progress` and `task_notification` report it.
|
|
43
|
+
function taskUsage(usage: unknown): ChatSubagentUsage | null {
|
|
44
|
+
return isRecord(usage) ? { totalTokens: num(usage.total_tokens), toolUses: num(usage.tool_uses), durationMs: num(usage.duration_ms) } : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function contextTokens(usage: unknown): number {
|
|
48
|
+
return isRecord(usage) ? num(usage.input_tokens) + num(usage.cache_creation_input_tokens) + num(usage.cache_read_input_tokens) : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The kinds of task Claude Code keeps beside its turns that are a command or a monitor; a subagent has a row of its own.
|
|
52
|
+
const BACKGROUND_TASK_TYPES = new Set(['local_bash', 'monitor_mcp', 'monitor_ws']);
|
|
53
|
+
const ENDED_TASK_STATUSES = new Set(['completed', 'failed', 'killed', 'stopped']);
|
|
54
|
+
|
|
55
|
+
// How Claude Code (2.1.273 through 2.1.282) answers a Workflow call, with the id of the task the workflow runs as.
|
|
56
|
+
const WORKFLOW_LAUNCHED = /^Workflow launched in background\. Task ID: (\S+)/;
|
|
57
|
+
|
|
58
|
+
const WORKFLOW_AGENT_STATUS: Record<string, ChatSubagentStatus> = { done: 'done', error: 'failed' };
|
|
59
|
+
|
|
60
|
+
function intOrNull(value: unknown): number | null {
|
|
61
|
+
return typeof value === 'number' && Number.isInteger(value) ? value : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* A workflow's `workflow_progress` (Claude Code 2.1.282): every phase the script announced and every
|
|
66
|
+
* agent it started so far, one entry per index, beside log lines the thread does not show. An agent
|
|
67
|
+
* reads `start` while it waits and while it works, `done` or `error` once it stopped.
|
|
68
|
+
*/
|
|
69
|
+
function parseWorkflowProgress(entries: unknown[]): Pick<ChatWorkflow, 'phases' | 'agents'> {
|
|
70
|
+
const phases = new Map<number, ChatWorkflowPhase>();
|
|
71
|
+
const agents = new Map<number, ChatWorkflowAgent>();
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!isRecord(entry)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const index = intOrNull(entry.index);
|
|
77
|
+
if (index === null) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (entry.type === 'workflow_phase') {
|
|
81
|
+
phases.set(index, { index, title: str(entry.title) ?? '' });
|
|
82
|
+
} else if (entry.type === 'workflow_agent') {
|
|
83
|
+
const durationMs = intOrNull(entry.durationMs);
|
|
84
|
+
agents.set(index, {
|
|
85
|
+
index,
|
|
86
|
+
label: str(entry.label) ?? '',
|
|
87
|
+
phaseIndex: intOrNull(entry.phaseIndex),
|
|
88
|
+
agentId: str(entry.agentId),
|
|
89
|
+
status: WORKFLOW_AGENT_STATUS[str(entry.state) ?? ''] ?? 'running',
|
|
90
|
+
startedAt: typeof entry.startedAt === 'number' ? entry.startedAt : null,
|
|
91
|
+
durationMs: durationMs !== null && durationMs >= 0 ? durationMs : null,
|
|
92
|
+
lastTool: str(entry.lastToolName)
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const byIndex = (a: { index: number }, b: { index: number }): number => a.index - b.index;
|
|
97
|
+
return { phases: [...phases.values()].sort(byIndex), agents: [...agents.values()].sort(byIndex) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The CLI's AskUserQuestion input, as far as the person needs to see it.
|
|
101
|
+
function parseQuestions(input: unknown): ChatQuestion[] {
|
|
102
|
+
if (!isRecord(input) || !Array.isArray(input.questions)) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return input.questions.filter(isRecord).map((question, index) => ({
|
|
106
|
+
id: String(index),
|
|
107
|
+
header: str(question.header) ?? '',
|
|
108
|
+
question: str(question.question) ?? '',
|
|
109
|
+
choices: Array.isArray(question.options)
|
|
110
|
+
? question.options.filter(isRecord).map((option) => ({ label: str(option.label) ?? '', description: str(option.description) ?? '' }))
|
|
111
|
+
: [],
|
|
112
|
+
multiSelect: question.multiSelect === true
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// What the CLI waits for on stdin, kept until `chat.approve` or `chat.answer` names the request.
|
|
117
|
+
type Pending =
|
|
118
|
+
| { type: 'approval'; toolUseId: string | null; input: unknown; suggestions: unknown[] }
|
|
119
|
+
| { type: 'question'; toolUseId: string | null; input: unknown };
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
* Claude Code's stream-json frames, in and out. Text is keyed by message id plus the ordinal of the
|
|
123
|
+
* text block inside that message, because the streaming events and the final `assistant` frames
|
|
124
|
+
* number their blocks differently (the final frames arrive one block at a time). One instance
|
|
125
|
+
* belongs to one process, so a resumed CLI that numbers its messages from the start gets its own.
|
|
126
|
+
*/
|
|
127
|
+
export class ClaudeProtocol {
|
|
128
|
+
private promptId: string | null = null;
|
|
129
|
+
private readonly pending = new Map<string, Pending>();
|
|
130
|
+
private streamMessageId: string | null = null;
|
|
131
|
+
private streamTextCount = 0;
|
|
132
|
+
private streamThinkingCount = 0;
|
|
133
|
+
// Stream block index to the ref of the text block collecting its deltas.
|
|
134
|
+
private readonly streamBlocks = new Map<number, string>();
|
|
135
|
+
private readonly frameTextCount = new Map<string, number>();
|
|
136
|
+
private readonly frameThinkingCount = new Map<string, number>();
|
|
137
|
+
// Commands started in the foreground, which the CLI may still send to the background, and what runs there now.
|
|
138
|
+
private readonly foregroundShells = new Map<string, { ref: string | null; description: string | null }>();
|
|
139
|
+
private readonly backgroundTasks = new Set<string>();
|
|
140
|
+
// Workflows still running, by task id, to the Workflow call that launched each.
|
|
141
|
+
private readonly workflows = new Map<string, string>();
|
|
142
|
+
// The uuid of the last main-chain assistant frame of the turn, which is the line of the transcript the turn ends on.
|
|
143
|
+
private lastUuid: string | null = null;
|
|
144
|
+
// The model each subagent last answered with, so a frame only reports one it had not named yet.
|
|
145
|
+
private readonly subagentModels = new Map<string, string>();
|
|
146
|
+
// Only main-chain errors classify the turn; each plan window can refuse it independently.
|
|
147
|
+
private apiError: string | null = null;
|
|
148
|
+
private readonly refusedWindows = new Map<string, number | null>();
|
|
149
|
+
|
|
150
|
+
beginPrompt(promptId: string): void {
|
|
151
|
+
this.promptId = promptId;
|
|
152
|
+
this.apiError = null;
|
|
153
|
+
this.refusedWindows.clear();
|
|
154
|
+
this.lastUuid = null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
handle(frame: unknown): BackendEvent[] {
|
|
158
|
+
const events: BackendEvent[] = [];
|
|
159
|
+
if (!isRecord(frame)) {
|
|
160
|
+
return events;
|
|
161
|
+
}
|
|
162
|
+
switch (frame.type) {
|
|
163
|
+
case 'system':
|
|
164
|
+
this.handleSystem(frame, events);
|
|
165
|
+
break;
|
|
166
|
+
case 'stream_event':
|
|
167
|
+
this.handleStreamEvent(frame, events);
|
|
168
|
+
break;
|
|
169
|
+
case 'assistant':
|
|
170
|
+
this.handleAssistant(frame, events);
|
|
171
|
+
break;
|
|
172
|
+
case 'user':
|
|
173
|
+
this.handleUser(frame, events);
|
|
174
|
+
break;
|
|
175
|
+
case 'result':
|
|
176
|
+
this.handleResult(frame, events);
|
|
177
|
+
break;
|
|
178
|
+
case 'control_request':
|
|
179
|
+
this.handleControlRequest(frame, events);
|
|
180
|
+
break;
|
|
181
|
+
case 'control_cancel_request': {
|
|
182
|
+
const requestId = str(frame.request_id);
|
|
183
|
+
if (requestId) {
|
|
184
|
+
this.pending.delete(requestId);
|
|
185
|
+
events.push({ type: 'request.withdrawn', requestId });
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'tool_progress':
|
|
190
|
+
this.handleToolProgress(frame, events);
|
|
191
|
+
break;
|
|
192
|
+
case 'rate_limit_event': {
|
|
193
|
+
this.noteRefusal(frame.rate_limit_info);
|
|
194
|
+
// The plan's own numbers, streamed while a turn runs; the usage monitor keeps them.
|
|
195
|
+
const update = readClaudeEvent(frame.rate_limit_info);
|
|
196
|
+
if (update !== null) {
|
|
197
|
+
events.push({ type: 'limits', update });
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
default:
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return events;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* The `control_response` that answers a permission request, or null when nothing waits under that id. */
|
|
208
|
+
approvalResponse(requestId: string, decision: ApprovalDecision, message?: string): unknown | null {
|
|
209
|
+
const pending = this.pending.get(requestId);
|
|
210
|
+
if (pending?.type !== 'approval') {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
this.pending.delete(requestId);
|
|
214
|
+
const toolUseID = pending.toolUseId ?? undefined;
|
|
215
|
+
const response =
|
|
216
|
+
decision === 'deny'
|
|
217
|
+
? { behavior: 'deny', message: message?.trim() || 'The user declined this action', toolUseID }
|
|
218
|
+
: {
|
|
219
|
+
behavior: 'allow',
|
|
220
|
+
updatedInput: pending.input,
|
|
221
|
+
toolUseID,
|
|
222
|
+
// The CLI's own suggestion of a rule; sending it back is what makes "always" stick.
|
|
223
|
+
...(decision === 'allow-always' && pending.suggestions.length > 0 ? { updatedPermissions: pending.suggestions } : {})
|
|
224
|
+
};
|
|
225
|
+
return { type: 'control_response', response: { subtype: 'success', request_id: requestId, response } };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* The `control_response` that answers an AskUserQuestion, or null when nothing waits under that id. */
|
|
229
|
+
questionResponse(requestId: string, answers: Record<string, string>): unknown | null {
|
|
230
|
+
const pending = this.pending.get(requestId);
|
|
231
|
+
if (pending?.type !== 'question') {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
this.pending.delete(requestId);
|
|
235
|
+
// The CLI keys answers by the question text, the client by the question's id.
|
|
236
|
+
const byText: Record<string, string> = {};
|
|
237
|
+
for (const question of parseQuestions(pending.input)) {
|
|
238
|
+
const given = answers[question.id];
|
|
239
|
+
if (given !== undefined) {
|
|
240
|
+
byText[question.question] = given;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const input = isRecord(pending.input) ? pending.input : {};
|
|
244
|
+
return {
|
|
245
|
+
type: 'control_response',
|
|
246
|
+
response: {
|
|
247
|
+
subtype: 'success',
|
|
248
|
+
request_id: requestId,
|
|
249
|
+
response: { behavior: 'allow', updatedInput: { ...input, answers: byText }, toolUseID: pending.toolUseId ?? undefined }
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* The `control_response` that turns down an approval or a question the CLI still holds, or null when nothing waits under that id. */
|
|
255
|
+
declineResponse(requestId: string, message: string): unknown | null {
|
|
256
|
+
const pending = this.pending.get(requestId);
|
|
257
|
+
if (pending === undefined) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
this.pending.delete(requestId);
|
|
261
|
+
return {
|
|
262
|
+
type: 'control_response',
|
|
263
|
+
response: { subtype: 'success', request_id: requestId, response: { behavior: 'deny', message, toolUseID: pending.toolUseId ?? undefined } }
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
forgetPending(): void {
|
|
268
|
+
this.pending.clear();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private handleSystem(frame: Frame, events: BackendEvent[]): void {
|
|
272
|
+
if (frame.subtype === 'init') {
|
|
273
|
+
const commands = Array.isArray(frame.slash_commands)
|
|
274
|
+
? frame.slash_commands.filter((command): command is string => typeof command === 'string')
|
|
275
|
+
: [];
|
|
276
|
+
const skills = Array.isArray(frame.skills) ? frame.skills.filter((skill): skill is string => typeof skill === 'string') : [];
|
|
277
|
+
const permissionMode = str(frame.permissionMode);
|
|
278
|
+
const effectiveRuntimeMode = permissionMode === null ? undefined : claudeRuntimeMode(permissionMode);
|
|
279
|
+
events.push({
|
|
280
|
+
type: 'session',
|
|
281
|
+
agentSessionId: str(frame.session_id),
|
|
282
|
+
model: str(frame.model),
|
|
283
|
+
slashCommands: commands,
|
|
284
|
+
skills,
|
|
285
|
+
...(permissionMode === null ? {} : { permissionMode }),
|
|
286
|
+
...(effectiveRuntimeMode === undefined ? {} : { effectiveRuntimeMode })
|
|
287
|
+
});
|
|
288
|
+
} else if (frame.subtype === 'status' && typeof frame.permissionMode === 'string') {
|
|
289
|
+
const effectiveRuntimeMode = claudeRuntimeMode(frame.permissionMode);
|
|
290
|
+
events.push({ type: 'permissions', permissionMode: frame.permissionMode, ...(effectiveRuntimeMode === undefined ? {} : { effectiveRuntimeMode }) });
|
|
291
|
+
} else if (frame.subtype === 'task_started') {
|
|
292
|
+
this.handleTaskStarted(frame, events);
|
|
293
|
+
} else if (frame.subtype === 'task_updated') {
|
|
294
|
+
this.handleTaskUpdated(frame, events);
|
|
295
|
+
} else if (frame.subtype === 'task_progress') {
|
|
296
|
+
const ref = str(frame.tool_use_id);
|
|
297
|
+
if (!ref) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Only some of a workflow's progress frames carry it; the rest only count what it spent.
|
|
301
|
+
if (Array.isArray(frame.workflow_progress)) {
|
|
302
|
+
events.push({
|
|
303
|
+
type: 'workflow.progress',
|
|
304
|
+
ref,
|
|
305
|
+
workflow: {
|
|
306
|
+
name: null,
|
|
307
|
+
...parseWorkflowProgress(frame.workflow_progress),
|
|
308
|
+
...(typeof frame.task_id === 'string' ? { taskId: frame.task_id } : {})
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
// Only a subagent reports what it spent; a task without that is a command with a description.
|
|
313
|
+
if (frame.task_type === 'local_agent' || str(frame.subagent_type)) {
|
|
314
|
+
events.push({
|
|
315
|
+
type: 'task.progress',
|
|
316
|
+
ref,
|
|
317
|
+
taskId: str(frame.task_id),
|
|
318
|
+
summary: str(frame.description),
|
|
319
|
+
lastTool: str(frame.last_tool_name),
|
|
320
|
+
usage: taskUsage(frame.usage)
|
|
321
|
+
});
|
|
322
|
+
} else {
|
|
323
|
+
const description = str(frame.description);
|
|
324
|
+
if (description) {
|
|
325
|
+
events.push({ type: 'tool.progress', ref, startedAt: null, description });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else if (frame.subtype === 'task_notification') {
|
|
329
|
+
const ref = str(frame.tool_use_id);
|
|
330
|
+
if (ref) {
|
|
331
|
+
this.subagentModels.delete(ref);
|
|
332
|
+
}
|
|
333
|
+
const taskId = str(frame.task_id);
|
|
334
|
+
if (taskId) {
|
|
335
|
+
this.endBackground(taskId, events);
|
|
336
|
+
this.workflows.delete(taskId);
|
|
337
|
+
}
|
|
338
|
+
// The CLI wakes the main agent itself when a background task settles; this frame is the only
|
|
339
|
+
// thing that says what it was about, and it arrives before the turn nobody asked for.
|
|
340
|
+
events.push({
|
|
341
|
+
type: 'task.done',
|
|
342
|
+
ref: str(frame.tool_use_id),
|
|
343
|
+
taskId,
|
|
344
|
+
summary: str(frame.summary),
|
|
345
|
+
ok: str(frame.status) === 'completed',
|
|
346
|
+
usage: taskUsage(frame.usage),
|
|
347
|
+
outputFile: str(frame.output_file) || null
|
|
348
|
+
});
|
|
349
|
+
} else if (frame.subtype === 'compact_boundary') {
|
|
350
|
+
const meta = isRecord(frame.compact_metadata) ? frame.compact_metadata : {};
|
|
351
|
+
const preTokens = num(meta.pre_tokens);
|
|
352
|
+
events.push({ type: 'compaction', preTokens: preTokens > 0 ? preTokens : null });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private handleTaskStarted(frame: Frame, events: BackendEvent[]): void {
|
|
357
|
+
const ref = str(frame.tool_use_id);
|
|
358
|
+
if (frame.task_type === 'local_agent') {
|
|
359
|
+
if (ref) {
|
|
360
|
+
events.push({
|
|
361
|
+
type: 'task.started',
|
|
362
|
+
ref,
|
|
363
|
+
taskId: str(frame.task_id),
|
|
364
|
+
description: str(frame.description),
|
|
365
|
+
subagentType: str(frame.subagent_type),
|
|
366
|
+
prompt: str(frame.prompt),
|
|
367
|
+
background: frame.is_backgrounded === true,
|
|
368
|
+
...(typeof frame.spawn_depth === 'number' ? { depth: frame.spawn_depth } : {})
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const taskId = str(frame.task_id);
|
|
374
|
+
const description = str(frame.description);
|
|
375
|
+
if (taskId && ref && frame.task_type === 'local_workflow') {
|
|
376
|
+
this.workflows.set(taskId, ref);
|
|
377
|
+
events.push({ type: 'workflow.progress', ref, workflow: { name: str(frame.workflow_name), taskId, phases: [], agents: [] } });
|
|
378
|
+
}
|
|
379
|
+
if (taskId && typeof frame.task_type === 'string' && BACKGROUND_TASK_TYPES.has(frame.task_type) && frame.ambient !== true) {
|
|
380
|
+
if (frame.is_backgrounded === false) {
|
|
381
|
+
this.foregroundShells.set(taskId, { ref, description });
|
|
382
|
+
} else {
|
|
383
|
+
this.startBackground(taskId, ref, frame.task_type !== 'local_bash', description, events);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// A Bash call became a task; its description is the CLI's own words for the work.
|
|
387
|
+
if (ref && description) {
|
|
388
|
+
events.push({ type: 'tool.progress', ref, startedAt: null, description });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* A command the person or the CLI sent to the background halfway, or a task that ended. */
|
|
393
|
+
private handleTaskUpdated(frame: Frame, events: BackendEvent[]): void {
|
|
394
|
+
const taskId = str(frame.task_id);
|
|
395
|
+
const patch = isRecord(frame.patch) ? frame.patch : {};
|
|
396
|
+
if (!taskId) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const shell = this.foregroundShells.get(taskId);
|
|
400
|
+
if (shell && patch.is_backgrounded === true) {
|
|
401
|
+
this.foregroundShells.delete(taskId);
|
|
402
|
+
this.startBackground(taskId, shell.ref, false, str(patch.description) ?? shell.description, events);
|
|
403
|
+
}
|
|
404
|
+
const status = str(patch.status);
|
|
405
|
+
if (status !== null && ENDED_TASK_STATUSES.has(status)) {
|
|
406
|
+
this.foregroundShells.delete(taskId);
|
|
407
|
+
this.endBackground(taskId, events);
|
|
408
|
+
this.endWorkflow(taskId, null, status === 'completed', events);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private startBackground(taskId: string, ref: string | null, monitor: boolean, description: string | null, events: BackendEvent[]): void {
|
|
413
|
+
if (this.backgroundTasks.has(taskId)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this.backgroundTasks.add(taskId);
|
|
417
|
+
events.push({ type: 'background.started', taskId, ref, monitor, description });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private endBackground(taskId: string, events: BackendEvent[]): void {
|
|
421
|
+
if (this.backgroundTasks.delete(taskId)) {
|
|
422
|
+
events.push({ type: 'background.ended', taskId });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/*
|
|
427
|
+
* A workflow that ended, told by either its notification or the task's own status, whichever comes
|
|
428
|
+
* first: the stream of a workflow run was not seen live, and both are what every other task sends.
|
|
429
|
+
*/
|
|
430
|
+
private endWorkflow(taskId: string, summary: string | null, ok: boolean, events: BackendEvent[]): void {
|
|
431
|
+
const ref = this.workflows.get(taskId);
|
|
432
|
+
if (ref === undefined) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.workflows.delete(taskId);
|
|
436
|
+
events.push({ type: 'task.done', ref, taskId, summary, ok });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* The control request that ends one task the CLI runs beside its turns. */
|
|
440
|
+
stopTaskRequest(taskId: string): unknown {
|
|
441
|
+
return { type: 'control_request', request_id: `stop-task-${taskId}-${Date.now()}`, request: { subtype: 'stop_task', task_id: taskId } };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private handleStreamEvent(frame: Frame, events: BackendEvent[]): void {
|
|
445
|
+
const event = frame.event;
|
|
446
|
+
if (!isRecord(event) || str(frame.parent_tool_use_id)) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (event.type === 'message_start') {
|
|
450
|
+
const message = isRecord(event.message) ? event.message : {};
|
|
451
|
+
this.streamMessageId = str(message.id);
|
|
452
|
+
this.streamTextCount = 0;
|
|
453
|
+
this.streamThinkingCount = 0;
|
|
454
|
+
this.streamBlocks.clear();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const index = num(event.index);
|
|
458
|
+
if (event.type === 'content_block_start' && isRecord(event.content_block) && this.streamMessageId) {
|
|
459
|
+
if (event.content_block.type === 'text') {
|
|
460
|
+
const ref = this.textRef(this.streamMessageId, this.streamTextCount++);
|
|
461
|
+
this.streamBlocks.set(index, ref);
|
|
462
|
+
events.push({ type: 'text.delta', ref, text: str(event.content_block.text) ?? '' });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (event.content_block.type === 'thinking') {
|
|
466
|
+
const ref = this.thinkingRef(this.streamMessageId, this.streamThinkingCount++);
|
|
467
|
+
this.streamBlocks.set(index, ref);
|
|
468
|
+
events.push({ type: 'thinking.delta', ref, text: str(event.content_block.thinking) ?? '' });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (event.type === 'content_block_delta' && isRecord(event.delta)) {
|
|
473
|
+
const ref = this.streamBlocks.get(index);
|
|
474
|
+
const text = str(event.delta.text) ?? str(event.delta.thinking);
|
|
475
|
+
if (!ref || !text) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (event.delta.type === 'text_delta') {
|
|
479
|
+
events.push({ type: 'text.delta', ref, text });
|
|
480
|
+
} else if (event.delta.type === 'thinking_delta') {
|
|
481
|
+
events.push({ type: 'thinking.delta', ref, text });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private handleAssistant(frame: Frame, events: BackendEvent[]): void {
|
|
487
|
+
const message = isRecord(frame.message) ? frame.message : {};
|
|
488
|
+
const messageId = str(message.id) ?? 'message';
|
|
489
|
+
const parentRef = str(frame.parent_tool_use_id);
|
|
490
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
491
|
+
const uuid = str(frame.uuid);
|
|
492
|
+
if (!parentRef && uuid) {
|
|
493
|
+
this.lastUuid = uuid;
|
|
494
|
+
}
|
|
495
|
+
if (!parentRef && typeof frame.error === 'string') {
|
|
496
|
+
this.apiError = frame.error;
|
|
497
|
+
}
|
|
498
|
+
const model = str(message.model);
|
|
499
|
+
// The CLI answers some frames itself and names them `<synthetic>`; no model wrote those.
|
|
500
|
+
if (parentRef && model && model !== '<synthetic>' && this.subagentModels.get(parentRef) !== model) {
|
|
501
|
+
this.subagentModels.set(parentRef, model);
|
|
502
|
+
events.push({ type: 'task.model', ref: parentRef, model });
|
|
503
|
+
}
|
|
504
|
+
for (const block of content) {
|
|
505
|
+
if (!isRecord(block)) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (block.type === 'text') {
|
|
509
|
+
const ordinal = this.frameTextCount.get(messageId) ?? 0;
|
|
510
|
+
this.frameTextCount.set(messageId, ordinal + 1);
|
|
511
|
+
// A subagent's text is its own report, not the thread's answer; the parent says whose row it is.
|
|
512
|
+
events.push({ type: 'text.done', ref: this.textRef(messageId, ordinal), text: str(block.text) ?? '', ...(parentRef ? { parentRef } : {}) });
|
|
513
|
+
} else if (block.type === 'thinking' && !parentRef) {
|
|
514
|
+
const ordinal = this.frameThinkingCount.get(messageId) ?? 0;
|
|
515
|
+
this.frameThinkingCount.set(messageId, ordinal + 1);
|
|
516
|
+
events.push({ type: 'thinking.done', ref: this.thinkingRef(messageId, ordinal), text: str(block.thinking) ?? '' });
|
|
517
|
+
} else if (block.type === 'tool_use') {
|
|
518
|
+
events.push({
|
|
519
|
+
type: 'tool.started',
|
|
520
|
+
ref: str(block.id) ?? `tool-${Date.now()}`,
|
|
521
|
+
name: str(block.name) ?? 'tool',
|
|
522
|
+
input: block.input ?? {},
|
|
523
|
+
parentRef
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const tokens = parentRef ? 0 : contextTokens(message.usage);
|
|
528
|
+
if (tokens > 0) {
|
|
529
|
+
events.push({ type: 'usage', contextTokens: tokens });
|
|
530
|
+
}
|
|
531
|
+
const error = str(frame.error);
|
|
532
|
+
if (error) {
|
|
533
|
+
events.push({ type: 'note', level: 'error', text: `The request failed: ${error.replaceAll('_', ' ')}` });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/*
|
|
538
|
+
* `tool_progress` carries how long the call has run (once per 30 s for a Bash under the CLI's
|
|
539
|
+
* remote gate, or a heartbeat for a slow MCP tool); the start is kept so the client can count on.
|
|
540
|
+
* That gate is `CLAUDE_CODE_REMOTE` or `CLAUDE_CODE_CONTAINER_ID`, never set here since both change other behavior.
|
|
541
|
+
*/
|
|
542
|
+
private handleToolProgress(frame: Frame, events: BackendEvent[]): void {
|
|
543
|
+
const elapsed = frame.elapsed_time_seconds;
|
|
544
|
+
const ref = str(frame.tool_use_id);
|
|
545
|
+
if (!ref || typeof elapsed !== 'number' || !Number.isFinite(elapsed) || elapsed < 0) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
events.push({ type: 'tool.progress', ref, startedAt: Date.now() - Math.round(elapsed * 1000), description: null });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private handleUser(frame: Frame, events: BackendEvent[]): void {
|
|
552
|
+
if (this.promptId !== null && frame.uuid === this.promptId) {
|
|
553
|
+
events.push({ type: 'turn.accepted', promptId: this.promptId });
|
|
554
|
+
}
|
|
555
|
+
const message = isRecord(frame.message) ? frame.message : {};
|
|
556
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
557
|
+
for (const block of content) {
|
|
558
|
+
if (!isRecord(block) || block.type !== 'tool_result') {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const ref = str(block.tool_use_id);
|
|
562
|
+
if (ref) {
|
|
563
|
+
this.subagentModels.delete(ref);
|
|
564
|
+
const output = resultText(block.content);
|
|
565
|
+
const workflow = WORKFLOW_LAUNCHED.exec(output)?.[1];
|
|
566
|
+
if (workflow) {
|
|
567
|
+
this.workflows.set(workflow, ref);
|
|
568
|
+
events.push({ type: 'workflow.progress', ref, workflow: { name: null, taskId: workflow, phases: [], agents: [] } });
|
|
569
|
+
}
|
|
570
|
+
events.push({ type: 'tool.done', ref, output, state: block.is_error === true ? 'error' : 'done' });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private handleResult(frame: Frame, events: BackendEvent[]): void {
|
|
576
|
+
const echoed = Array.isArray(frame.user_message_uuids) ? frame.user_message_uuids.filter((value): value is string => typeof value === 'string') : [];
|
|
577
|
+
const origin = str(frame.origin) ?? (isRecord(frame.origin) ? str(frame.origin.kind) : null);
|
|
578
|
+
if (this.promptId !== null && (echoed.length > 0 ? !echoed.includes(this.promptId) : origin !== null && origin !== 'human' && origin !== 'user')) {
|
|
579
|
+
if (typeof frame.total_cost_usd === 'number') {
|
|
580
|
+
events.push({ type: 'usage', costUsd: num(frame.total_cost_usd) });
|
|
581
|
+
}
|
|
582
|
+
if (frame.is_error === true) {
|
|
583
|
+
events.push({ type: 'note', level: 'warning', text: 'A background Claude turn failed; the submitted prompt is still pending.' });
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const promptId = this.promptId !== null && echoed.includes(this.promptId) ? this.promptId : null;
|
|
588
|
+
this.promptId = null;
|
|
589
|
+
const failed = frame.is_error === true || (typeof frame.subtype === 'string' && frame.subtype.startsWith('error'));
|
|
590
|
+
const errors = Array.isArray(frame.errors) ? frame.errors.filter((error): error is string => typeof error === 'string') : [];
|
|
591
|
+
const lastUuid = this.lastUuid;
|
|
592
|
+
this.lastUuid = null;
|
|
593
|
+
const limit = failed ? this.limit() : null;
|
|
594
|
+
this.apiError = null;
|
|
595
|
+
this.refusedWindows.clear();
|
|
596
|
+
events.push({
|
|
597
|
+
type: 'turn.done',
|
|
598
|
+
...(promptId === null ? {} : { promptId }),
|
|
599
|
+
state: failed ? 'error' : 'done',
|
|
600
|
+
costUsd: num(frame.total_cost_usd),
|
|
601
|
+
...(failed ? { error: errors[0] ?? str(frame.result) ?? `The turn ended with ${str(frame.subtype) ?? 'an error'}` } : {}),
|
|
602
|
+
...(lastUuid === null ? {} : { native: { lastUuid } }),
|
|
603
|
+
...(limit === null ? {} : { limit })
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private noteRefusal(info: unknown): void {
|
|
608
|
+
if (!isRecord(info) || typeof info.status !== 'string') {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const window = str(info.rateLimitType) ?? 'unknown';
|
|
612
|
+
const overageAllowed =
|
|
613
|
+
info.overageStatus === 'allowed' || info.overageStatus === 'allowed_warning' || info.isUsingOverage === true || info.overageInUse === true;
|
|
614
|
+
if (info.status === 'rejected' && !overageAllowed) {
|
|
615
|
+
const resetsAt = typeof info.resetsAt === 'number' ? info.resetsAt * 1000 : NaN;
|
|
616
|
+
this.refusedWindows.set(window, Number.isFinite(resetsAt) && resetsAt > 0 && resetsAt < 8.64e15 ? resetsAt : null);
|
|
617
|
+
} else if (info.status === 'allowed' || info.status === 'allowed_warning' || overageAllowed) {
|
|
618
|
+
this.refusedWindows.delete(window);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/*
|
|
623
|
+
* What stopped a failed turn, in the words of Claude Code 2.1.281: a window of the plan that refused
|
|
624
|
+
* the request is a usage limit, and an API error of `rate_limit` without one (a 429 the plan did
|
|
625
|
+
* not explain) or of `overloaded` is a model too busy to answer.
|
|
626
|
+
*/
|
|
627
|
+
private limit(): ChatTurnLimit | null {
|
|
628
|
+
if (this.refusedWindows.size > 0) {
|
|
629
|
+
let resetsAt = 0;
|
|
630
|
+
for (const reset of this.refusedWindows.values()) {
|
|
631
|
+
if (reset === null) {
|
|
632
|
+
return { kind: 'usage' };
|
|
633
|
+
}
|
|
634
|
+
resetsAt = Math.max(resetsAt, reset);
|
|
635
|
+
}
|
|
636
|
+
return { kind: 'usage', resetsAt };
|
|
637
|
+
}
|
|
638
|
+
if (this.apiError === 'rate_limit' || this.apiError === 'overloaded') {
|
|
639
|
+
return { kind: 'overload' };
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private handleControlRequest(frame: Frame, events: BackendEvent[]): void {
|
|
645
|
+
const request = isRecord(frame.request) ? frame.request : {};
|
|
646
|
+
const requestId = str(frame.request_id);
|
|
647
|
+
if (!requestId || request.subtype !== 'can_use_tool') {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const toolUseId = str(request.tool_use_id);
|
|
651
|
+
const toolName = str(request.tool_name) ?? 'tool';
|
|
652
|
+
const background = Boolean(str(request.agent_id));
|
|
653
|
+
if (toolName === 'AskUserQuestion') {
|
|
654
|
+
const questions = parseQuestions(request.input);
|
|
655
|
+
if (questions.length === 0) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
this.pending.set(requestId, { type: 'question', toolUseId, input: request.input });
|
|
659
|
+
events.push({ type: 'question.requested', requestId, questions, ...(background ? { background: true } : {}) });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const suggestions = Array.isArray(request.permission_suggestions) ? request.permission_suggestions : [];
|
|
663
|
+
this.pending.set(requestId, { type: 'approval', toolUseId, input: request.input ?? {}, suggestions });
|
|
664
|
+
events.push({
|
|
665
|
+
type: 'approval.requested',
|
|
666
|
+
...(background ? { background: true } : {}),
|
|
667
|
+
requestId,
|
|
668
|
+
ref: toolUseId,
|
|
669
|
+
toolName,
|
|
670
|
+
input: request.input ?? {},
|
|
671
|
+
description: str(request.description),
|
|
672
|
+
canAllowAlways: suggestions.length > 0,
|
|
673
|
+
...(suggestions.length > 0 ? { allowAlways: { label: 'Allow with these rules', description: JSON.stringify(suggestions, null, 2) } } : {})
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private textRef(messageId: string, ordinal: number): string {
|
|
678
|
+
return `${messageId}:t${ordinal}`;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private thinkingRef(messageId: string, ordinal: number): string {
|
|
682
|
+
return `${messageId}:k${ordinal}`;
|
|
683
|
+
}
|
|
684
|
+
}
|