bonecode 1.0.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/ARCHITECTURE.md +183 -0
- package/README.md +71 -0
- package/bin/bonecode +62 -0
- package/bone/migrations/rag_vectors.sql +258 -0
- package/bone/output/agent/.dockerignore +7 -0
- package/bone/output/agent/.env.example +36 -0
- package/bone/output/agent/.github/workflows/ci.yaml +58 -0
- package/bone/output/agent/AgentDomain.bone.map +350 -0
- package/bone/output/agent/AgentDomain.postman_collection.json +958 -0
- package/bone/output/agent/Dockerfile +22 -0
- package/bone/output/agent/README.md +47 -0
- package/bone/output/agent/admin/index.html +740 -0
- package/bone/output/agent/docker-compose.yaml +22 -0
- package/bone/output/agent/k8s/deployment.yaml +75 -0
- package/bone/output/agent/migrations/agent.sql +36 -0
- package/bone/output/agent/migrations/agent_instance.sql +36 -0
- package/bone/output/agent/migrations/audit_log.sql +18 -0
- package/bone/output/agent/migrations/build_step.sql +34 -0
- package/bone/output/agent/migrations/event_outbox.sql +31 -0
- package/bone/output/agent/migrations/plan.sql +30 -0
- package/bone/output/agent/migrations/task.sql +30 -0
- package/bone/output/agent/migrations/tool_call.sql +33 -0
- package/bone/output/agent/openapi.yaml +1116 -0
- package/bone/output/agent/package.json +36 -0
- package/bone/output/agent/schema.graphql +233 -0
- package/bone/output/agent/sdk/client.ts +231 -0
- package/bone/output/agent/src/algorithms.ts +2 -0
- package/bone/output/agent/src/audit.ts +44 -0
- package/bone/output/agent/src/auth.ts +57 -0
- package/bone/output/agent/src/cron.ts +12 -0
- package/bone/output/agent/src/db.ts +32 -0
- package/bone/output/agent/src/debug.ts +66 -0
- package/bone/output/agent/src/events.ts +243 -0
- package/bone/output/agent/src/extensions.ts +54 -0
- package/bone/output/agent/src/failure_rules.ts +323 -0
- package/bone/output/agent/src/flows.ts +168 -0
- package/bone/output/agent/src/health.ts +43 -0
- package/bone/output/agent/src/index.ts +100 -0
- package/bone/output/agent/src/logger.ts +66 -0
- package/bone/output/agent/src/metrics.ts +75 -0
- package/bone/output/agent/src/migrate.ts +352 -0
- package/bone/output/agent/src/migration_diff.ts +108 -0
- package/bone/output/agent/src/notify.ts +125 -0
- package/bone/output/agent/src/routes/agent_instance.ts +234 -0
- package/bone/output/agent/src/routes/build_step.ts +105 -0
- package/bone/output/agent/src/routes/plan.ts +91 -0
- package/bone/output/agent/src/routes/task.ts +105 -0
- package/bone/output/agent/src/routes/tool_call.ts +166 -0
- package/bone/output/agent/src/schemas.ts +384 -0
- package/bone/output/agent/src/state_machines/agent_instance.ts +24 -0
- package/bone/output/agent/src/state_machines/build_step.ts +22 -0
- package/bone/output/agent/src/state_machines/plan.ts +22 -0
- package/bone/output/agent/src/state_machines/task.ts +22 -0
- package/bone/output/agent/src/state_machines/tool_call.ts +22 -0
- package/bone/output/agent/src/tests.ts +362 -0
- package/bone/output/agent/src/websocket.ts +201 -0
- package/bone/output/agent/tsconfig.json +25 -0
- package/bone/output/rag/.dockerignore +7 -0
- package/bone/output/rag/.env.example +36 -0
- package/bone/output/rag/.github/workflows/ci.yaml +58 -0
- package/bone/output/rag/Dockerfile +22 -0
- package/bone/output/rag/RAGDomain.bone.map +287 -0
- package/bone/output/rag/RAGDomain.postman_collection.json +923 -0
- package/bone/output/rag/README.md +47 -0
- package/bone/output/rag/admin/index.html +818 -0
- package/bone/output/rag/docker-compose.yaml +22 -0
- package/bone/output/rag/k8s/deployment.yaml +75 -0
- package/bone/output/rag/migrations/audit_log.sql +18 -0
- package/bone/output/rag/migrations/code_chunk.sql +34 -0
- package/bone/output/rag/migrations/code_file.sql +33 -0
- package/bone/output/rag/migrations/event_outbox.sql +31 -0
- package/bone/output/rag/migrations/indexing_job.sql +33 -0
- package/bone/output/rag/migrations/knowledge_base.sql +35 -0
- package/bone/output/rag/migrations/memory_entry.sql +34 -0
- package/bone/output/rag/openapi.yaml +1097 -0
- package/bone/output/rag/package.json +36 -0
- package/bone/output/rag/schema.graphql +245 -0
- package/bone/output/rag/sdk/client.ts +234 -0
- package/bone/output/rag/src/algorithms.ts +2 -0
- package/bone/output/rag/src/audit.ts +37 -0
- package/bone/output/rag/src/auth.ts +57 -0
- package/bone/output/rag/src/cron.ts +12 -0
- package/bone/output/rag/src/db.ts +32 -0
- package/bone/output/rag/src/debug.ts +66 -0
- package/bone/output/rag/src/events.ts +243 -0
- package/bone/output/rag/src/extensions.ts +350 -0
- package/bone/output/rag/src/failure_rules.ts +315 -0
- package/bone/output/rag/src/flows.ts +239 -0
- package/bone/output/rag/src/health.ts +43 -0
- package/bone/output/rag/src/index.ts +95 -0
- package/bone/output/rag/src/logger.ts +66 -0
- package/bone/output/rag/src/metrics.ts +75 -0
- package/bone/output/rag/src/migrate.ts +364 -0
- package/bone/output/rag/src/migration_diff.ts +108 -0
- package/bone/output/rag/src/notify.ts +99 -0
- package/bone/output/rag/src/routes/code_chunk.ts +75 -0
- package/bone/output/rag/src/routes/code_file.ts +101 -0
- package/bone/output/rag/src/routes/indexing_job.ts +87 -0
- package/bone/output/rag/src/routes/knowledge_base.ts +230 -0
- package/bone/output/rag/src/routes/memory_entry.ts +87 -0
- package/bone/output/rag/src/schemas.ts +394 -0
- package/bone/output/rag/src/state_machines/code_file.ts +23 -0
- package/bone/output/rag/src/state_machines/indexing_job.ts +22 -0
- package/bone/output/rag/src/state_machines/knowledge_base.ts +23 -0
- package/bone/output/rag/src/state_machines/memory_entry.ts +20 -0
- package/bone/output/rag/src/tests.ts +340 -0
- package/bone/output/rag/tsconfig.json +25 -0
- package/bone/output/session/.dockerignore +7 -0
- package/bone/output/session/.env.example +36 -0
- package/bone/output/session/.github/workflows/ci.yaml +58 -0
- package/bone/output/session/Dockerfile +22 -0
- package/bone/output/session/README.md +47 -0
- package/bone/output/session/SessionDomain.bone.map +350 -0
- package/bone/output/session/SessionDomain.postman_collection.json +958 -0
- package/bone/output/session/admin/index.html +667 -0
- package/bone/output/session/docker-compose.yaml +22 -0
- package/bone/output/session/k8s/deployment.yaml +75 -0
- package/bone/output/session/migrations/audit_log.sql +18 -0
- package/bone/output/session/migrations/event_outbox.sql +31 -0
- package/bone/output/session/migrations/message.sql +31 -0
- package/bone/output/session/migrations/part.sql +28 -0
- package/bone/output/session/migrations/permission.sql +28 -0
- package/bone/output/session/migrations/project.sql +28 -0
- package/bone/output/session/migrations/session.sql +38 -0
- package/bone/output/session/openapi.yaml +1101 -0
- package/bone/output/session/package.json +36 -0
- package/bone/output/session/schema.graphql +222 -0
- package/bone/output/session/sdk/client.ts +225 -0
- package/bone/output/session/src/algorithms.ts +2 -0
- package/bone/output/session/src/audit.ts +44 -0
- package/bone/output/session/src/auth.ts +57 -0
- package/bone/output/session/src/cron.ts +12 -0
- package/bone/output/session/src/db.ts +32 -0
- package/bone/output/session/src/debug.ts +66 -0
- package/bone/output/session/src/events.ts +270 -0
- package/bone/output/session/src/extensions.ts +215 -0
- package/bone/output/session/src/failure_rules.ts +284 -0
- package/bone/output/session/src/flows.ts +168 -0
- package/bone/output/session/src/health.ts +43 -0
- package/bone/output/session/src/index.ts +100 -0
- package/bone/output/session/src/logger.ts +66 -0
- package/bone/output/session/src/metrics.ts +75 -0
- package/bone/output/session/src/migrate.ts +332 -0
- package/bone/output/session/src/migration_diff.ts +108 -0
- package/bone/output/session/src/notify.ts +112 -0
- package/bone/output/session/src/routes/message.ts +93 -0
- package/bone/output/session/src/routes/part.ts +79 -0
- package/bone/output/session/src/routes/permission.ts +79 -0
- package/bone/output/session/src/routes/project.ts +79 -0
- package/bone/output/session/src/routes/session.ts +294 -0
- package/bone/output/session/src/schemas.ts +357 -0
- package/bone/output/session/src/state_machines/session.ts +23 -0
- package/bone/output/session/src/tests.ts +326 -0
- package/bone/output/session/src/websocket.ts +201 -0
- package/bone/output/session/tsconfig.json +25 -0
- package/bone/output/workspace/.dockerignore +7 -0
- package/bone/output/workspace/.env.example +36 -0
- package/bone/output/workspace/.github/workflows/ci.yaml +58 -0
- package/bone/output/workspace/Dockerfile +22 -0
- package/bone/output/workspace/README.md +45 -0
- package/bone/output/workspace/WorkspaceDomain.bone.map +189 -0
- package/bone/output/workspace/WorkspaceDomain.postman_collection.json +621 -0
- package/bone/output/workspace/admin/index.html +485 -0
- package/bone/output/workspace/docker-compose.yaml +22 -0
- package/bone/output/workspace/k8s/deployment.yaml +75 -0
- package/bone/output/workspace/migrations/audit_log.sql +18 -0
- package/bone/output/workspace/migrations/codebase.sql +34 -0
- package/bone/output/workspace/migrations/event_outbox.sql +31 -0
- package/bone/output/workspace/migrations/snapshot.sql +32 -0
- package/bone/output/workspace/migrations/workspace.sql +33 -0
- package/bone/output/workspace/openapi.yaml +721 -0
- package/bone/output/workspace/package.json +36 -0
- package/bone/output/workspace/schema.graphql +153 -0
- package/bone/output/workspace/sdk/client.ts +155 -0
- package/bone/output/workspace/src/algorithms.ts +2 -0
- package/bone/output/workspace/src/audit.ts +37 -0
- package/bone/output/workspace/src/auth.ts +57 -0
- package/bone/output/workspace/src/cron.ts +12 -0
- package/bone/output/workspace/src/db.ts +32 -0
- package/bone/output/workspace/src/debug.ts +66 -0
- package/bone/output/workspace/src/events.ts +243 -0
- package/bone/output/workspace/src/extensions.ts +44 -0
- package/bone/output/workspace/src/failure_rules.ts +153 -0
- package/bone/output/workspace/src/health.ts +43 -0
- package/bone/output/workspace/src/index.ts +89 -0
- package/bone/output/workspace/src/logger.ts +66 -0
- package/bone/output/workspace/src/metrics.ts +75 -0
- package/bone/output/workspace/src/migrate.ts +220 -0
- package/bone/output/workspace/src/migration_diff.ts +108 -0
- package/bone/output/workspace/src/notify.ts +73 -0
- package/bone/output/workspace/src/routes/codebase.ts +87 -0
- package/bone/output/workspace/src/routes/snapshot.ts +127 -0
- package/bone/output/workspace/src/routes/workspace.ts +190 -0
- package/bone/output/workspace/src/schemas.ts +231 -0
- package/bone/output/workspace/src/state_machines/codebase.ts +21 -0
- package/bone/output/workspace/src/state_machines/snapshot.ts +20 -0
- package/bone/output/workspace/src/state_machines/workspace.ts +21 -0
- package/bone/output/workspace/src/tests.ts +249 -0
- package/bone/output/workspace/tsconfig.json +25 -0
- package/compat/opencode_adapter.ts +410 -0
- package/package.json +69 -0
- package/scripts/check_benchmark_session.js +34 -0
- package/scripts/check_finish_event.js +24 -0
- package/scripts/check_parts.js +15 -0
- package/scripts/compile.js +79 -0
- package/scripts/copy_opencode.ps1 +53 -0
- package/scripts/create_functions.sql +129 -0
- package/scripts/migrate.js +85 -0
- package/scripts/migrate_from_opencode.ts +218 -0
- package/scripts/test_agent_loop.js +101 -0
- package/scripts/test_api.ps1 +116 -0
- package/scripts/test_context_builder.js +136 -0
- package/scripts/test_context_builder.ts +97 -0
- package/scripts/test_rag.js +189 -0
- package/scripts/test_stream_events.js +36 -0
- package/scripts/test_websocket_and_saga.js +216 -0
- package/src/cli.ts +475 -0
- package/src/config.ts +162 -0
- package/src/context_builder.ts +598 -0
- package/src/engine/account/account.sql.ts +39 -0
- package/src/engine/account/account.ts +456 -0
- package/src/engine/account/repo.ts +166 -0
- package/src/engine/account/schema.ts +99 -0
- package/src/engine/account/url.ts +8 -0
- package/src/engine/acp/README.md +174 -0
- package/src/engine/acp/agent.ts +1968 -0
- package/src/engine/acp/runtime.ts +22 -0
- package/src/engine/acp/session.ts +122 -0
- package/src/engine/acp/types.ts +24 -0
- package/src/engine/agent/agent.ts +463 -0
- package/src/engine/agent/generate.txt +75 -0
- package/src/engine/agent/prompt/compaction.txt +9 -0
- package/src/engine/agent/prompt/explore.txt +18 -0
- package/src/engine/agent/prompt/scout.txt +36 -0
- package/src/engine/agent/prompt/summary.txt +11 -0
- package/src/engine/agent/prompt/title.txt +44 -0
- package/src/engine/agent/subagent-permissions.ts +34 -0
- package/src/engine/auth/index.ts +96 -0
- package/src/engine/background/background/job.ts +200 -0
- package/src/engine/background/job.ts +200 -0
- package/src/engine/bus/bus-event.ts +45 -0
- package/src/engine/bus/global.ts +22 -0
- package/src/engine/bus/index.ts +203 -0
- package/src/engine/command/command/index.ts +181 -0
- package/src/engine/command/command/template/initialize.txt +66 -0
- package/src/engine/command/command/template/review.txt +101 -0
- package/src/engine/command/index.ts +181 -0
- package/src/engine/command/template/initialize.txt +66 -0
- package/src/engine/command/template/review.txt +101 -0
- package/src/engine/config/agent.ts +172 -0
- package/src/engine/config/attachment.ts +25 -0
- package/src/engine/config/command.ts +62 -0
- package/src/engine/config/config.ts +833 -0
- package/src/engine/config/console-state.ts +14 -0
- package/src/engine/config/entry-name.ts +16 -0
- package/src/engine/config/error.ts +23 -0
- package/src/engine/config/formatter.ts +13 -0
- package/src/engine/config/layout.ts +6 -0
- package/src/engine/config/lsp.ts +43 -0
- package/src/engine/config/managed.ts +71 -0
- package/src/engine/config/markdown.ts +96 -0
- package/src/engine/config/mcp.ts +56 -0
- package/src/engine/config/model-id.ts +5 -0
- package/src/engine/config/parse.ts +79 -0
- package/src/engine/config/paths.ts +45 -0
- package/src/engine/config/permission.ts +58 -0
- package/src/engine/config/plugin.ts +84 -0
- package/src/engine/config/provider.ts +111 -0
- package/src/engine/config/reference.ts +23 -0
- package/src/engine/config/server.ts +19 -0
- package/src/engine/config/skills.ts +14 -0
- package/src/engine/config/variable.ts +90 -0
- package/src/engine/control-plane/adapters/index.ts +41 -0
- package/src/engine/control-plane/adapters/worktree.ts +96 -0
- package/src/engine/control-plane/dev/README.md +19 -0
- package/src/engine/control-plane/dev/debug-workspace-plugin.ts +73 -0
- package/src/engine/control-plane/schema.ts +14 -0
- package/src/engine/control-plane/types.ts +59 -0
- package/src/engine/control-plane/util.ts +39 -0
- package/src/engine/control-plane/workspace-adapter-runtime.ts +51 -0
- package/src/engine/control-plane/workspace-context.ts +26 -0
- package/src/engine/control-plane/workspace.sql.ts +20 -0
- package/src/engine/control-plane/workspace.ts +1072 -0
- package/src/engine/data-migration.ts +161 -0
- package/src/engine/effect/app-runtime.ts +143 -0
- package/src/engine/effect/bootstrap-runtime.ts +29 -0
- package/src/engine/effect/bridge.ts +84 -0
- package/src/engine/effect/config-service.ts +67 -0
- package/src/engine/effect/instance-ref.ts +11 -0
- package/src/engine/effect/instance-registry.ts +12 -0
- package/src/engine/effect/instance-state.ts +72 -0
- package/src/engine/effect/promise.ts +17 -0
- package/src/engine/effect/run-service.ts +47 -0
- package/src/engine/effect/runner.ts +217 -0
- package/src/engine/effect/runtime-flags.ts +74 -0
- package/src/engine/effect/service-use.ts +38 -0
- package/src/engine/env/index.ts +37 -0
- package/src/engine/event-v2-bridge.ts +89 -0
- package/src/engine/file/file/ignore.ts +81 -0
- package/src/engine/file/file/index.ts +651 -0
- package/src/engine/file/file/protected.ts +59 -0
- package/src/engine/file/file/ripgrep.ts +481 -0
- package/src/engine/file/file/watcher.ts +167 -0
- package/src/engine/file/ignore.ts +81 -0
- package/src/engine/file/index.ts +651 -0
- package/src/engine/file/protected.ts +59 -0
- package/src/engine/file/ripgrep.ts +481 -0
- package/src/engine/file/watcher.ts +167 -0
- package/src/engine/format/format/formatter.ts +404 -0
- package/src/engine/format/format/index.ts +209 -0
- package/src/engine/format/formatter.ts +404 -0
- package/src/engine/format/index.ts +209 -0
- package/src/engine/git/git/index.ts +347 -0
- package/src/engine/git/index.ts +347 -0
- package/src/engine/id/id.ts +80 -0
- package/src/engine/ide/index.ts +70 -0
- package/src/engine/image/image/image.ts +176 -0
- package/src/engine/image/image.ts +176 -0
- package/src/engine/index.ts +251 -0
- package/src/engine/installation/index.ts +327 -0
- package/src/engine/lsp/client.ts +707 -0
- package/src/engine/lsp/diagnostic.ts +29 -0
- package/src/engine/lsp/language.ts +121 -0
- package/src/engine/lsp/launch.ts +21 -0
- package/src/engine/lsp/lsp/client.ts +707 -0
- package/src/engine/lsp/lsp/diagnostic.ts +29 -0
- package/src/engine/lsp/lsp/language.ts +121 -0
- package/src/engine/lsp/lsp/launch.ts +21 -0
- package/src/engine/lsp/lsp/lsp.ts +507 -0
- package/src/engine/lsp/lsp/server.ts +2064 -0
- package/src/engine/lsp/lsp.ts +507 -0
- package/src/engine/lsp/server.ts +2064 -0
- package/src/engine/mcp/auth.ts +146 -0
- package/src/engine/mcp/index.ts +958 -0
- package/src/engine/mcp/mcp/auth.ts +146 -0
- package/src/engine/mcp/mcp/index.ts +958 -0
- package/src/engine/mcp/mcp/oauth-callback.ts +232 -0
- package/src/engine/mcp/mcp/oauth-provider.ts +214 -0
- package/src/engine/mcp/oauth-callback.ts +232 -0
- package/src/engine/mcp/oauth-provider.ts +214 -0
- package/src/engine/node.ts +6 -0
- package/src/engine/patch/index.ts +689 -0
- package/src/engine/patch/patch/index.ts +689 -0
- package/src/engine/permission/arity.ts +163 -0
- package/src/engine/permission/evaluate.ts +15 -0
- package/src/engine/permission/index.ts +306 -0
- package/src/engine/permission/permission/arity.ts +163 -0
- package/src/engine/permission/permission/evaluate.ts +15 -0
- package/src/engine/permission/permission/index.ts +306 -0
- package/src/engine/permission/permission/schema.ts +13 -0
- package/src/engine/permission/schema.ts +13 -0
- package/src/engine/plugin/azure.ts +26 -0
- package/src/engine/plugin/cloudflare.ts +76 -0
- package/src/engine/plugin/codex.ts +622 -0
- package/src/engine/plugin/digitalocean.ts +411 -0
- package/src/engine/plugin/github-copilot/copilot.ts +394 -0
- package/src/engine/plugin/github-copilot/models.ts +196 -0
- package/src/engine/plugin/index.ts +295 -0
- package/src/engine/plugin/install.ts +439 -0
- package/src/engine/plugin/loader.ts +216 -0
- package/src/engine/plugin/meta.ts +188 -0
- package/src/engine/plugin/shared.ts +323 -0
- package/src/engine/project/bootstrap-service.ts +9 -0
- package/src/engine/project/bootstrap.ts +75 -0
- package/src/engine/project/instance-context.ts +24 -0
- package/src/engine/project/instance-layer.ts +11 -0
- package/src/engine/project/instance-runtime.ts +16 -0
- package/src/engine/project/instance-store.ts +193 -0
- package/src/engine/project/project.sql.ts +17 -0
- package/src/engine/project/project.ts +537 -0
- package/src/engine/project/schema.ts +13 -0
- package/src/engine/project/vcs.ts +405 -0
- package/src/engine/provider/auth.ts +225 -0
- package/src/engine/provider/error.ts +204 -0
- package/src/engine/provider/model-status.ts +8 -0
- package/src/engine/provider/provider.ts +1843 -0
- package/src/engine/provider/schema.ts +30 -0
- package/src/engine/provider/sdk/copilot/AGENTS.md +1 -0
- package/src/engine/provider/transform.ts +1376 -0
- package/src/engine/pty/index.ts +365 -0
- package/src/engine/pty/input.ts +24 -0
- package/src/engine/pty/pty/index.ts +365 -0
- package/src/engine/pty/pty/input.ts +24 -0
- package/src/engine/pty/pty/pty.bun.ts +26 -0
- package/src/engine/pty/pty/pty.node.ts +27 -0
- package/src/engine/pty/pty/pty.ts +25 -0
- package/src/engine/pty/pty/schema.ts +14 -0
- package/src/engine/pty/pty/ticket.ts +68 -0
- package/src/engine/pty/pty.bun.ts +26 -0
- package/src/engine/pty/pty.node.ts +27 -0
- package/src/engine/pty/pty.ts +25 -0
- package/src/engine/pty/schema.ts +14 -0
- package/src/engine/pty/ticket.ts +68 -0
- package/src/engine/question/index.ts +213 -0
- package/src/engine/question/question/index.ts +213 -0
- package/src/engine/question/question/schema.ts +10 -0
- package/src/engine/question/schema.ts +10 -0
- package/src/engine/reference/reference/reference.ts +241 -0
- package/src/engine/reference/reference/repository-cache.ts +147 -0
- package/src/engine/reference/reference.ts +241 -0
- package/src/engine/reference/repository-cache.ts +147 -0
- package/src/engine/session/compaction.ts +651 -0
- package/src/engine/session/compaction_logic.ts +120 -0
- package/src/engine/session/instruction.ts +238 -0
- package/src/engine/session/instruction_loader.ts +54 -0
- package/src/engine/session/llm.ts +459 -0
- package/src/engine/session/message-error.ts +14 -0
- package/src/engine/session/message-v2.ts +1202 -0
- package/src/engine/session/message.ts +146 -0
- package/src/engine/session/overflow.ts +32 -0
- package/src/engine/session/overflow_check.ts +46 -0
- package/src/engine/session/processor.ts +823 -0
- package/src/engine/session/prompt/anthropic.txt +105 -0
- package/src/engine/session/prompt/beast.txt +147 -0
- package/src/engine/session/prompt/build-switch.txt +5 -0
- package/src/engine/session/prompt/codex.txt +79 -0
- package/src/engine/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/engine/session/prompt/default.txt +105 -0
- package/src/engine/session/prompt/gemini.txt +155 -0
- package/src/engine/session/prompt/gpt.txt +107 -0
- package/src/engine/session/prompt/kimi.txt +95 -0
- package/src/engine/session/prompt/max-steps.txt +16 -0
- package/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/engine/session/prompt/plan.txt +26 -0
- package/src/engine/session/prompt/trinity.txt +97 -0
- package/src/engine/session/prompt.ts +671 -0
- package/src/engine/session/provider_transform.ts +187 -0
- package/src/engine/session/retry.ts +200 -0
- package/src/engine/session/retry_logic.ts +65 -0
- package/src/engine/session/revert.ts +162 -0
- package/src/engine/session/run-state.ts +153 -0
- package/src/engine/session/schema.ts +26 -0
- package/src/engine/session/session.sql.ts +137 -0
- package/src/engine/session/session.ts +1011 -0
- package/src/engine/session/status.ts +94 -0
- package/src/engine/session/summary.ts +164 -0
- package/src/engine/session/system.ts +84 -0
- package/src/engine/session/system_prompt.ts +65 -0
- package/src/engine/session/todo.ts +81 -0
- package/src/engine/session/tool_registry.ts +162 -0
- package/src/engine/share/session.ts +61 -0
- package/src/engine/share/share-next.ts +376 -0
- package/src/engine/share/share.sql.ts +13 -0
- package/src/engine/shell/shell/shell.ts +215 -0
- package/src/engine/shell/shell.ts +215 -0
- package/src/engine/skill/discovery.ts +116 -0
- package/src/engine/skill/index.ts +336 -0
- package/src/engine/skill/prompt/customize-opencode.md +377 -0
- package/src/engine/skill/skill/discovery.ts +116 -0
- package/src/engine/skill/skill/index.ts +336 -0
- package/src/engine/skill/skill/prompt/customize-opencode.md +377 -0
- package/src/engine/snapshot/index.ts +762 -0
- package/src/engine/snapshot/snapshot/index.ts +762 -0
- package/src/engine/sync/README.md +179 -0
- package/src/engine/sync/event.sql.ts +17 -0
- package/src/engine/sync/index.ts +410 -0
- package/src/engine/sync/schema.ts +11 -0
- package/src/engine/temporary.ts +33 -0
- package/src/engine/tool/apply_patch.ts +313 -0
- package/src/engine/tool/apply_patch.txt +33 -0
- package/src/engine/tool/edit.ts +711 -0
- package/src/engine/tool/edit.txt +10 -0
- package/src/engine/tool/external-directory.ts +49 -0
- package/src/engine/tool/glob.ts +103 -0
- package/src/engine/tool/glob.txt +6 -0
- package/src/engine/tool/grep.ts +156 -0
- package/src/engine/tool/grep.txt +8 -0
- package/src/engine/tool/invalid.ts +21 -0
- package/src/engine/tool/json-schema.ts +164 -0
- package/src/engine/tool/lsp.ts +113 -0
- package/src/engine/tool/lsp.txt +24 -0
- package/src/engine/tool/mcp-websearch.ts +96 -0
- package/src/engine/tool/plan-enter.txt +14 -0
- package/src/engine/tool/plan-exit.txt +13 -0
- package/src/engine/tool/plan.ts +78 -0
- package/src/engine/tool/question.ts +44 -0
- package/src/engine/tool/question.txt +10 -0
- package/src/engine/tool/read.ts +337 -0
- package/src/engine/tool/read.txt +14 -0
- package/src/engine/tool/registry.ts +472 -0
- package/src/engine/tool/repo_clone.ts +80 -0
- package/src/engine/tool/repo_clone.txt +5 -0
- package/src/engine/tool/repo_overview.ts +279 -0
- package/src/engine/tool/repo_overview.txt +4 -0
- package/src/engine/tool/schema.ts +14 -0
- package/src/engine/tool/shell/id.ts +19 -0
- package/src/engine/tool/shell/prompt.ts +295 -0
- package/src/engine/tool/shell/shell.txt +77 -0
- package/src/engine/tool/shell.ts +647 -0
- package/src/engine/tool/skill.ts +75 -0
- package/src/engine/tool/skill.txt +5 -0
- package/src/engine/tool/task.ts +337 -0
- package/src/engine/tool/task.txt +58 -0
- package/src/engine/tool/task_status.ts +179 -0
- package/src/engine/tool/task_status.txt +13 -0
- package/src/engine/tool/todo.ts +57 -0
- package/src/engine/tool/todowrite.txt +167 -0
- package/src/engine/tool/tool/apply_patch.ts +313 -0
- package/src/engine/tool/tool/apply_patch.txt +33 -0
- package/src/engine/tool/tool/edit.ts +711 -0
- package/src/engine/tool/tool/edit.txt +10 -0
- package/src/engine/tool/tool/external-directory.ts +49 -0
- package/src/engine/tool/tool/glob.ts +103 -0
- package/src/engine/tool/tool/glob.txt +6 -0
- package/src/engine/tool/tool/grep.ts +156 -0
- package/src/engine/tool/tool/grep.txt +8 -0
- package/src/engine/tool/tool/invalid.ts +21 -0
- package/src/engine/tool/tool/json-schema.ts +164 -0
- package/src/engine/tool/tool/lsp.ts +113 -0
- package/src/engine/tool/tool/lsp.txt +24 -0
- package/src/engine/tool/tool/mcp-websearch.ts +96 -0
- package/src/engine/tool/tool/plan-enter.txt +14 -0
- package/src/engine/tool/tool/plan-exit.txt +13 -0
- package/src/engine/tool/tool/plan.ts +78 -0
- package/src/engine/tool/tool/question.ts +44 -0
- package/src/engine/tool/tool/question.txt +10 -0
- package/src/engine/tool/tool/read.ts +337 -0
- package/src/engine/tool/tool/read.txt +14 -0
- package/src/engine/tool/tool/registry.ts +472 -0
- package/src/engine/tool/tool/repo_clone.ts +80 -0
- package/src/engine/tool/tool/repo_clone.txt +5 -0
- package/src/engine/tool/tool/repo_overview.ts +279 -0
- package/src/engine/tool/tool/repo_overview.txt +4 -0
- package/src/engine/tool/tool/schema.ts +14 -0
- package/src/engine/tool/tool/shell/id.ts +19 -0
- package/src/engine/tool/tool/shell/prompt.ts +295 -0
- package/src/engine/tool/tool/shell/shell.txt +77 -0
- package/src/engine/tool/tool/shell.ts +647 -0
- package/src/engine/tool/tool/skill.ts +75 -0
- package/src/engine/tool/tool/skill.txt +5 -0
- package/src/engine/tool/tool/task.ts +337 -0
- package/src/engine/tool/tool/task.txt +58 -0
- package/src/engine/tool/tool/task_status.ts +179 -0
- package/src/engine/tool/tool/task_status.txt +13 -0
- package/src/engine/tool/tool/todo.ts +57 -0
- package/src/engine/tool/tool/todowrite.txt +167 -0
- package/src/engine/tool/tool/tool.ts +164 -0
- package/src/engine/tool/tool/truncate.ts +160 -0
- package/src/engine/tool/tool/truncation-dir.ts +4 -0
- package/src/engine/tool/tool/webfetch.ts +192 -0
- package/src/engine/tool/tool/webfetch.txt +13 -0
- package/src/engine/tool/tool/websearch.ts +143 -0
- package/src/engine/tool/tool/websearch.txt +14 -0
- package/src/engine/tool/tool/write.ts +104 -0
- package/src/engine/tool/tool/write.txt +8 -0
- package/src/engine/tool/tool.ts +164 -0
- package/src/engine/tool/truncate.ts +160 -0
- package/src/engine/tool/truncation-dir.ts +4 -0
- package/src/engine/tool/webfetch.ts +192 -0
- package/src/engine/tool/webfetch.txt +13 -0
- package/src/engine/tool/websearch.ts +143 -0
- package/src/engine/tool/websearch.txt +14 -0
- package/src/engine/tool/write.ts +104 -0
- package/src/engine/tool/write.txt +8 -0
- package/src/engine/util/archive.ts +17 -0
- package/src/engine/util/bom.ts +31 -0
- package/src/engine/util/data-url.ts +9 -0
- package/src/engine/util/defer.ts +10 -0
- package/src/engine/util/effect-http-client.ts +11 -0
- package/src/engine/util/error.ts +88 -0
- package/src/engine/util/filesystem.ts +252 -0
- package/src/engine/util/format.ts +20 -0
- package/src/engine/util/iife.ts +3 -0
- package/src/engine/util/lazy.ts +20 -0
- package/src/engine/util/local-context.ts +25 -0
- package/src/engine/util/locale.ts +86 -0
- package/src/engine/util/media.ts +26 -0
- package/src/engine/util/process.ts +176 -0
- package/src/engine/util/queue.ts +32 -0
- package/src/engine/util/record.ts +3 -0
- package/src/engine/util/repository.ts +158 -0
- package/src/engine/util/rpc.ts +66 -0
- package/src/engine/util/signal.ts +12 -0
- package/src/engine/util/timeout.ts +13 -0
- package/src/engine/util/token.ts +7 -0
- package/src/engine/util/util/archive.ts +17 -0
- package/src/engine/util/util/bom.ts +31 -0
- package/src/engine/util/util/data-url.ts +9 -0
- package/src/engine/util/util/defer.ts +10 -0
- package/src/engine/util/util/effect-http-client.ts +11 -0
- package/src/engine/util/util/error.ts +88 -0
- package/src/engine/util/util/filesystem.ts +252 -0
- package/src/engine/util/util/format.ts +20 -0
- package/src/engine/util/util/iife.ts +3 -0
- package/src/engine/util/util/lazy.ts +20 -0
- package/src/engine/util/util/local-context.ts +25 -0
- package/src/engine/util/util/locale.ts +86 -0
- package/src/engine/util/util/media.ts +26 -0
- package/src/engine/util/util/process.ts +176 -0
- package/src/engine/util/util/queue.ts +32 -0
- package/src/engine/util/util/record.ts +3 -0
- package/src/engine/util/util/repository.ts +158 -0
- package/src/engine/util/util/rpc.ts +66 -0
- package/src/engine/util/util/signal.ts +12 -0
- package/src/engine/util/util/timeout.ts +13 -0
- package/src/engine/util/util/token.ts +7 -0
- package/src/engine/util/util/which.ts +14 -0
- package/src/engine/util/util/wildcard.ts +59 -0
- package/src/engine/util/which.ts +14 -0
- package/src/engine/util/wildcard.ts +59 -0
- package/src/engine/worktree/index.ts +621 -0
- package/src/rag_worker.ts +519 -0
- package/src/server.ts +201 -0
- package/src/tui.ts +637 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1968 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RequestError,
|
|
3
|
+
type Agent as ACPAgent,
|
|
4
|
+
type AgentSideConnection,
|
|
5
|
+
type AuthenticateRequest,
|
|
6
|
+
type AuthMethod,
|
|
7
|
+
type CancelNotification,
|
|
8
|
+
type CloseSessionRequest,
|
|
9
|
+
type CloseSessionResponse,
|
|
10
|
+
type ForkSessionRequest,
|
|
11
|
+
type ForkSessionResponse,
|
|
12
|
+
type InitializeRequest,
|
|
13
|
+
type InitializeResponse,
|
|
14
|
+
type ListSessionsRequest,
|
|
15
|
+
type ListSessionsResponse,
|
|
16
|
+
type LoadSessionRequest,
|
|
17
|
+
type NewSessionRequest,
|
|
18
|
+
type PermissionOption,
|
|
19
|
+
type PlanEntry,
|
|
20
|
+
type PromptRequest,
|
|
21
|
+
type ResumeSessionRequest,
|
|
22
|
+
type ResumeSessionResponse,
|
|
23
|
+
type Role,
|
|
24
|
+
type SessionInfo,
|
|
25
|
+
type SetSessionModelRequest,
|
|
26
|
+
type SessionConfigOption,
|
|
27
|
+
type SetSessionConfigOptionRequest,
|
|
28
|
+
type SetSessionConfigOptionResponse,
|
|
29
|
+
type SetSessionModeRequest,
|
|
30
|
+
type SetSessionModeResponse,
|
|
31
|
+
type ToolCallContent,
|
|
32
|
+
type ToolKind,
|
|
33
|
+
type Usage,
|
|
34
|
+
} from "@agentclientprotocol/sdk"
|
|
35
|
+
|
|
36
|
+
import * as Log from "@opencode-ai/core/util/log"
|
|
37
|
+
import { pathToFileURL } from "url"
|
|
38
|
+
import { Filesystem } from "@/util/filesystem"
|
|
39
|
+
import { Hash } from "@opencode-ai/core/util/hash"
|
|
40
|
+
import { ACPSessionManager } from "./session"
|
|
41
|
+
import type { ACPConfig } from "./types"
|
|
42
|
+
import { ACPRuntime } from "./runtime"
|
|
43
|
+
import { Provider } from "@/provider/provider"
|
|
44
|
+
import { ModelID, ProviderID } from "../provider/schema"
|
|
45
|
+
import { Installation } from "@/installation"
|
|
46
|
+
import { MessageV2 } from "@/session/message-v2"
|
|
47
|
+
import { Config } from "@/config/config"
|
|
48
|
+
import { ConfigMCP } from "@/config/mcp"
|
|
49
|
+
import { Todo } from "@/session/todo"
|
|
50
|
+
import { Result, Schema } from "effect"
|
|
51
|
+
import { LoadAPIKeyError } from "ai"
|
|
52
|
+
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
|
|
53
|
+
import { applyPatch } from "diff"
|
|
54
|
+
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
|
55
|
+
import { ShellID } from "@/tool/shell/id"
|
|
56
|
+
|
|
57
|
+
type ModeOption = { id: string; name: string; description?: string }
|
|
58
|
+
type ModelOption = { modelId: string; name: string }
|
|
59
|
+
const decodeTodos = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Todo.Info)))
|
|
60
|
+
|
|
61
|
+
const DEFAULT_VARIANT_VALUE = "default"
|
|
62
|
+
|
|
63
|
+
const log = Log.create({ service: "acp-agent" })
|
|
64
|
+
|
|
65
|
+
async function getContextLimit(
|
|
66
|
+
sdk: OpencodeClient,
|
|
67
|
+
providerID: ProviderID,
|
|
68
|
+
modelID: ModelID,
|
|
69
|
+
directory: string,
|
|
70
|
+
): Promise<number | null> {
|
|
71
|
+
const providers = await sdk.config
|
|
72
|
+
.providers({ directory })
|
|
73
|
+
.then((x) => x.data?.providers ?? [])
|
|
74
|
+
.catch((error) => {
|
|
75
|
+
log.error("failed to get providers for context limit", { error })
|
|
76
|
+
return []
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const provider = providers.find((p) => p.id === providerID)
|
|
80
|
+
const model = provider?.models[modelID]
|
|
81
|
+
return model?.limit.context ?? null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function sendUsageUpdate(
|
|
85
|
+
connection: AgentSideConnection,
|
|
86
|
+
sdk: OpencodeClient,
|
|
87
|
+
sessionID: string,
|
|
88
|
+
directory: string,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const messages = await sdk.session
|
|
91
|
+
.messages({ sessionID, directory }, { throwOnError: true })
|
|
92
|
+
.then((x) => x.data)
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
log.error("failed to fetch messages for usage update", { error })
|
|
95
|
+
return undefined
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (!messages) return
|
|
99
|
+
|
|
100
|
+
const assistantMessages = messages.filter(
|
|
101
|
+
(m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
|
105
|
+
if (!lastAssistant) return
|
|
106
|
+
|
|
107
|
+
const msg = lastAssistant.info
|
|
108
|
+
if (!msg.providerID || !msg.modelID) return
|
|
109
|
+
const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory)
|
|
110
|
+
|
|
111
|
+
if (!size) {
|
|
112
|
+
// Cannot calculate usage without known context size
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
|
|
117
|
+
const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
|
|
118
|
+
|
|
119
|
+
await connection
|
|
120
|
+
.sessionUpdate({
|
|
121
|
+
sessionId: sessionID,
|
|
122
|
+
update: {
|
|
123
|
+
sessionUpdate: "usage_update",
|
|
124
|
+
used,
|
|
125
|
+
size,
|
|
126
|
+
cost: { amount: totalCost, currency: "USD" },
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
.catch((error) => {
|
|
130
|
+
log.error("failed to send usage update", { error })
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
|
|
135
|
+
return {
|
|
136
|
+
create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
|
|
137
|
+
return new Agent(connection, fullConfig)
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class Agent implements ACPAgent {
|
|
143
|
+
private connection: AgentSideConnection
|
|
144
|
+
private config: ACPConfig
|
|
145
|
+
private sdk: OpencodeClient
|
|
146
|
+
private sessionManager: ACPSessionManager
|
|
147
|
+
private eventAbort = new AbortController()
|
|
148
|
+
private eventStarted = false
|
|
149
|
+
private shellSnapshots = new Map<string, string>()
|
|
150
|
+
private toolStarts = new Set<string>()
|
|
151
|
+
private permissionQueues = new Map<string, Promise<void>>()
|
|
152
|
+
private permissionOptions: PermissionOption[] = [
|
|
153
|
+
{ optionId: "once", kind: "allow_once", name: "Allow once" },
|
|
154
|
+
{ optionId: "always", kind: "allow_always", name: "Always allow" },
|
|
155
|
+
{ optionId: "reject", kind: "reject_once", name: "Reject" },
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
constructor(connection: AgentSideConnection, config: ACPConfig) {
|
|
159
|
+
this.connection = connection
|
|
160
|
+
this.config = config
|
|
161
|
+
this.sdk = config.sdk
|
|
162
|
+
this.sessionManager = new ACPSessionManager(this.sdk)
|
|
163
|
+
this.startEventSubscription()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private startEventSubscription() {
|
|
167
|
+
if (this.eventStarted) return
|
|
168
|
+
this.eventStarted = true
|
|
169
|
+
this.runEventSubscription().catch((error) => {
|
|
170
|
+
if (this.eventAbort.signal.aborted) return
|
|
171
|
+
log.error("event subscription failed", { error })
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async runEventSubscription() {
|
|
176
|
+
while (true) {
|
|
177
|
+
if (this.eventAbort.signal.aborted) return
|
|
178
|
+
const events = await this.sdk.global.event({
|
|
179
|
+
signal: this.eventAbort.signal,
|
|
180
|
+
})
|
|
181
|
+
for await (const event of events.stream) {
|
|
182
|
+
if (this.eventAbort.signal.aborted) return
|
|
183
|
+
const payload = event?.payload
|
|
184
|
+
if (!payload) continue
|
|
185
|
+
await this.handleEvent(payload as Event).catch((error) => {
|
|
186
|
+
log.error("failed to handle event", { error, type: payload.type })
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async handleEvent(event: Event) {
|
|
193
|
+
switch (event.type) {
|
|
194
|
+
case "permission.asked": {
|
|
195
|
+
const permission = event.properties
|
|
196
|
+
const session = this.sessionManager.tryGet(permission.sessionID)
|
|
197
|
+
if (!session) return
|
|
198
|
+
|
|
199
|
+
const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
|
|
200
|
+
const next = prev
|
|
201
|
+
.then(async () => {
|
|
202
|
+
const directory = session.cwd
|
|
203
|
+
|
|
204
|
+
const res = await this.connection
|
|
205
|
+
.requestPermission({
|
|
206
|
+
sessionId: permission.sessionID,
|
|
207
|
+
toolCall: {
|
|
208
|
+
toolCallId: permission.tool?.callID ?? permission.id,
|
|
209
|
+
status: "pending",
|
|
210
|
+
title: permission.permission,
|
|
211
|
+
rawInput: permission.metadata,
|
|
212
|
+
kind: toToolKind(permission.permission),
|
|
213
|
+
locations: toLocations(permission.permission, permission.metadata),
|
|
214
|
+
},
|
|
215
|
+
options: this.permissionOptions,
|
|
216
|
+
})
|
|
217
|
+
.catch(async (error) => {
|
|
218
|
+
log.error("failed to request permission from ACP", {
|
|
219
|
+
error,
|
|
220
|
+
permissionID: permission.id,
|
|
221
|
+
sessionID: permission.sessionID,
|
|
222
|
+
})
|
|
223
|
+
await this.sdk.permission.reply({
|
|
224
|
+
requestID: permission.id,
|
|
225
|
+
reply: "reject",
|
|
226
|
+
directory,
|
|
227
|
+
})
|
|
228
|
+
return undefined
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
if (!res) return
|
|
232
|
+
if (res.outcome.outcome !== "selected") {
|
|
233
|
+
await this.sdk.permission.reply({
|
|
234
|
+
requestID: permission.id,
|
|
235
|
+
reply: "reject",
|
|
236
|
+
directory,
|
|
237
|
+
})
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
|
|
242
|
+
const metadata = permission.metadata || {}
|
|
243
|
+
const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
|
|
244
|
+
const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
|
|
245
|
+
const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
|
|
246
|
+
const newContent = getNewContent(content, diff)
|
|
247
|
+
|
|
248
|
+
if (newContent) {
|
|
249
|
+
void this.connection.writeTextFile({
|
|
250
|
+
sessionId: session.id,
|
|
251
|
+
path: filepath,
|
|
252
|
+
content: newContent,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await this.sdk.permission.reply({
|
|
258
|
+
requestID: permission.id,
|
|
259
|
+
reply: res.outcome.optionId as "once" | "always" | "reject",
|
|
260
|
+
directory,
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
.catch((error) => {
|
|
264
|
+
log.error("failed to handle permission", { error, permissionID: permission.id })
|
|
265
|
+
})
|
|
266
|
+
.finally(() => {
|
|
267
|
+
if (this.permissionQueues.get(permission.sessionID) === next) {
|
|
268
|
+
this.permissionQueues.delete(permission.sessionID)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
this.permissionQueues.set(permission.sessionID, next)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case "message.part.updated": {
|
|
276
|
+
log.info("message part updated", { event: event.properties })
|
|
277
|
+
const props = event.properties
|
|
278
|
+
const part = props.part
|
|
279
|
+
const session = this.sessionManager.tryGet(part.sessionID)
|
|
280
|
+
if (!session) return
|
|
281
|
+
const sessionId = session.id
|
|
282
|
+
|
|
283
|
+
if (part.type === "tool") {
|
|
284
|
+
await this.toolStart(sessionId, part)
|
|
285
|
+
|
|
286
|
+
switch (part.state.status) {
|
|
287
|
+
case "pending":
|
|
288
|
+
this.shellSnapshots.delete(part.callID)
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
case "running":
|
|
292
|
+
const output = this.shellOutput(part)
|
|
293
|
+
const content: ToolCallContent[] = []
|
|
294
|
+
if (output) {
|
|
295
|
+
const hash = Hash.fast(output)
|
|
296
|
+
if (part.tool === ShellID.ToolID) {
|
|
297
|
+
if (this.shellSnapshots.get(part.callID) === hash) {
|
|
298
|
+
await this.connection
|
|
299
|
+
.sessionUpdate({
|
|
300
|
+
sessionId,
|
|
301
|
+
update: {
|
|
302
|
+
sessionUpdate: "tool_call_update",
|
|
303
|
+
toolCallId: part.callID,
|
|
304
|
+
status: "in_progress",
|
|
305
|
+
kind: toToolKind(part.tool),
|
|
306
|
+
title: part.tool,
|
|
307
|
+
locations: toLocations(part.tool, part.state.input),
|
|
308
|
+
rawInput: part.state.input,
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
.catch((error) => {
|
|
312
|
+
log.error("failed to send tool in_progress to ACP", { error })
|
|
313
|
+
})
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
this.shellSnapshots.set(part.callID, hash)
|
|
317
|
+
}
|
|
318
|
+
content.push({
|
|
319
|
+
type: "content",
|
|
320
|
+
content: {
|
|
321
|
+
type: "text",
|
|
322
|
+
text: output,
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
await this.connection
|
|
327
|
+
.sessionUpdate({
|
|
328
|
+
sessionId,
|
|
329
|
+
update: {
|
|
330
|
+
sessionUpdate: "tool_call_update",
|
|
331
|
+
toolCallId: part.callID,
|
|
332
|
+
status: "in_progress",
|
|
333
|
+
kind: toToolKind(part.tool),
|
|
334
|
+
title: part.tool,
|
|
335
|
+
locations: toLocations(part.tool, part.state.input),
|
|
336
|
+
rawInput: part.state.input,
|
|
337
|
+
...(content.length > 0 && { content }),
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
.catch((error) => {
|
|
341
|
+
log.error("failed to send tool in_progress to ACP", { error })
|
|
342
|
+
})
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
case "completed": {
|
|
346
|
+
this.toolStarts.delete(part.callID)
|
|
347
|
+
this.shellSnapshots.delete(part.callID)
|
|
348
|
+
const kind = toToolKind(part.tool)
|
|
349
|
+
const content = completedToolContent(part, kind)
|
|
350
|
+
|
|
351
|
+
if (part.tool === "todowrite") {
|
|
352
|
+
const parsedTodos = decodeTodos(part.state.output)
|
|
353
|
+
if (Result.isSuccess(parsedTodos)) {
|
|
354
|
+
await this.connection
|
|
355
|
+
.sessionUpdate({
|
|
356
|
+
sessionId,
|
|
357
|
+
update: {
|
|
358
|
+
sessionUpdate: "plan",
|
|
359
|
+
entries: parsedTodos.success.map((todo) => {
|
|
360
|
+
const status: PlanEntry["status"] =
|
|
361
|
+
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
|
|
362
|
+
return {
|
|
363
|
+
priority: "medium",
|
|
364
|
+
status,
|
|
365
|
+
content: todo.content,
|
|
366
|
+
}
|
|
367
|
+
}),
|
|
368
|
+
},
|
|
369
|
+
})
|
|
370
|
+
.catch((error) => {
|
|
371
|
+
log.error("failed to send session update for todo", { error })
|
|
372
|
+
})
|
|
373
|
+
} else {
|
|
374
|
+
log.error("failed to parse todo output", { error: parsedTodos.failure })
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await this.connection
|
|
379
|
+
.sessionUpdate({
|
|
380
|
+
sessionId,
|
|
381
|
+
update: {
|
|
382
|
+
sessionUpdate: "tool_call_update",
|
|
383
|
+
toolCallId: part.callID,
|
|
384
|
+
status: "completed",
|
|
385
|
+
kind,
|
|
386
|
+
content,
|
|
387
|
+
title: part.state.title,
|
|
388
|
+
rawInput: part.state.input,
|
|
389
|
+
rawOutput: completedToolRawOutput(part),
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
.catch((error) => {
|
|
393
|
+
log.error("failed to send tool completed to ACP", { error })
|
|
394
|
+
})
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
case "error":
|
|
398
|
+
this.toolStarts.delete(part.callID)
|
|
399
|
+
this.shellSnapshots.delete(part.callID)
|
|
400
|
+
await this.connection
|
|
401
|
+
.sessionUpdate({
|
|
402
|
+
sessionId,
|
|
403
|
+
update: {
|
|
404
|
+
sessionUpdate: "tool_call_update",
|
|
405
|
+
toolCallId: part.callID,
|
|
406
|
+
status: "failed",
|
|
407
|
+
kind: toToolKind(part.tool),
|
|
408
|
+
title: part.tool,
|
|
409
|
+
rawInput: part.state.input,
|
|
410
|
+
content: [
|
|
411
|
+
{
|
|
412
|
+
type: "content",
|
|
413
|
+
content: {
|
|
414
|
+
type: "text",
|
|
415
|
+
text: part.state.error,
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
rawOutput: {
|
|
420
|
+
error: part.state.error,
|
|
421
|
+
metadata: part.state.metadata,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
.catch((error) => {
|
|
426
|
+
log.error("failed to send tool error to ACP", { error })
|
|
427
|
+
})
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ACP clients already know the prompt they just submitted, so replaying
|
|
433
|
+
// live user parts duplicates the message. We still replay user history in
|
|
434
|
+
// loadSession() and forkSession() via processMessage().
|
|
435
|
+
if (part.type !== "text" && part.type !== "file") return
|
|
436
|
+
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
case "message.part.delta": {
|
|
441
|
+
const props = event.properties
|
|
442
|
+
const session = this.sessionManager.tryGet(props.sessionID)
|
|
443
|
+
if (!session) return
|
|
444
|
+
const sessionId = session.id
|
|
445
|
+
|
|
446
|
+
const message = await this.sdk.session
|
|
447
|
+
.message(
|
|
448
|
+
{
|
|
449
|
+
sessionID: props.sessionID,
|
|
450
|
+
messageID: props.messageID,
|
|
451
|
+
directory: session.cwd,
|
|
452
|
+
},
|
|
453
|
+
{ throwOnError: true },
|
|
454
|
+
)
|
|
455
|
+
.then((x) => x.data)
|
|
456
|
+
.catch((error) => {
|
|
457
|
+
log.error("unexpected error when fetching message", { error })
|
|
458
|
+
return undefined
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
if (!message || message.info.role !== "assistant") return
|
|
462
|
+
|
|
463
|
+
const part = message.parts.find((p) => p.id === props.partID)
|
|
464
|
+
if (!part) return
|
|
465
|
+
|
|
466
|
+
if (part.type === "text" && props.field === "text" && part.ignored !== true) {
|
|
467
|
+
await this.connection
|
|
468
|
+
.sessionUpdate({
|
|
469
|
+
sessionId,
|
|
470
|
+
update: {
|
|
471
|
+
sessionUpdate: "agent_message_chunk",
|
|
472
|
+
messageId: props.messageID,
|
|
473
|
+
content: {
|
|
474
|
+
type: "text",
|
|
475
|
+
text: props.delta,
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
})
|
|
479
|
+
.catch((error) => {
|
|
480
|
+
log.error("failed to send text delta to ACP", { error })
|
|
481
|
+
})
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (part.type === "reasoning" && props.field === "text") {
|
|
486
|
+
await this.connection
|
|
487
|
+
.sessionUpdate({
|
|
488
|
+
sessionId,
|
|
489
|
+
update: {
|
|
490
|
+
sessionUpdate: "agent_thought_chunk",
|
|
491
|
+
messageId: props.messageID,
|
|
492
|
+
content: {
|
|
493
|
+
type: "text",
|
|
494
|
+
text: props.delta,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
})
|
|
498
|
+
.catch((error) => {
|
|
499
|
+
log.error("failed to send reasoning delta to ACP", { error })
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
|
|
508
|
+
log.info("initialize", { protocolVersion: params.protocolVersion })
|
|
509
|
+
|
|
510
|
+
const authMethod: AuthMethod = {
|
|
511
|
+
description: "Run `opencode auth login` in the terminal",
|
|
512
|
+
name: "Login with opencode",
|
|
513
|
+
id: "opencode-login",
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// If client supports terminal-auth capability, use that instead.
|
|
517
|
+
if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
|
|
518
|
+
authMethod._meta = {
|
|
519
|
+
"terminal-auth": {
|
|
520
|
+
command: "opencode",
|
|
521
|
+
args: ["auth", "login"],
|
|
522
|
+
label: "OpenCode Login",
|
|
523
|
+
},
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
protocolVersion: 1,
|
|
529
|
+
agentCapabilities: {
|
|
530
|
+
loadSession: true,
|
|
531
|
+
mcpCapabilities: {
|
|
532
|
+
http: true,
|
|
533
|
+
sse: true,
|
|
534
|
+
},
|
|
535
|
+
promptCapabilities: {
|
|
536
|
+
embeddedContext: true,
|
|
537
|
+
image: true,
|
|
538
|
+
},
|
|
539
|
+
sessionCapabilities: {
|
|
540
|
+
close: {},
|
|
541
|
+
fork: {},
|
|
542
|
+
list: {},
|
|
543
|
+
resume: {},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
authMethods: [authMethod],
|
|
547
|
+
agentInfo: {
|
|
548
|
+
name: "OpenCode",
|
|
549
|
+
version: InstallationVersion,
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async authenticate(_params: AuthenticateRequest) {
|
|
555
|
+
throw new Error("Authentication not implemented")
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async newSession(params: NewSessionRequest) {
|
|
559
|
+
const directory = params.cwd
|
|
560
|
+
try {
|
|
561
|
+
const model = await defaultModel(this.config, directory)
|
|
562
|
+
|
|
563
|
+
// Store ACP session state
|
|
564
|
+
const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
|
|
565
|
+
const sessionId = state.id
|
|
566
|
+
|
|
567
|
+
log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
|
|
568
|
+
|
|
569
|
+
const load = await this.loadSessionMode({
|
|
570
|
+
cwd: directory,
|
|
571
|
+
mcpServers: params.mcpServers,
|
|
572
|
+
sessionId,
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
sessionId,
|
|
577
|
+
configOptions: load.configOptions,
|
|
578
|
+
models: load.models,
|
|
579
|
+
modes: load.modes,
|
|
580
|
+
_meta: load._meta,
|
|
581
|
+
}
|
|
582
|
+
} catch (e) {
|
|
583
|
+
const error = MessageV2.fromError(e, {
|
|
584
|
+
providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
|
|
585
|
+
})
|
|
586
|
+
if (LoadAPIKeyError.isInstance(error)) {
|
|
587
|
+
throw RequestError.authRequired()
|
|
588
|
+
}
|
|
589
|
+
throw e
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async loadSession(params: LoadSessionRequest) {
|
|
594
|
+
const directory = params.cwd
|
|
595
|
+
const sessionId = params.sessionId
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const model = await defaultModel(this.config, directory)
|
|
599
|
+
|
|
600
|
+
// Store ACP session state
|
|
601
|
+
await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
|
|
602
|
+
|
|
603
|
+
const messages = await this.loadSessionMessages(directory, sessionId)
|
|
604
|
+
this.restoreSessionStateFromMessages(sessionId, messages)
|
|
605
|
+
|
|
606
|
+
log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
|
|
607
|
+
|
|
608
|
+
const result = await this.loadSessionMode({
|
|
609
|
+
cwd: directory,
|
|
610
|
+
mcpServers: params.mcpServers,
|
|
611
|
+
sessionId,
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
for (const msg of messages ?? []) {
|
|
615
|
+
log.debug("replay message", msg)
|
|
616
|
+
await this.processMessage(msg)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
|
|
620
|
+
|
|
621
|
+
return result
|
|
622
|
+
} catch (e) {
|
|
623
|
+
const error = MessageV2.fromError(e, {
|
|
624
|
+
providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
|
|
625
|
+
})
|
|
626
|
+
if (LoadAPIKeyError.isInstance(error)) {
|
|
627
|
+
throw RequestError.authRequired()
|
|
628
|
+
}
|
|
629
|
+
throw e
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
|
|
634
|
+
try {
|
|
635
|
+
const cursor = params.cursor ? Number(params.cursor) : undefined
|
|
636
|
+
const limit = 100
|
|
637
|
+
|
|
638
|
+
const sessions = await this.sdk.session
|
|
639
|
+
.list(
|
|
640
|
+
{
|
|
641
|
+
directory: params.cwd ?? undefined,
|
|
642
|
+
roots: true,
|
|
643
|
+
},
|
|
644
|
+
{ throwOnError: true },
|
|
645
|
+
)
|
|
646
|
+
.then((x) => x.data ?? [])
|
|
647
|
+
|
|
648
|
+
const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
|
|
649
|
+
const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
|
|
650
|
+
const page = filtered.slice(0, limit)
|
|
651
|
+
|
|
652
|
+
const entries: SessionInfo[] = page.map((session) => ({
|
|
653
|
+
sessionId: session.id,
|
|
654
|
+
cwd: session.directory,
|
|
655
|
+
title: session.title,
|
|
656
|
+
updatedAt: new Date(session.time.updated).toISOString(),
|
|
657
|
+
}))
|
|
658
|
+
|
|
659
|
+
const last = page[page.length - 1]
|
|
660
|
+
const next = filtered.length > limit && last ? String(last.time.updated) : undefined
|
|
661
|
+
|
|
662
|
+
const response: ListSessionsResponse = {
|
|
663
|
+
sessions: entries,
|
|
664
|
+
}
|
|
665
|
+
if (next) response.nextCursor = next
|
|
666
|
+
return response
|
|
667
|
+
} catch (e) {
|
|
668
|
+
const error = MessageV2.fromError(e, {
|
|
669
|
+
providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
|
|
670
|
+
})
|
|
671
|
+
if (LoadAPIKeyError.isInstance(error)) {
|
|
672
|
+
throw RequestError.authRequired()
|
|
673
|
+
}
|
|
674
|
+
throw e
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
|
|
679
|
+
const directory = params.cwd
|
|
680
|
+
const mcpServers = params.mcpServers ?? []
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
const model = await defaultModel(this.config, directory)
|
|
684
|
+
|
|
685
|
+
const forked = await this.sdk.session
|
|
686
|
+
.fork(
|
|
687
|
+
{
|
|
688
|
+
sessionID: params.sessionId,
|
|
689
|
+
directory,
|
|
690
|
+
},
|
|
691
|
+
{ throwOnError: true },
|
|
692
|
+
)
|
|
693
|
+
.then((x) => x.data)
|
|
694
|
+
|
|
695
|
+
if (!forked) {
|
|
696
|
+
throw new Error("Fork session returned no data")
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const sessionId = forked.id
|
|
700
|
+
await this.sessionManager.load(sessionId, directory, mcpServers, model)
|
|
701
|
+
|
|
702
|
+
const messages = await this.loadSessionMessages(directory, sessionId)
|
|
703
|
+
this.restoreSessionStateFromMessages(sessionId, messages)
|
|
704
|
+
|
|
705
|
+
log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
|
|
706
|
+
|
|
707
|
+
const mode = await this.loadSessionMode({
|
|
708
|
+
cwd: directory,
|
|
709
|
+
mcpServers,
|
|
710
|
+
sessionId,
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
for (const msg of messages ?? []) {
|
|
714
|
+
log.debug("replay message", msg)
|
|
715
|
+
await this.processMessage(msg)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
|
|
719
|
+
|
|
720
|
+
return mode
|
|
721
|
+
} catch (e) {
|
|
722
|
+
const error = MessageV2.fromError(e, {
|
|
723
|
+
providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
|
|
724
|
+
})
|
|
725
|
+
if (LoadAPIKeyError.isInstance(error)) {
|
|
726
|
+
throw RequestError.authRequired()
|
|
727
|
+
}
|
|
728
|
+
throw e
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
|
|
733
|
+
const directory = params.cwd
|
|
734
|
+
const sessionId = params.sessionId
|
|
735
|
+
const mcpServers = params.mcpServers ?? []
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const model = await defaultModel(this.config, directory)
|
|
739
|
+
await this.sessionManager.load(sessionId, directory, mcpServers, model)
|
|
740
|
+
|
|
741
|
+
const messages = await this.loadSessionMessages(directory, sessionId, 20)
|
|
742
|
+
this.restoreSessionStateFromMessages(sessionId, messages)
|
|
743
|
+
|
|
744
|
+
log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
|
|
745
|
+
|
|
746
|
+
const result = await this.loadSessionMode({
|
|
747
|
+
cwd: directory,
|
|
748
|
+
mcpServers,
|
|
749
|
+
sessionId,
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
|
|
753
|
+
|
|
754
|
+
return result
|
|
755
|
+
} catch (e) {
|
|
756
|
+
const error = MessageV2.fromError(e, {
|
|
757
|
+
providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
|
|
758
|
+
})
|
|
759
|
+
if (LoadAPIKeyError.isInstance(error)) {
|
|
760
|
+
throw RequestError.authRequired()
|
|
761
|
+
}
|
|
762
|
+
throw e
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
767
|
+
const session = this.sessionManager.remove(params.sessionId)
|
|
768
|
+
if (!session) return {}
|
|
769
|
+
|
|
770
|
+
await this.sdk.session
|
|
771
|
+
.abort(
|
|
772
|
+
{
|
|
773
|
+
sessionID: params.sessionId,
|
|
774
|
+
directory: session.cwd,
|
|
775
|
+
},
|
|
776
|
+
{ throwOnError: true },
|
|
777
|
+
)
|
|
778
|
+
.catch((error) => {
|
|
779
|
+
log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId })
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
this.permissionQueues.delete(params.sessionId)
|
|
783
|
+
log.info("close_session", { sessionId: params.sessionId })
|
|
784
|
+
return {}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private async processMessage(message: SessionMessageResponse) {
|
|
788
|
+
log.debug("process message", message)
|
|
789
|
+
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
|
790
|
+
const sessionId = message.info.sessionID
|
|
791
|
+
|
|
792
|
+
for (const part of message.parts) {
|
|
793
|
+
if (part.type === "tool") {
|
|
794
|
+
await this.toolStart(sessionId, part)
|
|
795
|
+
switch (part.state.status) {
|
|
796
|
+
case "pending":
|
|
797
|
+
this.shellSnapshots.delete(part.callID)
|
|
798
|
+
break
|
|
799
|
+
case "running":
|
|
800
|
+
const output = this.shellOutput(part)
|
|
801
|
+
const runningContent: ToolCallContent[] = []
|
|
802
|
+
if (output) {
|
|
803
|
+
runningContent.push({
|
|
804
|
+
type: "content",
|
|
805
|
+
content: {
|
|
806
|
+
type: "text",
|
|
807
|
+
text: output,
|
|
808
|
+
},
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
await this.connection
|
|
812
|
+
.sessionUpdate({
|
|
813
|
+
sessionId,
|
|
814
|
+
update: {
|
|
815
|
+
sessionUpdate: "tool_call_update",
|
|
816
|
+
toolCallId: part.callID,
|
|
817
|
+
status: "in_progress",
|
|
818
|
+
kind: toToolKind(part.tool),
|
|
819
|
+
title: part.tool,
|
|
820
|
+
locations: toLocations(part.tool, part.state.input),
|
|
821
|
+
rawInput: part.state.input,
|
|
822
|
+
...(runningContent.length > 0 && { content: runningContent }),
|
|
823
|
+
},
|
|
824
|
+
})
|
|
825
|
+
.catch((err) => {
|
|
826
|
+
log.error("failed to send tool in_progress to ACP", { error: err })
|
|
827
|
+
})
|
|
828
|
+
break
|
|
829
|
+
case "completed":
|
|
830
|
+
this.toolStarts.delete(part.callID)
|
|
831
|
+
this.shellSnapshots.delete(part.callID)
|
|
832
|
+
const kind = toToolKind(part.tool)
|
|
833
|
+
const content = completedToolContent(part, kind)
|
|
834
|
+
|
|
835
|
+
if (part.tool === "todowrite") {
|
|
836
|
+
const parsedTodos = decodeTodos(part.state.output)
|
|
837
|
+
if (Result.isSuccess(parsedTodos)) {
|
|
838
|
+
await this.connection
|
|
839
|
+
.sessionUpdate({
|
|
840
|
+
sessionId,
|
|
841
|
+
update: {
|
|
842
|
+
sessionUpdate: "plan",
|
|
843
|
+
entries: parsedTodos.success.map((todo) => {
|
|
844
|
+
const status: PlanEntry["status"] =
|
|
845
|
+
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
|
|
846
|
+
return {
|
|
847
|
+
priority: "medium",
|
|
848
|
+
status,
|
|
849
|
+
content: todo.content,
|
|
850
|
+
}
|
|
851
|
+
}),
|
|
852
|
+
},
|
|
853
|
+
})
|
|
854
|
+
.catch((err) => {
|
|
855
|
+
log.error("failed to send session update for todo", { error: err })
|
|
856
|
+
})
|
|
857
|
+
} else {
|
|
858
|
+
log.error("failed to parse todo output", { error: parsedTodos.failure })
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
await this.connection
|
|
863
|
+
.sessionUpdate({
|
|
864
|
+
sessionId,
|
|
865
|
+
update: {
|
|
866
|
+
sessionUpdate: "tool_call_update",
|
|
867
|
+
toolCallId: part.callID,
|
|
868
|
+
status: "completed",
|
|
869
|
+
kind,
|
|
870
|
+
content,
|
|
871
|
+
title: part.state.title,
|
|
872
|
+
rawInput: part.state.input,
|
|
873
|
+
rawOutput: completedToolRawOutput(part),
|
|
874
|
+
},
|
|
875
|
+
})
|
|
876
|
+
.catch((err) => {
|
|
877
|
+
log.error("failed to send tool completed to ACP", { error: err })
|
|
878
|
+
})
|
|
879
|
+
break
|
|
880
|
+
case "error":
|
|
881
|
+
this.toolStarts.delete(part.callID)
|
|
882
|
+
this.shellSnapshots.delete(part.callID)
|
|
883
|
+
await this.connection
|
|
884
|
+
.sessionUpdate({
|
|
885
|
+
sessionId,
|
|
886
|
+
update: {
|
|
887
|
+
sessionUpdate: "tool_call_update",
|
|
888
|
+
toolCallId: part.callID,
|
|
889
|
+
status: "failed",
|
|
890
|
+
kind: toToolKind(part.tool),
|
|
891
|
+
title: part.tool,
|
|
892
|
+
rawInput: part.state.input,
|
|
893
|
+
content: [
|
|
894
|
+
{
|
|
895
|
+
type: "content",
|
|
896
|
+
content: {
|
|
897
|
+
type: "text",
|
|
898
|
+
text: part.state.error,
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
rawOutput: {
|
|
903
|
+
error: part.state.error,
|
|
904
|
+
metadata: part.state.metadata,
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
})
|
|
908
|
+
.catch((err) => {
|
|
909
|
+
log.error("failed to send tool error to ACP", { error: err })
|
|
910
|
+
})
|
|
911
|
+
break
|
|
912
|
+
}
|
|
913
|
+
} else if (part.type === "text") {
|
|
914
|
+
if (part.text) {
|
|
915
|
+
const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined
|
|
916
|
+
await this.connection
|
|
917
|
+
.sessionUpdate({
|
|
918
|
+
sessionId,
|
|
919
|
+
update: {
|
|
920
|
+
sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
|
921
|
+
messageId: message.info.id,
|
|
922
|
+
content: {
|
|
923
|
+
type: "text",
|
|
924
|
+
text: part.text,
|
|
925
|
+
...(audience && { annotations: { audience } }),
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
})
|
|
929
|
+
.catch((err) => {
|
|
930
|
+
log.error("failed to send text to ACP", { error: err })
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
} else if (part.type === "file") {
|
|
934
|
+
// Replay file attachments as appropriate ACP content blocks.
|
|
935
|
+
// OpenCode stores files internally as { type: "file", url, filename, mime }.
|
|
936
|
+
// We convert these back to ACP blocks based on the URL scheme and MIME type:
|
|
937
|
+
// - file:// URLs → resource_link
|
|
938
|
+
// - data: URLs with image/* → image block
|
|
939
|
+
// - data: URLs with text/* or application/json → resource with text
|
|
940
|
+
// - data: URLs with other types → resource with blob
|
|
941
|
+
const url = part.url
|
|
942
|
+
const filename = part.filename ?? "file"
|
|
943
|
+
const mime = part.mime || "application/octet-stream"
|
|
944
|
+
const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk"
|
|
945
|
+
|
|
946
|
+
if (url.startsWith("file://")) {
|
|
947
|
+
// Local file reference - send as resource_link
|
|
948
|
+
await this.connection
|
|
949
|
+
.sessionUpdate({
|
|
950
|
+
sessionId,
|
|
951
|
+
update: {
|
|
952
|
+
sessionUpdate: messageChunk,
|
|
953
|
+
messageId: message.info.id,
|
|
954
|
+
content: { type: "resource_link", uri: url, name: filename, mimeType: mime },
|
|
955
|
+
},
|
|
956
|
+
})
|
|
957
|
+
.catch((err) => {
|
|
958
|
+
log.error("failed to send resource_link to ACP", { error: err })
|
|
959
|
+
})
|
|
960
|
+
} else if (url.startsWith("data:")) {
|
|
961
|
+
// Embedded content - parse data URL and send as appropriate block type
|
|
962
|
+
const base64Match = url.match(/^data:([^;]+);base64,(.*)$/)
|
|
963
|
+
const dataMime = base64Match?.[1]
|
|
964
|
+
const base64Data = base64Match?.[2] ?? ""
|
|
965
|
+
|
|
966
|
+
const effectiveMime = dataMime || mime
|
|
967
|
+
|
|
968
|
+
if (effectiveMime.startsWith("image/")) {
|
|
969
|
+
// Image - send as image block
|
|
970
|
+
await this.connection
|
|
971
|
+
.sessionUpdate({
|
|
972
|
+
sessionId,
|
|
973
|
+
update: {
|
|
974
|
+
sessionUpdate: messageChunk,
|
|
975
|
+
messageId: message.info.id,
|
|
976
|
+
content: {
|
|
977
|
+
type: "image",
|
|
978
|
+
mimeType: effectiveMime,
|
|
979
|
+
data: base64Data,
|
|
980
|
+
uri: pathToFileURL(filename).href,
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
})
|
|
984
|
+
.catch((err) => {
|
|
985
|
+
log.error("failed to send image to ACP", { error: err })
|
|
986
|
+
})
|
|
987
|
+
} else {
|
|
988
|
+
// Non-image: text types get decoded, binary types stay as blob
|
|
989
|
+
const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
|
|
990
|
+
const fileUri = pathToFileURL(filename).href
|
|
991
|
+
const resource = isText
|
|
992
|
+
? {
|
|
993
|
+
uri: fileUri,
|
|
994
|
+
mimeType: effectiveMime,
|
|
995
|
+
text: Buffer.from(base64Data, "base64").toString("utf-8"),
|
|
996
|
+
}
|
|
997
|
+
: { uri: fileUri, mimeType: effectiveMime, blob: base64Data }
|
|
998
|
+
|
|
999
|
+
await this.connection
|
|
1000
|
+
.sessionUpdate({
|
|
1001
|
+
sessionId,
|
|
1002
|
+
update: {
|
|
1003
|
+
sessionUpdate: messageChunk,
|
|
1004
|
+
messageId: message.info.id,
|
|
1005
|
+
content: { type: "resource", resource },
|
|
1006
|
+
},
|
|
1007
|
+
})
|
|
1008
|
+
.catch((err) => {
|
|
1009
|
+
log.error("failed to send resource to ACP", { error: err })
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// URLs that don't match file:// or data: are skipped (unsupported)
|
|
1014
|
+
} else if (part.type === "reasoning") {
|
|
1015
|
+
if (part.text) {
|
|
1016
|
+
await this.connection
|
|
1017
|
+
.sessionUpdate({
|
|
1018
|
+
sessionId,
|
|
1019
|
+
update: {
|
|
1020
|
+
sessionUpdate: "agent_thought_chunk",
|
|
1021
|
+
messageId: message.info.id,
|
|
1022
|
+
content: {
|
|
1023
|
+
type: "text",
|
|
1024
|
+
text: part.text,
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
})
|
|
1028
|
+
.catch((err) => {
|
|
1029
|
+
log.error("failed to send reasoning to ACP", { error: err })
|
|
1030
|
+
})
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private shellOutput(part: ToolPart) {
|
|
1037
|
+
if (part.tool !== ShellID.ToolID) return
|
|
1038
|
+
if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
|
|
1039
|
+
const output = part.state.metadata["output"]
|
|
1040
|
+
if (typeof output !== "string") return
|
|
1041
|
+
return output
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private async toolStart(sessionId: string, part: ToolPart) {
|
|
1045
|
+
if (this.toolStarts.has(part.callID)) return
|
|
1046
|
+
this.toolStarts.add(part.callID)
|
|
1047
|
+
await this.connection
|
|
1048
|
+
.sessionUpdate({
|
|
1049
|
+
sessionId,
|
|
1050
|
+
update: {
|
|
1051
|
+
sessionUpdate: "tool_call",
|
|
1052
|
+
toolCallId: part.callID,
|
|
1053
|
+
title: part.tool,
|
|
1054
|
+
kind: toToolKind(part.tool),
|
|
1055
|
+
status: "pending",
|
|
1056
|
+
locations: [],
|
|
1057
|
+
rawInput: {},
|
|
1058
|
+
},
|
|
1059
|
+
})
|
|
1060
|
+
.catch((error) => {
|
|
1061
|
+
log.error("failed to send tool pending to ACP", { error })
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
|
|
1066
|
+
const agents = await this.config.sdk.app
|
|
1067
|
+
.agents(
|
|
1068
|
+
{
|
|
1069
|
+
directory,
|
|
1070
|
+
},
|
|
1071
|
+
{ throwOnError: true },
|
|
1072
|
+
)
|
|
1073
|
+
.then((resp) => resp.data!)
|
|
1074
|
+
|
|
1075
|
+
return agents
|
|
1076
|
+
.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
|
|
1077
|
+
.map((agent) => ({
|
|
1078
|
+
id: agent.name,
|
|
1079
|
+
name: agent.name,
|
|
1080
|
+
description: agent.description,
|
|
1081
|
+
}))
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private async resolveModeState(
|
|
1085
|
+
directory: string,
|
|
1086
|
+
sessionId: string,
|
|
1087
|
+
): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
|
|
1088
|
+
const availableModes = await this.loadAvailableModes(directory)
|
|
1089
|
+
const storedModeId = this.sessionManager.get(sessionId).modeId
|
|
1090
|
+
if (storedModeId && availableModes.some((mode) => mode.id === storedModeId)) {
|
|
1091
|
+
return { availableModes, currentModeId: storedModeId }
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const currentModeId = await (async () => {
|
|
1095
|
+
if (!availableModes.length) return undefined
|
|
1096
|
+
const defaultAgent = await ACPRuntime.defaultAgentInfo(directory)
|
|
1097
|
+
const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgent.name)?.id ?? availableModes[0].id
|
|
1098
|
+
this.sessionManager.setMode(sessionId, resolvedModeId)
|
|
1099
|
+
return resolvedModeId
|
|
1100
|
+
})()
|
|
1101
|
+
|
|
1102
|
+
return { availableModes, currentModeId }
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private async loadSessionMode(params: LoadSessionRequest) {
|
|
1106
|
+
const directory = params.cwd
|
|
1107
|
+
const sessionId = params.sessionId
|
|
1108
|
+
const model = this.sessionManager.get(sessionId).model ?? (await defaultModel(this.config, directory))
|
|
1109
|
+
|
|
1110
|
+
const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
|
|
1111
|
+
const entries = sortProvidersByName(providers)
|
|
1112
|
+
const availableVariants = modelVariantsFromProviders(entries, model)
|
|
1113
|
+
const currentVariant = this.sessionManager.getVariant(sessionId)
|
|
1114
|
+
if (currentVariant && !availableVariants.includes(currentVariant)) {
|
|
1115
|
+
this.sessionManager.setVariant(sessionId, undefined)
|
|
1116
|
+
}
|
|
1117
|
+
const availableModels = buildAvailableModels(entries)
|
|
1118
|
+
const modeState = await this.resolveModeState(directory, sessionId)
|
|
1119
|
+
const currentModeId = modeState.currentModeId
|
|
1120
|
+
const modes = currentModeId
|
|
1121
|
+
? {
|
|
1122
|
+
availableModes: modeState.availableModes,
|
|
1123
|
+
currentModeId,
|
|
1124
|
+
}
|
|
1125
|
+
: undefined
|
|
1126
|
+
|
|
1127
|
+
const commands = await this.config.sdk.command
|
|
1128
|
+
.list(
|
|
1129
|
+
{
|
|
1130
|
+
directory,
|
|
1131
|
+
},
|
|
1132
|
+
{ throwOnError: true },
|
|
1133
|
+
)
|
|
1134
|
+
.then((resp) => resp.data!)
|
|
1135
|
+
|
|
1136
|
+
const availableCommands = commands.map((command) => ({
|
|
1137
|
+
name: command.name,
|
|
1138
|
+
description: command.description ?? "",
|
|
1139
|
+
}))
|
|
1140
|
+
const names = new Set(availableCommands.map((c) => c.name))
|
|
1141
|
+
if (!names.has("compact"))
|
|
1142
|
+
availableCommands.push({
|
|
1143
|
+
name: "compact",
|
|
1144
|
+
description: "compact the session",
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
const mcpServers: Record<string, ConfigMCP.Info> = {}
|
|
1148
|
+
for (const server of params.mcpServers) {
|
|
1149
|
+
if ("type" in server) {
|
|
1150
|
+
mcpServers[server.name] = {
|
|
1151
|
+
url: server.url,
|
|
1152
|
+
headers: server.headers.reduce<Record<string, string>>((acc, { name, value }) => {
|
|
1153
|
+
acc[name] = value
|
|
1154
|
+
return acc
|
|
1155
|
+
}, {}),
|
|
1156
|
+
type: "remote",
|
|
1157
|
+
}
|
|
1158
|
+
} else {
|
|
1159
|
+
mcpServers[server.name] = {
|
|
1160
|
+
type: "local",
|
|
1161
|
+
command: [server.command, ...server.args],
|
|
1162
|
+
environment: server.env.reduce<Record<string, string>>((acc, { name, value }) => {
|
|
1163
|
+
acc[name] = value
|
|
1164
|
+
return acc
|
|
1165
|
+
}, {}),
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
await Promise.all(
|
|
1171
|
+
Object.entries(mcpServers).map(async ([key, mcp]) => {
|
|
1172
|
+
await this.sdk.mcp
|
|
1173
|
+
.add(
|
|
1174
|
+
{
|
|
1175
|
+
directory,
|
|
1176
|
+
name: key,
|
|
1177
|
+
config: mcp,
|
|
1178
|
+
},
|
|
1179
|
+
{ throwOnError: true },
|
|
1180
|
+
)
|
|
1181
|
+
.catch((error) => {
|
|
1182
|
+
log.error("failed to add mcp server", { name: key, error })
|
|
1183
|
+
})
|
|
1184
|
+
}),
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
setTimeout(() => {
|
|
1188
|
+
void this.connection.sessionUpdate({
|
|
1189
|
+
sessionId,
|
|
1190
|
+
update: {
|
|
1191
|
+
sessionUpdate: "available_commands_update",
|
|
1192
|
+
availableCommands,
|
|
1193
|
+
},
|
|
1194
|
+
})
|
|
1195
|
+
}, 0)
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
sessionId,
|
|
1199
|
+
models: {
|
|
1200
|
+
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false),
|
|
1201
|
+
availableModels,
|
|
1202
|
+
},
|
|
1203
|
+
modes,
|
|
1204
|
+
configOptions: buildConfigOptions({
|
|
1205
|
+
currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false),
|
|
1206
|
+
availableModels,
|
|
1207
|
+
currentVariant,
|
|
1208
|
+
availableVariants,
|
|
1209
|
+
modes,
|
|
1210
|
+
}),
|
|
1211
|
+
_meta: buildVariantMeta({
|
|
1212
|
+
model,
|
|
1213
|
+
variant: this.sessionManager.getVariant(sessionId),
|
|
1214
|
+
availableVariants,
|
|
1215
|
+
}),
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async unstable_setSessionModel(params: SetSessionModelRequest) {
|
|
1220
|
+
const session = this.sessionManager.get(params.sessionId)
|
|
1221
|
+
const providers = await this.sdk.config
|
|
1222
|
+
.providers({ directory: session.cwd }, { throwOnError: true })
|
|
1223
|
+
.then((x) => x.data!.providers)
|
|
1224
|
+
|
|
1225
|
+
const selection = parseModelSelection(params.modelId, providers)
|
|
1226
|
+
this.sessionManager.setModel(session.id, selection.model)
|
|
1227
|
+
this.sessionManager.setVariant(session.id, selection.variant)
|
|
1228
|
+
|
|
1229
|
+
const entries = sortProvidersByName(providers)
|
|
1230
|
+
const availableVariants = modelVariantsFromProviders(entries, selection.model)
|
|
1231
|
+
const modeState = await this.resolveModeState(session.cwd, session.id)
|
|
1232
|
+
const modes = modeState.currentModeId
|
|
1233
|
+
? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
|
|
1234
|
+
: undefined
|
|
1235
|
+
|
|
1236
|
+
await this.connection.sessionUpdate({
|
|
1237
|
+
sessionId: session.id,
|
|
1238
|
+
update: {
|
|
1239
|
+
sessionUpdate: "config_option_update",
|
|
1240
|
+
configOptions: buildConfigOptions({
|
|
1241
|
+
currentModelId: formatModelIdWithVariant(selection.model, selection.variant, availableVariants, false),
|
|
1242
|
+
availableModels: buildAvailableModels(entries),
|
|
1243
|
+
currentVariant: selection.variant,
|
|
1244
|
+
availableVariants,
|
|
1245
|
+
modes,
|
|
1246
|
+
}),
|
|
1247
|
+
},
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
_meta: buildVariantMeta({
|
|
1252
|
+
model: selection.model,
|
|
1253
|
+
variant: selection.variant,
|
|
1254
|
+
availableVariants,
|
|
1255
|
+
}),
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
|
|
1260
|
+
const session = this.sessionManager.get(params.sessionId)
|
|
1261
|
+
const availableModes = await this.loadAvailableModes(session.cwd)
|
|
1262
|
+
if (!availableModes.some((mode) => mode.id === params.modeId)) {
|
|
1263
|
+
throw new Error(`Agent not found: ${params.modeId}`)
|
|
1264
|
+
}
|
|
1265
|
+
this.sessionManager.setMode(params.sessionId, params.modeId)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
|
|
1269
|
+
const session = this.sessionManager.get(params.sessionId)
|
|
1270
|
+
const providers = await this.sdk.config
|
|
1271
|
+
.providers({ directory: session.cwd }, { throwOnError: true })
|
|
1272
|
+
.then((x) => x.data!.providers)
|
|
1273
|
+
const entries = sortProvidersByName(providers)
|
|
1274
|
+
|
|
1275
|
+
if (params.configId === "model") {
|
|
1276
|
+
if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string")
|
|
1277
|
+
const selection = parseModelSelection(params.value, providers)
|
|
1278
|
+
this.sessionManager.setModel(session.id, selection.model)
|
|
1279
|
+
this.sessionManager.setVariant(session.id, selection.variant)
|
|
1280
|
+
} else if (params.configId === "effort") {
|
|
1281
|
+
if (typeof params.value !== "string") throw RequestError.invalidParams("effort value must be a string")
|
|
1282
|
+
const current = session.model ?? (await defaultModel(this.config, session.cwd))
|
|
1283
|
+
const availableVariants = modelVariantsFromProviders(entries, current)
|
|
1284
|
+
if (!availableVariants.includes(params.value)) {
|
|
1285
|
+
throw RequestError.invalidParams(JSON.stringify({ error: `Effort not found: ${params.value}` }))
|
|
1286
|
+
}
|
|
1287
|
+
this.sessionManager.setVariant(session.id, params.value)
|
|
1288
|
+
} else if (params.configId === "mode") {
|
|
1289
|
+
if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string")
|
|
1290
|
+
const availableModes = await this.loadAvailableModes(session.cwd)
|
|
1291
|
+
if (!availableModes.some((mode) => mode.id === params.value)) {
|
|
1292
|
+
throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` }))
|
|
1293
|
+
}
|
|
1294
|
+
this.sessionManager.setMode(session.id, params.value)
|
|
1295
|
+
} else {
|
|
1296
|
+
throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` }))
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const updatedSession = this.sessionManager.get(session.id)
|
|
1300
|
+
const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd))
|
|
1301
|
+
const availableVariants = modelVariantsFromProviders(entries, model)
|
|
1302
|
+
const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, false)
|
|
1303
|
+
const availableModels = buildAvailableModels(entries)
|
|
1304
|
+
const modeState = await this.resolveModeState(session.cwd, session.id)
|
|
1305
|
+
const modes = modeState.currentModeId
|
|
1306
|
+
? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
|
|
1307
|
+
: undefined
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
configOptions: buildConfigOptions({
|
|
1311
|
+
currentModelId,
|
|
1312
|
+
availableModels,
|
|
1313
|
+
currentVariant: updatedSession.variant,
|
|
1314
|
+
availableVariants,
|
|
1315
|
+
modes,
|
|
1316
|
+
}),
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async prompt(params: PromptRequest) {
|
|
1321
|
+
const sessionID = params.sessionId
|
|
1322
|
+
const session = this.sessionManager.get(sessionID)
|
|
1323
|
+
const directory = session.cwd
|
|
1324
|
+
|
|
1325
|
+
const current = session.model
|
|
1326
|
+
const model = current ?? (await defaultModel(this.config, directory))
|
|
1327
|
+
if (!current) {
|
|
1328
|
+
this.sessionManager.setModel(session.id, model)
|
|
1329
|
+
}
|
|
1330
|
+
const agent = session.modeId ?? (await ACPRuntime.defaultAgentInfo(directory)).name
|
|
1331
|
+
|
|
1332
|
+
const parts: Array<
|
|
1333
|
+
| { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
|
|
1334
|
+
| { type: "file"; url: string; filename: string; mime: string }
|
|
1335
|
+
> = []
|
|
1336
|
+
for (const part of params.prompt) {
|
|
1337
|
+
switch (part.type) {
|
|
1338
|
+
case "text":
|
|
1339
|
+
const audience = part.annotations?.audience
|
|
1340
|
+
const forAssistant = audience?.length === 1 && audience[0] === "assistant"
|
|
1341
|
+
const forUser = audience?.length === 1 && audience[0] === "user"
|
|
1342
|
+
parts.push({
|
|
1343
|
+
type: "text" as const,
|
|
1344
|
+
text: part.text,
|
|
1345
|
+
...(forAssistant && { synthetic: true }),
|
|
1346
|
+
...(forUser && { ignored: true }),
|
|
1347
|
+
})
|
|
1348
|
+
break
|
|
1349
|
+
case "image": {
|
|
1350
|
+
const parsed = parseUri(part.uri ?? "")
|
|
1351
|
+
const filename = parsed.type === "file" ? parsed.filename : "image"
|
|
1352
|
+
if (part.data) {
|
|
1353
|
+
parts.push({
|
|
1354
|
+
type: "file",
|
|
1355
|
+
url: `data:${part.mimeType};base64,${part.data}`,
|
|
1356
|
+
filename,
|
|
1357
|
+
mime: part.mimeType,
|
|
1358
|
+
})
|
|
1359
|
+
} else if (part.uri && part.uri.startsWith("http:")) {
|
|
1360
|
+
parts.push({
|
|
1361
|
+
type: "file",
|
|
1362
|
+
url: part.uri,
|
|
1363
|
+
filename,
|
|
1364
|
+
mime: part.mimeType,
|
|
1365
|
+
})
|
|
1366
|
+
}
|
|
1367
|
+
break
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
case "resource_link":
|
|
1371
|
+
const parsed = parseUri(part.uri)
|
|
1372
|
+
// Use the name from resource_link if available
|
|
1373
|
+
if (part.name && parsed.type === "file") {
|
|
1374
|
+
parsed.filename = part.name
|
|
1375
|
+
}
|
|
1376
|
+
parts.push(parsed)
|
|
1377
|
+
|
|
1378
|
+
break
|
|
1379
|
+
|
|
1380
|
+
case "resource": {
|
|
1381
|
+
const resource = part.resource
|
|
1382
|
+
if ("text" in resource && resource.text) {
|
|
1383
|
+
parts.push({
|
|
1384
|
+
type: "text",
|
|
1385
|
+
text: resource.text,
|
|
1386
|
+
})
|
|
1387
|
+
} else if ("blob" in resource && resource.blob && resource.mimeType) {
|
|
1388
|
+
// Binary resource (PDFs, etc.): store as file part with data URL
|
|
1389
|
+
const parsed = parseUri(resource.uri ?? "")
|
|
1390
|
+
const filename = parsed.type === "file" ? parsed.filename : "file"
|
|
1391
|
+
parts.push({
|
|
1392
|
+
type: "file",
|
|
1393
|
+
url: `data:${resource.mimeType};base64,${resource.blob}`,
|
|
1394
|
+
filename,
|
|
1395
|
+
mime: resource.mimeType,
|
|
1396
|
+
})
|
|
1397
|
+
}
|
|
1398
|
+
break
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
default:
|
|
1402
|
+
break
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
log.info("parts", { parts })
|
|
1407
|
+
|
|
1408
|
+
const cmd = (() => {
|
|
1409
|
+
const text = parts
|
|
1410
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
1411
|
+
.map((p) => p.text)
|
|
1412
|
+
.join("")
|
|
1413
|
+
.trim()
|
|
1414
|
+
|
|
1415
|
+
if (!text.startsWith("/")) return
|
|
1416
|
+
|
|
1417
|
+
const [name, ...rest] = text.slice(1).split(/\s+/)
|
|
1418
|
+
return { name, args: rest.join(" ").trim() }
|
|
1419
|
+
})()
|
|
1420
|
+
|
|
1421
|
+
const buildUsage = (msg: AssistantMessage): Usage => ({
|
|
1422
|
+
totalTokens:
|
|
1423
|
+
msg.tokens.input +
|
|
1424
|
+
msg.tokens.output +
|
|
1425
|
+
msg.tokens.reasoning +
|
|
1426
|
+
(msg.tokens.cache?.read ?? 0) +
|
|
1427
|
+
(msg.tokens.cache?.write ?? 0),
|
|
1428
|
+
inputTokens: msg.tokens.input,
|
|
1429
|
+
outputTokens: msg.tokens.output,
|
|
1430
|
+
thoughtTokens: msg.tokens.reasoning || undefined,
|
|
1431
|
+
cachedReadTokens: msg.tokens.cache?.read || undefined,
|
|
1432
|
+
cachedWriteTokens: msg.tokens.cache?.write || undefined,
|
|
1433
|
+
})
|
|
1434
|
+
|
|
1435
|
+
if (!cmd) {
|
|
1436
|
+
const response = await this.sdk.session.prompt({
|
|
1437
|
+
sessionID,
|
|
1438
|
+
model: {
|
|
1439
|
+
providerID: model.providerID,
|
|
1440
|
+
modelID: model.modelID,
|
|
1441
|
+
},
|
|
1442
|
+
variant: this.sessionManager.getVariant(sessionID),
|
|
1443
|
+
parts,
|
|
1444
|
+
agent,
|
|
1445
|
+
directory,
|
|
1446
|
+
})
|
|
1447
|
+
const msg = response.data?.info
|
|
1448
|
+
|
|
1449
|
+
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
|
|
1450
|
+
|
|
1451
|
+
return {
|
|
1452
|
+
stopReason: "end_turn" as const,
|
|
1453
|
+
usage: msg ? buildUsage(msg) : undefined,
|
|
1454
|
+
_meta: {},
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const command = await this.config.sdk.command
|
|
1459
|
+
.list({ directory }, { throwOnError: true })
|
|
1460
|
+
.then((x) => x.data!.find((c) => c.name === cmd.name))
|
|
1461
|
+
if (command) {
|
|
1462
|
+
const response = await this.sdk.session.command({
|
|
1463
|
+
sessionID,
|
|
1464
|
+
command: command.name,
|
|
1465
|
+
arguments: cmd.args,
|
|
1466
|
+
model: model.providerID + "/" + model.modelID,
|
|
1467
|
+
agent,
|
|
1468
|
+
directory,
|
|
1469
|
+
})
|
|
1470
|
+
const msg = response.data?.info
|
|
1471
|
+
|
|
1472
|
+
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
|
|
1473
|
+
|
|
1474
|
+
return {
|
|
1475
|
+
stopReason: "end_turn" as const,
|
|
1476
|
+
usage: msg ? buildUsage(msg) : undefined,
|
|
1477
|
+
_meta: {},
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
switch (cmd.name) {
|
|
1482
|
+
case "compact":
|
|
1483
|
+
await this.config.sdk.session.summarize(
|
|
1484
|
+
{
|
|
1485
|
+
sessionID,
|
|
1486
|
+
directory,
|
|
1487
|
+
providerID: model.providerID,
|
|
1488
|
+
modelID: model.modelID,
|
|
1489
|
+
},
|
|
1490
|
+
{ throwOnError: true },
|
|
1491
|
+
)
|
|
1492
|
+
break
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
|
|
1496
|
+
|
|
1497
|
+
return {
|
|
1498
|
+
stopReason: "end_turn" as const,
|
|
1499
|
+
_meta: {},
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
async cancel(params: CancelNotification) {
|
|
1504
|
+
const session = this.sessionManager.get(params.sessionId)
|
|
1505
|
+
await this.config.sdk.session.abort(
|
|
1506
|
+
{
|
|
1507
|
+
sessionID: params.sessionId,
|
|
1508
|
+
directory: session.cwd,
|
|
1509
|
+
},
|
|
1510
|
+
{ throwOnError: true },
|
|
1511
|
+
)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
private async loadSessionMessages(directory: string, sessionId: string, limit?: number) {
|
|
1515
|
+
return this.sdk.session
|
|
1516
|
+
.messages(
|
|
1517
|
+
{
|
|
1518
|
+
sessionID: sessionId,
|
|
1519
|
+
directory,
|
|
1520
|
+
limit,
|
|
1521
|
+
},
|
|
1522
|
+
{ throwOnError: true },
|
|
1523
|
+
)
|
|
1524
|
+
.then((x) => x.data)
|
|
1525
|
+
.catch((error) => {
|
|
1526
|
+
log.error("unexpected error when fetching message", { error })
|
|
1527
|
+
return undefined
|
|
1528
|
+
})
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
private restoreSessionStateFromMessages(sessionId: string, messages: SessionMessageResponse[] | undefined) {
|
|
1532
|
+
const lastUser = messages?.findLast((message) => message.info.role === "user")?.info
|
|
1533
|
+
if (lastUser?.role !== "user") return
|
|
1534
|
+
|
|
1535
|
+
this.sessionManager.setModel(sessionId, {
|
|
1536
|
+
providerID: ProviderID.make(lastUser.model.providerID),
|
|
1537
|
+
modelID: ModelID.make(lastUser.model.modelID),
|
|
1538
|
+
})
|
|
1539
|
+
this.sessionManager.setVariant(sessionId, lastUser.model.variant)
|
|
1540
|
+
if (lastUser.agent) {
|
|
1541
|
+
this.sessionManager.setMode(sessionId, lastUser.agent)
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function toToolKind(toolName: string): ToolKind {
|
|
1547
|
+
const tool = toolName.toLocaleLowerCase()
|
|
1548
|
+
|
|
1549
|
+
switch (tool) {
|
|
1550
|
+
case ShellID.ToolID:
|
|
1551
|
+
return "execute"
|
|
1552
|
+
|
|
1553
|
+
case "webfetch":
|
|
1554
|
+
return "fetch"
|
|
1555
|
+
|
|
1556
|
+
case "edit":
|
|
1557
|
+
case "patch":
|
|
1558
|
+
case "write":
|
|
1559
|
+
return "edit"
|
|
1560
|
+
|
|
1561
|
+
case "grep":
|
|
1562
|
+
case "glob":
|
|
1563
|
+
case "repo_clone":
|
|
1564
|
+
case "repo_overview":
|
|
1565
|
+
case "context7_resolve_library_id":
|
|
1566
|
+
case "context7_get_library_docs":
|
|
1567
|
+
return "search"
|
|
1568
|
+
|
|
1569
|
+
case "read":
|
|
1570
|
+
return "read"
|
|
1571
|
+
|
|
1572
|
+
default:
|
|
1573
|
+
return "other"
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
|
|
1578
|
+
const tool = toolName.toLocaleLowerCase()
|
|
1579
|
+
|
|
1580
|
+
switch (tool) {
|
|
1581
|
+
case "read":
|
|
1582
|
+
case "edit":
|
|
1583
|
+
case "write":
|
|
1584
|
+
return input["filePath"] ? [{ path: input["filePath"] }] : []
|
|
1585
|
+
case "glob":
|
|
1586
|
+
case "grep":
|
|
1587
|
+
return input["path"] ? [{ path: input["path"] }] : []
|
|
1588
|
+
case "repo_clone":
|
|
1589
|
+
return input["path"] ? [{ path: input["path"] }] : []
|
|
1590
|
+
case "repo_overview":
|
|
1591
|
+
return input["path"] ? [{ path: input["path"] }] : []
|
|
1592
|
+
case ShellID.ToolID:
|
|
1593
|
+
return []
|
|
1594
|
+
default:
|
|
1595
|
+
return []
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
function completedToolContent(part: ToolPart, kind: ToolKind): ToolCallContent[] {
|
|
1600
|
+
if (part.state.status !== "completed") return []
|
|
1601
|
+
|
|
1602
|
+
const content: ToolCallContent[] = [
|
|
1603
|
+
{
|
|
1604
|
+
type: "content",
|
|
1605
|
+
content: {
|
|
1606
|
+
type: "text",
|
|
1607
|
+
text: part.state.output,
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1610
|
+
]
|
|
1611
|
+
|
|
1612
|
+
if (kind === "edit") {
|
|
1613
|
+
const input = part.state.input
|
|
1614
|
+
const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
|
|
1615
|
+
const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
|
|
1616
|
+
const newText =
|
|
1617
|
+
typeof input["newString"] === "string"
|
|
1618
|
+
? input["newString"]
|
|
1619
|
+
: typeof input["content"] === "string"
|
|
1620
|
+
? input["content"]
|
|
1621
|
+
: ""
|
|
1622
|
+
content.push({
|
|
1623
|
+
type: "diff",
|
|
1624
|
+
path: filePath,
|
|
1625
|
+
oldText,
|
|
1626
|
+
newText,
|
|
1627
|
+
})
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
content.push(...imageContents(part.state.attachments ?? []))
|
|
1631
|
+
return content
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function completedToolRawOutput(part: ToolPart) {
|
|
1635
|
+
if (part.state.status !== "completed") return {}
|
|
1636
|
+
return {
|
|
1637
|
+
output: part.state.output,
|
|
1638
|
+
metadata: part.state.metadata,
|
|
1639
|
+
...(part.state.attachments?.length ? { attachments: part.state.attachments } : {}),
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function imageContents(attachments: Array<{ mime: string; url: string }>): ToolCallContent[] {
|
|
1644
|
+
return attachments.flatMap((attachment): ToolCallContent[] => {
|
|
1645
|
+
const match = attachment.url.match(/^data:([^;,]+)(?:;[^,]*)*;base64,(.*)$/)
|
|
1646
|
+
const mime = match?.[1] ?? attachment.mime
|
|
1647
|
+
if (!mime.startsWith("image/")) return []
|
|
1648
|
+
const data = match?.[2]
|
|
1649
|
+
if (data === undefined) return []
|
|
1650
|
+
return [
|
|
1651
|
+
{
|
|
1652
|
+
type: "content" as const,
|
|
1653
|
+
content: {
|
|
1654
|
+
type: "image" as const,
|
|
1655
|
+
mimeType: mime,
|
|
1656
|
+
data,
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
]
|
|
1660
|
+
})
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> {
|
|
1664
|
+
const sdk = config.sdk
|
|
1665
|
+
const configured = config.defaultModel
|
|
1666
|
+
if (configured) return configured
|
|
1667
|
+
|
|
1668
|
+
const directory = cwd ?? process.cwd()
|
|
1669
|
+
|
|
1670
|
+
const specified = await sdk.config
|
|
1671
|
+
.get({ directory }, { throwOnError: true })
|
|
1672
|
+
.then((resp) => {
|
|
1673
|
+
const cfg = resp.data
|
|
1674
|
+
if (!cfg || !cfg.model) return undefined
|
|
1675
|
+
return Provider.parseModel(cfg.model)
|
|
1676
|
+
})
|
|
1677
|
+
.catch((error) => {
|
|
1678
|
+
log.error("failed to load user config for default model", { error })
|
|
1679
|
+
return undefined
|
|
1680
|
+
})
|
|
1681
|
+
|
|
1682
|
+
const providers = await sdk.config
|
|
1683
|
+
.providers({ directory }, { throwOnError: true })
|
|
1684
|
+
.then((x) => x.data?.providers ?? [])
|
|
1685
|
+
.catch((error) => {
|
|
1686
|
+
log.error("failed to list providers for default model", { error })
|
|
1687
|
+
return []
|
|
1688
|
+
})
|
|
1689
|
+
|
|
1690
|
+
if (specified && providers.length) {
|
|
1691
|
+
const provider = providers.find((p) => p.id === specified.providerID)
|
|
1692
|
+
if (provider && provider.models[specified.modelID]) return specified
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
if (specified && !providers.length) return specified
|
|
1696
|
+
|
|
1697
|
+
const lastUsed = await lastUsedModel(sdk, directory, providers)
|
|
1698
|
+
if (lastUsed) return lastUsed
|
|
1699
|
+
|
|
1700
|
+
const opencodeProvider = providers.find((p) => p.id === "opencode")
|
|
1701
|
+
if (opencodeProvider) {
|
|
1702
|
+
const [best] = Provider.sort(Object.values(opencodeProvider.models))
|
|
1703
|
+
if (best) {
|
|
1704
|
+
return {
|
|
1705
|
+
providerID: ProviderID.make(best.providerID),
|
|
1706
|
+
modelID: ModelID.make(best.id),
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const models = providers.flatMap((p) => Object.values(p.models))
|
|
1712
|
+
const [best] = Provider.sort(models)
|
|
1713
|
+
if (best) {
|
|
1714
|
+
return {
|
|
1715
|
+
providerID: ProviderID.make(best.providerID),
|
|
1716
|
+
modelID: ModelID.make(best.id),
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (specified) return specified
|
|
1721
|
+
throw new Error("No models available")
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
async function lastUsedModel(
|
|
1725
|
+
sdk: OpencodeClient,
|
|
1726
|
+
directory: string,
|
|
1727
|
+
providers: Array<{ id: string; models: Record<string, unknown> }>,
|
|
1728
|
+
): Promise<{ providerID: ProviderID; modelID: ModelID } | undefined> {
|
|
1729
|
+
const session = await sdk.session
|
|
1730
|
+
.list({ directory, roots: true, limit: 1 }, { throwOnError: true })
|
|
1731
|
+
.then((x) => x.data?.[0])
|
|
1732
|
+
.catch((error) => {
|
|
1733
|
+
log.error("failed to list sessions for default model", { error })
|
|
1734
|
+
return undefined
|
|
1735
|
+
})
|
|
1736
|
+
if (!session) return
|
|
1737
|
+
|
|
1738
|
+
const lastUser = await sdk.session
|
|
1739
|
+
.messages({ sessionID: session.id, directory, limit: 20 }, { throwOnError: true })
|
|
1740
|
+
.then((x) => x.data?.findLast((message) => message.info.role === "user")?.info)
|
|
1741
|
+
.catch((error) => {
|
|
1742
|
+
log.error("failed to load session messages for default model", { error, sessionID: session.id })
|
|
1743
|
+
return undefined
|
|
1744
|
+
})
|
|
1745
|
+
if (lastUser?.role !== "user") return
|
|
1746
|
+
|
|
1747
|
+
const provider = providers.find((entry) => entry.id === lastUser.model.providerID)
|
|
1748
|
+
if (!provider?.models[lastUser.model.modelID]) return
|
|
1749
|
+
return {
|
|
1750
|
+
providerID: ProviderID.make(lastUser.model.providerID),
|
|
1751
|
+
modelID: ModelID.make(lastUser.model.modelID),
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function parseUri(
|
|
1756
|
+
uri: string,
|
|
1757
|
+
): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
|
|
1758
|
+
try {
|
|
1759
|
+
if (uri.startsWith("file://")) {
|
|
1760
|
+
const path = uri.slice(7)
|
|
1761
|
+
const name = path.split("/").pop() || path
|
|
1762
|
+
return {
|
|
1763
|
+
type: "file",
|
|
1764
|
+
url: uri,
|
|
1765
|
+
filename: name,
|
|
1766
|
+
mime: "text/plain",
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (uri.startsWith("zed://")) {
|
|
1770
|
+
const url = new URL(uri)
|
|
1771
|
+
const path = url.searchParams.get("path")
|
|
1772
|
+
if (path) {
|
|
1773
|
+
const name = path.split("/").pop() || path
|
|
1774
|
+
return {
|
|
1775
|
+
type: "file",
|
|
1776
|
+
url: pathToFileURL(path).href,
|
|
1777
|
+
filename: name,
|
|
1778
|
+
mime: "text/plain",
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return {
|
|
1783
|
+
type: "text",
|
|
1784
|
+
text: uri,
|
|
1785
|
+
}
|
|
1786
|
+
} catch {
|
|
1787
|
+
return {
|
|
1788
|
+
type: "text",
|
|
1789
|
+
text: uri,
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
|
|
1795
|
+
const result = applyPatch(fileOriginal, unifiedDiff)
|
|
1796
|
+
if (result === false) {
|
|
1797
|
+
log.error("Failed to apply unified diff (context mismatch)")
|
|
1798
|
+
return undefined
|
|
1799
|
+
}
|
|
1800
|
+
return result
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
|
|
1804
|
+
return [...providers].sort((a, b) => {
|
|
1805
|
+
const nameA = a.name.toLowerCase()
|
|
1806
|
+
const nameB = b.name.toLowerCase()
|
|
1807
|
+
if (nameA < nameB) return -1
|
|
1808
|
+
if (nameA > nameB) return 1
|
|
1809
|
+
return 0
|
|
1810
|
+
})
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function modelVariantsFromProviders(
|
|
1814
|
+
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
|
|
1815
|
+
model: { providerID: ProviderID; modelID: ModelID },
|
|
1816
|
+
): string[] {
|
|
1817
|
+
const provider = providers.find((entry) => entry.id === model.providerID)
|
|
1818
|
+
if (!provider) return []
|
|
1819
|
+
const modelInfo = provider.models[model.modelID]
|
|
1820
|
+
if (!modelInfo?.variants) return []
|
|
1821
|
+
return Object.keys(modelInfo.variants)
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function buildAvailableModels(
|
|
1825
|
+
providers: Array<{ id: string; name: string; models: Record<string, any> }>,
|
|
1826
|
+
options: { includeVariants?: boolean } = {},
|
|
1827
|
+
): ModelOption[] {
|
|
1828
|
+
const includeVariants = options.includeVariants ?? false
|
|
1829
|
+
return providers.flatMap((provider) => {
|
|
1830
|
+
const unsorted: Array<{ id: string; name: string; variants?: Record<string, any> }> = Object.values(provider.models)
|
|
1831
|
+
const models = Provider.sort(unsorted)
|
|
1832
|
+
return models.flatMap((model) => {
|
|
1833
|
+
const base: ModelOption = {
|
|
1834
|
+
modelId: `${provider.id}/${model.id}`,
|
|
1835
|
+
name: `${provider.name}/${model.name}`,
|
|
1836
|
+
}
|
|
1837
|
+
if (!includeVariants || !model.variants) return [base]
|
|
1838
|
+
const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
|
|
1839
|
+
const variantOptions = variants.map((variant) => ({
|
|
1840
|
+
modelId: `${provider.id}/${model.id}/${variant}`,
|
|
1841
|
+
name: `${provider.name}/${model.name} (${variant})`,
|
|
1842
|
+
}))
|
|
1843
|
+
return [base, ...variantOptions]
|
|
1844
|
+
})
|
|
1845
|
+
})
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function formatModelIdWithVariant(
|
|
1849
|
+
model: { providerID: ProviderID; modelID: ModelID },
|
|
1850
|
+
variant: string | undefined,
|
|
1851
|
+
availableVariants: string[],
|
|
1852
|
+
includeVariant: boolean,
|
|
1853
|
+
) {
|
|
1854
|
+
const base = `${model.providerID}/${model.modelID}`
|
|
1855
|
+
if (!includeVariant || availableVariants.length === 0) return base
|
|
1856
|
+
const selectedVariant =
|
|
1857
|
+
variant && availableVariants.includes(variant)
|
|
1858
|
+
? variant
|
|
1859
|
+
: availableVariants.includes(DEFAULT_VARIANT_VALUE)
|
|
1860
|
+
? DEFAULT_VARIANT_VALUE
|
|
1861
|
+
: availableVariants[0]
|
|
1862
|
+
return `${base}/${selectedVariant}`
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function buildVariantMeta(input: {
|
|
1866
|
+
model: { providerID: ProviderID; modelID: ModelID }
|
|
1867
|
+
variant?: string
|
|
1868
|
+
availableVariants: string[]
|
|
1869
|
+
}) {
|
|
1870
|
+
return {
|
|
1871
|
+
opencode: {
|
|
1872
|
+
modelId: `${input.model.providerID}/${input.model.modelID}`,
|
|
1873
|
+
variant: input.variant ?? null,
|
|
1874
|
+
availableVariants: input.availableVariants,
|
|
1875
|
+
},
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function parseModelSelection(
|
|
1880
|
+
modelId: string,
|
|
1881
|
+
providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
|
|
1882
|
+
): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } {
|
|
1883
|
+
const parsed = Provider.parseModel(modelId)
|
|
1884
|
+
const provider = providers.find((p) => p.id === parsed.providerID)
|
|
1885
|
+
if (!provider) {
|
|
1886
|
+
return { model: parsed, variant: undefined }
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// Check if modelID exists directly
|
|
1890
|
+
if (provider.models[parsed.modelID]) {
|
|
1891
|
+
return { model: parsed, variant: undefined }
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
|
|
1895
|
+
const segments = parsed.modelID.split("/")
|
|
1896
|
+
if (segments.length > 1) {
|
|
1897
|
+
const candidateVariant = segments[segments.length - 1]
|
|
1898
|
+
const baseModelId = segments.slice(0, -1).join("/")
|
|
1899
|
+
const baseModelInfo = provider.models[baseModelId]
|
|
1900
|
+
if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
|
|
1901
|
+
return {
|
|
1902
|
+
model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) },
|
|
1903
|
+
variant: candidateVariant,
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
return { model: parsed, variant: undefined }
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function buildConfigOptions(input: {
|
|
1912
|
+
currentModelId: string
|
|
1913
|
+
availableModels: ModelOption[]
|
|
1914
|
+
currentVariant?: string
|
|
1915
|
+
availableVariants?: string[]
|
|
1916
|
+
modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
|
|
1917
|
+
}): SessionConfigOption[] {
|
|
1918
|
+
const options: SessionConfigOption[] = [
|
|
1919
|
+
{
|
|
1920
|
+
id: "model",
|
|
1921
|
+
name: "Model",
|
|
1922
|
+
category: "model",
|
|
1923
|
+
type: "select",
|
|
1924
|
+
currentValue: input.currentModelId,
|
|
1925
|
+
options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })),
|
|
1926
|
+
},
|
|
1927
|
+
]
|
|
1928
|
+
if (input.availableVariants?.length) {
|
|
1929
|
+
options.push({
|
|
1930
|
+
id: "effort",
|
|
1931
|
+
name: "Effort",
|
|
1932
|
+
description: "Available effort levels for this model",
|
|
1933
|
+
category: "thought_level",
|
|
1934
|
+
type: "select",
|
|
1935
|
+
currentValue:
|
|
1936
|
+
input.currentVariant && input.availableVariants.includes(input.currentVariant)
|
|
1937
|
+
? input.currentVariant
|
|
1938
|
+
: input.availableVariants.includes(DEFAULT_VARIANT_VALUE)
|
|
1939
|
+
? DEFAULT_VARIANT_VALUE
|
|
1940
|
+
: input.availableVariants[0],
|
|
1941
|
+
options: input.availableVariants.map((variant) => ({ value: variant, name: formatVariantName(variant) })),
|
|
1942
|
+
})
|
|
1943
|
+
}
|
|
1944
|
+
if (input.modes) {
|
|
1945
|
+
options.push({
|
|
1946
|
+
id: "mode",
|
|
1947
|
+
name: "Session Mode",
|
|
1948
|
+
category: "mode",
|
|
1949
|
+
type: "select",
|
|
1950
|
+
currentValue: input.modes.currentModeId,
|
|
1951
|
+
options: input.modes.availableModes.map((m) => ({
|
|
1952
|
+
value: m.id,
|
|
1953
|
+
name: m.name,
|
|
1954
|
+
...(m.description ? { description: m.description } : {}),
|
|
1955
|
+
})),
|
|
1956
|
+
})
|
|
1957
|
+
}
|
|
1958
|
+
return options
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function formatVariantName(variant: string) {
|
|
1962
|
+
return variant
|
|
1963
|
+
.split(/[_-]/)
|
|
1964
|
+
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
1965
|
+
.join(" ")
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
export * as ACP from "./agent"
|