lorenz 0.1.0
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 +201 -0
- package/NOTICE +13 -0
- package/README.md +774 -0
- package/RELEASE-MANIFEST.json +211 -0
- package/apps/cli/bin/lorenz.js +25 -0
- package/apps/cli/dist/bin/cli.d.ts +3 -0
- package/apps/cli/dist/bin/cli.d.ts.map +1 -0
- package/apps/cli/dist/bin/cli.js +4 -0
- package/apps/cli/dist/bin/cli.js.map +1 -0
- package/apps/cli/dist/daemon.d.ts +76 -0
- package/apps/cli/dist/daemon.d.ts.map +1 -0
- package/apps/cli/dist/daemon.js +189 -0
- package/apps/cli/dist/daemon.js.map +1 -0
- package/apps/cli/dist/doctor.d.ts +40 -0
- package/apps/cli/dist/doctor.d.ts.map +1 -0
- package/apps/cli/dist/doctor.js +590 -0
- package/apps/cli/dist/doctor.js.map +1 -0
- package/apps/cli/dist/index.d.ts +32 -0
- package/apps/cli/dist/index.d.ts.map +1 -0
- package/apps/cli/dist/index.js +26 -0
- package/apps/cli/dist/index.js.map +1 -0
- package/apps/cli/dist/main.d.ts +40 -0
- package/apps/cli/dist/main.d.ts.map +1 -0
- package/apps/cli/dist/main.js +259 -0
- package/apps/cli/dist/main.js.map +1 -0
- package/apps/cli/dist/runs.d.ts +31 -0
- package/apps/cli/dist/runs.d.ts.map +1 -0
- package/apps/cli/dist/runs.js +281 -0
- package/apps/cli/dist/runs.js.map +1 -0
- package/apps/cli/dist/workerDriverLoader.d.ts +64 -0
- package/apps/cli/dist/workerDriverLoader.d.ts.map +1 -0
- package/apps/cli/dist/workerDriverLoader.js +211 -0
- package/apps/cli/dist/workerDriverLoader.js.map +1 -0
- package/apps/cli/package.json +57 -0
- package/apps/symphony-dashboard/dist/assets/index-B3owF3jd.css +1 -0
- package/apps/symphony-dashboard/dist/assets/index-DQ6XlL0d.js +227 -0
- package/apps/symphony-dashboard/dist/index.html +18 -0
- package/bin/lorenz +16 -0
- package/extensions/docker-worker/dist/index.d.ts +92 -0
- package/extensions/docker-worker/dist/index.d.ts.map +1 -0
- package/extensions/docker-worker/dist/index.js +283 -0
- package/extensions/docker-worker/dist/index.js.map +1 -0
- package/extensions/docker-worker/package.json +14 -0
- package/extensions/jira-tracker/dist/client.d.ts +50 -0
- package/extensions/jira-tracker/dist/client.d.ts.map +1 -0
- package/extensions/jira-tracker/dist/client.js +619 -0
- package/extensions/jira-tracker/dist/client.js.map +1 -0
- package/extensions/jira-tracker/dist/index.d.ts +5 -0
- package/extensions/jira-tracker/dist/index.d.ts.map +1 -0
- package/extensions/jira-tracker/dist/index.js +5 -0
- package/extensions/jira-tracker/dist/index.js.map +1 -0
- package/extensions/jira-tracker/dist/options.d.ts +38 -0
- package/extensions/jira-tracker/dist/options.d.ts.map +1 -0
- package/extensions/jira-tracker/dist/options.js +61 -0
- package/extensions/jira-tracker/dist/options.js.map +1 -0
- package/extensions/jira-tracker/dist/provider.d.ts +6 -0
- package/extensions/jira-tracker/dist/provider.d.ts.map +1 -0
- package/extensions/jira-tracker/dist/provider.js +178 -0
- package/extensions/jira-tracker/dist/provider.js.map +1 -0
- package/extensions/jira-tracker/dist/register.d.ts +10 -0
- package/extensions/jira-tracker/dist/register.d.ts.map +1 -0
- package/extensions/jira-tracker/dist/register.js +15 -0
- package/extensions/jira-tracker/dist/register.js.map +1 -0
- package/extensions/jira-tracker/package.json +16 -0
- package/extensions/linear-tracker/dist/client.d.ts +82 -0
- package/extensions/linear-tracker/dist/client.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/client.js +622 -0
- package/extensions/linear-tracker/dist/client.js.map +1 -0
- package/extensions/linear-tracker/dist/index.d.ts +8 -0
- package/extensions/linear-tracker/dist/index.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/index.js +7 -0
- package/extensions/linear-tracker/dist/index.js.map +1 -0
- package/extensions/linear-tracker/dist/options.d.ts +32 -0
- package/extensions/linear-tracker/dist/options.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/options.js +59 -0
- package/extensions/linear-tracker/dist/options.js.map +1 -0
- package/extensions/linear-tracker/dist/provider.d.ts +4 -0
- package/extensions/linear-tracker/dist/provider.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/provider.js +58 -0
- package/extensions/linear-tracker/dist/provider.js.map +1 -0
- package/extensions/linear-tracker/dist/register.d.ts +11 -0
- package/extensions/linear-tracker/dist/register.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/register.js +19 -0
- package/extensions/linear-tracker/dist/register.js.map +1 -0
- package/extensions/linear-tracker/dist/toolOps.d.ts +8 -0
- package/extensions/linear-tracker/dist/toolOps.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/toolOps.js +160 -0
- package/extensions/linear-tracker/dist/toolOps.js.map +1 -0
- package/extensions/linear-tracker/dist/tools.d.ts +7 -0
- package/extensions/linear-tracker/dist/tools.d.ts.map +1 -0
- package/extensions/linear-tracker/dist/tools.js +210 -0
- package/extensions/linear-tracker/dist/tools.js.map +1 -0
- package/extensions/linear-tracker/package.json +18 -0
- package/extensions/local-tracker/dist/boardStore.d.ts +116 -0
- package/extensions/local-tracker/dist/boardStore.d.ts.map +1 -0
- package/extensions/local-tracker/dist/boardStore.js +475 -0
- package/extensions/local-tracker/dist/boardStore.js.map +1 -0
- package/extensions/local-tracker/dist/client.d.ts +14 -0
- package/extensions/local-tracker/dist/client.d.ts.map +1 -0
- package/extensions/local-tracker/dist/client.js +27 -0
- package/extensions/local-tracker/dist/client.js.map +1 -0
- package/extensions/local-tracker/dist/index.d.ts +7 -0
- package/extensions/local-tracker/dist/index.d.ts.map +1 -0
- package/extensions/local-tracker/dist/index.js +7 -0
- package/extensions/local-tracker/dist/index.js.map +1 -0
- package/extensions/local-tracker/dist/options.d.ts +31 -0
- package/extensions/local-tracker/dist/options.d.ts.map +1 -0
- package/extensions/local-tracker/dist/options.js +69 -0
- package/extensions/local-tracker/dist/options.js.map +1 -0
- package/extensions/local-tracker/dist/provider.d.ts +9 -0
- package/extensions/local-tracker/dist/provider.d.ts.map +1 -0
- package/extensions/local-tracker/dist/provider.js +35 -0
- package/extensions/local-tracker/dist/provider.js.map +1 -0
- package/extensions/local-tracker/dist/register.d.ts +11 -0
- package/extensions/local-tracker/dist/register.d.ts.map +1 -0
- package/extensions/local-tracker/dist/register.js +19 -0
- package/extensions/local-tracker/dist/register.js.map +1 -0
- package/extensions/local-tracker/dist/resolveBoardDir.d.ts +24 -0
- package/extensions/local-tracker/dist/resolveBoardDir.d.ts.map +1 -0
- package/extensions/local-tracker/dist/resolveBoardDir.js +39 -0
- package/extensions/local-tracker/dist/resolveBoardDir.js.map +1 -0
- package/extensions/local-tracker/dist/toolOps.d.ts +9 -0
- package/extensions/local-tracker/dist/toolOps.d.ts.map +1 -0
- package/extensions/local-tracker/dist/toolOps.js +86 -0
- package/extensions/local-tracker/dist/toolOps.js.map +1 -0
- package/extensions/local-tracker/dist/tools.d.ts +7 -0
- package/extensions/local-tracker/dist/tools.d.ts.map +1 -0
- package/extensions/local-tracker/dist/tools.js +170 -0
- package/extensions/local-tracker/dist/tools.js.map +1 -0
- package/extensions/local-tracker/package.json +18 -0
- package/extensions/memory-tracker/dist/index.d.ts +24 -0
- package/extensions/memory-tracker/dist/index.d.ts.map +1 -0
- package/extensions/memory-tracker/dist/index.js +110 -0
- package/extensions/memory-tracker/dist/index.js.map +1 -0
- package/extensions/memory-tracker/package.json +16 -0
- package/extensions/slack-tracker/dist/client.d.ts +88 -0
- package/extensions/slack-tracker/dist/client.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/client.js +246 -0
- package/extensions/slack-tracker/dist/client.js.map +1 -0
- package/extensions/slack-tracker/dist/inMemoryTransport.d.ts +42 -0
- package/extensions/slack-tracker/dist/inMemoryTransport.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/inMemoryTransport.js +104 -0
- package/extensions/slack-tracker/dist/inMemoryTransport.js.map +1 -0
- package/extensions/slack-tracker/dist/index.d.ts +15 -0
- package/extensions/slack-tracker/dist/index.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/index.js +11 -0
- package/extensions/slack-tracker/dist/index.js.map +1 -0
- package/extensions/slack-tracker/dist/mapping.d.ts +27 -0
- package/extensions/slack-tracker/dist/mapping.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/mapping.js +109 -0
- package/extensions/slack-tracker/dist/mapping.js.map +1 -0
- package/extensions/slack-tracker/dist/operations.d.ts +41 -0
- package/extensions/slack-tracker/dist/operations.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/operations.js +97 -0
- package/extensions/slack-tracker/dist/operations.js.map +1 -0
- package/extensions/slack-tracker/dist/options.d.ts +30 -0
- package/extensions/slack-tracker/dist/options.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/options.js +49 -0
- package/extensions/slack-tracker/dist/options.js.map +1 -0
- package/extensions/slack-tracker/dist/provider.d.ts +9 -0
- package/extensions/slack-tracker/dist/provider.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/provider.js +74 -0
- package/extensions/slack-tracker/dist/provider.js.map +1 -0
- package/extensions/slack-tracker/dist/register.d.ts +11 -0
- package/extensions/slack-tracker/dist/register.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/register.js +19 -0
- package/extensions/slack-tracker/dist/register.js.map +1 -0
- package/extensions/slack-tracker/dist/threadState.d.ts +52 -0
- package/extensions/slack-tracker/dist/threadState.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/threadState.js +192 -0
- package/extensions/slack-tracker/dist/threadState.js.map +1 -0
- package/extensions/slack-tracker/dist/toolOps.d.ts +13 -0
- package/extensions/slack-tracker/dist/toolOps.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/toolOps.js +76 -0
- package/extensions/slack-tracker/dist/toolOps.js.map +1 -0
- package/extensions/slack-tracker/dist/tools.d.ts +8 -0
- package/extensions/slack-tracker/dist/tools.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/tools.js +266 -0
- package/extensions/slack-tracker/dist/tools.js.map +1 -0
- package/extensions/slack-tracker/dist/transport.d.ts +63 -0
- package/extensions/slack-tracker/dist/transport.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/transport.js +2 -0
- package/extensions/slack-tracker/dist/transport.js.map +1 -0
- package/extensions/slack-tracker/dist/webTransport.d.ts +44 -0
- package/extensions/slack-tracker/dist/webTransport.d.ts.map +1 -0
- package/extensions/slack-tracker/dist/webTransport.js +402 -0
- package/extensions/slack-tracker/dist/webTransport.js.map +1 -0
- package/extensions/slack-tracker/package.json +17 -0
- package/package.json +89 -0
- package/packages/acp/dist/childProcess.d.ts +4 -0
- package/packages/acp/dist/childProcess.d.ts.map +1 -0
- package/packages/acp/dist/childProcess.js +33 -0
- package/packages/acp/dist/childProcess.js.map +1 -0
- package/packages/acp/dist/index.d.ts +70 -0
- package/packages/acp/dist/index.d.ts.map +1 -0
- package/packages/acp/dist/index.js +701 -0
- package/packages/acp/dist/index.js.map +1 -0
- package/packages/acp/dist/options.d.ts +24 -0
- package/packages/acp/dist/options.d.ts.map +1 -0
- package/packages/acp/dist/options.js +92 -0
- package/packages/acp/dist/options.js.map +1 -0
- package/packages/acp/dist/toml.d.ts +2 -0
- package/packages/acp/dist/toml.d.ts.map +1 -0
- package/packages/acp/dist/toml.js +51 -0
- package/packages/acp/dist/toml.js.map +1 -0
- package/packages/acp/package.json +24 -0
- package/packages/agent-runner/dist/index.d.ts +58 -0
- package/packages/agent-runner/dist/index.d.ts.map +1 -0
- package/packages/agent-runner/dist/index.js +288 -0
- package/packages/agent-runner/dist/index.js.map +1 -0
- package/packages/agent-runner/package.json +19 -0
- package/packages/agent-sdk/dist/index.d.ts +2 -0
- package/packages/agent-sdk/dist/index.d.ts.map +1 -0
- package/packages/agent-sdk/dist/index.js +2 -0
- package/packages/agent-sdk/dist/index.js.map +1 -0
- package/packages/agent-sdk/dist/provider.d.ts +66 -0
- package/packages/agent-sdk/dist/provider.d.ts.map +1 -0
- package/packages/agent-sdk/dist/provider.js +38 -0
- package/packages/agent-sdk/dist/provider.js.map +1 -0
- package/packages/agent-sdk/package.json +14 -0
- package/packages/cli-kit/dist/index.d.ts +20 -0
- package/packages/cli-kit/dist/index.d.ts.map +1 -0
- package/packages/cli-kit/dist/index.js +72 -0
- package/packages/cli-kit/dist/index.js.map +1 -0
- package/packages/cli-kit/package.json +14 -0
- package/packages/config/dist/aliases.d.ts +10 -0
- package/packages/config/dist/aliases.d.ts.map +1 -0
- package/packages/config/dist/aliases.js +153 -0
- package/packages/config/dist/aliases.js.map +1 -0
- package/packages/config/dist/defaults.d.ts +12 -0
- package/packages/config/dist/defaults.d.ts.map +1 -0
- package/packages/config/dist/defaults.js +78 -0
- package/packages/config/dist/defaults.js.map +1 -0
- package/packages/config/dist/errors.d.ts +3 -0
- package/packages/config/dist/errors.d.ts.map +1 -0
- package/packages/config/dist/errors.js +56 -0
- package/packages/config/dist/errors.js.map +1 -0
- package/packages/config/dist/index.d.ts +5 -0
- package/packages/config/dist/index.d.ts.map +1 -0
- package/packages/config/dist/index.js +4 -0
- package/packages/config/dist/index.js.map +1 -0
- package/packages/config/dist/leaf-utils.d.ts +3 -0
- package/packages/config/dist/leaf-utils.d.ts.map +1 -0
- package/packages/config/dist/leaf-utils.js +9 -0
- package/packages/config/dist/leaf-utils.js.map +1 -0
- package/packages/config/dist/parse.d.ts +11 -0
- package/packages/config/dist/parse.d.ts.map +1 -0
- package/packages/config/dist/parse.js +821 -0
- package/packages/config/dist/parse.js.map +1 -0
- package/packages/config/dist/schemas.d.ts +214 -0
- package/packages/config/dist/schemas.d.ts.map +1 -0
- package/packages/config/dist/schemas.js +248 -0
- package/packages/config/dist/schemas.js.map +1 -0
- package/packages/config/package.json +19 -0
- package/packages/dispatch/dist/index.d.ts +22 -0
- package/packages/dispatch/dist/index.d.ts.map +1 -0
- package/packages/dispatch/dist/index.js +117 -0
- package/packages/dispatch/dist/index.js.map +1 -0
- package/packages/dispatch/package.json +16 -0
- package/packages/dispatch-coordinator/dist/coordinator.d.ts +158 -0
- package/packages/dispatch-coordinator/dist/coordinator.d.ts.map +1 -0
- package/packages/dispatch-coordinator/dist/coordinator.js +529 -0
- package/packages/dispatch-coordinator/dist/coordinator.js.map +1 -0
- package/packages/dispatch-coordinator/dist/gate.d.ts +24 -0
- package/packages/dispatch-coordinator/dist/gate.d.ts.map +1 -0
- package/packages/dispatch-coordinator/dist/gate.js +47 -0
- package/packages/dispatch-coordinator/dist/gate.js.map +1 -0
- package/packages/dispatch-coordinator/dist/index.d.ts +6 -0
- package/packages/dispatch-coordinator/dist/index.d.ts.map +1 -0
- package/packages/dispatch-coordinator/dist/index.js +16 -0
- package/packages/dispatch-coordinator/dist/index.js.map +1 -0
- package/packages/dispatch-coordinator/dist/mcpEndpointManager.d.ts +28 -0
- package/packages/dispatch-coordinator/dist/mcpEndpointManager.d.ts.map +1 -0
- package/packages/dispatch-coordinator/dist/mcpEndpointManager.js +54 -0
- package/packages/dispatch-coordinator/dist/mcpEndpointManager.js.map +1 -0
- package/packages/dispatch-coordinator/dist/nullEndpointManager.d.ts +18 -0
- package/packages/dispatch-coordinator/dist/nullEndpointManager.d.ts.map +1 -0
- package/packages/dispatch-coordinator/dist/nullEndpointManager.js +40 -0
- package/packages/dispatch-coordinator/dist/nullEndpointManager.js.map +1 -0
- package/packages/dispatch-coordinator/dist/types.d.ts +119 -0
- package/packages/dispatch-coordinator/dist/types.d.ts.map +1 -0
- package/packages/dispatch-coordinator/dist/types.js +17 -0
- package/packages/dispatch-coordinator/dist/types.js.map +1 -0
- package/packages/dispatch-coordinator/package.json +16 -0
- package/packages/domain/dist/index.d.ts +775 -0
- package/packages/domain/dist/index.d.ts.map +1 -0
- package/packages/domain/dist/index.js +124 -0
- package/packages/domain/dist/index.js.map +1 -0
- package/packages/domain/package.json +14 -0
- package/packages/humanize/dist/index.d.ts +4 -0
- package/packages/humanize/dist/index.d.ts.map +1 -0
- package/packages/humanize/dist/index.js +347 -0
- package/packages/humanize/dist/index.js.map +1 -0
- package/packages/humanize/package.json +11 -0
- package/packages/issue/dist/index.d.ts +7 -0
- package/packages/issue/dist/index.d.ts.map +1 -0
- package/packages/issue/dist/index.js +147 -0
- package/packages/issue/dist/index.js.map +1 -0
- package/packages/issue/package.json +14 -0
- package/packages/log-file/dist/index.d.ts +10 -0
- package/packages/log-file/dist/index.d.ts.map +1 -0
- package/packages/log-file/dist/index.js +200 -0
- package/packages/log-file/dist/index.js.map +1 -0
- package/packages/log-file/package.json +15 -0
- package/packages/mcp/dist/agentEndpoint.d.ts +31 -0
- package/packages/mcp/dist/agentEndpoint.d.ts.map +1 -0
- package/packages/mcp/dist/agentEndpoint.js +270 -0
- package/packages/mcp/dist/agentEndpoint.js.map +1 -0
- package/packages/mcp/dist/auth.d.ts +7 -0
- package/packages/mcp/dist/auth.d.ts.map +1 -0
- package/packages/mcp/dist/auth.js +48 -0
- package/packages/mcp/dist/auth.js.map +1 -0
- package/packages/mcp/dist/filter.d.ts +70 -0
- package/packages/mcp/dist/filter.d.ts.map +1 -0
- package/packages/mcp/dist/filter.js +231 -0
- package/packages/mcp/dist/filter.js.map +1 -0
- package/packages/mcp/dist/index.d.ts +7 -0
- package/packages/mcp/dist/index.d.ts.map +1 -0
- package/packages/mcp/dist/index.js +5 -0
- package/packages/mcp/dist/index.js.map +1 -0
- package/packages/mcp/dist/server.d.ts +31 -0
- package/packages/mcp/dist/server.d.ts.map +1 -0
- package/packages/mcp/dist/server.js +176 -0
- package/packages/mcp/dist/server.js.map +1 -0
- package/packages/mcp/dist/tools/linear.d.ts +5 -0
- package/packages/mcp/dist/tools/linear.d.ts.map +1 -0
- package/packages/mcp/dist/tools/linear.js +192 -0
- package/packages/mcp/dist/tools/linear.js.map +1 -0
- package/packages/mcp/dist/tools/local.d.ts +5 -0
- package/packages/mcp/dist/tools/local.d.ts.map +1 -0
- package/packages/mcp/dist/tools/local.js +161 -0
- package/packages/mcp/dist/tools/local.js.map +1 -0
- package/packages/mcp/dist/tools/result.d.ts +5 -0
- package/packages/mcp/dist/tools/result.d.ts.map +1 -0
- package/packages/mcp/dist/tools/result.js +15 -0
- package/packages/mcp/dist/tools/result.js.map +1 -0
- package/packages/mcp/dist/tools.d.ts +14 -0
- package/packages/mcp/dist/tools.d.ts.map +1 -0
- package/packages/mcp/dist/tools.js +58 -0
- package/packages/mcp/dist/tools.js.map +1 -0
- package/packages/mcp/package.json +20 -0
- package/packages/orchestrator/dist/index.d.ts +171 -0
- package/packages/orchestrator/dist/index.d.ts.map +1 -0
- package/packages/orchestrator/dist/index.js +524 -0
- package/packages/orchestrator/dist/index.js.map +1 -0
- package/packages/orchestrator/package.json +18 -0
- package/packages/policies/dist/index.d.ts +11 -0
- package/packages/policies/dist/index.d.ts.map +1 -0
- package/packages/policies/dist/index.js +6 -0
- package/packages/policies/dist/index.js.map +1 -0
- package/packages/policies/dist/reconciliation.d.ts +5 -0
- package/packages/policies/dist/reconciliation.d.ts.map +1 -0
- package/packages/policies/dist/reconciliation.js +17 -0
- package/packages/policies/dist/reconciliation.js.map +1 -0
- package/packages/policies/dist/resume.d.ts +14 -0
- package/packages/policies/dist/resume.d.ts.map +1 -0
- package/packages/policies/dist/resume.js +7 -0
- package/packages/policies/dist/resume.js.map +1 -0
- package/packages/policies/dist/retry.d.ts +4 -0
- package/packages/policies/dist/retry.d.ts.map +1 -0
- package/packages/policies/dist/retry.js +7 -0
- package/packages/policies/dist/retry.js.map +1 -0
- package/packages/policies/dist/stopReason.d.ts +4 -0
- package/packages/policies/dist/stopReason.d.ts.map +1 -0
- package/packages/policies/dist/stopReason.js +11 -0
- package/packages/policies/dist/stopReason.js.map +1 -0
- package/packages/policies/dist/usage.d.ts +14 -0
- package/packages/policies/dist/usage.d.ts.map +1 -0
- package/packages/policies/dist/usage.js +38 -0
- package/packages/policies/dist/usage.js.map +1 -0
- package/packages/policies/dist/workerHost.d.ts +8 -0
- package/packages/policies/dist/workerHost.d.ts.map +1 -0
- package/packages/policies/dist/workerHost.js +20 -0
- package/packages/policies/dist/workerHost.js.map +1 -0
- package/packages/policies/package.json +21 -0
- package/packages/presenter/dist/index.d.ts +81 -0
- package/packages/presenter/dist/index.d.ts.map +1 -0
- package/packages/presenter/dist/index.js +421 -0
- package/packages/presenter/dist/index.js.map +1 -0
- package/packages/presenter/package.json +16 -0
- package/packages/projections/dist/index.d.ts +10 -0
- package/packages/projections/dist/index.d.ts.map +1 -0
- package/packages/projections/dist/index.js +30 -0
- package/packages/projections/dist/index.js.map +1 -0
- package/packages/projections/package.json +15 -0
- package/packages/prompt/dist/index.d.ts +9 -0
- package/packages/prompt/dist/index.d.ts.map +1 -0
- package/packages/prompt/dist/index.js +71 -0
- package/packages/prompt/dist/index.js.map +1 -0
- package/packages/prompt/package.json +16 -0
- package/packages/retry-scheduler/dist/index.d.ts +12 -0
- package/packages/retry-scheduler/dist/index.d.ts.map +1 -0
- package/packages/retry-scheduler/dist/index.js +39 -0
- package/packages/retry-scheduler/dist/index.js.map +1 -0
- package/packages/retry-scheduler/package.json +15 -0
- package/packages/runtime/dist/index.d.ts +157 -0
- package/packages/runtime/dist/index.d.ts.map +1 -0
- package/packages/runtime/dist/index.js +1074 -0
- package/packages/runtime/dist/index.js.map +1 -0
- package/packages/runtime/package.json +26 -0
- package/packages/runtime-events/dist/index.d.ts +110 -0
- package/packages/runtime-events/dist/index.d.ts.map +1 -0
- package/packages/runtime-events/dist/index.js +25 -0
- package/packages/runtime-events/dist/index.js.map +1 -0
- package/packages/runtime-events/package.json +14 -0
- package/packages/server/dist/index.d.ts +25 -0
- package/packages/server/dist/index.d.ts.map +1 -0
- package/packages/server/dist/index.js +213 -0
- package/packages/server/dist/index.js.map +1 -0
- package/packages/server/dist/issue-store.d.ts +26 -0
- package/packages/server/dist/issue-store.d.ts.map +1 -0
- package/packages/server/dist/issue-store.js +88 -0
- package/packages/server/dist/issue-store.js.map +1 -0
- package/packages/server/dist/path-params.d.ts +6 -0
- package/packages/server/dist/path-params.d.ts.map +1 -0
- package/packages/server/dist/path-params.js +15 -0
- package/packages/server/dist/path-params.js.map +1 -0
- package/packages/server/dist/source.d.ts +12 -0
- package/packages/server/dist/source.d.ts.map +1 -0
- package/packages/server/dist/source.js +2 -0
- package/packages/server/dist/source.js.map +1 -0
- package/packages/server/dist/trace-routes.d.ts +21 -0
- package/packages/server/dist/trace-routes.d.ts.map +1 -0
- package/packages/server/dist/trace-routes.js +66 -0
- package/packages/server/dist/trace-routes.js.map +1 -0
- package/packages/server/dist/ws.d.ts +18 -0
- package/packages/server/dist/ws.d.ts.map +1 -0
- package/packages/server/dist/ws.js +168 -0
- package/packages/server/dist/ws.js.map +1 -0
- package/packages/server/package.json +22 -0
- package/packages/ssh/dist/index.d.ts +33 -0
- package/packages/ssh/dist/index.d.ts.map +1 -0
- package/packages/ssh/dist/index.js +281 -0
- package/packages/ssh/dist/index.js.map +1 -0
- package/packages/ssh/package.json +15 -0
- package/packages/static-worker/dist/index.d.ts +73 -0
- package/packages/static-worker/dist/index.d.ts.map +1 -0
- package/packages/static-worker/dist/index.js +150 -0
- package/packages/static-worker/dist/index.js.map +1 -0
- package/packages/static-worker/package.json +14 -0
- package/packages/tool-sdk/dist/filter.d.ts +70 -0
- package/packages/tool-sdk/dist/filter.d.ts.map +1 -0
- package/packages/tool-sdk/dist/filter.js +231 -0
- package/packages/tool-sdk/dist/filter.js.map +1 -0
- package/packages/tool-sdk/dist/index.d.ts +6 -0
- package/packages/tool-sdk/dist/index.d.ts.map +1 -0
- package/packages/tool-sdk/dist/index.js +4 -0
- package/packages/tool-sdk/dist/index.js.map +1 -0
- package/packages/tool-sdk/dist/provider.d.ts +51 -0
- package/packages/tool-sdk/dist/provider.d.ts.map +1 -0
- package/packages/tool-sdk/dist/provider.js +2 -0
- package/packages/tool-sdk/dist/provider.js.map +1 -0
- package/packages/tool-sdk/dist/registry.d.ts +35 -0
- package/packages/tool-sdk/dist/registry.d.ts.map +1 -0
- package/packages/tool-sdk/dist/registry.js +85 -0
- package/packages/tool-sdk/dist/registry.js.map +1 -0
- package/packages/tool-sdk/dist/result.d.ts +5 -0
- package/packages/tool-sdk/dist/result.d.ts.map +1 -0
- package/packages/tool-sdk/dist/result.js +15 -0
- package/packages/tool-sdk/dist/result.js.map +1 -0
- package/packages/tool-sdk/package.json +14 -0
- package/packages/traceviz-emitter/dist/index.d.ts +19 -0
- package/packages/traceviz-emitter/dist/index.d.ts.map +1 -0
- package/packages/traceviz-emitter/dist/index.js +97 -0
- package/packages/traceviz-emitter/dist/index.js.map +1 -0
- package/packages/traceviz-emitter/package.json +17 -0
- package/packages/traceviz-server/dist/index.d.ts +14 -0
- package/packages/traceviz-server/dist/index.d.ts.map +1 -0
- package/packages/traceviz-server/dist/index.js +10 -0
- package/packages/traceviz-server/dist/index.js.map +1 -0
- package/packages/traceviz-server/dist/models/api.d.ts +51 -0
- package/packages/traceviz-server/dist/models/api.d.ts.map +1 -0
- package/packages/traceviz-server/dist/models/api.js +5 -0
- package/packages/traceviz-server/dist/models/api.js.map +1 -0
- package/packages/traceviz-server/dist/models/display-events.d.ts +58 -0
- package/packages/traceviz-server/dist/models/display-events.d.ts.map +1 -0
- package/packages/traceviz-server/dist/models/display-events.js +6 -0
- package/packages/traceviz-server/dist/models/display-events.js.map +1 -0
- package/packages/traceviz-server/dist/parser.d.ts +14 -0
- package/packages/traceviz-server/dist/parser.d.ts.map +1 -0
- package/packages/traceviz-server/dist/parser.js +363 -0
- package/packages/traceviz-server/dist/parser.js.map +1 -0
- package/packages/traceviz-server/dist/stats.d.ts +7 -0
- package/packages/traceviz-server/dist/stats.d.ts.map +1 -0
- package/packages/traceviz-server/dist/stats.js +81 -0
- package/packages/traceviz-server/dist/stats.js.map +1 -0
- package/packages/traceviz-server/dist/watcher.d.ts +54 -0
- package/packages/traceviz-server/dist/watcher.d.ts.map +1 -0
- package/packages/traceviz-server/dist/watcher.js +368 -0
- package/packages/traceviz-server/dist/watcher.js.map +1 -0
- package/packages/traceviz-server/package.json +16 -0
- package/packages/tracker-sdk/dist/index.d.ts +5 -0
- package/packages/tracker-sdk/dist/index.d.ts.map +1 -0
- package/packages/tracker-sdk/dist/index.js +4 -0
- package/packages/tracker-sdk/dist/index.js.map +1 -0
- package/packages/tracker-sdk/dist/options.d.ts +20 -0
- package/packages/tracker-sdk/dist/options.d.ts.map +1 -0
- package/packages/tracker-sdk/dist/options.js +46 -0
- package/packages/tracker-sdk/dist/options.js.map +1 -0
- package/packages/tracker-sdk/dist/provider.d.ts +104 -0
- package/packages/tracker-sdk/dist/provider.d.ts.map +1 -0
- package/packages/tracker-sdk/dist/provider.js +2 -0
- package/packages/tracker-sdk/dist/provider.js.map +1 -0
- package/packages/tracker-sdk/dist/registry.d.ts +26 -0
- package/packages/tracker-sdk/dist/registry.d.ts.map +1 -0
- package/packages/tracker-sdk/dist/registry.js +52 -0
- package/packages/tracker-sdk/dist/registry.js.map +1 -0
- package/packages/tracker-sdk/dist/toolPack.d.ts +10 -0
- package/packages/tracker-sdk/dist/toolPack.d.ts.map +1 -0
- package/packages/tracker-sdk/dist/toolPack.js +185 -0
- package/packages/tracker-sdk/dist/toolPack.js.map +1 -0
- package/packages/tracker-sdk/package.json +15 -0
- package/packages/tui/dist/index.d.ts +35 -0
- package/packages/tui/dist/index.d.ts.map +1 -0
- package/packages/tui/dist/index.js +354 -0
- package/packages/tui/dist/index.js.map +1 -0
- package/packages/tui/package.json +18 -0
- package/packages/worker-host-pool/dist/index.d.ts +33 -0
- package/packages/worker-host-pool/dist/index.d.ts.map +1 -0
- package/packages/worker-host-pool/dist/index.js +311 -0
- package/packages/worker-host-pool/dist/index.js.map +1 -0
- package/packages/worker-host-pool/package.json +14 -0
- package/packages/worker-pool/dist/index.d.ts +6 -0
- package/packages/worker-pool/dist/index.d.ts.map +1 -0
- package/packages/worker-pool/dist/index.js +15 -0
- package/packages/worker-pool/dist/index.js.map +1 -0
- package/packages/worker-pool/dist/lease.d.ts +36 -0
- package/packages/worker-pool/dist/lease.d.ts.map +1 -0
- package/packages/worker-pool/dist/lease.js +53 -0
- package/packages/worker-pool/dist/lease.js.map +1 -0
- package/packages/worker-pool/dist/ledger.d.ts +51 -0
- package/packages/worker-pool/dist/ledger.d.ts.map +1 -0
- package/packages/worker-pool/dist/ledger.js +165 -0
- package/packages/worker-pool/dist/ledger.js.map +1 -0
- package/packages/worker-pool/dist/mutex.d.ts +10 -0
- package/packages/worker-pool/dist/mutex.d.ts.map +1 -0
- package/packages/worker-pool/dist/mutex.js +22 -0
- package/packages/worker-pool/dist/mutex.js.map +1 -0
- package/packages/worker-pool/dist/pool.d.ts +33 -0
- package/packages/worker-pool/dist/pool.d.ts.map +1 -0
- package/packages/worker-pool/dist/pool.js +1727 -0
- package/packages/worker-pool/dist/pool.js.map +1 -0
- package/packages/worker-pool/dist/reaper.d.ts +94 -0
- package/packages/worker-pool/dist/reaper.d.ts.map +1 -0
- package/packages/worker-pool/dist/reaper.js +295 -0
- package/packages/worker-pool/dist/reaper.js.map +1 -0
- package/packages/worker-pool/dist/types.d.ts +249 -0
- package/packages/worker-pool/dist/types.d.ts.map +1 -0
- package/packages/worker-pool/dist/types.js +2 -0
- package/packages/worker-pool/dist/types.js.map +1 -0
- package/packages/worker-pool/package.json +16 -0
- package/packages/worker-sdk/dist/conformance.d.ts +64 -0
- package/packages/worker-sdk/dist/conformance.d.ts.map +1 -0
- package/packages/worker-sdk/dist/conformance.js +109 -0
- package/packages/worker-sdk/dist/conformance.js.map +1 -0
- package/packages/worker-sdk/dist/fake.d.ts +76 -0
- package/packages/worker-sdk/dist/fake.d.ts.map +1 -0
- package/packages/worker-sdk/dist/fake.js +142 -0
- package/packages/worker-sdk/dist/fake.js.map +1 -0
- package/packages/worker-sdk/dist/index.d.ts +5 -0
- package/packages/worker-sdk/dist/index.d.ts.map +1 -0
- package/packages/worker-sdk/dist/index.js +10 -0
- package/packages/worker-sdk/dist/index.js.map +1 -0
- package/packages/worker-sdk/dist/module.d.ts +46 -0
- package/packages/worker-sdk/dist/module.d.ts.map +1 -0
- package/packages/worker-sdk/dist/module.js +59 -0
- package/packages/worker-sdk/dist/module.js.map +1 -0
- package/packages/worker-sdk/dist/registry.d.ts +24 -0
- package/packages/worker-sdk/dist/registry.d.ts.map +1 -0
- package/packages/worker-sdk/dist/registry.js +49 -0
- package/packages/worker-sdk/dist/registry.js.map +1 -0
- package/packages/worker-sdk/dist/types.d.ts +138 -0
- package/packages/worker-sdk/dist/types.d.ts.map +1 -0
- package/packages/worker-sdk/dist/types.js +21 -0
- package/packages/worker-sdk/dist/types.js.map +1 -0
- package/packages/worker-sdk/package.json +15 -0
- package/packages/workflow/dist/index.d.ts +33 -0
- package/packages/workflow/dist/index.d.ts.map +1 -0
- package/packages/workflow/dist/index.js +125 -0
- package/packages/workflow/dist/index.js.map +1 -0
- package/packages/workflow/package.json +19 -0
- package/packages/workspace/dist/index.d.ts +70 -0
- package/packages/workspace/dist/index.d.ts.map +1 -0
- package/packages/workspace/dist/index.js +1016 -0
- package/packages/workspace/dist/index.js.map +1 -0
- package/packages/workspace/package.json +17 -0
- package/runtime-deps/anthropic-claude-agent-sdk/LICENSE.md +1 -0
- package/runtime-deps/anthropic-claude-agent-sdk/README.md +65 -0
- package/runtime-deps/anthropic-claude-agent-sdk/agentSdkTypes.d.ts +1 -0
- package/runtime-deps/anthropic-claude-agent-sdk/assistant.d.ts +135 -0
- package/runtime-deps/anthropic-claude-agent-sdk/assistant.mjs +190 -0
- package/runtime-deps/anthropic-claude-agent-sdk/bridge.d.ts +231 -0
- package/runtime-deps/anthropic-claude-agent-sdk/bridge.mjs +168 -0
- package/runtime-deps/anthropic-claude-agent-sdk/browser-sdk.d.ts +53 -0
- package/runtime-deps/anthropic-claude-agent-sdk/browser-sdk.js +93 -0
- package/runtime-deps/anthropic-claude-agent-sdk/extractFromBunfs.d.ts +1 -0
- package/runtime-deps/anthropic-claude-agent-sdk/extractFromBunfs.js +156 -0
- package/runtime-deps/anthropic-claude-agent-sdk/manifest.json +47 -0
- package/runtime-deps/anthropic-claude-agent-sdk/manifest.zst.json +55 -0
- package/runtime-deps/anthropic-claude-agent-sdk/node_modules/.bin/anthropic-ai-sdk +21 -0
- package/runtime-deps/anthropic-claude-agent-sdk/package.json +81 -0
- package/runtime-deps/anthropic-claude-agent-sdk/sdk-tools.d.ts +3170 -0
- package/runtime-deps/anthropic-claude-agent-sdk/sdk.d.ts +6000 -0
- package/runtime-deps/anthropic-claude-agent-sdk/sdk.mjs +119 -0
- package/runtime-deps/openai-codex/README.md +60 -0
- package/runtime-deps/openai-codex/bin/codex.js +229 -0
- package/runtime-deps/openai-codex/bin/rg +79 -0
- package/runtime-deps/openai-codex/package.json +22 -0
- package/vendor/claude-agent-acp/dist/acp-agent.d.ts +239 -0
- package/vendor/claude-agent-acp/dist/acp-agent.d.ts.map +1 -0
- package/vendor/claude-agent-acp/dist/acp-agent.js +2693 -0
- package/vendor/claude-agent-acp/dist/bundle.js +41230 -0
- package/vendor/claude-agent-acp/dist/index.d.ts +3 -0
- package/vendor/claude-agent-acp/dist/index.d.ts.map +1 -0
- package/vendor/claude-agent-acp/dist/index.js +67 -0
- package/vendor/claude-agent-acp/dist/lib.d.ts +6 -0
- package/vendor/claude-agent-acp/dist/lib.d.ts.map +1 -0
- package/vendor/claude-agent-acp/dist/lib.js +5 -0
- package/vendor/claude-agent-acp/dist/settings.d.ts +68 -0
- package/vendor/claude-agent-acp/dist/settings.d.ts.map +1 -0
- package/vendor/claude-agent-acp/dist/settings.js +182 -0
- package/vendor/claude-agent-acp/dist/tools.d.ts +103 -0
- package/vendor/claude-agent-acp/dist/tools.d.ts.map +1 -0
- package/vendor/claude-agent-acp/dist/tools.js +713 -0
- package/vendor/claude-agent-acp/dist/utils.d.ts +16 -0
- package/vendor/claude-agent-acp/dist/utils.d.ts.map +1 -0
- package/vendor/claude-agent-acp/dist/utils.js +83 -0
- package/vendor/claude-agent-acp/package.json +23 -0
- package/vendor/codex-acp/dist/index.js +21280 -0
- package/vendor/codex-acp/package.json +17 -0
|
@@ -0,0 +1,2693 @@
|
|
|
1
|
+
import { AgentSideConnection, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { deleteSession, getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
7
|
+
import { SettingsManager } from "./settings.js";
|
|
8
|
+
import { applyTaskCreate, applyTaskUpdate, createPostToolUseHook, createTaskHook, parseTaskCreateOutput, planEntries, registerHookCallback, taskStateToPlanEntries, toolInfoFromToolUse, toolUpdateFromDiffToolResponse, toolUpdateFromToolResult, } from "./tools.js";
|
|
9
|
+
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
10
|
+
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
11
|
+
const MAX_TITLE_LENGTH = 256;
|
|
12
|
+
function sanitizeTitle(text) {
|
|
13
|
+
// Replace newlines and collapse whitespace
|
|
14
|
+
const sanitized = text
|
|
15
|
+
.replace(/[\r\n]+/g, " ")
|
|
16
|
+
.replace(/\s+/g, " ")
|
|
17
|
+
.trim();
|
|
18
|
+
if (sanitized.length <= MAX_TITLE_LENGTH) {
|
|
19
|
+
return sanitized;
|
|
20
|
+
}
|
|
21
|
+
return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
|
|
22
|
+
}
|
|
23
|
+
const ZERO_USAGE = Object.freeze({
|
|
24
|
+
input_tokens: 0,
|
|
25
|
+
output_tokens: 0,
|
|
26
|
+
cache_read_input_tokens: 0,
|
|
27
|
+
cache_creation_input_tokens: 0,
|
|
28
|
+
});
|
|
29
|
+
const DEFAULT_CONTEXT_WINDOW = 200000;
|
|
30
|
+
/** Compute a stable fingerprint of the session-defining params so we can
|
|
31
|
+
* detect when a loadSession/resumeSession call requires tearing down and
|
|
32
|
+
* recreating the underlying Query process. MCP servers are sorted by name
|
|
33
|
+
* so that ordering differences don't trigger unnecessary recreations. */
|
|
34
|
+
function computeSessionFingerprint(params) {
|
|
35
|
+
const servers = [...(params.mcpServers ?? [])].sort((a, b) => a.name.localeCompare(b.name));
|
|
36
|
+
return JSON.stringify({ cwd: params.cwd, mcpServers: servers });
|
|
37
|
+
}
|
|
38
|
+
export async function claudeCliPath() {
|
|
39
|
+
if (process.env.CLAUDE_CODE_EXECUTABLE) {
|
|
40
|
+
return process.env.CLAUDE_CODE_EXECUTABLE;
|
|
41
|
+
}
|
|
42
|
+
// The SDK's CLI is a native binary shipped as a platform-specific optional
|
|
43
|
+
// dependency of @anthropic-ai/claude-agent-sdk. Resolve via a require bound
|
|
44
|
+
// to the SDK so nested installs are found even when npm doesn't hoist.
|
|
45
|
+
const { createRequire } = await import("node:module");
|
|
46
|
+
const req = createRequire(import.meta.resolve("@anthropic-ai/claude-agent-sdk"));
|
|
47
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
48
|
+
// On linux, both glibc and musl variants may be installed side-by-side
|
|
49
|
+
// (e.g. bunx hydrates every optional dep), so picking one by trial is
|
|
50
|
+
// unreliable: the wrong binary segfaults at runtime instead of failing to
|
|
51
|
+
// spawn. Detect the runtime libc and prefer the matching variant, falling
|
|
52
|
+
// back to the other only if the preferred one isn't installed.
|
|
53
|
+
const candidates = process.platform === "linux"
|
|
54
|
+
? isMuslLibc()
|
|
55
|
+
? [
|
|
56
|
+
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
|
57
|
+
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
|
58
|
+
]
|
|
59
|
+
: [
|
|
60
|
+
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}/claude${ext}`,
|
|
61
|
+
`@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl/claude${ext}`,
|
|
62
|
+
]
|
|
63
|
+
: [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}/claude${ext}`];
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
try {
|
|
66
|
+
return req.resolve(candidate);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// try next candidate
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Claude native binary not found for ${process.platform}-${process.arch}. ` +
|
|
73
|
+
`Reinstall @anthropic-ai/claude-agent-sdk without --omit=optional, or set CLAUDE_CODE_EXECUTABLE.`);
|
|
74
|
+
}
|
|
75
|
+
function isMuslLibc() {
|
|
76
|
+
// process.report.getReport().header.glibcVersionRuntime is populated when
|
|
77
|
+
// Node is dynamically linked against glibc, and absent on musl.
|
|
78
|
+
const report = process.report?.getReport();
|
|
79
|
+
return !report?.header?.glibcVersionRuntime;
|
|
80
|
+
}
|
|
81
|
+
function shouldHideClaudeAuth() {
|
|
82
|
+
return process.argv.includes("--hide-claude-auth");
|
|
83
|
+
}
|
|
84
|
+
// Bypass Permissions doesn't work if we are a root/sudo user
|
|
85
|
+
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
86
|
+
const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
|
|
87
|
+
// Slash commands that the SDK handles locally without replaying the user
|
|
88
|
+
// message and without invoking the model.
|
|
89
|
+
const LOCAL_ONLY_COMMANDS = new Set(["/context", "/heapdump", "/extra-usage"]);
|
|
90
|
+
// The Claude SDK persists local slash command invocations (e.g. `/model`) and
|
|
91
|
+
// their output as user messages in the session transcript, wrapping the
|
|
92
|
+
// payload in these XML-like markers that the CLI uses for its own display.
|
|
93
|
+
// The live prompt loop drops them; replay must strip them too or they leak
|
|
94
|
+
// into the UI on session/load.
|
|
95
|
+
const LOCAL_COMMAND_MARKERS = [
|
|
96
|
+
"command-name",
|
|
97
|
+
"command-message",
|
|
98
|
+
"command-args",
|
|
99
|
+
"local-command-stdout",
|
|
100
|
+
"local-command-stderr",
|
|
101
|
+
].map((tag) => ({ open: `<${tag}>`, close: `</${tag}>` }));
|
|
102
|
+
// Single-pass scanner that removes each `<tag>…</tag>` marker (matching the
|
|
103
|
+
// nearest closing tag of the same name, like a lazy regex would).
|
|
104
|
+
function stripMarkerTags(text) {
|
|
105
|
+
const dead = new Set();
|
|
106
|
+
let result = "";
|
|
107
|
+
let copiedUpTo = 0;
|
|
108
|
+
let i = 0;
|
|
109
|
+
while (i < text.length) {
|
|
110
|
+
if (text[i] === "<") {
|
|
111
|
+
const marker = LOCAL_COMMAND_MARKERS.find((m) => !dead.has(m.open) && text.startsWith(m.open, i));
|
|
112
|
+
if (marker) {
|
|
113
|
+
const end = text.indexOf(marker.close, i + marker.open.length);
|
|
114
|
+
if (end !== -1) {
|
|
115
|
+
result += text.slice(copiedUpTo, i);
|
|
116
|
+
i = copiedUpTo = end + marker.close.length;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// No closing marker remains anywhere ahead, and `indexOf` only ever
|
|
120
|
+
// searches forward from here on, so stop treating this tag as an
|
|
121
|
+
// opener — that avoids rescanning the tail for it on every match.
|
|
122
|
+
dead.add(marker.open);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
return result + text.slice(copiedUpTo);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Return user-message content with local-command marker tags removed, or
|
|
131
|
+
* `null` if nothing meaningful remains (caller should skip the message).
|
|
132
|
+
* Preserves real prose that's mixed in alongside the markers — e.g. a
|
|
133
|
+
* message like `<command-name>…</command-name>hi` becomes `hi`.
|
|
134
|
+
*/
|
|
135
|
+
export function stripLocalCommandMetadata(content) {
|
|
136
|
+
if (typeof content === "string") {
|
|
137
|
+
const stripped = stripMarkerTags(content);
|
|
138
|
+
return stripped.trim() === "" ? null : stripped;
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(content))
|
|
141
|
+
return content;
|
|
142
|
+
const kept = [];
|
|
143
|
+
for (const block of content) {
|
|
144
|
+
if (block &&
|
|
145
|
+
typeof block === "object" &&
|
|
146
|
+
"type" in block &&
|
|
147
|
+
block.type === "text" &&
|
|
148
|
+
"text" in block &&
|
|
149
|
+
typeof block.text === "string") {
|
|
150
|
+
const stripped = stripMarkerTags(block.text);
|
|
151
|
+
if (stripped.trim() === "")
|
|
152
|
+
continue;
|
|
153
|
+
kept.push({ ...block, text: stripped });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
kept.push(block);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (kept.length === 0)
|
|
160
|
+
return null;
|
|
161
|
+
return kept;
|
|
162
|
+
}
|
|
163
|
+
export function isLocalCommandMetadata(content) {
|
|
164
|
+
return stripLocalCommandMetadata(content) === null;
|
|
165
|
+
}
|
|
166
|
+
const PERMISSION_MODE_ALIASES = {
|
|
167
|
+
auto: "auto",
|
|
168
|
+
default: "default",
|
|
169
|
+
acceptedits: "acceptEdits",
|
|
170
|
+
dontask: "dontAsk",
|
|
171
|
+
plan: "plan",
|
|
172
|
+
bypasspermissions: "bypassPermissions",
|
|
173
|
+
bypass: "bypassPermissions",
|
|
174
|
+
};
|
|
175
|
+
export function resolvePermissionMode(defaultMode, logger = console) {
|
|
176
|
+
if (defaultMode === undefined) {
|
|
177
|
+
return "default";
|
|
178
|
+
}
|
|
179
|
+
if (typeof defaultMode !== "string") {
|
|
180
|
+
logger.error("Ignoring permissions.defaultMode from settings: expected a string.");
|
|
181
|
+
return "default";
|
|
182
|
+
}
|
|
183
|
+
const normalized = defaultMode.trim().toLowerCase();
|
|
184
|
+
if (normalized === "") {
|
|
185
|
+
logger.error("Ignoring permissions.defaultMode from settings: expected a non-empty string.");
|
|
186
|
+
return "default";
|
|
187
|
+
}
|
|
188
|
+
const mapped = PERMISSION_MODE_ALIASES[normalized];
|
|
189
|
+
if (!mapped) {
|
|
190
|
+
logger.error(`Ignoring permissions.defaultMode from settings: unknown value '${defaultMode}'.`);
|
|
191
|
+
return "default";
|
|
192
|
+
}
|
|
193
|
+
if (mapped === "bypassPermissions" && !ALLOW_BYPASS) {
|
|
194
|
+
logger.error("Ignoring permissions.defaultMode from settings: bypassPermissions is not available when running as root.");
|
|
195
|
+
return "default";
|
|
196
|
+
}
|
|
197
|
+
return mapped;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Builds the label for the "Always Allow" permission option so the user can see
|
|
201
|
+
* the exact scope they are committing to. Uses the SDK-provided suggestions
|
|
202
|
+
* when available (e.g. `Bash(npm test:*)`) and falls back to naming the whole
|
|
203
|
+
* tool so "Always Allow" is never a blank check without disclosure.
|
|
204
|
+
*/
|
|
205
|
+
export function describeAlwaysAllow(suggestions, toolName) {
|
|
206
|
+
if (!suggestions || suggestions.length === 0) {
|
|
207
|
+
return `Always Allow all ${toolName}`;
|
|
208
|
+
}
|
|
209
|
+
const ruleLabels = [];
|
|
210
|
+
const directories = [];
|
|
211
|
+
for (const update of suggestions) {
|
|
212
|
+
if (update.type === "addRules" && update.behavior === "allow") {
|
|
213
|
+
for (const rule of update.rules) {
|
|
214
|
+
ruleLabels.push(rule.ruleContent ? `${rule.toolName}(${rule.ruleContent})` : `all ${rule.toolName}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (update.type === "addDirectories") {
|
|
218
|
+
directories.push(...update.directories);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const parts = [];
|
|
222
|
+
if (ruleLabels.length > 0) {
|
|
223
|
+
parts.push(ruleLabels.join(", "));
|
|
224
|
+
}
|
|
225
|
+
if (directories.length > 0) {
|
|
226
|
+
parts.push(`access to ${directories.join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
if (parts.length === 0) {
|
|
229
|
+
return `Always Allow all ${toolName}`;
|
|
230
|
+
}
|
|
231
|
+
return `Always Allow ${parts.join(" and ")}`;
|
|
232
|
+
}
|
|
233
|
+
// Implement the ACP Agent interface
|
|
234
|
+
export class ClaudeAcpAgent {
|
|
235
|
+
constructor(client, logger) {
|
|
236
|
+
this.backgroundTerminals = {};
|
|
237
|
+
this.sessions = {};
|
|
238
|
+
this.client = client;
|
|
239
|
+
this.toolUseCache = {};
|
|
240
|
+
this.logger = logger ?? console;
|
|
241
|
+
}
|
|
242
|
+
async initialize(request) {
|
|
243
|
+
this.clientCapabilities = request.clientCapabilities;
|
|
244
|
+
// Bypasses standard auth by routing requests through a custom Anthropic-protocol gateway.
|
|
245
|
+
// Only offered when the client advertises `auth._meta.gateway` capability.
|
|
246
|
+
const supportsGatewayAuth = request.clientCapabilities?.auth?._meta?.gateway === true;
|
|
247
|
+
const gatewayAuthMethod = {
|
|
248
|
+
id: "gateway",
|
|
249
|
+
name: "Custom model gateway",
|
|
250
|
+
description: "Use a custom gateway to authenticate and access models",
|
|
251
|
+
_meta: {
|
|
252
|
+
gateway: {
|
|
253
|
+
protocol: "anthropic",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
const gatewayBedrockAuthMethod = {
|
|
258
|
+
id: "gateway-bedrock",
|
|
259
|
+
name: "Custom model gateway",
|
|
260
|
+
description: "Use a custom gateway to authenticate and access models",
|
|
261
|
+
_meta: {
|
|
262
|
+
gateway: {
|
|
263
|
+
protocol: "bedrock",
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
const supportsTerminalAuth = request.clientCapabilities?.auth?.terminal === true;
|
|
268
|
+
const supportsMetaTerminalAuth = request.clientCapabilities?._meta?.["terminal-auth"] === true;
|
|
269
|
+
// Detect remote environments where the OAuth browser redirect to localhost
|
|
270
|
+
// won't work. This matches the SDK's internal isRemote check. In these cases,
|
|
271
|
+
// the `auth login` subcommand would fall back to a device-code-like manual
|
|
272
|
+
// flow, which doesn't work well over ACP, so we offer the TUI login instead.
|
|
273
|
+
const isRemote = !!(process.env.NO_BROWSER ||
|
|
274
|
+
process.env.SSH_CONNECTION ||
|
|
275
|
+
process.env.SSH_CLIENT ||
|
|
276
|
+
process.env.SSH_TTY ||
|
|
277
|
+
process.env.CLAUDE_CODE_REMOTE);
|
|
278
|
+
const terminalAuthMethods = [];
|
|
279
|
+
if (isRemote) {
|
|
280
|
+
const remoteLoginMethod = {
|
|
281
|
+
description: "Run `claude /login` in the terminal",
|
|
282
|
+
name: "Log in with Claude",
|
|
283
|
+
id: "claude-login",
|
|
284
|
+
type: "terminal",
|
|
285
|
+
args: ["--cli"],
|
|
286
|
+
};
|
|
287
|
+
if (supportsMetaTerminalAuth) {
|
|
288
|
+
remoteLoginMethod._meta = {
|
|
289
|
+
"terminal-auth": {
|
|
290
|
+
command: process.execPath,
|
|
291
|
+
args: [...process.argv.slice(1), "--cli"],
|
|
292
|
+
label: "Claude Login",
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (!shouldHideClaudeAuth() && (supportsTerminalAuth || supportsMetaTerminalAuth)) {
|
|
297
|
+
terminalAuthMethods.push(remoteLoginMethod);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
const claudeLoginMethod = {
|
|
302
|
+
description: "Use Claude subscription ",
|
|
303
|
+
name: "Claude Subscription",
|
|
304
|
+
id: "claude-ai-login",
|
|
305
|
+
type: "terminal",
|
|
306
|
+
args: ["--cli", "auth", "login", "--claudeai"],
|
|
307
|
+
};
|
|
308
|
+
const consoleLoginMethod = {
|
|
309
|
+
description: "Use Anthropic Console (API usage billing)",
|
|
310
|
+
name: "Anthropic Console",
|
|
311
|
+
id: "console-login",
|
|
312
|
+
type: "terminal",
|
|
313
|
+
args: ["--cli", "auth", "login", "--console"],
|
|
314
|
+
};
|
|
315
|
+
if (supportsMetaTerminalAuth) {
|
|
316
|
+
const baseArgs = process.argv.slice(1);
|
|
317
|
+
claudeLoginMethod._meta = {
|
|
318
|
+
"terminal-auth": {
|
|
319
|
+
command: process.execPath,
|
|
320
|
+
args: [...baseArgs, "--cli", "auth", "login", "--claudeai"],
|
|
321
|
+
label: "Claude Login",
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
consoleLoginMethod._meta = {
|
|
325
|
+
"terminal-auth": {
|
|
326
|
+
command: process.execPath,
|
|
327
|
+
args: [...baseArgs, "--cli", "auth", "login", "--console"],
|
|
328
|
+
label: "Anthropic Console Login",
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (!shouldHideClaudeAuth() && (supportsTerminalAuth || supportsMetaTerminalAuth)) {
|
|
333
|
+
terminalAuthMethods.push(claudeLoginMethod);
|
|
334
|
+
}
|
|
335
|
+
if (supportsTerminalAuth || supportsMetaTerminalAuth) {
|
|
336
|
+
terminalAuthMethods.push(consoleLoginMethod);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
protocolVersion: 1,
|
|
341
|
+
agentCapabilities: {
|
|
342
|
+
_meta: {
|
|
343
|
+
claudeCode: {
|
|
344
|
+
promptQueueing: true,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
promptCapabilities: {
|
|
348
|
+
image: true,
|
|
349
|
+
embeddedContext: true,
|
|
350
|
+
},
|
|
351
|
+
mcpCapabilities: {
|
|
352
|
+
http: true,
|
|
353
|
+
sse: true,
|
|
354
|
+
},
|
|
355
|
+
loadSession: true,
|
|
356
|
+
sessionCapabilities: {
|
|
357
|
+
additionalDirectories: {},
|
|
358
|
+
close: {},
|
|
359
|
+
delete: {},
|
|
360
|
+
fork: {},
|
|
361
|
+
list: {},
|
|
362
|
+
resume: {},
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
agentInfo: {
|
|
366
|
+
name: packageJson.name,
|
|
367
|
+
title: "Claude Agent",
|
|
368
|
+
version: packageJson.version,
|
|
369
|
+
},
|
|
370
|
+
authMethods: [
|
|
371
|
+
...terminalAuthMethods,
|
|
372
|
+
...(supportsGatewayAuth ? [gatewayAuthMethod, gatewayBedrockAuthMethod] : []),
|
|
373
|
+
],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async newSession(params) {
|
|
377
|
+
const response = await this.createSession(params, {
|
|
378
|
+
// Revisit these meta values once we support resume
|
|
379
|
+
resume: params._meta?.claudeCode?.options?.resume,
|
|
380
|
+
});
|
|
381
|
+
// Needs to happen after we return the session
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
this.sendAvailableCommandsUpdate(response.sessionId);
|
|
384
|
+
}, 0);
|
|
385
|
+
return response;
|
|
386
|
+
}
|
|
387
|
+
async unstable_forkSession(params) {
|
|
388
|
+
const response = await this.createSession({
|
|
389
|
+
cwd: params.cwd,
|
|
390
|
+
mcpServers: params.mcpServers ?? [],
|
|
391
|
+
additionalDirectories: params.additionalDirectories,
|
|
392
|
+
_meta: params._meta,
|
|
393
|
+
}, {
|
|
394
|
+
resume: params.sessionId,
|
|
395
|
+
forkSession: true,
|
|
396
|
+
});
|
|
397
|
+
// Needs to happen after we return the session
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
this.sendAvailableCommandsUpdate(response.sessionId);
|
|
400
|
+
}, 0);
|
|
401
|
+
return response;
|
|
402
|
+
}
|
|
403
|
+
async resumeSession(params) {
|
|
404
|
+
const result = await this.getOrCreateSession(params);
|
|
405
|
+
// Needs to happen after we return the session
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
this.sendAvailableCommandsUpdate(params.sessionId);
|
|
408
|
+
}, 0);
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
async loadSession(params) {
|
|
412
|
+
const result = await this.getOrCreateSession(params);
|
|
413
|
+
await this.replaySessionHistory(params.sessionId);
|
|
414
|
+
// Send available commands after replay so it doesn't interleave with history
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
this.sendAvailableCommandsUpdate(params.sessionId);
|
|
417
|
+
}, 0);
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
async listSessions(params) {
|
|
421
|
+
const sdk_sessions = await listSessions({ dir: params.cwd ?? undefined });
|
|
422
|
+
const sessions = [];
|
|
423
|
+
for (const session of sdk_sessions) {
|
|
424
|
+
if (!session.cwd)
|
|
425
|
+
continue;
|
|
426
|
+
sessions.push({
|
|
427
|
+
sessionId: session.sessionId,
|
|
428
|
+
cwd: session.cwd,
|
|
429
|
+
title: sanitizeTitle(session.summary),
|
|
430
|
+
updatedAt: new Date(session.lastModified).toISOString(),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
sessions,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async authenticate(_params) {
|
|
438
|
+
if (_params.methodId === "gateway" || _params.methodId === "gateway-bedrock") {
|
|
439
|
+
this.gatewayAuthRequest = _params;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
throw new Error("Method not implemented.");
|
|
443
|
+
}
|
|
444
|
+
async prompt(params) {
|
|
445
|
+
const session = this.sessions[params.sessionId];
|
|
446
|
+
if (!session) {
|
|
447
|
+
throw new Error("Session not found");
|
|
448
|
+
}
|
|
449
|
+
session.cancelled = false;
|
|
450
|
+
session.accumulatedUsage = {
|
|
451
|
+
inputTokens: 0,
|
|
452
|
+
outputTokens: 0,
|
|
453
|
+
cachedReadTokens: 0,
|
|
454
|
+
cachedWriteTokens: 0,
|
|
455
|
+
};
|
|
456
|
+
let lastAssistantTotalUsage = null;
|
|
457
|
+
let lastAssistantUsage = null;
|
|
458
|
+
let lastAssistantModel = null;
|
|
459
|
+
// When the Claude SDK classifies a turn as failed (e.g. rate limit, auth
|
|
460
|
+
// problem, billing), it sets a categorical `error` field on the
|
|
461
|
+
// `SDKAssistantMessage` that precedes the final `result` message. We
|
|
462
|
+
// capture it here so the subsequent `RequestError.internalError` can
|
|
463
|
+
// forward it to clients as structured `data`, sparing them from
|
|
464
|
+
// pattern-matching on the human-readable message text.
|
|
465
|
+
let lastAssistantError;
|
|
466
|
+
// Tracks whether we're inside a compaction. The SDK emits the terminal
|
|
467
|
+
// `status` (compact_result success/failed) twice for a single failed
|
|
468
|
+
// compaction, and the two messages are indistinguishable — so we report the
|
|
469
|
+
// outcome only while a compaction is in progress, then clear this. A fresh
|
|
470
|
+
// `compacting` status sets it again, so every distinct compaction (e.g.
|
|
471
|
+
// repeated auto-compactions in a long turn) is still shown.
|
|
472
|
+
let compactionInProgress = false;
|
|
473
|
+
const userMessage = promptToClaude(params);
|
|
474
|
+
const promptUuid = randomUUID();
|
|
475
|
+
userMessage.uuid = promptUuid;
|
|
476
|
+
// These local-only commands return a result without replaying the user
|
|
477
|
+
// message. Mark promptReplayed=true so their result isn't consumed as a
|
|
478
|
+
// background task result.
|
|
479
|
+
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
480
|
+
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
481
|
+
if (session.promptRunning) {
|
|
482
|
+
session.input.push(userMessage);
|
|
483
|
+
const order = session.nextPendingOrder++;
|
|
484
|
+
const cancelled = await new Promise((resolve) => {
|
|
485
|
+
session.pendingMessages.set(promptUuid, { resolve, order });
|
|
486
|
+
});
|
|
487
|
+
if (cancelled) {
|
|
488
|
+
return { stopReason: "cancelled" };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
session.input.push(userMessage);
|
|
493
|
+
}
|
|
494
|
+
session.promptRunning = true;
|
|
495
|
+
let handedOff = false;
|
|
496
|
+
let errored = false;
|
|
497
|
+
let stopReason = "end_turn";
|
|
498
|
+
try {
|
|
499
|
+
while (true) {
|
|
500
|
+
const { value: message, done } = await session.query.next();
|
|
501
|
+
if (done || !message) {
|
|
502
|
+
if (session.cancelled) {
|
|
503
|
+
return { stopReason: "cancelled" };
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (session.emitRawSDKMessages &&
|
|
508
|
+
shouldEmitRawMessage(session.emitRawSDKMessages, message)) {
|
|
509
|
+
await this.client.extNotification("_claude/sdkMessage", {
|
|
510
|
+
sessionId: params.sessionId,
|
|
511
|
+
message: message,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
switch (message.type) {
|
|
515
|
+
case "system":
|
|
516
|
+
switch (message.subtype) {
|
|
517
|
+
case "init":
|
|
518
|
+
break;
|
|
519
|
+
case "status": {
|
|
520
|
+
if (message.status === "compacting") {
|
|
521
|
+
compactionInProgress = true;
|
|
522
|
+
await this.client.sessionUpdate({
|
|
523
|
+
sessionId: message.session_id,
|
|
524
|
+
update: {
|
|
525
|
+
sessionUpdate: "agent_message_chunk",
|
|
526
|
+
content: { type: "text", text: "Compacting..." },
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
else if (message.compact_result === "success" && compactionInProgress) {
|
|
531
|
+
// The SDK signals manual `/compact` completion with a status
|
|
532
|
+
// message carrying `compact_result`, not the `compact_boundary`
|
|
533
|
+
// message (which only fires when there's content to compact).
|
|
534
|
+
compactionInProgress = false;
|
|
535
|
+
await this.client.sessionUpdate({
|
|
536
|
+
sessionId: message.session_id,
|
|
537
|
+
update: {
|
|
538
|
+
sessionUpdate: "agent_message_chunk",
|
|
539
|
+
content: { type: "text", text: "\n\nCompacting completed." },
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
else if (message.compact_result === "failed" && compactionInProgress) {
|
|
544
|
+
compactionInProgress = false;
|
|
545
|
+
const reason = message.compact_error ? `: ${message.compact_error}` : ".";
|
|
546
|
+
await this.client.sessionUpdate({
|
|
547
|
+
sessionId: message.session_id,
|
|
548
|
+
update: {
|
|
549
|
+
sessionUpdate: "agent_message_chunk",
|
|
550
|
+
content: { type: "text", text: `\n\nCompacting failed${reason}` },
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
case "compact_boundary": {
|
|
557
|
+
// Send used:0 immediately so the client doesn't keep showing
|
|
558
|
+
// the stale pre-compaction context size until the next turn.
|
|
559
|
+
//
|
|
560
|
+
// This is a deliberate approximation: we don't know the exact
|
|
561
|
+
// post-compaction token count (only the SDK's next API call
|
|
562
|
+
// reveals that). But used:0 is directionally correct — context
|
|
563
|
+
// just dropped dramatically — and the real value replaces it
|
|
564
|
+
// within seconds when the next result message arrives.
|
|
565
|
+
// The alternative (no update) leaves the client showing e.g.
|
|
566
|
+
// "944k/1m" right after the user sees "Compacting completed",
|
|
567
|
+
// which is confusing and wrong.
|
|
568
|
+
//
|
|
569
|
+
// The "Compacting completed." text is emitted from the `status`
|
|
570
|
+
// handler (keyed on `compact_result`), not here, so the failure
|
|
571
|
+
// path gets a message too.
|
|
572
|
+
lastAssistantTotalUsage = 0;
|
|
573
|
+
lastAssistantUsage = null;
|
|
574
|
+
await this.client.sessionUpdate({
|
|
575
|
+
sessionId: message.session_id,
|
|
576
|
+
update: {
|
|
577
|
+
sessionUpdate: "usage_update",
|
|
578
|
+
used: 0,
|
|
579
|
+
size: session.contextWindowSize,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case "local_command_output": {
|
|
585
|
+
await this.client.sessionUpdate({
|
|
586
|
+
sessionId: message.session_id,
|
|
587
|
+
update: {
|
|
588
|
+
sessionUpdate: "agent_message_chunk",
|
|
589
|
+
content: { type: "text", text: message.content },
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
case "session_state_changed": {
|
|
595
|
+
if (message.state === "idle") {
|
|
596
|
+
if (session.cancelled) {
|
|
597
|
+
stopReason = "cancelled";
|
|
598
|
+
}
|
|
599
|
+
return { stopReason, usage: sessionUsage(session) };
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case "memory_recall": {
|
|
604
|
+
const isSynthesis = message.mode === "synthesize";
|
|
605
|
+
const locations = isSynthesis
|
|
606
|
+
? []
|
|
607
|
+
: message.memories.map((m) => ({ path: m.path }));
|
|
608
|
+
const content = isSynthesis
|
|
609
|
+
? message.memories
|
|
610
|
+
.filter((m) => typeof m.content === "string")
|
|
611
|
+
.map((m) => ({
|
|
612
|
+
type: "content",
|
|
613
|
+
content: { type: "text", text: m.content },
|
|
614
|
+
}))
|
|
615
|
+
: [];
|
|
616
|
+
const count = message.memories.length;
|
|
617
|
+
const title = isSynthesis
|
|
618
|
+
? "Recalled synthesized memory"
|
|
619
|
+
: `Recalled ${count} ${count === 1 ? "memory" : "memories"}`;
|
|
620
|
+
await this.client.sessionUpdate({
|
|
621
|
+
sessionId: message.session_id,
|
|
622
|
+
update: {
|
|
623
|
+
sessionUpdate: "tool_call",
|
|
624
|
+
toolCallId: message.uuid,
|
|
625
|
+
title,
|
|
626
|
+
kind: "read",
|
|
627
|
+
status: "completed",
|
|
628
|
+
...(locations.length > 0 && { locations }),
|
|
629
|
+
...(content.length > 0 && { content }),
|
|
630
|
+
_meta: {
|
|
631
|
+
claudeCode: {
|
|
632
|
+
toolName: "memory_recall",
|
|
633
|
+
toolResponse: { mode: message.mode },
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
case "hook_started":
|
|
641
|
+
case "hook_progress":
|
|
642
|
+
case "hook_response":
|
|
643
|
+
case "files_persisted":
|
|
644
|
+
case "task_started":
|
|
645
|
+
case "task_notification":
|
|
646
|
+
case "task_progress":
|
|
647
|
+
case "task_updated":
|
|
648
|
+
case "elicitation_complete":
|
|
649
|
+
case "plugin_install":
|
|
650
|
+
case "notification":
|
|
651
|
+
case "api_retry":
|
|
652
|
+
case "mirror_error":
|
|
653
|
+
case "permission_denied":
|
|
654
|
+
case "thinking_tokens":
|
|
655
|
+
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
656
|
+
break;
|
|
657
|
+
default:
|
|
658
|
+
unreachable(message, this.logger);
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
case "result": {
|
|
663
|
+
// Accumulate usage from this result
|
|
664
|
+
session.accumulatedUsage.inputTokens += message.usage.input_tokens;
|
|
665
|
+
session.accumulatedUsage.outputTokens += message.usage.output_tokens;
|
|
666
|
+
session.accumulatedUsage.cachedReadTokens += message.usage.cache_read_input_tokens;
|
|
667
|
+
session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens;
|
|
668
|
+
const matchingModelUsage = lastAssistantModel
|
|
669
|
+
? getMatchingModelUsage(message.modelUsage, lastAssistantModel)
|
|
670
|
+
: null;
|
|
671
|
+
// Only overwrite when we have an authoritative value — a miss
|
|
672
|
+
// (e.g. a turn with no top-level assistant message) would
|
|
673
|
+
// otherwise discard the window learned on a prior turn and
|
|
674
|
+
// leave the next prompt's mid-stream updates reporting 200k.
|
|
675
|
+
if (matchingModelUsage) {
|
|
676
|
+
session.contextWindowSize = matchingModelUsage.contextWindow;
|
|
677
|
+
}
|
|
678
|
+
// Task-notification followups are autonomous work triggered by a
|
|
679
|
+
// task-notification system message, not by the user's prompt.
|
|
680
|
+
// They should not influence the user-turn lifecycle (stop reason,
|
|
681
|
+
// slash-command output forwarding) but their cost is real.
|
|
682
|
+
const isTaskNotification = message.origin?.kind === "task-notification";
|
|
683
|
+
// Send usage_update notification
|
|
684
|
+
if (lastAssistantTotalUsage !== null) {
|
|
685
|
+
await this.client.sessionUpdate({
|
|
686
|
+
sessionId: params.sessionId,
|
|
687
|
+
update: {
|
|
688
|
+
sessionUpdate: "usage_update",
|
|
689
|
+
used: lastAssistantTotalUsage,
|
|
690
|
+
size: session.contextWindowSize,
|
|
691
|
+
cost: {
|
|
692
|
+
amount: message.total_cost_usd,
|
|
693
|
+
currency: "USD",
|
|
694
|
+
},
|
|
695
|
+
...(message.origin && {
|
|
696
|
+
_meta: { "_claude/origin": message.origin },
|
|
697
|
+
}),
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
if (session.cancelled) {
|
|
702
|
+
if (!isTaskNotification) {
|
|
703
|
+
stopReason = "cancelled";
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
switch (message.subtype) {
|
|
708
|
+
case "success": {
|
|
709
|
+
if (message.result.includes("Please run /login")) {
|
|
710
|
+
throw RequestError.authRequired();
|
|
711
|
+
}
|
|
712
|
+
if (message.stop_reason === "max_tokens") {
|
|
713
|
+
if (!isTaskNotification) {
|
|
714
|
+
stopReason = "max_tokens";
|
|
715
|
+
}
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
if (message.is_error) {
|
|
719
|
+
throw RequestError.internalError(errorKindData(lastAssistantError), message.result);
|
|
720
|
+
}
|
|
721
|
+
// For local-only commands (no model invocation), the result
|
|
722
|
+
// text is the command output — forward it to the client.
|
|
723
|
+
// Task-notification followups never originate from a user
|
|
724
|
+
// slash command, so skip the forwarding for them.
|
|
725
|
+
if (isLocalOnlyCommand && !isTaskNotification) {
|
|
726
|
+
for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
727
|
+
await this.client.sessionUpdate(notification);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case "error_during_execution": {
|
|
733
|
+
if (message.stop_reason === "max_tokens") {
|
|
734
|
+
if (!isTaskNotification) {
|
|
735
|
+
stopReason = "max_tokens";
|
|
736
|
+
}
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
if (message.is_error) {
|
|
740
|
+
throw RequestError.internalError(errorKindData(lastAssistantError), message.errors.join(", ") || message.subtype);
|
|
741
|
+
}
|
|
742
|
+
if (!isTaskNotification) {
|
|
743
|
+
stopReason = "end_turn";
|
|
744
|
+
}
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "error_max_budget_usd":
|
|
748
|
+
case "error_max_turns":
|
|
749
|
+
case "error_max_structured_output_retries":
|
|
750
|
+
if (message.is_error) {
|
|
751
|
+
throw RequestError.internalError(errorKindData(lastAssistantError), message.errors.join(", ") || message.subtype);
|
|
752
|
+
}
|
|
753
|
+
if (!isTaskNotification) {
|
|
754
|
+
stopReason = "max_turn_requests";
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
default:
|
|
758
|
+
unreachable(message, this.logger);
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case "stream_event": {
|
|
764
|
+
if (message.parent_tool_use_id === null &&
|
|
765
|
+
(message.event.type === "message_start" || message.event.type === "message_delta")) {
|
|
766
|
+
if (message.event.type === "message_start") {
|
|
767
|
+
lastAssistantUsage = snapshotFromUsage(message.event.message.usage);
|
|
768
|
+
const model = message.event.message.model;
|
|
769
|
+
if (model && model !== "<synthetic>") {
|
|
770
|
+
lastAssistantModel = model;
|
|
771
|
+
// Only upgrade from the default — once a `result` has given
|
|
772
|
+
// us an authoritative window, trust it over the heuristic.
|
|
773
|
+
// Model switches invalidate the cached window via
|
|
774
|
+
// `syncSessionConfigState`, which resets us back to the
|
|
775
|
+
// default so this branch runs again for the new model.
|
|
776
|
+
if (session.contextWindowSize === DEFAULT_CONTEXT_WINDOW) {
|
|
777
|
+
const inferred = inferContextWindowFromModel(model);
|
|
778
|
+
if (inferred !== null) {
|
|
779
|
+
session.contextWindowSize = inferred;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
const usage = message.event.usage;
|
|
786
|
+
const prev = lastAssistantUsage ?? ZERO_USAGE;
|
|
787
|
+
// Per Anthropic API, message_delta usage fields are *cumulative*;
|
|
788
|
+
// nullable fields (input_tokens and the cache fields) fall back
|
|
789
|
+
// to the prior snapshot when the server omits them from this
|
|
790
|
+
// delta. Only output_tokens is guaranteed non-null.
|
|
791
|
+
lastAssistantUsage = {
|
|
792
|
+
input_tokens: usage.input_tokens ?? prev.input_tokens,
|
|
793
|
+
output_tokens: usage.output_tokens,
|
|
794
|
+
cache_read_input_tokens: usage.cache_read_input_tokens ?? prev.cache_read_input_tokens,
|
|
795
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? prev.cache_creation_input_tokens,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const nextUsage = totalTokens(lastAssistantUsage);
|
|
799
|
+
if (nextUsage !== lastAssistantTotalUsage) {
|
|
800
|
+
lastAssistantTotalUsage = nextUsage;
|
|
801
|
+
await this.client.sessionUpdate({
|
|
802
|
+
sessionId: params.sessionId,
|
|
803
|
+
update: {
|
|
804
|
+
sessionUpdate: "usage_update",
|
|
805
|
+
used: nextUsage,
|
|
806
|
+
size: session.contextWindowSize,
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger, {
|
|
812
|
+
clientCapabilities: this.clientCapabilities,
|
|
813
|
+
cwd: session.cwd,
|
|
814
|
+
taskState: session.taskState,
|
|
815
|
+
})) {
|
|
816
|
+
await this.client.sessionUpdate(notification);
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case "user":
|
|
821
|
+
case "assistant": {
|
|
822
|
+
if (session.cancelled) {
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
// Check for prompt replay
|
|
826
|
+
if (message.type === "user" && "uuid" in message && message.uuid) {
|
|
827
|
+
if (message.uuid === promptUuid) {
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
const pending = session.pendingMessages.get(message.uuid);
|
|
831
|
+
if (pending) {
|
|
832
|
+
pending.resolve(false);
|
|
833
|
+
session.pendingMessages.delete(message.uuid);
|
|
834
|
+
handedOff = true;
|
|
835
|
+
// the current loop stops with end_turn,
|
|
836
|
+
// the loop of the next prompt continues running
|
|
837
|
+
return { stopReason: "end_turn", usage: sessionUsage(session) };
|
|
838
|
+
}
|
|
839
|
+
if ("isReplay" in message && message.isReplay) {
|
|
840
|
+
// not pending or unrelated replay message
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Snapshot the latest top-level assistant usage and model so the
|
|
845
|
+
// next `result` can emit a usage_update tied to the right context
|
|
846
|
+
// window. Subagent messages are excluded to keep the snapshot
|
|
847
|
+
// aligned with what the user's current selection is producing.
|
|
848
|
+
if (message.type === "assistant" && message.parent_tool_use_id === null) {
|
|
849
|
+
lastAssistantUsage = snapshotFromUsage(message.message.usage);
|
|
850
|
+
lastAssistantTotalUsage = totalTokens(lastAssistantUsage);
|
|
851
|
+
if (message.message.model && message.message.model !== "<synthetic>") {
|
|
852
|
+
lastAssistantModel = message.message.model;
|
|
853
|
+
}
|
|
854
|
+
if (message.error) {
|
|
855
|
+
lastAssistantError = message.error;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// symphony-patch: every assistant message carries the final token
|
|
859
|
+
// usage of exactly one model call (subagent calls included via
|
|
860
|
+
// parent_tool_use_id), so surface it as a per-call bucket on
|
|
861
|
+
// usage_update._meta for call-by-call accounting downstream.
|
|
862
|
+
if (message.type === "assistant" &&
|
|
863
|
+
message.message.usage &&
|
|
864
|
+
message.message.model !== "<synthetic>") {
|
|
865
|
+
const callUsage = message.message.usage;
|
|
866
|
+
session.symphonyCallSeq = (session.symphonyCallSeq ?? 0) + 1;
|
|
867
|
+
await this.client.sessionUpdate({
|
|
868
|
+
sessionId: params.sessionId,
|
|
869
|
+
update: {
|
|
870
|
+
sessionUpdate: "usage_update",
|
|
871
|
+
used: lastAssistantTotalUsage ?? 0,
|
|
872
|
+
size: session.contextWindowSize,
|
|
873
|
+
_meta: {
|
|
874
|
+
"symphony/callUsage": {
|
|
875
|
+
seq: session.symphonyCallSeq,
|
|
876
|
+
inputTokens: callUsage.input_tokens ?? 0,
|
|
877
|
+
outputTokens: callUsage.output_tokens ?? 0,
|
|
878
|
+
cachedReadTokens: callUsage.cache_read_input_tokens ?? 0,
|
|
879
|
+
cachedWriteTokens: callUsage.cache_creation_input_tokens ?? 0,
|
|
880
|
+
totalTokens: (callUsage.input_tokens ?? 0) +
|
|
881
|
+
(callUsage.output_tokens ?? 0) +
|
|
882
|
+
(callUsage.cache_read_input_tokens ?? 0) +
|
|
883
|
+
(callUsage.cache_creation_input_tokens ?? 0),
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
// Strip <command-*>/<local-command-stdout> markers and render any
|
|
890
|
+
// remaining prose. Skill bodies and built-in slash commands (e.g.
|
|
891
|
+
// /usage, /status, /model) arrive wrapped in these tags; pure-marker
|
|
892
|
+
// payloads (e.g. /compact's malformed output) strip to null and are
|
|
893
|
+
// skipped. Mirrors the replay path at replaySessionHistory.
|
|
894
|
+
if (message.message.role !== "system" &&
|
|
895
|
+
typeof message.message.content === "string" &&
|
|
896
|
+
message.message.content.includes("<local-command-stdout>")) {
|
|
897
|
+
const stripped = stripLocalCommandMetadata(message.message.content);
|
|
898
|
+
if (typeof stripped === "string") {
|
|
899
|
+
for (const notification of toAcpNotifications(stripped, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
|
|
900
|
+
clientCapabilities: this.clientCapabilities,
|
|
901
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
902
|
+
cwd: session.cwd,
|
|
903
|
+
taskState: session.taskState,
|
|
904
|
+
})) {
|
|
905
|
+
await this.client.sessionUpdate(notification);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
this.logger.log(message.message.content);
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
if (typeof message.message.content === "string" &&
|
|
914
|
+
message.message.content.includes("<local-command-stderr>")) {
|
|
915
|
+
this.logger.error(message.message.content);
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
// Skip these user messages for now, since they seem to just be messages we don't want in the feed
|
|
919
|
+
if (message.type === "user" &&
|
|
920
|
+
(typeof message.message.content === "string" ||
|
|
921
|
+
(Array.isArray(message.message.content) &&
|
|
922
|
+
message.message.content.length === 1 &&
|
|
923
|
+
message.message.content[0].type === "text"))) {
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
if (message.message.role === "system") {
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
if (message.type === "assistant" &&
|
|
930
|
+
message.message.model === "<synthetic>" &&
|
|
931
|
+
Array.isArray(message.message.content) &&
|
|
932
|
+
message.message.content.length === 1 &&
|
|
933
|
+
message.message.content[0].type === "text" &&
|
|
934
|
+
message.message.content[0].text.includes("Please run /login")) {
|
|
935
|
+
throw RequestError.authRequired();
|
|
936
|
+
}
|
|
937
|
+
const content = message.type === "assistant"
|
|
938
|
+
? // Handled by stream events above
|
|
939
|
+
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
940
|
+
: message.message.content;
|
|
941
|
+
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
|
|
942
|
+
clientCapabilities: this.clientCapabilities,
|
|
943
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
944
|
+
cwd: session.cwd,
|
|
945
|
+
taskState: session.taskState,
|
|
946
|
+
})) {
|
|
947
|
+
await this.client.sessionUpdate(notification);
|
|
948
|
+
}
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
case "tool_progress":
|
|
952
|
+
case "tool_use_summary":
|
|
953
|
+
case "auth_status":
|
|
954
|
+
case "prompt_suggestion":
|
|
955
|
+
case "rate_limit_event":
|
|
956
|
+
break;
|
|
957
|
+
default:
|
|
958
|
+
unreachable(message);
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
throw new Error("Session did not end in result");
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
errored = true;
|
|
966
|
+
// A failed turn typically leaves a trailing `session_state_changed: idle`
|
|
967
|
+
// (and possibly more) in the query iterator. If we don't drain it here,
|
|
968
|
+
// the next prompt's first `query.next()` consumes that stale idle and
|
|
969
|
+
// short-circuits to end_turn with zero usage
|
|
970
|
+
// Bounded so a misbehaving SDK can't hang the next prompt indefinitely.
|
|
971
|
+
try {
|
|
972
|
+
await session.query.interrupt();
|
|
973
|
+
const MAX_DRAIN = 100;
|
|
974
|
+
for (let i = 0; i < MAX_DRAIN; i++) {
|
|
975
|
+
const { value: m, done } = await session.query.next();
|
|
976
|
+
if (done || !m)
|
|
977
|
+
break;
|
|
978
|
+
if (m.type === "system" && m.subtype === "session_state_changed" && m.state === "idle") {
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
if (i === MAX_DRAIN - 1) {
|
|
982
|
+
this.logger.error(`Session ${params.sessionId}: drained ${MAX_DRAIN} messages after error without observing idle`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch (drainErr) {
|
|
987
|
+
this.logger.error(`Session ${params.sessionId}: failed to drain query after prompt error:`, drainErr);
|
|
988
|
+
}
|
|
989
|
+
if (error instanceof RequestError || !(error instanceof Error)) {
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
const message = error.message;
|
|
993
|
+
if (message.includes("ProcessTransport") ||
|
|
994
|
+
message.includes("terminated process") ||
|
|
995
|
+
message.includes("process exited with") ||
|
|
996
|
+
message.includes("process terminated by signal") ||
|
|
997
|
+
message.includes("Failed to write to process stdin")) {
|
|
998
|
+
this.logger.error(`Session ${params.sessionId}: Claude Agent process died: ${message}`);
|
|
999
|
+
session.settingsManager.dispose();
|
|
1000
|
+
session.input.end();
|
|
1001
|
+
delete this.sessions[params.sessionId];
|
|
1002
|
+
throw RequestError.internalError(undefined, "The Claude Agent process exited unexpectedly. Please start a new session.");
|
|
1003
|
+
}
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
if (!handedOff) {
|
|
1008
|
+
session.promptRunning = false;
|
|
1009
|
+
if (errored) {
|
|
1010
|
+
// The query stream was just drained — handing pending prompts off
|
|
1011
|
+
// onto it would let them race with the recovery. Cancel them so
|
|
1012
|
+
// each waiting prompt() returns stopReason: "cancelled" and the
|
|
1013
|
+
// client can decide whether to retry.
|
|
1014
|
+
for (const pending of session.pendingMessages.values()) {
|
|
1015
|
+
pending.resolve(true);
|
|
1016
|
+
}
|
|
1017
|
+
session.pendingMessages.clear();
|
|
1018
|
+
}
|
|
1019
|
+
else if (session.pendingMessages.size > 0) {
|
|
1020
|
+
// This usually should not happen, but in case the loop finishes
|
|
1021
|
+
// without claude sending all message replays, we resolve the
|
|
1022
|
+
// next pending prompt call to ensure no prompts get stuck.
|
|
1023
|
+
const next = [...session.pendingMessages.entries()].sort((a, b) => a[1].order - b[1].order)[0];
|
|
1024
|
+
if (next) {
|
|
1025
|
+
next[1].resolve(false);
|
|
1026
|
+
session.pendingMessages.delete(next[0]);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async cancel(params) {
|
|
1033
|
+
const session = this.sessions[params.sessionId];
|
|
1034
|
+
if (!session) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
session.cancelled = true;
|
|
1038
|
+
for (const [, pending] of session.pendingMessages) {
|
|
1039
|
+
pending.resolve(true);
|
|
1040
|
+
}
|
|
1041
|
+
session.pendingMessages.clear();
|
|
1042
|
+
await session.query.interrupt();
|
|
1043
|
+
}
|
|
1044
|
+
/** Cleanly tear down a session: cancel in-flight work, dispose resources,
|
|
1045
|
+
* and remove it from the session map. */
|
|
1046
|
+
async teardownSession(sessionId) {
|
|
1047
|
+
const session = this.sessions[sessionId];
|
|
1048
|
+
if (!session) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
await this.cancel({ sessionId });
|
|
1052
|
+
session.settingsManager.dispose();
|
|
1053
|
+
session.abortController.abort();
|
|
1054
|
+
session.query.close();
|
|
1055
|
+
delete this.sessions[sessionId];
|
|
1056
|
+
}
|
|
1057
|
+
/** Tear down all active sessions. Called when the ACP connection closes. */
|
|
1058
|
+
async dispose() {
|
|
1059
|
+
await Promise.all(Object.keys(this.sessions).map((id) => this.teardownSession(id)));
|
|
1060
|
+
}
|
|
1061
|
+
async closeSession(params) {
|
|
1062
|
+
if (!this.sessions[params.sessionId]) {
|
|
1063
|
+
throw new Error("Session not found");
|
|
1064
|
+
}
|
|
1065
|
+
await this.teardownSession(params.sessionId);
|
|
1066
|
+
return {};
|
|
1067
|
+
}
|
|
1068
|
+
async unstable_deleteSession(params) {
|
|
1069
|
+
// Tear down any active in-memory state first so the on-disk file isn't
|
|
1070
|
+
// recreated by an outstanding query writing to it.
|
|
1071
|
+
if (this.sessions[params.sessionId]) {
|
|
1072
|
+
await this.teardownSession(params.sessionId);
|
|
1073
|
+
}
|
|
1074
|
+
await deleteSession(params.sessionId);
|
|
1075
|
+
return {};
|
|
1076
|
+
}
|
|
1077
|
+
async setSessionMode(params) {
|
|
1078
|
+
if (!this.sessions[params.sessionId]) {
|
|
1079
|
+
throw new Error("Session not found");
|
|
1080
|
+
}
|
|
1081
|
+
await this.applySessionMode(params.sessionId, params.modeId);
|
|
1082
|
+
await this.updateConfigOption(params.sessionId, "mode", params.modeId);
|
|
1083
|
+
return {};
|
|
1084
|
+
}
|
|
1085
|
+
async setSessionConfigOption(params) {
|
|
1086
|
+
const session = this.sessions[params.sessionId];
|
|
1087
|
+
if (!session) {
|
|
1088
|
+
throw new Error("Session not found");
|
|
1089
|
+
}
|
|
1090
|
+
if (typeof params.value !== "string") {
|
|
1091
|
+
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
|
1092
|
+
}
|
|
1093
|
+
const option = session.configOptions.find((o) => o.id === params.configId);
|
|
1094
|
+
if (!option) {
|
|
1095
|
+
throw new Error(`Unknown config option: ${params.configId}`);
|
|
1096
|
+
}
|
|
1097
|
+
const allValues = "options" in option && Array.isArray(option.options)
|
|
1098
|
+
? option.options.flatMap((o) => ("options" in o ? o.options : [o]))
|
|
1099
|
+
: [];
|
|
1100
|
+
let validValue = allValues.find((o) => o.value === params.value);
|
|
1101
|
+
// For model options, fall back to resolveModelPreference when the exact
|
|
1102
|
+
// value doesn't match. This lets callers use human-friendly aliases like
|
|
1103
|
+
// "opus" or "sonnet" instead of full model IDs like "claude-opus-4-6".
|
|
1104
|
+
if (!validValue && params.configId === "model") {
|
|
1105
|
+
const modelInfos = allValues.map((o) => ({
|
|
1106
|
+
value: o.value,
|
|
1107
|
+
displayName: o.name,
|
|
1108
|
+
description: o.description ?? "",
|
|
1109
|
+
}));
|
|
1110
|
+
const resolved = resolveModelPreference(modelInfos, params.value);
|
|
1111
|
+
if (resolved) {
|
|
1112
|
+
validValue = allValues.find((o) => o.value === resolved.value);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
if (!validValue) {
|
|
1116
|
+
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
|
1117
|
+
}
|
|
1118
|
+
// Use the canonical option value so downstream code always receives the
|
|
1119
|
+
// model ID rather than the caller-supplied alias.
|
|
1120
|
+
const resolvedValue = validValue.value;
|
|
1121
|
+
if (params.configId === "mode") {
|
|
1122
|
+
await this.applySessionMode(params.sessionId, resolvedValue);
|
|
1123
|
+
await this.client.sessionUpdate({
|
|
1124
|
+
sessionId: params.sessionId,
|
|
1125
|
+
update: {
|
|
1126
|
+
sessionUpdate: "current_mode_update",
|
|
1127
|
+
currentModeId: resolvedValue,
|
|
1128
|
+
},
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
else if (params.configId === "model") {
|
|
1132
|
+
await this.sessions[params.sessionId].query.setModel(resolvedValue);
|
|
1133
|
+
}
|
|
1134
|
+
// Effort SDK sync is handled inside applyConfigOptionValue so that direct
|
|
1135
|
+
// effort changes and effort changes induced by a model switch go through
|
|
1136
|
+
// the same path.
|
|
1137
|
+
await this.applyConfigOptionValue(params.sessionId, session, params.configId, resolvedValue);
|
|
1138
|
+
return { configOptions: session.configOptions };
|
|
1139
|
+
}
|
|
1140
|
+
async applySessionMode(sessionId, modeId) {
|
|
1141
|
+
switch (modeId) {
|
|
1142
|
+
case "auto":
|
|
1143
|
+
case "default":
|
|
1144
|
+
case "acceptEdits":
|
|
1145
|
+
case "bypassPermissions":
|
|
1146
|
+
case "dontAsk":
|
|
1147
|
+
case "plan":
|
|
1148
|
+
break;
|
|
1149
|
+
default:
|
|
1150
|
+
throw new Error("Invalid Mode");
|
|
1151
|
+
}
|
|
1152
|
+
const session = this.sessions[sessionId];
|
|
1153
|
+
if (!session) {
|
|
1154
|
+
throw new Error("Session not found");
|
|
1155
|
+
}
|
|
1156
|
+
if (!session.modes.availableModes.some((mode) => mode.id === modeId)) {
|
|
1157
|
+
throw new Error(`Mode ${modeId} is not available in this session`);
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
await session.query.setPermissionMode(modeId);
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
if (error instanceof Error) {
|
|
1164
|
+
if (!error.message) {
|
|
1165
|
+
error.message = "Invalid Mode";
|
|
1166
|
+
}
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
// eslint-disable-next-line preserve-caught-error
|
|
1171
|
+
throw new Error("Invalid Mode");
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async replaySessionHistory(sessionId) {
|
|
1176
|
+
const toolUseCache = {};
|
|
1177
|
+
const messages = await getSessionMessages(sessionId);
|
|
1178
|
+
for (const message of messages) {
|
|
1179
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
1180
|
+
let content = message.message.content;
|
|
1181
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
1182
|
+
if (message.message.role === "user") {
|
|
1183
|
+
content = stripLocalCommandMetadata(content);
|
|
1184
|
+
if (content === null)
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
for (const notification of toAcpNotifications(
|
|
1188
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
1189
|
+
content,
|
|
1190
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
1191
|
+
message.message.role, sessionId, toolUseCache, this.client, this.logger, {
|
|
1192
|
+
registerHooks: false,
|
|
1193
|
+
clientCapabilities: this.clientCapabilities,
|
|
1194
|
+
cwd: this.sessions[sessionId]?.cwd,
|
|
1195
|
+
taskState: this.sessions[sessionId]?.taskState,
|
|
1196
|
+
})) {
|
|
1197
|
+
await this.client.sessionUpdate(notification);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async readTextFile(params) {
|
|
1202
|
+
const response = await this.client.readTextFile(params);
|
|
1203
|
+
return response;
|
|
1204
|
+
}
|
|
1205
|
+
async writeTextFile(params) {
|
|
1206
|
+
const response = await this.client.writeTextFile(params);
|
|
1207
|
+
return response;
|
|
1208
|
+
}
|
|
1209
|
+
canUseTool(sessionId) {
|
|
1210
|
+
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
|
|
1211
|
+
const alwaysAllowLabel = describeAlwaysAllow(suggestions, toolName);
|
|
1212
|
+
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
1213
|
+
const session = this.sessions[sessionId];
|
|
1214
|
+
if (!session) {
|
|
1215
|
+
return {
|
|
1216
|
+
behavior: "deny",
|
|
1217
|
+
message: "Session not found",
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
if (toolName === "ExitPlanMode") {
|
|
1221
|
+
const optionsAll = [
|
|
1222
|
+
{ kind: "allow_always", name: 'Yes, and use "auto" mode', optionId: "auto" },
|
|
1223
|
+
{
|
|
1224
|
+
kind: "allow_always",
|
|
1225
|
+
name: "Yes, and auto-accept edits",
|
|
1226
|
+
optionId: "acceptEdits",
|
|
1227
|
+
},
|
|
1228
|
+
{ kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
|
|
1229
|
+
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
|
1230
|
+
];
|
|
1231
|
+
if (ALLOW_BYPASS) {
|
|
1232
|
+
optionsAll.unshift({
|
|
1233
|
+
kind: "allow_always",
|
|
1234
|
+
name: "Yes, and bypass permissions",
|
|
1235
|
+
optionId: "bypassPermissions",
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
// Filter against the session's currently-advertised modes so we never
|
|
1239
|
+
// present options the active model can't honor (e.g. `auto` on Haiku).
|
|
1240
|
+
// `bypassPermissions` is already covered by `availableModes` via
|
|
1241
|
+
// `buildAvailableModes`/`ALLOW_BYPASS`. The `plan` option is a
|
|
1242
|
+
// "keep planning" reject path; it's always present in `availableModes`.
|
|
1243
|
+
const options = optionsAll.filter((o) => session.modes.availableModes.some((m) => m.id === o.optionId));
|
|
1244
|
+
const response = await this.client.requestPermission({
|
|
1245
|
+
options,
|
|
1246
|
+
sessionId,
|
|
1247
|
+
toolCall: {
|
|
1248
|
+
toolCallId: toolUseID,
|
|
1249
|
+
rawInput: toolInput,
|
|
1250
|
+
...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
1254
|
+
throw new Error("Tool use aborted");
|
|
1255
|
+
}
|
|
1256
|
+
const selectedMode = response.outcome?.outcome === "selected" ? response.outcome.optionId : undefined;
|
|
1257
|
+
const selectedModeWasOffered = options.some((option) => option.optionId === selectedMode);
|
|
1258
|
+
if (selectedModeWasOffered &&
|
|
1259
|
+
(selectedMode === "default" ||
|
|
1260
|
+
selectedMode === "acceptEdits" ||
|
|
1261
|
+
selectedMode === "auto" ||
|
|
1262
|
+
selectedMode === "bypassPermissions")) {
|
|
1263
|
+
await this.client.sessionUpdate({
|
|
1264
|
+
sessionId,
|
|
1265
|
+
update: {
|
|
1266
|
+
sessionUpdate: "current_mode_update",
|
|
1267
|
+
currentModeId: selectedMode,
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
await this.updateConfigOption(sessionId, "mode", selectedMode);
|
|
1271
|
+
return {
|
|
1272
|
+
behavior: "allow",
|
|
1273
|
+
updatedInput: toolInput,
|
|
1274
|
+
updatedPermissions: suggestions ?? [
|
|
1275
|
+
{ type: "setMode", mode: selectedMode, destination: "session" },
|
|
1276
|
+
],
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
return {
|
|
1281
|
+
behavior: "deny",
|
|
1282
|
+
message: "User rejected request to exit plan mode.",
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (session.modes.currentModeId === "bypassPermissions") {
|
|
1287
|
+
return {
|
|
1288
|
+
behavior: "allow",
|
|
1289
|
+
updatedInput: toolInput,
|
|
1290
|
+
updatedPermissions: suggestions ?? [
|
|
1291
|
+
{ type: "addRules", rules: [{ toolName }], behavior: "allow", destination: "session" },
|
|
1292
|
+
],
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
const response = await this.client.requestPermission({
|
|
1296
|
+
options: [
|
|
1297
|
+
{
|
|
1298
|
+
kind: "allow_always",
|
|
1299
|
+
name: alwaysAllowLabel,
|
|
1300
|
+
optionId: "allow_always",
|
|
1301
|
+
},
|
|
1302
|
+
{ kind: "allow_once", name: "Allow", optionId: "allow" },
|
|
1303
|
+
{ kind: "reject_once", name: "Reject", optionId: "reject" },
|
|
1304
|
+
],
|
|
1305
|
+
sessionId,
|
|
1306
|
+
toolCall: {
|
|
1307
|
+
toolCallId: toolUseID,
|
|
1308
|
+
rawInput: toolInput,
|
|
1309
|
+
...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
|
|
1310
|
+
},
|
|
1311
|
+
});
|
|
1312
|
+
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
1313
|
+
throw new Error("Tool use aborted");
|
|
1314
|
+
}
|
|
1315
|
+
if (response.outcome?.outcome === "selected" &&
|
|
1316
|
+
(response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
|
|
1317
|
+
// If Claude Code has suggestions, it will update their settings already
|
|
1318
|
+
if (response.outcome.optionId === "allow_always") {
|
|
1319
|
+
return {
|
|
1320
|
+
behavior: "allow",
|
|
1321
|
+
updatedInput: toolInput,
|
|
1322
|
+
updatedPermissions: suggestions ?? [
|
|
1323
|
+
{
|
|
1324
|
+
type: "addRules",
|
|
1325
|
+
rules: [{ toolName }],
|
|
1326
|
+
behavior: "allow",
|
|
1327
|
+
destination: "session",
|
|
1328
|
+
},
|
|
1329
|
+
],
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
behavior: "allow",
|
|
1334
|
+
updatedInput: toolInput,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
return {
|
|
1339
|
+
behavior: "deny",
|
|
1340
|
+
message: "User refused permission to run tool",
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
async sendAvailableCommandsUpdate(sessionId) {
|
|
1346
|
+
const session = this.sessions[sessionId];
|
|
1347
|
+
if (!session)
|
|
1348
|
+
return;
|
|
1349
|
+
const commands = await session.query.supportedCommands();
|
|
1350
|
+
await this.client.sessionUpdate({
|
|
1351
|
+
sessionId,
|
|
1352
|
+
update: {
|
|
1353
|
+
sessionUpdate: "available_commands_update",
|
|
1354
|
+
availableCommands: getAvailableSlashCommands(commands),
|
|
1355
|
+
},
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
async updateConfigOption(sessionId, configId, value) {
|
|
1359
|
+
const session = this.sessions[sessionId];
|
|
1360
|
+
if (!session)
|
|
1361
|
+
return;
|
|
1362
|
+
await this.applyConfigOptionValue(sessionId, session, configId, value);
|
|
1363
|
+
await this.client.sessionUpdate({
|
|
1364
|
+
sessionId,
|
|
1365
|
+
update: {
|
|
1366
|
+
sessionUpdate: "config_option_update",
|
|
1367
|
+
configOptions: session.configOptions,
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
async applyConfigOptionValue(sessionId, session, configId, value) {
|
|
1372
|
+
if (configId === "mode") {
|
|
1373
|
+
session.modes = { ...session.modes, currentModeId: value };
|
|
1374
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
1375
|
+
}
|
|
1376
|
+
else if (configId === "model") {
|
|
1377
|
+
if (session.models.currentModelId !== value) {
|
|
1378
|
+
// The cached context window was learned for the previous model; reset
|
|
1379
|
+
// to the new model's heuristic so mid-stream updates between now and
|
|
1380
|
+
// the next `result` reflect the user's selection instead of the old
|
|
1381
|
+
// model's window.
|
|
1382
|
+
session.contextWindowSize = inferContextWindowFromModel(value) ?? DEFAULT_CONTEXT_WINDOW;
|
|
1383
|
+
}
|
|
1384
|
+
session.models = { ...session.models, currentModelId: value };
|
|
1385
|
+
// Recompute availableModes for the new model and clamp the current
|
|
1386
|
+
// mode if the SDK no longer offers it (today: "auto" on Haiku).
|
|
1387
|
+
// `ModelInfo.supportsAutoMode` is the canonical SDK signal.
|
|
1388
|
+
const newModelInfo = session.modelInfos.find((m) => m.value === value);
|
|
1389
|
+
const newAvailableModes = buildAvailableModes(newModelInfo);
|
|
1390
|
+
// Capture BEFORE mutating session.modes so the log message reflects
|
|
1391
|
+
// the invalidated mode rather than "default".
|
|
1392
|
+
const previousModeId = session.modes.currentModeId;
|
|
1393
|
+
let modeDowngraded = false;
|
|
1394
|
+
if (!newAvailableModes.some((m) => m.id === previousModeId)) {
|
|
1395
|
+
session.modes = {
|
|
1396
|
+
availableModes: newAvailableModes,
|
|
1397
|
+
currentModeId: "default",
|
|
1398
|
+
};
|
|
1399
|
+
try {
|
|
1400
|
+
await session.query.setPermissionMode("default");
|
|
1401
|
+
}
|
|
1402
|
+
catch (err) {
|
|
1403
|
+
// Failing the entire model switch over a bookkeeping sync error is
|
|
1404
|
+
// worse UX than logging and continuing; the user explicitly asked
|
|
1405
|
+
// to change models. The next setPermissionMode from the user will
|
|
1406
|
+
// either succeed or surface a fresh error.
|
|
1407
|
+
this.logger.error(`Failed to sync permissionMode to "default" after model switch invalidated "${previousModeId}":`, err);
|
|
1408
|
+
}
|
|
1409
|
+
modeDowngraded = true;
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
session.modes = { ...session.modes, availableModes: newAvailableModes };
|
|
1413
|
+
}
|
|
1414
|
+
// Rebuild config options since effort levels depend on the selected model
|
|
1415
|
+
const effortOpt = session.configOptions.find((o) => o.id === "effort");
|
|
1416
|
+
const currentEffort = typeof effortOpt?.currentValue === "string" ? effortOpt.currentValue : undefined;
|
|
1417
|
+
session.configOptions = buildConfigOptions(session.modes, session.models, session.modelInfos, currentEffort);
|
|
1418
|
+
// Sync effort with the SDK if it changed after the model switch
|
|
1419
|
+
const newEffortOpt = session.configOptions.find((o) => o.id === "effort");
|
|
1420
|
+
const newEffort = typeof newEffortOpt?.currentValue === "string" ? newEffortOpt.currentValue : undefined;
|
|
1421
|
+
if (newEffort !== currentEffort) {
|
|
1422
|
+
await session.query.applyFlagSettings({
|
|
1423
|
+
effortLevel: toSdkEffortLevel(newEffort),
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
// Emit current_mode_update only after session.modes AND
|
|
1427
|
+
// session.configOptions have been fully reconciled. This way, a failure
|
|
1428
|
+
// in the configOptions/effort rebuild above can't leave the client with
|
|
1429
|
+
// a clamped currentModeId but stale configOptions, and the notification
|
|
1430
|
+
// still precedes the caller's config_option_update so order-sensitive
|
|
1431
|
+
// clients update currentModeId before re-rendering the option list.
|
|
1432
|
+
if (modeDowngraded) {
|
|
1433
|
+
await this.client.sessionUpdate({
|
|
1434
|
+
sessionId,
|
|
1435
|
+
update: {
|
|
1436
|
+
sessionUpdate: "current_mode_update",
|
|
1437
|
+
currentModeId: "default",
|
|
1438
|
+
},
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
1444
|
+
if (configId === "effort") {
|
|
1445
|
+
await session.query.applyFlagSettings({
|
|
1446
|
+
effortLevel: toSdkEffortLevel(value),
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
async getOrCreateSession(params) {
|
|
1452
|
+
const existingSession = this.sessions[params.sessionId];
|
|
1453
|
+
if (existingSession) {
|
|
1454
|
+
const fingerprint = computeSessionFingerprint(params);
|
|
1455
|
+
if (fingerprint === existingSession.sessionFingerprint) {
|
|
1456
|
+
return {
|
|
1457
|
+
sessionId: params.sessionId,
|
|
1458
|
+
modes: existingSession.modes,
|
|
1459
|
+
configOptions: existingSession.configOptions,
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
// Session-defining params changed (e.g. cwd pointed at a git worktree,
|
|
1463
|
+
// or MCP servers reconfigured). Tear down the existing session and
|
|
1464
|
+
// recreate it so the underlying Query process picks up the new values.
|
|
1465
|
+
await this.teardownSession(params.sessionId);
|
|
1466
|
+
}
|
|
1467
|
+
const response = await this.createSession({
|
|
1468
|
+
cwd: params.cwd,
|
|
1469
|
+
mcpServers: params.mcpServers ?? [],
|
|
1470
|
+
additionalDirectories: params.additionalDirectories,
|
|
1471
|
+
_meta: params._meta,
|
|
1472
|
+
}, {
|
|
1473
|
+
resume: params.sessionId,
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
sessionId: response.sessionId,
|
|
1477
|
+
modes: response.modes,
|
|
1478
|
+
configOptions: response.configOptions,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
async createSession(params, creationOpts = {}) {
|
|
1482
|
+
// We want to create a new session id unless it is resume,
|
|
1483
|
+
// but not resume + forkSession.
|
|
1484
|
+
let sessionId;
|
|
1485
|
+
if (creationOpts.forkSession) {
|
|
1486
|
+
sessionId = randomUUID();
|
|
1487
|
+
}
|
|
1488
|
+
else if (creationOpts.resume) {
|
|
1489
|
+
sessionId = creationOpts.resume;
|
|
1490
|
+
}
|
|
1491
|
+
else {
|
|
1492
|
+
sessionId = randomUUID();
|
|
1493
|
+
}
|
|
1494
|
+
const input = new Pushable();
|
|
1495
|
+
const settingsManager = new SettingsManager(params.cwd, {
|
|
1496
|
+
logger: this.logger,
|
|
1497
|
+
});
|
|
1498
|
+
await settingsManager.initialize();
|
|
1499
|
+
// symphony-patch: overlay per-session settings supplied via session
|
|
1500
|
+
// _meta (settings.json shape) on top of the resolved file settings.
|
|
1501
|
+
// Everything the bridge derives from settings (permissions.defaultMode,
|
|
1502
|
+
// model, availableModels, effortLevel) then works per session without
|
|
1503
|
+
// writing settings files into the workspace.
|
|
1504
|
+
const symphonySettings = params._meta?.["symphony/settings"];
|
|
1505
|
+
if (symphonySettings &&
|
|
1506
|
+
typeof symphonySettings === "object" &&
|
|
1507
|
+
!Array.isArray(symphonySettings)) {
|
|
1508
|
+
const baseGetSettings = settingsManager.getSettings.bind(settingsManager);
|
|
1509
|
+
settingsManager.getSettings = () => mergeSymphonySettings(baseGetSettings(), symphonySettings);
|
|
1510
|
+
}
|
|
1511
|
+
const mcpServers = {};
|
|
1512
|
+
if (Array.isArray(params.mcpServers)) {
|
|
1513
|
+
for (const server of params.mcpServers) {
|
|
1514
|
+
if ("type" in server && (server.type === "http" || server.type === "sse")) {
|
|
1515
|
+
// HTTP or SSE type MCP server
|
|
1516
|
+
mcpServers[server.name] = {
|
|
1517
|
+
type: server.type,
|
|
1518
|
+
url: server.url,
|
|
1519
|
+
headers: server.headers
|
|
1520
|
+
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
|
1521
|
+
: undefined,
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
else if (!("type" in server)) {
|
|
1525
|
+
// Stdio type MCP server (with or without explicit type field)
|
|
1526
|
+
mcpServers[server.name] = {
|
|
1527
|
+
type: "stdio",
|
|
1528
|
+
command: server.command,
|
|
1529
|
+
args: server.args,
|
|
1530
|
+
env: server.env
|
|
1531
|
+
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
|
1532
|
+
: undefined,
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
let systemPrompt = { type: "preset", preset: "claude_code" };
|
|
1538
|
+
if (params._meta?.systemPrompt) {
|
|
1539
|
+
const customPrompt = params._meta.systemPrompt;
|
|
1540
|
+
if (typeof customPrompt === "string") {
|
|
1541
|
+
systemPrompt = customPrompt;
|
|
1542
|
+
}
|
|
1543
|
+
else if (typeof customPrompt === "object" &&
|
|
1544
|
+
customPrompt !== null &&
|
|
1545
|
+
!Array.isArray(customPrompt)) {
|
|
1546
|
+
// Forward all preset options (append, excludeDynamicSections, and
|
|
1547
|
+
// anything the SDK adds later) while locking type/preset.
|
|
1548
|
+
systemPrompt = {
|
|
1549
|
+
...customPrompt,
|
|
1550
|
+
type: "preset",
|
|
1551
|
+
preset: "claude_code",
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const permissionMode = resolvePermissionMode(settingsManager.getSettings().permissions?.defaultMode, this.logger);
|
|
1556
|
+
// Extract options from _meta if provided
|
|
1557
|
+
const sessionMeta = params._meta;
|
|
1558
|
+
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
|
1559
|
+
// Configure thinking tokens from environment variable
|
|
1560
|
+
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
|
|
1561
|
+
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
|
1562
|
+
: undefined;
|
|
1563
|
+
// Parse model configuration from environment (e.g. Bedrock model overrides)
|
|
1564
|
+
const modelConfig = parseModelConfig(process.env.CLAUDE_MODEL_CONFIG);
|
|
1565
|
+
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
1566
|
+
const disallowedTools = ["AskUserQuestion"];
|
|
1567
|
+
// Resolve which built-in tools to expose.
|
|
1568
|
+
// Explicit tools array from _meta.claudeCode.options takes precedence.
|
|
1569
|
+
// disableBuiltInTools is a legacy shorthand for tools: [] — kept for
|
|
1570
|
+
// backward compatibility but callers should prefer the tools array.
|
|
1571
|
+
const tools = userProvidedOptions?.tools ??
|
|
1572
|
+
(params._meta?.disableBuiltInTools === true ? [] : { type: "preset", preset: "claude_code" });
|
|
1573
|
+
const abortController = userProvidedOptions?.abortController || new AbortController();
|
|
1574
|
+
// Per-session task state. Created here (rather than in the session record
|
|
1575
|
+
// below) so the TaskCreated/TaskCompleted hook callbacks can close over
|
|
1576
|
+
// the same Map that the streaming message handler will read from.
|
|
1577
|
+
const taskState = new Map();
|
|
1578
|
+
const options = {
|
|
1579
|
+
systemPrompt,
|
|
1580
|
+
settingSources: ["user", "project", "local"],
|
|
1581
|
+
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
|
|
1582
|
+
...userProvidedOptions,
|
|
1583
|
+
// CLAUDE_MODEL_CONFIG env var is a fallback for model
|
|
1584
|
+
// configuration (e.g. Bedrock model ID overrides). When the caller
|
|
1585
|
+
// provides settings via _meta, we intentionally ignore the env var —
|
|
1586
|
+
// the caller is assumed to have full control over model configuration.
|
|
1587
|
+
...(!userProvidedOptions?.settings &&
|
|
1588
|
+
modelConfig && {
|
|
1589
|
+
settings: {
|
|
1590
|
+
...(modelConfig.modelOverrides && { modelOverrides: modelConfig.modelOverrides }),
|
|
1591
|
+
...(modelConfig.availableModels && { availableModels: modelConfig.availableModels }),
|
|
1592
|
+
},
|
|
1593
|
+
}),
|
|
1594
|
+
env: {
|
|
1595
|
+
...process.env,
|
|
1596
|
+
...userProvidedOptions?.env,
|
|
1597
|
+
...createEnvForGateway(this.gatewayAuthRequest),
|
|
1598
|
+
// Opt-in to session state events like when the agent is idle
|
|
1599
|
+
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
1600
|
+
},
|
|
1601
|
+
// Override certain fields that must be controlled by ACP
|
|
1602
|
+
cwd: params.cwd,
|
|
1603
|
+
includePartialMessages: true,
|
|
1604
|
+
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
|
1605
|
+
// If we want bypassPermissions to be an option, we have to allow it here.
|
|
1606
|
+
// But it doesn't work in root mode, so we only activate it if it will work.
|
|
1607
|
+
allowDangerouslySkipPermissions: ALLOW_BYPASS,
|
|
1608
|
+
permissionMode,
|
|
1609
|
+
canUseTool: this.canUseTool(sessionId),
|
|
1610
|
+
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),
|
|
1611
|
+
extraArgs: {
|
|
1612
|
+
...userProvidedOptions?.extraArgs,
|
|
1613
|
+
"replay-user-messages": "",
|
|
1614
|
+
},
|
|
1615
|
+
disallowedTools: [...(userProvidedOptions?.disallowedTools || []), ...disallowedTools],
|
|
1616
|
+
tools,
|
|
1617
|
+
hooks: {
|
|
1618
|
+
...userProvidedOptions?.hooks,
|
|
1619
|
+
PostToolUse: [
|
|
1620
|
+
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
1621
|
+
{
|
|
1622
|
+
hooks: [
|
|
1623
|
+
createPostToolUseHook(this.logger, {
|
|
1624
|
+
onEnterPlanMode: async () => {
|
|
1625
|
+
await this.client.sessionUpdate({
|
|
1626
|
+
sessionId,
|
|
1627
|
+
update: {
|
|
1628
|
+
sessionUpdate: "current_mode_update",
|
|
1629
|
+
currentModeId: "plan",
|
|
1630
|
+
},
|
|
1631
|
+
});
|
|
1632
|
+
await this.updateConfigOption(sessionId, "mode", "plan");
|
|
1633
|
+
},
|
|
1634
|
+
}),
|
|
1635
|
+
],
|
|
1636
|
+
},
|
|
1637
|
+
],
|
|
1638
|
+
TaskCreated: [
|
|
1639
|
+
...(userProvidedOptions?.hooks?.TaskCreated || []),
|
|
1640
|
+
{
|
|
1641
|
+
hooks: [
|
|
1642
|
+
createTaskHook({
|
|
1643
|
+
taskState,
|
|
1644
|
+
onChange: async () => {
|
|
1645
|
+
await this.client.sessionUpdate({
|
|
1646
|
+
sessionId,
|
|
1647
|
+
update: {
|
|
1648
|
+
sessionUpdate: "plan",
|
|
1649
|
+
entries: taskStateToPlanEntries(taskState),
|
|
1650
|
+
},
|
|
1651
|
+
});
|
|
1652
|
+
},
|
|
1653
|
+
}),
|
|
1654
|
+
],
|
|
1655
|
+
},
|
|
1656
|
+
],
|
|
1657
|
+
TaskCompleted: [
|
|
1658
|
+
...(userProvidedOptions?.hooks?.TaskCompleted || []),
|
|
1659
|
+
{
|
|
1660
|
+
hooks: [
|
|
1661
|
+
createTaskHook({
|
|
1662
|
+
taskState,
|
|
1663
|
+
onChange: async () => {
|
|
1664
|
+
await this.client.sessionUpdate({
|
|
1665
|
+
sessionId,
|
|
1666
|
+
update: {
|
|
1667
|
+
sessionUpdate: "plan",
|
|
1668
|
+
entries: taskStateToPlanEntries(taskState),
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
},
|
|
1672
|
+
}),
|
|
1673
|
+
],
|
|
1674
|
+
},
|
|
1675
|
+
],
|
|
1676
|
+
},
|
|
1677
|
+
...creationOpts,
|
|
1678
|
+
abortController,
|
|
1679
|
+
};
|
|
1680
|
+
// Prefer the official ACP `additionalDirectories` field. Fall back to the
|
|
1681
|
+
// legacy `_meta.additionalRoots` extension for clients that haven't been
|
|
1682
|
+
// updated yet. Either source is merged with directories supplied via
|
|
1683
|
+
// `_meta.claudeCode.options.additionalDirectories` (SDK pass-through).
|
|
1684
|
+
const acpAdditionalDirectories = params.additionalDirectories ?? sessionMeta?.additionalRoots ?? [];
|
|
1685
|
+
options.additionalDirectories = [
|
|
1686
|
+
...(userProvidedOptions?.additionalDirectories ?? []),
|
|
1687
|
+
...acpAdditionalDirectories,
|
|
1688
|
+
];
|
|
1689
|
+
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
1690
|
+
// Set our own session id if not resuming an existing session.
|
|
1691
|
+
options.sessionId = sessionId;
|
|
1692
|
+
}
|
|
1693
|
+
// Handle abort controller from meta options
|
|
1694
|
+
if (abortController?.signal.aborted) {
|
|
1695
|
+
throw new Error("Cancelled");
|
|
1696
|
+
}
|
|
1697
|
+
const q = query({
|
|
1698
|
+
prompt: input,
|
|
1699
|
+
options,
|
|
1700
|
+
});
|
|
1701
|
+
let initializationResult;
|
|
1702
|
+
try {
|
|
1703
|
+
initializationResult = await q.initializationResult();
|
|
1704
|
+
}
|
|
1705
|
+
catch (error) {
|
|
1706
|
+
if (creationOpts.resume &&
|
|
1707
|
+
error instanceof Error &&
|
|
1708
|
+
(error.message === "Query closed before response received" ||
|
|
1709
|
+
error.message.includes("No conversation found with session ID"))) {
|
|
1710
|
+
throw RequestError.resourceNotFound(sessionId);
|
|
1711
|
+
}
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
if (shouldHideClaudeAuth() &&
|
|
1715
|
+
initializationResult.account.subscriptionType &&
|
|
1716
|
+
!this.gatewayAuthRequest) {
|
|
1717
|
+
throw RequestError.authRequired(undefined, "This integration does not support using claude.ai subscriptions.");
|
|
1718
|
+
}
|
|
1719
|
+
// Apply user's `availableModels` allowlist from settings.json before any
|
|
1720
|
+
// downstream model handling. The SDK only enforces this allowlist in its
|
|
1721
|
+
// own UI, not in `initializationResult.models`, so we filter here to keep
|
|
1722
|
+
// configOptions, the current-model resolver, and the stored modelInfos
|
|
1723
|
+
// consistent with what the user configured.
|
|
1724
|
+
const settingsAvailableModels = settingsManager.getSettings().availableModels;
|
|
1725
|
+
const allowedModels = Array.isArray(settingsAvailableModels)
|
|
1726
|
+
? applyAvailableModelsAllowlist(initializationResult.models, settingsAvailableModels)
|
|
1727
|
+
: initializationResult.models;
|
|
1728
|
+
const models = await getAvailableModels(q, allowedModels, initializationResult.models, settingsManager, this.logger);
|
|
1729
|
+
// Gate `auto` (and future model-specific modes) on the resolved model's
|
|
1730
|
+
// `ModelInfo`. See `buildAvailableModes` for the canonical SDK signal.
|
|
1731
|
+
const currentModelInfo = allowedModels.find((m) => m.value === models.currentModelId);
|
|
1732
|
+
const availableModes = buildAvailableModes(currentModelInfo);
|
|
1733
|
+
// Clamp `permissionMode` if the resolved session does not offer it. The
|
|
1734
|
+
// common case is `permissions.defaultMode: "auto"` resolving to a model
|
|
1735
|
+
// that does not support auto mode (e.g. Haiku); without this clamp the
|
|
1736
|
+
// SDK would later throw `"auto mode unavailable for this model"` from
|
|
1737
|
+
// `setPermissionMode`. Keep `permissionMode` as the resolved user intent
|
|
1738
|
+
// (matches what was passed into `options.permissionMode` above) and use
|
|
1739
|
+
// `effectiveMode` for the post-clamp value the session actually runs in.
|
|
1740
|
+
let effectiveMode = permissionMode;
|
|
1741
|
+
if (!availableModes.some((m) => m.id === effectiveMode)) {
|
|
1742
|
+
if (effectiveMode === "auto") {
|
|
1743
|
+
this.logger.error(`permissions.defaultMode "auto" is not available for model ` +
|
|
1744
|
+
`"${models.currentModelId}"; falling back to "default".`);
|
|
1745
|
+
}
|
|
1746
|
+
else {
|
|
1747
|
+
this.logger.error(`permissions.defaultMode "${effectiveMode}" is not available in ` +
|
|
1748
|
+
`this session; falling back to "default".`);
|
|
1749
|
+
}
|
|
1750
|
+
effectiveMode = "default";
|
|
1751
|
+
// Sync the SDK so it doesn't keep "auto" cached internally. Wrapped in
|
|
1752
|
+
// try/catch since failing here would abort session creation entirely.
|
|
1753
|
+
try {
|
|
1754
|
+
await q.setPermissionMode("default");
|
|
1755
|
+
}
|
|
1756
|
+
catch (err) {
|
|
1757
|
+
this.logger.error("Failed to sync clamped permissionMode to SDK:", err);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
const modes = {
|
|
1761
|
+
currentModeId: effectiveMode,
|
|
1762
|
+
availableModes,
|
|
1763
|
+
};
|
|
1764
|
+
const configOptions = buildConfigOptions(modes, models, allowedModels, settingsManager.getSettings().effortLevel);
|
|
1765
|
+
// Apply the initial effort level to the SDK so it matches the UI default
|
|
1766
|
+
const initialEffort = configOptions.find((o) => o.id === "effort");
|
|
1767
|
+
if (initialEffort &&
|
|
1768
|
+
typeof initialEffort.currentValue === "string" &&
|
|
1769
|
+
initialEffort.currentValue !== "default") {
|
|
1770
|
+
await q.applyFlagSettings({
|
|
1771
|
+
effortLevel: initialEffort.currentValue,
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
this.sessions[sessionId] = {
|
|
1775
|
+
query: q,
|
|
1776
|
+
input: input,
|
|
1777
|
+
cancelled: false,
|
|
1778
|
+
cwd: params.cwd,
|
|
1779
|
+
sessionFingerprint: computeSessionFingerprint(params),
|
|
1780
|
+
settingsManager,
|
|
1781
|
+
accumulatedUsage: {
|
|
1782
|
+
inputTokens: 0,
|
|
1783
|
+
outputTokens: 0,
|
|
1784
|
+
cachedReadTokens: 0,
|
|
1785
|
+
cachedWriteTokens: 0,
|
|
1786
|
+
},
|
|
1787
|
+
modes,
|
|
1788
|
+
models,
|
|
1789
|
+
modelInfos: allowedModels,
|
|
1790
|
+
configOptions,
|
|
1791
|
+
promptRunning: false,
|
|
1792
|
+
pendingMessages: new Map(),
|
|
1793
|
+
nextPendingOrder: 0,
|
|
1794
|
+
abortController,
|
|
1795
|
+
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
|
1796
|
+
contextWindowSize: inferContextWindowFromModel(models.currentModelId) ?? DEFAULT_CONTEXT_WINDOW,
|
|
1797
|
+
taskState,
|
|
1798
|
+
};
|
|
1799
|
+
return {
|
|
1800
|
+
sessionId,
|
|
1801
|
+
modes,
|
|
1802
|
+
configOptions,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function shouldEmitRawMessage(config, message) {
|
|
1807
|
+
if (config === true)
|
|
1808
|
+
return true;
|
|
1809
|
+
if (config === false)
|
|
1810
|
+
return false;
|
|
1811
|
+
return config.some((f) => f.type === message.type &&
|
|
1812
|
+
(f.subtype === undefined || f.subtype === message.subtype) &&
|
|
1813
|
+
(f.origin === undefined || f.origin === message.origin?.kind));
|
|
1814
|
+
}
|
|
1815
|
+
function sessionUsage(session) {
|
|
1816
|
+
return {
|
|
1817
|
+
inputTokens: session.accumulatedUsage.inputTokens,
|
|
1818
|
+
outputTokens: session.accumulatedUsage.outputTokens,
|
|
1819
|
+
cachedReadTokens: session.accumulatedUsage.cachedReadTokens,
|
|
1820
|
+
cachedWriteTokens: session.accumulatedUsage.cachedWriteTokens,
|
|
1821
|
+
totalTokens: session.accumulatedUsage.inputTokens +
|
|
1822
|
+
session.accumulatedUsage.outputTokens +
|
|
1823
|
+
session.accumulatedUsage.cachedReadTokens +
|
|
1824
|
+
session.accumulatedUsage.cachedWriteTokens,
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
/** Sum all four fields as a proxy for post-turn context occupancy: the current
|
|
1828
|
+
* turn's output becomes next turn's input. Per the Anthropic API, input_tokens
|
|
1829
|
+
* excludes cache tokens — cache_read and cache_creation are reported
|
|
1830
|
+
* separately — so summing all four is not double-counting. */
|
|
1831
|
+
function totalTokens(usage) {
|
|
1832
|
+
return (usage.input_tokens +
|
|
1833
|
+
usage.output_tokens +
|
|
1834
|
+
usage.cache_read_input_tokens +
|
|
1835
|
+
usage.cache_creation_input_tokens);
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Build the `data` payload attached to a `RequestError.internalError` when we
|
|
1839
|
+
* have a categorical error from the Claude SDK. Returns `undefined` when no
|
|
1840
|
+
* categorical error is available, matching the previous behavior of passing
|
|
1841
|
+
* `undefined` to `RequestError.internalError`.
|
|
1842
|
+
*
|
|
1843
|
+
* The `errorKind` field is a convention for ACP clients to dispatch on
|
|
1844
|
+
* without having to pattern-match the human-readable message text. Clients
|
|
1845
|
+
* that don't understand it fall back to the existing message-based rendering.
|
|
1846
|
+
*/
|
|
1847
|
+
function errorKindData(errorKind) {
|
|
1848
|
+
return errorKind ? { errorKind } : undefined;
|
|
1849
|
+
}
|
|
1850
|
+
// symphony-patch: two-level merge for per-session settings overlays. Nested
|
|
1851
|
+
// plain objects (e.g. permissions) merge key-wise so an overlay that only
|
|
1852
|
+
// sets permissions.defaultMode keeps the rest of the resolved permissions;
|
|
1853
|
+
// every other value replaces the base wholesale.
|
|
1854
|
+
function mergeSymphonySettings(base, overlay) {
|
|
1855
|
+
const merged = { ...base };
|
|
1856
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
1857
|
+
const baseValue = merged[key];
|
|
1858
|
+
const bothPlainObjects = value !== null &&
|
|
1859
|
+
typeof value === "object" &&
|
|
1860
|
+
!Array.isArray(value) &&
|
|
1861
|
+
baseValue !== null &&
|
|
1862
|
+
typeof baseValue === "object" &&
|
|
1863
|
+
!Array.isArray(baseValue);
|
|
1864
|
+
merged[key] = bothPlainObjects ? { ...baseValue, ...value } : value;
|
|
1865
|
+
}
|
|
1866
|
+
return merged;
|
|
1867
|
+
}
|
|
1868
|
+
/** Project a nullable API usage object into our non-null snapshot shape.
|
|
1869
|
+
* Both SDK message_start and assistant message `usage` have `number | null`
|
|
1870
|
+
* cache fields; we coerce absent values to 0 so `totalTokens` never hits
|
|
1871
|
+
* NaN. `input_tokens`/`output_tokens` are typed `number` by the SDK but
|
|
1872
|
+
* synthetic or third-party-backend stream events have been observed emitting
|
|
1873
|
+
* them as null/undefined — coerce those too so a malformed upstream event
|
|
1874
|
+
* can't leak NaN into the wire `used` field. Delta events have different
|
|
1875
|
+
* semantics (cumulative + prev fallback) and are handled inline. */
|
|
1876
|
+
function snapshotFromUsage(usage) {
|
|
1877
|
+
return {
|
|
1878
|
+
input_tokens: usage.input_tokens ?? 0,
|
|
1879
|
+
output_tokens: usage.output_tokens ?? 0,
|
|
1880
|
+
cache_read_input_tokens: usage.cache_read_input_tokens ?? 0,
|
|
1881
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens ?? 0,
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
function createEnvForGateway(request) {
|
|
1885
|
+
if (!request?._meta) {
|
|
1886
|
+
return {};
|
|
1887
|
+
}
|
|
1888
|
+
const customHeaders = Object.entries(request._meta.gateway.headers)
|
|
1889
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
1890
|
+
.join("\n");
|
|
1891
|
+
if (request.methodId === "gateway-bedrock") {
|
|
1892
|
+
return {
|
|
1893
|
+
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
1894
|
+
AWS_BEARER_TOKEN_BEDROCK: " ", // Must be non-empty to bypass pass configuration check
|
|
1895
|
+
ANTHROPIC_BEDROCK_BASE_URL: request._meta.gateway.baseUrl,
|
|
1896
|
+
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
return {
|
|
1900
|
+
ANTHROPIC_BASE_URL: request._meta.gateway.baseUrl,
|
|
1901
|
+
ANTHROPIC_CUSTOM_HEADERS: customHeaders,
|
|
1902
|
+
ANTHROPIC_AUTH_TOKEN: " ", // Must be specified to bypass claude login requirement
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Build the list of permission modes the agent will advertise for the given
|
|
1907
|
+
* model. `auto` is gated by `ModelInfo.supportsAutoMode === true`, which is
|
|
1908
|
+
* the SDK's model-level availability signal. `undefined`/`false` both exclude
|
|
1909
|
+
* `auto`. `bypassPermissions` is still gated by `ALLOW_BYPASS`.
|
|
1910
|
+
*/
|
|
1911
|
+
function buildAvailableModes(modelInfo) {
|
|
1912
|
+
const modes = [];
|
|
1913
|
+
// Only advertise "auto" when the SDK reports the model supports it.
|
|
1914
|
+
if (modelInfo?.supportsAutoMode === true) {
|
|
1915
|
+
modes.push({
|
|
1916
|
+
id: "auto",
|
|
1917
|
+
name: "Auto",
|
|
1918
|
+
description: "Use a model classifier to approve/deny permission prompts",
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
modes.push({
|
|
1922
|
+
id: "default",
|
|
1923
|
+
name: "Default",
|
|
1924
|
+
description: "Standard behavior, prompts for dangerous operations",
|
|
1925
|
+
}, {
|
|
1926
|
+
id: "acceptEdits",
|
|
1927
|
+
name: "Accept Edits",
|
|
1928
|
+
description: "Auto-accept file edit operations",
|
|
1929
|
+
}, {
|
|
1930
|
+
id: "plan",
|
|
1931
|
+
name: "Plan Mode",
|
|
1932
|
+
description: "Planning mode, no actual tool execution",
|
|
1933
|
+
}, {
|
|
1934
|
+
id: "dontAsk",
|
|
1935
|
+
name: "Don't Ask",
|
|
1936
|
+
description: "Don't prompt for permissions, deny if not pre-approved",
|
|
1937
|
+
});
|
|
1938
|
+
if (ALLOW_BYPASS) {
|
|
1939
|
+
modes.push({
|
|
1940
|
+
id: "bypassPermissions",
|
|
1941
|
+
name: "Bypass Permissions",
|
|
1942
|
+
description: "Bypass all permission checks",
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
return modes;
|
|
1946
|
+
}
|
|
1947
|
+
// Translate a UI effort value into the flag-layer payload. The SDK
|
|
1948
|
+
// shallow-merges `applyFlagSettings`, drops `undefined` during JSON transport,
|
|
1949
|
+
// and only clears a key when an explicit `null` is sent — see
|
|
1950
|
+
// `applyFlagSettings` in @anthropic-ai/claude-agent-sdk. Mapping both the
|
|
1951
|
+
// `"default"` sentinel and `undefined` (effort option absent for the model) to
|
|
1952
|
+
// `null` ensures any previously-applied flag is actually cleared.
|
|
1953
|
+
function toSdkEffortLevel(value) {
|
|
1954
|
+
return value === undefined || value === "default" ? null : value;
|
|
1955
|
+
}
|
|
1956
|
+
function buildConfigOptions(modes, models, modelInfos, currentEffortLevel) {
|
|
1957
|
+
const options = [
|
|
1958
|
+
{
|
|
1959
|
+
id: "mode",
|
|
1960
|
+
name: "Mode",
|
|
1961
|
+
description: "Session permission mode",
|
|
1962
|
+
category: "mode",
|
|
1963
|
+
type: "select",
|
|
1964
|
+
currentValue: modes.currentModeId,
|
|
1965
|
+
options: modes.availableModes.map((m) => ({
|
|
1966
|
+
value: m.id,
|
|
1967
|
+
name: m.name,
|
|
1968
|
+
description: m.description,
|
|
1969
|
+
})),
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
id: "model",
|
|
1973
|
+
name: "Model",
|
|
1974
|
+
description: "AI model to use",
|
|
1975
|
+
category: "model",
|
|
1976
|
+
type: "select",
|
|
1977
|
+
currentValue: models.currentModelId,
|
|
1978
|
+
options: models.availableModels.map((m) => ({
|
|
1979
|
+
value: m.modelId,
|
|
1980
|
+
name: m.name,
|
|
1981
|
+
description: m.description ?? undefined,
|
|
1982
|
+
})),
|
|
1983
|
+
},
|
|
1984
|
+
];
|
|
1985
|
+
// Add effort level option based on the currently selected model
|
|
1986
|
+
const currentModelInfo = modelInfos.find((m) => m.value === models.currentModelId);
|
|
1987
|
+
const supportedLevels = currentModelInfo?.supportsEffort
|
|
1988
|
+
? (currentModelInfo.supportedEffortLevels ?? [])
|
|
1989
|
+
: [];
|
|
1990
|
+
if (supportedLevels.length > 0) {
|
|
1991
|
+
const effortOptions = [
|
|
1992
|
+
{ value: "default", name: "Default" },
|
|
1993
|
+
...supportedLevels.map((level) => ({
|
|
1994
|
+
value: level,
|
|
1995
|
+
name: level
|
|
1996
|
+
.split(/[_-]/)
|
|
1997
|
+
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
1998
|
+
.join(" "),
|
|
1999
|
+
})),
|
|
2000
|
+
];
|
|
2001
|
+
const includes = (l) => l === "default" || supportedLevels.includes(l);
|
|
2002
|
+
const validEffort = currentEffortLevel && includes(currentEffortLevel) ? currentEffortLevel : "default";
|
|
2003
|
+
options.push({
|
|
2004
|
+
id: "effort",
|
|
2005
|
+
name: "Effort",
|
|
2006
|
+
description: "Available effort levels for this model",
|
|
2007
|
+
category: "thought_level",
|
|
2008
|
+
type: "select",
|
|
2009
|
+
currentValue: validEffort,
|
|
2010
|
+
options: effortOptions,
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
return options;
|
|
2014
|
+
}
|
|
2015
|
+
// Claude Code CLI persists display strings like "opus[1m]" in settings,
|
|
2016
|
+
// but the SDK model list uses IDs like "claude-opus-4-6-1m".
|
|
2017
|
+
const MODEL_CONTEXT_HINT_PATTERN = /\[(\d+m)\]$/i;
|
|
2018
|
+
// Captures a model family version such as `4-6` or `4.7` so we can keep
|
|
2019
|
+
// `claude-opus-4-6` from being copied onto the SDK's `opus` alias when that
|
|
2020
|
+
// alias currently resolves to a different family version (e.g. Opus 4.7).
|
|
2021
|
+
const MODEL_FAMILY_VERSION_PATTERN = /\b(\d+)[-.](\d+)\b/;
|
|
2022
|
+
function extractModelFamilyVersion(s) {
|
|
2023
|
+
const match = s.match(MODEL_FAMILY_VERSION_PATTERN);
|
|
2024
|
+
return match ? `${match[1]}.${match[2]}` : null;
|
|
2025
|
+
}
|
|
2026
|
+
function modelVersionsCompatible(preference, candidate) {
|
|
2027
|
+
const preferred = extractModelFamilyVersion(preference);
|
|
2028
|
+
if (!preferred)
|
|
2029
|
+
return true;
|
|
2030
|
+
const candidateVersion = extractModelFamilyVersion(candidate.value) ??
|
|
2031
|
+
extractModelFamilyVersion(candidate.displayName) ??
|
|
2032
|
+
extractModelFamilyVersion(candidate.description);
|
|
2033
|
+
if (!candidateVersion)
|
|
2034
|
+
return true;
|
|
2035
|
+
return preferred === candidateVersion;
|
|
2036
|
+
}
|
|
2037
|
+
function tokenizeModelPreference(model) {
|
|
2038
|
+
const lower = model.trim().toLowerCase();
|
|
2039
|
+
const contextHint = lower.match(MODEL_CONTEXT_HINT_PATTERN)?.[1]?.toLowerCase();
|
|
2040
|
+
const normalized = lower.replace(MODEL_CONTEXT_HINT_PATTERN, " $1 ");
|
|
2041
|
+
const rawTokens = normalized.split(/[^a-z0-9]+/).filter(Boolean);
|
|
2042
|
+
const tokens = rawTokens
|
|
2043
|
+
.map((token) => {
|
|
2044
|
+
if (token === "opusplan")
|
|
2045
|
+
return "opus";
|
|
2046
|
+
if (token === "best" || token === "default")
|
|
2047
|
+
return "";
|
|
2048
|
+
return token;
|
|
2049
|
+
})
|
|
2050
|
+
.filter((token) => token && token !== "claude")
|
|
2051
|
+
.filter((token) => /[a-z]/.test(token) || token.endsWith("m"));
|
|
2052
|
+
return { tokens, contextHint };
|
|
2053
|
+
}
|
|
2054
|
+
function scoreModelMatch(model, tokens, contextHint) {
|
|
2055
|
+
const haystack = `${model.value} ${model.displayName}`.toLowerCase();
|
|
2056
|
+
let score = 0;
|
|
2057
|
+
for (const token of tokens) {
|
|
2058
|
+
if (haystack.includes(token)) {
|
|
2059
|
+
score += token === contextHint ? 3 : 1;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return score;
|
|
2063
|
+
}
|
|
2064
|
+
function resolveModelPreference(models, preference) {
|
|
2065
|
+
const trimmed = preference.trim();
|
|
2066
|
+
if (!trimmed)
|
|
2067
|
+
return null;
|
|
2068
|
+
const lower = trimmed.toLowerCase();
|
|
2069
|
+
// Exact match on value or display name
|
|
2070
|
+
const directMatch = models.find((model) => model.value === trimmed ||
|
|
2071
|
+
model.value.toLowerCase() === lower ||
|
|
2072
|
+
model.displayName.toLowerCase() === lower);
|
|
2073
|
+
if (directMatch)
|
|
2074
|
+
return directMatch;
|
|
2075
|
+
// Substring match
|
|
2076
|
+
const includesMatch = models.find((model) => {
|
|
2077
|
+
if (!modelVersionsCompatible(trimmed, model))
|
|
2078
|
+
return false;
|
|
2079
|
+
const value = model.value.toLowerCase();
|
|
2080
|
+
const display = model.displayName.toLowerCase();
|
|
2081
|
+
return value.includes(lower) || display.includes(lower) || lower.includes(value);
|
|
2082
|
+
});
|
|
2083
|
+
if (includesMatch)
|
|
2084
|
+
return includesMatch;
|
|
2085
|
+
// Tokenized matching for aliases like "opus[1m]"
|
|
2086
|
+
const { tokens, contextHint } = tokenizeModelPreference(trimmed);
|
|
2087
|
+
if (tokens.length === 0)
|
|
2088
|
+
return null;
|
|
2089
|
+
let bestMatch = null;
|
|
2090
|
+
let bestScore = 0;
|
|
2091
|
+
for (const model of models) {
|
|
2092
|
+
if (!modelVersionsCompatible(trimmed, model))
|
|
2093
|
+
continue;
|
|
2094
|
+
const score = scoreModelMatch(model, tokens, contextHint);
|
|
2095
|
+
if (0 < score && (!bestMatch || bestScore < score)) {
|
|
2096
|
+
bestMatch = model;
|
|
2097
|
+
bestScore = score;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return bestMatch;
|
|
2101
|
+
}
|
|
2102
|
+
function resolveSettingsModel(models, settingsModel, logger) {
|
|
2103
|
+
if (settingsModel === undefined) {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
if (typeof settingsModel !== "string") {
|
|
2107
|
+
const typeLabel = settingsModel === null ? "null" : typeof settingsModel;
|
|
2108
|
+
logger.error(`Ignoring model from settings: expected a string, got ${typeLabel}.`);
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
return resolveModelPreference(models, settingsModel);
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Restrict the SDK's model list to the user's `availableModels` allowlist
|
|
2115
|
+
* (already merged-and-deduped across settings sources by `SettingsManager`).
|
|
2116
|
+
* The user's exact entries become the model IDs surfaced via configOptions
|
|
2117
|
+
* and passed to `setModel`, which prevents Claude Code from silently
|
|
2118
|
+
* substituting a date-pinned variant (e.g. `haiku` →
|
|
2119
|
+
* `claude-haiku-4-5-20251001`) that the user may not have access to.
|
|
2120
|
+
*
|
|
2121
|
+
* Display info and capability flags are copied from the closest SDK match so
|
|
2122
|
+
* the UI still renders sensible names and effort levels.
|
|
2123
|
+
*
|
|
2124
|
+
* Semantics from https://code.claude.com/docs/en/model-config#restrict-model-selection:
|
|
2125
|
+
* - `undefined` is handled by the caller (no allowlist applied).
|
|
2126
|
+
* - The Default option is unaffected by `availableModels` — it always remains
|
|
2127
|
+
* available, even when the allowlist is `[]`.
|
|
2128
|
+
*/
|
|
2129
|
+
function applyAvailableModelsAllowlist(sdkModels, allowlist) {
|
|
2130
|
+
// Default is always preserved per the docs. Synthesize one if the SDK
|
|
2131
|
+
// didn't surface it so downstream code (e.g. `getAvailableModels` picking
|
|
2132
|
+
// `models[0]` as a fallback) still has something to work with.
|
|
2133
|
+
const defaultModel = sdkModels.find((m) => m.value === "default") ?? {
|
|
2134
|
+
value: "default",
|
|
2135
|
+
displayName: "Default",
|
|
2136
|
+
description: "",
|
|
2137
|
+
};
|
|
2138
|
+
const result = [defaultModel];
|
|
2139
|
+
const seen = new Set([defaultModel.value]);
|
|
2140
|
+
const sdkModelsWithoutDefault = sdkModels.filter((m) => m.value !== "default");
|
|
2141
|
+
for (const entry of allowlist) {
|
|
2142
|
+
const trimmed = entry.trim();
|
|
2143
|
+
if (!trimmed || seen.has(trimmed))
|
|
2144
|
+
continue;
|
|
2145
|
+
const sdkMatch = resolveModelPreference(sdkModelsWithoutDefault, trimmed);
|
|
2146
|
+
if (sdkMatch) {
|
|
2147
|
+
result.push({ ...sdkMatch, value: trimmed });
|
|
2148
|
+
}
|
|
2149
|
+
else {
|
|
2150
|
+
result.push({ value: trimmed, displayName: trimmed, description: "" });
|
|
2151
|
+
}
|
|
2152
|
+
seen.add(trimmed);
|
|
2153
|
+
}
|
|
2154
|
+
return result;
|
|
2155
|
+
}
|
|
2156
|
+
async function getAvailableModels(query, models, sdkModels, settingsManager, logger) {
|
|
2157
|
+
const settings = settingsManager.getSettings();
|
|
2158
|
+
let currentModel = models[0];
|
|
2159
|
+
let resolvedFromInput;
|
|
2160
|
+
// Model priority (highest to lowest):
|
|
2161
|
+
// 1. ANTHROPIC_MODEL environment variable
|
|
2162
|
+
// 2. settings.model (user configuration)
|
|
2163
|
+
// 3. models[0] (default first model)
|
|
2164
|
+
if (process.env.ANTHROPIC_MODEL) {
|
|
2165
|
+
const match = resolveModelPreference(models, process.env.ANTHROPIC_MODEL);
|
|
2166
|
+
if (match) {
|
|
2167
|
+
currentModel = match;
|
|
2168
|
+
resolvedFromInput = process.env.ANTHROPIC_MODEL;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
else if (typeof settings.model === "string") {
|
|
2172
|
+
const match = resolveSettingsModel(models, settings.model, logger);
|
|
2173
|
+
if (match) {
|
|
2174
|
+
currentModel = match;
|
|
2175
|
+
resolvedFromInput = settings.model;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
// Skip the setModel round-trip when we can prove the SDK has already landed
|
|
2179
|
+
// on the same model. Two cases qualify:
|
|
2180
|
+
// (a) No override applied — currentModel stayed at models[0]; the SDK is on
|
|
2181
|
+
// its own default and we have nothing to sync.
|
|
2182
|
+
// (b) The resolver returned the user's input verbatim AND that value exists
|
|
2183
|
+
// in the SDK's original model list — meaning no fuzzy match or
|
|
2184
|
+
// allowlist rewrite was involved, and the SDK (which reads the same
|
|
2185
|
+
// ANTHROPIC_MODEL / settings.json) will have arrived at the same entry.
|
|
2186
|
+
// Anything else (fuzzy match, allowlist-synthesized value, alias) gets a
|
|
2187
|
+
// setModel call so we don't drift from the user's intended pin.
|
|
2188
|
+
const sdkSawSameValue = sdkModels.some((m) => m.value === currentModel.value);
|
|
2189
|
+
const skipSetModel = resolvedFromInput === undefined ||
|
|
2190
|
+
(currentModel.value === resolvedFromInput && sdkSawSameValue);
|
|
2191
|
+
if (!skipSetModel) {
|
|
2192
|
+
await query.setModel(currentModel.value);
|
|
2193
|
+
}
|
|
2194
|
+
return {
|
|
2195
|
+
availableModels: models.map((model) => ({
|
|
2196
|
+
modelId: model.value,
|
|
2197
|
+
name: model.displayName,
|
|
2198
|
+
description: model.description,
|
|
2199
|
+
})),
|
|
2200
|
+
currentModelId: currentModel.value,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
function getAvailableSlashCommands(commands) {
|
|
2204
|
+
const UNSUPPORTED_COMMANDS = [
|
|
2205
|
+
"clear",
|
|
2206
|
+
"cost",
|
|
2207
|
+
"keybindings-help",
|
|
2208
|
+
"login",
|
|
2209
|
+
"logout",
|
|
2210
|
+
"output-style:new",
|
|
2211
|
+
"release-notes",
|
|
2212
|
+
"todos",
|
|
2213
|
+
];
|
|
2214
|
+
return commands
|
|
2215
|
+
.map((command) => {
|
|
2216
|
+
const input = command.argumentHint
|
|
2217
|
+
? {
|
|
2218
|
+
hint: Array.isArray(command.argumentHint)
|
|
2219
|
+
? command.argumentHint.join(" ")
|
|
2220
|
+
: command.argumentHint,
|
|
2221
|
+
}
|
|
2222
|
+
: null;
|
|
2223
|
+
let name = command.name;
|
|
2224
|
+
if (command.name.endsWith(" (MCP)")) {
|
|
2225
|
+
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
2226
|
+
}
|
|
2227
|
+
return {
|
|
2228
|
+
name,
|
|
2229
|
+
description: command.description || "",
|
|
2230
|
+
input,
|
|
2231
|
+
};
|
|
2232
|
+
})
|
|
2233
|
+
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
2234
|
+
}
|
|
2235
|
+
function formatUriAsLink(uri) {
|
|
2236
|
+
try {
|
|
2237
|
+
if (uri.startsWith("file://")) {
|
|
2238
|
+
const path = uri.slice(7); // Remove "file://"
|
|
2239
|
+
const name = path.split("/").pop() || path;
|
|
2240
|
+
return `[@${name}](${uri})`;
|
|
2241
|
+
}
|
|
2242
|
+
else if (uri.startsWith("zed://")) {
|
|
2243
|
+
const parts = uri.split("/");
|
|
2244
|
+
const name = parts[parts.length - 1] || uri;
|
|
2245
|
+
return `[@${name}](${uri})`;
|
|
2246
|
+
}
|
|
2247
|
+
return uri;
|
|
2248
|
+
}
|
|
2249
|
+
catch {
|
|
2250
|
+
return uri;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
export function promptToClaude(prompt) {
|
|
2254
|
+
const content = [];
|
|
2255
|
+
const context = [];
|
|
2256
|
+
for (const chunk of prompt.prompt) {
|
|
2257
|
+
switch (chunk.type) {
|
|
2258
|
+
case "text": {
|
|
2259
|
+
let text = chunk.text;
|
|
2260
|
+
// change /mcp:server:command args -> /server:command (MCP) args
|
|
2261
|
+
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s(.*))?$/);
|
|
2262
|
+
if (mcpMatch) {
|
|
2263
|
+
const [, server, command, args] = mcpMatch;
|
|
2264
|
+
text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
|
|
2265
|
+
}
|
|
2266
|
+
content.push({ type: "text", text });
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
case "resource_link": {
|
|
2270
|
+
const formattedUri = formatUriAsLink(chunk.uri);
|
|
2271
|
+
content.push({
|
|
2272
|
+
type: "text",
|
|
2273
|
+
text: formattedUri,
|
|
2274
|
+
});
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
case "resource": {
|
|
2278
|
+
if ("text" in chunk.resource) {
|
|
2279
|
+
const formattedUri = formatUriAsLink(chunk.resource.uri);
|
|
2280
|
+
content.push({
|
|
2281
|
+
type: "text",
|
|
2282
|
+
text: formattedUri,
|
|
2283
|
+
});
|
|
2284
|
+
context.push({
|
|
2285
|
+
type: "text",
|
|
2286
|
+
text: `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`,
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
// Ignore blob resources (unsupported)
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
case "image":
|
|
2293
|
+
if (chunk.data) {
|
|
2294
|
+
content.push({
|
|
2295
|
+
type: "image",
|
|
2296
|
+
source: {
|
|
2297
|
+
type: "base64",
|
|
2298
|
+
data: chunk.data,
|
|
2299
|
+
media_type: chunk.mimeType,
|
|
2300
|
+
},
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
else if (chunk.uri && chunk.uri.startsWith("http")) {
|
|
2304
|
+
content.push({
|
|
2305
|
+
type: "image",
|
|
2306
|
+
source: {
|
|
2307
|
+
type: "url",
|
|
2308
|
+
url: chunk.uri,
|
|
2309
|
+
},
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
break;
|
|
2313
|
+
// Ignore audio and other unsupported types
|
|
2314
|
+
default:
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
content.push(...context);
|
|
2319
|
+
return {
|
|
2320
|
+
type: "user",
|
|
2321
|
+
message: {
|
|
2322
|
+
role: "user",
|
|
2323
|
+
content: content,
|
|
2324
|
+
},
|
|
2325
|
+
session_id: prompt.sessionId,
|
|
2326
|
+
parent_tool_use_id: null,
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
2331
|
+
* Only handles text, image, and thinking chunks for now.
|
|
2332
|
+
*/
|
|
2333
|
+
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
2334
|
+
const taskState = options?.taskState ?? new Map();
|
|
2335
|
+
const registerHooks = options?.registerHooks !== false;
|
|
2336
|
+
const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
2337
|
+
if (typeof content === "string") {
|
|
2338
|
+
const update = {
|
|
2339
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
2340
|
+
content: {
|
|
2341
|
+
type: "text",
|
|
2342
|
+
text: content,
|
|
2343
|
+
},
|
|
2344
|
+
};
|
|
2345
|
+
if (options?.parentToolUseId) {
|
|
2346
|
+
update._meta = {
|
|
2347
|
+
...update._meta,
|
|
2348
|
+
claudeCode: {
|
|
2349
|
+
...(update._meta?.claudeCode || {}),
|
|
2350
|
+
parentToolUseId: options.parentToolUseId,
|
|
2351
|
+
},
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
return [{ sessionId, update }];
|
|
2355
|
+
}
|
|
2356
|
+
const output = [];
|
|
2357
|
+
// Only handle the first chunk for streaming; extend as needed for batching
|
|
2358
|
+
for (const chunk of content) {
|
|
2359
|
+
let update = null;
|
|
2360
|
+
switch (chunk.type) {
|
|
2361
|
+
case "text":
|
|
2362
|
+
case "text_delta":
|
|
2363
|
+
update = {
|
|
2364
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
2365
|
+
content: {
|
|
2366
|
+
type: "text",
|
|
2367
|
+
text: chunk.text,
|
|
2368
|
+
},
|
|
2369
|
+
};
|
|
2370
|
+
break;
|
|
2371
|
+
case "image":
|
|
2372
|
+
update = {
|
|
2373
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
2374
|
+
content: {
|
|
2375
|
+
type: "image",
|
|
2376
|
+
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
|
2377
|
+
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
|
|
2378
|
+
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
|
2379
|
+
},
|
|
2380
|
+
};
|
|
2381
|
+
break;
|
|
2382
|
+
case "thinking":
|
|
2383
|
+
case "thinking_delta":
|
|
2384
|
+
update = {
|
|
2385
|
+
sessionUpdate: "agent_thought_chunk",
|
|
2386
|
+
content: {
|
|
2387
|
+
type: "text",
|
|
2388
|
+
text: chunk.thinking,
|
|
2389
|
+
},
|
|
2390
|
+
};
|
|
2391
|
+
break;
|
|
2392
|
+
case "tool_use":
|
|
2393
|
+
case "server_tool_use":
|
|
2394
|
+
case "mcp_tool_use": {
|
|
2395
|
+
const alreadyCached = chunk.id in toolUseCache;
|
|
2396
|
+
toolUseCache[chunk.id] = chunk;
|
|
2397
|
+
if (chunk.name === "TodoWrite") {
|
|
2398
|
+
// @ts-expect-error - sometimes input is empty object or undefined
|
|
2399
|
+
if (Array.isArray(chunk.input?.todos)) {
|
|
2400
|
+
update = {
|
|
2401
|
+
sessionUpdate: "plan",
|
|
2402
|
+
entries: planEntries(chunk.input),
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
else if (chunk.name === "TaskCreate" ||
|
|
2407
|
+
chunk.name === "TaskUpdate" ||
|
|
2408
|
+
chunk.name === "TaskList" ||
|
|
2409
|
+
chunk.name === "TaskGet") {
|
|
2410
|
+
// Task* tool_use is suppressed; the plan update is emitted at
|
|
2411
|
+
// tool_result time once we have the task ID (for TaskCreate) and
|
|
2412
|
+
// confirmation that the change took effect.
|
|
2413
|
+
}
|
|
2414
|
+
else {
|
|
2415
|
+
// Only register hooks on first encounter to avoid double-firing
|
|
2416
|
+
if (registerHooks && !alreadyCached) {
|
|
2417
|
+
registerHookCallback(chunk.id, {
|
|
2418
|
+
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
|
2419
|
+
const toolUse = toolUseCache[toolUseId];
|
|
2420
|
+
if (toolUse) {
|
|
2421
|
+
// Both `Edit` and `Write` produce a structuredPatch in their
|
|
2422
|
+
// PostToolUse tool_response. For Edit the diff replaces the
|
|
2423
|
+
// optimistic content built at tool_use time. For Write the
|
|
2424
|
+
// optimistic content (built from `input.content` alone with
|
|
2425
|
+
// `oldText: null`) shows "creation" semantics regardless of
|
|
2426
|
+
// whether the file existed; the structuredPatch from the
|
|
2427
|
+
// hook lets us emit the real diff for `type: "update"`. The
|
|
2428
|
+
// helper returns `{}` if the response shape isn't usable.
|
|
2429
|
+
const editDiff = toolUse.name === "Edit" || toolUse.name === "Write"
|
|
2430
|
+
? toolUpdateFromDiffToolResponse(toolResponse)
|
|
2431
|
+
: {};
|
|
2432
|
+
const update = {
|
|
2433
|
+
_meta: {
|
|
2434
|
+
claudeCode: {
|
|
2435
|
+
toolResponse,
|
|
2436
|
+
toolName: toolUse.name,
|
|
2437
|
+
},
|
|
2438
|
+
},
|
|
2439
|
+
toolCallId: toolUseId,
|
|
2440
|
+
sessionUpdate: "tool_call_update",
|
|
2441
|
+
...editDiff,
|
|
2442
|
+
};
|
|
2443
|
+
await client.sessionUpdate({
|
|
2444
|
+
sessionId,
|
|
2445
|
+
update,
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
logger.error(`[claude-agent-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
|
|
2450
|
+
}
|
|
2451
|
+
},
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
let rawInput;
|
|
2455
|
+
try {
|
|
2456
|
+
rawInput = JSON.parse(JSON.stringify(chunk.input));
|
|
2457
|
+
}
|
|
2458
|
+
catch {
|
|
2459
|
+
// ignore if we can't turn it to JSON
|
|
2460
|
+
}
|
|
2461
|
+
if (alreadyCached) {
|
|
2462
|
+
// Second encounter (full assistant message after streaming) —
|
|
2463
|
+
// send as tool_call_update to refine the existing tool_call
|
|
2464
|
+
// rather than emitting a duplicate tool_call.
|
|
2465
|
+
update = {
|
|
2466
|
+
_meta: {
|
|
2467
|
+
claudeCode: {
|
|
2468
|
+
toolName: chunk.name,
|
|
2469
|
+
},
|
|
2470
|
+
},
|
|
2471
|
+
toolCallId: chunk.id,
|
|
2472
|
+
sessionUpdate: "tool_call_update",
|
|
2473
|
+
rawInput,
|
|
2474
|
+
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
else {
|
|
2478
|
+
// First encounter (streaming content_block_start or replay) —
|
|
2479
|
+
// send as tool_call with terminal_info for Bash tools.
|
|
2480
|
+
update = {
|
|
2481
|
+
_meta: {
|
|
2482
|
+
claudeCode: {
|
|
2483
|
+
toolName: chunk.name,
|
|
2484
|
+
},
|
|
2485
|
+
...(chunk.name === "Bash" && supportsTerminalOutput
|
|
2486
|
+
? { terminal_info: { terminal_id: chunk.id } }
|
|
2487
|
+
: {}),
|
|
2488
|
+
},
|
|
2489
|
+
toolCallId: chunk.id,
|
|
2490
|
+
sessionUpdate: "tool_call",
|
|
2491
|
+
rawInput,
|
|
2492
|
+
status: "pending",
|
|
2493
|
+
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
case "tool_result":
|
|
2500
|
+
case "tool_search_tool_result":
|
|
2501
|
+
case "web_fetch_tool_result":
|
|
2502
|
+
case "web_search_tool_result":
|
|
2503
|
+
case "code_execution_tool_result":
|
|
2504
|
+
case "bash_code_execution_tool_result":
|
|
2505
|
+
case "text_editor_code_execution_tool_result":
|
|
2506
|
+
case "mcp_tool_result": {
|
|
2507
|
+
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
2508
|
+
if (!toolUse) {
|
|
2509
|
+
logger.error(`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
|
|
2510
|
+
break;
|
|
2511
|
+
}
|
|
2512
|
+
if (toolUse.name === "TaskCreate" ||
|
|
2513
|
+
toolUse.name === "TaskUpdate" ||
|
|
2514
|
+
toolUse.name === "TaskList" ||
|
|
2515
|
+
toolUse.name === "TaskGet") {
|
|
2516
|
+
// Headless/SDK sessions emit Task* tools instead of TodoWrite.
|
|
2517
|
+
// TaskCreate / TaskUpdate mutate the accumulated task list; TaskList
|
|
2518
|
+
// and TaskGet are read-only so we just suppress their tool_call /
|
|
2519
|
+
// tool_result events. The plan update is emitted as a snapshot of
|
|
2520
|
+
// the accumulated state, mirroring the legacy TodoWrite behavior.
|
|
2521
|
+
const isError = "is_error" in chunk && chunk.is_error;
|
|
2522
|
+
if (!isError) {
|
|
2523
|
+
if (toolUse.name === "TaskCreate") {
|
|
2524
|
+
applyTaskCreate(taskState, toolUse.input, parseTaskCreateOutput(chunk.content));
|
|
2525
|
+
}
|
|
2526
|
+
else if (toolUse.name === "TaskUpdate") {
|
|
2527
|
+
applyTaskUpdate(taskState, toolUse.input);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
if (!isError && (toolUse.name === "TaskCreate" || toolUse.name === "TaskUpdate")) {
|
|
2531
|
+
update = {
|
|
2532
|
+
sessionUpdate: "plan",
|
|
2533
|
+
entries: taskStateToPlanEntries(taskState),
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
else if (toolUse.name !== "TodoWrite") {
|
|
2538
|
+
const { _meta: toolMeta, ...toolUpdate } = toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id], supportsTerminalOutput);
|
|
2539
|
+
// When terminal output is supported, send terminal_output as a
|
|
2540
|
+
// separate notification to match codex-acp's streaming lifecycle:
|
|
2541
|
+
// 1. tool_call → _meta.terminal_info (already sent above)
|
|
2542
|
+
// 2. tool_call_update → _meta.terminal_output (sent here)
|
|
2543
|
+
// 3. tool_call_update → _meta.terminal_exit (sent below with status)
|
|
2544
|
+
if (toolMeta?.terminal_output) {
|
|
2545
|
+
output.push({
|
|
2546
|
+
sessionId,
|
|
2547
|
+
update: {
|
|
2548
|
+
_meta: {
|
|
2549
|
+
terminal_output: toolMeta.terminal_output,
|
|
2550
|
+
...(options?.parentToolUseId
|
|
2551
|
+
? { claudeCode: { parentToolUseId: options.parentToolUseId } }
|
|
2552
|
+
: {}),
|
|
2553
|
+
},
|
|
2554
|
+
toolCallId: chunk.tool_use_id,
|
|
2555
|
+
sessionUpdate: "tool_call_update",
|
|
2556
|
+
},
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
update = {
|
|
2560
|
+
_meta: {
|
|
2561
|
+
claudeCode: {
|
|
2562
|
+
toolName: toolUse.name,
|
|
2563
|
+
},
|
|
2564
|
+
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
|
2565
|
+
},
|
|
2566
|
+
toolCallId: chunk.tool_use_id,
|
|
2567
|
+
sessionUpdate: "tool_call_update",
|
|
2568
|
+
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
2569
|
+
rawOutput: chunk.content,
|
|
2570
|
+
...toolUpdate,
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
break;
|
|
2574
|
+
}
|
|
2575
|
+
case "document":
|
|
2576
|
+
case "search_result":
|
|
2577
|
+
case "redacted_thinking":
|
|
2578
|
+
case "input_json_delta":
|
|
2579
|
+
case "citations_delta":
|
|
2580
|
+
case "signature_delta":
|
|
2581
|
+
case "container_upload":
|
|
2582
|
+
case "compaction":
|
|
2583
|
+
case "compaction_delta":
|
|
2584
|
+
case "advisor_tool_result":
|
|
2585
|
+
case "mid_conv_system":
|
|
2586
|
+
break;
|
|
2587
|
+
default:
|
|
2588
|
+
unreachable(chunk, logger);
|
|
2589
|
+
break;
|
|
2590
|
+
}
|
|
2591
|
+
if (update) {
|
|
2592
|
+
if (options?.parentToolUseId) {
|
|
2593
|
+
update._meta = {
|
|
2594
|
+
...update._meta,
|
|
2595
|
+
claudeCode: {
|
|
2596
|
+
...(update._meta?.claudeCode || {}),
|
|
2597
|
+
parentToolUseId: options.parentToolUseId,
|
|
2598
|
+
},
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
output.push({ sessionId, update });
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return output;
|
|
2605
|
+
}
|
|
2606
|
+
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger, options) {
|
|
2607
|
+
const event = message.event;
|
|
2608
|
+
switch (event.type) {
|
|
2609
|
+
case "content_block_start":
|
|
2610
|
+
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger, {
|
|
2611
|
+
clientCapabilities: options?.clientCapabilities,
|
|
2612
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
2613
|
+
cwd: options?.cwd,
|
|
2614
|
+
taskState: options?.taskState,
|
|
2615
|
+
});
|
|
2616
|
+
case "content_block_delta":
|
|
2617
|
+
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, {
|
|
2618
|
+
clientCapabilities: options?.clientCapabilities,
|
|
2619
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
2620
|
+
cwd: options?.cwd,
|
|
2621
|
+
taskState: options?.taskState,
|
|
2622
|
+
});
|
|
2623
|
+
// No content. `ping` is a Messages-API keep-alive event that the SDK's
|
|
2624
|
+
// `BetaRawMessageStreamEvent` union doesn't include even though the
|
|
2625
|
+
// wire format emits it; the `as never` cast lets us no-op it here
|
|
2626
|
+
// instead of letting it fall through to `unreachable`.
|
|
2627
|
+
case "ping":
|
|
2628
|
+
case "message_start":
|
|
2629
|
+
case "message_delta":
|
|
2630
|
+
case "message_stop":
|
|
2631
|
+
case "content_block_stop":
|
|
2632
|
+
return [];
|
|
2633
|
+
default:
|
|
2634
|
+
unreachable(event, logger);
|
|
2635
|
+
return [];
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
export function runAcp() {
|
|
2639
|
+
const input = nodeToWebWritable(process.stdout);
|
|
2640
|
+
const output = nodeToWebReadable(process.stdin);
|
|
2641
|
+
const stream = ndJsonStream(input, output);
|
|
2642
|
+
let agent;
|
|
2643
|
+
const connection = new AgentSideConnection((client) => {
|
|
2644
|
+
agent = new ClaudeAcpAgent(client);
|
|
2645
|
+
return agent;
|
|
2646
|
+
}, stream);
|
|
2647
|
+
return { connection, agent };
|
|
2648
|
+
}
|
|
2649
|
+
function commonPrefixLength(a, b) {
|
|
2650
|
+
let i = 0;
|
|
2651
|
+
while (i < a.length && i < b.length && a[i] === b[i]) {
|
|
2652
|
+
i++;
|
|
2653
|
+
}
|
|
2654
|
+
return i;
|
|
2655
|
+
}
|
|
2656
|
+
/** Best-effort first guess of a model's context window from its ID, used only
|
|
2657
|
+
* until a `result` message arrives with the authoritative `modelUsage` value.
|
|
2658
|
+
* Anthropic 1M-context variants encode "1m" as a distinct token in the SDK
|
|
2659
|
+
* model ID (e.g., "claude-opus-4-6-1m"), which `\b1m\b` catches without also
|
|
2660
|
+
* matching things like "10m" or embedded substrings. */
|
|
2661
|
+
function inferContextWindowFromModel(model) {
|
|
2662
|
+
if (/\b1m\b/i.test(model))
|
|
2663
|
+
return 1_000_000;
|
|
2664
|
+
return null;
|
|
2665
|
+
}
|
|
2666
|
+
function parseModelConfig(raw) {
|
|
2667
|
+
if (!raw)
|
|
2668
|
+
return undefined;
|
|
2669
|
+
const parsed = JSON.parse(raw);
|
|
2670
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2671
|
+
throw new Error("CLAUDE_MODEL_CONFIG must be a JSON object");
|
|
2672
|
+
}
|
|
2673
|
+
const result = {};
|
|
2674
|
+
if (parsed.modelOverrides !== undefined)
|
|
2675
|
+
result.modelOverrides = parsed.modelOverrides;
|
|
2676
|
+
if (parsed.availableModels !== undefined)
|
|
2677
|
+
result.availableModels = parsed.availableModels;
|
|
2678
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
2679
|
+
}
|
|
2680
|
+
function getMatchingModelUsage(modelUsage, currentModel) {
|
|
2681
|
+
let bestKey = null;
|
|
2682
|
+
let bestLen = 0;
|
|
2683
|
+
for (const key of Object.keys(modelUsage)) {
|
|
2684
|
+
const len = commonPrefixLength(key, currentModel);
|
|
2685
|
+
if (len > bestLen) {
|
|
2686
|
+
bestLen = len;
|
|
2687
|
+
bestKey = key;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
if (bestKey) {
|
|
2691
|
+
return modelUsage[bestKey];
|
|
2692
|
+
}
|
|
2693
|
+
}
|