@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,872 @@
|
|
|
1
|
+
import type { ChatFileChange, ChatQuestion, ChatTurnLimit, UsageWindow } from '@adecore/agent-contracts';
|
|
2
|
+
import { readCodexLimits } from '../usage/limits/normalize.ts';
|
|
3
|
+
import { codexRuntimeMode } from '../providers/codex.ts';
|
|
4
|
+
import type { ApprovalDecision, BackendEvent } from './backend.ts';
|
|
5
|
+
import type { CodexFrame } from './codex-transport.ts';
|
|
6
|
+
import { cleanTitle } from '../title-file.ts';
|
|
7
|
+
|
|
8
|
+
type CodexRpcId = number | string;
|
|
9
|
+
|
|
10
|
+
/* How an answer reaches the app-server, as the reply to its request or as a steer into the turn. */
|
|
11
|
+
type CodexAnswer = { kind: 'respond'; rpcId: CodexRpcId; result: unknown } | { kind: 'steer'; text: string };
|
|
12
|
+
|
|
13
|
+
type Frame = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is Frame {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function str(value: unknown): string | null {
|
|
20
|
+
return typeof value === 'string' ? value : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function num(value: unknown): number {
|
|
24
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Codex wraps every command in a login shell; the person wants to read the command, not the wrapper.
|
|
28
|
+
const SHELL_WRAPPER = /^(?:\/bin\/|\/usr\/bin\/)?(?:zsh|bash|sh) -lc (.*)$/s;
|
|
29
|
+
|
|
30
|
+
export function unwrapCommand(command: string): string {
|
|
31
|
+
const match = SHELL_WRAPPER.exec(command);
|
|
32
|
+
if (!match) {
|
|
33
|
+
return command;
|
|
34
|
+
}
|
|
35
|
+
const inner = match[1]!;
|
|
36
|
+
const quoted = /^(["'])([\s\S]*)\1$/.exec(inner);
|
|
37
|
+
return quoted ? quoted[2]! : inner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function textOf(content: unknown): string {
|
|
41
|
+
if (typeof content === 'string') {
|
|
42
|
+
return content;
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(content)) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
return content
|
|
48
|
+
.map((block) => (isRecord(block) ? (str(block.text) ?? null) : null))
|
|
49
|
+
.filter((text): text is string => text !== null)
|
|
50
|
+
.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// What `agentsStates` says of an agent that stopped working; the call that reports it may have completed long before.
|
|
54
|
+
const ENDED_AGENT_STATES = new Set(['completed', 'errored', 'interrupted', 'shutdown', 'notFound']);
|
|
55
|
+
|
|
56
|
+
/* What became of every agent a collab call touched, one line each; empty while the call still runs. */
|
|
57
|
+
function collabAgentOutput(states: unknown): string {
|
|
58
|
+
if (!isRecord(states)) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
return Object.entries(states)
|
|
62
|
+
.map(([thread, state]) => {
|
|
63
|
+
const status = isRecord(state) ? (str(state.status) ?? 'unknown') : 'unknown';
|
|
64
|
+
const message = isRecord(state) ? str(state.message) : null;
|
|
65
|
+
return message ? `${thread}: ${status}, ${message}` : `${thread}: ${status}`;
|
|
66
|
+
})
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* A blocking `request_user_input` question, as far as the person needs to see it. */
|
|
71
|
+
function parseBlockingQuestions(questions: unknown): ChatQuestion[] {
|
|
72
|
+
if (!Array.isArray(questions)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return questions.filter(isRecord).map((question, index) => ({
|
|
76
|
+
id: str(question.id) ?? String(index),
|
|
77
|
+
header: str(question.header) ?? '',
|
|
78
|
+
question: str(question.question) ?? '',
|
|
79
|
+
choices: Array.isArray(question.options)
|
|
80
|
+
? question.options.filter(isRecord).map((option) => ({ label: str(option.label) ?? '', description: str(option.description) ?? '' }))
|
|
81
|
+
: [],
|
|
82
|
+
multiSelect: false
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* The questions an agent message carries when Codex asks without blocking the turn. */
|
|
87
|
+
function parseAsyncQuestions(questions: unknown): ChatQuestion[] {
|
|
88
|
+
if (!Array.isArray(questions)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return questions.filter(isRecord).map((question, index) => ({
|
|
92
|
+
id: String(index),
|
|
93
|
+
header: '',
|
|
94
|
+
question: str(question.title) ?? '',
|
|
95
|
+
choices: Array.isArray(question.options)
|
|
96
|
+
? question.options.filter((option): option is string => typeof option === 'string').map((option) => ({ label: option, description: '' }))
|
|
97
|
+
: [],
|
|
98
|
+
multiSelect: false
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toLines(value: unknown): string[] {
|
|
103
|
+
return Array.isArray(value) ? value.filter((line): line is string => typeof line === 'string' && line.trim() !== '') : [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const CHANGE_KINDS = new Set<ChatFileChange['kind']>(['add', 'update', 'delete']);
|
|
107
|
+
|
|
108
|
+
/* The files a `fileChange` item touches, with the unified diff Codex reports per file. */
|
|
109
|
+
function parseChanges(value: unknown): ChatFileChange[] {
|
|
110
|
+
if (!Array.isArray(value)) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return value.filter(isRecord).map((change) => {
|
|
114
|
+
const kind = isRecord(change.kind) ? str(change.kind.type) : str(change.kind);
|
|
115
|
+
return {
|
|
116
|
+
path: str(change.path) ?? '',
|
|
117
|
+
kind: kind !== null && CHANGE_KINDS.has(kind as ChatFileChange['kind']) ? (kind as ChatFileChange['kind']) : 'update',
|
|
118
|
+
diff: str(change.diff) ?? ''
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Partial command output arrives as text or as the raw bytes of it, depending on the build.
|
|
124
|
+
function outputChunk(params: Frame): string | null {
|
|
125
|
+
const text = str(params.delta) ?? str(params.chunk);
|
|
126
|
+
if (text !== null) {
|
|
127
|
+
return text;
|
|
128
|
+
}
|
|
129
|
+
const bytes = params.chunk ?? params.delta;
|
|
130
|
+
if (!Array.isArray(bytes)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return new TextDecoder().decode(Uint8Array.from(bytes.filter((byte): byte is number => typeof byte === 'number')));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type RequestOwner = { threadId: string | null; turnId: string | null };
|
|
137
|
+
|
|
138
|
+
type Pending = RequestOwner &
|
|
139
|
+
(
|
|
140
|
+
| { type: 'approval'; rpcId: CodexRpcId; kind: 'command' | 'fileChange'; amendment: string[] | null; decisions: string[] }
|
|
141
|
+
| { type: 'question'; rpcId: CodexRpcId | null; questionIds: string[] }
|
|
142
|
+
| { type: 'elicitation'; rpcId: CodexRpcId; persist: ElicitationPersist | null }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
type ElicitationPersist = 'always' | 'session';
|
|
146
|
+
|
|
147
|
+
/* A form that asks for no fields is a yes or no, which is all an approval card can answer. */
|
|
148
|
+
function isConfirmation(params: Frame): boolean {
|
|
149
|
+
return (
|
|
150
|
+
params.mode === 'form' &&
|
|
151
|
+
isRecord(params.requestedSchema) &&
|
|
152
|
+
Object.keys(isRecord(params.requestedSchema.properties) ? params.requestedSchema.properties : {}).length === 0
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* The widest remembering the MCP server offers, which the one "always" button of a card stands for. */
|
|
157
|
+
function elicitationPersist(meta: Frame): ElicitationPersist | null {
|
|
158
|
+
const offered = Array.isArray(meta.persist) ? meta.persist : [];
|
|
159
|
+
return offered.includes('always') ? 'always' : offered.includes('session') ? 'session' : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* The arguments as the MCP server shows them to a person, keyed by name, so the card sums up the call. */
|
|
163
|
+
function elicitationInput(meta: Frame): Frame {
|
|
164
|
+
const input: Frame = {};
|
|
165
|
+
for (const param of Array.isArray(meta.tool_params_display) ? meta.tool_params_display : []) {
|
|
166
|
+
if (isRecord(param) && typeof param.name === 'string') {
|
|
167
|
+
input[param.name] = param.value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return input;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Every spent window must reset; one unknown reset keeps resuming a person's choice.
|
|
174
|
+
function spentUntil(windows: readonly UsageWindow[]): number | null {
|
|
175
|
+
const spent = windows.filter((window) => window.used >= 1);
|
|
176
|
+
if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return Math.max(...spent.map((window) => window.resetsAt!));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* The `codex app-server` protocol, in and out. Notifications and the server's own requests become
|
|
184
|
+
* backend events, and an answer becomes the reply or the steer that settles one. One instance belongs
|
|
185
|
+
* to one process; request ids start at 0 in every process, so they carry the generation.
|
|
186
|
+
*/
|
|
187
|
+
export class CodexProtocol {
|
|
188
|
+
private readonly generation: number;
|
|
189
|
+
private readonly pending = new Map<string, Pending>();
|
|
190
|
+
// The input the thread shows for an item, so an approval about it can repeat what it is for.
|
|
191
|
+
private readonly toolInputs = new Map<string, RequestOwner & { input: unknown; changes: ChatFileChange[] }>();
|
|
192
|
+
private codexTurnId: string | null = null;
|
|
193
|
+
// Command items with a terminal session of their own, by item id, to the process id that ending one names.
|
|
194
|
+
private readonly openTerminals = new Map<string, string>();
|
|
195
|
+
private readonly backgroundTerminals = new Map<string, string>();
|
|
196
|
+
// When the plan's spent window lifts, from its last rate limit update; null while no window is spent.
|
|
197
|
+
private spentUntil: number | null = null;
|
|
198
|
+
// The chat's own thread. A spawned agent's thread streams over the same connection, and none of it is the chat's.
|
|
199
|
+
private ownThreadId: string | null = null;
|
|
200
|
+
// Per agent thread, the ref of the row that stands for it and whether that row has settled.
|
|
201
|
+
private readonly agentRows = new Map<string, { ref: string; label: string; settled: boolean; interaction?: string }>();
|
|
202
|
+
|
|
203
|
+
constructor(generation: number) {
|
|
204
|
+
this.generation = generation;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* The turn Codex is running, what `turn/interrupt` needs. */
|
|
208
|
+
get turnId(): string | null {
|
|
209
|
+
return this.codexTurnId;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* The result of `thread/start` or `thread/resume`. The thread id is what a terminal resumes. */
|
|
213
|
+
threadReady(result: unknown): BackendEvent[] {
|
|
214
|
+
const thread = isRecord(result) && isRecord(result.thread) ? result.thread : {};
|
|
215
|
+
this.ownThreadId = str(thread.id) ?? this.ownThreadId;
|
|
216
|
+
const policy = isRecord(result) ? str(result.approvalPolicy) : null;
|
|
217
|
+
const sandbox = isRecord(result) && isRecord(result.sandbox) ? str(result.sandbox.type) : null;
|
|
218
|
+
const effectiveRuntimeMode = policy && sandbox ? codexRuntimeMode(policy, sandbox) : undefined;
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
type: 'session',
|
|
222
|
+
agentSessionId: str(thread.id),
|
|
223
|
+
model: (isRecord(result) ? str(result.model) : null) ?? str(thread.model),
|
|
224
|
+
title: cleanTitle(thread.name),
|
|
225
|
+
...(policy && sandbox ? { permissionMode: `${policy} / ${sandbox}` } : {}),
|
|
226
|
+
...(effectiveRuntimeMode === undefined ? {} : { effectiveRuntimeMode })
|
|
227
|
+
}
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
handle(frame: CodexFrame): BackendEvent[] {
|
|
232
|
+
const events: BackendEvent[] = [];
|
|
233
|
+
if (typeof frame.method !== 'string') {
|
|
234
|
+
return events;
|
|
235
|
+
}
|
|
236
|
+
const params = isRecord(frame.params) ? frame.params : {};
|
|
237
|
+
if (frame.id !== undefined) {
|
|
238
|
+
this.handleServerRequest(frame.method, frame.id as CodexRpcId, params, events);
|
|
239
|
+
return events;
|
|
240
|
+
}
|
|
241
|
+
if (frame.method === 'serverRequest/resolved') {
|
|
242
|
+
const rpcId = params.requestId;
|
|
243
|
+
if (typeof rpcId === 'number' || typeof rpcId === 'string') {
|
|
244
|
+
const requestId = this.requestId(rpcId);
|
|
245
|
+
this.pending.delete(requestId);
|
|
246
|
+
events.push({ type: 'request.withdrawn', requestId });
|
|
247
|
+
}
|
|
248
|
+
return events;
|
|
249
|
+
}
|
|
250
|
+
if (frame.method === 'item/started' || frame.method === 'item/completed') {
|
|
251
|
+
this.rememberPatch(params);
|
|
252
|
+
}
|
|
253
|
+
// Codex 0.156.1 streams a spawned agent's turns, items and usage here too, under that agent's thread id.
|
|
254
|
+
const threadId = frame.method === 'thread/started' ? (isRecord(params.thread) ? str(params.thread.id) : null) : str(params.threadId);
|
|
255
|
+
if (threadId !== null && this.ownThreadId !== null && threadId !== this.ownThreadId) {
|
|
256
|
+
if (frame.method === 'turn/completed') {
|
|
257
|
+
this.withdrawOwner(threadId, isRecord(params.turn) ? str(params.turn.id) : null, events);
|
|
258
|
+
} else if (frame.method === 'thread/closed') {
|
|
259
|
+
this.withdrawOwner(threadId, null, events);
|
|
260
|
+
}
|
|
261
|
+
return events;
|
|
262
|
+
}
|
|
263
|
+
switch (frame.method) {
|
|
264
|
+
case 'thread/started':
|
|
265
|
+
this.ownThreadId = threadId ?? this.ownThreadId;
|
|
266
|
+
events.push({ type: 'session', agentSessionId: threadId, model: null });
|
|
267
|
+
break;
|
|
268
|
+
case 'thread/name/updated': {
|
|
269
|
+
const title = cleanTitle(params.threadName);
|
|
270
|
+
if (title !== null) {
|
|
271
|
+
events.push({ type: 'title', title });
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case 'turn/started':
|
|
276
|
+
this.codexTurnId = isRecord(params.turn) ? str(params.turn.id) : null;
|
|
277
|
+
break;
|
|
278
|
+
case 'turn/completed':
|
|
279
|
+
this.handleTurnCompleted(params, events);
|
|
280
|
+
break;
|
|
281
|
+
case 'thread/closed':
|
|
282
|
+
this.withdrawOwner(threadId ?? this.ownThreadId, null, events);
|
|
283
|
+
break;
|
|
284
|
+
case 'item/started':
|
|
285
|
+
case 'item/completed':
|
|
286
|
+
this.handleItem(params, frame.method === 'item/completed', events);
|
|
287
|
+
break;
|
|
288
|
+
case 'item/agentMessage/delta': {
|
|
289
|
+
const ref = str(params.itemId);
|
|
290
|
+
const text = str(params.delta);
|
|
291
|
+
if (ref && text) {
|
|
292
|
+
events.push({ type: 'text.delta', ref, text });
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case 'item/reasoning/summaryTextDelta':
|
|
297
|
+
case 'item/reasoning/textDelta': {
|
|
298
|
+
const ref = str(params.itemId);
|
|
299
|
+
const text = str(params.delta);
|
|
300
|
+
if (ref && text) {
|
|
301
|
+
events.push({ type: 'thinking.delta', ref, text });
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
case 'item/reasoning/summaryPartAdded': {
|
|
306
|
+
// A new part of the same summary; without a break the parts run into each other.
|
|
307
|
+
const ref = str(params.itemId);
|
|
308
|
+
if (ref && num(params.summaryIndex) > 0) {
|
|
309
|
+
events.push({ type: 'thinking.delta', ref, text: '\n\n' });
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'item/commandExecution/outputDelta': {
|
|
314
|
+
const ref = str(params.itemId);
|
|
315
|
+
const text = outputChunk(params);
|
|
316
|
+
if (ref && text) {
|
|
317
|
+
events.push({ type: 'tool.output', ref, text });
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
case 'thread/tokenUsage/updated': {
|
|
322
|
+
const usage = isRecord(params.tokenUsage) ? params.tokenUsage : {};
|
|
323
|
+
const last = isRecord(usage.last) ? usage.last : {};
|
|
324
|
+
const contextWindow = num(usage.modelContextWindow);
|
|
325
|
+
events.push({
|
|
326
|
+
type: 'usage',
|
|
327
|
+
contextTokens: num(last.totalTokens),
|
|
328
|
+
...(contextWindow > 0 ? { contextWindow } : {})
|
|
329
|
+
});
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'error': {
|
|
333
|
+
const error = isRecord(params.error) ? params.error : {};
|
|
334
|
+
events.push({ type: 'note', level: params.willRetry === true ? 'warning' : 'error', text: str(error.message) ?? 'Codex reported an error' });
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 'account/rateLimits/updated': {
|
|
338
|
+
// The plan's own numbers, sent beside a token usage tick; the usage monitor keeps them.
|
|
339
|
+
const reading = readCodexLimits(isRecord(params.rateLimits) ? params.rateLimits : params);
|
|
340
|
+
if (reading !== null) {
|
|
341
|
+
this.spentUntil = spentUntil(reading.windows);
|
|
342
|
+
if (reading.windows.length > 0) {
|
|
343
|
+
events.push({ type: 'limits', update: { kind: 'codex', plan: reading.plan, windows: reading.windows } });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
case 'model/rerouted': {
|
|
349
|
+
const model = str(params.toModel);
|
|
350
|
+
if (model) {
|
|
351
|
+
events.push({ type: 'model', model });
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
default:
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
return events;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* The reply that settles an approval, or null when nothing waits under that id. */
|
|
362
|
+
approvalDecision(requestId: string, decision: ApprovalDecision): { rpcId: CodexRpcId; result: unknown } | null {
|
|
363
|
+
const pending = this.pending.get(requestId);
|
|
364
|
+
if (pending?.type === 'elicitation') {
|
|
365
|
+
this.pending.delete(requestId);
|
|
366
|
+
return { rpcId: pending.rpcId, result: elicitationAnswer(decision, pending.persist) };
|
|
367
|
+
}
|
|
368
|
+
if (pending?.type !== 'approval') {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
this.pending.delete(requestId);
|
|
372
|
+
return { rpcId: pending.rpcId, result: { decision: codexDecision(decision, pending) } };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* What answers a question, the reply to a blocking one or the text that steers a running turn. */
|
|
376
|
+
questionAnswer(requestId: string, answers: Record<string, string>): CodexAnswer | null {
|
|
377
|
+
const pending = this.pending.get(requestId);
|
|
378
|
+
if (pending?.type !== 'question') {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
this.pending.delete(requestId);
|
|
382
|
+
const given = pending.questionIds.map((id) => answers[id]).filter((answer): answer is string => answer !== undefined);
|
|
383
|
+
if (pending.rpcId === null) {
|
|
384
|
+
// An async question is answered like a message from the person, into the running turn.
|
|
385
|
+
return { kind: 'steer', text: given.join('\n') };
|
|
386
|
+
}
|
|
387
|
+
const byId: Record<string, { answers: string[] }> = {};
|
|
388
|
+
for (const id of pending.questionIds) {
|
|
389
|
+
const answer = answers[id];
|
|
390
|
+
if (answer !== undefined) {
|
|
391
|
+
byId[id] = { answers: [answer] };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { kind: 'respond', rpcId: pending.rpcId, result: { answers: byId } };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Drops an asynchronous question the person walked away from; a blocking one has to be answered. */
|
|
398
|
+
dismissQuestion(requestId: string): boolean {
|
|
399
|
+
const pending = this.pending.get(requestId);
|
|
400
|
+
if (pending?.type !== 'question' || pending.rpcId !== null) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
this.pending.delete(requestId);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
forgetPending(): void {
|
|
408
|
+
this.pending.clear();
|
|
409
|
+
this.toolInputs.clear();
|
|
410
|
+
this.agentRows.clear();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
declineRequest(requestId: string): { rpcId: CodexRpcId; result: unknown } | null {
|
|
414
|
+
const pending = this.pending.get(requestId);
|
|
415
|
+
if (!pending) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
if (pending.type !== 'question') {
|
|
419
|
+
return this.approvalDecision(requestId, 'deny');
|
|
420
|
+
}
|
|
421
|
+
this.pending.delete(requestId);
|
|
422
|
+
return pending.rpcId === null ? null : { rpcId: pending.rpcId, result: { answers: {} } };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private owner(params: Frame): RequestOwner {
|
|
426
|
+
const threadId = str(params.threadId) ?? this.ownThreadId;
|
|
427
|
+
return { threadId, turnId: str(params.turnId) ?? (threadId === this.ownThreadId ? this.codexTurnId : null) };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private toolKey(threadId: string | null, itemId: string): string {
|
|
431
|
+
return JSON.stringify([threadId, itemId]);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private rememberPatch(params: Frame): void {
|
|
435
|
+
const item = isRecord(params.item) ? params.item : null;
|
|
436
|
+
if (item?.type !== 'fileChange' || typeof item.id !== 'string') {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const owner = this.owner(params);
|
|
440
|
+
const changes = parseChanges(item.changes);
|
|
441
|
+
this.toolInputs.set(this.toolKey(owner.threadId, item.id), { ...owner, input: { summary: changes.map((change) => change.path).join(', ') }, changes });
|
|
442
|
+
// An approval copies its input into the event, so evicting cached metadata cannot change an open card.
|
|
443
|
+
if (this.toolInputs.size > 512) {
|
|
444
|
+
this.toolInputs.delete(this.toolInputs.keys().next().value!);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private withdrawOwner(threadId: string | null, turnId: string | null, events: BackendEvent[]): void {
|
|
449
|
+
const belongs = (owner: RequestOwner): boolean => owner.threadId === threadId && (turnId === null || owner.turnId === null || owner.turnId === turnId);
|
|
450
|
+
for (const [requestId, pending] of this.pending) {
|
|
451
|
+
if (belongs(pending)) {
|
|
452
|
+
this.pending.delete(requestId);
|
|
453
|
+
events.push({ type: 'request.withdrawn', requestId });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const [key, input] of this.toolInputs) {
|
|
457
|
+
if (belongs(input)) {
|
|
458
|
+
this.toolInputs.delete(key);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private isChildRequest(params: Frame): boolean {
|
|
464
|
+
const threadId = str(params.threadId);
|
|
465
|
+
return threadId !== null && this.ownThreadId !== null && threadId !== this.ownThreadId;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private requestDescription(params: Frame, description: string | null): string | null {
|
|
469
|
+
if (!this.isChildRequest(params)) {
|
|
470
|
+
return description;
|
|
471
|
+
}
|
|
472
|
+
const threadId = str(params.threadId)!;
|
|
473
|
+
const label = this.agentRows.get(threadId)?.label || threadId;
|
|
474
|
+
return [`Requested by subagent ${label}.`, description].filter(Boolean).join(' ');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private requestId(rpcId: CodexRpcId): string {
|
|
478
|
+
return `${this.generation}-${rpcId}`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private handleServerRequest(method: string, rpcId: CodexRpcId, params: Frame, events: BackendEvent[]): void {
|
|
482
|
+
const requestId = this.requestId(rpcId);
|
|
483
|
+
if (method === 'item/tool/requestUserInput') {
|
|
484
|
+
const questions = parseBlockingQuestions(params.questions);
|
|
485
|
+
if (questions.length === 0) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.pending.set(requestId, { ...this.owner(params), type: 'question', rpcId, questionIds: questions.map((question) => question.id) });
|
|
489
|
+
events.push({ type: 'question.requested', requestId, questions, ...(this.isChildRequest(params) ? { background: true } : {}) });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (method === 'mcpServer/elicitation/request') {
|
|
493
|
+
this.elicitation(requestId, rpcId, params, events);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const kind = method === 'item/commandExecution/requestApproval' ? 'command' : method === 'item/fileChange/requestApproval' ? 'fileChange' : null;
|
|
497
|
+
if (!kind) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const itemId = str(params.itemId);
|
|
501
|
+
const known = itemId ? this.toolInputs.get(this.toolKey(this.owner(params).threadId, itemId)) : undefined;
|
|
502
|
+
const decisions = Array.isArray(params.availableDecisions)
|
|
503
|
+
? params.availableDecisions.map((decision) =>
|
|
504
|
+
typeof decision === 'string' ? decision : (Object.keys(isRecord(decision) ? decision : {})[0] ?? '')
|
|
505
|
+
)
|
|
506
|
+
: [];
|
|
507
|
+
const amendment = Array.isArray(params.proposedExecpolicyAmendment)
|
|
508
|
+
? params.proposedExecpolicyAmendment.filter((word): word is string => typeof word === 'string')
|
|
509
|
+
: null;
|
|
510
|
+
// The person must see what they are approving, so a file change repeats its diffs here.
|
|
511
|
+
const input =
|
|
512
|
+
kind === 'command'
|
|
513
|
+
? { command: unwrapCommand(str(params.command) ?? ''), cwd: str(params.cwd) ?? undefined }
|
|
514
|
+
: { ...(isRecord(known?.input) ? known.input : { itemId }), changes: known?.changes ?? [] };
|
|
515
|
+
this.pending.set(requestId, {
|
|
516
|
+
...this.owner(params),
|
|
517
|
+
type: 'approval',
|
|
518
|
+
rpcId,
|
|
519
|
+
kind,
|
|
520
|
+
amendment: amendment && amendment.length > 0 ? amendment : null,
|
|
521
|
+
decisions
|
|
522
|
+
});
|
|
523
|
+
events.push({
|
|
524
|
+
type: 'approval.requested',
|
|
525
|
+
requestId,
|
|
526
|
+
ref: itemId,
|
|
527
|
+
toolName: kind === 'command' ? 'Bash' : 'ApplyPatch',
|
|
528
|
+
input,
|
|
529
|
+
description: this.requestDescription(params, str(params.reason)),
|
|
530
|
+
canAllowAlways: (amendment !== null && amendment.length > 0) || decisions.includes('acceptForSession'),
|
|
531
|
+
...(this.isChildRequest(params) ? { background: true } : {}),
|
|
532
|
+
...(amendment && amendment.length > 0
|
|
533
|
+
? {
|
|
534
|
+
allowAlways: {
|
|
535
|
+
label: 'Allow this command prefix',
|
|
536
|
+
description: `Allow future commands matching this prefix: ${JSON.stringify(amendment)}`
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
: decisions.includes('acceptForSession')
|
|
540
|
+
? { allowAlways: { label: 'Allow for this session', description: 'Allow this provider permission for the rest of the session.' } }
|
|
541
|
+
: {})
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/*
|
|
546
|
+
* An MCP server asking the person something, such as computer use asking before it touches an app.
|
|
547
|
+
* Only a yes or no becomes a card; a form with fields or a link stays unanswered, which Codex reads as a decline.
|
|
548
|
+
*/
|
|
549
|
+
private elicitation(requestId: string, rpcId: CodexRpcId, params: Frame, events: BackendEvent[]): void {
|
|
550
|
+
if (!isConfirmation(params)) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const meta = isRecord(params._meta) ? params._meta : {};
|
|
554
|
+
const persist = elicitationPersist(meta);
|
|
555
|
+
this.pending.set(requestId, { ...this.owner(params), type: 'elicitation', rpcId, persist });
|
|
556
|
+
events.push({
|
|
557
|
+
type: 'approval.requested',
|
|
558
|
+
requestId,
|
|
559
|
+
ref: str(meta.callId),
|
|
560
|
+
toolName: str(meta.connector_name) ?? str(params.serverName) ?? 'MCP',
|
|
561
|
+
input: elicitationInput(meta),
|
|
562
|
+
description: this.requestDescription(params, str(params.message)),
|
|
563
|
+
canAllowAlways: persist !== null,
|
|
564
|
+
...(this.isChildRequest(params) ? { background: true } : {}),
|
|
565
|
+
...(persist === 'always'
|
|
566
|
+
? { allowAlways: { label: 'Always allow', description: 'Codex remembers this approval, also in later chats.' } }
|
|
567
|
+
: persist === 'session'
|
|
568
|
+
? { allowAlways: { label: 'Allow for this session', description: 'Codex remembers this approval for the rest of the session.' } }
|
|
569
|
+
: {})
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private handleItem(params: Frame, completed: boolean, events: BackendEvent[]): void {
|
|
574
|
+
const item = isRecord(params.item) ? params.item : null;
|
|
575
|
+
const ref = item ? str(item.id) : null;
|
|
576
|
+
if (!item || !ref) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
switch (item.type) {
|
|
580
|
+
case 'agentMessage': {
|
|
581
|
+
const questions = parseAsyncQuestions(item.questions);
|
|
582
|
+
if (questions.length > 0) {
|
|
583
|
+
// Codex keeps polling for an answer to this one; the id it carries is the item's own.
|
|
584
|
+
if (!completed || this.pending.has(ref)) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
this.pending.set(ref, { ...this.owner(params), type: 'question', rpcId: null, questionIds: questions.map((question) => question.id) });
|
|
588
|
+
events.push({ type: 'question.requested', requestId: ref, questions, async: true });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.text(ref, str(item.text) ?? '', completed, events);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
case 'plan':
|
|
595
|
+
this.text(`plan-${ref}`, str(item.text) ?? '', completed, events);
|
|
596
|
+
return;
|
|
597
|
+
case 'reasoning': {
|
|
598
|
+
// The deltas already streamed this; the completed item is what a resumed thread replays.
|
|
599
|
+
const lines = [...toLines(item.summary), ...toLines(item.content)];
|
|
600
|
+
if (completed && lines.length > 0) {
|
|
601
|
+
events.push({ type: 'thinking.done', ref, text: lines.join('\n\n') });
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
case 'commandExecution': {
|
|
606
|
+
const output = str(item.aggregatedOutput) ?? '';
|
|
607
|
+
const succeeded = str(item.status) === 'completed';
|
|
608
|
+
// Its turn is long over, so only the row learns how it ended; a start again would open a turn nobody ends.
|
|
609
|
+
const backgroundId = this.backgroundTerminals.get(ref);
|
|
610
|
+
if (completed && backgroundId !== undefined) {
|
|
611
|
+
this.backgroundTerminals.delete(ref);
|
|
612
|
+
events.push({ type: 'tool.done', ref, output, state: succeeded ? 'done' : 'error' });
|
|
613
|
+
events.push({ type: 'background.ended', taskId: backgroundId });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const processId = str(item.processId);
|
|
617
|
+
if (!completed && processId !== null) {
|
|
618
|
+
this.openTerminals.set(ref, processId);
|
|
619
|
+
} else {
|
|
620
|
+
this.openTerminals.delete(ref);
|
|
621
|
+
}
|
|
622
|
+
this.tool(
|
|
623
|
+
ref,
|
|
624
|
+
'Bash',
|
|
625
|
+
{ command: unwrapCommand(str(item.command) ?? ''), cwd: str(item.cwd) ?? undefined },
|
|
626
|
+
completed,
|
|
627
|
+
output,
|
|
628
|
+
succeeded,
|
|
629
|
+
events
|
|
630
|
+
);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
case 'fileChange': {
|
|
634
|
+
const changes = parseChanges(item.changes);
|
|
635
|
+
this.tool(
|
|
636
|
+
ref,
|
|
637
|
+
'ApplyPatch',
|
|
638
|
+
{ summary: changes.map((change) => change.path).join(', ') },
|
|
639
|
+
completed,
|
|
640
|
+
changes.map((change) => change.diff).join('\n'),
|
|
641
|
+
str(item.status) === 'completed',
|
|
642
|
+
events,
|
|
643
|
+
changes
|
|
644
|
+
);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
case 'mcpToolCall': {
|
|
648
|
+
const result = isRecord(item.result) ? textOf(item.result.content) : '';
|
|
649
|
+
const error = isRecord(item.error) ? (str(item.error.message) ?? '') : '';
|
|
650
|
+
this.tool(
|
|
651
|
+
ref,
|
|
652
|
+
`${str(item.server) ?? 'mcp'}/${str(item.tool) ?? 'tool'}`,
|
|
653
|
+
item.arguments ?? {},
|
|
654
|
+
completed,
|
|
655
|
+
error || result,
|
|
656
|
+
str(item.status) === 'completed',
|
|
657
|
+
events
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
case 'dynamicToolCall':
|
|
662
|
+
this.tool(ref, str(item.tool) ?? 'tool', item.arguments ?? {}, completed, textOf(item.contentItems), item.success !== false, events);
|
|
663
|
+
return;
|
|
664
|
+
case 'webSearch': {
|
|
665
|
+
const action = isRecord(item.action) ? item.action : {};
|
|
666
|
+
this.tool(ref, 'WebSearch', { query: str(item.query) ?? str(action.query) ?? '' }, completed, '', true, events);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
case 'collabAgentToolCall': {
|
|
670
|
+
// Codex's own multi-agent calls; spawnAgent is the one handled as a delegation.
|
|
671
|
+
const tool = str(item.tool) ?? 'collabAgent';
|
|
672
|
+
const input = { tool, prompt: str(item.prompt) ?? undefined, model: str(item.model) ?? undefined, threads: item.receiverThreadIds ?? [] };
|
|
673
|
+
if (tool === 'spawnAgent') {
|
|
674
|
+
this.spawnedAgent(ref, item, input, events);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
this.tool(ref, tool, input, completed, collabAgentOutput(item.agentsStates), str(item.status) === 'completed', events);
|
|
678
|
+
this.settleEndedAgents(item.agentsStates, events);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
case 'subAgentActivity':
|
|
682
|
+
this.subAgentActivity(item, events);
|
|
683
|
+
return;
|
|
684
|
+
case 'contextCompaction':
|
|
685
|
+
if (completed) {
|
|
686
|
+
events.push({ type: 'compaction', preTokens: null });
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
default:
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private handleTurnCompleted(params: Frame, events: BackendEvent[]): void {
|
|
695
|
+
const turn = isRecord(params.turn) ? params.turn : {};
|
|
696
|
+
const status = str(turn.status);
|
|
697
|
+
const error = isRecord(turn.error) ? str(turn.error.message) : null;
|
|
698
|
+
const limit = status === 'failed' && isRecord(turn.error) ? this.limitOf(turn.error.codexErrorInfo) : null;
|
|
699
|
+
const turnId = str(turn.id) ?? this.codexTurnId;
|
|
700
|
+
this.codexTurnId = null;
|
|
701
|
+
this.withdrawOwner(str(params.threadId) ?? this.ownThreadId, turnId, events);
|
|
702
|
+
// A terminal session still open as the turn ends is one Codex left running; it only completes once its process does.
|
|
703
|
+
for (const [ref, processId] of this.openTerminals) {
|
|
704
|
+
this.backgroundTerminals.set(ref, processId);
|
|
705
|
+
events.push({ type: 'background.started', taskId: processId, ref, monitor: false, description: null });
|
|
706
|
+
}
|
|
707
|
+
this.openTerminals.clear();
|
|
708
|
+
events.push({
|
|
709
|
+
type: 'turn.done',
|
|
710
|
+
state: status === 'interrupted' ? 'aborted' : status === 'failed' ? 'error' : 'done',
|
|
711
|
+
// Codex reports no cost; the turn keeps a zero so the fold label can still say how long it took.
|
|
712
|
+
costUsd: 0,
|
|
713
|
+
...(status === 'failed' ? { error: error ?? 'The turn failed' } : {}),
|
|
714
|
+
...(turnId === null ? {} : { native: { turnId } }),
|
|
715
|
+
...(limit === null ? {} : { limit })
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/*
|
|
720
|
+
* The `codexErrorInfo` of a failed turn, as Codex 0.156.1 names it: `usageLimitExceeded` is the plan's
|
|
721
|
+
* limit, with the reset of the window its last update showed spent, and `serverOverloaded` or
|
|
722
|
+
* `rateLimitExceeded` a model too busy to answer now.
|
|
723
|
+
*/
|
|
724
|
+
private limitOf(info: unknown): ChatTurnLimit | null {
|
|
725
|
+
if (info === 'usageLimitExceeded') {
|
|
726
|
+
return { kind: 'usage', ...(this.spentUntil === null ? {} : { resetsAt: this.spentUntil }) };
|
|
727
|
+
}
|
|
728
|
+
if (info === 'serverOverloaded' || info === 'rateLimitExceeded') {
|
|
729
|
+
return { kind: 'overload' };
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private text(ref: string, text: string, completed: boolean, events: BackendEvent[]): void {
|
|
735
|
+
events.push(completed ? { type: 'text.done', ref, text } : { type: 'text.delta', ref, text });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/*
|
|
739
|
+
* An agent Codex spawned, as a spawnAgent call reports it. It works in a thread of its own, so this row
|
|
740
|
+
* never grows children or a report. The call completes once the agent runs; only the state Codex keeps
|
|
741
|
+
* per agent says when it stopped. A spawn that failed opened no agent to wait for.
|
|
742
|
+
*/
|
|
743
|
+
private spawnedAgent(ref: string, item: Frame, input: unknown, events: BackendEvent[]): void {
|
|
744
|
+
const summary = collabAgentOutput(item.agentsStates);
|
|
745
|
+
// One spawn opens one thread; Codex only knows its id once the call went through.
|
|
746
|
+
const threadId = Array.isArray(item.receiverThreadIds) ? str(item.receiverThreadIds[0]) : null;
|
|
747
|
+
events.push({ type: 'tool.started', ref, name: 'Agent', input, parentRef: null });
|
|
748
|
+
events.push({
|
|
749
|
+
type: 'task.started',
|
|
750
|
+
ref,
|
|
751
|
+
description: (str(item.prompt) ?? '').split('\n')[0] ?? '',
|
|
752
|
+
subagentType: null,
|
|
753
|
+
prompt: str(item.prompt),
|
|
754
|
+
// A spawned agent never blocks the turn that spawned it.
|
|
755
|
+
background: true,
|
|
756
|
+
...(threadId ? { threadId } : {})
|
|
757
|
+
});
|
|
758
|
+
if (threadId === null) {
|
|
759
|
+
if (str(item.status) === 'failed') {
|
|
760
|
+
events.push({ type: 'task.done', ref, summary: summary || null, ok: false });
|
|
761
|
+
}
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (!this.agentRows.has(threadId)) {
|
|
765
|
+
this.agentRows.set(threadId, { ref, label: (str(item.prompt) ?? '').split('\n')[0] ?? '', settled: false });
|
|
766
|
+
}
|
|
767
|
+
if (!this.settleEndedAgents(item.agentsStates, events)) {
|
|
768
|
+
events.push({ type: 'task.progress', ref, summary: summary || null, lastTool: null, usage: null });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/* Settles the row of every agent a collab call reports stopped; true when one of them did just now. */
|
|
773
|
+
private settleEndedAgents(states: unknown, events: BackendEvent[]): boolean {
|
|
774
|
+
let settled = false;
|
|
775
|
+
for (const [threadId, state] of Object.entries(isRecord(states) ? states : {})) {
|
|
776
|
+
const status = isRecord(state) ? str(state.status) : null;
|
|
777
|
+
const row = this.agentRows.get(threadId);
|
|
778
|
+
if (row === undefined || row.settled || status === null || !ENDED_AGENT_STATES.has(status)) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
row.settled = true;
|
|
782
|
+
this.withdrawOwner(threadId, null, events);
|
|
783
|
+
settled = true;
|
|
784
|
+
events.push({ type: 'task.done', ref: row.ref, summary: collabAgentOutput({ [threadId]: state }), ok: status === 'completed' });
|
|
785
|
+
}
|
|
786
|
+
return settled;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/*
|
|
790
|
+
* What Codex 0.156.1 reports of an agent it spawned instead of a spawnAgent call: `started` under the
|
|
791
|
+
* spawn's call id with the agent's thread, then `completed` or `interrupted` under an id of its own that
|
|
792
|
+
* only the thread ties back to the row. Each comes as item/started and again as item/completed.
|
|
793
|
+
*/
|
|
794
|
+
private subAgentActivity(item: Frame, events: BackendEvent[]): void {
|
|
795
|
+
const ref = str(item.id);
|
|
796
|
+
const threadId = str(item.agentThreadId);
|
|
797
|
+
const kind = str(item.kind);
|
|
798
|
+
if (ref === null || threadId === null) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const row = this.agentRows.get(threadId);
|
|
802
|
+
if ((kind === 'started' || kind === 'interacted') && row === undefined) {
|
|
803
|
+
// The path ends in the name the parent gave the agent, which is all the stream says about its task.
|
|
804
|
+
const path = str(item.agentPath) ?? '';
|
|
805
|
+
this.agentRows.set(threadId, { ref, label: path.split('/').at(-1) ?? '', settled: false, ...(kind === 'interacted' ? { interaction: ref } : {}) });
|
|
806
|
+
events.push({ type: 'tool.started', ref, name: 'Agent', input: { agentPath: path }, parentRef: null });
|
|
807
|
+
events.push({ type: 'task.started', ref, description: path.split('/').at(-1) ?? '', subagentType: null, prompt: null, background: true, threadId });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (kind === 'interacted' && row !== undefined && row.interaction !== ref) {
|
|
811
|
+
row.interaction = ref;
|
|
812
|
+
row.settled = false;
|
|
813
|
+
events.push({
|
|
814
|
+
type: 'task.started',
|
|
815
|
+
ref: row.ref,
|
|
816
|
+
description: row.label,
|
|
817
|
+
subagentType: null,
|
|
818
|
+
prompt: null,
|
|
819
|
+
background: true,
|
|
820
|
+
threadId,
|
|
821
|
+
resumed: true
|
|
822
|
+
});
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if ((kind === 'completed' || kind === 'interrupted') && row !== undefined && !row.settled) {
|
|
826
|
+
row.settled = true;
|
|
827
|
+
this.withdrawOwner(threadId, null, events);
|
|
828
|
+
events.push({ type: 'task.done', ref: row.ref, summary: null, ok: kind === 'completed' });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private tool(
|
|
833
|
+
ref: string,
|
|
834
|
+
name: string,
|
|
835
|
+
input: unknown,
|
|
836
|
+
completed: boolean,
|
|
837
|
+
output: string,
|
|
838
|
+
succeeded: boolean,
|
|
839
|
+
events: BackendEvent[],
|
|
840
|
+
changes?: ChatFileChange[]
|
|
841
|
+
): void {
|
|
842
|
+
events.push({ type: 'tool.started', ref, name, input, parentRef: null, ...(changes && changes.length > 0 ? { changes } : {}) });
|
|
843
|
+
if (completed) {
|
|
844
|
+
events.push({ type: 'tool.done', ref, output, state: succeeded ? 'done' : 'error', ...(changes && changes.length > 0 ? { changes } : {}) });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/*
|
|
850
|
+
* The decision the app-server takes for one of ours; "always" becomes its own policy amendment when it offered one.
|
|
851
|
+
* `decline` is accepted even when `availableDecisions` lists only accept, the amendment and cancel.
|
|
852
|
+
*/
|
|
853
|
+
function codexDecision(decision: ApprovalDecision, pending: Extract<Pending, { type: 'approval' }>): unknown {
|
|
854
|
+
if (decision === 'deny') {
|
|
855
|
+
return 'decline';
|
|
856
|
+
}
|
|
857
|
+
if (decision === 'allow') {
|
|
858
|
+
return 'accept';
|
|
859
|
+
}
|
|
860
|
+
if (pending.amendment) {
|
|
861
|
+
return { acceptWithExecpolicyAmendment: { execpolicy_amendment: pending.amendment } };
|
|
862
|
+
}
|
|
863
|
+
return pending.kind === 'fileChange' || pending.decisions.includes('acceptForSession') ? 'acceptForSession' : 'accept';
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/* The MCP answer to a yes or no; how long it is remembered rides along in `_meta.persist`. */
|
|
867
|
+
function elicitationAnswer(decision: ApprovalDecision, persist: ElicitationPersist | null): unknown {
|
|
868
|
+
if (decision === 'deny') {
|
|
869
|
+
return { action: 'decline' };
|
|
870
|
+
}
|
|
871
|
+
return decision === 'allow-always' && persist !== null ? { action: 'accept', content: {}, _meta: { persist } } : { action: 'accept', content: {} };
|
|
872
|
+
}
|