From 8db7487c894b46d8093135a18f5cf23c8447a4e9 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sun, 26 Jul 2026 20:08:55 -0400 Subject: [PATCH] refactor(core): consolidate tool architecture --- bun.lock | 34 +- package.json | 14 +- .../ai/src/protocols/anthropic-messages.ts | 6 +- packages/ai/src/protocols/gemini.ts | 4 +- packages/ai/src/protocols/open-responses.ts | 8 +- packages/ai/src/protocols/openai-chat.ts | 4 +- packages/ai/src/protocols/shared.ts | 4 +- packages/ai/src/schema/errors.ts | 7 +- packages/ai/src/schema/messages.ts | 12 +- packages/ai/src/tool-runtime.ts | 2 +- packages/ai/src/tool.ts | 12 +- packages/ai/test/tool-runtime.test.ts | 4 +- packages/app/src/context/server-sdk.test.ts | 2 +- packages/app/src/context/server-sdk.tsx | 10 +- packages/cli/src/acp/event.ts | 2 +- packages/cli/src/acp/permission.ts | 2 +- .../cli/src/node/plugin-runtime.effect.ts | 6 +- .../cli/src/node/plugin-runtime.promise.ts | 4 +- packages/cli/src/run/noninteractive.ts | 2 +- packages/cli/src/services/standalone.ts | 4 +- packages/cli/src/util/process.ts | 8 +- .../cli/test/acp/permission-behavior.test.ts | 2 +- packages/cli/test/fixture/standalone-owner.ts | 6 +- packages/cli/vite.node.config.ts | 31 +- packages/client/package.json | 2 +- packages/client/script/build.ts | 107 +- packages/client/src/effect/api/api.ts | 1506 +++++++++----- .../client/src/effect/generated/client.ts | 1822 ++++++++--------- .../client/src/promise/generated/client.ts | 14 + .../client/src/promise/generated/types.ts | 320 ++- packages/client/test/api.types.ts | 7 +- .../client/test/contract-identity.test.ts | 8 + packages/core/package.json | 1 + packages/core/src/account.ts | 2 +- packages/core/src/account/sql.ts | 16 +- packages/core/src/agent.ts | 24 +- packages/core/src/aisdk.ts | 40 +- packages/core/src/{event.ts => bus.ts} | 116 +- packages/core/src/catalog.ts | 102 +- packages/core/src/codemode.ts | 77 - .../src/{tool/execute.ts => codemode/tool.ts} | 59 +- packages/core/src/command.ts | 20 +- packages/core/src/config.ts | 14 +- packages/core/src/config/agent.ts | 2 +- packages/core/src/config/attachments.ts | 4 +- packages/core/src/config/command.ts | 2 +- packages/core/src/config/compaction.ts | 4 +- packages/core/src/config/formatter.ts | 2 +- packages/core/src/config/lsp.ts | 2 +- packages/core/src/config/mcp.ts | 2 +- packages/core/src/config/plugin.ts | 2 +- packages/core/src/config/plugin/agent.ts | 16 +- packages/core/src/config/plugin/command.ts | 4 +- packages/core/src/config/plugin/policy.ts | 2 +- packages/core/src/config/plugin/provider.ts | 22 +- packages/core/src/config/plugin/reference.ts | 2 +- packages/core/src/config/plugin/skill.ts | 14 +- packages/core/src/config/plugin/websearch.ts | 2 +- packages/core/src/config/provider.ts | 24 +- packages/core/src/config/reference.ts | 4 +- packages/core/src/config/tool-output.ts | 2 +- packages/core/src/config/warming.ts | 2 +- packages/core/src/config/watcher.ts | 2 +- .../core/src/control-plane/move-session.ts | 24 +- .../core/src/control-plane/workspace.sql.ts | 8 +- packages/core/src/credential.ts | 2 +- packages/core/src/database/database.ts | 2 +- packages/core/src/event-logger.ts | 8 +- packages/core/src/event/sql.ts | 4 +- packages/core/src/file-mutation.ts | 12 +- packages/core/src/filesystem.ts | 2 +- .../core/src/filesystem/location-watcher.ts | 8 +- packages/core/src/filesystem/search.ts | 2 +- packages/core/src/form.ts | 14 +- packages/core/src/generate.ts | 6 +- packages/core/src/git.ts | 2 +- packages/core/src/github-copilot/models.ts | 34 +- packages/core/src/instruction-discovery.ts | 2 +- packages/core/src/instructions/builtins.ts | 2 +- packages/core/src/integration.ts | 32 +- packages/core/src/kv.ts | 2 +- packages/core/src/location-mutation.ts | 2 +- packages/core/src/location-services.ts | 35 +- packages/core/src/mcp/index.ts | 48 +- packages/core/src/mcp/instructions.ts | 12 +- packages/core/src/model-resolver.ts | 96 +- packages/core/src/model.ts | 8 +- packages/core/src/models-dev.ts | 116 +- packages/core/src/permission.ts | 64 +- packages/core/src/permission/saved.ts | 8 +- packages/core/src/permission/sql.ts | 4 +- packages/core/src/plugin.ts | 50 +- packages/core/src/plugin/agent.ts | 54 +- packages/core/src/plugin/command.ts | 2 +- packages/core/src/plugin/hooks.ts | 8 +- packages/core/src/plugin/host.ts | 165 +- packages/core/src/plugin/internal.ts | 62 +- packages/core/src/plugin/models-dev.ts | 11 +- packages/core/src/plugin/promise.ts | 78 +- packages/core/src/plugin/provider/alibaba.ts | 2 +- .../src/plugin/provider/amazon-bedrock.ts | 14 +- .../core/src/plugin/provider/anthropic.ts | 8 +- packages/core/src/plugin/provider/azure.ts | 20 +- packages/core/src/plugin/provider/cerebras.ts | 8 +- .../plugin/provider/cloudflare-ai-gateway.ts | 2 +- .../plugin/provider/cloudflare-workers-ai.ts | 10 +- packages/core/src/plugin/provider/cohere.ts | 2 +- .../core/src/plugin/provider/deepinfra.ts | 2 +- packages/core/src/plugin/provider/dynamic.ts | 2 +- packages/core/src/plugin/provider/gateway.ts | 2 +- .../src/plugin/provider/github-copilot.ts | 34 +- packages/core/src/plugin/provider/gitlab.ts | 6 +- .../core/src/plugin/provider/google-vertex.ts | 24 +- packages/core/src/plugin/provider/google.ts | 2 +- packages/core/src/plugin/provider/groq.ts | 2 +- packages/core/src/plugin/provider/kilo.ts | 8 +- .../core/src/plugin/provider/llmgateway.ts | 8 +- packages/core/src/plugin/provider/mistral.ts | 2 +- packages/core/src/plugin/provider/nvidia.ts | 8 +- .../src/plugin/provider/openai-compatible.ts | 2 +- packages/core/src/plugin/provider/openai.ts | 26 +- packages/core/src/plugin/provider/opencode.ts | 30 +- .../core/src/plugin/provider/openrouter.ts | 12 +- .../core/src/plugin/provider/perplexity.ts | 2 +- .../core/src/plugin/provider/sap-ai-core.ts | 8 +- .../src/plugin/provider/snowflake-cortex.ts | 6 +- .../core/src/plugin/provider/togetherai.ts | 2 +- packages/core/src/plugin/provider/venice.ts | 2 +- packages/core/src/plugin/provider/vercel.ts | 8 +- packages/core/src/plugin/provider/xai.ts | 8 +- packages/core/src/plugin/provider/zenmux.ts | 8 +- packages/core/src/plugin/runtime.ts | 14 +- packages/core/src/plugin/sdk.ts | 18 +- packages/core/src/plugin/skill.ts | 20 +- packages/core/src/plugin/skill/opencode.md | 38 +- packages/core/src/plugin/supervisor.ts | 47 +- packages/core/src/plugin/system-prompt.ts | 10 +- packages/core/src/plugin/variant.ts | 12 +- packages/core/src/plugin/warming.ts | 2 +- packages/core/src/plugin/websearch/exa.ts | 2 +- .../core/src/plugin/websearch/parallel.ts | 2 +- packages/core/src/project.ts | 3 +- packages/core/src/project/copy.ts | 8 +- packages/core/src/provider.ts | 8 +- packages/core/src/pty.ts | 18 +- packages/core/src/pty/ticket.ts | 4 +- packages/core/src/question.ts | 30 +- packages/core/src/reference.ts | 12 +- packages/core/src/reference/instructions.ts | 2 +- packages/core/src/ripgrep.ts | 2 +- packages/core/src/session.ts | 185 +- packages/core/src/session/compaction.ts | 28 +- packages/core/src/session/context.ts | 30 +- packages/core/src/session/execution.ts | 16 +- .../core/src/session/execution/restart.ts | 2 +- packages/core/src/session/generate-node.ts | 4 +- packages/core/src/session/generate.ts | 2 +- packages/core/src/session/info.ts | 22 +- .../core/src/session/instruction-entry.ts | 2 +- .../core/src/session/instruction-state.ts | 21 +- packages/core/src/session/instructions.ts | 14 +- packages/core/src/session/model-request.ts | 50 +- packages/core/src/session/pending.ts | 24 +- packages/core/src/session/projector.ts | 112 +- packages/core/src/session/revert.ts | 14 +- packages/core/src/session/runner/index.ts | 4 +- packages/core/src/session/runner/llm.ts | 45 +- packages/core/src/session/runner/model.ts | 20 +- .../src/session/runner/publish-llm-event.ts | 118 +- packages/core/src/session/runner/retry.ts | 6 +- .../core/src/session/runner/to-llm-message.ts | 10 +- packages/core/src/session/sql.ts | 8 +- packages/core/src/session/store.ts | 2 +- packages/core/src/session/title.ts | 24 +- packages/core/src/session/to-session-error.ts | 14 +- packages/core/src/session/usage.ts | 6 +- packages/core/src/shell.ts | 14 +- packages/core/src/skill.ts | 34 +- packages/core/src/skill/discovery.ts | 2 +- packages/core/src/skill/instructions.ts | 18 +- packages/core/src/snapshot.ts | 2 +- packages/core/src/tool-output-store.ts | 201 -- packages/core/src/tool.ts | 285 +++ packages/core/src/tool/AGENTS.md | 29 +- packages/core/src/tool/hooks.ts | 79 - packages/core/src/tool/mcp.ts | 92 +- packages/core/src/tool/{ => plugin}/edit.ts | 27 +- packages/core/src/tool/{ => plugin}/glob.ts | 23 +- packages/core/src/tool/{ => plugin}/grep.ts | 23 +- packages/core/src/tool/{ => plugin}/patch.ts | 17 +- .../core/src/tool/{ => plugin}/question.ts | 31 +- packages/core/src/tool/{ => plugin}/read.ts | 32 +- packages/core/src/tool/{ => plugin}/shell.ts | 30 +- packages/core/src/tool/{ => plugin}/skill.ts | 23 +- .../core/src/tool/{ => plugin}/subagent.ts | 27 +- .../core/src/tool/{ => plugin}/webfetch.ts | 15 +- .../core/src/tool/{ => plugin}/websearch.ts | 16 +- packages/core/src/tool/{ => plugin}/write.ts | 27 +- packages/core/src/tool/registry.ts | 377 ---- packages/core/src/tool/runtime.ts | 125 ++ packages/core/src/tool/tool.ts | 90 - packages/core/src/tool/tools.ts | 23 - packages/core/src/v1/config/migrate.ts | 10 +- packages/core/src/v2-schema.ts | 3 - packages/core/src/vcs.ts | 2 +- packages/core/src/websearch.ts | 12 +- packages/core/src/wellknown.ts | 16 +- packages/core/src/wellknown/plugin.ts | 8 +- packages/core/src/workspace.ts | 2 +- packages/core/test/agent.test.ts | 56 +- packages/core/test/aisdk.test.ts | 18 +- .../core/test/{event.test.ts => bus.test.ts} | 586 +++--- packages/core/test/catalog.test.ts | 88 +- packages/core/test/codemode.test.ts | 20 +- .../core/test/codemode/instructions.test.ts | 32 +- packages/core/test/command.test.ts | 36 +- packages/core/test/config/agent.test.ts | 62 +- packages/core/test/config/command.test.ts | 32 +- packages/core/test/config/config.test.ts | 24 +- .../fixtures/plugin/directory-plugin.ts | 2 +- packages/core/test/config/plugin.test.ts | 67 +- packages/core/test/config/policy.test.ts | 34 +- packages/core/test/config/provider.test.ts | 38 +- packages/core/test/config/reload.test.ts | 36 +- packages/core/test/config/skill.test.ts | 22 +- packages/core/test/database-migration.test.ts | 14 +- packages/core/test/event-logger.test.ts | 20 +- packages/core/test/filesystem/watcher.test.ts | 14 +- packages/core/test/form.test.ts | 16 +- packages/core/test/generate.test.ts | 16 +- .../core/test/github-copilot/models.test.ts | 28 +- packages/core/test/instruction-state.test.ts | 11 +- packages/core/test/integration.test.ts | 10 +- packages/core/test/lib/image.ts | 2 +- packages/core/test/lib/tool.ts | 68 +- packages/core/test/location-layer.test.ts | 103 +- packages/core/test/location.test.ts | 4 +- packages/core/test/mcp-instructions.test.ts | 10 +- packages/core/test/mcp.test.ts | 67 +- packages/core/test/model-resolver.test.ts | 88 +- packages/core/test/model.test.ts | 18 +- packages/core/test/models.test.ts | 32 +- packages/core/test/move-session.test.ts | 20 +- packages/core/test/permission.test.ts | 130 +- packages/core/test/plugin.test.ts | 115 +- packages/core/test/plugin/command.test.ts | 6 +- packages/core/test/plugin/fixture.ts | 27 +- .../plugin/fixtures/config-effect-plugin.ts | 2 +- .../plugin/fixtures/config-promise-plugin.ts | 2 +- .../test/plugin/fixtures/failing-plugin.ts | 2 +- .../plugin/fixtures/variant-source-plugin.ts | 13 +- packages/core/test/plugin/host.ts | 82 +- packages/core/test/plugin/models-dev.test.ts | 196 +- packages/core/test/plugin/promise.test.ts | 108 +- .../plugin/provider-amazon-bedrock.test.ts | 218 +- .../test/plugin/provider-anthropic.test.ts | 42 +- .../provider-azure-cognitive-services.test.ts | 68 +- .../core/test/plugin/provider-azure.test.ts | 126 +- .../test/plugin/provider-cerebras.test.ts | 54 +- .../provider-cloudflare-ai-gateway.test.ts | 100 +- .../provider-cloudflare-workers-ai.test.ts | 74 +- .../core/test/plugin/provider-dynamic.test.ts | 68 +- .../core/test/plugin/provider-factory.test.ts | 16 +- .../plugin/provider-github-copilot.test.ts | 124 +- .../core/test/plugin/provider-gitlab.test.ts | 72 +- .../provider-google-vertex-anthropic.test.ts | 96 +- .../plugin/provider-google-vertex.test.ts | 76 +- .../core/test/plugin/provider-google.test.ts | 38 +- .../core/test/plugin/provider-kilo.test.ts | 40 +- .../test/plugin/provider-llmgateway.test.ts | 24 +- .../core/test/plugin/provider-nvidia.test.ts | 28 +- .../plugin/provider-openai-compatible.test.ts | 44 +- .../core/test/plugin/provider-openai.test.ts | 118 +- .../test/plugin/provider-opencode.test.ts | 172 +- .../test/plugin/provider-openrouter.test.ts | 60 +- .../test/plugin/provider-sap-ai-core.test.ts | 26 +- .../plugin/provider-snowflake-cortex.test.ts | 56 +- .../core/test/plugin/provider-vercel.test.ts | 34 +- .../core/test/plugin/provider-xai.test.ts | 38 +- .../core/test/plugin/provider-zenmux.test.ts | 28 +- packages/core/test/plugin/skill.test.ts | 6 +- .../core/test/plugin/system-prompt.test.ts | 37 +- packages/core/test/plugin/variant.test.ts | 26 +- .../core/test/plugin/websearch-fixture.ts | 4 +- packages/core/test/project-copy.test.ts | 16 +- packages/core/test/project.test.ts | 64 +- packages/core/test/pty/pty-session.test.ts | 8 +- packages/core/test/pty/ticket.test.ts | 6 +- packages/core/test/question.test.ts | 71 +- packages/core/test/session-compact.test.ts | 28 +- packages/core/test/session-compaction.test.ts | 20 +- packages/core/test/session-create.test.ts | 196 +- packages/core/test/session-error.test.ts | 8 +- packages/core/test/session-execution.test.ts | 34 +- packages/core/test/session-generate.test.ts | 67 +- .../core/test/session-instructions.test.ts | 83 +- packages/core/test/session-log.test.ts | 61 +- packages/core/test/session-projector.test.ts | 107 +- packages/core/test/session-prompt.test.ts | 170 +- packages/core/test/session-remove.test.ts | 24 +- .../core/test/session-runner-message.test.ts | 34 +- .../core/test/session-runner-recorded.test.ts | 41 +- .../test/session-runner-tool-events.test.ts | 33 +- .../test/session-runner-tool-registry.test.ts | 261 ++- packages/core/test/session-runner.test.ts | 534 ++--- packages/core/test/session-skill.test.ts | 30 +- packages/core/test/session-title.test.ts | 42 +- .../core/test/session-tool-progress.test.ts | 28 +- packages/core/test/session-wait.test.ts | 20 +- packages/core/test/shared-schema.test.ts | 62 +- packages/core/test/skill.test.ts | 52 +- packages/core/test/skill/instructions.test.ts | 66 +- packages/core/test/tool-edit.test.ts | 32 +- packages/core/test/tool-execute.test.ts | 133 +- packages/core/test/tool-output-store.test.ts | 213 -- packages/core/test/tool-patch.test.ts | 34 +- packages/core/test/tool-question.test.ts | 34 +- packages/core/test/tool-read.test.ts | 69 +- .../test/tool-schema.test.ts} | 64 +- packages/core/test/tool-search.test.ts | 47 +- packages/core/test/tool-shell.test.ts | 66 +- packages/core/test/tool-skill.test.ts | 48 +- packages/core/test/tool-subagent.test.ts | 84 +- packages/core/test/tool-webfetch.test.ts | 44 +- packages/core/test/tool-websearch.test.ts | 30 +- packages/core/test/tool-write.test.ts | 32 +- packages/core/test/websearch.test.ts | 4 +- packages/core/test/wellknown.test.ts | 12 +- packages/http-recorder/README.md | 6 +- packages/http-recorder/package.json | 2 +- packages/httpapi-codegen/src/index.ts | 223 +- .../httpapi-codegen/test/generate.test.ts | 67 +- packages/plugin/AGENTS.md | 6 + packages/plugin/package.json | 16 +- .../plugin/src/{v2/promise => }/README.md | 9 +- packages/plugin/src/{v2 => }/app.ts | 0 packages/plugin/src/{v2 => }/effect/PLAN.md | 10 +- packages/plugin/src/{v2 => }/effect/README.md | 2 +- packages/plugin/src/effect/agent.ts | 17 + packages/plugin/src/{v2 => }/effect/aisdk.ts | 0 .../plugin/src/{v2 => }/effect/catalog.ts | 23 +- .../plugin/src/{v2 => }/effect/command.ts | 2 +- packages/plugin/src/{v2 => }/effect/event.ts | 0 packages/plugin/src/{v2 => }/effect/index.ts | 0 .../plugin/src/{v2 => }/effect/integration.ts | 20 +- packages/plugin/src/{v2 => }/effect/plugin.ts | 0 .../plugin/src/{v2 => }/effect/reference.ts | 2 +- .../src/{v2 => }/effect/registration.ts | 0 .../plugin/src/{v2 => }/effect/session.ts | 0 packages/plugin/src/{v2 => }/effect/skill.ts | 6 +- packages/plugin/src/effect/tool.ts | 45 + .../plugin/src/{v2 => }/effect/websearch.ts | 0 packages/plugin/src/example-workspace.ts | 34 - packages/plugin/src/example.ts | 18 - packages/plugin/src/{v2 => }/options.ts | 0 packages/plugin/src/promise/agent.ts | 17 + packages/plugin/src/{v2 => }/promise/aisdk.ts | 0 packages/plugin/src/promise/catalog.ts | 33 + packages/plugin/src/promise/command.ts | 15 + packages/plugin/src/{v2 => }/promise/event.ts | 0 packages/plugin/src/{v2 => }/promise/index.ts | 0 packages/plugin/src/promise/integration.ts | 64 + .../plugin/src/{v2 => }/promise/plugin.ts | 0 packages/plugin/src/promise/reference.ts | 14 + .../src/{v2 => }/promise/registration.ts | 0 .../plugin/src/{v2 => }/promise/session.ts | 0 packages/plugin/src/{v2 => }/promise/skill.ts | 7 +- packages/plugin/src/promise/tool.ts | 62 + packages/plugin/src/promise/types.ts | 9 + .../plugin/src/{v2 => }/promise/websearch.ts | 0 packages/plugin/src/{v2 => }/tui/context.ts | 8 +- packages/plugin/src/{v2 => }/tui/index.ts | 0 packages/plugin/src/{v2 => }/tui/plugin.ts | 0 packages/plugin/src/{ => v1}/index.ts | 0 packages/plugin/src/{ => v1}/shell.ts | 0 packages/plugin/src/{ => v1}/tool.ts | 0 packages/plugin/src/{ => v1}/tui.ts | 0 packages/plugin/src/v2/effect/agent.ts | 20 - packages/plugin/src/v2/effect/filesystem.ts | 17 - .../plugin/src/v2/effect/internal/tool.ts | 315 --- packages/plugin/src/v2/effect/location.ts | 6 - packages/plugin/src/v2/effect/npm.ts | 11 - packages/plugin/src/v2/effect/path.ts | 8 - packages/plugin/src/v2/effect/tool.ts | 2 - packages/plugin/src/v2/promise/agent.ts | 13 - packages/plugin/src/v2/promise/catalog.ts | 15 - packages/plugin/src/v2/promise/command.ts | 10 - packages/plugin/src/v2/promise/integration.ts | 39 - .../plugin/src/v2/promise/internal/tool.ts | 64 - packages/plugin/src/v2/promise/reference.ts | 10 - packages/plugin/src/v2/promise/tool.ts | 2 - .../plugin/test/contract-identity.test.ts | 8 +- packages/protocol/openapi.json | 869 +++----- packages/protocol/src/errors.ts | 9 + packages/protocol/src/groups/agent.ts | 17 + packages/schema/package.json | 1 + packages/schema/src/event-manifest.ts | 14 +- packages/schema/src/llm.ts | 19 - packages/schema/src/permission.ts | 18 +- packages/schema/src/provider.ts | 12 +- packages/schema/src/question.ts | 22 +- packages/schema/src/session-event.ts | 7 +- packages/schema/src/session-message.ts | 10 +- packages/schema/src/tool.ts | 89 + packages/schema/src/v1/legacy-event.ts | 2 +- packages/schema/src/v1/permission.ts | 18 +- packages/schema/src/v1/question.ts | 20 +- packages/schema/src/v1/session.ts | 70 +- packages/schema/src/workspace-id.ts | 2 +- packages/schema/test/contract-hygiene.test.ts | 22 +- packages/schema/test/legacy-event.test.ts | 4 +- packages/schema/test/v1-isolation.test.ts | 18 +- packages/sdk-next/src/tool.ts | 5 +- .../sdk-next/test/contract-identity.test.ts | 20 +- packages/sdk-next/test/embedded.test.ts | 12 +- packages/server/src/event-feed.ts | 13 +- packages/server/src/handlers/agent.ts | 26 +- packages/server/src/handlers/command.ts | 4 +- packages/server/src/handlers/event.ts | 5 +- packages/server/src/handlers/message.ts | 4 +- packages/server/src/handlers/permission.ts | 16 +- packages/server/src/handlers/plugin.ts | 4 +- packages/server/src/handlers/question.ts | 20 +- packages/server/src/handlers/session.ts | 4 +- packages/server/src/handlers/skill.ts | 4 +- packages/server/src/location.ts | 4 +- .../server/src/middleware/form-location.ts | 8 +- .../server/src/middleware/session-location.ts | 8 +- packages/server/src/routes.ts | 14 +- packages/server/test/event-feed.test.ts | 31 +- .../src/backend/simulated-provider.ts | 38 +- .../test/simulated-provider.test.ts | 60 +- packages/tui/src/attention.ts | 2 +- packages/tui/src/context/data.tsx | 14 +- packages/tui/src/context/keymap.tsx | 4 +- packages/tui/src/feature-plugins/builtins.ts | 2 +- .../tui/src/feature-plugins/home/footer.tsx | 2 +- .../src/feature-plugins/sidebar/context.tsx | 2 +- .../src/feature-plugins/sidebar/footer.tsx | 2 +- .../tui/src/feature-plugins/sidebar/lsp.tsx | 2 +- .../tui/src/feature-plugins/sidebar/mcp.tsx | 2 +- .../feature-plugins/system/diff-viewer.tsx | 4 +- .../feature-plugins/system/notifications.ts | 2 +- .../src/feature-plugins/system/plugins.tsx | 2 +- .../tui/src/feature-plugins/system/scrap.tsx | 2 +- .../src/feature-plugins/system/which-key.tsx | 2 +- packages/tui/src/mini/stream-v2.subagent.ts | 12 +- packages/tui/src/mini/stream-v2.transport.ts | 10 +- packages/tui/src/mini/theme.ts | 2 +- packages/tui/src/mini/types.ts | 4 +- packages/tui/src/plugin/api.ts | 2 +- packages/tui/src/plugin/command-shim.ts | 2 +- packages/tui/src/plugin/context.tsx | 4 +- packages/tui/src/plugin/runtime.tsx | 2 +- packages/tui/src/plugin/slots.tsx | 2 +- .../tui/src/routes/session/permission.tsx | 4 +- .../test/cli/cmd/tui/notifications.test.ts | 8 +- packages/tui/test/cli/tui/data.test.tsx | 17 +- .../tui/test/cli/tui/diff-viewer.test.tsx | 2 +- packages/tui/test/fixture/tui-plugin.ts | 2 +- .../tui/test/mini/stream-v2.transport.test.ts | 10 +- packages/util/src/fs-util.ts | 2 +- packages/www/content/docs/build/plugins.mdx | 33 +- packages/www/openapi.json | 730 ++----- packages/www/public/openapi.json | 730 ++----- ...a.98.patch => effect@4.0.0-beta.101.patch} | 0 466 files changed, 9380 insertions(+), 11046 deletions(-) rename packages/core/src/{event.ts => bus.ts} (88%) delete mode 100644 packages/core/src/codemode.ts rename packages/core/src/{tool/execute.ts => codemode/tool.ts} (81%) delete mode 100644 packages/core/src/tool-output-store.ts create mode 100644 packages/core/src/tool.ts delete mode 100644 packages/core/src/tool/hooks.ts rename packages/core/src/tool/{ => plugin}/edit.ts (92%) rename packages/core/src/tool/{ => plugin}/glob.ts (92%) rename packages/core/src/tool/{ => plugin}/grep.ts (93%) rename packages/core/src/tool/{ => plugin}/patch.ts (98%) rename packages/core/src/tool/{ => plugin}/question.ts (86%) rename packages/core/src/tool/{ => plugin}/read.ts (89%) rename packages/core/src/tool/{ => plugin}/shell.ts (94%) rename packages/core/src/tool/{ => plugin}/skill.ts (87%) rename packages/core/src/tool/{ => plugin}/subagent.ts (95%) rename packages/core/src/tool/{ => plugin}/webfetch.ts (96%) rename packages/core/src/tool/{ => plugin}/websearch.ts (94%) rename packages/core/src/tool/{ => plugin}/write.ts (82%) delete mode 100644 packages/core/src/tool/registry.ts create mode 100644 packages/core/src/tool/runtime.ts delete mode 100644 packages/core/src/tool/tool.ts delete mode 100644 packages/core/src/tool/tools.ts delete mode 100644 packages/core/src/v2-schema.ts rename packages/core/test/{event.test.ts => bus.test.ts} (64%) delete mode 100644 packages/core/test/tool-output-store.test.ts rename packages/{plugin/test/tool.test.ts => core/test/tool-schema.test.ts} (68%) create mode 100644 packages/plugin/AGENTS.md rename packages/plugin/src/{v2/promise => }/README.md (91%) rename packages/plugin/src/{v2 => }/app.ts (100%) rename packages/plugin/src/{v2 => }/effect/PLAN.md (98%) rename packages/plugin/src/{v2 => }/effect/README.md (98%) create mode 100644 packages/plugin/src/effect/agent.ts rename packages/plugin/src/{v2 => }/effect/aisdk.ts (100%) rename packages/plugin/src/{v2 => }/effect/catalog.ts (51%) rename packages/plugin/src/{v2 => }/effect/command.ts (89%) rename packages/plugin/src/{v2 => }/effect/event.ts (100%) rename packages/plugin/src/{v2 => }/effect/index.ts (100%) rename packages/plugin/src/{v2 => }/effect/integration.ts (80%) rename packages/plugin/src/{v2 => }/effect/plugin.ts (100%) rename packages/plugin/src/{v2 => }/effect/reference.ts (95%) rename packages/plugin/src/{v2 => }/effect/registration.ts (100%) rename packages/plugin/src/{v2 => }/effect/session.ts (100%) rename packages/plugin/src/{v2 => }/effect/skill.ts (71%) create mode 100644 packages/plugin/src/effect/tool.ts rename packages/plugin/src/{v2 => }/effect/websearch.ts (100%) delete mode 100644 packages/plugin/src/example-workspace.ts delete mode 100644 packages/plugin/src/example.ts rename packages/plugin/src/{v2 => }/options.ts (100%) create mode 100644 packages/plugin/src/promise/agent.ts rename packages/plugin/src/{v2 => }/promise/aisdk.ts (100%) create mode 100644 packages/plugin/src/promise/catalog.ts create mode 100644 packages/plugin/src/promise/command.ts rename packages/plugin/src/{v2 => }/promise/event.ts (100%) rename packages/plugin/src/{v2 => }/promise/index.ts (100%) create mode 100644 packages/plugin/src/promise/integration.ts rename packages/plugin/src/{v2 => }/promise/plugin.ts (100%) create mode 100644 packages/plugin/src/promise/reference.ts rename packages/plugin/src/{v2 => }/promise/registration.ts (100%) rename packages/plugin/src/{v2 => }/promise/session.ts (100%) rename packages/plugin/src/{v2 => }/promise/skill.ts (61%) create mode 100644 packages/plugin/src/promise/tool.ts create mode 100644 packages/plugin/src/promise/types.ts rename packages/plugin/src/{v2 => }/promise/websearch.ts (100%) rename packages/plugin/src/{v2 => }/tui/context.ts (97%) rename packages/plugin/src/{v2 => }/tui/index.ts (100%) rename packages/plugin/src/{v2 => }/tui/plugin.ts (100%) rename packages/plugin/src/{ => v1}/index.ts (100%) rename packages/plugin/src/{ => v1}/shell.ts (100%) rename packages/plugin/src/{ => v1}/tool.ts (100%) rename packages/plugin/src/{ => v1}/tui.ts (100%) delete mode 100644 packages/plugin/src/v2/effect/agent.ts delete mode 100644 packages/plugin/src/v2/effect/filesystem.ts delete mode 100644 packages/plugin/src/v2/effect/internal/tool.ts delete mode 100644 packages/plugin/src/v2/effect/location.ts delete mode 100644 packages/plugin/src/v2/effect/npm.ts delete mode 100644 packages/plugin/src/v2/effect/path.ts delete mode 100644 packages/plugin/src/v2/effect/tool.ts delete mode 100644 packages/plugin/src/v2/promise/agent.ts delete mode 100644 packages/plugin/src/v2/promise/catalog.ts delete mode 100644 packages/plugin/src/v2/promise/command.ts delete mode 100644 packages/plugin/src/v2/promise/integration.ts delete mode 100644 packages/plugin/src/v2/promise/internal/tool.ts delete mode 100644 packages/plugin/src/v2/promise/reference.ts delete mode 100644 packages/plugin/src/v2/promise/tool.ts create mode 100644 packages/schema/src/tool.ts rename patches/{effect@4.0.0-beta.98.patch => effect@4.0.0-beta.101.patch} (100%) diff --git a/bun.lock b/bun.lock index 6b08db9e5cf..a5b8672ee44 100644 --- a/bun.lock +++ b/bun.lock @@ -184,7 +184,7 @@ "effect": "catalog:", }, "peerDependencies": { - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", }, "optionalPeers": [ "effect", @@ -376,6 +376,7 @@ "@openrouter/ai-sdk-provider": "2.9.0", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", + "@standard-schema/spec": "catalog:", "ai-gateway-provider": "3.1.2", "bun-pty": "0.4.8", "diff": "catalog:", @@ -546,7 +547,7 @@ "name": "@opencode-ai/http-recorder", "version": "1.18.4", "dependencies": { - "@effect/platform-node-shared": "4.0.0-beta.98", + "@effect/platform-node-shared": "4.0.0-beta.101", }, "devDependencies": { "@effect/platform-node": "catalog:", @@ -583,7 +584,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "1.18.5", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "catalog:", "effect": "catalog:", "zod": "catalog:", }, @@ -626,6 +627,7 @@ "name": "@opencode-ai/schema", "version": "1.17.11", "dependencies": { + "@standard-schema/spec": "catalog:", "effect": "catalog:", }, "devDependencies": { @@ -1042,16 +1044,16 @@ ], "patchedDependencies": { "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", - "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", + "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1059,13 +1061,14 @@ "@opentui/solid": "catalog:", "@types/bun": "catalog:", "@types/node": "catalog:", + "effect": "catalog:", }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", "@corvu/drawer": "0.2.4", - "@effect/opentelemetry": "4.0.0-beta.98", - "@effect/platform-node": "4.0.0-beta.98", - "@effect/sql-sqlite-bun": "4.0.0-beta.98", + "@effect/opentelemetry": "4.0.0-beta.101", + "@effect/platform-node": "4.0.0-beta.101", + "@effect/sql-sqlite-bun": "4.0.0-beta.101", "@hono/standard-validator": "0.2.0", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", @@ -1085,6 +1088,7 @@ "@solidjs/meta": "0.29.4", "@solidjs/router": "0.15.4", "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020", + "@standard-schema/spec": "1.1.0", "@tailwindcss/vite": "4.1.11", "@tanstack/solid-virtual": "3.13.32", "@tsconfig/bun": "1.0.9", @@ -1101,7 +1105,7 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", "fuzzysort": "3.1.0", "get-east-asian-width": "1.6.0", "hono": "4.10.7", @@ -1545,13 +1549,13 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.98", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.98" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-ITfK8xhcl+9GXOvPwzADWkOQ+dgUGZrJNefT3r2+uLFmzjyKRLtHzhLOl6lZaLSsf5io13+nmt8adfMRQPq+oA=="], + "@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-beta.101", "", { "peerDependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.33.0", "effect": "^4.0.0-beta.101" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-IdejlqRLbjRHJgVnea4s8CxTWfvkSjM0HlnpNfP07IGTbhmAvPs7PMaWt1xzYWbOnI7CriQwxv+54eW1PwIkZg=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.101", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.101", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101", "ioredis": "^5.7.0" } }, "sha512-pClk7dmMtHgM6Byu7CzGfrPvZ1/4BwmrRlCg2Op+iJozMkwVUxq8v4beK8b9SvxsliJjHZFznjvkVLX7LQjBqw=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.101", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-g4L7XiyJSNJLJVhlslyg2zBCQsoKQf1y1gd+Yfd+3wD9ymC+m7ymbd/5FGqnT1aXV6E2AwRr4D/R1eyRUikvWQ=="], - "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.98", "", { "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-cc41uLhYBqexdbTNu4dlui+31E8hcVLEapLySa0C8d60FmBY8IEAV/RD3oF+6pqPslKEZ9p1+XVLdDm0iflw5Q=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.101", "", { "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-s6AC7LXCEjCN+nKegKFY4MOi6bmT1+SLR9YHEYwhY3P5qyQQB4R5yYLgt+3J4EPp5fa3t4FIDdPEUwz9LdKm6g=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], @@ -2105,8 +2109,6 @@ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], - "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], @@ -3829,7 +3831,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="], + "effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], diff --git a/package.json b/package.json index 3ff72db18e8..de344eac278 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "packages/slack" ], "catalog": { - "@effect/opentelemetry": "4.0.0-beta.98", - "@effect/platform-node": "4.0.0-beta.98", - "@effect/sql-sqlite-bun": "4.0.0-beta.98", + "@effect/opentelemetry": "4.0.0-beta.101", + "@effect/platform-node": "4.0.0-beta.101", + "@effect/sql-sqlite-bun": "4.0.0-beta.101", "@npmcli/arborist": "9.4.0", "@types/bun": "1.3.13", "@types/cross-spawn": "6.0.6", @@ -50,6 +50,7 @@ "@opentui/solid": "0.4.5", "@tanstack/solid-virtual": "3.13.32", "@shikijs/stream": "4.2.0", + "@standard-schema/spec": "1.1.0", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@corvu/drawer": "0.2.4", @@ -68,7 +69,7 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-rc.2", "drizzle-orm": "1.0.0-rc.2", - "effect": "4.0.0-beta.98", + "effect": "4.0.0-beta.101", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.10.7", @@ -151,7 +152,8 @@ "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@types/bun": "catalog:", - "@types/node": "catalog:" + "@types/node": "catalog:", + "effect": "catalog:" }, "patchedDependencies": { "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", @@ -165,7 +167,7 @@ "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "effect@4.0.0-beta.98": "patches/effect@4.0.0-beta.98.patch", + "effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch", "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch" } } diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 316f193d92e..6041500be64 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -19,7 +20,6 @@ import { type ProviderMetadata, type ToolCallPart, type ToolDefinition, - type ToolContent, type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -425,7 +425,7 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me // Tool results may carry structured text, images, and documents. Keep media as provider-native // content instead of JSON-stringifying base64 into a prompt string. const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* ( - item: ToolContent, + item: Tool.Content, ) { if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name }) @@ -436,7 +436,7 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte // with existing cassettes and provider expectations. if (part.result.type !== "content") return ProviderShared.toolResultText(part) // Preserve the narrowed array element type when compiled through a consumer package. - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value return yield* Effect.forEach(content, lowerToolResultContentItem) }) diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 8a62d50fb89..03e3c00353c 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -16,7 +17,6 @@ import { type TextPart, type ToolCallPart, type ToolDefinition, - type ToolContent, } from "../schema" import { JsonObject, optionalArray, ProviderShared } from "./shared" import { GeminiToolSchema } from "./utils/gemini-tool-schema" @@ -289,7 +289,7 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR }) continue } - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value const text = content.filter((item) => item.type === "text").map((item) => item.text) const media: GeminiInlineDataPart[] = [] for (const item of content) { diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 3372145087c..97ad0846141 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import type { Content } from "@opencode-ai/schema/tool" import { HttpTransport } from "../route/transport" import { Protocol } from "../route/protocol" import { @@ -14,7 +15,6 @@ import { type TextPart, type ToolCallPart, type ToolDefinition, - type ToolContent, type ToolResultPart, } from "../schema" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -371,7 +371,7 @@ const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* ( // Tool results may carry structured text, images, and files. Keep media as provider-native // content instead of JSON-stringifying base64 into a prompt string. const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* ( - item: ToolContent, + item: Content, request: LLMRequest, extension: Extension, ) { @@ -392,7 +392,7 @@ const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(f // compatibility with existing cassettes and provider expectations. if (part.result.type !== "content") return ProviderShared.toolResultText(part) // Preserve the narrowed array element type when compiled through a consumer package. - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value return yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)) }) @@ -496,7 +496,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques if (store !== false && itemID && !hostedToolReferences.has(itemID)) input.push({ type: "item_reference", id: itemID }) if (store === false && part.result.type === "content") { - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value input.push({ role: "user", content: yield* Effect.forEach(content, (item) => lowerToolResultContentItem(item, request, extension)), diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index e130f0bdfe4..a2d8a7b5417 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -1,4 +1,5 @@ import { Effect, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { Route } from "../route/client" import { Auth } from "../route/auth" import { Endpoint } from "../route/endpoint" @@ -17,7 +18,6 @@ import { type TextPart, type ToolCallPart, type ToolDefinition, - type ToolContent, } from "../schema" import { classifyProviderFailure } from "../provider-error" import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" @@ -335,7 +335,7 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m messages.push({ role: "tool", tool_call_id: part.id, content: ProviderShared.toolResultText(part) }) continue } - const content: ReadonlyArray = part.result.value + const content: ReadonlyArray = part.result.value const text = content.filter((item) => item.type === "text").map((item) => item.text) messages.push({ role: "tool", tool_call_id: part.id, content: text.join("\n") }) const files = content.filter((item) => item.type === "file") diff --git a/packages/ai/src/protocols/shared.ts b/packages/ai/src/protocols/shared.ts index 478088b0817..9d8f98a1ad2 100644 --- a/packages/ai/src/protocols/shared.ts +++ b/packages/ai/src/protocols/shared.ts @@ -1,4 +1,5 @@ import { Buffer } from "node:buffer" +import { Tool } from "@opencode-ai/schema/tool" import { Effect, Schema, Stream } from "effect" import * as Sse from "effect/unstable/encoding/Sse" import { Headers, HttpClientRequest } from "effect/unstable/http" @@ -9,7 +10,6 @@ import { type ContentPart, type LLMRequest, type MediaPart, - type ToolFileContent, type TextPart, type ToolResultPart, } from "../schema" @@ -206,7 +206,7 @@ export const validateMedia = Effect.fn("ProviderShared.validateMedia")(function* return { mime, base64, dataUrl: `data:${mime};base64,${base64}`, bytes } satisfies ValidatedMedia }) -export const validateToolFile = (route: string, part: ToolFileContent, supportedMimes: ReadonlySet) => +export const validateToolFile = (route: string, part: Tool.FileContent, supportedMimes: ReadonlySet) => validateMedia(route, { type: "media", mediaType: part.mime, data: part.uri, filename: part.name }, supportedMimes) export const trimBaseUrl = (value: string) => value.replace(/\/+$/, "") diff --git a/packages/ai/src/schema/errors.ts b/packages/ai/src/schema/errors.ts index 82acb7cb788..7b4cb96425b 100644 --- a/packages/ai/src/schema/errors.ts +++ b/packages/ai/src/schema/errors.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import { ModelID, ProviderID, ProviderMetadata, RouteID } from "./ids" export const ProviderFailureClassification = Schema.Literal("context-overflow") @@ -152,8 +153,4 @@ export class LLMError extends Schema.TaggedErrorClass()("LLM.Error", { * Anything thrown or yielded by a handler that is not a `ToolFailure` is * treated as a defect and fails the stream. */ -export class ToolFailure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { - message: Schema.String, - error: Schema.optional(Schema.Defect()), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), -}) {} +export class ToolFailure extends Tool.Error {} diff --git a/packages/ai/src/schema/messages.ts b/packages/ai/src/schema/messages.ts index e6617ddc9ed..bf859f946cf 100644 --- a/packages/ai/src/schema/messages.ts +++ b/packages/ai/src/schema/messages.ts @@ -1,5 +1,5 @@ import { Schema } from "effect" -import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm" +import { Tool } from "@opencode-ai/schema/tool" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" import { isRecord } from "../utils/record" @@ -40,8 +40,6 @@ export const MediaPart = Schema.Struct({ }).annotate({ identifier: "LLM.Content.Media" }) export type MediaPart = Schema.Schema.Type -export { ToolContent, ToolFileContent, ToolTextContent } - const isToolResultValue = (value: unknown): value is ToolResultValue => isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") && @@ -63,7 +61,7 @@ export const ToolResultValue = Object.assign( }), Schema.Struct({ type: Schema.Literal("content"), - value: Schema.Array(ToolContent), + value: Schema.Array(Tool.Content), }), ]).annotate({ identifier: "LLM.ToolResult" }), { @@ -79,16 +77,16 @@ export type ToolResultValue = Schema.Schema.Type export interface ToolOutput { readonly structured: unknown - readonly content: ReadonlyArray + readonly content: ReadonlyArray } export const ToolOutput = Object.assign( Schema.Struct({ structured: Schema.Unknown, - content: Schema.Array(ToolContent), + content: Schema.Array(Tool.Content), }).annotate({ identifier: "LLM.ToolOutput" }), { - make: (structured: unknown, content: ReadonlyArray = []): ToolOutput => ({ structured, content }), + make: (structured: unknown, content: ReadonlyArray = []): ToolOutput => ({ structured, content }), fromResultValue: (result: ToolResultValue): ToolOutput | undefined => { switch (result.type) { case "json": diff --git a/packages/ai/src/tool-runtime.ts b/packages/ai/src/tool-runtime.ts index c483950c125..71958977cb7 100644 --- a/packages/ai/src/tool-runtime.ts +++ b/packages/ai/src/tool-runtime.ts @@ -28,7 +28,7 @@ export const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect result(call, value)), - Effect.catchTag("LLM.ToolFailure", (failure) => + Effect.catchTag("Tool.Error", (failure) => Effect.succeed(result(call, { type: "error", value: failure.message }, failure.error)), ), ) diff --git a/packages/ai/src/tool.ts b/packages/ai/src/tool.ts index 62bd0df82f1..25df2be2878 100644 --- a/packages/ai/src/tool.ts +++ b/packages/ai/src/tool.ts @@ -1,7 +1,7 @@ import { Effect, JsonSchema, Schema } from "effect" +import { Tool } from "@opencode-ai/schema/tool" import type { ToolCallPart, - ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType, } from "./schema" @@ -31,7 +31,7 @@ export interface ToolModelOutputInput { export type ToolToModelOutput, Success extends ToolSchema> = ( input: ToolModelOutputInput, Success["Encoded"]>, -) => ReadonlyArray +) => ReadonlyArray /** * A type-safe LLM tool. Each tool bundles its own description, parameter @@ -95,7 +95,7 @@ type DynamicToolConfig = { readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect - readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray readonly toStructuredOutput?: (output: unknown) => unknown } @@ -151,7 +151,7 @@ export function make(config: { readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect - readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray readonly toStructuredOutput?: (output: unknown) => unknown }): AnyExecutableTool export function make(config: { @@ -159,7 +159,7 @@ export function make(config: { readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema readonly execute?: undefined - readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray + readonly toModelOutput?: (input: ToolModelOutputInput) => ReadonlyArray readonly toStructuredOutput?: (output: unknown) => unknown }): AnyTool export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool { @@ -236,7 +236,7 @@ const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { } const project = ( - toModelOutput: ((input: ToolModelOutputInput) => ReadonlyArray) | undefined, + toModelOutput: ((input: ToolModelOutputInput) => ReadonlyArray) | undefined, toStructuredOutput: ((output: unknown) => unknown) | undefined, parameters: unknown, callID: ToolCallPart["id"], diff --git a/packages/ai/test/tool-runtime.test.ts b/packages/ai/test/tool-runtime.test.ts index 356ee779c8b..227f07fc245 100644 --- a/packages/ai/test/tool-runtime.test.ts +++ b/packages/ai/test/tool-runtime.test.ts @@ -1,4 +1,5 @@ import { describe, expect } from "bun:test" +import { Content } from "@opencode-ai/schema/tool" import { Effect, Schema, Stream } from "effect" import { GenerationOptions, @@ -7,7 +8,6 @@ import { LLMRequest, LLMResponse, ToolChoice, - ToolContent, ToolOutput, toDefinitions, } from "../src" @@ -279,7 +279,7 @@ describe("LLMClient tools", () => { it.effect("models canonical tool files with URIs", () => Effect.sync(() => { - const decode = Schema.decodeUnknownSync(ToolContent) + const decode = Schema.decodeUnknownSync(Content) expect(decode({ type: "file", uri: "data:image/png;base64,AAAA", mime: "image/png" })).toEqual({ type: "file", diff --git a/packages/app/src/context/server-sdk.test.ts b/packages/app/src/context/server-sdk.test.ts index 767d16ffa05..4144df859ee 100644 --- a/packages/app/src/context/server-sdk.test.ts +++ b/packages/app/src/context/server-sdk.test.ts @@ -25,7 +25,7 @@ describe("adaptServerEvent", () => { } as OpenCodeEvent expect(adaptServerEvent(current)).toMatchObject({ - type: "permission.asked", + type: "permission.v2.asked", properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] }, current, }) diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 62c58577948..4606363b7ed 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -29,7 +29,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { if (event.type === "permission.v2.asked") { return { id: event.id, - type: "permission.asked", + type: "permission.v2.asked", properties: { id: event.data.id, sessionID: event.data.sessionID, @@ -46,13 +46,13 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { } as ServerEvent } if (event.type === "permission.v2.replied") - return { id: event.id, type: "permission.replied", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "permission.v2.replied", properties: event.data, current: event } as ServerEvent if (event.type === "question.v2.asked") - return { id: event.id, type: "question.asked", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "question.v2.asked", properties: event.data, current: event } as ServerEvent if (event.type === "question.v2.replied") - return { id: event.id, type: "question.replied", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "question.v2.replied", properties: event.data, current: event } as ServerEvent if (event.type === "question.v2.rejected") - return { id: event.id, type: "question.rejected", properties: event.data, current: event } as ServerEvent + return { id: event.id, type: "question.v2.rejected", properties: event.data, current: event } as ServerEvent return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent } diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index 187b660141c..380da2e22e8 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -71,7 +71,7 @@ export async function streamTurn(input: { const next = await stream.next() if (next.done) throw new Error("event stream disconnected during prompt execution") const event = next.value - if (event.type === "permission.v2.asked" && event.data.sessionID === input.sessionID) { + if (event.type === "permission.asked" && event.data.sessionID === input.sessionID) { const tool = event.data.source?.callID ? tools.get(event.data.source.callID) : undefined await replyPermission({ client: input.client, diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 87f6e68d06b..948ad0eeace 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -5,7 +5,7 @@ import { Result } from "effect" import { isAbsolute, resolve } from "node:path" import { pendingToolCall, stringValue, toLocations, toToolKind, type ToolInput } from "./tool" -type PermissionEvent = Extract +type PermissionEvent = Extract type Connection = Pick & Partial> type Tool = { readonly name: string; readonly input: ToolInput } diff --git a/packages/cli/src/node/plugin-runtime.effect.ts b/packages/cli/src/node/plugin-runtime.effect.ts index 5e38b00d4c3..d17fab342fd 100644 --- a/packages/cli/src/node/plugin-runtime.effect.ts +++ b/packages/cli/src/node/plugin-runtime.effect.ts @@ -9,8 +9,8 @@ import { Provider, Reference, Skill, -} from "@opencode-ai/plugin/v2/effect" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +} from "@opencode-ai/plugin/effect" +import { Tool } from "@opencode-ai/schema/tool" const key = Symbol.for("opencode.plugin.v2.effect") ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { @@ -24,5 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.effect") Provider, Reference, Skill, - Tool, + Tool: { Error: Tool.Error }, } diff --git a/packages/cli/src/node/plugin-runtime.promise.ts b/packages/cli/src/node/plugin-runtime.promise.ts index 49c46512328..fa33d6edae7 100644 --- a/packages/cli/src/node/plugin-runtime.promise.ts +++ b/packages/cli/src/node/plugin-runtime.promise.ts @@ -9,8 +9,7 @@ import { Provider, Reference, Skill, -} from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" +} from "@opencode-ai/plugin" const key = Symbol.for("opencode.plugin.v2.promise") ;(globalThis as typeof globalThis & { [key]?: unknown })[key] = { @@ -24,5 +23,4 @@ const key = Symbol.for("opencode.plugin.v2.promise") Provider, Reference, Skill, - Tool, } diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index 824d228c6a5..bf568d2df54 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -177,7 +177,7 @@ export async function runNonInteractivePrompt(input: Input) { } const event = next.value - if (event.type === "permission.v2.asked" && submitted && event.data.sessionID === input.sessionID) { + if (event.type === "permission.asked" && submitted && event.data.sessionID === input.sessionID) { await replyPermission(event.data) continue } diff --git a/packages/cli/src/services/standalone.ts b/packages/cli/src/services/standalone.ts index 3a98a578e41..40b1e73a054 100644 --- a/packages/cli/src/services/standalone.ts +++ b/packages/cli/src/services/standalone.ts @@ -13,11 +13,13 @@ type Options = { readonly command?: ReadonlyArray } +const startupDirectory = process.cwd() + function command(password: string, options: Options) { const [executable, ...args] = options.command ?? [...selfCommand(), "serve"] if (!executable) throw new Error("Failed to resolve standalone server command") return ChildProcess.make(executable, [...args, "--stdio", "--port", "0"], { - cwd: process.cwd(), + cwd: startupDirectory, // Explicit entry wins over anything inherited, so a user-exported // OPENCODE_PASSWORD cannot shadow the child's lease credential. env: { OPENCODE_PASSWORD: password }, diff --git a/packages/cli/src/util/process.ts b/packages/cli/src/util/process.ts index a0c567aad53..4721a12ffa4 100644 --- a/packages/cli/src/util/process.ts +++ b/packages/cli/src/util/process.ts @@ -1,11 +1,13 @@ import path from "node:path" +const entrypoint = process.argv[1] ? path.resolve(process.argv[1]) : undefined + export function selfCommand() { const runtime = path.basename(process.execPath, path.extname(process.execPath)).toLowerCase() if (runtime !== "bun" && runtime !== "node" && runtime !== "nodejs") return [process.execPath] - if (!process.argv[1]) throw new Error("Failed to resolve CLI entrypoint") - if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), process.argv[1]] - return [process.execPath, process.argv[1]] + if (!entrypoint) throw new Error("Failed to resolve CLI entrypoint") + if (runtime === "node" || runtime === "nodejs") return [process.execPath, ...nodeFlags(), entrypoint] + return [process.execPath, entrypoint] } function nodeFlags() { diff --git a/packages/cli/test/acp/permission-behavior.test.ts b/packages/cli/test/acp/permission-behavior.test.ts index df7de586fdd..9418daa4b1a 100644 --- a/packages/cli/test/acp/permission-behavior.test.ts +++ b/packages/cli/test/acp/permission-behavior.test.ts @@ -502,7 +502,7 @@ function permissionAsked( readonly source?: { readonly type: "tool"; readonly messageID: string; readonly callID: string } } = {}, ) { - return ephemeralEvent("permission.v2.asked", { + return ephemeralEvent("permission.asked", { id, sessionID, action: input.action ?? "shell", diff --git a/packages/cli/test/fixture/standalone-owner.ts b/packages/cli/test/fixture/standalone-owner.ts index 62e1b748873..d34385a10e4 100644 --- a/packages/cli/test/fixture/standalone-owner.ts +++ b/packages/cli/test/fixture/standalone-owner.ts @@ -3,12 +3,14 @@ import { Service } from "@opencode-ai/client/effect/service" import path from "node:path" import { Standalone } from "../../src/services/standalone" -process.argv[1] = path.join(import.meta.dir, "../../src/index.ts") +process.chdir(path.join(import.meta.dir, "../../../..")) await Effect.runPromise( Effect.scoped( Effect.gen(function* () { - const endpoint = yield* Standalone.start() + const endpoint = yield* Standalone.start({ + command: [process.execPath, path.join(import.meta.dir, "../../src/index.ts"), "serve"], + }) const response = yield* Effect.promise(() => fetch(new URL("/api/health", endpoint.url), { headers: Service.headers(endpoint) }), ) diff --git a/packages/cli/vite.node.config.ts b/packages/cli/vite.node.config.ts index 15bcc8490a3..52f00a2b572 100644 --- a/packages/cli/vite.node.config.ts +++ b/packages/cli/vite.node.config.ts @@ -75,20 +75,11 @@ export const define = sdk.Plugin.define` const effectPluginModule = promisePluginModule .replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect") .replace("Promise plugin", "Effect plugin") - const promiseToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")] -if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable") -export const Tool = sdk.Tool -export const make = sdk.Tool.make` + const promiseToolModule = `export {}` const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")] if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable") -export const Tool = sdk.Tool -export const Failure = sdk.Tool.Failure -export const RegistrationError = sdk.Tool.RegistrationError -export const make = sdk.Tool.make -export const validateName = sdk.Tool.validateName -export const registrationEntries = sdk.Tool.registrationEntries -export const validateNamespace = sdk.Tool.validateNamespace -export const toLLMDefinition = sdk.Tool.toLLMDefinition` +export const Error = sdk.Tool.Error +` return `#!/usr/bin/env -S node ${nodeExecArgv.join(" ")} import __cjs_mod__ from "node:module" import { chmodSync as __ocChmod, existsSync as __ocExists, lstatSync as __ocLstat, mkdirSync as __ocMkdir, renameSync as __ocRename, rmSync as __ocRm, writeFileSync as __ocWrite } from "node:fs" @@ -100,17 +91,17 @@ const __filename = import.meta.filename const __dirname = import.meta.dirname const require = __cjs_mod__.createRequire(import.meta.url) const __ocPluginModules = ${JSON.stringify({ - "@opencode-ai/plugin/v2": "opencode:plugin-v2", - "@opencode-ai/plugin/v2/plugin": "opencode:plugin-v2-plugin", - "@opencode-ai/plugin/v2/tool": "opencode:plugin-v2-tool", - "@opencode-ai/plugin/v2/effect": "opencode:plugin-v2-effect", - "@opencode-ai/plugin/v2/effect/plugin": "opencode:plugin-v2-effect-plugin", - "@opencode-ai/plugin/v2/effect/tool": "opencode:plugin-v2-effect-tool", + "@opencode-ai/plugin": "opencode:plugin-v2", + "@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin", + "@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool", + "@opencode-ai/plugin/effect": "opencode:plugin-v2-effect", + "@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin", + "@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool", })} const __ocPluginSources = ${JSON.stringify({ "opencode:plugin-v2": promiseModule, - "opencode:plugin-v2-plugin": promisePluginModule, - "opencode:plugin-v2-tool": promiseToolModule, + "opencode:plugin-promise-plugin": promisePluginModule, + "opencode:plugin-promise-tool": promiseToolModule, "opencode:plugin-v2-effect": effectModule, "opencode:plugin-v2-effect-plugin": effectPluginModule, "opencode:plugin-v2-effect-tool": effectToolModule, diff --git a/packages/client/package.json b/packages/client/package.json index ffb6458d849..fe379b54577 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,7 +36,7 @@ "@opencode-ai/protocol": "workspace:*" }, "peerDependencies": { - "effect": "4.0.0-beta.98" + "effect": "4.0.0-beta.101" }, "peerDependenciesMeta": { "effect": { diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts index 8e432b0a17c..6f0dbaad99f 100644 --- a/packages/client/script/build.ts +++ b/packages/client/script/build.ts @@ -6,11 +6,86 @@ import { groupNames, promiseOmitEndpoints, } from "@opencode-ai/protocol/client" -import { Effect } from "effect" +import { Agent } from "@opencode-ai/schema/agent" +import { Command } from "@opencode-ai/schema/command" +import { Credential } from "@opencode-ai/schema/credential" +import { Event } from "@opencode-ai/schema/event" +import { EventLog } from "@opencode-ai/schema/event-log" +import { FileDiff } from "@opencode-ai/schema/file-diff" +import { FileSystem } from "@opencode-ai/schema/filesystem" +import { Form } from "@opencode-ai/schema/form" +import { InstructionEntry } from "@opencode-ai/schema/instruction-entry" +import { Integration } from "@opencode-ai/schema/integration" +import { Location } from "@opencode-ai/schema/location" +import { Mcp } from "@opencode-ai/schema/mcp" +import { Model } from "@opencode-ai/schema/model" +import { Permission } from "@opencode-ai/schema/permission" +import { PermissionSaved } from "@opencode-ai/schema/permission-saved" +import { Plugin } from "@opencode-ai/schema/plugin" +import { Project } from "@opencode-ai/schema/project" +import { ProjectCopy } from "@opencode-ai/schema/project-copy" +import { AgentAttachment, FileAttachment, Prompt, PromptMention } from "@opencode-ai/schema/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" +import { Provider } from "@opencode-ai/schema/provider" +import { Pty } from "@opencode-ai/schema/pty" +import { PtyTicket } from "@opencode-ai/schema/pty-ticket" +import { Question } from "@opencode-ai/schema/question" +import { Reference } from "@opencode-ai/schema/reference" +import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema" +import { Session } from "@opencode-ai/schema/session" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import { SessionPending } from "@opencode-ai/schema/session-pending" +import { Shell } from "@opencode-ai/schema/shell" +import { Skill } from "@opencode-ai/schema/skill" +import { Vcs } from "@opencode-ai/schema/vcs" +import { WebSearch } from "@opencode-ai/schema/websearch" +import { Workspace } from "@opencode-ai/schema/workspace" +import { Effect, Schema } from "effect" import { fileURLToPath } from "url" const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints }) +const effectTypeReferences = [ + ...namespaceTypes("Agent", "@opencode-ai/schema/agent", Agent), + ...namespaceTypes("Command", "@opencode-ai/schema/command", Command), + ...namespaceTypes("Credential", "@opencode-ai/schema/credential", Credential), + ...namespaceTypes("Event", "@opencode-ai/schema/event", Event), + ...namespaceTypes("EventLog", "@opencode-ai/schema/event-log", EventLog), + ...namespaceTypes("FileDiff", "@opencode-ai/schema/file-diff", FileDiff), + ...namespaceTypes("FileSystem", "@opencode-ai/schema/filesystem", FileSystem), + ...namespaceTypes("Form", "@opencode-ai/schema/form", Form), + ...namespaceTypes("InstructionEntry", "@opencode-ai/schema/instruction-entry", InstructionEntry), + ...namespaceTypes("Integration", "@opencode-ai/schema/integration", Integration), + ...namespaceTypes("Location", "@opencode-ai/schema/location", Location), + ...namespaceTypes("Mcp", "@opencode-ai/schema/mcp", Mcp), + ...namespaceTypes("Model", "@opencode-ai/schema/model", Model), + ...namespaceTypes("Permission", "@opencode-ai/schema/permission", Permission), + ...namespaceTypes("PermissionSaved", "@opencode-ai/schema/permission-saved", PermissionSaved), + ...namespaceTypes("Plugin", "@opencode-ai/schema/plugin", Plugin), + ...namespaceTypes("Project", "@opencode-ai/schema/project", Project), + ...namespaceTypes("ProjectCopy", "@opencode-ai/schema/project-copy", ProjectCopy), + ...namespaceTypes("PromptInput", "@opencode-ai/schema/prompt-input", PromptInput), + ...namespaceTypes("Provider", "@opencode-ai/schema/provider", Provider), + ...namespaceTypes("Pty", "@opencode-ai/schema/pty", Pty), + ...namespaceTypes("PtyTicket", "@opencode-ai/schema/pty-ticket", PtyTicket), + ...namespaceTypes("Question", "@opencode-ai/schema/question", Question), + ...namespaceTypes("Reference", "@opencode-ai/schema/reference", Reference), + ...namespaceTypes("Session", "@opencode-ai/schema/session", Session), + ...namespaceTypes("SessionMessage", "@opencode-ai/schema/session-message", SessionMessage), + ...namespaceTypes("SessionPending", "@opencode-ai/schema/session-pending", SessionPending), + ...namespaceTypes("Shell", "@opencode-ai/schema/shell", Shell), + ...namespaceTypes("Skill", "@opencode-ai/schema/skill", Skill), + ...namespaceTypes("Vcs", "@opencode-ai/schema/vcs", Vcs), + ...namespaceTypes("WebSearch", "@opencode-ai/schema/websearch", WebSearch), + ...namespaceTypes("Workspace", "@opencode-ai/schema/workspace", Workspace), + typeReference("Prompt", "@opencode-ai/schema/prompt", Prompt), + typeReference("PromptMention", "@opencode-ai/schema/prompt", PromptMention), + typeReference("FileAttachment", "@opencode-ai/schema/prompt", FileAttachment), + typeReference("AgentAttachment", "@opencode-ai/schema/prompt", AgentAttachment), + typeReference("AbsolutePath", "@opencode-ai/schema/schema", AbsolutePath), + typeReference("PositiveInt", "@opencode-ai/schema/schema", PositiveInt), + typeReference("RelativePath", "@opencode-ai/schema/schema", RelativePath), +] await Effect.runPromise( Effect.all( @@ -22,14 +97,40 @@ await Effect.runPromise( fileURLToPath(new URL("../src/promise/generated", import.meta.url)), ), write( - emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }), + emitEffectImported(effectContract, { + module: "../../contract", + api: "ClientApi", + shapeModule: "../api/api.js", + }), fileURLToPath(new URL("../src/effect/generated", import.meta.url)), ), write( - emitEffectShape(effectContract, { module: "../../contract", api: "ClientApi" }), + emitEffectShape(effectContract, { + typeReferences: effectTypeReferences, + outputTypes: { + "event.subscribe": { + name: "OpenCodeEvent", + import: 'import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"', + }, + }, + }), fileURLToPath(new URL("../src/effect/api", import.meta.url)), ), ], { concurrency: 3, discard: true }, ).pipe(Effect.provide(NodeFileSystem.layer)), ) + +function namespaceTypes(namespace: string, module: string, values: object) { + return Object.entries(values).flatMap(([name, schema]) => + Schema.isSchema(schema) ? [typeReference(`${namespace}.${name}`, module, schema)] : [], + ) +} + +function typeReference(name: string, module: string, schema: Schema.Top) { + return { + schema, + name, + import: `import type { ${name.split(".")[0]} } from ${JSON.stringify(module)}`, + } +} diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 3c229bc6151..3bb65cc0073 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -1,18 +1,49 @@ // Generated by @opencode-ai/httpapi-codegen. Do not edit. import type { Effect, Stream } from "effect" -import type { HttpApiClient } from "effect/unstable/httpapi" -import type { ClientApi } from "../../contract" +import type { Location } from "@opencode-ai/schema/location" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Plugin } from "@opencode-ai/schema/plugin" +import type { Workspace } from "@opencode-ai/schema/workspace" +import type { Session } from "@opencode-ai/schema/session" +import type { AbsolutePath } from "@opencode-ai/schema/schema" +import type { Project } from "@opencode-ai/schema/project" +import type { RelativePath } from "@opencode-ai/schema/schema" +import type { Brand } from "effect" +import type { Model } from "@opencode-ai/schema/model" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { PromptInput } from "@opencode-ai/schema/prompt-input" +import type { AgentAttachment } from "@opencode-ai/schema/prompt" +import type { SessionPending } from "@opencode-ai/schema/session-pending" +import type { Skill } from "@opencode-ai/schema/skill" +import type { Event } from "@opencode-ai/schema/event" +import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry" +import type { Schema } from "effect" +import type { EventLog } from "@opencode-ai/schema/event-log" +import type { Shell } from "@opencode-ai/schema/shell" +import type { DateTime } from "effect" +import type { Provider } from "@opencode-ai/schema/provider" +import type { Integration } from "@opencode-ai/schema/integration" +import type { Mcp } from "@opencode-ai/schema/mcp" +import type { Credential } from "@opencode-ai/schema/credential" +import type { Form } from "@opencode-ai/schema/form" +import type { Permission } from "@opencode-ai/schema/permission" +import type { PermissionSaved } from "@opencode-ai/schema/permission-saved" +import type { FileSystem } from "@opencode-ai/schema/filesystem" +import type { Command } from "@opencode-ai/schema/command" +import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" +import type { Pty } from "@opencode-ai/schema/pty" +import type { Question } from "@opencode-ai/schema/question" +import type { Reference } from "@opencode-ai/schema/reference" +import type { ProjectCopy } from "@opencode-ai/schema/project-copy" +import type { Vcs } from "@opencode-ai/schema/vcs" +import type { FileDiff } from "@opencode-ai/schema/file-diff" +import type { WebSearch } from "@opencode-ai/schema/websearch" -type RawClient = HttpApiClient.ForApi -type EffectValue = A extends Effect.Effect ? Success : never -type StreamValue = A extends Stream.Stream ? Success : never - -export type Endpoint0_0Output = EffectValue> +export type Endpoint0_0Output = { readonly healthy: true; readonly version: string; readonly pid: number } export type HealthGetOperation = () => Effect.Effect -type Endpoint0_1Request = Parameters[0] -export type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] } -export type Endpoint0_1Output = EffectValue> +export type Endpoint0_1Input = { readonly instanceID: string } +export type Endpoint0_1Output = { readonly accepted: boolean } export type HealthStopOperation = (input: Endpoint0_1Input) => Effect.Effect export interface HealthApi { @@ -20,288 +51,792 @@ export interface HealthApi { readonly stop: HealthStopOperation } -export type Endpoint1_0Output = EffectValue> +export type Endpoint1_0Output = { readonly urls: ReadonlyArray } export type ServerGetOperation = () => Effect.Effect export interface ServerApi { readonly get: ServerGetOperation } -type Endpoint2_0Request = Parameters[0] -export type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } -export type Endpoint2_0Output = EffectValue> +export type Endpoint2_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint2_0Output = Location.Info export type LocationGetOperation = (input?: Endpoint2_0Input) => Effect.Effect export interface LocationApi { readonly get: LocationGetOperation } -type Endpoint3_0Request = Parameters[0] -export type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } -export type Endpoint3_0Output = EffectValue> +export type Endpoint3_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint3_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type AgentListOperation = (input?: Endpoint3_0Input) => Effect.Effect +export type Endpoint3_1Input = { + readonly agentID: Agent.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint3_1Output = { readonly location: Location.Info; readonly data: Agent.Info } +export type AgentGetOperation = (input: Endpoint3_1Input) => Effect.Effect + export interface AgentApi { readonly list: AgentListOperation + readonly get: AgentGetOperation } -type Endpoint4_0Request = Parameters[0] -export type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } -export type Endpoint4_0Output = EffectValue> +export type Endpoint4_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint4_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type PluginListOperation = (input?: Endpoint4_0Input) => Effect.Effect export interface PluginApi { readonly list: PluginListOperation } -type Endpoint5_0Request = Parameters[0] export type Endpoint5_0Input = { - readonly workspace?: Endpoint5_0Request["query"]["workspace"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly search?: Endpoint5_0Request["query"]["search"] - readonly parentID?: Endpoint5_0Request["query"]["parentID"] - readonly directory?: Endpoint5_0Request["query"]["directory"] - readonly project?: Endpoint5_0Request["query"]["project"] - readonly subpath?: Endpoint5_0Request["query"]["subpath"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] + readonly workspace?: Workspace.ID | undefined + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly search?: string | undefined + readonly parentID?: Session.ID | null | undefined + readonly directory?: AbsolutePath | undefined + readonly project?: Project.ID | undefined + readonly subpath?: RelativePath | undefined + readonly cursor?: (string & Brand.Brand<"SessionsCursor">) | undefined +} +export type Endpoint5_0Output = { + readonly data: ReadonlyArray + readonly cursor: { + readonly previous?: (string & Brand.Brand<"SessionsCursor">) | undefined + readonly next?: (string & Brand.Brand<"SessionsCursor">) | undefined + } } -export type Endpoint5_0Output = EffectValue> export type SessionListOperation = (input?: Endpoint5_0Input) => Effect.Effect -type Endpoint5_1Request = Parameters[0] export type Endpoint5_1Input = { - readonly id?: Endpoint5_1Request["payload"]["id"] - readonly agent?: Endpoint5_1Request["payload"]["agent"] - readonly model?: Endpoint5_1Request["payload"]["model"] - readonly location?: Endpoint5_1Request["payload"]["location"] + readonly id?: Session.ID | undefined + readonly agent?: Agent.ID | undefined + readonly model?: Model.Ref | undefined + readonly location?: Location.Ref | undefined } -export type Endpoint5_1Output = EffectValue>["data"] +export type Endpoint5_1Output = Session.Info export type SessionCreateOperation = (input?: Endpoint5_1Input) => Effect.Effect -export type Endpoint5_2Output = EffectValue>["data"] +export type Endpoint5_2Output = { readonly [x: Session.ID]: { readonly type: "running" } } export type SessionActiveOperation = () => Effect.Effect -type Endpoint5_3Request = Parameters[0] -export type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } -export type Endpoint5_3Output = EffectValue>["data"] +export type Endpoint5_3Input = { readonly sessionID: Session.ID } +export type Endpoint5_3Output = Session.Info export type SessionGetOperation = (input: Endpoint5_3Input) => Effect.Effect -type Endpoint5_4Request = Parameters[0] -export type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } -export type Endpoint5_4Output = EffectValue> +export type Endpoint5_4Input = { readonly sessionID: Session.ID } +export type Endpoint5_4Output = void export type SessionRemoveOperation = (input: Endpoint5_4Input) => Effect.Effect -type Endpoint5_5Request = Parameters[0] -export type Endpoint5_5Input = { - readonly sessionID: Endpoint5_5Request["params"]["sessionID"] - readonly messageID?: Endpoint5_5Request["payload"]["messageID"] -} -export type Endpoint5_5Output = EffectValue>["data"] +export type Endpoint5_5Input = { readonly sessionID: Session.ID; readonly messageID?: SessionMessage.ID | undefined } +export type Endpoint5_5Output = Session.Info export type SessionForkOperation = (input: Endpoint5_5Input) => Effect.Effect -type Endpoint5_6Request = Parameters[0] -export type Endpoint5_6Input = { - readonly sessionID: Endpoint5_6Request["params"]["sessionID"] - readonly agent: Endpoint5_6Request["payload"]["agent"] -} -export type Endpoint5_6Output = EffectValue> +export type Endpoint5_6Input = { readonly sessionID: Session.ID; readonly agent: Agent.ID } +export type Endpoint5_6Output = void export type SessionSwitchAgentOperation = (input: Endpoint5_6Input) => Effect.Effect -type Endpoint5_7Request = Parameters[0] -export type Endpoint5_7Input = { - readonly sessionID: Endpoint5_7Request["params"]["sessionID"] - readonly model: Endpoint5_7Request["payload"]["model"] -} -export type Endpoint5_7Output = EffectValue> +export type Endpoint5_7Input = { readonly sessionID: Session.ID; readonly model: Model.Ref } +export type Endpoint5_7Output = void export type SessionSwitchModelOperation = (input: Endpoint5_7Input) => Effect.Effect -type Endpoint5_8Request = Parameters[0] -export type Endpoint5_8Input = { - readonly sessionID: Endpoint5_8Request["params"]["sessionID"] - readonly title: Endpoint5_8Request["payload"]["title"] -} -export type Endpoint5_8Output = EffectValue> +export type Endpoint5_8Input = { readonly sessionID: Session.ID; readonly title: string } +export type Endpoint5_8Output = void export type SessionRenameOperation = (input: Endpoint5_8Input) => Effect.Effect -type Endpoint5_9Request = Parameters[0] export type Endpoint5_9Input = { - readonly sessionID: Endpoint5_9Request["params"]["sessionID"] - readonly directory: Endpoint5_9Request["payload"]["directory"] - readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"] + readonly sessionID: Session.ID + readonly directory: AbsolutePath + readonly workspaceID?: Workspace.ID | undefined } -export type Endpoint5_9Output = EffectValue> +export type Endpoint5_9Output = void export type SessionMoveOperation = (input: Endpoint5_9Input) => Effect.Effect -type Endpoint5_10Request = Parameters[0] export type Endpoint5_10Input = { - readonly sessionID: Endpoint5_10Request["params"]["sessionID"] - readonly id?: Endpoint5_10Request["payload"]["id"] - readonly text: Endpoint5_10Request["payload"]["text"] - readonly files?: Endpoint5_10Request["payload"]["files"] - readonly agents?: Endpoint5_10Request["payload"]["agents"] - readonly metadata?: Endpoint5_10Request["payload"]["metadata"] - readonly delivery?: Endpoint5_10Request["payload"]["delivery"] - readonly resume?: Endpoint5_10Request["payload"]["resume"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly text: string + readonly files?: ReadonlyArray | undefined + readonly agents?: ReadonlyArray | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly delivery?: "steer" | "queue" | undefined + readonly resume?: boolean | undefined } -export type Endpoint5_10Output = EffectValue>["data"] +export type Endpoint5_10Output = SessionPending.User export type SessionPromptOperation = (input: Endpoint5_10Input) => Effect.Effect -type Endpoint5_11Request = Parameters[0] export type Endpoint5_11Input = { - readonly sessionID: Endpoint5_11Request["params"]["sessionID"] - readonly id?: Endpoint5_11Request["payload"]["id"] - readonly command: Endpoint5_11Request["payload"]["command"] - readonly arguments?: Endpoint5_11Request["payload"]["arguments"] - readonly agent?: Endpoint5_11Request["payload"]["agent"] - readonly model?: Endpoint5_11Request["payload"]["model"] - readonly files?: Endpoint5_11Request["payload"]["files"] - readonly agents?: Endpoint5_11Request["payload"]["agents"] - readonly delivery?: Endpoint5_11Request["payload"]["delivery"] - readonly resume?: Endpoint5_11Request["payload"]["resume"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly command: string + readonly arguments?: string | undefined + readonly agent?: Agent.ID | undefined + readonly model?: Model.Ref | undefined + readonly files?: ReadonlyArray | undefined + readonly agents?: ReadonlyArray | undefined + readonly delivery?: "steer" | "queue" | undefined + readonly resume?: boolean | undefined } -export type Endpoint5_11Output = EffectValue>["data"] +export type Endpoint5_11Output = SessionPending.User export type SessionCommandOperation = (input: Endpoint5_11Input) => Effect.Effect -type Endpoint5_12Request = Parameters[0] export type Endpoint5_12Input = { - readonly sessionID: Endpoint5_12Request["params"]["sessionID"] - readonly id?: Endpoint5_12Request["payload"]["id"] - readonly skill: Endpoint5_12Request["payload"]["skill"] - readonly resume?: Endpoint5_12Request["payload"]["resume"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly skill: Skill.ID + readonly resume?: boolean | undefined } -export type Endpoint5_12Output = EffectValue> +export type Endpoint5_12Output = void export type SessionSkillOperation = (input: Endpoint5_12Input) => Effect.Effect -type Endpoint5_13Request = Parameters[0] export type Endpoint5_13Input = { - readonly sessionID: Endpoint5_13Request["params"]["sessionID"] - readonly id?: Endpoint5_13Request["payload"]["id"] - readonly text: Endpoint5_13Request["payload"]["text"] - readonly description?: Endpoint5_13Request["payload"]["description"] - readonly metadata?: Endpoint5_13Request["payload"]["metadata"] - readonly delivery?: Endpoint5_13Request["payload"]["delivery"] - readonly resume?: Endpoint5_13Request["payload"]["resume"] + readonly sessionID: Session.ID + readonly id?: SessionMessage.ID | undefined + readonly text: string + readonly description?: string | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly delivery?: "steer" | "queue" | undefined + readonly resume?: boolean | undefined } -export type Endpoint5_13Output = EffectValue>["data"] +export type Endpoint5_13Output = SessionPending.Synthetic export type SessionSyntheticOperation = (input: Endpoint5_13Input) => Effect.Effect -type Endpoint5_14Request = Parameters[0] export type Endpoint5_14Input = { - readonly sessionID: Endpoint5_14Request["params"]["sessionID"] - readonly id?: Endpoint5_14Request["payload"]["id"] - readonly command: Endpoint5_14Request["payload"]["command"] + readonly sessionID: Session.ID + readonly id?: Event.ID | undefined + readonly command: string } -export type Endpoint5_14Output = EffectValue> +export type Endpoint5_14Output = void export type SessionShellOperation = (input: Endpoint5_14Input) => Effect.Effect -type Endpoint5_15Request = Parameters[0] -export type Endpoint5_15Input = { - readonly sessionID: Endpoint5_15Request["params"]["sessionID"] - readonly id?: Endpoint5_15Request["payload"]["id"] -} -export type Endpoint5_15Output = EffectValue>["data"] +export type Endpoint5_15Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined } +export type Endpoint5_15Output = SessionPending.Compaction export type SessionCompactOperation = (input: Endpoint5_15Input) => Effect.Effect -type Endpoint5_16Request = Parameters[0] -export type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } -export type Endpoint5_16Output = EffectValue> +export type Endpoint5_16Input = { readonly sessionID: Session.ID } +export type Endpoint5_16Output = void export type SessionWaitOperation = (input: Endpoint5_16Input) => Effect.Effect -type Endpoint5_17Request = Parameters[0] export type Endpoint5_17Input = { - readonly sessionID: Endpoint5_17Request["params"]["sessionID"] - readonly messageID: Endpoint5_17Request["payload"]["messageID"] - readonly files?: Endpoint5_17Request["payload"]["files"] + readonly sessionID: Session.ID + readonly messageID: SessionMessage.ID + readonly files?: boolean | undefined } -export type Endpoint5_17Output = EffectValue>["data"] +export type Endpoint5_17Output = Session.Revert export type SessionRevertStageOperation = (input: Endpoint5_17Input) => Effect.Effect -type Endpoint5_18Request = Parameters[0] -export type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } -export type Endpoint5_18Output = EffectValue> +export type Endpoint5_18Input = { readonly sessionID: Session.ID } +export type Endpoint5_18Output = void export type SessionRevertClearOperation = (input: Endpoint5_18Input) => Effect.Effect -type Endpoint5_19Request = Parameters[0] -export type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } -export type Endpoint5_19Output = EffectValue> +export type Endpoint5_19Input = { readonly sessionID: Session.ID } +export type Endpoint5_19Output = void export type SessionRevertCommitOperation = (input: Endpoint5_19Input) => Effect.Effect -type Endpoint5_20Request = Parameters[0] -export type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } -export type Endpoint5_20Output = EffectValue>["data"] +export type Endpoint5_20Input = { readonly sessionID: Session.ID } +export type Endpoint5_20Output = ReadonlyArray export type SessionContextOperation = (input: Endpoint5_20Input) => Effect.Effect -type Endpoint5_21Request = Parameters[0] -export type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } -export type Endpoint5_21Output = EffectValue>["data"] +export type Endpoint5_21Input = { readonly sessionID: Session.ID } +export type Endpoint5_21Output = ReadonlyArray export type SessionPendingListOperation = (input: Endpoint5_21Input) => Effect.Effect -type Endpoint5_22Request = Parameters[0] -export type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } -export type Endpoint5_22Output = EffectValue< - ReturnType ->["data"] +export type Endpoint5_22Input = { readonly sessionID: Session.ID } +export type Endpoint5_22Output = ReadonlyArray export type SessionInstructionsEntryListOperation = ( input: Endpoint5_22Input, ) => Effect.Effect -type Endpoint5_23Request = Parameters[0] export type Endpoint5_23Input = { - readonly sessionID: Endpoint5_23Request["params"]["sessionID"] - readonly key: Endpoint5_23Request["params"]["key"] - readonly value: Endpoint5_23Request["payload"]["value"] + readonly sessionID: Session.ID + readonly key: InstructionEntry.Key + readonly value: Schema.Json } -export type Endpoint5_23Output = EffectValue> +export type Endpoint5_23Output = void export type SessionInstructionsEntryPutOperation = ( input: Endpoint5_23Input, ) => Effect.Effect -type Endpoint5_24Request = Parameters[0] -export type Endpoint5_24Input = { - readonly sessionID: Endpoint5_24Request["params"]["sessionID"] - readonly key: Endpoint5_24Request["params"]["key"] -} -export type Endpoint5_24Output = EffectValue< - ReturnType -> +export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } +export type Endpoint5_24Output = void export type SessionInstructionsEntryRemoveOperation = ( input: Endpoint5_24Input, ) => Effect.Effect -type Endpoint5_25Request = Parameters[0] -export type Endpoint5_25Input = { - readonly sessionID: Endpoint5_25Request["params"]["sessionID"] - readonly prompt: Endpoint5_25Request["payload"]["prompt"] -} -export type Endpoint5_25Output = EffectValue>["data"] +export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly prompt: string } +export type Endpoint5_25Output = { readonly text: string } export type SessionGenerateOperation = (input: Endpoint5_25Input) => Effect.Effect -type Endpoint5_26Request = Parameters[0] export type Endpoint5_26Input = { - readonly sessionID: Endpoint5_26Request["params"]["sessionID"] - readonly after?: Endpoint5_26Request["query"]["after"] - readonly follow?: Endpoint5_26Request["query"]["follow"] + readonly sessionID: Session.ID + readonly after?: Event.Seq | undefined + readonly follow?: boolean | undefined } -export type Endpoint5_26Output = StreamValue>> +export type Endpoint5_26Output = + | ( + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.agent.selected" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly agent: Agent.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.model.selected" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly model: Model.Ref } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.moved" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly location: Location.Ref + readonly projectID?: Project.ID | undefined + readonly subpath?: RelativePath | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.renamed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly title: string } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.deleted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.forked" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly parentID: Session.ID + readonly parentSeq: number + readonly from?: SessionMessage.ID | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.input.promoted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.input.admitted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly inputID: SessionMessage.ID + readonly input: SessionPending.Message + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.succeeded" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly error: { readonly type: string; readonly message: string } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.execution.interrupted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly reason: "user" | "shutdown" | "superseded" } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.instructions.updated" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly delta: { readonly [x: string]: (string & Brand.Brand<"Instruction.Hash">) | "removed" } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.synthetic" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly text: string + readonly description?: string | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.skill.activated" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly id: Skill.ID + readonly name: Skill.Name + readonly text: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.shell.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly shell: Shell.Info } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.shell.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly shell: Shell.Info + readonly output: { + readonly output: string + readonly cursor: number + readonly size: number + readonly truncated: boolean + } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.step.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly agent: Agent.ID + readonly model: Model.Ref + readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.step.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" + readonly cost: number & Brand.Brand<"Money.USD"> + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined + readonly files?: ReadonlyArray | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.step.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly error: { readonly type: string; readonly message: string } + readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined + readonly tokens?: + | { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + | undefined + readonly snapshot?: (string & Brand.Brand<"Snapshot.ID">) | undefined + readonly files?: ReadonlyArray | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.text.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.text.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + readonly text: string + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.reasoning.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.reasoning.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly ordinal: number + readonly text: string + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.input.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly name: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.input.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly text: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.called" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly input: { readonly [x: string]: unknown } + readonly executed: boolean + readonly state?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.success" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly content: readonly [ + ( + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + ), + ...Array< + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + >, + ] + readonly metadata?: { readonly [x: string]: Schema.Json } | undefined + readonly executed: boolean + readonly resultState?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.tool.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly callID: string + readonly error: { readonly type: string; readonly message: string } + readonly content?: + | readonly [ + ( + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + ), + ...Array< + | { readonly type: "text"; readonly text: string } + | { + readonly type: "file" + readonly uri: string + readonly mime: string + readonly name?: string | undefined + } + >, + ] + | undefined + readonly metadata?: { readonly [x: string]: Schema.Json } | undefined + readonly executed: boolean + readonly resultState?: SessionMessage.ProviderState | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.retry.scheduled" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly assistantMessageID: SessionMessage.ID + readonly attempt: number + readonly at: number + readonly error: { readonly type: string; readonly message: string } + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.admitted" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.started" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly reason: "auto" | "manual" + readonly recent: string + readonly inputID?: SessionMessage.ID | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.ended" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly reason: "auto" | "manual" + readonly text: string + readonly recent: string + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.compaction.failed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly reason: "auto" | "manual" + readonly error: { readonly type: string; readonly message: string } + readonly inputID?: SessionMessage.ID | undefined + } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.revert.staged" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly revert: Session.Revert } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.revert.cleared" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.revert.committed" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { readonly sessionID: Session.ID; readonly to: SessionMessage.ID } + } + | { + readonly id: Event.ID + readonly created: DateTime.Utc + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly type: "session.usage.recorded" + readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version } + readonly location?: Location.Ref | undefined + readonly data: { + readonly sessionID: Session.ID + readonly source: "title" | "compaction" + readonly cost: number & Brand.Brand<"Money.USD"> + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + } + } + ) + | EventLog.Synced export type SessionLogOperation = (input: Endpoint5_26Input) => Stream.Stream -type Endpoint5_27Request = Parameters[0] -export type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } -export type Endpoint5_27Output = EffectValue> +export type Endpoint5_27Input = { readonly sessionID: Session.ID } +export type Endpoint5_27Output = void export type SessionInterruptOperation = (input: Endpoint5_27Input) => Effect.Effect -type Endpoint5_28Request = Parameters[0] -export type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] } -export type Endpoint5_28Output = EffectValue> +export type Endpoint5_28Input = { readonly sessionID: Session.ID } +export type Endpoint5_28Output = void export type SessionBackgroundOperation = (input: Endpoint5_28Input) => Effect.Effect -type Endpoint5_29Request = Parameters[0] -export type Endpoint5_29Input = { - readonly sessionID: Endpoint5_29Request["params"]["sessionID"] - readonly messageID: Endpoint5_29Request["params"]["messageID"] -} -export type Endpoint5_29Output = EffectValue>["data"] +export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } +export type Endpoint5_29Output = SessionMessage.Info export type SessionMessageOperation = (input: Endpoint5_29Input) => Effect.Effect export interface SessionApi { @@ -343,28 +878,32 @@ export interface SessionApi { readonly message: SessionMessageOperation } -type Endpoint6_0Request = Parameters[0] export type Endpoint6_0Input = { - readonly sessionID: Endpoint6_0Request["params"]["sessionID"] - readonly limit?: Endpoint6_0Request["query"]["limit"] - readonly order?: Endpoint6_0Request["query"]["order"] - readonly cursor?: Endpoint6_0Request["query"]["cursor"] + readonly sessionID: Session.ID + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined +} +export type Endpoint6_0Output = { + readonly data: ReadonlyArray + readonly cursor: { readonly previous?: string | undefined; readonly next?: string | undefined } } -export type Endpoint6_0Output = EffectValue> export type MessageListOperation = (input: Endpoint6_0Input) => Effect.Effect export interface MessageApi { readonly list: MessageListOperation } -type Endpoint7_0Request = Parameters[0] -export type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } -export type Endpoint7_0Output = EffectValue> +export type Endpoint7_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint7_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ModelListOperation = (input?: Endpoint7_0Input) => Effect.Effect -type Endpoint7_1Request = Parameters[0] -export type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } -export type Endpoint7_1Output = EffectValue> +export type Endpoint7_1Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint7_1Output = { readonly location: Location.Info; readonly data: Model.Info | undefined } export type ModelDefaultOperation = (input?: Endpoint7_1Input) => Effect.Effect export interface ModelApi { @@ -372,30 +911,29 @@ export interface ModelApi { readonly default: ModelDefaultOperation } -type Endpoint8_0Request = Parameters[0] export type Endpoint8_0Input = { - readonly location?: Endpoint8_0Request["query"]["location"] - readonly prompt: Endpoint8_0Request["payload"]["prompt"] - readonly model?: Endpoint8_0Request["payload"]["model"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly prompt: string + readonly model?: Model.Ref | undefined } -export type Endpoint8_0Output = EffectValue>["data"] +export type Endpoint8_0Output = { readonly text: string } export type GenerateTextOperation = (input: Endpoint8_0Input) => Effect.Effect export interface GenerateApi { readonly text: GenerateTextOperation } -type Endpoint9_0Request = Parameters[0] -export type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } -export type Endpoint9_0Output = EffectValue> +export type Endpoint9_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint9_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ProviderListOperation = (input?: Endpoint9_0Input) => Effect.Effect -type Endpoint9_1Request = Parameters[0] export type Endpoint9_1Input = { - readonly providerID: Endpoint9_1Request["params"]["providerID"] - readonly location?: Endpoint9_1Request["query"]["location"] + readonly providerID: Provider.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint9_1Output = EffectValue> +export type Endpoint9_1Output = { readonly location: Location.Info; readonly data: Provider.Info } export type ProviderGetOperation = (input: Endpoint9_1Input) => Effect.Effect export interface ProviderApi { @@ -403,118 +941,109 @@ export interface ProviderApi { readonly get: ProviderGetOperation } -type Endpoint10_0Request = Parameters[0] -export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } -export type Endpoint10_0Output = EffectValue> +export type Endpoint10_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint10_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type IntegrationListOperation = (input?: Endpoint10_0Input) => Effect.Effect -type Endpoint10_1Request = Parameters[0] export type Endpoint10_1Input = { - readonly integrationID: Endpoint10_1Request["params"]["integrationID"] - readonly location?: Endpoint10_1Request["query"]["location"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_1Output = EffectValue> +export type Endpoint10_1Output = { readonly location: Location.Info; readonly data: Integration.Info | undefined } export type IntegrationGetOperation = (input: Endpoint10_1Input) => Effect.Effect -type Endpoint10_2Request = Parameters[0] export type Endpoint10_2Input = { - readonly location?: Endpoint10_2Request["query"]["location"] - readonly url: Endpoint10_2Request["payload"]["url"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly url: string } -export type Endpoint10_2Output = EffectValue> +export type Endpoint10_2Output = void export type IntegrationWellknownAddOperation = ( input: Endpoint10_2Input, ) => Effect.Effect -type Endpoint10_3Request = Parameters[0] export type Endpoint10_3Input = { - readonly integrationID: Endpoint10_3Request["params"]["integrationID"] - readonly location?: Endpoint10_3Request["query"]["location"] - readonly key: Endpoint10_3Request["payload"]["key"] - readonly label?: Endpoint10_3Request["payload"]["label"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly key: string + readonly label?: string | undefined } -export type Endpoint10_3Output = EffectValue> +export type Endpoint10_3Output = void export type IntegrationConnectKeyOperation = ( input: Endpoint10_3Input, ) => Effect.Effect -type Endpoint10_4Request = Parameters[0] export type Endpoint10_4Input = { - readonly integrationID: Endpoint10_4Request["params"]["integrationID"] - readonly location?: Endpoint10_4Request["query"]["location"] - readonly methodID: Endpoint10_4Request["payload"]["methodID"] - readonly inputs: Endpoint10_4Request["payload"]["inputs"] - readonly label?: Endpoint10_4Request["payload"]["label"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly methodID: Integration.MethodID + readonly inputs: { readonly [x: string]: string } + readonly label?: string | undefined } -export type Endpoint10_4Output = EffectValue> +export type Endpoint10_4Output = { readonly location: Location.Info; readonly data: Integration.Attempt } export type IntegrationOauthConnectOperation = ( input: Endpoint10_4Input, ) => Effect.Effect -type Endpoint10_5Request = Parameters[0] export type Endpoint10_5Input = { - readonly integrationID: Endpoint10_5Request["params"]["integrationID"] - readonly attemptID: Endpoint10_5Request["params"]["attemptID"] - readonly location?: Endpoint10_5Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_5Output = EffectValue> +export type Endpoint10_5Output = { readonly location: Location.Info; readonly data: Integration.AttemptStatus } export type IntegrationOauthStatusOperation = ( input: Endpoint10_5Input, ) => Effect.Effect -type Endpoint10_6Request = Parameters[0] export type Endpoint10_6Input = { - readonly integrationID: Endpoint10_6Request["params"]["integrationID"] - readonly attemptID: Endpoint10_6Request["params"]["attemptID"] - readonly location?: Endpoint10_6Request["query"]["location"] - readonly code?: Endpoint10_6Request["payload"]["code"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly code?: string | undefined } -export type Endpoint10_6Output = EffectValue> +export type Endpoint10_6Output = void export type IntegrationOauthCompleteOperation = ( input: Endpoint10_6Input, ) => Effect.Effect -type Endpoint10_7Request = Parameters[0] export type Endpoint10_7Input = { - readonly integrationID: Endpoint10_7Request["params"]["integrationID"] - readonly attemptID: Endpoint10_7Request["params"]["attemptID"] - readonly location?: Endpoint10_7Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_7Output = EffectValue> +export type Endpoint10_7Output = void export type IntegrationOauthCancelOperation = ( input: Endpoint10_7Input, ) => Effect.Effect -type Endpoint10_8Request = Parameters[0] export type Endpoint10_8Input = { - readonly integrationID: Endpoint10_8Request["params"]["integrationID"] - readonly location?: Endpoint10_8Request["query"]["location"] - readonly methodID: Endpoint10_8Request["payload"]["methodID"] - readonly label?: Endpoint10_8Request["payload"]["label"] + readonly integrationID: Integration.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly methodID: Integration.MethodID + readonly label?: string | undefined } -export type Endpoint10_8Output = EffectValue> +export type Endpoint10_8Output = { readonly location: Location.Info; readonly data: Integration.CommandAttempt } export type IntegrationCommandConnectOperation = ( input: Endpoint10_8Input, ) => Effect.Effect -type Endpoint10_9Request = Parameters[0] export type Endpoint10_9Input = { - readonly integrationID: Endpoint10_9Request["params"]["integrationID"] - readonly attemptID: Endpoint10_9Request["params"]["attemptID"] - readonly location?: Endpoint10_9Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_9Output = EffectValue> +export type Endpoint10_9Output = { readonly location: Location.Info; readonly data: Integration.CommandAttemptStatus } export type IntegrationCommandStatusOperation = ( input: Endpoint10_9Input, ) => Effect.Effect -type Endpoint10_10Request = Parameters[0] export type Endpoint10_10Input = { - readonly integrationID: Endpoint10_10Request["params"]["integrationID"] - readonly attemptID: Endpoint10_10Request["params"]["attemptID"] - readonly location?: Endpoint10_10Request["query"]["location"] + readonly integrationID: Integration.ID + readonly attemptID: Integration.AttemptID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint10_10Output = EffectValue> +export type Endpoint10_10Output = void export type IntegrationCommandCancelOperation = ( input: Endpoint10_10Input, ) => Effect.Effect @@ -537,47 +1066,45 @@ export interface IntegrationApi { } } -type Endpoint11_0Request = Parameters[0] -export type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } -export type Endpoint11_0Output = EffectValue> +export type Endpoint11_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint11_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type McpListOperation = (input?: Endpoint11_0Input) => Effect.Effect -type Endpoint11_1Request = Parameters[0] export type Endpoint11_1Input = { - readonly server: Endpoint11_1Request["params"]["server"] - readonly location?: Endpoint11_1Request["query"]["location"] - readonly config: Endpoint11_1Request["payload"]["config"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly config: Mcp.LocalConfig | Mcp.RemoteConfig } -export type Endpoint11_1Output = EffectValue> +export type Endpoint11_1Output = void export type McpAddOperation = (input: Endpoint11_1Input) => Effect.Effect -type Endpoint11_2Request = Parameters[0] export type Endpoint11_2Input = { - readonly server: Endpoint11_2Request["params"]["server"] - readonly location?: Endpoint11_2Request["query"]["location"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint11_2Output = EffectValue> +export type Endpoint11_2Output = void export type McpRemoveOperation = (input: Endpoint11_2Input) => Effect.Effect -type Endpoint11_3Request = Parameters[0] export type Endpoint11_3Input = { - readonly server: Endpoint11_3Request["params"]["server"] - readonly location?: Endpoint11_3Request["query"]["location"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint11_3Output = EffectValue> +export type Endpoint11_3Output = void export type McpConnectOperation = (input: Endpoint11_3Input) => Effect.Effect -type Endpoint11_4Request = Parameters[0] export type Endpoint11_4Input = { - readonly server: Endpoint11_4Request["params"]["server"] - readonly location?: Endpoint11_4Request["query"]["location"] + readonly server: string + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint11_4Output = EffectValue> +export type Endpoint11_4Output = void export type McpDisconnectOperation = (input: Endpoint11_4Input) => Effect.Effect -type Endpoint11_5Request = Parameters[0] -export type Endpoint11_5Input = { readonly location?: Endpoint11_5Request["query"]["location"] } -export type Endpoint11_5Output = EffectValue> +export type Endpoint11_5Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint11_5Output = { readonly location: Location.Info; readonly data: Mcp.ResourceCatalog } export type McpResourceCatalogOperation = (input?: Endpoint11_5Input) => Effect.Effect export interface McpApi { @@ -589,21 +1116,19 @@ export interface McpApi { readonly resource: { readonly catalog: McpResourceCatalogOperation } } -type Endpoint12_0Request = Parameters[0] export type Endpoint12_0Input = { - readonly credentialID: Endpoint12_0Request["params"]["credentialID"] - readonly location?: Endpoint12_0Request["query"]["location"] - readonly label: Endpoint12_0Request["payload"]["label"] + readonly credentialID: Credential.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly label: string } -export type Endpoint12_0Output = EffectValue> +export type Endpoint12_0Output = void export type CredentialUpdateOperation = (input: Endpoint12_0Input) => Effect.Effect -type Endpoint12_1Request = Parameters[0] export type Endpoint12_1Input = { - readonly credentialID: Endpoint12_1Request["params"]["credentialID"] - readonly location?: Endpoint12_1Request["query"]["location"] + readonly credentialID: Credential.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint12_1Output = EffectValue> +export type Endpoint12_1Output = void export type CredentialRemoveOperation = (input: Endpoint12_1Input) => Effect.Effect export interface CredentialApi { @@ -611,20 +1136,20 @@ export interface CredentialApi { readonly remove: CredentialRemoveOperation } -export type Endpoint13_0Output = EffectValue> +export type Endpoint13_0Output = ReadonlyArray export type ProjectListOperation = () => Effect.Effect -type Endpoint13_1Request = Parameters[0] -export type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } -export type Endpoint13_1Output = EffectValue> +export type Endpoint13_1Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint13_1Output = Project.Current export type ProjectCurrentOperation = (input?: Endpoint13_1Input) => Effect.Effect -type Endpoint13_2Request = Parameters[0] export type Endpoint13_2Input = { - readonly projectID: Endpoint13_2Request["params"]["projectID"] - readonly location?: Endpoint13_2Request["query"]["location"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint13_2Output = EffectValue> +export type Endpoint13_2Output = Project.Directories export type ProjectDirectoriesOperation = (input: Endpoint13_2Input) => Effect.Effect export interface ProjectApi { @@ -633,58 +1158,40 @@ export interface ProjectApi { readonly directories: ProjectDirectoriesOperation } -type Endpoint14_0Request = Parameters[0] -export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } -export type Endpoint14_0Output = EffectValue> +export type Endpoint14_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint14_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type FormRequestListOperation = (input?: Endpoint14_0Input) => Effect.Effect -type Endpoint14_1Request = Parameters[0] -export type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } -export type Endpoint14_1Output = EffectValue>["data"] +export type Endpoint14_1Input = { readonly sessionID: string } +export type Endpoint14_1Output = ReadonlyArray export type FormListOperation = (input: Endpoint14_1Input) => Effect.Effect -type Endpoint14_2Request = Parameters[0] export type Endpoint14_2Input = { - readonly sessionID: Endpoint14_2Request["params"]["sessionID"] - readonly id?: Endpoint14_2Request["payload"]["id"] - readonly title: Endpoint14_2Request["payload"]["title"] - readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly fields: Endpoint14_2Request["payload"]["fields"] + readonly sessionID: string + readonly id?: Form.ID | undefined + readonly title: string + readonly metadata?: Form.Metadata | undefined + readonly fields: Form.Fields } -export type Endpoint14_2Output = EffectValue>["data"] +export type Endpoint14_2Output = Form.Info export type FormCreateOperation = (input: Endpoint14_2Input) => Effect.Effect -type Endpoint14_3Request = Parameters[0] -export type Endpoint14_3Input = { - readonly sessionID: Endpoint14_3Request["params"]["sessionID"] - readonly formID: Endpoint14_3Request["params"]["formID"] -} -export type Endpoint14_3Output = EffectValue>["data"] +export type Endpoint14_3Input = { readonly sessionID: string; readonly formID: Form.ID } +export type Endpoint14_3Output = Form.Info export type FormGetOperation = (input: Endpoint14_3Input) => Effect.Effect -type Endpoint14_4Request = Parameters[0] -export type Endpoint14_4Input = { - readonly sessionID: Endpoint14_4Request["params"]["sessionID"] - readonly formID: Endpoint14_4Request["params"]["formID"] -} -export type Endpoint14_4Output = EffectValue>["data"] +export type Endpoint14_4Input = { readonly sessionID: string; readonly formID: Form.ID } +export type Endpoint14_4Output = Form.State export type FormStateOperation = (input: Endpoint14_4Input) => Effect.Effect -type Endpoint14_5Request = Parameters[0] -export type Endpoint14_5Input = { - readonly sessionID: Endpoint14_5Request["params"]["sessionID"] - readonly formID: Endpoint14_5Request["params"]["formID"] - readonly answer: Endpoint14_5Request["payload"]["answer"] -} -export type Endpoint14_5Output = EffectValue> +export type Endpoint14_5Input = { readonly sessionID: string; readonly formID: Form.ID; readonly answer: Form.Answer } +export type Endpoint14_5Output = void export type FormReplyOperation = (input: Endpoint14_5Input) => Effect.Effect -type Endpoint14_6Request = Parameters[0] -export type Endpoint14_6Input = { - readonly sessionID: Endpoint14_6Request["params"]["sessionID"] - readonly formID: Endpoint14_6Request["params"]["formID"] -} -export type Endpoint14_6Output = EffectValue> +export type Endpoint14_6Input = { readonly sessionID: string; readonly formID: Form.ID } +export type Endpoint14_6Output = void export type FormCancelOperation = (input: Endpoint14_6Input) => Effect.Effect export interface FormApi { @@ -697,70 +1204,54 @@ export interface FormApi { readonly cancel: FormCancelOperation } -type Endpoint15_0Request = Parameters[0] -export type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } -export type Endpoint15_0Output = EffectValue> +export type Endpoint15_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint15_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type PermissionRequestListOperation = ( input?: Endpoint15_0Input, ) => Effect.Effect -type Endpoint15_1Request = Parameters[0] -export type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } -export type Endpoint15_1Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_1Input = { readonly projectID?: Project.ID | undefined } +export type Endpoint15_1Output = ReadonlyArray export type PermissionSavedListOperation = ( input?: Endpoint15_1Input, ) => Effect.Effect -type Endpoint15_2Request = Parameters[0] -export type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } -export type Endpoint15_2Output = EffectValue> +export type Endpoint15_2Input = { readonly id: PermissionSaved.ID } +export type Endpoint15_2Output = void export type PermissionSavedRemoveOperation = ( input: Endpoint15_2Input, ) => Effect.Effect -type Endpoint15_3Request = Parameters[0] export type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly id?: Endpoint15_3Request["payload"]["id"] - readonly action: Endpoint15_3Request["payload"]["action"] - readonly resources: Endpoint15_3Request["payload"]["resources"] - readonly save?: Endpoint15_3Request["payload"]["save"] - readonly metadata?: Endpoint15_3Request["payload"]["metadata"] - readonly source?: Endpoint15_3Request["payload"]["source"] - readonly agent?: Endpoint15_3Request["payload"]["agent"] + readonly sessionID: Session.ID + readonly id?: Permission.ID | undefined + readonly action: string + readonly resources: ReadonlyArray + readonly save?: ReadonlyArray | undefined + readonly metadata?: { readonly [x: string]: unknown } | undefined + readonly source?: Permission.Source | undefined + readonly agent?: Agent.ID | undefined } -export type Endpoint15_3Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_3Output = { readonly id: Permission.ID; readonly effect: Permission.Effect } export type PermissionCreateOperation = (input: Endpoint15_3Input) => Effect.Effect -type Endpoint15_4Request = Parameters[0] -export type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } -export type Endpoint15_4Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_4Input = { readonly sessionID: Session.ID } +export type Endpoint15_4Output = ReadonlyArray export type PermissionListOperation = (input: Endpoint15_4Input) => Effect.Effect -type Endpoint15_5Request = Parameters[0] -export type Endpoint15_5Input = { - readonly sessionID: Endpoint15_5Request["params"]["sessionID"] - readonly requestID: Endpoint15_5Request["params"]["requestID"] -} -export type Endpoint15_5Output = EffectValue< - ReturnType ->["data"] +export type Endpoint15_5Input = { readonly sessionID: Session.ID; readonly requestID: Permission.ID } +export type Endpoint15_5Output = Permission.Request export type PermissionGetOperation = (input: Endpoint15_5Input) => Effect.Effect -type Endpoint15_6Request = Parameters[0] export type Endpoint15_6Input = { - readonly sessionID: Endpoint15_6Request["params"]["sessionID"] - readonly requestID: Endpoint15_6Request["params"]["requestID"] - readonly reply: Endpoint15_6Request["payload"]["reply"] - readonly message?: Endpoint15_6Request["payload"]["message"] + readonly sessionID: Session.ID + readonly requestID: Permission.ID + readonly reply: Permission.Reply + readonly message?: string | undefined } -export type Endpoint15_6Output = EffectValue> +export type Endpoint15_6Output = void export type PermissionReplyOperation = (input: Endpoint15_6Input) => Effect.Effect export interface PermissionApi { @@ -772,22 +1263,20 @@ export interface PermissionApi { readonly reply: PermissionReplyOperation } -type Endpoint16_0Request = Parameters[0] export type Endpoint16_0Input = { - readonly location?: Endpoint16_0Request["query"]["location"] - readonly path?: Endpoint16_0Request["query"]["path"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly path?: RelativePath | undefined } -export type Endpoint16_0Output = EffectValue> +export type Endpoint16_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type FileListOperation = (input?: Endpoint16_0Input) => Effect.Effect -type Endpoint16_1Request = Parameters[0] export type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly query: Endpoint16_1Request["query"]["query"] - readonly type?: Endpoint16_1Request["query"]["type"] - readonly limit?: Endpoint16_1Request["query"]["limit"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly type?: "file" | "directory" | undefined + readonly limit?: number | undefined } -export type Endpoint16_1Output = EffectValue> +export type Endpoint16_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type FileFindOperation = (input: Endpoint16_1Input) => Effect.Effect export interface FileApi { @@ -795,72 +1284,71 @@ export interface FileApi { readonly find: FileFindOperation } -type Endpoint17_0Request = Parameters[0] -export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } -export type Endpoint17_0Output = EffectValue> +export type Endpoint17_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint17_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type CommandListOperation = (input?: Endpoint17_0Input) => Effect.Effect export interface CommandApi { readonly list: CommandListOperation } -type Endpoint18_0Request = Parameters[0] -export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } -export type Endpoint18_0Output = EffectValue> +export type Endpoint18_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint18_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type SkillListOperation = (input?: Endpoint18_0Input) => Effect.Effect export interface SkillApi { readonly list: SkillListOperation } -export type Endpoint19_0Output = StreamValue>> +export type Endpoint19_0Output = OpenCodeEvent export type EventSubscribeOperation = () => Stream.Stream export interface EventApi { readonly subscribe: EventSubscribeOperation } -type Endpoint20_0Request = Parameters[0] -export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } -export type Endpoint20_0Output = EffectValue> +export type Endpoint20_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint20_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type PtyListOperation = (input?: Endpoint20_0Input) => Effect.Effect -type Endpoint20_1Request = Parameters[0] export type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly command?: string | undefined + readonly args?: ReadonlyArray | undefined + readonly cwd?: string | undefined + readonly title?: string | undefined + readonly env?: { readonly [x: string]: string } | undefined } -export type Endpoint20_1Output = EffectValue> +export type Endpoint20_1Output = { readonly location: Location.Info; readonly data: Pty.Info } export type PtyCreateOperation = (input?: Endpoint20_1Input) => Effect.Effect -type Endpoint20_2Request = Parameters[0] export type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] + readonly ptyID: Pty.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint20_2Output = EffectValue> +export type Endpoint20_2Output = { readonly location: Location.Info; readonly data: Pty.Info } export type PtyGetOperation = (input: Endpoint20_2Input) => Effect.Effect -type Endpoint20_3Request = Parameters[0] export type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] + readonly ptyID: Pty.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly title?: string | undefined + readonly size?: { readonly rows: number; readonly cols: number } | undefined } -export type Endpoint20_3Output = EffectValue> +export type Endpoint20_3Output = { readonly location: Location.Info; readonly data: Pty.Info } export type PtyUpdateOperation = (input: Endpoint20_3Input) => Effect.Effect -type Endpoint20_4Request = Parameters[0] export type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] + readonly ptyID: Pty.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint20_4Output = EffectValue> +export type Endpoint20_4Output = void export type PtyRemoveOperation = (input: Endpoint20_4Input) => Effect.Effect export interface PtyApi { @@ -871,55 +1359,59 @@ export interface PtyApi { readonly remove: PtyRemoveOperation } -type Endpoint21_0Request = Parameters[0] -export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } -export type Endpoint21_0Output = EffectValue> +export type Endpoint21_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint21_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ShellListOperation = (input?: Endpoint21_0Input) => Effect.Effect -type Endpoint21_1Request = Parameters[0] export type Endpoint21_1Input = { - readonly location?: Endpoint21_1Request["query"]["location"] - readonly command: Endpoint21_1Request["payload"]["command"] - readonly cwd?: Endpoint21_1Request["payload"]["cwd"] - readonly timeout: Endpoint21_1Request["payload"]["timeout"] - readonly metadata?: Endpoint21_1Request["payload"]["metadata"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly command: string + readonly cwd?: string | undefined + readonly timeout: number + readonly metadata?: { readonly [x: string]: unknown } | undefined } -export type Endpoint21_1Output = EffectValue> +export type Endpoint21_1Output = { readonly location: Location.Info; readonly data: Shell.Info } export type ShellCreateOperation = (input: Endpoint21_1Input) => Effect.Effect -type Endpoint21_2Request = Parameters[0] export type Endpoint21_2Input = { - readonly id: Endpoint21_2Request["params"]["id"] - readonly location?: Endpoint21_2Request["query"]["location"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint21_2Output = EffectValue> +export type Endpoint21_2Output = { readonly location: Location.Info; readonly data: Shell.Info } export type ShellGetOperation = (input: Endpoint21_2Input) => Effect.Effect -type Endpoint21_3Request = Parameters[0] export type Endpoint21_3Input = { - readonly id: Endpoint21_3Request["params"]["id"] - readonly location?: Endpoint21_3Request["query"]["location"] - readonly timeout: Endpoint21_3Request["payload"]["timeout"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly timeout: number } -export type Endpoint21_3Output = EffectValue> +export type Endpoint21_3Output = { readonly location: Location.Info; readonly data: Shell.Info } export type ShellTimeoutOperation = (input: Endpoint21_3Input) => Effect.Effect -type Endpoint21_4Request = Parameters[0] export type Endpoint21_4Input = { - readonly id: Endpoint21_4Request["params"]["id"] - readonly location?: Endpoint21_4Request["query"]["location"] - readonly cursor?: Endpoint21_4Request["query"]["cursor"] - readonly limit?: Endpoint21_4Request["query"]["limit"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly cursor?: number | undefined + readonly limit?: number | undefined +} +export type Endpoint21_4Output = { + readonly location: Location.Info + readonly data: { + readonly output: string + readonly cursor: number + readonly size: number + readonly truncated: boolean + } } -export type Endpoint21_4Output = EffectValue> export type ShellOutputOperation = (input: Endpoint21_4Input) => Effect.Effect -type Endpoint21_5Request = Parameters[0] export type Endpoint21_5Input = { - readonly id: Endpoint21_5Request["params"]["id"] - readonly location?: Endpoint21_5Request["query"]["location"] + readonly id: Shell.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint21_5Output = EffectValue> +export type Endpoint21_5Output = void export type ShellRemoveOperation = (input: Endpoint21_5Input) => Effect.Effect export interface ShellApi { @@ -931,33 +1423,28 @@ export interface ShellApi { readonly remove: ShellRemoveOperation } -type Endpoint22_0Request = Parameters[0] -export type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } -export type Endpoint22_0Output = EffectValue> +export type Endpoint22_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type QuestionRequestListOperation = ( input?: Endpoint22_0Input, ) => Effect.Effect -type Endpoint22_1Request = Parameters[0] -export type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } -export type Endpoint22_1Output = EffectValue>["data"] +export type Endpoint22_1Input = { readonly sessionID: Session.ID } +export type Endpoint22_1Output = ReadonlyArray export type QuestionListOperation = (input: Endpoint22_1Input) => Effect.Effect -type Endpoint22_2Request = Parameters[0] export type Endpoint22_2Input = { - readonly sessionID: Endpoint22_2Request["params"]["sessionID"] - readonly requestID: Endpoint22_2Request["params"]["requestID"] - readonly answers: Endpoint22_2Request["payload"]["answers"] + readonly sessionID: Session.ID + readonly requestID: Question.ID + readonly answers: ReadonlyArray } -export type Endpoint22_2Output = EffectValue> +export type Endpoint22_2Output = void export type QuestionReplyOperation = (input: Endpoint22_2Input) => Effect.Effect -type Endpoint22_3Request = Parameters[0] -export type Endpoint22_3Input = { - readonly sessionID: Endpoint22_3Request["params"]["sessionID"] - readonly requestID: Endpoint22_3Request["params"]["requestID"] -} -export type Endpoint22_3Output = EffectValue> +export type Endpoint22_3Input = { readonly sessionID: Session.ID; readonly requestID: Question.ID } +export type Endpoint22_3Output = void export type QuestionRejectOperation = (input: Endpoint22_3Input) => Effect.Effect export interface QuestionApi { @@ -967,42 +1454,40 @@ export interface QuestionApi { readonly reject: QuestionRejectOperation } -type Endpoint23_0Request = Parameters[0] -export type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } -export type Endpoint23_0Output = EffectValue> +export type Endpoint23_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type ReferenceListOperation = (input?: Endpoint23_0Input) => Effect.Effect export interface ReferenceApi { readonly list: ReferenceListOperation } -type Endpoint24_0Request = Parameters[0] export type Endpoint24_0Input = { - readonly projectID: Endpoint24_0Request["params"]["projectID"] - readonly location?: Endpoint24_0Request["query"]["location"] - readonly strategy: Endpoint24_0Request["payload"]["strategy"] - readonly directory: Endpoint24_0Request["payload"]["directory"] - readonly name?: Endpoint24_0Request["payload"]["name"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly strategy: ProjectCopy.StrategyID + readonly directory: AbsolutePath + readonly name?: string | undefined } -export type Endpoint24_0Output = EffectValue> +export type Endpoint24_0Output = ProjectCopy.Copy export type ProjectCopyCreateOperation = (input: Endpoint24_0Input) => Effect.Effect -type Endpoint24_1Request = Parameters[0] export type Endpoint24_1Input = { - readonly projectID: Endpoint24_1Request["params"]["projectID"] - readonly location?: Endpoint24_1Request["query"]["location"] - readonly directory: Endpoint24_1Request["payload"]["directory"] - readonly force: Endpoint24_1Request["payload"]["force"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly directory: AbsolutePath + readonly force: boolean } -export type Endpoint24_1Output = EffectValue> +export type Endpoint24_1Output = void export type ProjectCopyRemoveOperation = (input: Endpoint24_1Input) => Effect.Effect -type Endpoint24_2Request = Parameters[0] export type Endpoint24_2Input = { - readonly projectID: Endpoint24_2Request["params"]["projectID"] - readonly location?: Endpoint24_2Request["query"]["location"] + readonly projectID: Project.ID + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint24_2Output = EffectValue> +export type Endpoint24_2Output = void export type ProjectCopyRefreshOperation = (input: Endpoint24_2Input) => Effect.Effect export interface ProjectCopyApi { @@ -1011,18 +1496,18 @@ export interface ProjectCopyApi { readonly refresh: ProjectCopyRefreshOperation } -type Endpoint25_0Request = Parameters[0] -export type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } -export type Endpoint25_0Output = EffectValue> +export type Endpoint25_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type VcsStatusOperation = (input?: Endpoint25_0Input) => Effect.Effect -type Endpoint25_1Request = Parameters[0] export type Endpoint25_1Input = { - readonly location?: Endpoint25_1Request["query"]["location"] - readonly mode: Endpoint25_1Request["query"]["mode"] - readonly context?: Endpoint25_1Request["query"]["context"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly mode: Vcs.Mode + readonly context?: number | undefined } -export type Endpoint25_1Output = EffectValue> +export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type VcsDiffOperation = (input: Endpoint25_1Input) => Effect.Effect export interface VcsApi { @@ -1030,30 +1515,31 @@ export interface VcsApi { readonly diff: VcsDiffOperation } -export type Endpoint26_0Output = EffectValue> +export type Endpoint26_0Output = ReadonlyArray export type DebugLocationListOperation = () => Effect.Effect -type Endpoint26_1Request = Parameters[0] -export type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } -export type Endpoint26_1Output = EffectValue> +export type Endpoint26_1Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint26_1Output = void export type DebugLocationEvictOperation = (input?: Endpoint26_1Input) => Effect.Effect export interface DebugApi { readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } } -type Endpoint27_0Request = Parameters[0] -export type Endpoint27_0Input = { readonly location?: Endpoint27_0Request["query"]["location"] } -export type Endpoint27_0Output = EffectValue> +export type Endpoint27_0Input = { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined +} +export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } export type WebsearchProvidersOperation = (input?: Endpoint27_0Input) => Effect.Effect -type Endpoint27_1Request = Parameters[0] export type Endpoint27_1Input = { - readonly location?: Endpoint27_1Request["query"]["location"] - readonly query: Endpoint27_1Request["payload"]["query"] - readonly providerID?: Endpoint27_1Request["payload"]["providerID"] + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + readonly query: string + readonly providerID?: WebSearch.ID | undefined } -export type Endpoint27_1Output = EffectValue> +export type Endpoint27_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response } export type WebsearchQueryOperation = (input: Endpoint27_1Input) => Effect.Effect export interface WebsearchApi { diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index ce791ce4f17..8b457db56fd 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -4,6 +4,220 @@ import { Sse } from "effect/unstable/encoding" import { HttpClientError } from "effect/unstable/http" import { HttpApiClient } from "effect/unstable/httpapi" import { ClientApi } from "../../contract" +import type { + Endpoint0_0Output, + Endpoint0_1Input, + Endpoint0_1Output, + Endpoint1_0Output, + Endpoint2_0Input, + Endpoint2_0Output, + Endpoint3_0Input, + Endpoint3_0Output, + Endpoint3_1Input, + Endpoint3_1Output, + Endpoint4_0Input, + Endpoint4_0Output, + Endpoint5_0Input, + Endpoint5_0Output, + Endpoint5_1Input, + Endpoint5_1Output, + Endpoint5_2Output, + Endpoint5_3Input, + Endpoint5_3Output, + Endpoint5_4Input, + Endpoint5_4Output, + Endpoint5_5Input, + Endpoint5_5Output, + Endpoint5_6Input, + Endpoint5_6Output, + Endpoint5_7Input, + Endpoint5_7Output, + Endpoint5_8Input, + Endpoint5_8Output, + Endpoint5_9Input, + Endpoint5_9Output, + Endpoint5_10Input, + Endpoint5_10Output, + Endpoint5_11Input, + Endpoint5_11Output, + Endpoint5_12Input, + Endpoint5_12Output, + Endpoint5_13Input, + Endpoint5_13Output, + Endpoint5_14Input, + Endpoint5_14Output, + Endpoint5_15Input, + Endpoint5_15Output, + Endpoint5_16Input, + Endpoint5_16Output, + Endpoint5_17Input, + Endpoint5_17Output, + Endpoint5_18Input, + Endpoint5_18Output, + Endpoint5_19Input, + Endpoint5_19Output, + Endpoint5_20Input, + Endpoint5_20Output, + Endpoint5_21Input, + Endpoint5_21Output, + Endpoint5_22Input, + Endpoint5_22Output, + Endpoint5_23Input, + Endpoint5_23Output, + Endpoint5_24Input, + Endpoint5_24Output, + Endpoint5_25Input, + Endpoint5_25Output, + Endpoint5_26Input, + Endpoint5_26Output, + Endpoint5_27Input, + Endpoint5_27Output, + Endpoint5_28Input, + Endpoint5_28Output, + Endpoint5_29Input, + Endpoint5_29Output, + Endpoint6_0Input, + Endpoint6_0Output, + Endpoint7_0Input, + Endpoint7_0Output, + Endpoint7_1Input, + Endpoint7_1Output, + Endpoint8_0Input, + Endpoint8_0Output, + Endpoint9_0Input, + Endpoint9_0Output, + Endpoint9_1Input, + Endpoint9_1Output, + Endpoint10_0Input, + Endpoint10_0Output, + Endpoint10_1Input, + Endpoint10_1Output, + Endpoint10_2Input, + Endpoint10_2Output, + Endpoint10_3Input, + Endpoint10_3Output, + Endpoint10_4Input, + Endpoint10_4Output, + Endpoint10_5Input, + Endpoint10_5Output, + Endpoint10_6Input, + Endpoint10_6Output, + Endpoint10_7Input, + Endpoint10_7Output, + Endpoint10_8Input, + Endpoint10_8Output, + Endpoint10_9Input, + Endpoint10_9Output, + Endpoint10_10Input, + Endpoint10_10Output, + Endpoint11_0Input, + Endpoint11_0Output, + Endpoint11_1Input, + Endpoint11_1Output, + Endpoint11_2Input, + Endpoint11_2Output, + Endpoint11_3Input, + Endpoint11_3Output, + Endpoint11_4Input, + Endpoint11_4Output, + Endpoint11_5Input, + Endpoint11_5Output, + Endpoint12_0Input, + Endpoint12_0Output, + Endpoint12_1Input, + Endpoint12_1Output, + Endpoint13_0Output, + Endpoint13_1Input, + Endpoint13_1Output, + Endpoint13_2Input, + Endpoint13_2Output, + Endpoint14_0Input, + Endpoint14_0Output, + Endpoint14_1Input, + Endpoint14_1Output, + Endpoint14_2Input, + Endpoint14_2Output, + Endpoint14_3Input, + Endpoint14_3Output, + Endpoint14_4Input, + Endpoint14_4Output, + Endpoint14_5Input, + Endpoint14_5Output, + Endpoint14_6Input, + Endpoint14_6Output, + Endpoint15_0Input, + Endpoint15_0Output, + Endpoint15_1Input, + Endpoint15_1Output, + Endpoint15_2Input, + Endpoint15_2Output, + Endpoint15_3Input, + Endpoint15_3Output, + Endpoint15_4Input, + Endpoint15_4Output, + Endpoint15_5Input, + Endpoint15_5Output, + Endpoint15_6Input, + Endpoint15_6Output, + Endpoint16_0Input, + Endpoint16_0Output, + Endpoint16_1Input, + Endpoint16_1Output, + Endpoint17_0Input, + Endpoint17_0Output, + Endpoint18_0Input, + Endpoint18_0Output, + Endpoint19_0Output, + Endpoint20_0Input, + Endpoint20_0Output, + Endpoint20_1Input, + Endpoint20_1Output, + Endpoint20_2Input, + Endpoint20_2Output, + Endpoint20_3Input, + Endpoint20_3Output, + Endpoint20_4Input, + Endpoint20_4Output, + Endpoint21_0Input, + Endpoint21_0Output, + Endpoint21_1Input, + Endpoint21_1Output, + Endpoint21_2Input, + Endpoint21_2Output, + Endpoint21_3Input, + Endpoint21_3Output, + Endpoint21_4Input, + Endpoint21_4Output, + Endpoint21_5Input, + Endpoint21_5Output, + Endpoint22_0Input, + Endpoint22_0Output, + Endpoint22_1Input, + Endpoint22_1Output, + Endpoint22_2Input, + Endpoint22_2Output, + Endpoint22_3Input, + Endpoint22_3Output, + Endpoint23_0Input, + Endpoint23_0Output, + Endpoint24_0Input, + Endpoint24_0Output, + Endpoint24_1Input, + Endpoint24_1Output, + Endpoint24_2Input, + Endpoint24_2Output, + Endpoint25_0Input, + Endpoint25_0Output, + Endpoint25_1Input, + Endpoint25_1Output, + Endpoint26_0Output, + Endpoint26_1Input, + Endpoint26_1Output, + Endpoint27_0Input, + Endpoint27_0Output, + Endpoint27_1Input, + Endpoint27_1Output, +} from "../api/api.js" import { ClientError } from "./client-error" type RawClient = HttpApiClient.ForApi @@ -13,401 +227,327 @@ const mapClientError = (error: E) => ? new ClientError({ cause: error }) : error -const Endpoint0_0 = (raw: RawClient["server.health"]) => () => - raw["health.get"]({}).pipe(Effect.mapError(mapClientError)) +const preserveEffect = + () => + (effect: Effect.Effect) => + effect +const preserveStream = + () => + (stream: Stream.Stream) => + stream + +const Endpoint0_0 = (raw: RawClient["server.health"]) => () => + preserveEffect()(raw["health.get"]({}).pipe(Effect.mapError(mapClientError))) -type Endpoint0_1Request = Parameters[0] -type Endpoint0_1Input = { readonly instanceID: Endpoint0_1Request["payload"]["instanceID"] } const Endpoint0_1 = (raw: RawClient["server.health"]) => (input: Endpoint0_1Input) => - raw["health.stop"]({ payload: { instanceID: input["instanceID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["health.stop"]({ payload: { instanceID: input["instanceID"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw), stop: Endpoint0_1(raw) }) const Endpoint1_0 = (raw: RawClient["server.server"]) => () => - raw["server.get"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["server.get"]({}).pipe(Effect.mapError(mapClientError))) const adaptGroup1 = (raw: RawClient["server.server"]) => ({ get: Endpoint1_0(raw) }) -type Endpoint2_0Request = Parameters[0] -type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] } const Endpoint2_0 = (raw: RawClient["server.location"]) => (input?: Endpoint2_0Input) => - raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup2 = (raw: RawClient["server.location"]) => ({ get: Endpoint2_0(raw) }) -type Endpoint3_0Request = Parameters[0] -type Endpoint3_0Input = { readonly location?: Endpoint3_0Request["query"]["location"] } const Endpoint3_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint3_0Input) => - raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw) }) - -type Endpoint4_0Request = Parameters[0] -type Endpoint4_0Input = { readonly location?: Endpoint4_0Request["query"]["location"] } -const Endpoint4_0 = (raw: RawClient["server.plugin"]) => (input?: Endpoint4_0Input) => - raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -const adaptGroup4 = (raw: RawClient["server.plugin"]) => ({ list: Endpoint4_0(raw) }) - -type Endpoint5_0Request = Parameters[0] -type Endpoint5_0Input = { - readonly workspace?: Endpoint5_0Request["query"]["workspace"] - readonly limit?: Endpoint5_0Request["query"]["limit"] - readonly order?: Endpoint5_0Request["query"]["order"] - readonly search?: Endpoint5_0Request["query"]["search"] - readonly parentID?: Endpoint5_0Request["query"]["parentID"] - readonly directory?: Endpoint5_0Request["query"]["directory"] - readonly project?: Endpoint5_0Request["query"]["project"] - readonly subpath?: Endpoint5_0Request["query"]["subpath"] - readonly cursor?: Endpoint5_0Request["query"]["cursor"] -} -const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0Input) => - raw["session.list"]({ - query: { - workspace: input?.["workspace"], - limit: input?.["limit"], - order: input?.["order"], - search: input?.["search"], - parentID: input?.["parentID"], - directory: input?.["directory"], - project: input?.["project"], - subpath: input?.["subpath"], - cursor: input?.["cursor"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_1Request = Parameters[0] -type Endpoint5_1Input = { - readonly id?: Endpoint5_1Request["payload"]["id"] - readonly agent?: Endpoint5_1Request["payload"]["agent"] - readonly model?: Endpoint5_1Request["payload"]["model"] - readonly location?: Endpoint5_1Request["payload"]["location"] -} -const Endpoint5_1 = (raw: RawClient["server.session"]) => (input?: Endpoint5_1Input) => - raw["session.create"]({ - payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const Endpoint5_2 = (raw: RawClient["server.session"]) => () => - raw["session.active"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_3Request = Parameters[0] -type Endpoint5_3Input = { readonly sessionID: Endpoint5_3Request["params"]["sessionID"] } -const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) => - raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_4Request = Parameters[0] -type Endpoint5_4Input = { readonly sessionID: Endpoint5_4Request["params"]["sessionID"] } -const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Input) => - raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_5Request = Parameters[0] -type Endpoint5_5Input = { - readonly sessionID: Endpoint5_5Request["params"]["sessionID"] - readonly messageID?: Endpoint5_5Request["payload"]["messageID"] -} -const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) => - raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_6Request = Parameters[0] -type Endpoint5_6Input = { - readonly sessionID: Endpoint5_6Request["params"]["sessionID"] - readonly agent: Endpoint5_6Request["payload"]["agent"] -} -const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) => - raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_7Request = Parameters[0] -type Endpoint5_7Input = { - readonly sessionID: Endpoint5_7Request["params"]["sessionID"] - readonly model: Endpoint5_7Request["payload"]["model"] -} -const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) => - raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_8Request = Parameters[0] -type Endpoint5_8Input = { - readonly sessionID: Endpoint5_8Request["params"]["sessionID"] - readonly title: Endpoint5_8Request["payload"]["title"] -} -const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) => - raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_9Request = Parameters[0] -type Endpoint5_9Input = { - readonly sessionID: Endpoint5_9Request["params"]["sessionID"] - readonly directory: Endpoint5_9Request["payload"]["directory"] - readonly workspaceID?: Endpoint5_9Request["payload"]["workspaceID"] -} -const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) => - raw["session.move"]({ - params: { sessionID: input["sessionID"] }, - payload: { directory: input["directory"], workspaceID: input["workspaceID"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_10Request = Parameters[0] -type Endpoint5_10Input = { - readonly sessionID: Endpoint5_10Request["params"]["sessionID"] - readonly id?: Endpoint5_10Request["payload"]["id"] - readonly text: Endpoint5_10Request["payload"]["text"] - readonly files?: Endpoint5_10Request["payload"]["files"] - readonly agents?: Endpoint5_10Request["payload"]["agents"] - readonly metadata?: Endpoint5_10Request["payload"]["metadata"] - readonly delivery?: Endpoint5_10Request["payload"]["delivery"] - readonly resume?: Endpoint5_10Request["payload"]["resume"] -} -const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) => - raw["session.prompt"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - text: input["text"], - files: input["files"], - agents: input["agents"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_11Request = Parameters[0] -type Endpoint5_11Input = { - readonly sessionID: Endpoint5_11Request["params"]["sessionID"] - readonly id?: Endpoint5_11Request["payload"]["id"] - readonly command: Endpoint5_11Request["payload"]["command"] - readonly arguments?: Endpoint5_11Request["payload"]["arguments"] - readonly agent?: Endpoint5_11Request["payload"]["agent"] - readonly model?: Endpoint5_11Request["payload"]["model"] - readonly files?: Endpoint5_11Request["payload"]["files"] - readonly agents?: Endpoint5_11Request["payload"]["agents"] - readonly delivery?: Endpoint5_11Request["payload"]["delivery"] - readonly resume?: Endpoint5_11Request["payload"]["resume"] -} -const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) => - raw["session.command"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - command: input["command"], - arguments: input["arguments"], - agent: input["agent"], - model: input["model"], - files: input["files"], - agents: input["agents"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_12Request = Parameters[0] -type Endpoint5_12Input = { - readonly sessionID: Endpoint5_12Request["params"]["sessionID"] - readonly id?: Endpoint5_12Request["payload"]["id"] - readonly skill: Endpoint5_12Request["payload"]["skill"] - readonly resume?: Endpoint5_12Request["payload"]["resume"] -} -const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) => - raw["session.skill"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_13Request = Parameters[0] -type Endpoint5_13Input = { - readonly sessionID: Endpoint5_13Request["params"]["sessionID"] - readonly id?: Endpoint5_13Request["payload"]["id"] - readonly text: Endpoint5_13Request["payload"]["text"] - readonly description?: Endpoint5_13Request["payload"]["description"] - readonly metadata?: Endpoint5_13Request["payload"]["metadata"] - readonly delivery?: Endpoint5_13Request["payload"]["delivery"] - readonly resume?: Endpoint5_13Request["payload"]["resume"] -} -const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) => - raw["session.synthetic"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - text: input["text"], - description: input["description"], - metadata: input["metadata"], - delivery: input["delivery"], - resume: input["resume"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_14Request = Parameters[0] -type Endpoint5_14Input = { - readonly sessionID: Endpoint5_14Request["params"]["sessionID"] - readonly id?: Endpoint5_14Request["payload"]["id"] - readonly command: Endpoint5_14Request["payload"]["command"] -} -const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) => - raw["session.shell"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], command: input["command"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_15Request = Parameters[0] -type Endpoint5_15Input = { - readonly sessionID: Endpoint5_15Request["params"]["sessionID"] - readonly id?: Endpoint5_15Request["payload"]["id"] -} -const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) => - raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_16Request = Parameters[0] -type Endpoint5_16Input = { readonly sessionID: Endpoint5_16Request["params"]["sessionID"] } -const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) => - raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_17Request = Parameters[0] -type Endpoint5_17Input = { - readonly sessionID: Endpoint5_17Request["params"]["sessionID"] - readonly messageID: Endpoint5_17Request["payload"]["messageID"] - readonly files?: Endpoint5_17Request["payload"]["files"] -} -const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) => - raw["session.revert.stage"]({ - params: { sessionID: input["sessionID"] }, - payload: { messageID: input["messageID"], files: input["files"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_18Request = Parameters[0] -type Endpoint5_18Input = { readonly sessionID: Endpoint5_18Request["params"]["sessionID"] } -const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) => - raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_19Request = Parameters[0] -type Endpoint5_19Input = { readonly sessionID: Endpoint5_19Request["params"]["sessionID"] } -const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) => - raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_20Request = Parameters[0] -type Endpoint5_20Input = { readonly sessionID: Endpoint5_20Request["params"]["sessionID"] } -const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) => - raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_21Request = Parameters[0] -type Endpoint5_21Input = { readonly sessionID: Endpoint5_21Request["params"]["sessionID"] } -const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) => - raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_22Request = Parameters[0] -type Endpoint5_22Input = { readonly sessionID: Endpoint5_22Request["params"]["sessionID"] } -const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) => - raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_23Request = Parameters[0] -type Endpoint5_23Input = { - readonly sessionID: Endpoint5_23Request["params"]["sessionID"] - readonly key: Endpoint5_23Request["params"]["key"] - readonly value: Endpoint5_23Request["payload"]["value"] -} -const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) => - raw["session.instructions.entry.put"]({ - params: { sessionID: input["sessionID"], key: input["key"] }, - payload: { value: input["value"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint5_24Request = Parameters[0] -type Endpoint5_24Input = { - readonly sessionID: Endpoint5_24Request["params"]["sessionID"] - readonly key: Endpoint5_24Request["params"]["key"] -} -const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => - raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( - Effect.mapError(mapClientError), - ) - -type Endpoint5_25Request = Parameters[0] -type Endpoint5_25Input = { - readonly sessionID: Endpoint5_25Request["params"]["sessionID"] - readonly prompt: Endpoint5_25Request["payload"]["prompt"] -} -const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => - raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ) - -type Endpoint5_26Request = Parameters[0] -type Endpoint5_26Input = { - readonly sessionID: Endpoint5_26Request["params"]["sessionID"] - readonly after?: Endpoint5_26Request["query"]["after"] - readonly follow?: Endpoint5_26Request["query"]["follow"] -} -const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => - Stream.unwrap( - raw["session.log"]({ - params: { sessionID: input["sessionID"] }, - query: { after: input["after"], follow: input["follow"] }, - }).pipe( +const Endpoint3_1 = (raw: RawClient["server.agent"]) => (input: Endpoint3_1Input) => + preserveEffect()( + raw["agent.get"]({ params: { agentID: input["agentID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), ), ) -type Endpoint5_27Request = Parameters[0] -type Endpoint5_27Input = { readonly sessionID: Endpoint5_27Request["params"]["sessionID"] } +const adaptGroup3 = (raw: RawClient["server.agent"]) => ({ list: Endpoint3_0(raw), get: Endpoint3_1(raw) }) + +const Endpoint4_0 = (raw: RawClient["server.plugin"]) => (input?: Endpoint4_0Input) => + preserveEffect()( + raw["plugin.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const adaptGroup4 = (raw: RawClient["server.plugin"]) => ({ list: Endpoint4_0(raw) }) + +const Endpoint5_0 = (raw: RawClient["server.session"]) => (input?: Endpoint5_0Input) => + preserveEffect()( + raw["session.list"]({ + query: { + workspace: input?.["workspace"], + limit: input?.["limit"], + order: input?.["order"], + search: input?.["search"], + parentID: input?.["parentID"], + directory: input?.["directory"], + project: input?.["project"], + subpath: input?.["subpath"], + cursor: input?.["cursor"], + }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_1 = (raw: RawClient["server.session"]) => (input?: Endpoint5_1Input) => + preserveEffect()( + raw["session.create"]({ + payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_2 = (raw: RawClient["server.session"]) => () => + preserveEffect()( + raw["session.active"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_3 = (raw: RawClient["server.session"]) => (input: Endpoint5_3Input) => + preserveEffect()( + raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_4 = (raw: RawClient["server.session"]) => (input: Endpoint5_4Input) => + preserveEffect()( + raw["session.remove"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_5 = (raw: RawClient["server.session"]) => (input: Endpoint5_5Input) => + preserveEffect()( + raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_6 = (raw: RawClient["server.session"]) => (input: Endpoint5_6Input) => + preserveEffect()( + raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const Endpoint5_7 = (raw: RawClient["server.session"]) => (input: Endpoint5_7Input) => + preserveEffect()( + raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const Endpoint5_8 = (raw: RawClient["server.session"]) => (input: Endpoint5_8Input) => + preserveEffect()( + raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const Endpoint5_9 = (raw: RawClient["server.session"]) => (input: Endpoint5_9Input) => + preserveEffect()( + raw["session.move"]({ + params: { sessionID: input["sessionID"] }, + payload: { directory: input["directory"], workspaceID: input["workspaceID"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_10 = (raw: RawClient["server.session"]) => (input: Endpoint5_10Input) => + preserveEffect()( + raw["session.prompt"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + text: input["text"], + files: input["files"], + agents: input["agents"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11Input) => + preserveEffect()( + raw["session.command"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + command: input["command"], + arguments: input["arguments"], + agent: input["agent"], + model: input["model"], + files: input["files"], + agents: input["agents"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12Input) => + preserveEffect()( + raw["session.skill"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], skill: input["skill"], resume: input["resume"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13Input) => + preserveEffect()( + raw["session.synthetic"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + text: input["text"], + description: input["description"], + metadata: input["metadata"], + delivery: input["delivery"], + resume: input["resume"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_14 = (raw: RawClient["server.session"]) => (input: Endpoint5_14Input) => + preserveEffect()( + raw["session.shell"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], command: input["command"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_15 = (raw: RawClient["server.session"]) => (input: Endpoint5_15Input) => + preserveEffect()( + raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16Input) => + preserveEffect()( + raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) => + preserveEffect()( + raw["session.revert.stage"]({ + params: { sessionID: input["sessionID"] }, + payload: { messageID: input["messageID"], files: input["files"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_18 = (raw: RawClient["server.session"]) => (input: Endpoint5_18Input) => + preserveEffect()( + raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_19 = (raw: RawClient["server.session"]) => (input: Endpoint5_19Input) => + preserveEffect()( + raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_20 = (raw: RawClient["server.session"]) => (input: Endpoint5_20Input) => + preserveEffect()( + raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_21 = (raw: RawClient["server.session"]) => (input: Endpoint5_21Input) => + preserveEffect()( + raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22Input) => + preserveEffect()( + raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) => + preserveEffect()( + raw["session.instructions.entry.put"]({ + params: { sessionID: input["sessionID"], key: input["key"] }, + payload: { value: input["value"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => + preserveEffect()( + raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => + preserveEffect()( + raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => + preserveStream()( + Stream.unwrap( + raw["session.log"]({ + params: { sessionID: input["sessionID"] }, + query: { after: input["after"], follow: input["follow"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), + ), + ) + const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => - raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_28Request = Parameters[0] -type Endpoint5_28Input = { readonly sessionID: Endpoint5_28Request["params"]["sessionID"] } const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => - raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint5_29Request = Parameters[0] -type Endpoint5_29Input = { - readonly sessionID: Endpoint5_29Request["params"]["sessionID"] - readonly messageID: Endpoint5_29Request["params"]["messageID"] -} const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) => - raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) const adaptGroup5 = (raw: RawClient["server.session"]) => ({ @@ -439,197 +579,142 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({ message: Endpoint5_29(raw), }) -type Endpoint6_0Request = Parameters[0] -type Endpoint6_0Input = { - readonly sessionID: Endpoint6_0Request["params"]["sessionID"] - readonly limit?: Endpoint6_0Request["query"]["limit"] - readonly order?: Endpoint6_0Request["query"]["order"] - readonly cursor?: Endpoint6_0Request["query"]["cursor"] -} const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => - raw["session.messages"]({ - params: { sessionID: input["sessionID"] }, - query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.messages"]({ + params: { sessionID: input["sessionID"] }, + query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup6 = (raw: RawClient["server.message"]) => ({ list: Endpoint6_0(raw) }) -type Endpoint7_0Request = Parameters[0] -type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] } const Endpoint7_0 = (raw: RawClient["server.model"]) => (input?: Endpoint7_0Input) => - raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint7_1Request = Parameters[0] -type Endpoint7_1Input = { readonly location?: Endpoint7_1Request["query"]["location"] } const Endpoint7_1 = (raw: RawClient["server.model"]) => (input?: Endpoint7_1Input) => - raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup7 = (raw: RawClient["server.model"]) => ({ list: Endpoint7_0(raw), default: Endpoint7_1(raw) }) -type Endpoint8_0Request = Parameters[0] -type Endpoint8_0Input = { - readonly location?: Endpoint8_0Request["query"]["location"] - readonly prompt: Endpoint8_0Request["payload"]["prompt"] - readonly model?: Endpoint8_0Request["payload"]["model"] -} const Endpoint8_0 = (raw: RawClient["server.generate"]) => (input: Endpoint8_0Input) => - raw["generate.text"]({ - query: { location: input["location"] }, - payload: { prompt: input["prompt"], model: input["model"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["generate.text"]({ + query: { location: input["location"] }, + payload: { prompt: input["prompt"], model: input["model"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) const adaptGroup8 = (raw: RawClient["server.generate"]) => ({ text: Endpoint8_0(raw) }) -type Endpoint9_0Request = Parameters[0] -type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] } const Endpoint9_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint9_0Input) => - raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint9_1Request = Parameters[0] -type Endpoint9_1Input = { - readonly providerID: Endpoint9_1Request["params"]["providerID"] - readonly location?: Endpoint9_1Request["query"]["location"] -} const Endpoint9_1 = (raw: RawClient["server.provider"]) => (input: Endpoint9_1Input) => - raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup9 = (raw: RawClient["server.provider"]) => ({ list: Endpoint9_0(raw), get: Endpoint9_1(raw) }) -type Endpoint10_0Request = Parameters[0] -type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["location"] } const Endpoint10_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint10_0Input) => - raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_1Request = Parameters[0] -type Endpoint10_1Input = { - readonly integrationID: Endpoint10_1Request["params"]["integrationID"] - readonly location?: Endpoint10_1Request["query"]["location"] -} -const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint10_1Input) => - raw["integration.get"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint10_2Request = Parameters[0] -type Endpoint10_2Input = { - readonly location?: Endpoint10_2Request["query"]["location"] - readonly url: Endpoint10_2Request["payload"]["url"] -} -const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) => - raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint10_1Input) => + preserveEffect()( + raw["integration.get"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) => + preserveEffect()( + raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint10_3Request = Parameters[0] -type Endpoint10_3Input = { - readonly integrationID: Endpoint10_3Request["params"]["integrationID"] - readonly location?: Endpoint10_3Request["query"]["location"] - readonly key: Endpoint10_3Request["payload"]["key"] - readonly label?: Endpoint10_3Request["payload"]["label"] -} const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) => - raw["integration.connect.key"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { key: input["key"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.connect.key"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { key: input["key"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_4Request = Parameters[0] -type Endpoint10_4Input = { - readonly integrationID: Endpoint10_4Request["params"]["integrationID"] - readonly location?: Endpoint10_4Request["query"]["location"] - readonly methodID: Endpoint10_4Request["payload"]["methodID"] - readonly inputs: Endpoint10_4Request["payload"]["inputs"] - readonly label?: Endpoint10_4Request["payload"]["label"] -} const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) => - raw["integration.oauth.connect"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.oauth.connect"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_5Request = Parameters[0] -type Endpoint10_5Input = { - readonly integrationID: Endpoint10_5Request["params"]["integrationID"] - readonly attemptID: Endpoint10_5Request["params"]["attemptID"] - readonly location?: Endpoint10_5Request["query"]["location"] -} const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) => - raw["integration.oauth.status"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.oauth.status"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_6Request = Parameters[0] -type Endpoint10_6Input = { - readonly integrationID: Endpoint10_6Request["params"]["integrationID"] - readonly attemptID: Endpoint10_6Request["params"]["attemptID"] - readonly location?: Endpoint10_6Request["query"]["location"] - readonly code?: Endpoint10_6Request["payload"]["code"] -} const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) => - raw["integration.oauth.complete"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - payload: { code: input["code"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.oauth.complete"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + payload: { code: input["code"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_7Request = Parameters[0] -type Endpoint10_7Input = { - readonly integrationID: Endpoint10_7Request["params"]["integrationID"] - readonly attemptID: Endpoint10_7Request["params"]["attemptID"] - readonly location?: Endpoint10_7Request["query"]["location"] -} const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) => - raw["integration.oauth.cancel"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.oauth.cancel"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_8Request = Parameters[0] -type Endpoint10_8Input = { - readonly integrationID: Endpoint10_8Request["params"]["integrationID"] - readonly location?: Endpoint10_8Request["query"]["location"] - readonly methodID: Endpoint10_8Request["payload"]["methodID"] - readonly label?: Endpoint10_8Request["payload"]["label"] -} const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) => - raw["integration.command.connect"]({ - params: { integrationID: input["integrationID"] }, - query: { location: input["location"] }, - payload: { methodID: input["methodID"], label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.command.connect"]({ + params: { integrationID: input["integrationID"] }, + query: { location: input["location"] }, + payload: { methodID: input["methodID"], label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_9Request = Parameters[0] -type Endpoint10_9Input = { - readonly integrationID: Endpoint10_9Request["params"]["integrationID"] - readonly attemptID: Endpoint10_9Request["params"]["attemptID"] - readonly location?: Endpoint10_9Request["query"]["location"] -} const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) => - raw["integration.command.status"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.command.status"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint10_10Request = Parameters[0] -type Endpoint10_10Input = { - readonly integrationID: Endpoint10_10Request["params"]["integrationID"] - readonly attemptID: Endpoint10_10Request["params"]["attemptID"] - readonly location?: Endpoint10_10Request["query"]["location"] -} const Endpoint10_10 = (raw: RawClient["server.integration"]) => (input: Endpoint10_10Input) => - raw["integration.command.cancel"]({ - params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["integration.command.cancel"]({ + params: { integrationID: input["integrationID"], attemptID: input["attemptID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ list: Endpoint10_0(raw), @@ -645,58 +730,45 @@ const adaptGroup10 = (raw: RawClient["server.integration"]) => ({ command: { connect: Endpoint10_8(raw), status: Endpoint10_9(raw), cancel: Endpoint10_10(raw) }, }) -type Endpoint11_0Request = Parameters[0] -type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] } const Endpoint11_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_0Input) => - raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint11_1Request = Parameters[0] -type Endpoint11_1Input = { - readonly server: Endpoint11_1Request["params"]["server"] - readonly location?: Endpoint11_1Request["query"]["location"] - readonly config: Endpoint11_1Request["payload"]["config"] -} const Endpoint11_1 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_1Input) => - raw["mcp.add"]({ - params: { server: input["server"] }, - query: { location: input["location"] }, - payload: { config: input["config"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["mcp.add"]({ + params: { server: input["server"] }, + query: { location: input["location"] }, + payload: { config: input["config"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint11_2Request = Parameters[0] -type Endpoint11_2Input = { - readonly server: Endpoint11_2Request["params"]["server"] - readonly location?: Endpoint11_2Request["query"]["location"] -} const Endpoint11_2 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_2Input) => - raw["mcp.remove"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["mcp.remove"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint11_3Request = Parameters[0] -type Endpoint11_3Input = { - readonly server: Endpoint11_3Request["params"]["server"] - readonly location?: Endpoint11_3Request["query"]["location"] -} const Endpoint11_3 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_3Input) => - raw["mcp.connect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["mcp.connect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint11_4Request = Parameters[0] -type Endpoint11_4Input = { - readonly server: Endpoint11_4Request["params"]["server"] - readonly location?: Endpoint11_4Request["query"]["location"] -} const Endpoint11_4 = (raw: RawClient["server.mcp"]) => (input: Endpoint11_4Input) => - raw["mcp.disconnect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["mcp.disconnect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint11_5Request = Parameters[0] -type Endpoint11_5Input = { readonly location?: Endpoint11_5Request["query"]["location"] } const Endpoint11_5 = (raw: RawClient["server.mcp"]) => (input?: Endpoint11_5Input) => - raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint11_0(raw), @@ -707,50 +779,40 @@ const adaptGroup11 = (raw: RawClient["server.mcp"]) => ({ resource: { catalog: Endpoint11_5(raw) }, }) -type Endpoint12_0Request = Parameters[0] -type Endpoint12_0Input = { - readonly credentialID: Endpoint12_0Request["params"]["credentialID"] - readonly location?: Endpoint12_0Request["query"]["location"] - readonly label: Endpoint12_0Request["payload"]["label"] -} const Endpoint12_0 = (raw: RawClient["server.credential"]) => (input: Endpoint12_0Input) => - raw["credential.update"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - payload: { label: input["label"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["credential.update"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + payload: { label: input["label"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint12_1Request = Parameters[0] -type Endpoint12_1Input = { - readonly credentialID: Endpoint12_1Request["params"]["credentialID"] - readonly location?: Endpoint12_1Request["query"]["location"] -} const Endpoint12_1 = (raw: RawClient["server.credential"]) => (input: Endpoint12_1Input) => - raw["credential.remove"]({ - params: { credentialID: input["credentialID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["credential.remove"]({ + params: { credentialID: input["credentialID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup12 = (raw: RawClient["server.credential"]) => ({ update: Endpoint12_0(raw), remove: Endpoint12_1(raw) }) const Endpoint13_0 = (raw: RawClient["server.project"]) => () => - raw["project.list"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["project.list"]({}).pipe(Effect.mapError(mapClientError))) -type Endpoint13_1Request = Parameters[0] -type Endpoint13_1Input = { readonly location?: Endpoint13_1Request["query"]["location"] } const Endpoint13_1 = (raw: RawClient["server.project"]) => (input?: Endpoint13_1Input) => - raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint13_2Request = Parameters[0] -type Endpoint13_2Input = { - readonly projectID: Endpoint13_2Request["params"]["projectID"] - readonly location?: Endpoint13_2Request["query"]["location"] -} const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2Input) => - raw["project.directories"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["project.directories"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup13 = (raw: RawClient["server.project"]) => ({ list: Endpoint13_0(raw), @@ -758,78 +820,59 @@ const adaptGroup13 = (raw: RawClient["server.project"]) => ({ directories: Endpoint13_2(raw), }) -type Endpoint14_0Request = Parameters[0] -type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] } const Endpoint14_0 = (raw: RawClient["server.form"]) => (input?: Endpoint14_0Input) => - raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint14_1Request = Parameters[0] -type Endpoint14_1Input = { readonly sessionID: Endpoint14_1Request["params"]["sessionID"] } const Endpoint14_1 = (raw: RawClient["server.form"]) => (input: Endpoint14_1Input) => - raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint14_2Request = Parameters[0] -type Endpoint14_2Input = { - readonly sessionID: Endpoint14_2Request["params"]["sessionID"] - readonly id?: Endpoint14_2Request["payload"]["id"] - readonly title: Endpoint14_2Request["payload"]["title"] - readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly fields: Endpoint14_2Request["payload"]["fields"] -} const Endpoint14_2 = (raw: RawClient["server.form"]) => (input: Endpoint14_2Input) => - raw["session.form.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.form.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint14_3Request = Parameters[0] -type Endpoint14_3Input = { - readonly sessionID: Endpoint14_3Request["params"]["sessionID"] - readonly formID: Endpoint14_3Request["params"]["formID"] -} const Endpoint14_3 = (raw: RawClient["server.form"]) => (input: Endpoint14_3Input) => - raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint14_4Request = Parameters[0] -type Endpoint14_4Input = { - readonly sessionID: Endpoint14_4Request["params"]["sessionID"] - readonly formID: Endpoint14_4Request["params"]["formID"] -} const Endpoint14_4 = (raw: RawClient["server.form"]) => (input: Endpoint14_4Input) => - raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint14_5Request = Parameters[0] -type Endpoint14_5Input = { - readonly sessionID: Endpoint14_5Request["params"]["sessionID"] - readonly formID: Endpoint14_5Request["params"]["formID"] - readonly answer: Endpoint14_5Request["payload"]["answer"] -} const Endpoint14_5 = (raw: RawClient["server.form"]) => (input: Endpoint14_5Input) => - raw["session.form.reply"]({ - params: { sessionID: input["sessionID"], formID: input["formID"] }, - payload: { answer: input["answer"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.form.reply"]({ + params: { sessionID: input["sessionID"], formID: input["formID"] }, + payload: { answer: input["answer"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint14_6Request = Parameters[0] -type Endpoint14_6Input = { - readonly sessionID: Endpoint14_6Request["params"]["sessionID"] - readonly formID: Endpoint14_6Request["params"]["formID"] -} const Endpoint14_6 = (raw: RawClient["server.form"]) => (input: Endpoint14_6Input) => - raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup14 = (raw: RawClient["server.form"]) => ({ @@ -842,83 +885,66 @@ const adaptGroup14 = (raw: RawClient["server.form"]) => ({ cancel: Endpoint14_6(raw), }) -type Endpoint15_0Request = Parameters[0] -type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] } const Endpoint15_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_0Input) => - raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint15_1Request = Parameters[0] -type Endpoint15_1Input = { readonly projectID?: Endpoint15_1Request["query"]["projectID"] } const Endpoint15_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint15_1Input) => - raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint15_2Request = Parameters[0] -type Endpoint15_2Input = { readonly id: Endpoint15_2Request["params"]["id"] } const Endpoint15_2 = (raw: RawClient["server.permission"]) => (input: Endpoint15_2Input) => - raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint15_3Request = Parameters[0] -type Endpoint15_3Input = { - readonly sessionID: Endpoint15_3Request["params"]["sessionID"] - readonly id?: Endpoint15_3Request["payload"]["id"] - readonly action: Endpoint15_3Request["payload"]["action"] - readonly resources: Endpoint15_3Request["payload"]["resources"] - readonly save?: Endpoint15_3Request["payload"]["save"] - readonly metadata?: Endpoint15_3Request["payload"]["metadata"] - readonly source?: Endpoint15_3Request["payload"]["source"] - readonly agent?: Endpoint15_3Request["payload"]["agent"] -} const Endpoint15_3 = (raw: RawClient["server.permission"]) => (input: Endpoint15_3Input) => - raw["session.permission.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - action: input["action"], - resources: input["resources"], - save: input["save"], - metadata: input["metadata"], - source: input["source"], - agent: input["agent"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.permission.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { + id: input["id"], + action: input["action"], + resources: input["resources"], + save: input["save"], + metadata: input["metadata"], + source: input["source"], + agent: input["agent"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint15_4Request = Parameters[0] -type Endpoint15_4Input = { readonly sessionID: Endpoint15_4Request["params"]["sessionID"] } const Endpoint15_4 = (raw: RawClient["server.permission"]) => (input: Endpoint15_4Input) => - raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint15_5Request = Parameters[0] -type Endpoint15_5Input = { - readonly sessionID: Endpoint15_5Request["params"]["sessionID"] - readonly requestID: Endpoint15_5Request["params"]["requestID"] -} const Endpoint15_5 = (raw: RawClient["server.permission"]) => (input: Endpoint15_5Input) => - raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), ) -type Endpoint15_6Request = Parameters[0] -type Endpoint15_6Input = { - readonly sessionID: Endpoint15_6Request["params"]["sessionID"] - readonly requestID: Endpoint15_6Request["params"]["requestID"] - readonly reply: Endpoint15_6Request["payload"]["reply"] - readonly message?: Endpoint15_6Request["payload"]["message"] -} const Endpoint15_6 = (raw: RawClient["server.permission"]) => (input: Endpoint15_6Input) => - raw["session.permission.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { reply: input["reply"], message: input["message"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["session.permission.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { reply: input["reply"], message: input["message"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup15 = (raw: RawClient["server.permission"]) => ({ request: { list: Endpoint15_0(raw) }, @@ -929,112 +955,88 @@ const adaptGroup15 = (raw: RawClient["server.permission"]) => ({ reply: Endpoint15_6(raw), }) -type Endpoint16_0Request = Parameters[0] -type Endpoint16_0Input = { - readonly location?: Endpoint16_0Request["query"]["location"] - readonly path?: Endpoint16_0Request["query"]["path"] -} const Endpoint16_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint16_0Input) => - raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint16_1Request = Parameters[0] -type Endpoint16_1Input = { - readonly location?: Endpoint16_1Request["query"]["location"] - readonly query: Endpoint16_1Request["query"]["query"] - readonly type?: Endpoint16_1Request["query"]["type"] - readonly limit?: Endpoint16_1Request["query"]["limit"] -} const Endpoint16_1 = (raw: RawClient["server.fs"]) => (input: Endpoint16_1Input) => - raw["fs.find"]({ - query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["fs.find"]({ + query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup16 = (raw: RawClient["server.fs"]) => ({ list: Endpoint16_0(raw), find: Endpoint16_1(raw) }) -type Endpoint17_0Request = Parameters[0] -type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] } const Endpoint17_0 = (raw: RawClient["server.command"]) => (input?: Endpoint17_0Input) => - raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup17 = (raw: RawClient["server.command"]) => ({ list: Endpoint17_0(raw) }) -type Endpoint18_0Request = Parameters[0] -type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] } const Endpoint18_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint18_0Input) => - raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup18 = (raw: RawClient["server.skill"]) => ({ list: Endpoint18_0(raw) }) const Endpoint19_0 = (raw: RawClient["server.event"]) => () => - Stream.unwrap( - raw["event.subscribe"]({}).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + preserveStream()( + Stream.unwrap( + raw["event.subscribe"]({}).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), ), ) const adaptGroup19 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint19_0(raw) }) -type Endpoint20_0Request = Parameters[0] -type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } const Endpoint20_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_0Input) => - raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_1Request = Parameters[0] -type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] -} -const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => - raw["pty.create"]({ - query: { location: input?.["location"] }, - payload: { - command: input?.["command"], - args: input?.["args"], - cwd: input?.["cwd"], - title: input?.["title"], - env: input?.["env"], - }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint20_2Request = Parameters[0] -type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] -} -const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) => - raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -type Endpoint20_3Request = Parameters[0] -type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] -} -const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => - raw["pty.update"]({ - params: { ptyID: input["ptyID"] }, - query: { location: input["location"] }, - payload: { title: input["title"], size: input["size"] }, - }).pipe(Effect.mapError(mapClientError)) +const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => + preserveEffect()( + raw["pty.create"]({ + query: { location: input?.["location"] }, + payload: { + command: input?.["command"], + args: input?.["args"], + cwd: input?.["cwd"], + title: input?.["title"], + env: input?.["env"], + }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) => + preserveEffect()( + raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => + preserveEffect()( + raw["pty.update"]({ + params: { ptyID: input["ptyID"] }, + query: { location: input["location"] }, + payload: { title: input["title"], size: input["size"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint20_4Request = Parameters[0] -type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] -} const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) => - raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ @@ -1045,69 +1047,48 @@ const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ remove: Endpoint20_4(raw), }) -type Endpoint21_0Request = Parameters[0] -type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] } const Endpoint21_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint21_0Input) => - raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_1Request = Parameters[0] -type Endpoint21_1Input = { - readonly location?: Endpoint21_1Request["query"]["location"] - readonly command: Endpoint21_1Request["payload"]["command"] - readonly cwd?: Endpoint21_1Request["payload"]["cwd"] - readonly timeout: Endpoint21_1Request["payload"]["timeout"] - readonly metadata?: Endpoint21_1Request["payload"]["metadata"] -} -const Endpoint21_1 = (raw: RawClient["server.shell"]) => (input: Endpoint21_1Input) => - raw["shell.create"]({ - query: { location: input["location"] }, - payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, - }).pipe(Effect.mapError(mapClientError)) - -type Endpoint21_2Request = Parameters[0] -type Endpoint21_2Input = { - readonly id: Endpoint21_2Request["params"]["id"] - readonly location?: Endpoint21_2Request["query"]["location"] -} -const Endpoint21_2 = (raw: RawClient["server.shell"]) => (input: Endpoint21_2Input) => - raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint21_1 = (raw: RawClient["server.shell"]) => (input: Endpoint21_1Input) => + preserveEffect()( + raw["shell.create"]({ + query: { location: input["location"] }, + payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const Endpoint21_2 = (raw: RawClient["server.shell"]) => (input: Endpoint21_2Input) => + preserveEffect()( + raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) -type Endpoint21_3Request = Parameters[0] -type Endpoint21_3Input = { - readonly id: Endpoint21_3Request["params"]["id"] - readonly location?: Endpoint21_3Request["query"]["location"] - readonly timeout: Endpoint21_3Request["payload"]["timeout"] -} const Endpoint21_3 = (raw: RawClient["server.shell"]) => (input: Endpoint21_3Input) => - raw["shell.timeout"]({ - params: { id: input["id"] }, - query: { location: input["location"] }, - payload: { timeout: input["timeout"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["shell.timeout"]({ + params: { id: input["id"] }, + query: { location: input["location"] }, + payload: { timeout: input["timeout"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint21_4Request = Parameters[0] -type Endpoint21_4Input = { - readonly id: Endpoint21_4Request["params"]["id"] - readonly location?: Endpoint21_4Request["query"]["location"] - readonly cursor?: Endpoint21_4Request["query"]["cursor"] - readonly limit?: Endpoint21_4Request["query"]["limit"] -} const Endpoint21_4 = (raw: RawClient["server.shell"]) => (input: Endpoint21_4Input) => - raw["shell.output"]({ - params: { id: input["id"] }, - query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["shell.output"]({ + params: { id: input["id"] }, + query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint21_5Request = Parameters[0] -type Endpoint21_5Input = { - readonly id: Endpoint21_5Request["params"]["id"] - readonly location?: Endpoint21_5Request["query"]["location"] -} const Endpoint21_5 = (raw: RawClient["server.shell"]) => (input: Endpoint21_5Input) => - raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup21 = (raw: RawClient["server.shell"]) => ({ @@ -1119,39 +1100,32 @@ const adaptGroup21 = (raw: RawClient["server.shell"]) => ({ remove: Endpoint21_5(raw), }) -type Endpoint22_0Request = Parameters[0] -type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] } const Endpoint22_0 = (raw: RawClient["server.question"]) => (input?: Endpoint22_0Input) => - raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) - -type Endpoint22_1Request = Parameters[0] -type Endpoint22_1Input = { readonly sessionID: Endpoint22_1Request["params"]["sessionID"] } -const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) => - raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), + preserveEffect()( + raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -type Endpoint22_2Request = Parameters[0] -type Endpoint22_2Input = { - readonly sessionID: Endpoint22_2Request["params"]["sessionID"] - readonly requestID: Endpoint22_2Request["params"]["requestID"] - readonly answers: Endpoint22_2Request["payload"]["answers"] -} -const Endpoint22_2 = (raw: RawClient["server.question"]) => (input: Endpoint22_2Input) => - raw["session.question.reply"]({ - params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { answers: input["answers"] }, - }).pipe(Effect.mapError(mapClientError)) +const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) => + preserveEffect()( + raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const Endpoint22_2 = (raw: RawClient["server.question"]) => (input: Endpoint22_2Input) => + preserveEffect()( + raw["session.question.reply"]({ + params: { sessionID: input["sessionID"], requestID: input["requestID"] }, + payload: { answers: input["answers"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint22_3Request = Parameters[0] -type Endpoint22_3Input = { - readonly sessionID: Endpoint22_3Request["params"]["sessionID"] - readonly requestID: Endpoint22_3Request["params"]["requestID"] -} const Endpoint22_3 = (raw: RawClient["server.question"]) => (input: Endpoint22_3Input) => - raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup22 = (raw: RawClient["server.question"]) => ({ @@ -1161,52 +1135,38 @@ const adaptGroup22 = (raw: RawClient["server.question"]) => ({ reject: Endpoint22_3(raw), }) -type Endpoint23_0Request = Parameters[0] -type Endpoint23_0Input = { readonly location?: Endpoint23_0Request["query"]["location"] } const Endpoint23_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint23_0Input) => - raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup23 = (raw: RawClient["server.reference"]) => ({ list: Endpoint23_0(raw) }) -type Endpoint24_0Request = Parameters[0] -type Endpoint24_0Input = { - readonly projectID: Endpoint24_0Request["params"]["projectID"] - readonly location?: Endpoint24_0Request["query"]["location"] - readonly strategy: Endpoint24_0Request["payload"]["strategy"] - readonly directory: Endpoint24_0Request["payload"]["directory"] - readonly name?: Endpoint24_0Request["payload"]["name"] -} const Endpoint24_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_0Input) => - raw["projectCopy.create"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["projectCopy.create"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint24_1Request = Parameters[0] -type Endpoint24_1Input = { - readonly projectID: Endpoint24_1Request["params"]["projectID"] - readonly location?: Endpoint24_1Request["query"]["location"] - readonly directory: Endpoint24_1Request["payload"]["directory"] - readonly force: Endpoint24_1Request["payload"]["force"] -} const Endpoint24_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_1Input) => - raw["projectCopy.remove"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - payload: { directory: input["directory"], force: input["force"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["projectCopy.remove"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + payload: { directory: input["directory"], force: input["force"] }, + }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint24_2Request = Parameters[0] -type Endpoint24_2Input = { - readonly projectID: Endpoint24_2Request["params"]["projectID"] - readonly location?: Endpoint24_2Request["query"]["location"] -} const Endpoint24_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_2Input) => - raw["projectCopy.refresh"]({ - params: { projectID: input["projectID"] }, - query: { location: input["location"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["projectCopy.refresh"]({ + params: { projectID: input["projectID"] }, + query: { location: input["location"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({ create: Endpoint24_0(raw), @@ -1214,52 +1174,44 @@ const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({ refresh: Endpoint24_2(raw), }) -type Endpoint25_0Request = Parameters[0] -type Endpoint25_0Input = { readonly location?: Endpoint25_0Request["query"]["location"] } const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) => - raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint25_1Request = Parameters[0] -type Endpoint25_1Input = { - readonly location?: Endpoint25_1Request["query"]["location"] - readonly mode: Endpoint25_1Request["query"]["mode"] - readonly context?: Endpoint25_1Request["query"]["context"] -} const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) => - raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe( - Effect.mapError(mapClientError), + preserveEffect()( + raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe( + Effect.mapError(mapClientError), + ), ) const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) }) const Endpoint26_0 = (raw: RawClient["server.debug"]) => () => - raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)) + preserveEffect()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))) -type Endpoint26_1Request = Parameters[0] -type Endpoint26_1Input = { readonly location?: Endpoint26_1Request["query"]["location"] } const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) => - raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup26 = (raw: RawClient["server.debug"]) => ({ location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) }, }) -type Endpoint27_0Request = Parameters[0] -type Endpoint27_0Input = { readonly location?: Endpoint27_0Request["query"]["location"] } const Endpoint27_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint27_0Input) => - raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) -type Endpoint27_1Request = Parameters[0] -type Endpoint27_1Input = { - readonly location?: Endpoint27_1Request["query"]["location"] - readonly query: Endpoint27_1Request["payload"]["query"] - readonly providerID?: Endpoint27_1Request["payload"]["providerID"] -} const Endpoint27_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint27_1Input) => - raw["websearch.query"]({ - query: { location: input["location"] }, - payload: { query: input["query"], providerID: input["providerID"] }, - }).pipe(Effect.mapError(mapClientError)) + preserveEffect()( + raw["websearch.query"]({ + query: { location: input["location"] }, + payload: { query: input["query"], providerID: input["providerID"] }, + }).pipe(Effect.mapError(mapClientError)), + ) const adaptGroup27 = (raw: RawClient["server.websearch"]) => ({ providers: Endpoint27_0(raw), diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index b5fd3fd6c3f..e6edc8a8568 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -7,6 +7,8 @@ import type { LocationGetOutput, AgentListInput, AgentListOutput, + AgentGetInput, + AgentGetOutput, PluginListInput, PluginListOutput, SessionListInput, @@ -403,6 +405,18 @@ export function make(options: ClientOptions) { }, requestOptions, ), + get: (input: AgentGetInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/agent/${encodeURIComponent(input.agentID)}`, + query: { location: input["location"] }, + successStatus: 200, + declaredStatuses: [404, 401, 400], + empty: false, + }, + requestOptions, + ), }, plugin: { list: (input?: PluginListInput, requestOptions?: RequestOptions) => diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 19ae6c3f5dd..d679249b15e 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -6,11 +6,11 @@ export type ServiceStopResponse = { accepted: boolean } export type ModelRef = { id: string; providerID: string; variant?: string } -export type ProviderSettings = { [x: string]: JsonValue } +export type ProviderSettings = { [x: string]: any } export type AgentColor = string -export type PermissionV2Effect = "allow" | "deny" | "ask" +export type PermissionEffect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -175,24 +175,24 @@ export type ModelCapabilities = { tools: boolean; input: Array; output: export type ModelVariant = { id: string - settings?: { [x: string]: JsonValue } + settings?: { [x: string]: any } headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } + body?: { [x: string]: any } } export type MoneyUSDPerMillionTokens = number export type GenerateTextResponse = { data: { text: string } } -export type ProviderV2Info = { +export type ProviderInfo = { id: string integrationID?: string name: string disabled?: boolean package: string - settings?: { [x: string]: JsonValue } + settings?: { [x: string]: any } headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } + body?: { [x: string]: any } } export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string } @@ -299,7 +299,7 @@ export type FormExternalField = { key: string; type: "external"; url: string; ti export type FormValue = string | number | boolean | Array -export type PermissionV2Source = { type: "tool"; messageID: string; callID: string } +export type PermissionSource = { type: "tool"; messageID: string; callID: string } export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string } @@ -323,9 +323,9 @@ export type FileDiffLegacyInfo = { status?: "added" | "deleted" | "modified" } -export type PermissionAction = "allow" | "deny" | "ask" +export type PermissionV1Action = "allow" | "deny" | "ask" -export type JSONSchema = { [x: string]: any } +export type SessionV1JSONSchema = { [x: string]: any } export type ProviderAuthError = { name: "ProviderAuthError"; data: { providerID: string; message: string } } @@ -356,7 +356,7 @@ export type APIError = { } } -export type TextPart = { +export type SessionV1TextPart = { id: string sessionID: string messageID: string @@ -368,7 +368,7 @@ export type TextPart = { metadata?: { [x: string]: any } | undefined } -export type SubtaskPart = { +export type SessionV1SubtaskPart = { id: string sessionID: string messageID: string @@ -380,7 +380,7 @@ export type SubtaskPart = { command?: string | undefined } -export type ReasoningPart = { +export type SessionV1ReasoningPart = { id: string sessionID: string messageID: string @@ -390,13 +390,13 @@ export type ReasoningPart = { time: { start: number; end?: number | undefined } } -export type FilePartSourceText = { value: string; start: number; end: number } +export type SessionV1FilePartSourceText = { value: string; start: number; end: number } -export type Range = { start: { line: number; character: number }; end: { line: number; character: number } } +export type SessionV1Range = { start: { line: number; character: number }; end: { line: number; character: number } } -export type ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } +export type SessionV1ToolStatePending = { status: "pending"; input: { [x: string]: any }; raw: string } -export type ToolStateRunning = { +export type SessionV1ToolStateRunning = { status: "running" input: { [x: string]: any } title?: string | undefined @@ -404,7 +404,7 @@ export type ToolStateRunning = { time: { start: number } } -export type ToolStateError = { +export type SessionV1ToolStateError = { status: "error" input: { [x: string]: any } error: string @@ -412,7 +412,7 @@ export type ToolStateError = { time: { start: number; end: number } } -export type StepStartPart = { +export type SessionV1StepStartPart = { id: string sessionID: string messageID: string @@ -420,7 +420,7 @@ export type StepStartPart = { snapshot?: string | undefined } -export type StepFinishPart = { +export type SessionV1StepFinishPart = { id: string sessionID: string messageID: string @@ -437,9 +437,15 @@ export type StepFinishPart = { } } -export type SnapshotPart = { id: string; sessionID: string; messageID: string; type: "snapshot"; snapshot: string } +export type SessionV1SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} -export type PatchPart = { +export type SessionV1PatchPart = { id: string sessionID: string messageID: string @@ -448,7 +454,7 @@ export type PatchPart = { files: Array } -export type AgentPart = { +export type SessionV1AgentPart = { id: string sessionID: string messageID: string @@ -457,7 +463,7 @@ export type AgentPart = { source?: { value: string; start: number; end: number } | undefined } -export type CompactionPart = { +export type SessionV1CompactionPart = { id: string sessionID: string messageID: string @@ -467,7 +473,7 @@ export type CompactionPart = { tail_start_id?: string | undefined } -export type PermissionV2Reply = "once" | "always" | "reject" +export type PermissionReply = "once" | "always" | "reject" export type Pty = { id: string @@ -480,11 +486,11 @@ export type Pty = { exitCode?: number } -export type QuestionV2Option = { label: string; description: string } +export type QuestionOption = { label: string; description: string } -export type QuestionV2Tool = { messageID: string; callID: string } +export type QuestionTool = { messageID: string; callID: string } -export type QuestionV2Answer = Array +export type QuestionAnswer = Array export type FormMetadata1 = { [x: string]: any } @@ -501,12 +507,6 @@ export type SessionStatus = } | { type: "busy" } -export type QuestionOption = { label: string; description: string } - -export type QuestionTool = { messageID: string; callID: string } - -export type QuestionAnswer = Array - export type ShellInfo1 = { id: string status: "running" | "exited" | "timeout" | "killed" @@ -564,10 +564,10 @@ export type CommandInfo = { export type ProviderRequest = { settings: ProviderSettings headers: { [x: string]: string } - body: { [x: string]: JsonValue } + body: { [x: string]: any } } -export type PermissionV2Rule = { action: string; resource: string; effect: PermissionV2Effect } +export type PermissionRule = { action: string; resource: string; effect: PermissionEffect } export type SessionAgentSelected = { id: string @@ -1044,11 +1044,11 @@ export type ShellDeleted = { data: { id: string } } -export type QuestionV2Rejected = { +export type QuestionRejected = { id: string created: number metadata?: { [x: string]: any } - type: "question.v2.rejected" + type: "question.rejected" location?: LocationRef data: { sessionID: string; requestID: string } } @@ -1186,41 +1186,6 @@ export type McpResourcesChanged = { data: { server: string } } -export type PermissionAsked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.asked" - location?: LocationRef - data: { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { [x: string]: any } - always: Array - tool?: { messageID: string; callID: string } | undefined - } -} - -export type PermissionReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "permission.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; reply: "once" | "always" | "reject" } -} - -export type QuestionRejected = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.rejected" - location?: LocationRef - data: { sessionID: string; requestID: string } -} - export type V2EventServerConnected = { id: string metadata?: { [x: string]: any } | undefined @@ -1544,21 +1509,21 @@ export type FormMultiselectField = { export type FormAnswer = { [x: string]: FormValue } -export type PermissionV2Request = { +export type PermissionRequest = { id: string sessionID: string action: string resources: Array save?: Array metadata?: { [x: string]: JsonValue } - source?: PermissionV2Source + source?: PermissionSource } -export type PermissionV2Asked = { +export type PermissionAsked = { id: string created: number metadata?: { [x: string]: any } - type: "permission.v2.asked" + type: "permission.asked" location?: LocationRef data: { id: string @@ -1567,17 +1532,17 @@ export type PermissionV2Asked = { resources: Array save?: Array metadata?: { [x: string]: any } - source?: PermissionV2Source + source?: PermissionSource } } -export type PermissionRule = { permission: string; pattern: string; action: PermissionAction } +export type PermissionV1Rule = { permission: string; pattern: string; action: PermissionV1Action } -export type OutputFormat = +export type SessionV1OutputFormat = | { type: "text" } - | { type: "json_schema"; schema: JSONSchema; retryCount?: number | undefined | undefined } + | { type: "json_schema"; schema: SessionV1JSONSchema; retryCount?: number | undefined | undefined } -export type AssistantMessage = { +export type SessionV1AssistantMessage = { id: string sessionID: string role: "assistant" @@ -1612,7 +1577,7 @@ export type AssistantMessage = { finish?: string | undefined } -export type RetryPart = { +export type SessionV1RetryPart = { id: string sessionID: string messageID: string @@ -1643,26 +1608,31 @@ export type SessionError = { } } -export type FileSource = { text: FilePartSourceText; type: "file"; path: string } +export type SessionV1FileSource = { text: SessionV1FilePartSourceText; type: "file"; path: string } -export type ResourceSource = { text: FilePartSourceText; type: "resource"; clientName: string; uri: string } +export type SessionV1ResourceSource = { + text: SessionV1FilePartSourceText + type: "resource" + clientName: string + uri: string +} -export type SymbolSource = { - text: FilePartSourceText +export type SessionV1SymbolSource = { + text: SessionV1FilePartSourceText type: "symbol" path: string - range: Range + range: SessionV1Range name: string kind: number } -export type PermissionV2Replied = { +export type PermissionReplied = { id: string created: number metadata?: { [x: string]: any } - type: "permission.v2.replied" + type: "permission.replied" location?: LocationRef - data: { sessionID: string; requestID: string; reply: PermissionV2Reply } + data: { sessionID: string; requestID: string; reply: PermissionReply } } export type PtyCreated = { @@ -1683,21 +1653,21 @@ export type PtyUpdated = { data: { info: Pty } } -export type QuestionV2Info = { +export type QuestionInfo = { question: string header: string - options: Array + options: Array multiple?: boolean custom?: boolean } -export type QuestionV2Replied = { +export type QuestionReplied = { id: string created: number metadata?: { [x: string]: any } - type: "question.v2.replied" + type: "question.replied" location?: LocationRef - data: { sessionID: string; requestID: string; answers: Array } + data: { sessionID: string; requestID: string; answers: Array } } export type FormStringField1 = { @@ -1774,26 +1744,9 @@ export type SessionStatus2 = { data: { sessionID: string; status: SessionStatus } } -export type QuestionInfo = { - question: string - header: string - options: Array - multiple?: boolean | undefined - custom?: boolean | undefined -} - -export type QuestionReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.replied" - location?: LocationRef - data: { sessionID: string; requestID: string; answers: Array } -} - export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource -export type PermissionV2Ruleset = Array +export type PermissionRuleset = Array export type SessionInfo = { id: string @@ -1910,9 +1863,9 @@ export type ModelInfo = { name: string compatibility?: ModelCompatibility package?: string - settings?: { [x: string]: JsonValue } + settings?: { [x: string]: any } headers?: { [x: string]: string } - body?: { [x: string]: JsonValue } + body?: { [x: string]: any } capabilities: ModelCapabilities variants: Array time: { released: number } @@ -1948,14 +1901,14 @@ export type FormReplied = { data: { id: string; sessionID: string; answer: FormAnswer } } -export type PermissionRuleset = Array +export type PermissionV1Ruleset = Array -export type UserMessage = { +export type SessionV1UserMessage = { id: string sessionID: string role: "user" time: { created: number } - format?: OutputFormat | undefined + format?: SessionV1OutputFormat | undefined summary?: { title?: string | undefined; body?: string | undefined; diffs: Array } | undefined agent: string model: { providerID: string; modelID: string; variant?: string | undefined } @@ -1963,23 +1916,18 @@ export type UserMessage = { tools?: { [x: string]: boolean } | undefined } -export type FilePartSource = FileSource | SymbolSource | ResourceSource +export type SessionV1FilePartSource = SessionV1FileSource | SessionV1SymbolSource | SessionV1ResourceSource -export type QuestionV2Asked = { +export type QuestionAsked = { id: string created: number metadata?: { [x: string]: any } - type: "question.v2.asked" + type: "question.asked" location?: LocationRef - data: { id: string; sessionID: string; questions: Array; tool?: QuestionV2Tool } + data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool } } -export type QuestionV2Request = { - id: string - sessionID: string - questions: Array - tool?: QuestionV2Tool -} +export type QuestionRequest = { id: string; sessionID: string; questions: Array; tool?: QuestionTool } export type FormField1 = | FormStringField1 @@ -1989,15 +1937,6 @@ export type FormField1 = | FormMultiselectField1 | FormExternalField -export type QuestionAsked = { - id: string - created: number - metadata?: { [x: string]: any } - type: "question.asked" - location?: LocationRef - data: { id: string; sessionID: string; questions: Array; tool?: QuestionTool | undefined } -} - export type ReferenceInfo = { name: string path: string @@ -2017,7 +1956,7 @@ export type AgentInfo = { hidden: boolean color?: AgentColor steps?: number - permissions: PermissionV2Ruleset + permissions: PermissionRuleset } export type SessionsResponse = { data: Array; cursor: { previous?: string | null; next?: string | null } } @@ -2075,13 +2014,13 @@ export type SessionV1Info = { version: string metadata?: { [x: string]: any } time: { created: number; updated: number; compacting?: number; archived?: number } - permission?: PermissionRuleset + permission?: PermissionV1Ruleset revert?: { messageID: string; partID?: string; snapshot?: string; diff?: string } } -export type Message = UserMessage | AssistantMessage +export type SessionV1Message = SessionV1UserMessage | SessionV1AssistantMessage -export type FilePart = { +export type SessionV1FilePart = { id: string sessionID: string messageID: string @@ -2089,7 +2028,7 @@ export type FilePart = { mime: string filename?: string | undefined url: string - source?: FilePartSource | undefined + source?: SessionV1FilePartSource | undefined } export type FormFields1 = [FormField1, ...Array] @@ -2160,17 +2099,17 @@ export type MessageUpdated = { type: "message.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; info: Message } + data: { sessionID: string; info: SessionV1Message } } -export type ToolStateCompleted = { +export type SessionV1ToolStateCompleted = { status: "completed" input: { [x: string]: any } output: string title: string metadata: { [x: string]: any } time: { start: number; end: number; compacted?: number | undefined } - attachments?: Array | undefined + attachments?: Array | undefined } export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 } @@ -2196,7 +2135,11 @@ export type SessionMessageInfo = | SessionMessageAssistant | SessionMessageCompaction -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError +export type SessionV1ToolState = + | SessionV1ToolStatePending + | SessionV1ToolStateRunning + | SessionV1ToolStateCompleted + | SessionV1ToolStateError export type FormCreated = { id: string @@ -2252,32 +2195,32 @@ export type SessionMessagesResponse = { cursor: { previous?: string | null; next?: string | null } } -export type ToolPart = { +export type SessionV1ToolPart = { id: string sessionID: string messageID: string type: "tool" callID: string tool: string - state: ToolState + state: SessionV1ToolState metadata?: { [x: string]: any } | undefined } export type SessionLogItem = SessionEventDurable | EventLogSynced -export type Part = - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart +export type SessionV1Part = + | SessionV1TextPart + | SessionV1SubtaskPart + | SessionV1ReasoningPart + | SessionV1FilePart + | SessionV1ToolPart + | SessionV1StepStartPart + | SessionV1StepFinishPart + | SessionV1SnapshotPart + | SessionV1PatchPart + | SessionV1AgentPart + | SessionV1RetryPart + | SessionV1CompactionPart export type MessagePartUpdated = { id: string @@ -2286,7 +2229,7 @@ export type MessagePartUpdated = { type: "message.part.updated" durable: { aggregateID: string; seq: number; version: 1 } location?: LocationRef - data: { sessionID: string; part: Part; time: number } + data: { sessionID: string; part: SessionV1Part; time: number } } export type V2Event = @@ -2347,8 +2290,8 @@ export type V2Event = | SessionRevertCommitted | FilesystemChanged | ReferenceUpdated - | PermissionV2Asked - | PermissionV2Replied + | PermissionAsked + | PermissionReplied | PluginAdded | PluginUpdated | ProjectDirectoriesUpdated @@ -2362,9 +2305,9 @@ export type V2Event = | ShellCreated | ShellExited | ShellDeleted - | QuestionV2Asked - | QuestionV2Replied - | QuestionV2Rejected + | QuestionAsked + | QuestionReplied + | QuestionRejected | FormCreated | FormReplied | FormCancelled @@ -2380,11 +2323,6 @@ export type V2Event = | VcsBranchUpdated | McpStatusChanged | McpResourcesChanged - | PermissionAsked - | PermissionReplied - | QuestionAsked - | QuestionReplied - | QuestionRejected | SessionError | V2EventServerConnected @@ -2401,6 +2339,14 @@ export type InvalidRequestError = { export const isInvalidRequestError = (value: unknown): value is InvalidRequestError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidRequestError" +export type AgentNotFoundError = { + readonly _tag: "AgentNotFoundError" + readonly agentID: string + readonly message: string +} +export const isAgentNotFoundError = (value: unknown): value is AgentNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "AgentNotFoundError" + export type InvalidCursorError = { readonly _tag: "InvalidCursorError"; readonly message: string } export const isInvalidCursorError = (value: unknown): value is InvalidCursorError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "InvalidCursorError" @@ -2584,6 +2530,18 @@ export type AgentListOutput = { data: Array } +export type AgentGetInput = { + readonly agentID: { readonly agentID: string }["agentID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type AgentGetOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: AgentInfo +} + export type PluginListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined @@ -3312,7 +3270,7 @@ export type ProviderListInput = { export type ProviderListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type ProviderGetInput = { @@ -3324,7 +3282,7 @@ export type ProviderGetInput = { export type ProviderGetOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: ProviderV2Info + data: ProviderInfo } export type IntegrationListInput = { @@ -4476,7 +4434,7 @@ export type PermissionRequestListInput = { export type PermissionRequestListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type PermissionSavedListInput = { readonly projectID?: { readonly projectID?: string | undefined }["projectID"] } @@ -4554,18 +4512,18 @@ export type PermissionCreateInput = { }["agent"] } -export type PermissionCreateOutput = { data: { id: string; effect: PermissionV2Effect } }["data"] +export type PermissionCreateOutput = { data: { id: string; effect: PermissionEffect } }["data"] export type PermissionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } -export type PermissionListOutput = { data: Array }["data"] +export type PermissionListOutput = { data: Array }["data"] export type PermissionGetInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] } -export type PermissionGetOutput = { data: PermissionV2Request }["data"] +export type PermissionGetOutput = { data: PermissionRequest }["data"] export type PermissionReplyInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] @@ -4864,12 +4822,12 @@ export type QuestionRequestListInput = { export type QuestionRequestListOutput = { location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } - data: Array + data: Array } export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } -export type QuestionListOutput = { data: Array }["data"] +export type QuestionListOutput = { data: Array }["data"] export type QuestionReplyInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] diff --git a/packages/client/test/api.types.ts b/packages/client/test/api.types.ts index ba95bb42970..a4c3f0f0036 100644 --- a/packages/client/test/api.types.ts +++ b/packages/client/test/api.types.ts @@ -1,5 +1,6 @@ import { Effect } from "effect" import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect" +import type { Session } from "@opencode-ai/schema/session" type EffectClient = Effect.Success> type PromiseClient = ReturnType @@ -9,6 +10,10 @@ declare const promiseClient: PromiseClient const effectApi: EffectApi = effectClient +const effectSession: Effect.Effect = effectClient.session.get({ + sessionID: "ses_test" as Session.ID, +}) + declare const sessionID: Parameters[0]["sessionID"] const effectList: Effect.Effect< @@ -37,4 +42,4 @@ const promiseRemove: Promise = promiseClient.session.instructions.entry.re key: "review-notes", }) -void [effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] +void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove] diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 8de2115fa78..ace9d86a185 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -14,6 +14,14 @@ test("effect entrypoint exposes canonical Schema contracts", () => { expect(Client.Session).toBe(Session) }) +test("generated Effect API names canonical and composed outputs", async () => { + const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text() + + expect(source).toContain("export type Endpoint5_3Output = Session.Info") + expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent") + expect(source).not.toContain("HttpApiClient.ForApi") +}) + test("shared DTO schemas construct and decode plain objects", () => { const made = Prompt.make({ text: "hello" }) const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" }) diff --git a/packages/core/package.json b/packages/core/package.json index 86f97f7d509..de86b87b101 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -99,6 +99,7 @@ "@opencode-ai/schema": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/util": "workspace:*", + "@standard-schema/spec": "catalog:", "@parcel/watcher": "2.5.1", "@silvia-odwyer/photon-node": "0.3.4", "@openrouter/ai-sdk-provider": "2.9.0", diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts index 7e81780e95b..d9d29d742d9 100644 --- a/packages/core/src/account.ts +++ b/packages/core/src/account.ts @@ -1,4 +1,4 @@ -export * as AccountV2 from "./account" +export * as Account from "./account" import { Schema } from "effect" import type { HttpClientError } from "effect/unstable/http" diff --git a/packages/core/src/account/sql.ts b/packages/core/src/account/sql.ts index 4f45651d78e..706ddd34a59 100644 --- a/packages/core/src/account/sql.ts +++ b/packages/core/src/account/sql.ts @@ -1,14 +1,14 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" -import { AccountV2 } from "../account" +import { Account } from "../account" import { Timestamps } from "../database/schema.sql" export const AccountTable = sqliteTable("account", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), email: text().notNull(), url: text().notNull(), - access_token: text().$type().notNull(), - refresh_token: text().$type().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), token_expiry: integer(), ...Timestamps, }) @@ -16,9 +16,9 @@ export const AccountTable = sqliteTable("account", { export const AccountStateTable = sqliteTable("account_state", { id: integer().primaryKey(), active_account_id: text() - .$type() + .$type() .references(() => AccountTable.id, { onDelete: "set null" }), - active_org_id: text().$type(), + active_org_id: text().$type(), }) // LEGACY @@ -27,8 +27,8 @@ export const ControlAccountTable = sqliteTable( { email: text().notNull(), url: text().notNull(), - access_token: text().$type().notNull(), - refresh_token: text().$type().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), token_expiry: integer(), active: integer({ mode: "boolean" }) .notNull() diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 23f1fd1240b..548dcdb2aa6 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,9 +1,9 @@ -export * as AgentV2 from "./agent" +export * as Agent from "./agent" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Array, Context, Effect, Layer, Types } from "effect" import { Agent } from "@opencode-ai/schema/agent" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { State } from "./state" export const ID = Agent.ID @@ -17,7 +17,7 @@ export const Color = Agent.Color export const Info = Agent.Info export type Info = Agent.Info -export const Event = Agent.Event +export { Event } from "@opencode-ai/schema/agent" export interface Selection { readonly id: ID @@ -45,12 +45,12 @@ export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Agent") {} +export class Service extends Context.Service()("@opencode/Agent") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const state = State.create({ name: "agent", initial: () => ({ agents: new Map() }), @@ -70,7 +70,7 @@ const layer = Layer.effect( draft.agents.delete(id) }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Agent.Event.Updated, {}).pipe(Effect.asVoid), }) const selectable = (agent: Info | undefined) => agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined @@ -89,17 +89,17 @@ const layer = Layer.effect( return Service.of({ transform: state.transform, reload: state.reload, - get: Effect.fn("AgentV2.get")(function* (id) { + get: Effect.fn("Agent.get")(function* (id) { return state.get().agents.get(id) }), - default: Effect.fn("AgentV2.default")(function* () { + default: Effect.fn("Agent.default")(function* () { return selectedDefault() }), - resolve: Effect.fn("AgentV2.resolve")(function* (id) { + resolve: Effect.fn("Agent.resolve")(function* (id) { if (id !== undefined) return state.get().agents.get(ID.make(id)) return selectedDefault() }), - select: Effect.fn("AgentV2.select")(function* (id) { + select: Effect.fn("Agent.select")(function* (id) { if (id !== undefined) { const selected = ID.make(id) return { id: selected, info: state.get().agents.get(selected) } @@ -107,11 +107,11 @@ const layer = Layer.effect( const info = selectedDefault() return { id: info?.id ?? defaultID, info } }), - list: Effect.fn("AgentV2.list")(function* () { + list: Effect.fn("Agent.list")(function* () { return Array.fromIterable(state.get().agents.values()) }), }) }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 7c9e54b4f75..7aaa5ec6c95 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -32,8 +32,8 @@ import { import { Auth, Endpoint, type AnyRoute } from "@opencode-ai/ai/route" import { ProviderShared } from "@opencode-ai/ai/protocols/shared" import { Cause, Context, Effect, Layer, Option, Schema, Scope, Stream } from "effect" -import { ModelV2 } from "./model" -import { ProviderV2 } from "./provider" +import type { ID, Info } from "./model" +import { Provider } from "./provider" import { State } from "./state" type SDK = any @@ -42,14 +42,14 @@ type AssistantContent = Extract[" type ToolResultContent = Extract export interface SDKEvent { - readonly model: ModelV2.Info + readonly model: Info readonly package: string readonly options: Record sdk?: SDK } export interface LanguageEvent { - readonly model: ModelV2.Info + readonly model: Info readonly sdk: SDK readonly options: Record language?: LanguageModelV3 @@ -103,7 +103,7 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { }) } -function prepareOptions(model: ModelV2.Info, pkg: string) { +function prepareOptions(model: Info, pkg: string) { const projected = mapBodyToProviderOptions(model, pkg) const options: Record = { name: model.providerID, @@ -146,7 +146,7 @@ function prepareOptions(model: ModelV2.Info, pkg: string) { if (typeof opts.body === "string" && model.body !== undefined) { const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body)) if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) { - opts.body = JSON.stringify(ProviderV2.mergeOverlay(decoded, model.body)) + opts.body = JSON.stringify(Provider.mergeOverlay(decoded, model.body)) } } @@ -162,11 +162,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) { } export class InitError extends Schema.TaggedErrorClass()("AISDK.InitError", { - providerID: ProviderV2.ID, + providerID: Provider.ID, cause: Schema.Defect(), }) {} -function initError(providerID: ProviderV2.ID) { +function initError(providerID: Provider.ID) { return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) }))) } @@ -181,11 +181,11 @@ export interface Interface { } readonly runSDK: (event: SDKEvent) => Effect.Effect readonly runLanguage: (event: LanguageEvent) => Effect.Effect - readonly language: (model: ModelV2.Info) => Effect.Effect - readonly model: (model: ModelV2.Info) => Effect.Effect + readonly language: (model: Info) => Effect.Effect + readonly model: (model: Info) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/AISDK") {} +export class Service extends Context.Service()("@opencode/AISDK") {} export const locationLayer = Layer.effect( Service, @@ -260,13 +260,13 @@ export const locationLayer = Layer.effect( }) const existing = languages.get(key) if (existing) return existing - if (!ProviderV2.isAISDK(model.package)) + if (!Provider.isAISDK(model.package)) return yield* new InitError({ providerID: model.providerID, cause: new Error(`Unsupported package ${model.package}`), }) - const packageName = ProviderV2.packageName(model.package) + const packageName = Provider.packageName(model.package) const options = prepareOptions(model, packageName) const sdkKey = cacheKey({ providerID: model.providerID, @@ -301,8 +301,8 @@ export const locationLayer = Layer.effect( export const defaultLayer = locationLayer -function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { - const packageName = ProviderV2.packageName(info.package!) +function modelFromLanguage(info: Info, language: LanguageModelV3) { + const packageName = Provider.packageName(info.package!) const projected = mapBodyToProviderOptions(info, packageName) const optionKey = providerOptionKey(packageName, info.providerID) const providerOptions = (() => { @@ -352,7 +352,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { }) } -function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly>) { +function gatewayProviderOptions(modelID: ID, settings: Readonly>) { const gateway = typeof settings.gateway === "object" && settings.gateway !== null && !Array.isArray(settings.gateway) ? Object.fromEntries(Object.entries(settings.gateway)) @@ -369,7 +369,7 @@ function gatewayProviderOptions(modelID: ModelV2.ID, settings: Readonly> | undefined return Object.keys(result).length === 0 ? undefined : result } -function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) { +function mapBodyToProviderOptions(model: Info, packageName: string) { const settings = requestSettings(model.settings) const pro = Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(model.body?.reasoning) const forceReasoning = ["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle"].includes(packageName) && (pro || settings?.reasoningEffort !== undefined || settings?.reasoningSummary !== undefined) - const normalized = forceReasoning ? ProviderV2.mergeOverlay(settings, { forceReasoning: true }) : settings + const normalized = forceReasoning ? Provider.mergeOverlay(settings, { forceReasoning: true }) : settings if (!pro) return { settings: normalized, body: model.body } const body = { ...model.body } delete body.reasoning return { - settings: ProviderV2.mergeOverlay(normalized, { reasoningMode: "pro" }), + settings: Provider.mergeOverlay(normalized, { reasoningMode: "pro" }), body: Object.keys(body).length === 0 ? undefined : body, } } diff --git a/packages/core/src/event.ts b/packages/core/src/bus.ts similarity index 88% rename from packages/core/src/event.ts rename to packages/core/src/bus.ts index 759a55721d3..2e498e0a397 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/bus.ts @@ -1,8 +1,7 @@ -export * as EventV2 from "./event" +export * as Bus from "./bus" import { Cause, Context, DateTime, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" import { Event } from "@opencode-ai/schema/event" -import type { Data, Definition, Payload } from "@opencode-ai/schema/event" import type { EventLog } from "@opencode-ai/schema/event-log" import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { Database } from "./database/database" @@ -12,18 +11,10 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { isDeepStrictEqual } from "node:util" import { Durable } from "@opencode-ai/schema/durable-event-manifest" -export const ID = Event.ID -export type ID = import("@opencode-ai/schema/event").ID -export const Seq = Event.Seq -export type Seq = import("@opencode-ai/schema/event").Seq -export const Version = Event.Version -export type Version = import("@opencode-ai/schema/event").Version -export type { Data, Definition, Payload } from "@opencode-ai/schema/event" - -export type Subscriber = (event: Payload) => Effect.Effect +export type Subscriber = (event: Event.Payload) => Effect.Effect export type Unsubscribe = Effect.Effect -export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( +export const latestSequence = Effect.fn("Bus.latestSequence")(function* ( db: Database.Interface["db"], aggregateID: string, ) { @@ -36,7 +27,7 @@ export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( return row?.seq ?? -1 }) -export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* ( +export const reserveSequence = Effect.fn("Bus.reserveSequence")(function* ( db: Database.Interface["db"], aggregateID: string, seq: number, @@ -53,7 +44,7 @@ export const reserveSequence = Effect.fn("EventV2.reserveSequence")(function* ( }) export type SerializedEvent = { - readonly id: ID + readonly id: Event.ID readonly type: string readonly created?: DateTime.Utc readonly seq: number @@ -62,7 +53,7 @@ export type SerializedEvent = { } export class InvalidDurableEventError extends Schema.TaggedErrorClass()( - "EventV2.InvalidDurableEvent", + "Bus.InvalidDurableEvent", { type: Schema.String, message: Schema.String, @@ -71,11 +62,11 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass ({ aggregateID, - seq: Seq.make(seq), - version: Version.make(version), + seq: Event.Seq.make(seq), + version: Event.Version.make(version), }) -const decodeSerializedEvent = (event: SerializedEvent): Payload => { +const decodeSerializedEvent = (event: SerializedEvent): Event.Payload => { const definition = Durable.get(event.type) if (!definition?.durable) { throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) @@ -94,7 +85,7 @@ export const durable = Event.durable export const ephemeral = Event.ephemeral export interface PublishOptions { - readonly id?: ID + readonly id?: Event.ID readonly metadata?: Record readonly location?: Location.Ref /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */ @@ -102,13 +93,13 @@ export interface PublishOptions { } /** Marker/event union emitted by `log`. */ -export type LogItem = Payload | EventLog.Synced +export type LogItem = Event.Payload | EventLog.Synced export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced" -export type SubscribePayload = D[number] extends infer Item - ? Item extends Definition - ? Payload +export type SubscribePayload = D[number] extends infer Item + ? Item extends Event.Definition + ? Event.Payload : never : never @@ -117,19 +108,19 @@ export interface Subscribe { * Volatile live channel: every event published from now on, nothing before or * across a disconnect. Consumers that need reliability combine it with `log`. */ - (): Stream.Stream - (definition: D): Stream.Stream> - (definitions: D): Stream.Stream> + (): Stream.Stream + (definition: D): Stream.Stream> + (definitions: D): Stream.Stream> } -const isDefinition = (input: Definition | readonly Definition[]): input is Definition => !Array.isArray(input) +const isDefinition = (input: Event.Definition | readonly Event.Definition[]): input is Event.Definition => !Array.isArray(input) export interface Interface { - readonly publish: ( + readonly publish: ( definition: D, - data: Data, + data: Event.Data, options?: PublishOptions, - ) => Effect.Effect> + ) => Effect.Effect> readonly subscribe: Subscribe /** * Durable, ordered per-aggregate log read. Forked aggregates may reserve an @@ -144,10 +135,10 @@ export interface Interface { readonly follow?: boolean }) => Stream.Stream /** Latest committed seq per aggregate. Aggregates without events are absent. */ - readonly sequences: (aggregateIDs: ReadonlyArray) => Effect.Effect> + readonly sequences: (aggregateIDs: ReadonlyArray) => Effect.Effect> /** @deprecated Use `subscribe()` and consume the returned stream. */ readonly listen: (listener: Subscriber) => Effect.Effect - readonly project: (definition: D, projector: Subscriber) => Effect.Effect + readonly project: (definition: D, projector: Subscriber) => Effect.Effect readonly replay: ( event: SerializedEvent, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, @@ -160,7 +151,8 @@ export interface Interface { readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect } -export class Service extends Context.Service()("@opencode/Event") {} + +export class Service extends Context.Service()("@opencode/Bus") {} export interface LayerOptions { readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect @@ -173,20 +165,20 @@ export const layerWith = (options?: LayerOptions) => Service, Effect.gen(function* () { const pubsub = { - live: yield* PubSub.unbounded(), + live: yield* PubSub.unbounded(), durable: new Map>>(), - typed: new Map>(), + typed: new Map>(), } const projectors = new Map() const listeners = new Array() const { db } = yield* Database.Service const logReadPageSize = options?.logReadPageSize ?? 512 - const getOrCreate = (definition: Definition) => + const getOrCreate = (definition: Event.Definition) => Effect.gen(function* () { const existing = pubsub.typed.get(definition.type) if (existing) return existing - const created = yield* PubSub.unbounded() + const created = yield* PubSub.unbounded() pubsub.typed.set(definition.type, created) return created }) @@ -204,8 +196,8 @@ export const layerWith = (options?: LayerOptions) => ) function commitDurableEvent( - definition: Definition, - event: Payload, + definition: Event.Definition, + event: Event.Payload, input?: { readonly seq: number readonly aggregateID: string @@ -318,7 +310,7 @@ export const layerWith = (options?: LayerOptions) => const committed = { ...event, durable: { aggregateID, seq, version: durable.version }, - } as Payload + } as Event.Payload for (const projector of list) { yield* projector(committed) } @@ -369,7 +361,7 @@ export const layerWith = (options?: LayerOptions) => }) } - function publishEvent(definition: D, event: Payload, commit?: PublishOptions["commit"]) { + function publishEvent(definition: D, event: Event.Payload, commit?: PublishOptions["commit"]) { return Effect.gen(function* () { if (!definition?.durable && commit) return yield* Effect.die( @@ -379,22 +371,22 @@ export const layerWith = (options?: LayerOptions) => }), ) if (definition?.durable) { - const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) + const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit) if (committed) { event = { ...event, durable: envelope(committed.aggregateID, committed.seq, definition.durable.version), } - yield* notify(event as Payload, true) + yield* notify(event as Event.Payload, true) return event } } - yield* notify(event as Payload, false) + yield* notify(event as Event.Payload, false) return event }) } - const observe = (event: Payload, observer: (event: Payload) => Effect.Effect) => + const observe = (event: Event.Payload, observer: (event: Event.Payload) => Effect.Effect) => Effect.suspend(() => observer(event)).pipe( Effect.catchCauseIf( (cause) => !Cause.hasInterrupts(cause), @@ -402,7 +394,7 @@ export const layerWith = (options?: LayerOptions) => ), ) - function notify(event: Payload, isolateListeners: boolean) { + function notify(event: Event.Payload, isolateListeners: boolean) { return Effect.gen(function* () { yield* Effect.forEach( listeners, @@ -415,7 +407,7 @@ export const layerWith = (options?: LayerOptions) => }) } - function publish(definition: D, data: Data, options?: PublishOptions) { + function publish(definition: D, data: Event.Data, options?: PublishOptions) { return Effect.gen(function* () { const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) const location = @@ -426,13 +418,13 @@ export const layerWith = (options?: LayerOptions) => return yield* publishEvent( definition, { - id: options?.id ?? ID.create(), + id: options?.id ?? Event.ID.create(), created: yield* DateTime.now, ...(options?.metadata ? { metadata: options.metadata } : {}), type: definition.type, ...(location ? { location } : {}), data, - } as Payload, + } as Event.Payload, options?.commit, ) }) @@ -454,7 +446,7 @@ export const layerWith = (options?: LayerOptions) => created: event.created ?? DateTime.makeUnsafe(0), type: definition.type, data: Schema.decodeUnknownSync(definition.data)(event.data), - } as Payload + } as Event.Payload const committed = yield* commitDurableEvent(definition, payload, { seq: event.seq, aggregateID: event.aggregateID, @@ -516,7 +508,7 @@ export const layerWith = (options?: LayerOptions) => .pipe(Effect.orDie) } - const local = (stream: Stream.Stream) => + const local = (stream: Stream.Stream) => Stream.unwrap( Effect.serviceOption(Location.Service).pipe( Effect.map((location) => @@ -536,12 +528,12 @@ export const layerWith = (options?: LayerOptions) => ), ) - function subscribe(): Stream.Stream - function subscribe(definition: D): Stream.Stream> - function subscribe( + function subscribe(): Stream.Stream + function subscribe(definition: D): Stream.Stream> + function subscribe( definitions: D, ): Stream.Stream> - function subscribe(input?: Definition | readonly Definition[]): Stream.Stream { + function subscribe(input?: Event.Definition | readonly Event.Definition[]): Stream.Stream { if (input === undefined) return streamLive() if (isDefinition(input)) { return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))) @@ -550,7 +542,7 @@ export const layerWith = (options?: LayerOptions) => return streamLive().pipe(Stream.filter((event) => types.has(event.type))) } - const streamLive = (): Stream.Stream => local(Stream.fromPubSub(pubsub.live)) + const streamLive = (): Stream.Stream => local(Stream.fromPubSub(pubsub.live)) const readAfter = ( aggregateID: string, @@ -624,7 +616,7 @@ export const layerWith = (options?: LayerOptions) => Stream.unwrap( Effect.gen(function* () { let sequence = input.after ?? -1 - const readThrough = (through: number): Stream.Stream => + const readThrough = (through: number): Stream.Stream => Stream.paginate(sequence, (cursor) => readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe( Effect.tap((page) => @@ -648,7 +640,7 @@ export const layerWith = (options?: LayerOptions) => const marker: EventLog.Synced = { type: "log.synced", aggregateID: input.aggregateID, - ...(target >= 0 ? { seq: Seq.make(target) } : {}), + ...(target >= 0 ? { seq: Event.Seq.make(target) } : {}), } const replay: Stream.Stream = readThrough(target).pipe( Stream.map((event): LogItem => event), @@ -665,7 +657,7 @@ export const layerWith = (options?: LayerOptions) => }), ) - const sequences = (aggregateIDs: ReadonlyArray): Effect.Effect> => { + const sequences = (aggregateIDs: ReadonlyArray): Effect.Effect> => { if (aggregateIDs.length === 0) return Effect.succeed(new Map()) return db .select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq }) @@ -674,7 +666,7 @@ export const layerWith = (options?: LayerOptions) => .all() .pipe( Effect.orDie, - Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Seq.make(row.seq)]))), + Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Event.Seq.make(row.seq)]))), ) } @@ -687,11 +679,11 @@ export const layerWith = (options?: LayerOptions) => }) }) - const project = (definition: D, projector: Subscriber): Effect.Effect => + const project = (definition: D, projector: Subscriber): Effect.Effect => Effect.sync(() => { const key = definition.durable ? versionedType(definition.type, definition.durable.version) : definition.type const list = projectors.get(key) ?? [] - list.push((event) => projector(event as Payload)) + list.push((event) => projector(event as Event.Payload)) projectors.set(key, list) }) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 8d2c10c1a88..9bc9611cf89 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -3,82 +3,82 @@ export * as Catalog from "./catalog" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Array, Context, Effect, Layer, Order, pipe } from "effect" import { Catalog } from "@opencode-ai/schema/catalog" -import { ModelV2 } from "./model" -import { ProviderV2 } from "./provider" -import { EventV2 } from "./event" +import { Model } from "./model" +import { Provider } from "./provider" +import { Bus } from "./bus" import { State } from "./state" import { Integration } from "./integration" export type ProviderRecord = { - provider: ProviderV2.MutableInfo - models: Map + provider: Provider.MutableInfo + models: Map } -export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } +export type DefaultModel = { providerID: Provider.ID; modelID: Model.ID } -export const Event = Catalog.Event +export { Event } from "@opencode-ai/schema/catalog" type Data = { - providers: Map + providers: Map defaultModel?: DefaultModel } export type Draft = { provider: { list: () => readonly ProviderRecord[] - get: (providerID: ProviderV2.ID) => ProviderRecord | undefined - update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void - remove: (providerID: ProviderV2.ID) => void + get: (providerID: Provider.ID) => ProviderRecord | undefined + update: (providerID: Provider.ID, fn: (provider: Provider.MutableInfo) => void) => void + remove: (providerID: Provider.ID) => void } model: { - get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined - update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void - remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + get: (providerID: Provider.ID, modelID: Model.ID) => Model.Info | undefined + update: (providerID: Provider.ID, modelID: Model.ID, fn: (model: Model.MutableInfo) => void) => void + remove: (providerID: Provider.ID, modelID: Model.ID) => void default: { get: () => DefaultModel | undefined - set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + set: (providerID: Provider.ID, modelID: Model.ID) => void } } } export interface Interface extends State.Transformable { readonly provider: { - readonly get: (providerID: ProviderV2.ID) => Effect.Effect - readonly all: () => Effect.Effect - readonly available: () => Effect.Effect + readonly get: (providerID: Provider.ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly available: () => Effect.Effect } readonly model: { - readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect - readonly all: () => Effect.Effect - readonly available: () => Effect.Effect - readonly default: () => Effect.Effect - readonly small: (providerID: ProviderV2.ID) => Effect.Effect + readonly get: (providerID: Provider.ID, modelID: Model.ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly available: () => Effect.Effect + readonly default: () => Effect.Effect + readonly small: (providerID: Provider.ID) => Effect.Effect } } -export class Service extends Context.Service()("@opencode/v2/Catalog") {} +export class Service extends Context.Service()("@opencode/Catalog") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const integrations = yield* Integration.Service - const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { + const available = (provider: Provider.Info, integration: Integration.Info | undefined) => { if (provider.disabled) return false if (typeof provider.settings?.apiKey === "string") return true if (integration?.connections.length) return true return provider.integrationID === undefined && !integration } - const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { + const projectModel = (model: Model.Info, provider: Provider.Info) => { return { ...model, package: model.package ?? provider.package, - settings: ProviderV2.mergeOverlay(provider.settings, model.settings), - headers: ProviderV2.mergeHeaders(provider.headers, model.headers), - body: ProviderV2.mergeOverlay(provider.body, model.body), - } satisfies ModelV2.Info + settings: Provider.mergeOverlay(provider.settings, model.settings), + headers: Provider.mergeHeaders(provider.headers, model.headers), + body: Provider.mergeOverlay(provider.body, model.body), + } satisfies Model.Info } const state = State.create({ @@ -93,8 +93,8 @@ const layer = Layer.effect( let current = draft.providers.get(providerID) if (!current) { current = { - provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, - models: new Map(), + provider: Provider.Info.empty(providerID) as Provider.MutableInfo, + models: new Map(), } draft.providers.set(providerID, current) } @@ -110,13 +110,13 @@ const layer = Layer.effect( let record = draft.providers.get(providerID) if (!record) { record = { - provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, - models: new Map(), + provider: Provider.Info.empty(providerID) as Provider.MutableInfo, + models: new Map(), } draft.providers.set(providerID, record) } const model = - record.models.get(modelID) ?? (ModelV2.Info.default(providerID, modelID) as ModelV2.MutableInfo) + record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo) if (!record.models.has(modelID)) record.models.set(modelID, model) fn(model) model.id = modelID @@ -135,8 +135,8 @@ const layer = Layer.effect( } return result }, - finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) { - yield* events.publish(Event.Updated, {}) + finalize: Effect.fn("Catalog.finalize")(function* (catalog) { + yield* bus.publish(Catalog.Event.Updated, {}) }), }) const result: Interface = { @@ -144,15 +144,15 @@ const layer = Layer.effect( reload: state.reload, provider: { - get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { + get: Effect.fn("Catalog.provider.get")(function* (providerID) { return state.get().providers.get(providerID)?.provider }), - all: Effect.fn("CatalogV2.provider.all")(function* () { + all: Effect.fn("Catalog.provider.all")(function* () { return Array.fromIterable(state.get().providers.values()).map((record) => record.provider) }), - available: Effect.fn("CatalogV2.provider.available")(function* () { + available: Effect.fn("Catalog.provider.available")(function* () { const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration])) return (yield* result.provider.all()).filter((provider) => available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))), @@ -161,14 +161,14 @@ const layer = Layer.effect( }, model: { - get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { + get: Effect.fn("Catalog.model.get")(function* (providerID, modelID) { const record = state.get().providers.get(providerID) if (!record) return const model = record.models.get(modelID) return model && projectModel(model, record.provider) }), - all: Effect.fn("CatalogV2.model.all")(function* () { + all: Effect.fn("Catalog.model.all")(function* () { return pipe( Array.fromIterable(state.get().providers.values()), Array.flatMap((record) => { @@ -178,9 +178,9 @@ const layer = Layer.effect( ) }), - available: Effect.fn("CatalogV2.model.available")(function* () { + available: Effect.fn("Catalog.model.available")(function* () { const providers = new Set((yield* result.provider.available()).map((provider) => provider.id)) - const models: ModelV2.Info[] = [] + const models: Model.Info[] = [] for (const record of state.get().providers.values()) { if (!providers.has(record.provider.id)) continue for (const model of record.models.values()) { @@ -194,7 +194,7 @@ const layer = Layer.effect( ) }), - default: Effect.fn("CatalogV2.model.default")(function* () { + default: Effect.fn("Catalog.model.default")(function* () { const defaultModel = state.get().defaultModel if (defaultModel) { const provider = yield* result.provider.get(defaultModel.providerID) @@ -207,18 +207,18 @@ const layer = Layer.effect( return (yield* result.model.available())[0] }), - small: Effect.fn("CatalogV2.model.small")(function* (providerID) { + small: Effect.fn("Catalog.model.small")(function* (providerID) { const record = state.get().providers.get(providerID) if (!record) return const provider = record.provider // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. - if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { + if (providerID === Provider.ID.azure || providerID === Provider.ID.make("azure-cognitive-services")) { return } - if (providerID === ProviderV2.ID.opencode) { - const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) + if (providerID === Provider.ID.opencode) { + const gpt5Nano = record.models.get(Model.ID.make("gpt-5-nano")) if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider) } @@ -268,4 +268,4 @@ const layer = Layer.effect( const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Integration.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node, Integration.node] }) diff --git a/packages/core/src/codemode.ts b/packages/core/src/codemode.ts deleted file mode 100644 index 6d858097f8b..00000000000 --- a/packages/core/src/codemode.ts +++ /dev/null @@ -1,77 +0,0 @@ -export * as CodeMode from "./codemode" - -import { Context, Effect, Layer, Scope } from "effect" -import { CodeModeCatalog } from "./codemode/catalog" -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { PermissionV2 } from "./permission" -import { ExecuteTool } from "./tool/execute" -import type { Any, Registration } from "./tool/tool" -import { Wildcard } from "./util/wildcard" - -export interface Materialization { - readonly tool?: Any - readonly catalog?: ReadonlyArray -} - -export interface Interface { - readonly register: ( - registrations: ReadonlyArray, - ) => Effect.Effect - readonly materialize: (permissions?: PermissionV2.Ruleset) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/CodeMode") {} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - const local = new Map>() - - return Service.of({ - register: Effect.fn("CodeMode.register")(function* (registrations) { - if (registrations.length === 0) return - yield* Effect.uninterruptible( - Effect.gen(function* () { - const token = {} - for (const registration of registrations) - local.set(registration.key, [ - ...(local.get(registration.key) ?? []), - { - token, - registration, - }, - ]) - yield* Effect.addFinalizer(() => - Effect.sync(() => { - for (const registration of registrations) { - const remaining = local.get(registration.key)?.filter((item) => item.token !== token) ?? [] - if (remaining.length > 0) local.set(registration.key, remaining) - else local.delete(registration.key) - } - }), - ) - }), - ) - }), - materialize: Effect.fn("CodeMode.materialize")(function* (permissions) { - const registrations = new Map() - const rules = permissions ?? [] - for (const [name, entries] of local) { - const registration = entries.at(-1)?.registration - if (!registration) continue - const rule = rules.findLast((rule) => Wildcard.match(registration.permission, rule.action)) - if (rule?.resource === "*" && rule.effect === "deny") continue - registrations.set(name, registration) - } - const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action)) - if (executeRule?.resource === "*" && executeRule.effect === "deny") return {} - return { - tool: ExecuteTool.create(registrations), - catalog: ExecuteTool.catalog(registrations), - } - }), - }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/tool/execute.ts b/packages/core/src/codemode/tool.ts similarity index 81% rename from packages/core/src/tool/execute.ts rename to packages/core/src/codemode/tool.ts index edb22fa78b7..7d9c5106be6 100644 --- a/packages/core/src/tool/execute.ts +++ b/packages/core/src/codemode/tool.ts @@ -1,10 +1,9 @@ -export * as ExecuteTool from "./execute" -export type { Registration } from "./tool" +export * as CodeModeTool from "./tool" import { CodeMode, Tool, toolError } from "@opencode-ai/codemode" -import type { ToolContent } from "@opencode-ai/ai" +import type { Content, Context, Error, Info, Metadata, Result } from "@opencode-ai/schema/tool" import { Effect, Ref, Schema, Semaphore } from "effect" -import { execute, make, toLLMDefinition, type Content, type Metadata, type Registration } from "./tool" +import { definition } from "../tool/runtime" const ExecuteFile = Schema.Struct({ data: Schema.String, @@ -42,8 +41,17 @@ const description = [ "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.", ].join("\n") -export const create = (registrations: ReadonlyMap) => { - return make({ +export const create = ( + registrations: ReadonlyMap, + executeTool: ( + name: string, + tool: Info, + input: unknown, + context: Context, + ) => Effect.Effect, +) => { + return ({ + name: "execute", description, input: CodeMode.Input, output: ExecuteOutput, @@ -59,17 +67,17 @@ export const create = (registrations: ReadonlyMap) => { ) const result = yield* runtime( registrations, - (name, registration, input) => + (name, tool, input) => Effect.gen(function* () { const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1) - const executed = yield* execute(registration.tool, input, { - sessionID: context.sessionID, - agent: context.agent, - messageID: context.messageID, - callID: context.callID, - progress: () => Effect.void, - }).pipe(Effect.mapError((failure) => toolError(failure.message, failure))) - const outputFileParts = outputFiles(executed.content) + const executed = yield* executeTool(name, tool, input, context).pipe( + Effect.mapError((failure) => toolError(failure.message, failure)), + ) + const content = + typeof executed.content === "string" + ? [{ type: "text" as const, text: executed.content }] + : executed.content ?? [] + const outputFileParts = outputFiles(content) if (outputFileParts.length > 0) yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }]) return executed.output @@ -107,11 +115,11 @@ export const create = (registrations: ReadonlyMap) => { files: collected, ...(result.ok ? {} : { error: true }), } - const content: [Content, ...Content[]] = [{ type: "text", text: value.output }] + const content: Array = [{ type: "text", text: value.output }] content.push( ...value.files.map((file) => ({ type: "file" as const, - data: file.data, + uri: `data:${file.mime};base64,${file.data}`, mime: file.mime, ...(file.name === undefined ? {} : { name: file.name }), })), @@ -126,23 +134,26 @@ export const create = (registrations: ReadonlyMap) => { metadata, } }), - }) + }) satisfies Info } -export const catalog = (registrations: ReadonlyMap) => { +export const catalog = (registrations: ReadonlyMap) => { return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog() } function runtime( - registrations: ReadonlyMap, - executeTool: (name: string, registration: Registration, input: unknown) => Effect.Effect, + registrations: ReadonlyMap, + executeTool: (name: string, tool: Info, input: unknown) => Effect.Effect, hooks?: CodeMode.ToolCallHooks, ) { const tools: Record> = {} for (const [name, registration] of registrations) { - const child = toLLMDefinition(name, registration.tool) + const child = definition(registration) + const normalized = registration.name.replace(/[^a-zA-Z0-9_-]/g, "_") const path = - registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}` + registration.options?.namespace === undefined + ? normalized + : `${registration.options.namespace}.${normalized}` tools[path] = Tool.make({ description: child.description, input: child.inputSchema, @@ -180,7 +191,7 @@ function formatValue(value: CodeMode.DataValue) { return JSON.stringify(value, null, 2) ?? String(value) } -function outputFiles(content: ReadonlyArray): Array { +function outputFiles(content: ReadonlyArray): Array { return content.flatMap((part) => { if (part.type !== "file") return [] const prefix = `data:${part.mime};base64,` diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index f673231beb5..7ce432af8db 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -1,11 +1,11 @@ -export * as CommandV2 from "./command" +export * as Command from "./command" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Schema, Types } from "effect" import { Command } from "@opencode-ai/schema/command" import { State } from "./state" import { MCP } from "./mcp/index" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { AppProcess } from "@opencode-ai/util/process" import { ChildProcess } from "effect/unstable/process" import { Config } from "./config" @@ -14,7 +14,7 @@ import { ShellSelect } from "./shell/select" export const Info = Command.Info export type Info = Command.Info -export const Event = Command.Event +export { Event } from "@opencode-ai/schema/command" export type Evaluation = { readonly text: string @@ -50,13 +50,13 @@ export interface Interface extends State.Transformable { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Command") {} +export class Service extends Context.Service()("@opencode/Command") {} export const layer = (options?: ShellSelect.Options) => Layer.effect( Service, Effect.gen(function* () { const mcp = yield* MCP.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const processes = yield* AppProcess.Service const config = yield* Config.Service const location = yield* Location.Service @@ -76,7 +76,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( draft.commands.delete(name) }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid), }) const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined const mcpCommands = Effect.fnUntraced(function* () { @@ -92,12 +92,12 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( return Service.of({ reload: state.reload, transform: state.transform, - get: Effect.fn("CommandV2.get")(function* (name) { + get: Effect.fn("Command.get")(function* (name) { const command = staticCommand(name) if (command) return command return (yield* mcpCommands()).find((command) => command.name === name) }), - list: Effect.fn("CommandV2.list")(function* () { + list: Effect.fn("Command.list")(function* () { const commands = Array.from(state.get().commands.values()) as Info[] const names = new Set(commands.map((command) => command.name)) return [ @@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ...(yield* mcpCommands()).filter((command) => !names.has(command.name)), ] }), - evaluate: Effect.fn("CommandV2.evaluate")(function* (input) { + evaluate: Effect.fn("Command.evaluate")(function* (input) { const command = staticCommand(input.name) if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", { config, @@ -247,7 +247,7 @@ export function configured(options?: ShellSelect.Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node], + deps: [MCP.node, Bus.node, AppProcess.node, Config.node, Location.node], }) } diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 99cdff45e13..b24b30f27b5 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -9,7 +9,7 @@ import { Permission } from "@opencode-ai/schema/permission" import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Integration } from "@opencode-ai/schema/integration" import { Credential } from "./credential" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Watcher } from "./filesystem/watcher" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" @@ -168,7 +168,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/Config") {} +export class Service extends Context.Service()("@opencode/Config") {} export const layer = (options?: Options) => Layer.effect( Service, @@ -177,7 +177,7 @@ export const layer = (options?: Options) => Layer.effect( const global = yield* Global.Service const location = yield* Location.Service const watcher = yield* Watcher.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const credentials = yield* Credential.Service const wellknown = yield* WellKnown.Service const names = ["opencode.json", "opencode.jsonc"] @@ -375,7 +375,7 @@ export const layer = (options?: Options) => Layer.effect( if (isDeepStrictEqual(configs, next)) return configs = next yield* reconcile(next) - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) }), ), ) @@ -389,7 +389,7 @@ export const layer = (options?: Options) => Layer.effect( ), Effect.forkScoped({ startImmediately: true }), ) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filterEffect((event) => wellknown.entries().pipe( Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)), @@ -401,7 +401,7 @@ export const layer = (options?: Options) => Layer.effect( ), Effect.forkScoped({ startImmediately: true }), ) - yield* events.subscribe(WellKnown.Event.Updated).pipe( + yield* bus.subscribe(WellKnown.Event.Updated).pipe( Stream.runForEach(() => reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))), ), @@ -438,7 +438,7 @@ export function configured(options?: Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node], + deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node], }) } diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 075feea2507..94c7efb97b3 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -8,7 +8,7 @@ import { PositiveInt } from "../schema" export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) -export class Info extends Schema.Class("ConfigV2.Agent")({ +export class Info extends Schema.Class("Config.Agent")({ model: ConfigModel.Selection.pipe(Schema.optional), request: ConfigProvider.Request.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional), diff --git a/packages/core/src/config/attachments.ts b/packages/core/src/config/attachments.ts index f14775ff3f7..ea7862c0fab 100644 --- a/packages/core/src/config/attachments.ts +++ b/packages/core/src/config/attachments.ts @@ -3,13 +3,13 @@ export * as ConfigAttachments from "./attachments" import { Schema } from "effect" import { PositiveInt } from "../schema" -export class Image extends Schema.Class("ConfigV2.Attachments.Image")({ +export class Image extends Schema.Class("Config.Attachments.Image")({ auto_resize: Schema.Boolean.pipe(Schema.optional), max_width: PositiveInt.pipe(Schema.optional), max_height: PositiveInt.pipe(Schema.optional), max_base64_bytes: PositiveInt.pipe(Schema.optional), }) {} -export class Info extends Schema.Class("ConfigV2.Attachments")({ +export class Info extends Schema.Class("Config.Attachments")({ image: Image.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/command.ts b/packages/core/src/config/command.ts index 8168107fd15..ff9fdf926ad 100644 --- a/packages/core/src/config/command.ts +++ b/packages/core/src/config/command.ts @@ -3,7 +3,7 @@ export * as ConfigCommand from "./command" import { Schema } from "effect" import { ConfigModel } from "./model" -export class Info extends Schema.Class("ConfigV2.Command")({ +export class Info extends Schema.Class("Config.Command")({ template: Schema.String, description: Schema.String.pipe(Schema.optional), agent: Schema.String.pipe(Schema.optional), diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts index fde64cd3622..c52e0da7998 100644 --- a/packages/core/src/config/compaction.ts +++ b/packages/core/src/config/compaction.ts @@ -3,11 +3,11 @@ export * as ConfigCompaction from "./compaction" import { Schema } from "effect" import { NonNegativeInt } from "../schema" -export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ +export class Keep extends Schema.Class("Config.Compaction.Keep")({ tokens: NonNegativeInt.pipe(Schema.optional), }) {} -export class Info extends Schema.Class("ConfigV2.Compaction")({ +export class Info extends Schema.Class("Config.Compaction")({ auto: Schema.Boolean.pipe(Schema.optional), keep: Keep.pipe(Schema.optional), buffer: NonNegativeInt.pipe(Schema.optional), diff --git a/packages/core/src/config/formatter.ts b/packages/core/src/config/formatter.ts index e1f90302d1a..5730ceeec68 100644 --- a/packages/core/src/config/formatter.ts +++ b/packages/core/src/config/formatter.ts @@ -2,7 +2,7 @@ export * as ConfigFormatter from "./formatter" import { Schema } from "effect" -export class Entry extends Schema.Class("ConfigV2.Formatter.Entry")({ +export class Entry extends Schema.Class("Config.Formatter.Entry")({ disabled: Schema.Boolean.pipe(Schema.optional), command: Schema.String.pipe(Schema.Array, Schema.optional), environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), diff --git a/packages/core/src/config/lsp.ts b/packages/core/src/config/lsp.ts index 651597befdd..14c37718c9a 100644 --- a/packages/core/src/config/lsp.ts +++ b/packages/core/src/config/lsp.ts @@ -6,7 +6,7 @@ export const Disabled = Schema.Struct({ disabled: Schema.Literal(true), }) -export class Server extends Schema.Class("ConfigV2.LSP.Server")({ +export class Server extends Schema.Class("Config.LSP.Server")({ command: Schema.String.pipe(Schema.Array), extensions: Schema.String.pipe(Schema.Array, Schema.optional), disabled: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/config/mcp.ts b/packages/core/src/config/mcp.ts index 12c3a98e315..96bb2978b8b 100644 --- a/packages/core/src/config/mcp.ts +++ b/packages/core/src/config/mcp.ts @@ -15,7 +15,7 @@ export const Remote = Mcp.RemoteConfig export type Remote = Mcp.RemoteConfig export const Server = Mcp.ServerConfig -export class Info extends Schema.Class("ConfigV2.MCP")({ +export class Info extends Schema.Class("Config.MCP")({ timeout: Timeout.pipe(Schema.optional), servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/plugin.ts b/packages/core/src/config/plugin.ts index e5fd6661ff5..4268a7f79d2 100644 --- a/packages/core/src/config/plugin.ts +++ b/packages/core/src/config/plugin.ts @@ -2,7 +2,7 @@ export * as ConfigPlugin from "./plugin" import { Schema } from "effect" -export class Entry extends Schema.Class("ConfigV2.Plugin.Entry")({ +export class Entry extends Schema.Class("Config.Plugin.Entry")({ package: Schema.String, options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index 1254576e22a..0cb4c43f1d1 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -1,9 +1,9 @@ export * as ConfigAgentPlugin from "./agent" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Option, Schema, Stream } from "effect" -import { AgentV2 } from "../../agent" +import { Agent } from "../../agent" import { Config } from "../../config" import { ConfigAgent } from "../agent" import { ConfigMarkdown } from "../markdown" @@ -11,10 +11,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { ConfigAgentV1 } from "../../v1/config/agent" import { ConfigMigrateV1 } from "../../v1/config/migrate" import { Global } from "@opencode-ai/util/global" -import { PermissionV2 } from "../../permission" +import { Permission } from "../../permission" import type { LocationMutation } from "../../location-mutation" -import type { ReadTool } from "../../tool/read" -import type { EditTool } from "../../tool/edit" +import type { ReadTool } from "../../tool/plugin/read" +import type { EditTool } from "../../tool/plugin/edit" const legacySources = [ { pattern: "{agent,agents}/**/*.md", primary: false }, @@ -74,14 +74,14 @@ export const Plugin = define({ global.home, ) const configuredDefault = Config.latest(loaded.documents, "default_agent") - if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) + if (configuredDefault !== undefined) draft.default(Agent.ID.make(configuredDefault)) for (const current of draft.list()) { draft.update(current.id, (agent) => agent.permissions.push(...permissions)) } for (const document of loaded.documents) { for (const [id, item] of Object.entries(document.info.agents ?? {})) { - const agentID = AgentV2.ID.make(id) + const agentID = Agent.ID.make(id) if (item.disabled) { draft.remove(agentID) continue @@ -126,7 +126,7 @@ export const Plugin = define({ }), }) -function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset { +function expandPermissions(rules: Permission.Ruleset, home: string): Permission.Ruleset { // Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text: // rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing. return rules.map((rule) => diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index 29de7eddb57..f42d62d259f 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -1,9 +1,9 @@ export * as ConfigCommandPlugin from "./command" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Option, Schema, Stream } from "effect" -import { CommandV2 } from "../../command" +import { Command } from "../../command" import { Config } from "../../config" import { FSUtil } from "@opencode-ai/util/fs-util" import { ConfigCommand } from "../command" diff --git a/packages/core/src/config/plugin/policy.ts b/packages/core/src/config/plugin/policy.ts index fd05939d173..97bacc51163 100644 --- a/packages/core/src/config/plugin/policy.ts +++ b/packages/core/src/config/plugin/policy.ts @@ -1,6 +1,6 @@ export * as ConfigPolicyPlugin from "./policy" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Stream } from "effect" import { Config } from "../../config" import { Wildcard } from "../../util/wildcard" diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index b0dceca26e3..ec00ddf36c7 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -1,10 +1,10 @@ export * as ConfigProviderPlugin from "./provider" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Money } from "@opencode-ai/schema/money" import { Effect, Stream } from "effect" import { Config } from "../../config" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" export const Plugin = define({ id: "opencode.config.provider", @@ -49,9 +49,9 @@ export const Plugin = define({ if (item.name !== undefined) provider.name = item.name if (item.package !== undefined) provider.package = item.package if (item.settings !== undefined) - provider.settings = ProviderV2.mergeOverlay(provider.settings, item.settings) - if (item.headers !== undefined) provider.headers = ProviderV2.mergeHeaders(provider.headers, item.headers) - if (item.body !== undefined) provider.body = ProviderV2.mergeOverlay(provider.body, item.body) + provider.settings = Provider.mergeOverlay(provider.settings, item.settings) + if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers) + if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body) }) for (const [id, config] of Object.entries(item.models ?? {})) { catalog.model.update(providerID, id, (model) => { @@ -62,9 +62,9 @@ export const Plugin = define({ model.compatibility = { ...model.compatibility, ...config.compatibility } if (config.package !== undefined) model.package = config.package if (config.settings !== undefined) - model.settings = ProviderV2.mergeOverlay(model.settings, config.settings) - if (config.headers !== undefined) model.headers = ProviderV2.mergeHeaders(model.headers, config.headers) - if (config.body !== undefined) model.body = ProviderV2.mergeOverlay(model.body, config.body) + model.settings = Provider.mergeOverlay(model.settings, config.settings) + if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers) + if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body) if (config.capabilities !== undefined) { model.capabilities = { tools: config.capabilities.tools, @@ -81,10 +81,10 @@ export const Plugin = define({ model.variants.push(existing) } if (variant.settings !== undefined) - existing.settings = ProviderV2.mergeOverlay(existing.settings, variant.settings) + existing.settings = Provider.mergeOverlay(existing.settings, variant.settings) if (variant.headers !== undefined) - existing.headers = ProviderV2.mergeHeaders(existing.headers, variant.headers) - if (variant.body !== undefined) existing.body = ProviderV2.mergeOverlay(existing.body, variant.body) + existing.headers = Provider.mergeHeaders(existing.headers, variant.headers) + if (variant.body !== undefined) existing.body = Provider.mergeOverlay(existing.body, variant.body) } } if (config.cost !== undefined) { diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index a790a7cc548..c0b62e00e75 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -1,6 +1,6 @@ export * as ConfigReferencePlugin from "./reference" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Stream } from "effect" import { Config } from "../../config" diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts index c4271c4843d..427a7f974b3 100644 --- a/packages/core/src/config/plugin/skill.ts +++ b/packages/core/src/config/plugin/skill.ts @@ -1,11 +1,11 @@ export * as ConfigSkillPlugin from "./skill" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { Effect, Stream } from "effect" import { Config } from "../../config" import { AbsolutePath } from "../../schema" -import { SkillV2 } from "../../skill" +import { Skill } from "../../skill" import { Global } from "@opencode-ai/util/global" import { Location } from "../../location" @@ -23,7 +23,7 @@ export const Plugin = define({ const items = loaded.entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) for (const directory of [...claude, ...agents]) { draft.source( - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")), }), @@ -31,10 +31,10 @@ export const Plugin = define({ } for (const directory of directories) { draft.source( - SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), ) draft.source( - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")), }), @@ -42,12 +42,12 @@ export const Plugin = define({ } for (const item of items) { if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { - draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) + draft.source(Skill.UrlSource.make({ type: "url", url: item })) continue } const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item draft.source( - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), }), diff --git a/packages/core/src/config/plugin/websearch.ts b/packages/core/src/config/plugin/websearch.ts index 1a063549fac..343d1dfd610 100644 --- a/packages/core/src/config/plugin/websearch.ts +++ b/packages/core/src/config/plugin/websearch.ts @@ -1,6 +1,6 @@ export * as ConfigWebSearchPlugin from "./websearch" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Stream } from "effect" import { Config } from "../../config" diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index 8441ef0828c..feb92a5c5e5 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -2,7 +2,7 @@ export * as ConfigProvider from "./provider" import { Schema } from "effect" import { Money } from "@opencode-ai/schema/money" -import { ModelV2 } from "../model" +import { Capabilities, Compatibility, Family, ID, VariantID } from "../model" const JsonRecord = Schema.Record(Schema.String, Schema.Json) @@ -12,17 +12,17 @@ export const Overlays = { body: JsonRecord.pipe(Schema.optional), } -export class Request extends Schema.Class("ConfigV2.Provider.Request")({ +export class Request extends Schema.Class("Config.Provider.Request")({ headers: Overlays.headers, body: Overlays.body, }) {} -class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ +class Cache extends Schema.Class("Config.Model.Cost.Cache")({ read: Money.USDPerMillionTokens.pipe(Schema.optional), write: Money.USDPerMillionTokens.pipe(Schema.optional), }) {} -class Cost extends Schema.Class("ConfigV2.Model.Cost")({ +class Cost extends Schema.Class("Config.Model.Cost")({ tier: Schema.Struct({ type: Schema.Literal("context"), size: Schema.Int, @@ -32,22 +32,22 @@ class Cost extends Schema.Class("ConfigV2.Model.Cost")({ cache: Cache.pipe(Schema.optional), }) {} -class Limit extends Schema.Class("ConfigV2.Model.Limit")({ +class Limit extends Schema.Class("Config.Model.Limit")({ context: Schema.Int.pipe(Schema.optional), input: Schema.Int.pipe(Schema.optional), output: Schema.Int.pipe(Schema.optional), }) {} -class Model extends Schema.Class("ConfigV2.Model")({ - modelID: ModelV2.ID.pipe(Schema.optional), - family: ModelV2.Family.pipe(Schema.optional), +class Model extends Schema.Class("Config.Model")({ + modelID: ID.pipe(Schema.optional), + family: Family.pipe(Schema.optional), name: Schema.String.pipe(Schema.optional), - compatibility: ModelV2.Compatibility.pipe(Schema.optional), + compatibility: Compatibility.pipe(Schema.optional), package: Schema.String.pipe(Schema.optional), ...Overlays, - capabilities: ModelV2.Capabilities.pipe(Schema.optional), + capabilities: Capabilities.pipe(Schema.optional), variants: Schema.Struct({ - id: ModelV2.VariantID, + id: VariantID, ...Overlays, }).pipe(Schema.Array, Schema.optional), cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), @@ -55,7 +55,7 @@ class Model extends Schema.Class("ConfigV2.Model")({ limit: Limit.pipe(Schema.optional), }) {} -export class Info extends Schema.Class("ConfigV2.Provider")({ +export class Info extends Schema.Class("Config.Provider")({ name: Schema.String.pipe(Schema.optional), env: Schema.String.pipe(Schema.Array, Schema.optional), package: Schema.String.pipe(Schema.optional), diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts index 4518eb4f87d..d2fabb0a899 100644 --- a/packages/core/src/config/reference.ts +++ b/packages/core/src/config/reference.ts @@ -2,14 +2,14 @@ export * as ConfigReference from "./reference" import { Schema } from "effect" -export class Git extends Schema.Class("ConfigV2.Reference.Git")({ +export class Git extends Schema.Class("Config.Reference.Git")({ repository: Schema.String, branch: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), }) {} -export class Local extends Schema.Class("ConfigV2.Reference.Local")({ +export class Local extends Schema.Class("Config.Reference.Local")({ path: Schema.String, description: Schema.String.pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/config/tool-output.ts b/packages/core/src/config/tool-output.ts index 53e4d4d088b..0eac04307dc 100644 --- a/packages/core/src/config/tool-output.ts +++ b/packages/core/src/config/tool-output.ts @@ -3,7 +3,7 @@ export * as ConfigToolOutput from "./tool-output" import { Schema } from "effect" import { PositiveInt } from "../schema" -export class Info extends Schema.Class("ConfigV2.ToolOutput")({ +export class Info extends Schema.Class("Config.ToolOutput")({ max_lines: PositiveInt.pipe(Schema.optional), max_bytes: PositiveInt.pipe(Schema.optional), }) {} diff --git a/packages/core/src/config/warming.ts b/packages/core/src/config/warming.ts index 5e3b7d4a913..cb90afc2eaf 100644 --- a/packages/core/src/config/warming.ts +++ b/packages/core/src/config/warming.ts @@ -2,7 +2,7 @@ export * as ConfigWarming from "./warming" import { Schema } from "effect" -export class Info extends Schema.Class("ConfigV2.Warming")({ +export class Info extends Schema.Class("Config.Warming")({ prompt: Schema.String.pipe(Schema.optional).annotate({ description: "Prompt sent for keep-alive requests", }), diff --git a/packages/core/src/config/watcher.ts b/packages/core/src/config/watcher.ts index 2df6c876bfc..be5c91a9bfe 100644 --- a/packages/core/src/config/watcher.ts +++ b/packages/core/src/config/watcher.ts @@ -2,6 +2,6 @@ export * as ConfigWatcher from "./watcher" import { Schema } from "effect" -export class Info extends Schema.Class("ConfigV2.Watcher")({ +export class Info extends Schema.Class("Config.Watcher")({ ignore: Schema.String.pipe(Schema.Array, Schema.optional), }) {} diff --git a/packages/core/src/control-plane/move-session.ts b/packages/core/src/control-plane/move-session.ts index b5d229b7157..220c503f615 100644 --- a/packages/core/src/control-plane/move-session.ts +++ b/packages/core/src/control-plane/move-session.ts @@ -5,8 +5,8 @@ import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Git } from "../git" import { Global } from "@opencode-ai/util/global" -import { ProjectV2 } from "../project" -import { SessionV2 } from "../session" +import { Project } from "../project" +import { Session } from "../session" import { SessionExecution } from "../session/execution" import { SessionSchema } from "../session/schema" import { SessionStore } from "../session/store" @@ -28,8 +28,8 @@ export type Input = typeof Input.Type export class DestinationProjectMismatchError extends Schema.TaggedErrorClass()( "MoveSession.DestinationProjectMismatchError", { - expected: ProjectV2.ID, - actual: ProjectV2.ID, + expected: Project.ID, + actual: Project.ID, }, ) {} @@ -64,12 +64,12 @@ export class ResetSourceChangesError extends Schema.TaggedErrorClass().primaryKey(), + id: text().$type().primaryKey(), type: text().notNull(), name: text().notNull().default(""), branch: text(), directory: text(), extra: text({ mode: "json" }), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), time_used: integer() diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index be8b97db3d6..47f35c809b1 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -46,7 +46,7 @@ export interface Interface { readonly remove: (id: ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Credential") {} +export class Service extends Context.Service()("@opencode/Credential") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index b45958867ef..3150011717b 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -20,7 +20,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/storage/Database") {} +export class Service extends Context.Service()("@opencode/storage/Database") {} const databaseLayer = Layer.effect( Service, diff --git a/packages/core/src/event-logger.ts b/packages/core/src/event-logger.ts index 12cc9c490ec..eebdb10101e 100644 --- a/packages/core/src/event-logger.ts +++ b/packages/core/src/event-logger.ts @@ -2,7 +2,7 @@ export * as EventLogger from "./event-logger" import { Effect, Layer } from "effect" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" const Types = new Set([ "agent.updated", @@ -13,12 +13,12 @@ const Types = new Set([ export const layer = Layer.effectDiscard( Effect.gen(function* () { - const events = yield* EventV2.Service - const unsubscribe = yield* events.listen((event) => + const bus = yield* Bus.Service + const unsubscribe = yield* bus.listen((event) => Types.has(event.type) ? Effect.logInfo("event", { event }) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) }), ) -export const node = makeGlobalNode({ name: "event-logger", layer, deps: [EventV2.node] }) +export const node = makeGlobalNode({ name: "event-logger", layer, deps: [Bus.node] }) diff --git a/packages/core/src/event/sql.ts b/packages/core/src/event/sql.ts index 17c88cefd3c..966003c3179 100644 --- a/packages/core/src/event/sql.ts +++ b/packages/core/src/event/sql.ts @@ -1,5 +1,5 @@ import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core" -import type { EventV2 } from "../event" +import { Event } from "@opencode-ai/schema/event" export const EventSequenceTable = sqliteTable("event_sequence", { aggregate_id: text().notNull().primaryKey(), @@ -10,7 +10,7 @@ export const EventSequenceTable = sqliteTable("event_sequence", { export const EventTable = sqliteTable( "event", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), aggregate_id: text() .notNull() .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), diff --git a/packages/core/src/file-mutation.ts b/packages/core/src/file-mutation.ts index d11144d5767..82312b22da6 100644 --- a/packages/core/src/file-mutation.ts +++ b/packages/core/src/file-mutation.ts @@ -64,7 +64,7 @@ export interface Interface { readonly remove: (input: RemoveInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/FileMutation") {} +export class Service extends Context.Service()("@opencode/FileMutation") {} /** * Serialize file changes by canonical target. Conditional writes compare and @@ -194,12 +194,12 @@ function sameBytes(left: Uint8Array, right: Uint8Array) { export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] }) /** - * Deferred until the corresponding V2 integrations exist. + * Deferred until the corresponding integrations exist. */ -// TODO: Add formatter integration after V2 formatter runtime exists. -// TODO: Publish watcher/file-edit events after V2 watcher integration exists. -// TODO: Add snapshots / undo after V2 snapshot design exists. -// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. +// TODO: Add formatter integration after formatter runtime exists. +// TODO: Publish watcher/file-edit events after watcher integration exists. +// TODO: Add snapshots / undo after snapshot design exists. +// TODO: Notify LSP and collect diagnostics after LSP runtime exists. // TODO: Design multi-file transactions / rollback if patch needs atomic edits. // Until then, edits are sequential and report partial application. // TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement. diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 3d0f54af6cb..0e5f24e677c 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -56,7 +56,7 @@ export interface Interface { readonly grep: (input: GrepInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/FileSystem") {} +export class Service extends Context.Service()("@opencode/FileSystem") {} const baseLayer = Layer.effect( Service, diff --git a/packages/core/src/filesystem/location-watcher.ts b/packages/core/src/filesystem/location-watcher.ts index ff18d2cbee8..25c98320599 100644 --- a/packages/core/src/filesystem/location-watcher.ts +++ b/packages/core/src/filesystem/location-watcher.ts @@ -6,7 +6,7 @@ import { FileSystem } from "@opencode-ai/schema/filesystem" import os from "os" import path from "path" import { Config } from "../config" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Git } from "../git" import { Location } from "../location" @@ -30,12 +30,12 @@ const layer = Layer.effect( Effect.gen(function* () { const location = yield* Location.Service const watcher = yield* Watcher.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const fs = yield* FSUtil.Service const git = yield* Git.Service const configService = yield* Config.Service const publish = (update: { type: "create" | "update" | "delete"; path: string }) => - events.publish(FileSystem.Event.Changed, { + bus.publish(FileSystem.Event.Changed, { file: update.path, event: update.type === "create" ? "add" : update.type === "update" ? "change" : "unlink", }) @@ -86,5 +86,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, EventV2.node], + deps: [Watcher.node, FSUtil.node, Location.node, Config.node, Git.node, Bus.node], }) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 2bc92a5994d..23473381e54 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -22,7 +22,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/FileSystem/Search") {} +export class Service extends Context.Service()("@opencode/FileSystem/Search") {} export const ripgrepLayer = Layer.effect( Service, diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index bef4ad4f542..0a23ef3efe9 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -3,7 +3,7 @@ export * as Form from "./form" import { Form } from "@opencode-ai/schema/form" import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" const RETENTION = Duration.minutes(10) @@ -32,7 +32,7 @@ export type Answer = typeof Answer.Type export const Reply = Form.Reply export type Reply = typeof Reply.Type -export const Event = Form.Event +export { Event } from "@opencode-ai/schema/form" export class NotFoundError extends Schema.TaggedErrorClass()("Form.NotFoundError", { id: ID, @@ -99,7 +99,7 @@ interface Entry { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const forms = yield* Cache.makeWith( () => Effect.die(new Error("Form cache must be used via set/getSuccess, never get")), { @@ -141,7 +141,7 @@ export const layer = Layer.effect( deferred: yield* Deferred.make(), } yield* Cache.set(forms, id, entry) - yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id))) + yield* bus.publish(Form.Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id))) return form }), ), @@ -183,7 +183,7 @@ export const layer = Layer.effect( const invalid = validateAnswer(entry.form, input.answer) if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid }) const next: TerminalState = { status: "answered", answer: input.answer } - yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer }) + yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer }) yield* Cache.set(forms, input.id, { ...entry, state: next }) yield* Deferred.succeed(entry.deferred, next) }), @@ -196,7 +196,7 @@ export const layer = Layer.effect( const entry = yield* find(id) if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id }) const next: TerminalState = { status: "cancelled" } - yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID }) + yield* bus.publish(Form.Event.Cancelled, { id, sessionID: entry.form.sessionID }) yield* Cache.set(forms, id, { ...entry, state: next }) yield* Deferred.succeed(entry.deferred, next) }), @@ -221,7 +221,7 @@ export const layer = Layer.effect( export const locationLayer = layer -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) function validateAnswer(form: Info, answer: Answer) { const fields = new Map(form.fields.map((field) => [field.key, field] as const)) diff --git a/packages/core/src/generate.ts b/packages/core/src/generate.ts index 432827bce0b..ceec4de4449 100644 --- a/packages/core/src/generate.ts +++ b/packages/core/src/generate.ts @@ -5,11 +5,11 @@ import { Context, Effect, Layer, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "./effect/app-node-platform" import { ModelResolver } from "./model-resolver" -import { ModelV2 } from "./model" +import { Model } from "./model" export interface TextInput { readonly prompt: string - readonly model?: ModelV2.Ref + readonly model?: Model.Ref } export class ModelSelectionError extends Schema.TaggedErrorClass()( @@ -28,7 +28,7 @@ export interface Interface { readonly text: (input: TextInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Generate") {} +export class Service extends Context.Service()("@opencode/Generate") {} export const layer = Layer.effect( Service, diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 8802ca7317a..c874ead415c 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -189,7 +189,7 @@ export interface Interface { } } -export class Service extends Context.Service()("@opencode/GitV2") {} +export class Service extends Context.Service()("@opencode/Git") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/github-copilot/models.ts b/packages/core/src/github-copilot/models.ts index 52f98753b5f..34dfe2e4cc8 100644 --- a/packages/core/src/github-copilot/models.ts +++ b/packages/core/src/github-copilot/models.ts @@ -2,8 +2,8 @@ export * as CopilotModels from "./models" import { Money } from "@opencode-ai/schema/money" import { Option, Schema } from "effect" -import { ModelV2 } from "../model" -import { ProviderV2 } from "../provider" +import { Model } from "../model" +import { Provider } from "../provider" const RemoteModel = Schema.Struct({ model_picker_enabled: Schema.Boolean, @@ -70,7 +70,7 @@ type UsableModel = RemoteModel & { } } -export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly ModelV2.Info[]) { +export async function get(baseURL: string, headers: RequestInit["headers"], existing: readonly Model.Info[]) { const response = await fetch(`${baseURL}/models`, { headers, signal: AbortSignal.timeout(5_000), @@ -97,7 +97,7 @@ export async function get(baseURL: string, headers: RequestInit["headers"], exis } for (const [id, model] of remote) { - const key = ModelV2.ID.make(id) + const key = Model.ID.make(id) if (result.has(key)) continue result.set(key, build(key, model, baseURL)) } @@ -114,7 +114,7 @@ function usable(model: RemoteModel): model is UsableModel { ) } -function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: ModelV2.Info) { +function build(id: Model.ID, remote: UsableModel, baseURL: string, previous?: Model.Info) { const messages = remote.supported_endpoints?.includes("/v1/messages") ?? false const endpoint = messages ? "messages" @@ -134,15 +134,15 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: : remote.version const released = previous?.time.released || Date.parse(version) - return ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, id), + return Model.Info.make({ + ...Model.Info.default(Provider.ID.githubCopilot, id), id, - modelID: ModelV2.ID.make(remote.id), - providerID: ProviderV2.ID.githubCopilot, - family: previous?.family ?? ModelV2.Family.make(remote.capabilities.family), + modelID: Model.ID.make(remote.id), + providerID: Provider.ID.githubCopilot, + family: previous?.family ?? Model.Family.make(remote.capabilities.family), name: previous?.name ?? remote.name, - package: ProviderV2.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"), - settings: ProviderV2.mergeOverlay(previous?.settings, { + package: Provider.aisdk(messages ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot"), + settings: Provider.mergeOverlay(previous?.settings, { baseURL: messages ? `${baseURL}/v1` : baseURL, ...(endpoint ? { endpoint } : {}), }), @@ -175,11 +175,11 @@ function build(id: ModelV2.ID, remote: UsableModel, baseURL: string, previous?: }) } -function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variants"] { +function variants(remote: UsableModel, messages: boolean): Model.Info["variants"] { const efforts = remote.capabilities.supports.reasoning_effort ?? [] if (!messages && efforts.length) { return efforts.map((effort) => ({ - id: ModelV2.VariantID.make(effort), + id: Model.VariantID.make(effort), settings: { reasoningEffort: effort, reasoningSummary: "auto", @@ -189,7 +189,7 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant } if (efforts.length && remote.capabilities.supports.adaptive_thinking) { return efforts.map((effort) => ({ - id: ModelV2.VariantID.make(effort), + id: Model.VariantID.make(effort), settings: { thinking: { type: "adaptive", @@ -203,11 +203,11 @@ function variants(remote: UsableModel, messages: boolean): ModelV2.Info["variant if (max === undefined) return [] return [ { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinking: { type: "enabled", budgetTokens: max - 1 } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinking: { type: "enabled", budgetTokens: Math.floor(max / 2) } }, }, ] diff --git a/packages/core/src/instruction-discovery.ts b/packages/core/src/instruction-discovery.ts index 910e92f8029..834d1e0cc00 100644 --- a/packages/core/src/instruction-discovery.ts +++ b/packages/core/src/instruction-discovery.ts @@ -26,7 +26,7 @@ export const Options = Schema.Struct({ }) export type Options = typeof Options.Type -export class Service extends Context.Service()("@opencode/v2/InstructionDiscovery") {} +export class Service extends Context.Service()("@opencode/InstructionDiscovery") {} export const layer = (options?: Options) => Layer.effect( Service, diff --git a/packages/core/src/instructions/builtins.ts b/packages/core/src/instructions/builtins.ts index 03b38d75b7b..fdb8f87fcdf 100644 --- a/packages/core/src/instructions/builtins.ts +++ b/packages/core/src/instructions/builtins.ts @@ -10,7 +10,7 @@ export interface Interface { readonly load: (sessionID: SessionSchema.ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/InstructionBuiltIns") {} +export class Service extends Context.Service()("@opencode/InstructionBuiltIns") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 5c9548bccb0..a14635a5764 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -20,7 +20,7 @@ import { import { Integration } from "@opencode-ai/schema/integration" import { Credential } from "./credential" import { State } from "./state" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { IntegrationConnection } from "./integration/connection" import { AppProcess } from "@opencode-ai/util/process" import { ChildProcess } from "effect/unstable/process" @@ -129,7 +129,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass { } } -export class Service extends Context.Service()("@opencode/v2/Integration") {} +export class Service extends Context.Service()("@opencode/Integration") {} const attemptLifetime = Duration.toMillis(Duration.minutes(10)) const terminalRetention = Duration.toMillis(Duration.minutes(1)) @@ -271,7 +271,7 @@ const layer = Layer.effect( Service, Effect.gen(function* () { const credentials = yield* Credential.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const processes = yield* AppProcess.Service const scope = yield* Scope.Scope const attempts = SynchronizedRef.makeUnsafe(new Map()) @@ -338,7 +338,7 @@ const layer = Layer.effect( }, }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Integration.Event.Updated, {}).pipe(Effect.asVoid), }) const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => { @@ -433,8 +433,8 @@ const layer = Layer.effect( // Persisting attempts cannot be cancelled, expired, or claimed again. yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal)) if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause) - yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID }) + yield* bus.publish(Integration.Event.Updated, {}) }).pipe(Effect.ensuring(close(attempt.scope))) }), ) @@ -489,8 +489,8 @@ const layer = Layer.effect( yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal)) yield* close(attempt.scope) if (Exit.isFailure(persistence)) return - yield* events.publish(Event.ConnectionUpdated, { integrationID: attempt.integrationID }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: attempt.integrationID }) + yield* bus.publish(Integration.Event.Updated, {}) }), ) }) @@ -706,24 +706,24 @@ const layer = Layer.effect( label: input.label, value: Credential.Key.make({ type: "key", key: input.key }), }) - yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: input.integrationID }) + yield* bus.publish(Integration.Event.Updated, {}) }), update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) { const credential = yield* credentials.get(credentialID) yield* credentials.update(credentialID, updates) if (credential) { - yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID }) } - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.Updated, {}) }), remove: Effect.fn("Integration.connection.remove")(function* (credentialID) { const credential = yield* credentials.get(credentialID) yield* credentials.remove(credentialID) if (credential) { - yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID: credential.integrationID }) } - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Integration.Event.Updated, {}) }), }, oauth: { @@ -809,5 +809,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [Credential.node, EventV2.node, AppProcess.node], + deps: [Credential.node, Bus.node, AppProcess.node], }) diff --git a/packages/core/src/kv.ts b/packages/core/src/kv.ts index 92b7bd9c2c0..505beff7ec0 100644 --- a/packages/core/src/kv.ts +++ b/packages/core/src/kv.ts @@ -14,7 +14,7 @@ export interface Interface { readonly remove: (key: string) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/KV") {} +export class Service extends Context.Service()("@opencode/KV") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts index 0de177aafe4..313b4dc79ef 100644 --- a/packages/core/src/location-mutation.ts +++ b/packages/core/src/location-mutation.ts @@ -60,7 +60,7 @@ export interface Interface { readonly resolve: (input: ResolveInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/LocationMutation") {} +export class Service extends Context.Service()("@opencode/LocationMutation") {} interface ResolvedPath { readonly canonical: string diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 3cce750142c..7a39aa278a1 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -1,13 +1,12 @@ import { Effect, Layer, LayerMap } from "effect" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" -import { CodeMode } from "./codemode" -import { CommandV2 } from "./command" +import { Command } from "./command" import { Config } from "./config" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Node } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { FileMutation } from "./file-mutation" import { FileSystem } from "./filesystem" import { FileSystemSearch } from "./filesystem/search" @@ -21,12 +20,12 @@ import { LocationMutation } from "./location-mutation" import { LocationServiceMap } from "./location-service-map" import { ModelResolver } from "./model-resolver" import { MCP } from "./mcp/index" -import { PermissionV2 } from "./permission" -import { PluginV2 } from "./plugin" +import { Permission } from "./permission" +import { Plugin } from "./plugin" import { PluginSupervisor } from "./plugin/supervisor" import { ProjectCopy } from "./project/copy" import { Pty } from "./pty" -import { QuestionV2 } from "./question" +import { Question } from "./question" import { Shell } from "./shell" import { Reference } from "./reference" import { WebSearch } from "./websearch" @@ -35,7 +34,7 @@ import { SessionRunnerLLM } from "./session/runner/llm" import { SessionRunnerModel } from "./session/runner/model" import { SessionCompaction } from "./session/compaction" import { SessionTitle } from "./session/title" -import { SkillV2 } from "./skill" +import { Skill } from "./skill" import { SkillInstructions } from "./skill/instructions" import { Snapshot } from "./snapshot" import { InstructionDiscovery } from "./instruction-discovery" @@ -45,8 +44,7 @@ import { SessionInstructions } from "./session/instructions" import { SessionGenerateNode } from "./session/generate-node" import { McpTool } from "./tool/mcp" import { ReadToolFileSystem } from "./tool/read-filesystem" -import { ToolRegistry } from "./tool/registry" -import { ToolOutputStore } from "./tool-output-store" +import { Tool } from "./tool" import { Vcs } from "./vcs" export { LocationServiceMap } from "./location-service-map" @@ -54,15 +52,15 @@ export { LocationServiceMap } from "./location-service-map" const locationServiceNodes = [ Location.node, Config.node, - AgentV2.node, - CommandV2.node, + Agent.node, + Command.node, Reference.node, WebSearch.node, Integration.node, Catalog.node, ModelResolver.node, AISDK.node, - PluginV2.node, + Plugin.node, PluginSupervisor.node, ProjectCopy.node, ProjectCopy.refreshNode, @@ -70,23 +68,20 @@ const locationServiceNodes = [ FileSystem.node, Pty.node, Shell.node, - SkillV2.node, - CodeMode.node, + Skill.node, InstructionBuiltIns.node, InstructionDiscovery.node, LocationMutation.node, FileMutation.node, MCP.node, - PermissionV2.node, - ToolOutputStore.node, - ToolRegistry.node, - ToolRegistry.toolsNode, + Permission.node, + Tool.node, Image.node, SkillInstructions.node, ReferenceInstructions.node, InstructionEntry.node, Form.node, - QuestionV2.node, + Question.node, Generate.node, SessionGenerateNode.node, ReadToolFileSystem.node, diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 225919c27f4..a325311ade8 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -9,7 +9,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Config } from "../config" import { ConfigMCP } from "../config/mcp" import { Credential } from "../credential" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { Form } from "../form" import { Integration } from "../integration" import { IntegrationConnection } from "../integration/connection" @@ -155,7 +155,7 @@ export interface Interface { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/MCP") {} +export class Service extends Context.Service()("@opencode/MCP") {} export const Options = Schema.Struct({ clientInfo: Schema.optional( @@ -172,7 +172,7 @@ export const layer = (options?: Options) => Layer.effect( Effect.gen(function* () { const config = yield* Config.Service const location = yield* Location.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const forms = yield* Form.Service const integration = yield* Integration.Service const credentials = yield* Credential.Service @@ -433,9 +433,9 @@ export const layer = (options?: Options) => Layer.effect( Effect.map((defs) => { entry.prompts = defs.map((def) => toPrompt(name, def)) }), - Effect.andThen(events.publish(Command.Event.Updated, {})), + Effect.andThen(bus.publish(Command.Event.Updated, {})), Effect.catch(() => - Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))), + Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(bus.publish(Command.Event.Updated, {}))), ), ) @@ -460,10 +460,10 @@ export const layer = (options?: Options) => Layer.effect( entry.tools = undefined entry.prompts = undefined entry.status = { status: "failed", error: "Connection closed" } - yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }), ), ) @@ -471,12 +471,12 @@ export const layer = (options?: Options) => Layer.effect( connection.onToolsChanged(() => live( refreshTools(name, entry, connection).pipe( - Effect.andThen(events.publish(McpEvent.ToolsChanged, { server: name })), + Effect.andThen(bus.publish(McpEvent.ToolsChanged, { server: name })), ), ), ) connection.onPromptsChanged(() => live(refreshPrompts(name, entry, connection))) - connection.onResourcesChanged(() => live(events.publish(McpEvent.ResourcesChanged, { server: name }))) + connection.onResourcesChanged(() => live(bus.publish(McpEvent.ResourcesChanged, { server: name }))) } const serverLog = (server: ServerName, message: MCPClient.LogMessage) => { @@ -502,7 +502,7 @@ export const layer = (options?: Options) => Layer.effect( // Announce the handshake so connect() and credential reconnects don't show a stale // disabled/failed status for the duration of the connection attempt. entry.status = { status: "pending" } - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) const scope = yield* Scope.fork(root) entry.scope = scope const authProvider = yield* connectProvider(entry) @@ -530,9 +530,9 @@ export const layer = (options?: Options) => Layer.effect( // Announce the new tool set so the tool registry registers it. A server that finishes connecting // after the initial registration sweep and emits no list-changed notification would otherwise // stay invisible to the model. - yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) whenLive(name, entry, result.value.connection)(refreshPrompts(name, entry, result.value.connection)) return } @@ -544,7 +544,7 @@ export const layer = (options?: Options) => Layer.effect( ? { status: "needs_auth" } : { status: "failed", error: error instanceof Error ? error.message : String(error) } yield* Effect.logWarning("mcp connect failed", { server: name, status: entry.status }) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }).pipe(Effect.ensuring(Deferred.succeed(entry.startup, undefined))) const stopServer = Effect.fnUntraced(function* (name: ServerName, entry: ServerEntry) { @@ -555,9 +555,9 @@ export const layer = (options?: Options) => Layer.effect( entry.tools = undefined entry.prompts = undefined yield* Scope.close(scope, Exit.void) - yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) - yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore) }) // Disabled servers settle their startup immediately so queries never block on them. @@ -587,7 +587,7 @@ export const layer = (options?: Options) => Layer.effect( }).pipe(locks.withLock(name)) }) fork( - events.subscribe(Integration.Event.ConnectionUpdated).pipe( + bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => owned.has(event.data.integrationID)), Stream.runForEach((event) => Effect.sync(() => fork(reconnect(event.data.integrationID)))), Effect.ignore, @@ -632,7 +632,7 @@ export const layer = (options?: Options) => Layer.effect( yield* register(name, entry) if (config.disabled) { entry.status = { status: "disabled" } - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) return } yield* startServer(name, entry) @@ -657,7 +657,7 @@ export const layer = (options?: Options) => Layer.effect( const target = yield* requireServer(name) yield* stopServer(name, target.entry) target.entry.status = { status: "disabled" } - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }).pipe(locks.withLock(name)) }), remove: Effect.fn("MCP.remove")(function* (server) { @@ -670,7 +670,7 @@ export const layer = (options?: Options) => Layer.effect( // Credentials are kept: they are keyed by name + url, so re-adding the same server // reuses them without forcing re-auth, matching add()'s replacement semantics. runtime.delete(name) - yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) + yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore) }).pipe(locks.withLock(name)) }), tools: Effect.fn("MCP.tools")(function* () { @@ -793,7 +793,7 @@ export function configured(options?: Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [Config.node, Location.node, EventV2.node, Form.node, Integration.node, Credential.node], + deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node], }) } diff --git a/packages/core/src/mcp/instructions.ts b/packages/core/src/mcp/instructions.ts index d0cf45ec6dc..198272f59d2 100644 --- a/packages/core/src/mcp/instructions.ts +++ b/packages/core/src/mcp/instructions.ts @@ -2,8 +2,8 @@ export * as McpInstructions from "./instructions" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Schema } from "effect" -import { AgentV2 } from "../agent" -import { PermissionV2 } from "../permission" +import { Agent } from "../agent" +import { Permission } from "../permission" import { McpTool } from "../tool/mcp" import { MCP } from "./index" import { Instructions } from "../instructions/index" @@ -55,10 +55,10 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray Effect.Effect + readonly load: (agent: Agent.Selection) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/McpInstructions") {} +export class Service extends Context.Service()("@opencode/McpInstructions") {} export const layer = Layer.effect( Service, @@ -83,7 +83,7 @@ export const layer = Layer.effect( const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], { concurrency: "unbounded", }) - const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny" + const canExecute = Permission.evaluate("execute", "*", agent.permissions).effect !== "deny" // Instructions are useful only when this agent can reach at least one server tool. const visible = instructions .flatMap((item) => { @@ -93,7 +93,7 @@ export const layer = Layer.effect( if ( !owned.some( (tool) => - PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", + Permission.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny", ) ) return [] diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 44faea8b12d..024ab8b048f 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -15,17 +15,17 @@ import { AISDK } from "./aisdk" import { Catalog } from "./catalog" import { Credential } from "./credential" import { Integration } from "./integration" -import { ModelV2 } from "./model" +import { Capabilities, ID, Info, Ref, VariantID } from "./model" import { Npm } from "@opencode-ai/util/npm" import { OpenAICodex } from "./plugin/provider/openai-codex" -import { ProviderV2 } from "./provider" +import { Provider } from "./provider" export class VariantUnavailableError extends Schema.TaggedErrorClass()( "SessionRunnerModel.VariantUnavailableError", { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, - variant: ModelV2.VariantID, + providerID: Provider.ID, + modelID: ID, + variant: VariantID, }, ) { override get message() { @@ -36,8 +36,8 @@ export class VariantUnavailableError extends Schema.TaggedErrorClass()( "SessionRunnerModel.UnsupportedPackageError", { - providerID: ProviderV2.ID, - modelID: ModelV2.ID, + providerID: Provider.ID, + modelID: ID, package: Schema.String, }, ) { @@ -52,21 +52,21 @@ export interface Resolved { /** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */ readonly model: Model /** Selected catalog identity. Durable records and displays must use this, never the API model id. */ - readonly ref: ModelV2.Ref + readonly ref: Ref /** Catalog capabilities used to shape requests before provider lowering. */ - readonly capabilities: ModelV2.Capabilities + readonly capabilities: Capabilities /** Catalog pricing in dollars per million tokens. */ - readonly cost: ModelV2.Info["cost"] + readonly cost: Info["cost"] } export interface Interface { - readonly resolve: (requested?: ModelV2.Ref) => Effect.Effect - readonly resolveModel: (model: ModelV2.Info, variant?: ModelV2.VariantID) => Effect.Effect + readonly resolve: (requested?: Ref) => Effect.Effect + readonly resolveModel: (model: Info, variant?: VariantID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/ModelResolver") {} +export class Service extends Context.Service()("@opencode/ModelResolver") {} -const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { +const apiKey = (model: Info, credential?: Credential.Value) => { if (credential?.type === "key") return Auth.value(credential.key) if (credential?.type === "oauth") return Auth.value(credential.access) const value = model.settings?.apiKey @@ -74,7 +74,7 @@ const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => { return undefined } -const withDefaults = (model: ModelV2.Info, route: AnyRoute) => +const withDefaults = (model: Info, route: AnyRoute) => route.with({ provider: model.providerID, endpoint: typeof model.settings?.baseURL === "string" ? { baseURL: model.settings.baseURL } : undefined, @@ -84,8 +84,8 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => limits: { context: model.limit.context, output: model.limit.output }, }) -const providerHeaders = (model: ModelV2.Info) => { - const packageName = ProviderV2.packageName(model.package) +const providerHeaders = (model: Info) => { + const packageName = Provider.packageName(model.package) const generated = new Map() if (packageName === "@ai-sdk/openai" && typeof model.settings?.organization === "string") generated.set("OpenAI-Organization", model.settings.organization) @@ -93,16 +93,16 @@ const providerHeaders = (model: ModelV2.Info) => { generated.set("OpenAI-Project", model.settings.project) if (packageName === "@ai-sdk/anthropic" && typeof model.settings?.authToken === "string") generated.set("Authorization", `Bearer ${model.settings.authToken}`) - return ProviderV2.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) + return Provider.mergeHeaders(generated.size === 0 ? undefined : Object.fromEntries(generated), model.headers) } const providerOptions = ( - model: ModelV2.Info, + model: Info, ): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => { - if (!ProviderV2.isAISDK(model.package) || model.settings === undefined) return undefined + if (!Provider.isAISDK(model.package) || model.settings === undefined) return undefined const { apiKey: _, baseURL: _baseURL, ...settings } = model.settings if (Object.keys(settings).length === 0) return undefined - const packageName = ProviderV2.packageName(model.package) + const packageName = Provider.packageName(model.package) if (packageName === "@ai-sdk/openai") return { openai: settings } if (packageName === "@ai-sdk/anthropic") return { anthropic: settings } if (packageName === "@ai-sdk/openai-compatible") return { openai: settings } @@ -110,9 +110,9 @@ const providerOptions = ( } export const withVariant = ( - model: ModelV2.Info, - variantID: ModelV2.VariantID | undefined, -): Effect.Effect => { + model: Info, + variantID: VariantID | undefined, +): Effect.Effect => { const id = variantID === "default" ? undefined : variantID const variant = model.variants?.find((item) => item.id === id) if (!variant && variantID !== undefined && variantID !== "default") @@ -126,37 +126,37 @@ export const withVariant = ( return Effect.succeed( variant ? produce(model, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, variant.settings) - draft.headers = ProviderV2.mergeHeaders(draft.headers, variant.headers) - draft.body = ProviderV2.mergeOverlay(draft.body, variant.body) + draft.settings = Provider.mergeOverlay(draft.settings, variant.settings) + draft.headers = Provider.mergeHeaders(draft.headers, variant.headers) + draft.body = Provider.mergeOverlay(draft.body, variant.body) }) : model, ) } export interface Dependencies { - readonly loadPackage?: (specifier: string) => Effect.Effect - readonly loadAISDK?: (model: ModelV2.Info) => Effect.Effect + readonly loadPackage?: (specifier: string) => Effect.Effect + readonly loadAISDK?: (model: Info) => Effect.Effect } export const fromCatalogModel = ( - model: ModelV2.Info, + model: Info, credential?: Credential.Value, dependencies?: Dependencies, ): Effect.Effect => { const resolved = produce(model, (draft) => { if (draft.settings?.apiKey === "") delete draft.settings.apiKey if (credential?.type === "key" && credential.metadata !== undefined) - draft.body = ProviderV2.mergeOverlay(draft.body, credential.metadata) + draft.body = Provider.mergeOverlay(draft.body, credential.metadata) }) - const packageName = ProviderV2.packageName(resolved.package) + const packageName = Provider.packageName(resolved.package) const key = apiKey(resolved, credential) - if (OpenAICodex.isChatGPT(credential) && !ProviderV2.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { + if (OpenAICodex.isChatGPT(credential) && !Provider.isAISDK(resolved.package) && isNativeOpenAI(resolved.package)) { return Effect.succeed(codexModel(resolved, credential, key)) } - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { + if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai") { if (OpenAICodex.isChatGPT(credential)) return Effect.succeed(codexModel(resolved, credential, key)) return Effect.succeed( withDefaults(resolved, OpenAIResponses.route) @@ -164,7 +164,7 @@ export const fromCatalogModel = ( .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), ) } - if (ProviderV2.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { + if (Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/anthropic") { return Effect.succeed( withDefaults(resolved, AnthropicMessages.route) .with({ auth: key === undefined ? Auth.none : Auth.header("x-api-key", key) }) @@ -172,7 +172,7 @@ export const fromCatalogModel = ( ) } if ( - ProviderV2.isAISDK(resolved.package) && + Provider.isAISDK(resolved.package) && packageName === "@ai-sdk/openai-compatible" && typeof resolved.settings?.baseURL === "string" ) { @@ -182,10 +182,10 @@ export const fromCatalogModel = ( .model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }), ) } - if (ProviderV2.isAISDK(resolved.package)) { + if (Provider.isAISDK(resolved.package)) { if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved)) const runtime = produce(resolved, (draft) => { - draft.settings = ProviderV2.mergeOverlay(draft.settings, { + draft.settings = Provider.mergeOverlay(draft.settings, { ...(credential?.type === "key" ? { apiKey: credential.key } : {}), ...(credential?.type === "oauth" ? { apiKey: credential.access } : {}), ...credential?.metadata, @@ -197,7 +197,7 @@ export const fromCatalogModel = ( const specifier = resolved.package return Effect.gen(function* () { - const module = yield* (dependencies?.loadPackage ?? ProviderV2.loadPackage)(specifier).pipe( + const module = yield* (dependencies?.loadPackage ?? Provider.loadPackage)(specifier).pipe( Effect.mapError(() => unsupported(resolved)), ) const configured = { ...resolved.settings, ...credential?.metadata } @@ -249,7 +249,7 @@ const withoutNativeAuthSettings = (settings: Record) => { } const codexModel = ( - model: ModelV2.Info, + model: Info, credential: Credential.Value | undefined, key: ReturnType | undefined, ) => { @@ -264,7 +264,7 @@ const codexModel = ( .model({ id: model.modelID ?? model.id, compatibility: model.compatibility }) } -const unsupported = (model: ModelV2.Info) => +const unsupported = (model: Info) => new UnsupportedPackageError({ providerID: model.providerID, modelID: model.id, @@ -272,13 +272,13 @@ const unsupported = (model: ModelV2.Info) => }) export const resolveModel = ( - model: ModelV2.Info, - variant: ModelV2.VariantID | undefined, + model: Info, + variant: VariantID | undefined, credential?: Credential.Value, dependencies?: Dependencies, ) => withVariant(model, variant).pipe(Effect.flatMap((model) => fromCatalogModel(model, credential, dependencies))) -export const supported = (model: ModelV2.Info) => Boolean(model.package) +export const supported = (model: Info) => Boolean(model.package) /** Resolves catalog selections into runtime models for the current Location. */ export const layer = Layer.effect( @@ -289,8 +289,8 @@ export const layer = Layer.effect( const npm = yield* Npm.Service const aisdk = yield* AISDK.Service const load = Effect.fn("ModelResolver.resolveModel")(function* ( - selected: ModelV2.Info, - variant?: ModelV2.VariantID, + selected: Info, + variant?: VariantID, ) { const provider = yield* catalog.provider.get(selected.providerID) const connection = yield* integrations.connection.active( @@ -301,13 +301,13 @@ export const layer = Layer.effect( variant, connection ? yield* integrations.connection.resolve(connection) : undefined, { - loadPackage: (specifier) => ProviderV2.loadPackage(specifier, npm), + loadPackage: (specifier) => Provider.loadPackage(specifier, npm), loadAISDK: (model) => aisdk.model(model), }, ) return { model, - ref: ModelV2.Ref.make({ + ref: Ref.make({ id: selected.id, providerID: selected.providerID, ...(variant === undefined ? {} : { variant }), diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index e8045f11043..36c0be16019 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,5 +1,5 @@ import { Model } from "@opencode-ai/schema/model" -import { ProviderV2 } from "./provider" +import { Provider } from "./provider" import type { DeepMutable } from "./schema" export const ID = Model.ID @@ -37,12 +37,12 @@ export function compatibility(input: unknown): Compatibility | undefined { return typeof input.field === "string" ? { reasoningField: input.field } : undefined } -export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { +export function parse(input: string): { providerID: Provider.ID; modelID: ID } { const [providerID, ...modelID] = input.split("/") return { - providerID: ProviderV2.ID.make(providerID), + providerID: Provider.ID.make(providerID), modelID: ID.make(modelID.join("/")), } } -export * as ModelV2 from "./model" +export * as Model from "./model" diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index 2db39ab124e..252d04f5304 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -8,11 +8,11 @@ import { Global } from "@opencode-ai/util/global" import { Flock } from "@opencode-ai/util/flock" import { Hash } from "@opencode-ai/util/hash" import { FSUtil } from "@opencode-ai/util/fs-util" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" -import { ModelV2 } from "./model" -import { ProviderV2 } from "./provider" +import { Model } from "./model" +import { Provider } from "./provider" export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) export type CatalogModelStatus = typeof CatalogModelStatus.Type @@ -54,7 +54,7 @@ type SourceModel = { { readonly cost?: Cost readonly provider?: { - readonly body?: ProviderV2.Settings + readonly body?: Provider.Settings readonly headers?: Readonly> } } @@ -75,29 +75,29 @@ type SourceProvider = { } export type Snapshot = { - readonly info: ProviderV2.Info - readonly models: readonly ModelV2.Info[] + readonly info: Provider.Info + readonly models: readonly Model.Info[] readonly environment: readonly string[] } function normalize(input: Record): readonly Snapshot[] { const providers: Snapshot[] = [] for (const item of Object.values(input)) { - const providerID = ProviderV2.ID.make(item.id) + const providerID = Provider.ID.make(item.id) const info = { id: providerID, name: item.name, - package: ProviderV2.aisdk(item.npm), + package: Provider.aisdk(item.npm), ...(item.api ? { settings: { baseURL: item.api } } : {}), - } satisfies ProviderV2.Info - const models: ModelV2.Info[] = [] + } satisfies Provider.Info + const models: Model.Info[] = [] for (const model of Object.values(item.models)) { const baseCost = cost(model.cost) const variants = reasoningVariants(item, model) - const id = ModelV2.ID.make(model.id) + const id = Model.ID.make(model.id) models.push(modelInfo(providerID, id, model, { cost: baseCost, variants })) for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { - const modeID = ModelV2.ID.make(`${model.id}-${mode}`) + const modeID = Model.ID.make(`${model.id}-${mode}`) models.push( modelInfo(providerID, modeID, model, { name: modeName(model, mode), @@ -118,7 +118,7 @@ function released(date: string) { return Number.isFinite(time) ? time : 0 } -function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] { +function cost(input: SourceModel["cost"]): Model.Info["cost"] { const base = { input: input?.input ?? Money.USDPerMillionTokens.zero, output: input?.output ?? Money.USDPerMillionTokens.zero, @@ -154,13 +154,13 @@ function cost(input: SourceModel["cost"]): ModelV2.Info["cost"] { ] } -function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | undefined) { +function mergeCost(base: Model.Info["cost"], override: SourceModel["cost"] | undefined) { if (!override) return base const next = cost(override) const [baseDefault, ...baseTiers] = base const [nextDefault, ...nextTiers] = next - const tierKey = (item: ModelV2.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` - const merge = (left: ModelV2.Info["cost"][number], right: ModelV2.Info["cost"][number]) => ({ + const tierKey = (item: Model.Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` + const merge = (left: Model.Info["cost"][number], right: Model.Info["cost"][number]) => ({ ...left, ...right, tier: right.tier ?? left.tier, @@ -187,7 +187,7 @@ function mergeCost(base: ModelV2.Info["cost"], override: SourceModel["cost"] | u const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] const OUTPUT_TOKEN_MAX = 32_000 -function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable { +function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNullable { const npm = model.provider?.npm ?? provider.npm const options = model.reasoning_options if (!options?.length) return [] @@ -203,7 +203,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul if (id === undefined) return [] if (id === "none" && off.length > 0) return [] const settings = settingsForEffort(npm, model.id, id) - return settings ? [{ id: ModelV2.VariantID.make(id), settings }] : [] + return settings ? [{ id: Model.VariantID.make(id), settings }] : [] }), ] return [...new Map(variants.map((variant) => [variant.id, variant])).values()] @@ -218,7 +218,7 @@ function reasoningVariants(provider: SourceProvider, model: SourceModel): NonNul return [] } -function settingsForEffort(npm: string, modelID: string, effort: string): ProviderV2.Settings | undefined { +function settingsForEffort(npm: string, modelID: string, effort: string): Provider.Settings | undefined { if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } } if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") { if (anthropicManualThinking(modelID)) return { effort } @@ -287,7 +287,7 @@ function budgetVariants( npm: string, model: SourceModel, option: Extract[number], { type: "budget_tokens" }>, -): NonNullable { +): NonNullable { const maximum = Math.min(option.max ?? OUTPUT_TOKEN_MAX - 1, model.limit.output - 1, OUTPUT_TOKEN_MAX - 1) if (maximum <= 0) return [] const high = Math.min(Math.max(option.min ?? 0, Math.floor((maximum + 1) / 2)), maximum) @@ -296,35 +296,35 @@ function budgetVariants( { id: "max", budget: maximum }, ].flatMap((item) => { const settings = settingsForBudget(npm, model.id, item.budget) - return settings ? [{ id: ModelV2.VariantID.make(item.id), settings }] : [] + return settings ? [{ id: Model.VariantID.make(item.id), settings }] : [] }) } -function toggleVariants(npm: string, modelID: string): NonNullable { +function toggleVariants(npm: string, modelID: string): NonNullable { if (npm === "@ai-sdk/gateway") { const upstream = gatewayPackage(modelID) if (upstream) return toggleVariants(upstream, modelID) return [ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } }, }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } }, }, ] } if (npm === "@openrouter/ai-sdk-provider") return [ - { id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, - { id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, + { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, + { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, ] if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") return [ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { thinking: { type: "adaptive", display: "summarized" }, }, @@ -333,11 +333,11 @@ function toggleVariants(npm: string, modelID: string): NonNullable["modes"]>[string]["provider"] - readonly variants?: NonNullable + readonly variants?: NonNullable } = {}, -): ModelV2.Info { +): Model.Info { return { id, - modelID: ModelV2.ID.make(model.id), + modelID: Model.ID.make(model.id), providerID, name: input.name ?? model.name, - compatibility: ModelV2.compatibility(model.interleaved), - family: model.family ? ModelV2.Family.make(model.family) : undefined, - package: model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined, + compatibility: Model.compatibility(model.interleaved), + family: model.family ? Model.Family.make(model.family) : undefined, + package: model.provider?.npm ? Provider.aisdk(model.provider.npm) : undefined, settings: model.provider?.api ? { baseURL: model.provider.api } : undefined, capabilities: { tools: model.tool_call, @@ -519,7 +519,7 @@ function modelInfo( } } -export const Event = ModelsDev.Event +export { Event } from "@opencode-ai/schema/models-dev" declare const OPENCODE_MODELS_DEV: Record | undefined @@ -542,7 +542,7 @@ export const layer = (options?: Options) => Service, Effect.gen(function* () { const fs = yield* FSUtil.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const app = yield* App.Metadata const http = HttpClient.filterStatusOk( (yield* HttpClient.HttpClient).pipe( @@ -639,7 +639,7 @@ export const layer = (options?: Options) => if (!force && (yield* fresh())) return yield* fetchAndWrite() yield* invalidate - yield* events.publish(Event.Refreshed, {}) + yield* bus.publish(ModelsDev.Event.Refreshed, {}) }), ).pipe( Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })), @@ -660,7 +660,7 @@ export function configured(options?: Options) { return makeGlobalNode({ service: Service, layer: layer(options), - deps: [FSUtil.node, EventV2.node, App.node, httpClient], + deps: [FSUtil.node, Bus.node, App.node, httpClient], }) } diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index 8b276833dd3..b957a2e763c 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -1,11 +1,11 @@ -export * as PermissionV2 from "./permission" +export * as Permission from "./permission" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Permission } from "@opencode-ai/schema/permission" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Location } from "./location" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { SessionErrors } from "./session/error" import { SessionSchema } from "./session/schema" import { SessionStore } from "./session/store" @@ -41,32 +41,32 @@ export type Reply = typeof Reply.Type export const AssertInput = Schema.Struct({ id: ID.pipe(Schema.optional), ...RequestFields, - agent: AgentV2.ID.pipe(Schema.optional), -}).annotate({ identifier: "PermissionV2.AssertInput" }) + agent: Agent.ID.pipe(Schema.optional), +}).annotate({ identifier: "Permission.AssertInput" }) export type AssertInput = typeof AssertInput.Type export const ReplyInput = Schema.Struct({ requestID: ID, reply: Reply, message: Schema.String.pipe(Schema.optional), -}).annotate({ identifier: "PermissionV2.ReplyInput" }) +}).annotate({ identifier: "Permission.ReplyInput" }) export type ReplyInput = typeof ReplyInput.Type export const AskResult = Schema.Struct({ id: ID, effect: Permission.Effect, -}).annotate({ identifier: "PermissionV2.AskResult" }) +}).annotate({ identifier: "Permission.AskResult" }) export type AskResult = typeof AskResult.Type -export const Event = Permission.Event +export { Event } from "@opencode-ai/schema/permission" -export class DeclinedError extends Schema.TaggedErrorClass()("PermissionV2.DeclinedError", {}) {} +export class DeclinedError extends Schema.TaggedErrorClass()("Permission.DeclinedError", {}) {} -export class CorrectedError extends Schema.TaggedErrorClass()("PermissionV2.CorrectedError", { +export class CorrectedError extends Schema.TaggedErrorClass()("Permission.CorrectedError", { feedback: Schema.String, }) {} -export class BlockedError extends Schema.TaggedErrorClass()("PermissionV2.BlockedError", { +export class BlockedError extends Schema.TaggedErrorClass()("Permission.BlockedError", { rules: Permission.Ruleset, permission: Schema.String, resources: Schema.Array(Schema.String), @@ -76,7 +76,7 @@ export class BlockedError extends Schema.TaggedErrorClass()("Permi } } -export class NotFoundError extends Schema.TaggedErrorClass()("PermissionV2.NotFoundError", { +export class NotFoundError extends Schema.TaggedErrorClass()("Permission.NotFoundError", { requestID: ID, }) {} @@ -107,20 +107,20 @@ export interface Interface { readonly list: () => Effect.Effect> } -export class Service extends Context.Service()("@opencode/v2/Permission") {} +export class Service extends Context.Service()("@opencode/Permission") {} interface Pending { readonly request: Request - readonly agent?: AgentV2.ID + readonly agent?: Agent.ID readonly deferred: Deferred.Deferred } const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const location = yield* Location.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const sessions = yield* SessionStore.Service const saved = yield* PermissionSaved.Service const pending = new Map() @@ -143,9 +143,9 @@ const layer = Layer.effect( ) }) - const configured = Effect.fn("PermissionV2.configured")(function* ( + const configured = Effect.fn("Permission.configured")(function* ( sessionID: SessionSchema.ID, - agentID?: AgentV2.ID, + agentID?: Agent.ID, ) { const session = yield* sessions.get(sessionID) if (!session) return yield* new SessionErrors.NotFoundError({ sessionID }) @@ -182,7 +182,7 @@ const layer = Layer.effect( } } - const create = (request: Request, agent?: AgentV2.ID) => + const create = (request: Request, agent?: Agent.ID) => Effect.uninterruptible( Effect.gen(function* () { const deferred = yield* Deferred.make() @@ -190,21 +190,21 @@ const layer = Layer.effect( if (pending.has(request.id)) return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`)) pending.set(request.id, item) - yield* events - .publish(Event.Asked, request) + yield* bus + .publish(Permission.Event.Asked, request) .pipe(Effect.onError(() => Effect.sync(() => pending.delete(request.id)))) return item }), ) - const ask = Effect.fn("PermissionV2.ask")(function* (input: AssertInput) { + const ask = Effect.fn("Permission.ask")(function* (input: AssertInput) { const result = yield* evaluateInput(input) const value = request(input) if (result.effect === "ask") yield* create(value, input.agent) return { id: value.id, effect: result.effect } }) - const assert = Effect.fn("PermissionV2.assert")((input: AssertInput) => + const assert = Effect.fn("Permission.assert")((input: AssertInput) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const result = yield* evaluateInput(input) @@ -223,7 +223,7 @@ const layer = Layer.effect( // resurfaces as a typed failure at SessionModelRequest.executeTool. A decline // WITH feedback (CorrectedError) intentionally stays typed so the leaf can turn // it into ToolFailure and the model continues. - Effect.catchTag("PermissionV2.DeclinedError", (error) => Effect.die(error)), + Effect.catchTag("Permission.DeclinedError", (error) => Effect.die(error)), Effect.ensuring( Effect.sync(() => { pending.delete(item.request.id) @@ -234,12 +234,12 @@ const layer = Layer.effect( ), ) - const reply = Effect.fn("PermissionV2.reply")((input: ReplyInput) => + const reply = Effect.fn("Permission.reply")((input: ReplyInput) => Effect.uninterruptible( Effect.gen(function* () { const existing = pending.get(input.requestID) if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) - yield* events.publish(Event.Replied, { + yield* bus.publish(Permission.Event.Replied, { sessionID: existing.request.sessionID, requestID: existing.request.id, reply: input.reply, @@ -253,7 +253,7 @@ const layer = Layer.effect( pending.delete(input.requestID) for (const [id, item] of pending) { if (item.request.sessionID !== existing.request.sessionID) continue - yield* events.publish(Event.Replied, { + yield* bus.publish(Permission.Event.Replied, { sessionID: item.request.sessionID, requestID: item.request.id, reply: "reject", @@ -290,7 +290,7 @@ const layer = Layer.effect( ) ) continue - yield* events.publish(Event.Replied, { + yield* bus.publish(Permission.Event.Replied, { sessionID: item.request.sessionID, requestID: item.request.id, reply: "always", @@ -302,15 +302,15 @@ const layer = Layer.effect( ), ) - const list = Effect.fn("PermissionV2.list")(function* () { + const list = Effect.fn("Permission.list")(function* () { return Array.from(pending.values(), (item) => item.request) }) - const get = Effect.fn("PermissionV2.get")(function* (id: ID) { + const get = Effect.fn("Permission.get")(function* (id: ID) { return pending.get(id)?.request }) - const forSession = Effect.fn("PermissionV2.forSession")(function* (sessionID: SessionSchema.ID) { + const forSession = Effect.fn("Permission.forSession")(function* (sessionID: SessionSchema.ID) { return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) }) @@ -321,5 +321,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node], + deps: [Bus.node, Location.node, Agent.node, SessionStore.node, PermissionSaved.node], }) diff --git a/packages/core/src/permission/saved.ts b/packages/core/src/permission/saved.ts index a973ae3e1ba..0c0fe2d067b 100644 --- a/packages/core/src/permission/saved.ts +++ b/packages/core/src/permission/saved.ts @@ -4,7 +4,7 @@ import { eq } from "drizzle-orm" import { Context, Effect, Layer, Schema } from "effect" import { Database } from "../database/database" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { ProjectV2 } from "../project" +import { Project } from "../project" import { PermissionTable } from "./sql" import { PermissionSaved } from "@opencode-ai/schema/permission-saved" @@ -15,12 +15,12 @@ export const Info = PermissionSaved.Info export type Info = typeof Info.Type export const ListInput = Schema.Struct({ - projectID: ProjectV2.ID.pipe(Schema.optional), + projectID: Project.ID.pipe(Schema.optional), }).annotate({ identifier: "PermissionSaved.ListInput" }) export type ListInput = typeof ListInput.Type export const AddInput = Schema.Struct({ - projectID: ProjectV2.ID, + projectID: Project.ID, action: Schema.String, resources: Schema.Array(Schema.String), }).annotate({ identifier: "PermissionSaved.AddInput" }) @@ -32,7 +32,7 @@ export interface Interface { readonly remove: (id: ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/PermissionSaved") {} +export class Service extends Context.Service()("@opencode/PermissionSaved") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/permission/sql.ts b/packages/core/src/permission/sql.ts index c395555d795..d5deea9d5b0 100644 --- a/packages/core/src/permission/sql.ts +++ b/packages/core/src/permission/sql.ts @@ -1,6 +1,6 @@ import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" import { Timestamps } from "../database/schema.sql" -import { ProjectV2 } from "../project" +import { Project } from "../project" import { ProjectTable } from "../project/sql" import type { PermissionSaved } from "./saved" @@ -9,7 +9,7 @@ export const PermissionTable = sqliteTable( { id: text().$type().primaryKey(), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), action: text().notNull(), diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index bb093a7b97a..009d9d06052 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -1,46 +1,43 @@ -export * as PluginV2 from "./plugin" +export * as Plugin from "./plugin" +export { Event, ID, Info } from "@opencode-ai/schema/plugin" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" -import { Event, ID, type Info } from "@opencode-ai/schema/plugin" +import { Plugin } from "@opencode-ai/schema/plugin" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "./app" import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" -import { CommandV2 } from "./command" -import { EventV2 } from "./event" +import { Command } from "./command" +import { Bus } from "./bus" import { Integration } from "./integration" import { Location } from "./location" import { PluginHost } from "./plugin/host" import { PluginRuntime } from "./plugin/runtime" import { WebSearch } from "./websearch" import { Reference } from "./reference" -import { SkillV2 } from "./skill" +import { Skill } from "./skill" import { State } from "./state" -import { ToolRegistry } from "./tool/registry" -import { ToolHooks } from "./tool/hooks" +import { Tool } from "./tool" import { PluginHooks } from "./plugin/hooks" export interface Interface { readonly activate: (plugins: readonly Versioned[]) => Effect.Effect - readonly list: () => Effect.Effect + readonly list: () => Effect.Effect } -export interface Versioned extends Plugin { - readonly version: string -} +export type Versioned = import("@opencode-ai/plugin/effect/plugin").Plugin & { readonly version: string } -export class Service extends Context.Service()("@opencode/v2/Plugin") {} +export class Service extends Context.Service()("@opencode/Plugin") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const scope = yield* Scope.make() - const active = new Map() + const active = new Map() const lock = Semaphore.makeUnsafe(1) - let host: Parameters[0] + let host: Parameters[0] const load = Effect.fnUntraced(function* (plugin: Versioned) { const child = yield* Scope.fork(scope) @@ -49,7 +46,7 @@ const layer = Layer.effect( inherit, Effect.updateContext((_context: Context.Context) => Context.make(Scope.Scope, child)), Effect.withSpan("Plugin.load", { attributes: { "plugin.id": plugin.id } }), - Effect.andThen(events.publish(Event.Added, { id: ID.make(plugin.id) })), + Effect.andThen(bus.publish(Plugin.Event.Added, { id: Plugin.ID.make(plugin.id) })), Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)), Effect.exit, ) @@ -62,8 +59,8 @@ const layer = Layer.effect( }) const activate = Effect.fn("Plugin.activate")(function* (plugins: readonly Versioned[]) { - const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) })) - const ids = new Set() + const definitions = plugins.map((plugin) => ({ ...plugin, id: Plugin.ID.make(plugin.id) })) + const ids = new Set() for (const definition of definitions) { if (ids.has(definition.id)) yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`)) ids.add(definition.id) @@ -118,7 +115,7 @@ const layer = Layer.effect( }) }), ) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Plugin.Event.Updated, {}) }), ) }) @@ -145,18 +142,17 @@ export const node = makeLocationNode({ service: Service, layer, deps: [ - EventV2.node, + Bus.node, App.node, - AgentV2.node, + Agent.node, AISDK.node, Catalog.node, - CommandV2.node, + Command.node, Integration.node, Location.node, Reference.node, - SkillV2.node, - ToolRegistry.toolsNode, - ToolHooks.node, + Skill.node, + Tool.node, PluginHooks.node, PluginRuntime.node, WebSearch.node, diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 080cdb1ebeb..ac1adb71f12 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -1,12 +1,12 @@ export * as AgentPlugin from "./agent" import path from "path" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Global } from "@opencode-ai/util/global" import { Location } from "../location" -import { PermissionV2 } from "../permission" +import { Permission } from "../permission" // Combined output files written by the Shell service, e.g. `/shell//.out`. // Whitelisted so agents can read a command's full captured output without an external-directory prompt. @@ -105,13 +105,13 @@ export const Plugin = define({ const location = yield* Location.Service const worktree = location.directory const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")] - const readonlyExternalDirectory: PermissionV2.Ruleset = [ + const readonlyExternalDirectory: Permission.Ruleset = [ { action: "external_directory", resource: "*", effect: "ask" }, ...whitelistedDirs.map( - (resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }), + (resource): Permission.Rule => ({ action: "external_directory", resource, effect: "allow" }), ), ] - const defaults: PermissionV2.Ruleset = [ + const defaults: Permission.Ruleset = [ { action: "*", resource: "*", effect: "allow" }, ...readonlyExternalDirectory, { action: "question", resource: "*", effect: "deny" }, @@ -124,24 +124,24 @@ export const Plugin = define({ ] yield* ctx.agent.transform((draft) => { - draft.update(AgentV2.defaultID, (item) => { - item.name = AgentV2.Name.make("Build") + draft.update(Agent.defaultID, (item) => { + item.name = Agent.Name.make("Build") item.description = "The default agent. Executes tools based on configured permissions." item.mode = "primary" item.permissions.push( - ...PermissionV2.merge(defaults, [ + ...Permission.merge(defaults, [ { action: "question", resource: "*", effect: "allow" }, { action: "plan_enter", resource: "*", effect: "allow" }, ]), ) }) - draft.update(AgentV2.ID.make("plan"), (item) => { - item.name = AgentV2.Name.make("Plan") + draft.update(Agent.ID.make("plan"), (item) => { + item.name = Agent.Name.make("Plan") item.description = "Plan mode. Disallows all edit tools." item.mode = "primary" item.permissions.push( - ...PermissionV2.merge(defaults, [ + ...Permission.merge(defaults, [ { action: "question", resource: "*", effect: "allow" }, { action: "plan_exit", resource: "*", effect: "allow" }, { action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" }, @@ -156,22 +156,22 @@ export const Plugin = define({ ) }) - draft.update(AgentV2.ID.make("general"), (item) => { - item.name = AgentV2.Name.make("General") + draft.update(Agent.ID.make("general"), (item) => { + item.name = Agent.Name.make("General") item.description = "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." item.mode = "subagent" - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }])) }) - draft.update(AgentV2.ID.make("explore"), (item) => { - item.name = AgentV2.Name.make("Explore") + draft.update(Agent.ID.make("explore"), (item) => { + item.name = Agent.Name.make("Explore") item.description = 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' item.system = PROMPT_EXPLORE item.mode = "subagent" item.permissions.push( - ...PermissionV2.merge( + ...Permission.merge( defaults, [ { action: "*", resource: "*", effect: "deny" }, @@ -187,28 +187,28 @@ export const Plugin = define({ ) }) - draft.update(AgentV2.ID.make("compaction"), (item) => { - item.name = AgentV2.Name.make("Compaction") + draft.update(Agent.ID.make("compaction"), (item) => { + item.name = Agent.Name.make("Compaction") item.mode = "primary" item.hidden = true item.system = PROMPT_COMPACTION - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) - draft.update(AgentV2.ID.make("title"), (item) => { - item.name = AgentV2.Name.make("Title") + draft.update(Agent.ID.make("title"), (item) => { + item.name = Agent.Name.make("Title") item.mode = "primary" item.hidden = true item.system = PROMPT_TITLE - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) - draft.update(AgentV2.ID.make("summary"), (item) => { - item.name = AgentV2.Name.make("Summary") + draft.update(Agent.ID.make("summary"), (item) => { + item.name = Agent.Name.make("Summary") item.mode = "primary" item.hidden = true item.system = PROMPT_SUMMARY - item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + item.permissions.push(...Permission.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) }) }) }), diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts index 7bb14dad319..676d88c0c44 100644 --- a/packages/core/src/plugin/command.ts +++ b/packages/core/src/plugin/command.ts @@ -1,6 +1,6 @@ export * as CommandPlugin from "./command" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import { Location } from "../location" import PROMPT_INITIALIZE from "./command/initialize.txt" diff --git a/packages/core/src/plugin/hooks.ts b/packages/core/src/plugin/hooks.ts index 5538601c915..50c019e6fb4 100644 --- a/packages/core/src/plugin/hooks.ts +++ b/packages/core/src/plugin/hooks.ts @@ -1,8 +1,8 @@ export * as PluginHooks from "./hooks" -import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk" -import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" -import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool" +import type { AISDKHooks } from "@opencode-ai/plugin/effect/aisdk" +import type { SessionHooks } from "@opencode-ai/plugin/effect/session" +import type { ToolHooks } from "@opencode-ai/plugin/effect/tool" import { Context, Effect, Layer, Scope } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { State } from "../state" @@ -28,7 +28,7 @@ export interface Interface { ) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/PluginHooks") {} +export class Service extends Context.Service()("@opencode/PluginHooks") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 730ddafecc5..b0b11d2169b 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -1,48 +1,44 @@ export * as PluginHost from "./host" -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import { Plugin } from "@opencode-ai/plugin/effect" +import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" import type { CredentialOAuth } from "@opencode-ai/sdk/v2/types" import { EventManifest } from "@opencode-ai/schema/event-manifest" import { App } from "../app" import { Effect, Schema, Stream } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { AISDK } from "../aisdk" import { Catalog } from "../catalog" -import { CommandV2 } from "../command" +import { Command } from "../command" import { Credential } from "../credential" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { Integration } from "../integration" import { Location } from "../location" -import { ModelV2 } from "../model" -import type { PluginV2 } from "../plugin" +import { Model } from "../model" import { PluginRuntime } from "./runtime" -import { ProviderV2 } from "../provider" +import { Provider } from "../provider" import { Reference } from "../reference" import { AbsolutePath, type DeepMutable } from "../schema" -import { SkillV2 } from "../skill" -import { Tool } from "../tool/tool" -import { Tools } from "../tool/tools" -import { ToolHooks } from "../tool/hooks" -import { WorkspaceV2 } from "../workspace" +import { Skill } from "../skill" +import { Tool } from "../tool" +import { Workspace } from "../workspace" import { WebSearch } from "../websearch" import { PluginHooks } from "./hooks" const mutable = (value: T) => value as DeepMutable -export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { +export const make = Effect.fn("PluginHost.make")(function* (plugin: import("../plugin").Interface) { const app = yield* App.Metadata - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service - const commands = yield* CommandV2.Service - const events = yield* EventV2.Service + const commands = yield* Command.Service + const bus = yield* Bus.Service const integration = yield* Integration.Service const location = yield* Location.Service const reference = yield* Reference.Service - const skill = yield* SkillV2.Service - const tools = yield* Tools.Service + const skill = yield* Skill.Service + const tools = yield* Tool.Service const websearch = yield* WebSearch.Service - const toolHooks = yield* ToolHooks.Service const hooks = yield* PluginHooks.Service const runtime = yield* PluginRuntime.Service const locationInfo = () => @@ -51,7 +47,9 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int workspaceID: location.workspaceID, project: location.project, }) - const locationRef = (input?: Parameters[0]) => + const locationRef = (input?: { + readonly location?: { readonly directory?: string; readonly workspace?: string } + }) => input?.location === undefined ? undefined : Location.Ref.make({ @@ -59,7 +57,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int workspaceID: input.location.workspace === undefined ? location.workspaceID - : WorkspaceV2.ID.make(input.location.workspace), + : Workspace.ID.make(input.location.workspace), }) const isCurrentLocation = (ref: Location.Ref) => ref.directory === location.directory && ref.workspaceID === location.workspaceID @@ -70,7 +68,22 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int app, options: {}, agent: { - get: (id) => agents.get(AgentV2.ID.make(id)), + get: (input) => { + const ref = locationRef(input) + const output = + ref && !isCurrentLocation(ref) + ? runtime.location.agent + .list(ref) + .pipe(Effect.map((result) => ({ ...result, data: result.data.find((agent) => agent.id === input.agentID) }))) + : response(agents.get(input.agentID)) + return output.pipe( + Effect.flatMap((result) => + result.data + ? Effect.succeed({ ...result, data: result.data }) + : Effect.fail(new Error(`Agent not found: ${input.agentID}`)), + ), + ) + }, list: (input) => { const ref = locationRef(input) if (ref && !isCurrentLocation(ref)) return runtime.location.agent.list(ref) @@ -81,10 +94,10 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int agents.transform((draft) => { callback({ list: () => mutable(draft.list()), - get: (id) => mutable(draft.get(AgentV2.ID.make(id))), - default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), - update: (id, update) => draft.update(AgentV2.ID.make(id), update), - remove: (id) => draft.remove(AgentV2.ID.make(id)), + get: (id) => mutable(draft.get(Agent.ID.make(id))), + default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)), + update: (id, update) => draft.update(Agent.ID.make(id), update), + remove: (id) => draft.remove(Agent.ID.make(id)), }) }), }, @@ -121,7 +134,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int list: () => response(catalog.provider.available()), get: (input) => catalog.provider - .get(ProviderV2.ID.make(input.providerID)) + .get(Provider.ID.make(input.providerID)) .pipe( Effect.flatMap((provider) => provider === undefined @@ -131,7 +144,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int ), }, model: { - get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), list: () => response(catalog.model.available()), default: () => response(catalog.model.default()), }, @@ -141,21 +153,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int callback({ provider: { list: () => mutable(draft.provider.list()), - get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))), - update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update), - remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), + get: (id) => mutable(draft.provider.get(Provider.ID.make(id))), + update: (id, update) => draft.provider.update(Provider.ID.make(id), update), + remove: (id) => draft.provider.remove(Provider.ID.make(id)), }, model: { get: (providerID, modelID) => - mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))), + mutable(draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID))), update: (providerID, modelID, update) => - draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update), + draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), update), remove: (providerID, modelID) => - draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)), default: { get: draft.model.default.get, set: (providerID, modelID) => - draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)), }, }, }) @@ -170,7 +182,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int }), }, event: { - subscribe: () => events.subscribe().pipe(Stream.filter(EventManifest.isServer)), + subscribe: () => bus.subscribe().pipe(Stream.filter(EventManifest.isServer)), }, integration: { list: () => response(integration.list()), @@ -279,88 +291,21 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int transform: (callback) => skill.transform((draft) => { callback({ - source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)), + source: (source) => draft.source(Schema.decodeUnknownSync(Skill.Source)(source)), list: draft.list, }) }), }, tool: { transform: (callback) => - Effect.gen(function* () { - const registrations: Array<{ - readonly name: string - readonly tool: Tool.Any - readonly options?: Tool.RegisterOptions - }> = [] - yield* Effect.sync(() => + tools + .transform((draft) => callback({ - add: (name, tool, options) => { - registrations.push({ name, tool, ...(options ? { options } : {}) }) - }, + add: (tool) => draft.add(tool), }), ) - yield* tools - .registerBatch( - registrations.map((registration) => ({ - tools: { [registration.name]: registration.tool }, - ...(registration.options === undefined ? {} : { options: registration.options }), - })), - ) - .pipe(Effect.orDie) - return { dispose: Effect.void } - }), - hook: (name, callback) => { - if (name === "execute.before") { - return toolHooks.hook.before((event) => { - const output = { - tool: event.tool, - sessionID: event.sessionID, - agent: event.agent, - messageID: event.messageID, - callID: event.callID, - input: event.input, - } - return Reflect.apply(callback, undefined, [output]).pipe( - Effect.tap(() => Effect.sync(() => (event.input = output.input))), - ) - }) - } - return toolHooks.hook.after((event) => { - // Decode first so plugin mutations cannot alias the canonical outcome. - const output = { - tool: event.tool, - sessionID: event.sessionID, - agent: event.agent, - messageID: event.messageID, - callID: event.callID, - input: event.input, - ...Schema.decodeUnknownSync(Tool.ExecuteAfterOutcome)(event), - } - return Reflect.apply(callback, undefined, [output]).pipe( - Effect.tap(() => { - const decoded = Schema.decodeUnknownOption(Tool.ExecuteAfterOutcome)(output) - if (decoded._tag === "None") - return Effect.logWarning("ignoring invalid execute.after tool outcome", { tool: event.tool }) - if (decoded.value.status !== event.status) - return Effect.logWarning("ignoring execute.after tool status change", { tool: event.tool }) - return Effect.sync(() => { - if (event.status === "completed" && decoded.value.status === "completed") { - event.content = decoded.value.content - event.metadata = decoded.value.metadata - event.outputPaths = decoded.value.outputPaths - return - } - if (event.status === "error" && decoded.value.status === "error") { - event.error = decoded.value.error - event.content = decoded.value.content - event.metadata = decoded.value.metadata - event.outputPaths = decoded.value.outputPaths - } - }) - }), - ) - }) - }, + .pipe(Effect.orDie, Effect.as({ dispose: Effect.void })), + hook: (name, callback) => hooks.register("tool", name, callback), }, websearch: { providers: () => response(websearch.providers()), diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 815dd4339d1..b46a6ae7460 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -1,11 +1,11 @@ export * as PluginInternal from "./internal" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Plugin } from "@opencode-ai/plugin/effect/plugin" import { Context, Effect, Scope } from "effect" import { HttpClient } from "effect/unstable/http" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Catalog } from "../catalog" -import { CommandV2 } from "../command" +import { Command } from "../command" import { Config } from "../config" import { ConfigAgentPlugin } from "../config/plugin/agent" import { ConfigCommandPlugin } from "../config/plugin/command" @@ -14,7 +14,7 @@ import { ConfigPolicyPlugin } from "../config/plugin/policy" import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigSkillPlugin } from "../config/plugin/skill" import { ConfigWebSearchPlugin } from "../config/plugin/websearch" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FileMutation } from "../file-mutation" import { Form } from "../form" import { FileSystem } from "../filesystem" @@ -27,28 +27,28 @@ import { Location } from "../location" import { LocationMutation } from "../location-mutation" import { ModelsDev } from "../models-dev" import { Npm } from "@opencode-ai/util/npm" -import { PermissionV2 } from "../permission" +import { Permission } from "../permission" import { Reference } from "../reference" import { WebSearch } from "../websearch" import { Ripgrep } from "../ripgrep" import { SessionInstructions } from "../session/instructions" import { Shell } from "../shell" -import { SkillV2 } from "../skill" -import { PatchTool } from "../tool/patch" -import { EditTool } from "../tool/edit" -import { GlobTool } from "../tool/glob" -import { GrepTool } from "../tool/grep" -import { QuestionTool } from "../tool/question" +import { Skill } from "../skill" +import { PatchTool } from "../tool/plugin/patch" +import { EditTool } from "../tool/plugin/edit" +import { GlobTool } from "../tool/plugin/glob" +import { GrepTool } from "../tool/plugin/grep" +import { QuestionTool } from "../tool/plugin/question" import { ReadToolFileSystem } from "../tool/read-filesystem" -import { ReadTool } from "../tool/read" -import { ShellTool } from "../tool/shell" -import { SkillTool } from "../tool/skill" -import { SubagentTool } from "../tool/subagent" -import { Tools } from "../tool/tools" -import { WebFetchTool } from "../tool/webfetch" -import { WebSearchTool } from "../tool/websearch" +import { ReadTool } from "../tool/plugin/read" +import { ShellTool } from "../tool/plugin/shell" +import { SkillTool } from "../tool/plugin/skill" +import { SubagentTool } from "../tool/plugin/subagent" +import { Tool } from "../tool" +import { WebFetchTool } from "../tool/plugin/webfetch" +import { WebSearchTool } from "../tool/plugin/websearch" import { WellKnown } from "../wellknown" -import { WriteTool } from "../tool/write" +import { WriteTool } from "../tool/plugin/write" import { AgentPlugin } from "./agent" import { CommandPlugin } from "./command" import { ModelsDevPlugin } from "./models-dev" @@ -62,11 +62,11 @@ import { WarmingPlugin } from "./warming" import { WellKnownPlugin } from "../wellknown/plugin" const services = Effect.fn("PluginInternal.services")(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service const catalog = yield* Catalog.Service - const command = yield* CommandV2.Service + const command = yield* Command.Service const config = yield* Config.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const mutation = yield* FileMutation.Service const filesystem = yield* FileSystem.Service const fs = yield* FSUtil.Service @@ -79,7 +79,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { const locationMutation = yield* LocationMutation.Service const models = yield* ModelsDev.Service const npm = yield* Npm.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const runtime = yield* PluginRuntime.Service const form = yield* Form.Service const read = yield* ReadToolFileSystem.Service @@ -88,15 +88,15 @@ const services = Effect.fn("PluginInternal.services")(function* () { const ripgrep = yield* Ripgrep.Service const instructions = yield* SessionInstructions.Service const shell = yield* Shell.Service - const skill = yield* SkillV2.Service - const tools = yield* Tools.Service + const skill = yield* Skill.Service + const tools = yield* Tool.Service const wellknown = yield* WellKnown.Service return Context.mergeAll( - Context.make(AgentV2.Service, agent), + Context.make(Agent.Service, agent), Context.make(Catalog.Service, catalog), - Context.make(CommandV2.Service, command), + Context.make(Command.Service, command), Context.make(Config.Service, config), - Context.make(EventV2.Service, events), + Context.make(Bus.Service, bus), Context.make(FileMutation.Service, mutation), Context.make(FileSystem.Service, filesystem), Context.make(FSUtil.Service, fs), @@ -109,7 +109,7 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(LocationMutation.Service, locationMutation), Context.make(ModelsDev.Service, models), Context.make(Npm.Service, npm), - Context.make(PermissionV2.Service, permission), + Context.make(Permission.Service, permission), Context.make(PluginRuntime.Service, runtime), Context.make(Form.Service, form), Context.make(ReadToolFileSystem.Service, read), @@ -118,8 +118,8 @@ const services = Effect.fn("PluginInternal.services")(function* () { Context.make(Ripgrep.Service, ripgrep), Context.make(SessionInstructions.Service, instructions), Context.make(Shell.Service, shell), - Context.make(SkillV2.Service, skill), - Context.make(Tools.Service, tools), + Context.make(Skill.Service, skill), + Context.make(Tool.Service, tools), Context.make(WellKnown.Service, wellknown), ) }) diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index ec23dfd36b3..b643cfd7558 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,13 +1,14 @@ -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Integration } from "@opencode-ai/schema/integration" import { Effect, Stream } from "effect" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { ModelsDev } from "../models-dev" export const ModelsDevPlugin = define({ id: "opencode.models-dev", effect: Effect.fn(function* (ctx) { const modelsDev = yield* ModelsDev.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const loaded = { data: structuredClone(yield* modelsDev.get()) } yield* ctx.integration.transform((integrations) => { for (const provider of loaded.data) { @@ -28,14 +29,14 @@ export const ModelsDevPlugin = define({ for (const provider of loaded.data) { catalog.provider.update(provider.info.id, (draft) => { Object.assign(draft, provider.info) - draft.integrationID = provider.info.id + draft.integrationID = Integration.ID.make(provider.info.id) }) for (const model of provider.models) { catalog.model.update(provider.info.id, model.id, (draft) => Object.assign(draft, model)) } } }) - yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( + yield* bus.subscribe(ModelsDev.Event.Refreshed).pipe( Stream.runForEach(() => modelsDev.get().pipe( Effect.tap((data) => Effect.sync(() => (loaded.data = structuredClone(data)))), diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts index a5172b0c98a..21380800af4 100644 --- a/packages/core/src/plugin/promise.ts +++ b/packages/core/src/plugin/promise.ts @@ -1,8 +1,8 @@ export * as PluginPromise from "./promise" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import type { Context, Plugin } from "@opencode-ai/plugin/v2/plugin" -import type { Any, RegisterOptions } from "@opencode-ai/plugin/v2/tool" +import { define } from "@opencode-ai/plugin/effect/plugin" +import type { Context, Plugin } from "@opencode-ai/plugin/promise/plugin" +import type { Info } from "@opencode-ai/plugin/promise/tool" import { Agent } from "@opencode-ai/schema/agent" import { Integration } from "@opencode-ai/schema/integration" import { Location } from "@opencode-ai/schema/location" @@ -14,7 +14,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message" import { Workspace } from "@opencode-ai/schema/workspace" import { WebSearch } from "@opencode-ai/schema/websearch" import { DateTime, Effect, Scope, Stream } from "effect" -import { Tool } from "../tool/tool" +import { Tool } from "../tool" type HostRegistration = { readonly dispose: Effect.Effect } type Registration = { readonly dispose: () => Promise } @@ -23,7 +23,7 @@ type JsonValue = null | boolean | number | string | Array | { [key: s /** * Adapts a Promise plugin into an Effect plugin so the existing Effect-only - * loader (`PluginV2` / `PluginSupervisor`) can run it unchanged. + * loader (`Plugin` / `PluginSupervisor`) can run it unchanged. * * Hook registrations created during the async `setup` attach to the plugin's * scope, so unloading the plugin disposes them. The captured fiber context @@ -61,7 +61,7 @@ export function fromPromise(plugin: Plugin) { app: host.app, options: host.options, agent: { - get: (id) => run(host.agent.get(id)), + get: (input) => run(host.agent.get({ ...input, agentID: Agent.ID.make(input.agentID) })), list: (input) => run(host.agent.list(input)), transform: transform(host.agent), reload: () => run(host.agent.reload()), @@ -77,7 +77,6 @@ export function fromPromise(plugin: Plugin) { run(host.catalog.provider.get({ ...input, providerID: Provider.ID.make(input.providerID) })), }, model: { - get: (providerID, modelID) => run(host.catalog.model.get(providerID, modelID)), list: (input) => run(host.catalog.model.list(input)), default: (input) => run(host.catalog.model.default(input)).then((result) => ({ ...result, data: result.data ?? null })), @@ -165,7 +164,44 @@ export function fromPromise(plugin: Plugin) { }), ), }, - transform: transform(host.integration), + transform: (callback) => + register( + host.integration.transform((draft) => + callback({ + list: draft.list, + get: draft.get, + update: draft.update, + remove: draft.remove, + method: { + list: draft.method.list, + update: (input) => { + if (!("authorize" in input)) return draft.method.update(input) + const refresh = input.refresh + draft.method.update({ + ...input, + authorize: (inputs) => + Effect.promise(() => input.authorize(inputs)).pipe( + Effect.map((authorization) => + authorization.mode === "auto" + ? { + ...authorization, + callback: Effect.promise(() => authorization.callback), + } + : { + ...authorization, + callback: (code) => Effect.promise(() => authorization.callback(code)), + }, + ), + ), + refresh: + refresh === undefined ? undefined : (credential) => Effect.promise(() => refresh(credential)), + }) + }, + remove: draft.method.remove, + }, + }), + ), + ), reload: () => run(host.integration.reload()), connection: { active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)), @@ -190,8 +226,11 @@ export function fromPromise(plugin: Plugin) { register( host.tool.transform((draft) => callback({ - add: (name: string, tool: Any, options?: RegisterOptions) => - draft.add(name, fromPromiseTool(tool), options), + add: (tool: Info) => + draft.add({ + ...tool, + execute: (input, context) => executePromiseTool(tool, input, context), + }), }), ), ), @@ -333,15 +372,10 @@ function wireEvent(value: unknown): unknown { return wire(value) } -function fromPromiseTool(tool: Any): Tool.Any { - return { - ...tool, - execute: (input, context) => - Effect.promise(() => - tool.execute(input, { - ...context, - progress: (update) => Effect.runPromise(context.progress(update)), - }), - ), - } -} +const executePromiseTool = (tool: Info, input: any, context: Tool.Context) => + Effect.promise(() => + tool.execute(input, { + ...context, + progress: (update) => Effect.runPromise(context.progress(update)), + }), + ) diff --git a/packages/core/src/plugin/provider/alibaba.ts b/packages/core/src/plugin/provider/alibaba.ts index ea37b004531..47c7e6b25c3 100644 --- a/packages/core/src/plugin/provider/alibaba.ts +++ b/packages/core/src/plugin/provider/alibaba.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const AlibabaPlugin = define({ id: "opencode.provider.alibaba", diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index ecc137bbde3..322f5ba45c2 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -1,7 +1,7 @@ import { Effect } from "effect" import type { LanguageModelV3 } from "@ai-sdk/provider" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" type MantleSDK = { languageModel: (modelID: string) => LanguageModelV3 @@ -64,8 +64,8 @@ export const AmazonBedrockPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue evt.provider.update(item.provider.id, (provider) => { if (typeof provider.settings?.endpoint !== "string") return // The AI SDK expects a base URL, but users configure Bedrock private/VPC @@ -112,10 +112,10 @@ export const AmazonBedrockPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return + if (evt.model.providerID !== Provider.ID.amazonBedrock) return if ( - ProviderV2.isAISDK(evt.model.package) && - ProviderV2.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle" + Provider.isAISDK(evt.model.package) && + Provider.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle" ) { evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id) return diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts index bbf0d56dfbe..06ba5042014 100644 --- a/packages/core/src/plugin/provider/anthropic.ts +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const AnthropicPlugin = define({ id: "opencode.provider.anthropic", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/anthropic") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index f50bbdb1ba2..ccc12eef7f4 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" function selectLanguage(sdk: any, modelID: string, useChat: boolean) { if (useChat && sdk.chat) return sdk.chat(modelID) @@ -15,8 +15,8 @@ export const AzurePlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/azure") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/azure") continue const configured = item.provider.settings?.resourceName const resourceName = typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME @@ -30,11 +30,11 @@ export const AzurePlugin = define({ "sdk", Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/azure") return - if (evt.model.providerID === ProviderV2.ID.azure) { + if (evt.model.providerID === Provider.ID.azure) { if ( !evt.options.resourceName && !evt.options.baseURL && - (!ProviderV2.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string") + (!Provider.isAISDK(evt.model.package) || typeof evt.model.settings?.baseURL !== "string") ) { throw new Error( "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", @@ -48,7 +48,7 @@ export const AzurePlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.azure) return + if (evt.model.providerID !== Provider.ID.azure) return evt.language = selectLanguage( evt.sdk, evt.model.modelID ?? evt.model.id, @@ -66,8 +66,8 @@ export const AzureCognitiveServicesPlugin = define({ const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME if (!resourceName) return for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (!item.provider.id.includes("azure-cognitive-services")) continue evt.provider.update(item.provider.id, (provider) => { provider.settings = { @@ -80,7 +80,7 @@ export const AzureCognitiveServicesPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return + if (evt.model.providerID !== Provider.ID.make("azure-cognitive-services")) return evt.language = selectLanguage( evt.sdk, evt.model.modelID ?? evt.model.id, diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index 13614d6be08..cb071e71fb4 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const CerebrasPlugin = define({ id: "opencode.provider.cerebras", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/cerebras") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "X-Cerebras-3rd-Party-Integration": "opencode" } }) diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index 6787e916979..6e423471152 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -1,7 +1,7 @@ import os from "os" import { App } from "../../app" import { Effect, Option, Schema } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const CloudflareAIGatewayPlugin = define({ id: "opencode.provider.cloudflare-ai-gateway", diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 99f94320baf..1ac31d77f99 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -1,10 +1,10 @@ import os from "os" import { App } from "../../app" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" -const providerID = ProviderV2.ID.make("cloudflare-workers-ai") +const providerID = Provider.ID.make("cloudflare-workers-ai") export const CloudflareWorkersAIPlugin = define({ id: "opencode.provider.cloudflare-workers-ai", @@ -13,7 +13,7 @@ export const CloudflareWorkersAIPlugin = define({ const item = evt.provider.get(providerID) if (!item) return evt.provider.update(item.provider.id, (provider) => { - if (!ProviderV2.isAISDK(provider.package)) return + if (!Provider.isAISDK(provider.package)) return if (typeof provider.settings?.baseURL === "string") return const accountId = resolveAccountId(provider.settings ?? {}) if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) } @@ -61,7 +61,7 @@ function hasWorkersEndpoint(model: { readonly package?: string readonly settings?: Readonly> }) { - return ProviderV2.isAISDK(model.package) && typeof model.settings?.baseURL === "string" + return Provider.isAISDK(model.package) && typeof model.settings?.baseURL === "string" } function sdkOptions(options: Record, app: App.Info) { diff --git a/packages/core/src/plugin/provider/cohere.ts b/packages/core/src/plugin/provider/cohere.ts index 9284defcb14..7fffb9db4b7 100644 --- a/packages/core/src/plugin/provider/cohere.ts +++ b/packages/core/src/plugin/provider/cohere.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const CoherePlugin = define({ id: "opencode.provider.cohere", diff --git a/packages/core/src/plugin/provider/deepinfra.ts b/packages/core/src/plugin/provider/deepinfra.ts index f4ddf97859a..6f76bf6c37f 100644 --- a/packages/core/src/plugin/provider/deepinfra.ts +++ b/packages/core/src/plugin/provider/deepinfra.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const DeepInfraPlugin = define({ id: "opencode.provider.deepinfra", diff --git a/packages/core/src/plugin/provider/dynamic.ts b/packages/core/src/plugin/provider/dynamic.ts index 4e8eedb81ef..aea911363cb 100644 --- a/packages/core/src/plugin/provider/dynamic.ts +++ b/packages/core/src/plugin/provider/dynamic.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { pathToFileURL } from "url" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Npm } from "@opencode-ai/util/npm" import { importModule } from "@opencode-ai/util/runtime-import" diff --git a/packages/core/src/plugin/provider/gateway.ts b/packages/core/src/plugin/provider/gateway.ts index b67c0ef1798..8ee5a6ccf31 100644 --- a/packages/core/src/plugin/provider/gateway.ts +++ b/packages/core/src/plugin/provider/gateway.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const GatewayPlugin = define({ id: "opencode.provider.gateway", diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 94414eda3d7..c8aff68697c 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -1,14 +1,14 @@ -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" import { Effect, Option, Schema, Semaphore, Stream } from "effect" import { Catalog } from "../../catalog" import { Credential } from "../../credential" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { CopilotModels } from "../../github-copilot/models" import { App } from "../../app" import { Integration } from "../../integration" -import { ModelV2 } from "../../model" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { Model } from "../../model" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" import type { PluginInternal } from "../internal" const clientID = "Ov23li8tweQw6odWQebz" @@ -152,11 +152,11 @@ export const GithubCopilotPlugin = define({ id: "opencode.provider.github-copilot", effect: Effect.fn(function* (ctx) { const catalog = yield* Catalog.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const loading = Semaphore.makeUnsafe(1) const loaded: { baseURL?: string - models?: Map + models?: Map } = {} const load = Effect.fn("GithubCopilotPlugin.load")(function* () { @@ -171,8 +171,8 @@ export const GithubCopilotPlugin = define({ } loaded.baseURL = copilotBaseURL(credential.metadata) - const provider = yield* catalog.provider.get(ProviderV2.ID.githubCopilot) - const existing = (yield* catalog.model.all()).filter((model) => model.providerID === ProviderV2.ID.githubCopilot) + const provider = yield* catalog.provider.get(Provider.ID.githubCopilot) + const existing = (yield* catalog.model.all()).filter((model) => model.providerID === Provider.ID.githubCopilot) loaded.models = yield* Effect.tryPromise({ try: () => CopilotModels.get( @@ -197,11 +197,11 @@ export const GithubCopilotPlugin = define({ draft.method.update(oauth(ctx.app)) }) yield* ctx.catalog.transform((evt) => { - const item = evt.provider.get(ProviderV2.ID.githubCopilot) + const item = evt.provider.get(Provider.ID.githubCopilot) if (!item) return if (loaded.models) { for (const id of item.models.keys()) { - if (!loaded.models.has(ModelV2.ID.make(id))) evt.model.remove(item.provider.id, id) + if (!loaded.models.has(Model.ID.make(id))) evt.model.remove(item.provider.id, id) } for (const [id, model] of loaded.models) { evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model))) @@ -209,12 +209,12 @@ export const GithubCopilotPlugin = define({ } else if (loaded.baseURL) { for (const id of item.models.keys()) { evt.model.update(item.provider.id, id, (model) => { - model.settings = ProviderV2.mergeOverlay(model.settings, { baseURL: loaded.baseURL }) + model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL }) }) } } - if (item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) { - evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + if (item.models.has(Model.ID.make("gpt-5-chat-latest"))) { + evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => { // This chat-only alias conflicts with the Copilot GPT-5 Responses route, // so hide it only for Copilot rather than for every provider catalog. model.enabled = false @@ -222,7 +222,7 @@ export const GithubCopilotPlugin = define({ } }) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("github-copilot")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), @@ -231,7 +231,7 @@ export const GithubCopilotPlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return + if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.package !== "@ai-sdk/github-copilot" && evt.package !== "@ai-sdk/anthropic") return evt.options.fetch = copilotFetch( typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined, @@ -255,7 +255,7 @@ export const GithubCopilotPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return + if (evt.model.providerID !== Provider.ID.githubCopilot) return if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id) return diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 9772a38b3a6..27354117906 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -1,8 +1,8 @@ import os from "os" import { App } from "../../app" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const GitLabPlugin = define({ id: "opencode.provider.gitlab", @@ -35,7 +35,7 @@ export const GitLabPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.gitlab) return + if (evt.model.providerID !== Provider.ID.gitlab) return const featureFlags = typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {} const id = evt.model.modelID ?? evt.model.id diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index ffc29f4bf35..8014f2662c0 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" function resolveProject(options: Record) { // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs @@ -59,12 +59,12 @@ export const GoogleVertexPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue + if (!Provider.isAISDK(item.provider.package)) continue if ( - ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex" && + Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex" && !( - item.provider.id === ProviderV2.ID.googleVertex && - ProviderV2.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible") + item.provider.id === Provider.ID.googleVertex && + Provider.packageName(item.provider.package)?.includes("@ai-sdk/openai-compatible") ) ) continue @@ -78,7 +78,7 @@ export const GoogleVertexPlugin = define({ ...(typeof provider.settings?.baseURL === "string" ? { baseURL: replaceVertexVars(provider.settings.baseURL, project, location) } : {}), - ...(ProviderV2.packageName(provider.package)?.includes("@ai-sdk/openai-compatible") + ...(Provider.packageName(provider.package)?.includes("@ai-sdk/openai-compatible") ? { fetch: authFetch(provider.settings?.fetch) } : {}), } @@ -88,7 +88,7 @@ export const GoogleVertexPlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { + if (evt.model.providerID === Provider.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { evt.options.fetch = authFetch(evt.options.fetch) return } @@ -108,7 +108,7 @@ export const GoogleVertexPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.googleVertex) return + if (evt.model.providerID !== Provider.ID.googleVertex) return evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim()) }), ) @@ -120,8 +120,8 @@ export const GoogleVertexAnthropicPlugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/google-vertex/anthropic") continue const project = item.provider.settings?.project ?? process.env.GOOGLE_CLOUD_PROJECT ?? @@ -167,7 +167,7 @@ export const GoogleVertexAnthropicPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return + if (evt.model.providerID !== Provider.ID.make("google-vertex-anthropic")) return evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim()) }), ) diff --git a/packages/core/src/plugin/provider/google.ts b/packages/core/src/plugin/provider/google.ts index c62d962eb59..f30be656f7f 100644 --- a/packages/core/src/plugin/provider/google.ts +++ b/packages/core/src/plugin/provider/google.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const GooglePlugin = define({ id: "opencode.provider.google", diff --git a/packages/core/src/plugin/provider/groq.ts b/packages/core/src/plugin/provider/groq.ts index 51b8e4c42f7..f2f078979ea 100644 --- a/packages/core/src/plugin/provider/groq.ts +++ b/packages/core/src/plugin/provider/groq.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const GroqPlugin = define({ id: "opencode.provider.groq", diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index b1c53fca908..0d69c92d5c5 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const KiloPlugin = define({ id: "opencode.provider.kilo", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://api.kilo.ai/api/gateway") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" } diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index 88a597164b4..2f545478c9d 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -1,7 +1,7 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Integration } from "../../integration" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" export const LLMGatewayPlugin = define({ id: "opencode.provider.llmgateway", @@ -11,8 +11,8 @@ export const LLMGatewayPlugin = define({ yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { if (item.provider.disabled) continue - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://api.llmgateway.io/v1") continue if (!configured.has(Integration.ID.make(item.provider.id))) continue evt.provider.update(item.provider.id, (provider) => { diff --git a/packages/core/src/plugin/provider/mistral.ts b/packages/core/src/plugin/provider/mistral.ts index f5de664432e..f7a42deb40b 100644 --- a/packages/core/src/plugin/provider/mistral.ts +++ b/packages/core/src/plugin/provider/mistral.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const MistralPlugin = define({ id: "opencode.provider.mistral", diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index 7363e88f52a..41581366d8a 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const NvidiaPlugin = define({ id: "opencode.provider.nvidia", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://integrate.api.nvidia.com/v1") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { diff --git a/packages/core/src/plugin/provider/openai-compatible.ts b/packages/core/src/plugin/provider/openai-compatible.ts index 76646b4e453..fb523a3a242 100644 --- a/packages/core/src/plugin/provider/openai-compatible.ts +++ b/packages/core/src/plugin/provider/openai-compatible.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const OpenAICompatiblePlugin = define({ id: "opencode.provider.openai-compatible", diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index b6ff9e58106..e133d9edc22 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -1,14 +1,14 @@ import { createServer } from "node:http" -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect" import { App } from "../../app" import { Credential } from "../../credential" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { Integration } from "../../integration" -import { ModelV2 } from "../../model" +import { Model } from "../../model" import { OauthCallbackPage } from "../../oauth/page" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" import type { PluginInternal } from "../internal" import { OpenAICodex } from "./openai-codex" @@ -162,7 +162,7 @@ const headless = (app: App.Info) => ({ export const OpenAIPlugin = define({ id: "opencode.provider.openai", effect: Effect.fn(function* (ctx) { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const loading = Semaphore.makeUnsafe(1) let chatgpt = false @@ -181,17 +181,17 @@ export const OpenAIPlugin = define({ yield* load() yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai") continue - if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue - evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai") continue + if (!item.models.has(Model.ID.make("gpt-5-chat-latest"))) continue + evt.model.update(item.provider.id, Model.ID.make("gpt-5-chat-latest"), (model) => { // OpenAIPlugin sends OpenAI models through Responses; this alias is a // chat-completions-only model, so hide it only from OpenAI's catalog. model.enabled = false }) } if (!chatgpt) return - const item = evt.provider.get(ProviderV2.ID.openai) + const item = evt.provider.get(Provider.ID.openai) if (!item) return for (const model of item.models.values()) { // ChatGPT-plan tokens only authorize codex-eligible models, and the @@ -211,7 +211,7 @@ export const OpenAIPlugin = define({ }) const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), @@ -227,7 +227,7 @@ export const OpenAIPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.openai) return + if (evt.model.providerID !== Provider.ID.openai) return evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) }), ) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index e70be428142..75704a1c7b8 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -1,14 +1,14 @@ import { Duration, Effect, Schema, Semaphore, Stream } from "effect" import type { Scope } from "effect" -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { define } from "@opencode-ai/plugin/effect/plugin" import type { CredentialValue } from "@opencode-ai/sdk/v2/types" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { Credential } from "../../credential" import { Integration } from "../../integration" -import { ModelV2 } from "../../model" -import { ProviderV2 } from "../../provider" +import { Model } from "../../model" +import { Provider } from "../../provider" import { ConfigProviderV1 } from "../../v1/config/provider" import { Money } from "@opencode-ai/schema/money" import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" @@ -82,10 +82,10 @@ function oauth(http: HttpClient.HttpClient) { } satisfies IntegrationOAuthMethodRegistration } -export const OpencodePlugin = define({ +export const OpencodePlugin = define({ id: "opencode.provider.opencode", effect: Effect.fn(function* (ctx) { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const http = yield* HttpClient.HttpClient const loading = Semaphore.makeUnsafe(1) let connected = false @@ -120,7 +120,7 @@ export const OpencodePlugin = define { provider.integrationID = Integration.ID.make("opencode") if (item.name !== undefined) provider.name = item.name - provider.package = item.npm ? ProviderV2.aisdk(item.npm) : "" + provider.package = item.npm ? Provider.aisdk(item.npm) : "" provider.settings = { ...provider.settings, ...withoutCredentials(item.options), @@ -131,12 +131,12 @@ export const OpencodePlugin = define { - if (config.family !== undefined) model.family = config.family + if (config.family !== undefined) model.family = Model.Family.make(config.family) if (config.name !== undefined) model.name = config.name - if (config.id !== undefined) model.modelID = config.id - model.compatibility = ModelV2.compatibility(config.interleaved) ?? model.compatibility + if (config.id !== undefined) model.modelID = Model.ID.make(config.id) + model.compatibility = Model.compatibility(config.interleaved) ?? model.compatibility if (config.provider !== undefined) { - model.package = config.provider.npm ? ProviderV2.aisdk(config.provider.npm) : undefined + model.package = config.provider.npm ? Provider.aisdk(config.provider.npm) : undefined if (config.provider.api) model.settings = { ...model.settings, baseURL: config.provider.api } } if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call @@ -147,7 +147,7 @@ export const OpencodePlugin = define item.id === variantID) if (!existing) { existing = { id: variantID } @@ -174,7 +174,7 @@ export const OpencodePlugin = define { @@ -190,7 +190,7 @@ export const OpencodePlugin = define loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) - yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe( Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index f00386a94a1..a9ace9f91ad 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -1,19 +1,19 @@ import { Effect } from "effect" -import { ModelV2 } from "../../model" -import { ProviderV2 } from "../../provider" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Model } from "../../model" +import { Provider } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" export const OpenRouterPlugin = define({ id: "opencode.provider.openrouter", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@openrouter/ai-sdk-provider") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" } }) - for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { + for (const modelID of [Model.ID.make("gpt-5-chat-latest"), Model.ID.make("openai/gpt-5-chat")]) { if (!item.models.has(modelID)) continue evt.model.update(item.provider.id, modelID, (model) => { // These are OpenRouter-specific OpenAI chat aliases that do not work diff --git a/packages/core/src/plugin/provider/perplexity.ts b/packages/core/src/plugin/provider/perplexity.ts index 36cafdb2d1c..37a390948ae 100644 --- a/packages/core/src/plugin/provider/perplexity.ts +++ b/packages/core/src/plugin/provider/perplexity.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const PerplexityPlugin = define({ id: "opencode.provider.perplexity", diff --git a/packages/core/src/plugin/provider/sap-ai-core.ts b/packages/core/src/plugin/provider/sap-ai-core.ts index 4015a77c147..9a8a28c5bcb 100644 --- a/packages/core/src/plugin/provider/sap-ai-core.ts +++ b/packages/core/src/plugin/provider/sap-ai-core.ts @@ -1,8 +1,8 @@ import { Effect } from "effect" import { pathToFileURL } from "url" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Npm } from "@opencode-ai/util/npm" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" import { importModule } from "@opencode-ai/util/runtime-import" export const SapAICorePlugin = define({ @@ -12,7 +12,7 @@ export const SapAICorePlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return + if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return const serviceKey = process.env.AICORE_SERVICE_KEY ?? (typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined) @@ -42,7 +42,7 @@ export const SapAICorePlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return + if (evt.model.providerID !== Provider.ID.make("sap-ai-core")) return evt.language = evt.sdk(evt.model.modelID ?? evt.model.id) }), ) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts index 45c5f095ee1..8e4a6f39f73 100644 --- a/packages/core/src/plugin/provider/snowflake-cortex.ts +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise @@ -70,7 +70,7 @@ export const SnowflakeCortexPlugin = define({ yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return + if (evt.model.providerID !== Provider.ID.make("snowflake-cortex")) return const token = process.env.SNOWFLAKE_CORTEX_TOKEN ?? process.env.SNOWFLAKE_CORTEX_PAT ?? diff --git a/packages/core/src/plugin/provider/togetherai.ts b/packages/core/src/plugin/provider/togetherai.ts index ea43db9e4a5..eb8a8f76420 100644 --- a/packages/core/src/plugin/provider/togetherai.ts +++ b/packages/core/src/plugin/provider/togetherai.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const TogetherAIPlugin = define({ id: "opencode.provider.togetherai", diff --git a/packages/core/src/plugin/provider/venice.ts b/packages/core/src/plugin/provider/venice.ts index 9930ce831d9..1c140db5b23 100644 --- a/packages/core/src/plugin/provider/venice.ts +++ b/packages/core/src/plugin/provider/venice.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" export const VenicePlugin = define({ id: "opencode.provider.venice", diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index fc392fe5e17..5ac2bb29666 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const VercelPlugin = define({ id: "opencode.provider.vercel", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/vercel") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/vercel") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { ...provider.headers, "http-referer": "https://opencode.ai/", "x-title": "opencode" } }) diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts index c97f8d68f46..f4d6307ddc9 100644 --- a/packages/core/src/plugin/provider/xai.ts +++ b/packages/core/src/plugin/provider/xai.ts @@ -1,12 +1,12 @@ import { createServer } from "node:http" -import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Clock, Deferred, Effect, Option, Schema } from "effect" import { App } from "../../app" import { Credential } from "../../credential" import { Integration } from "../../integration" import { OauthCallbackPage } from "../../oauth/page" -import { ProviderV2 } from "../../provider" +import { Provider } from "../../provider" const clientID = "b1a00492-073a-47ea-816f-4c329264a828" const issuer = "https://auth.x.ai/oauth2" @@ -173,7 +173,7 @@ export const XAIPlugin = define({ yield* ctx.aisdk.hook( "language", Effect.fn(function* (evt) { - if (evt.model.providerID !== ProviderV2.ID.make("xai")) return + if (evt.model.providerID !== Provider.ID.make("xai")) return evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id) }), ) diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index 44619a1ac5b..13684f5bac5 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -1,14 +1,14 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ProviderV2 } from "../../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Provider } from "../../provider" export const ZenmuxPlugin = define({ id: "opencode.provider.zenmux", effect: Effect.fn(function* (ctx) { yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!ProviderV2.isAISDK(item.provider.package)) continue - if (ProviderV2.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue + if (!Provider.isAISDK(item.provider.package)) continue + if (Provider.packageName(item.provider.package) !== "@ai-sdk/openai-compatible") continue if (item.provider.settings?.baseURL !== "https://zenmux.ai/api/v1") continue evt.provider.update(item.provider.id, (provider) => { provider.headers = { diff --git a/packages/core/src/plugin/runtime.ts b/packages/core/src/plugin/runtime.ts index 4bb85f059dc..cafa5d443e5 100644 --- a/packages/core/src/plugin/runtime.ts +++ b/packages/core/src/plugin/runtime.ts @@ -1,16 +1,16 @@ export * as PluginRuntime from "./runtime" import { Context, Effect, Layer } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { Job } from "../job" import { Location } from "../location" import { LocationServiceMap } from "../location-service-map" -import { SessionV2 } from "../session" +import { Session } from "../session" export interface Interface { readonly session: Pick< - SessionV2.Interface, + Session.Interface, "get" | "create" | "messages" | "prompt" | "generate" | "command" | "resume" | "interrupt" | "synthetic" > readonly job: Pick @@ -18,7 +18,7 @@ export interface Interface { readonly agent: { readonly list: ( ref: Location.Ref, - ) => Effect.Effect<{ readonly location: Location.Info; readonly data: AgentV2.Info[] }> + ) => Effect.Effect<{ readonly location: Location.Info; readonly data: Agent.Info[] }> } } } @@ -74,7 +74,7 @@ export const layerWithCell = (cell: Cell) => export const providerLayerWithCell = (cell: Cell) => Layer.effectDiscard( Effect.gen(function* () { - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const jobs = yield* Job.Service const locations = yield* LocationServiceMap.Service const runtime: Interface = { @@ -85,7 +85,7 @@ export const providerLayerWithCell = (cell: Cell) => list: (ref) => Effect.gen(function* () { const location = yield* Location.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service return { location: new Location.Info({ directory: location.directory, @@ -118,7 +118,7 @@ export const providerNodeWithCell = (cell: Cell) => makeGlobalNode({ name: "plugin-runtime-provider", layer: providerLayerWithCell(cell), - deps: [node, SessionV2.node, Job.node, LocationServiceMap.node], + deps: [node, Session.node, Job.node, LocationServiceMap.node], }) export const providerNode = providerNodeWithCell(defaultCell) diff --git a/packages/core/src/plugin/sdk.ts b/packages/core/src/plugin/sdk.ts index 99861d90d5a..4a432c06c81 100644 --- a/packages/core/src/plugin/sdk.ts +++ b/packages/core/src/plugin/sdk.ts @@ -1,12 +1,12 @@ export * as SdkPlugins from "./sdk" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Plugin } from "@opencode-ai/plugin/effect/plugin" import { Context, Effect, Layer } from "effect" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "../event" -import type { PluginV2 } from "../plugin" +import { Bus } from "../bus" +import type { Versioned } from "../plugin" -export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} }) +export const Updated = Bus.ephemeral({ type: "sdk.plugin.updated", schema: {} }) /** * Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes, @@ -21,7 +21,7 @@ export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: { */ export interface Interface { readonly register: (plugin: Plugin) => Effect.Effect - readonly all: () => readonly PluginV2.Versioned[] + readonly all: () => readonly Versioned[] } export class Service extends Context.Service()("@opencode/SdkPlugins") {} @@ -29,17 +29,17 @@ export class Service extends Context.Service()("@opencode/Sd export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service - const plugins = new Map() + const bus = yield* Bus.Service + const plugins = new Map() let revision = 0 return Service.of({ register: (plugin) => Effect.sync(() => { plugins.set(plugin.id, { ...plugin, version: String(++revision) }) - }).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid), + }).pipe(Effect.andThen(bus.publish(Updated, {})), Effect.asVoid), all: () => [...plugins.values()], }) }), ) -export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeGlobalNode({ service: Service, layer, deps: [Bus.node] }) diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts index c0edc4a4312..e10ce3e848f 100644 --- a/packages/core/src/plugin/skill.ts +++ b/packages/core/src/plugin/skill.ts @@ -2,10 +2,10 @@ export * as SkillPlugin from "./skill" -import { define, type Context } from "@opencode-ai/plugin/v2/effect/plugin" +import { define, type Context } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import { AbsolutePath } from "../schema" -import { SkillV2 } from "../skill" +import { Skill } from "../skill" import { Config } from "../config" import { Location } from "../location" import { FSUtil } from "@opencode-ai/util/fs-util" @@ -29,11 +29,11 @@ export const Plugin = define({ const reportContent = yield* reportContentWithDiagnostics(ctx.app) yield* ctx.skill.transform((draft) => { draft.source( - SkillV2.EmbeddedSource.make({ + Skill.EmbeddedSource.make({ type: "embedded", - skill: SkillV2.Info.make({ - id: SkillV2.ID.make("opencode"), - name: SkillV2.Name.make("OpenCode"), + skill: Skill.Info.make({ + id: Skill.ID.make("opencode"), + name: Skill.Name.make("OpenCode"), description: OpencodeDescription, location: AbsolutePath.make("/builtin/opencode.md"), content: OpencodeContent, @@ -41,11 +41,11 @@ export const Plugin = define({ }), ) draft.source( - SkillV2.EmbeddedSource.make({ + Skill.EmbeddedSource.make({ type: "embedded", - skill: SkillV2.Info.make({ - id: SkillV2.ID.make("report"), - name: SkillV2.Name.make("Report"), + skill: Skill.Info.make({ + id: Skill.ID.make("report"), + name: Skill.Name.make("Report"), description: REPORT_DESCRIPTION, slash: true, location: AbsolutePath.make("/builtin/report.md"), diff --git a/packages/core/src/plugin/skill/opencode.md b/packages/core/src/plugin/skill/opencode.md index 913082d3f1c..7566a7e588a 100644 --- a/packages/core/src/plugin/skill/opencode.md +++ b/packages/core/src/plugin/skill/opencode.md @@ -4,11 +4,11 @@ Use this guide as the starting point for work involving OpenCode itself. It covers the core concepts needed to configure and customize OpenCode, extend it with plugins, and build integrations with the OpenCode SDK, clients, and API. -Full documentation is available at . This overview is +Full documentation is available at . This overview is only an index of core concepts. Before answering a question about a topic below, fetch the URL named in that section and use the full page as the source of truth. Follow links from that page when the question needs more detail. Fetch - first when you need to discover the relevant + first when you need to discover the relevant documentation page. ## Version policy @@ -16,10 +16,10 @@ documentation page. Always answer for OpenCode V2 unless the user explicitly asks about V1, legacy OpenCode, or migrating from V1. -Use only documentation as the source of truth for V2. +Use only documentation as the source of truth for V2. Do not use , which documents V1, and do not use general web search to resolve a V2 documentation question when the V2 docs or -their `llms.txt` index cover it. The schema served from +linked pages cover it. The schema served from may describe V1 even though V2 configuration files include that URL for editor integration. Never use it to infer V2 field names or shapes. If V2 documentation is missing or contradictory, state the @@ -29,7 +29,7 @@ V1 documentation and syntax may be consulted only when the user explicitly asks about V1 or when needed as migration input. Outputs and recommendations must still use V2 unless the user specifically requests a V1 result. -## [Configuration](https://v2.opencode.ai/docs/config) +## [Configuration](https://opencode.ai/v2/docs/config) OpenCode configuration uses JSON or JSONC. Include the published schema so the user's editor can validate fields and provide autocomplete: @@ -60,14 +60,14 @@ linked topic guide as the source of truth, and preserve unrelated settings when editing an existing file. Keep the published `$schema` URL in configuration examples, but do not fetch it to determine the V2 configuration shape. -See the [full configuration guide](https://v2.opencode.ai/docs/config) for +See the [full configuration guide](https://opencode.ai/v2/docs/config) for every field, examples, config locations, and links to dedicated feature guides. -## [V1 to V2 migration](https://v2.opencode.ai/docs/migrate-v1) +## [V1 to V2 migration](https://opencode.ai/v2/docs/migrate-v1) For any request to migrate OpenCode configuration, agents, commands, skills, plugins, integrations, or other behavior from V1 to V2, read the full -[migration guide](https://v2.opencode.ai/docs/migrate-v1) before acting. In +[migration guide](https://opencode.ai/v2/docs/migrate-v1) before acting. In the repository, its source is `packages/www/content/docs/(Get started)/migrate-v1.mdx`. V1 config files and `.opencode/` definitions are intended to remain compatible. @@ -76,18 +76,18 @@ V2 config uses more ergonomic shapes, but conversion is optional. When the user requests conversion, inspect the complete configuration, preserve behavior and unrelated settings, and apply only the relevant migrations from the guide. For plugin migrations, fetch and follow both the migration guide and the full -[plugins guide](https://v2.opencode.ai/docs/build/plugins). If non-API V1 +[plugins guide](https://opencode.ai/v2/docs/build/plugins). If non-API V1 functionality fails in V2, use the `report` skill to file it as a compatibility bug. -## [Plugins](https://v2.opencode.ai/docs/build/plugins) +## [Plugins](https://opencode.ai/v2/docs/build/plugins) For questions about creating, configuring, loading, publishing, or migrating -plugins, fetch the full [plugins guide](https://v2.opencode.ai/docs/build/plugins) +plugins, fetch the full [plugins guide](https://opencode.ai/v2/docs/build/plugins) before answering. This includes questions about the Effect plugin API, hooks, transforms, tools, plugin context capabilities, and package entrypoints. -## [Service](https://v2.opencode.ai/docs/troubleshooting#check-the-background-service) +## [Service](https://opencode.ai/v2/docs/troubleshooting#check-the-background-service) OpenCode uses a client-server architecture. Interfaces such as the TUI connect to a background OpenCode service, which owns sessions, configuration, plugins, @@ -106,7 +106,7 @@ Check its status after restarting: opencode2 service status ``` -## [API](https://v2.opencode.ai/docs/api) +## [API](https://opencode.ai/v2/docs/api) OpenCode exposes an HTTP API from its server. The API is described by an OpenAPI document available from the running server at `/openapi.json`. @@ -135,15 +135,15 @@ connected to an explicit server instead of its managed background service, use the same configured server and authentication context rather than constructing an unauthenticated request separately. -See the [full API reference](https://v2.opencode.ai/docs/api) for available +See the [full API reference](https://opencode.ai/v2/docs/api) for available endpoints, parameters, request bodies, and response schemas. The -raw [OpenAPI specification](https://v2.opencode.ai/openapi.json) is also +raw [OpenAPI specification](https://opencode.ai/v2/openapi.json) is also available for code generation and other tooling. -## [Client](https://v2.opencode.ai/docs/build/client) +## [Client](https://opencode.ai/v2/docs/build/client) For questions about connecting an application to OpenCode over the network, -fetch the full [client guide](https://v2.opencode.ai/docs/build/client) before +fetch the full [client guide](https://opencode.ai/v2/docs/build/client) before answering. `@opencode-ai/client` is the generated TypeScript client for the OpenCode HTTP @@ -154,7 +154,7 @@ exposes typed Effects, Streams, and decoded OpenCode schema values. Its `Service` API can discover, start, stop, and authenticate with the local background service from a Node application. -## [Troubleshooting](https://v2.opencode.ai/docs/troubleshooting) +## [Troubleshooting](https://opencode.ai/v2/docs/troubleshooting) OpenCode runs a client and a background server. Start by determining whether a problem belongs to the client, the shared server, or one project. @@ -174,6 +174,6 @@ problem belongs to the client, the shared server, or one project. - Redact API keys, authorization headers, prompts, file contents, and other sensitive data before sharing diagnostics. -See the [full troubleshooting guide](https://v2.opencode.ai/docs/troubleshooting) +See the [full troubleshooting guide](https://opencode.ai/v2/docs/troubleshooting) for service lifecycle commands, API inspection, log locations, explicit server connections, issue-reporting details, and local development paths. diff --git a/packages/core/src/plugin/supervisor.ts b/packages/core/src/plugin/supervisor.ts index c6b977d5a76..2b14c42db19 100644 --- a/packages/core/src/plugin/supervisor.ts +++ b/packages/core/src/plugin/supervisor.ts @@ -1,18 +1,17 @@ export * as PluginSupervisor from "./supervisor" -import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin" import { Event } from "@opencode-ai/schema/config" import { Context, Deferred, Effect, Layer, Option, Schema, Semaphore, Stream } from "effect" import path from "path" import { fileURLToPath, pathToFileURL } from "url" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Catalog } from "../catalog" -import { CommandV2 } from "../command" +import { Command } from "../command" import { Config } from "../config" import { ConfigPlugin } from "../config/plugin" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FileMutation } from "../file-mutation" import { FileSystem } from "../filesystem" import { Form } from "../form" @@ -25,16 +24,16 @@ import { Location } from "../location" import { LocationMutation } from "../location-mutation" import { ModelsDev } from "../models-dev" import { Npm } from "@opencode-ai/util/npm" -import { PermissionV2 } from "../permission" -import { PluginV2 } from "../plugin" +import { Permission } from "../permission" +import { Plugin } from "../plugin" import { PluginPromise } from "../plugin/promise" import { Reference } from "../reference" import { Ripgrep } from "../ripgrep" import { SessionInstructions } from "../session/instructions" import { Shell } from "../shell" -import { SkillV2 } from "../skill" +import { Skill } from "../skill" import { ReadToolFileSystem } from "../tool/read-filesystem" -import { ToolRegistry } from "../tool/registry" +import { Tool } from "../tool" import { WebSearch } from "../websearch" import { WellKnown } from "../wellknown" import { PluginInternal } from "./internal" @@ -46,7 +45,9 @@ const PluginModule = Schema.Struct({ default: Schema.Union([ Schema.Struct({ id: Schema.String, - effect: Schema.declare((input): input is Plugin["effect"] => typeof input === "function"), + effect: Schema.declare( + (input): input is import("@opencode-ai/plugin/effect/plugin").Plugin["effect"] => typeof input === "function", + ), }), Schema.Struct({ id: Schema.String, @@ -113,15 +114,15 @@ const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Con }) const resolve = Effect.fn("PluginSupervisor.resolve")(function* ( - pre: readonly PluginV2.Versioned[], - post: readonly PluginV2.Versioned[], + pre: readonly Plugin.Versioned[], + post: readonly Plugin.Versioned[], operations: readonly Operation[], ) { const matches = (selector: string, target: string) => selector === "*" || (selector.endsWith(".*") ? target.startsWith(selector.slice(0, -1)) : selector === target) const definitions = [...pre, ...post] const enabled = new Set(definitions.map((plugin) => plugin.id)) - const packages = new Map() + const packages = new Map() const plugins = () => [...definitions, ...packages.values()] for (const operation of operations) { @@ -183,7 +184,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract plugin.effect({ ...host, options: operation.options }), - } satisfies PluginV2.Versioned + } satisfies Plugin.Versioned }) function discoverDirectory(fs: FSUtil.Interface, directory: string) { @@ -211,10 +212,10 @@ export class Service extends Context.Service()("@opencode/Pl const layer = Layer.effect( Service, Effect.gen(function* () { - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service const sdk = yield* SdkPlugins.Service const config = yield* Config.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const lock = Semaphore.makeUnsafe(1) const ready = yield* Deferred.make() let observed = 0 @@ -238,7 +239,7 @@ const layer = Layer.effect( }), ) }) - const updates = yield* events + const updates = yield* bus .subscribe([Event.Updated, SdkPlugins.Updated]) .pipe(Stream.toQueue({ capacity: 1, strategy: "sliding" })) const signals = yield* Stream.concat( @@ -276,13 +277,13 @@ export const node = makeLocationNode({ service: Service, layer: nodeLayer, deps: [ - PluginV2.node, + Plugin.node, SdkPlugins.node, - AgentV2.node, + Agent.node, Catalog.node, - CommandV2.node, + Command.node, Config.node, - EventV2.node, + Bus.node, FileMutation.node, FileSystem.node, FSUtil.node, @@ -295,7 +296,7 @@ export const node = makeLocationNode({ LocationMutation.node, ModelsDev.node, Npm.node, - PermissionV2.node, + Permission.node, PluginRuntime.node, Form.node, ReadToolFileSystem.node, @@ -303,8 +304,8 @@ export const node = makeLocationNode({ Ripgrep.node, SessionInstructions.node, Shell.node, - SkillV2.node, - ToolRegistry.toolsNode, + Skill.node, + Tool.node, WebSearch.node, WellKnown.node, ], diff --git a/packages/core/src/plugin/system-prompt.ts b/packages/core/src/plugin/system-prompt.ts index 9d5d5578717..6bb78c864b3 100644 --- a/packages/core/src/plugin/system-prompt.ts +++ b/packages/core/src/plugin/system-prompt.ts @@ -1,6 +1,6 @@ export * as SystemPromptPlugin from "./system-prompt" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect } from "effect" import PROMPT_ANTHROPIC from "./system-prompt/anthropic.txt" @@ -33,14 +33,16 @@ function make(id: string, select: (modelID: string) => string | undefined) { effect: Effect.fn(`SystemPromptPlugin.${id}`)(function* (ctx) { yield* ctx.session.hook("context", (event) => Effect.gen(function* () { - if ((yield* ctx.agent.get(event.agent))?.system) return + if ((yield* ctx.agent.get({ agentID: event.agent })).data.system) return const system = event.system[0] if (!system) return - const model = yield* ctx.catalog.model.get(event.model.providerID, event.model.id) + const model = (yield* ctx.catalog.model.list()).data.find( + (model) => model.providerID === event.model.providerID && model.id === event.model.id, + ) const prompt = select(`${model?.modelID ?? event.model.id} ${model?.family ?? ""}`.toLowerCase()) if (!prompt) return event.system[0] = { ...system, text: prompt } - }), + }).pipe(Effect.catch(() => Effect.void)), ) }), }) diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts index 55c9667f9e8..b7e9e567d2b 100644 --- a/packages/core/src/plugin/variant.ts +++ b/packages/core/src/plugin/variant.ts @@ -1,9 +1,9 @@ export * as VariantPlugin from "./variant" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import { ModelV2 } from "../model" -import { ProviderV2 } from "../provider" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Model } from "../model" +import { Provider } from "../provider" export const Plugin = define({ id: "opencode.variant", @@ -32,13 +32,13 @@ export const Plugin = define({ export function generate( model: { readonly id: string; readonly modelID?: string; readonly package?: string }, provider?: { readonly package: string }, -): NonNullable { +): NonNullable { const packageName = model.package ?? provider?.package - if (!ProviderV2.isAISDK(packageName) || ProviderV2.packageName(packageName) !== "@ai-sdk/openai-compatible") return [] + if (!Provider.isAISDK(packageName) || Provider.packageName(packageName) !== "@ai-sdk/openai-compatible") return [] const ids = `${model.id} ${model.modelID ?? ""}`.toLowerCase() if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] return ["high", "max"].map((id) => ({ - id: ModelV2.VariantID.make(id), + id: Model.VariantID.make(id), settings: { reasoningEffort: id }, })) } diff --git a/packages/core/src/plugin/warming.ts b/packages/core/src/plugin/warming.ts index d7bb9d0fd84..828773de733 100644 --- a/packages/core/src/plugin/warming.ts +++ b/packages/core/src/plugin/warming.ts @@ -1,6 +1,6 @@ export * as WarmingPlugin from "./warming" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Clock, Duration, Effect, Scope } from "effect" import { Config } from "../config" import { SessionSchema } from "../session/schema" diff --git a/packages/core/src/plugin/websearch/exa.ts b/packages/core/src/plugin/websearch/exa.ts index ba759604078..af99fd49c20 100644 --- a/packages/core/src/plugin/websearch/exa.ts +++ b/packages/core/src/plugin/websearch/exa.ts @@ -1,6 +1,6 @@ export * as WebSearchExa from "./exa" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" import { HttpClient } from "effect/unstable/http" import { WebSearchMcp } from "./mcp" diff --git a/packages/core/src/plugin/websearch/parallel.ts b/packages/core/src/plugin/websearch/parallel.ts index e55302d8af7..05379652de0 100644 --- a/packages/core/src/plugin/websearch/parallel.ts +++ b/packages/core/src/plugin/websearch/parallel.ts @@ -1,6 +1,6 @@ export * as WebSearchParallel from "./parallel" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" import { HttpClient } from "effect/unstable/http" import { App } from "../../app" diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 30ea4654073..2fb0aabc18d 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -1,4 +1,3 @@ -export * as ProjectV2 from "./project" export * as Project from "./project" import { Context, Effect, Layer, Schema } from "effect" @@ -71,7 +70,7 @@ export interface Interface { readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/ProjectV2") {} +export class Service extends Context.Service()("@opencode/Project") {} function fromRow(row: typeof ProjectTable.$inferSelect): Info { const icon = diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts index 8b09eb78864..7a2b156df17 100644 --- a/packages/core/src/project/copy.ts +++ b/packages/core/src/project/copy.ts @@ -10,7 +10,7 @@ import { Project } from "../project" import { ProjectDirectories } from "./directories" import { makeGitWorktreeStrategy } from "./copy-strategies" import { Slug } from "../util/slug" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { Database } from "../database/database" import { Location } from "../location" import { Event } from "@opencode-ai/schema/project-directories" @@ -132,10 +132,10 @@ const layer = Layer.effect( const git = yield* Git.Service const directories = yield* ProjectDirectories.Service const db = (yield* Database.Service).db - const events = yield* EventV2.Service + const bus = yield* Bus.Service const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { - if (update) yield* events.publish(Event.Updated, { projectID }) + if (update) yield* bus.publish(Event.Updated, { projectID }) }) const canonical = Effect.fnUntraced(function* (input: AbsolutePath) { @@ -282,7 +282,7 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer: layer, - deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node], + deps: [FSUtil.node, Git.node, ProjectDirectories.node, Bus.node, Database.node], }) export const refreshNode = makeLocationNode({ diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index e378882d79e..629fe4cb66e 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -1,4 +1,4 @@ -export * as ProviderV2 from "./provider" +export * as Provider from "./provider" import { Effect, Schema } from "effect" import { Provider } from "@opencode-ai/schema/provider" @@ -25,7 +25,7 @@ type Json = Schema.Schema.Type const JsonRecord = Schema.Record(Schema.String, Schema.Json) const decodeJsonRecord = Schema.decodeUnknownSync(JsonRecord) -export class LoadError extends Schema.TaggedErrorClass()("ProviderV2.LoadError", { +export class LoadError extends Schema.TaggedErrorClass()("Provider.LoadError", { package: Schema.String, cause: Schema.Defect(), }) {} @@ -47,7 +47,7 @@ const builtins = new Map Promise>([ ["@opencode-ai/ai/providers/xai", () => import("@opencode-ai/ai/providers/xai")], ]) -export const loadPackage = Effect.fn("ProviderV2.loadPackage")(function* (specifier: string, npm?: Npm.Interface) { +export const loadPackage = Effect.fn("Provider.loadPackage")(function* (specifier: string, npm?: Npm.Interface) { const builtin = builtins.get(specifier) if (builtin) return yield* importPackage(specifier, specifier, builtin) const resolved = yield* Effect.sync(() => { @@ -133,7 +133,7 @@ export type Info = Provider.Info export type MutableInfo = DeepMutable -const importPackage = Effect.fn("ProviderV2.importPackage")(function* ( +const importPackage = Effect.fn("Provider.importPackage")(function* ( specifier: string, entrypoint: string, load = () => importModule(entrypoint), diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts index 427444d91a2..ad00debe8c0 100644 --- a/packages/core/src/pty.ts +++ b/packages/core/src/pty.ts @@ -5,7 +5,7 @@ import type { Disp, Proc } from "#pty" import { Context, Effect, Layer, Schema, Types } from "effect" import { Pty } from "@opencode-ai/schema/pty" import { Config } from "./config" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Location } from "./location" import { PtyID } from "./pty/schema" import { ShellSelect } from "./shell/select" @@ -47,7 +47,7 @@ export const UpdateInput = Pty.UpdateInput export type UpdateInput = Types.DeepMutable -export const Event = Pty.Event +export { Event } from "@opencode-ai/schema/pty" export type AttachInput = { // Absolute output cursor to replay from. -1 tails from the current end; omitted replays the full retained buffer. @@ -87,12 +87,12 @@ export interface Interface { readonly attach: (id: PtyID, input: AttachInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Pty") {} +export class Service extends Context.Service()("@opencode/Pty") {} export const layer = (options?: ShellSelect.Options) => Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const location = yield* Location.Service const config = yield* Config.Service const context = yield* Effect.context() @@ -146,7 +146,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( if (index !== -1) exitOrder.splice(index, 1) yield* Effect.logInfo("removing session", { id }) teardown(session) - yield* events.publish(Event.Deleted, { id: session.info.id }) + yield* bus.publish(Pty.Event.Deleted, { id: session.info.id }) }) const remove = Effect.fn("Pty.remove")(function* (id: PtyID) { @@ -229,7 +229,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( runFork( Effect.gen(function* () { yield* Effect.logInfo("session exited", { id, exitCode }) - yield* events.publish(Event.Exited, { id, exitCode }) + yield* bus.publish(Pty.Event.Exited, { id, exitCode }) while (exitOrder.length > EXITED_LIMIT) { const oldest = exitOrder[0] if (!oldest) break @@ -239,7 +239,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ) }), ) - yield* events.publish(Event.Created, { info }) + yield* bus.publish(Pty.Event.Created, { info }) return info }) @@ -247,7 +247,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( const session = yield* requireSession(id) if (input.title) session.info.title = input.title if (input.size && session.info.status === "running") session.process.resize(input.size.cols, input.size.rows) - yield* events.publish(Event.Updated, { info: session.info }) + yield* bus.publish(Pty.Event.Updated, { info: session.info }) return session.info }) @@ -314,7 +314,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ) export function configured(options?: ShellSelect.Options) { - return makeLocationNode({ service: Service, layer: layer(options), deps: [EventV2.node, Location.node, Config.node] }) + return makeLocationNode({ service: Service, layer: layer(options), deps: [Bus.node, Location.node, Config.node] }) } export const node = configured() diff --git a/packages/core/src/pty/ticket.ts b/packages/core/src/pty/ticket.ts index f434c367914..033460818f4 100644 --- a/packages/core/src/pty/ticket.ts +++ b/packages/core/src/pty/ticket.ts @@ -1,6 +1,6 @@ export * as PtyTicket from "./ticket" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { PtyTicket } from "@opencode-ai/schema/pty-ticket" import { PtyID } from "./schema" import { Cache, Context, Duration, Effect, Layer } from "effect" @@ -14,7 +14,7 @@ export const ConnectToken = PtyTicket.ConnectToken export type Scope = { readonly ptyID: PtyID readonly directory?: string - readonly workspaceID?: WorkspaceV2.ID + readonly workspaceID?: Workspace.ID } export interface Interface { diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts index 3961ad5b1fb..7164d1e4bf0 100644 --- a/packages/core/src/question.ts +++ b/packages/core/src/question.ts @@ -1,9 +1,9 @@ -export * as QuestionV2 from "./question" +export * as Question from "./question" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Deferred, Effect, Layer, Schema } from "effect" import { Question } from "@opencode-ai/schema/question" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { SessionSchema } from "./session/schema" export const ID = Question.ID @@ -30,15 +30,15 @@ export type Answer = typeof Answer.Type export const Reply = Question.Reply export type Reply = typeof Reply.Type -export const Event = Question.Event +export { Event } from "@opencode-ai/schema/question" -export class RejectedError extends Schema.TaggedErrorClass()("QuestionV2.RejectedError", {}) { +export class RejectedError extends Schema.TaggedErrorClass()("Question.RejectedError", {}) { override get message() { return "The user dismissed this question" } } -export class NotFoundError extends Schema.TaggedErrorClass()("QuestionV2.NotFoundError", { +export class NotFoundError extends Schema.TaggedErrorClass()("Question.NotFoundError", { requestID: ID, }) {} @@ -60,7 +60,7 @@ export interface Interface { readonly list: () => Effect.Effect> } -export class Service extends Context.Service()("@opencode/v2/Question") {} +export class Service extends Context.Service()("@opencode/Question") {} interface Pending { readonly request: Request @@ -75,7 +75,7 @@ interface Pending { const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const pending = new Map() yield* Effect.addFinalizer(() => @@ -90,14 +90,14 @@ const layer = Layer.effect( ), ) - const ask = Effect.fn("QuestionV2.ask")((input: AskInput) => + const ask = Effect.fn("Question.ask")((input: AskInput) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () { const id = ID.ascending() const deferred = yield* Deferred.make, RejectedError>() const request: Request = { id, ...input } pending.set(id, { request, deferred }) - return yield* events.publish(Event.Asked, request).pipe( + return yield* bus.publish(Question.Event.Asked, request).pipe( Effect.andThen(restore(Deferred.await(deferred))), Effect.ensuring( Effect.sync(() => { @@ -109,12 +109,12 @@ const layer = Layer.effect( ), ) - const reply = Effect.fn("QuestionV2.reply")((input: ReplyInput) => + const reply = Effect.fn("Question.reply")((input: ReplyInput) => Effect.uninterruptible( Effect.gen(function* () { const existing = pending.get(input.requestID) if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) - yield* events.publish(Event.Replied, { + yield* bus.publish(Question.Event.Replied, { sessionID: existing.request.sessionID, requestID: existing.request.id, answers: input.answers.map((answer) => [...answer]), @@ -125,12 +125,12 @@ const layer = Layer.effect( ), ) - const reject = Effect.fn("QuestionV2.reject")((requestID: ID) => + const reject = Effect.fn("Question.reject")((requestID: ID) => Effect.uninterruptible( Effect.gen(function* () { const existing = pending.get(requestID) if (!existing) return yield* new NotFoundError({ requestID }) - yield* events.publish(Event.Rejected, { + yield* bus.publish(Question.Event.Rejected, { sessionID: existing.request.sessionID, requestID: existing.request.id, }) @@ -140,7 +140,7 @@ const layer = Layer.effect( ), ) - const list = Effect.fn("QuestionV2.list")(function* () { + const list = Effect.fn("Question.list")(function* () { return Array.from(pending.values(), (item) => item.request) }) @@ -148,4 +148,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] }) diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 414af3ba32c..d6905051b99 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -4,7 +4,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Scope, Types } from "effect" import { Reference } from "@opencode-ai/schema/reference" import { Global } from "@opencode-ai/util/global" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Repository } from "./repository" import { RepositoryCache } from "./repository-cache" import { AbsolutePath } from "./schema" @@ -19,7 +19,7 @@ export type GitSource = Reference.GitSource export const Source = Reference.Source export type Source = Reference.Source -export const Event = Reference.Event +export { Event } from "@opencode-ai/schema/reference" export const Info = Reference.Info export type Info = Reference.Info @@ -38,13 +38,13 @@ export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Reference") {} +export class Service extends Context.Service()("@opencode/Reference") {} const layer = Layer.effect( Service, Effect.gen(function* () { const global = yield* Global.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const cache = yield* RepositoryCache.Service const scope = yield* Scope.Scope const materialized = new Map() @@ -107,7 +107,7 @@ const layer = Layer.effect( Effect.forkIn(scope), ) } - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Reference.Event.Updated, {}) }), }) @@ -124,5 +124,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [Global.node, EventV2.node, RepositoryCache.node], + deps: [Global.node, Bus.node, RepositoryCache.node], }) diff --git a/packages/core/src/reference/instructions.ts b/packages/core/src/reference/instructions.ts index 83632ba5850..34801bde10b 100644 --- a/packages/core/src/reference/instructions.ts +++ b/packages/core/src/reference/instructions.ts @@ -57,7 +57,7 @@ export interface Interface { readonly load: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/ReferenceInstructions") {} +export class Service extends Context.Service()("@opencode/ReferenceInstructions") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 5673c9ab4ce..a6aa9ed426c 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -83,7 +83,7 @@ export interface Interface { readonly grep: (input: GrepInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Ripgrep") {} +export class Service extends Context.Service()("@opencode/Ripgrep") {} const failure = (message: string, cause?: unknown) => new Error({ message, cause }) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 53a64866c2e..c2cdbb3ffc8 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,23 +1,23 @@ -export * as SessionV2 from "./session" +export * as Session from "./session" export * from "./session/schema" import { Effect, Layer, Schema, Context, Stream, Scope } from "effect" import { ListAnchor } from "@opencode-ai/schema/session" import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm" -import { ProjectV2 } from "./project" -import { WorkspaceV2 } from "./workspace" -import { ModelV2 } from "./model" +import { Project } from "./project" +import { Workspace } from "./workspace" +import { Model } from "./model" import { Location } from "./location" import { SessionMessage } from "./session/message" import { Base64, FileAttachment, Prompt } from "@opencode-ai/schema/prompt" import { PromptInput } from "@opencode-ai/schema/prompt-input" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Database } from "./database/database" import { SessionProjector } from "./session/projector" import { SessionMessageTable, SessionTable } from "./session/sql" import { SessionSchema } from "./session/schema" import { AbsolutePath, PositiveInt, RelativePath } from "./schema" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { SessionV1 } from "./v1/session" import { Money } from "@opencode-ai/schema/money" import { App } from "./app" @@ -41,9 +41,10 @@ import { FSUtil } from "@opencode-ai/util/fs-util" import { Image } from "./image" import { Mime } from "./mime" import type { EventLog } from "@opencode-ai/schema/event-log" -import { SkillV2 } from "./skill" +import { Event } from "@opencode-ai/schema/event" +import { Skill } from "./skill" import { Job } from "./job" -import { CommandV2 } from "./command" +import { Command } from "./command" import { Shell } from "./shell" import { Global } from "@opencode-ai/util/global" import { Shell as ShellSchema } from "@opencode-ai/schema/shell" @@ -65,7 +66,7 @@ export type RevertState = Session.Revert export { ListAnchor } const ListInputBase = { - workspaceID: WorkspaceV2.ID.pipe(Schema.optional), + workspaceID: Workspace.ID.pipe(Schema.optional), search: Schema.String.pipe(Schema.optional), limit: PositiveInt.pipe(Schema.optional), order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional), @@ -80,7 +81,7 @@ const ListDirectoryInput = Schema.Struct({ const ListProjectInput = Schema.Struct({ ...ListInputBase, - project: ProjectV2.ID, + project: Project.ID, subpath: RelativePath.pipe(Schema.optional), }) @@ -92,8 +93,8 @@ export type ListInput = typeof ListInput.Type type CreateBaseInput = { id?: SessionSchema.ID title?: string - agent?: AgentV2.ID - model?: ModelV2.Ref + agent?: Agent.ID + model?: Model.Ref } type CreateInput = CreateBaseInput & ({ location: Location.Ref; parentID?: never } | { parentID: SessionSchema.ID; location?: never }) @@ -143,7 +144,7 @@ export class BusyError extends Schema.TaggedErrorClass()("Session.Bus sessionID: SessionSchema.ID, }) {} export class SkillNotFoundError extends Schema.TaggedErrorClass()("Session.SkillNotFoundError", { - skill: SkillV2.ID, + skill: Skill.ID, }) {} export class DestinationNotFoundError extends Schema.TaggedErrorClass()( @@ -170,8 +171,8 @@ export type Error = | SkillNotFoundError | DestinationNotFoundError | DestinationNotDirectoryError - | CommandV2.NotFoundError - | CommandV2.EvaluationError + | Command.NotFoundError + | Command.EvaluationError | MessageNotFoundError | SessionGenerate.Error @@ -206,11 +207,11 @@ export interface Interface { */ readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect /** - * Durable, ordered session log read. Replays durable session events after + * Durable, ordered session log read. Replays durable session bus after * the exclusive `after` cursor, emits a `Synced` marker at the captured * replay watermark, then continues live when `follow` is set. * The marker's seq may exceed the last emitted event because other durable - * events share the aggregate's sequence space. + * bus share the aggregate's sequence space. */ readonly log: (input: { sessionID: SessionSchema.ID @@ -219,11 +220,11 @@ export interface Interface { }) => Stream.Stream readonly switchAgent: (input: { sessionID: SessionSchema.ID - agent: AgentV2.ID + agent: Agent.ID }) => Effect.Effect readonly switchModel: (input: { sessionID: SessionSchema.ID - model: ModelV2.Ref + model: Model.Ref }) => Effect.Effect readonly rename: (input: { sessionID: SessionSchema.ID; title: string }) => Effect.Effect readonly move: (input: { @@ -251,25 +252,25 @@ export interface Interface { sessionID: SessionSchema.ID command: string arguments?: string - agent?: AgentV2.ID - model?: ModelV2.Ref + agent?: Agent.ID + model?: Model.Ref files?: PromptInput.Prompt["files"] agents?: PromptInput.Prompt["agents"] delivery?: SessionPending.Delivery resume?: boolean }) => Effect.Effect< SessionPending.User, - NotFoundError | PromptConflictError | AttachmentError | CommandV2.NotFoundError | CommandV2.EvaluationError + NotFoundError | PromptConflictError | AttachmentError | Command.NotFoundError | Command.EvaluationError > readonly shell: (input: { - id?: EventV2.ID + id?: Event.ID sessionID: SessionSchema.ID command: string }) => Effect.Effect readonly skill: (input: { id?: SessionMessage.ID sessionID: SessionSchema.ID - skill: SkillV2.ID + skill: Skill.ID resume?: boolean }) => Effect.Effect readonly compact: ( @@ -300,7 +301,7 @@ export interface Interface { } } -export class Service extends Context.Service()("@opencode/v2/Session") {} +export class Service extends Context.Service()("@opencode/Session") {} const layer = Layer.effect( Service, @@ -308,8 +309,8 @@ const layer = Layer.effect( const app = yield* App.Metadata const database = yield* Database.Service const db = database.db - const events = yield* EventV2.Service - const projects = yield* ProjectV2.Service + const bus = yield* Bus.Service + const projects = yield* Project.Service const global = yield* Global.Service const execution = yield* SessionExecution.Service const store = yield* SessionStore.Service @@ -333,7 +334,7 @@ const layer = Layer.effect( ) const result = Service.of({ - create: Effect.fn("V2Session.create")(function* (input) { + create: Effect.fn("Session.create")(function* (input) { const sessionID = input.id ?? SessionSchema.ID.create() const recorded = yield* store.get(sessionID) if (recorded) return recorded @@ -341,7 +342,7 @@ const layer = Layer.effect( if (input.parentID && parent === undefined) return yield* new NotFoundError({ sessionID: input.parentID }) const location = parent?.location ?? input.location if (location === undefined) - return yield* Effect.die(new Error("V2Session.create requires either location or an existing parentID")) + return yield* Effect.die(new Error("Session.create requires either location or an existing parentID")) const project = yield* projects.resolve(location.directory) yield* db .insert(ProjectTable) @@ -358,12 +359,12 @@ const layer = Layer.effect( parentID: input.parentID, directory: location.directory, path: path.relative(project.directory, location.directory).replaceAll("\\", "/"), - workspaceID: location.workspaceID ? WorkspaceV2.ID.make(location.workspaceID) : undefined, + workspaceID: location.workspaceID ? Workspace.ID.make(location.workspaceID) : undefined, title: input.title ?? `New session - ${new Date(now).toISOString()}`, agent: input.agent, model: input.model ? { - id: ModelV2.ID.make(input.model.id), + id: Model.ID.make(input.model.id), providerID: input.model.providerID, variant: input.model.variant, } @@ -372,7 +373,7 @@ const layer = Layer.effect( tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: now, updated: now }, }) - const projected = yield* events.publish(SessionV1.Event.Created, { sessionID, info }, { location }).pipe( + const projected = yield* bus.publish(SessionV1.Event.Created, { sessionID, info }, { location }).pipe( Effect.as({ type: "created" } as const), Effect.catchDefect((defect) => { if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { @@ -392,7 +393,7 @@ const layer = Layer.effect( // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. return yield* result.get(sessionID).pipe(Effect.orDie) }), - fork: Effect.fn("V2Session.fork")(function* (input) { + fork: Effect.fn("Session.fork")(function* (input) { const parent = yield* result.get(input.sessionID) const boundary = input.messageID ? yield* db @@ -407,8 +408,8 @@ const layer = Layer.effect( if (input.messageID && !boundary) return yield* new MessageNotFoundError({ sessionID: input.sessionID, messageID: input.messageID }) const sessionID = SessionSchema.ID.create() - const parentSeq = boundary ? boundary.seq - 1 : yield* EventV2.latestSequence(db, parent.id) - yield* events.publish(SessionEvent.Forked, { + const parentSeq = boundary ? boundary.seq - 1 : yield* Bus.latestSequence(db, parent.id) + yield* bus.publish(SessionEvent.Forked, { sessionID, parentID: parent.id, parentSeq, @@ -416,21 +417,21 @@ const layer = Layer.effect( }) return yield* result.get(sessionID).pipe(Effect.orDie) }), - get: Effect.fn("V2Session.get")(function* (sessionID) { + get: Effect.fn("Session.get")(function* (sessionID) { const session = yield* store.get(sessionID) if (!session) return yield* new NotFoundError({ sessionID }) return session }), - remove: Effect.fn("V2Session.remove")(function* (sessionID) { + remove: Effect.fn("Session.remove")(function* (sessionID) { yield* result.get(sessionID) yield* execution.interrupt(sessionID) yield* execution.awaitIdle(sessionID) const children = yield* result.list({ parentID: sessionID }) yield* Effect.forEach(children.data, (child) => result.remove(child.id), { concurrency: 1, discard: true }) - yield* events.publish(SessionEvent.Deleted, { sessionID }) - yield* events.remove(sessionID) + yield* bus.publish(SessionEvent.Deleted, { sessionID }) + yield* bus.remove(sessionID) }), - list: Effect.fn("V2Session.list")(function* (input = {}) { + list: Effect.fn("Session.list")(function* (input = {}) { const direction = input.anchor?.direction ?? "next" const requestedOrder = input.order ?? "desc" const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder @@ -470,7 +471,7 @@ const layer = Layer.effect( ) return { data: (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) } }), - messages: Effect.fn("V2Session.messages")(function* (input) { + messages: Effect.fn("Session.messages")(function* (input) { yield* result.get(input.sessionID) const direction = input.cursor?.direction ?? "next" const requestedOrder = input.order ?? "desc" @@ -504,15 +505,15 @@ const layer = Layer.effect( ) return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) }), - message: Effect.fn("V2Session.message")(function* (input) { + message: Effect.fn("Session.message")(function* (input) { const stored = yield* store.message(input.messageID) return stored?.sessionID === input.sessionID ? stored.message : undefined }), - context: Effect.fn("V2Session.context")(function* (sessionID) { + context: Effect.fn("Session.context")(function* (sessionID) { yield* result.get(sessionID) return yield* store.context(sessionID) }), - pending: Effect.fn("V2Session.pending")(function* (sessionID) { + pending: Effect.fn("Session.pending")(function* (sessionID) { yield* result.get(sessionID) return yield* SessionPending.list(db, sessionID) }), @@ -520,21 +521,21 @@ const layer = Layer.effect( Stream.unwrap( result .get(input.sessionID) - .pipe(Effect.as(events.log({ aggregateID: input.sessionID, after: input.after, follow: input.follow }))), + .pipe(Effect.as(bus.log({ aggregateID: input.sessionID, after: input.after, follow: input.follow }))), ).pipe( Stream.filter( (item): item is SessionEvent.DurableEvent | EventLog.Synced => - EventV2.isSynced(item) || isDurableSessionEvent(item), + Bus.isSynced(item) || isDurableSessionEvent(item), ), ), - prompt: Effect.fn("V2Session.prompt")((input) => + prompt: Effect.fn("Session.prompt")((input) => Effect.uninterruptible( Effect.gen(function* () { const session = yield* result.get(input.sessionID) // A staged revert must be committed before admitting new input so the prompt // continues from the reverted boundary rather than stale post-boundary history. if (session.revert) - yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + yield* SessionRevert.commit(session).pipe(Effect.provideService(Bus.Service, bus)) // Resolved lazily so prompt admission only boots location services when an // image attachment actually needs the resizer. const image = Image.Service.pipe(Effect.provide(locations.get(session.location))) @@ -548,7 +549,7 @@ const layer = Layer.effect( data: { ...prompt, metadata: input.metadata }, delivery: input.delivery ?? "steer", }) - const admitted = yield* SessionPending.admit(db, events, { + const admitted = yield* SessionPending.admit(db, bus, { id: messageID, sessionID: input.sessionID, input: admittedInput, @@ -572,17 +573,17 @@ const layer = Layer.effect( }), ), ), - generate: Effect.fn("V2Session.generate")(function* (input) { + generate: Effect.fn("Session.generate")(function* (input) { const session = yield* result.get(input.sessionID) const generate = yield* SessionGenerate.Service.pipe(Effect.provide(locations.get(session.location))) return yield* generate.generate(input) }), - command: Effect.fn("V2Session.command")(function* (input) { + command: Effect.fn("Session.command")(function* (input) { const session = yield* result.get(input.sessionID) - const commands = yield* CommandV2.Service.pipe(Effect.provide(locations.get(session.location))) + const commands = yield* Command.Service.pipe(Effect.provide(locations.get(session.location))) const command = yield* commands.get(input.command) if (!command) - return yield* new CommandV2.NotFoundError({ + return yield* new Command.NotFoundError({ command: input.command, message: `Command not found: ${input.command}`, }) @@ -592,12 +593,12 @@ const layer = Layer.effect( const agent = command.agent ?? input.agent const commandAgent = yield* Effect.gen(function* () { if (!command.agent) return undefined - const agents = yield* AgentV2.Service.pipe(Effect.provide(locations.get(session.location))) - return yield* agents.get(AgentV2.ID.make(command.agent)) + const agents = yield* Agent.Service.pipe(Effect.provide(locations.get(session.location))) + return yield* agents.get(Agent.ID.make(command.agent)) }) const model = command.model ?? commandAgent?.model ?? input.model - if (agent !== undefined && session.agent !== AgentV2.ID.make(agent)) - yield* result.switchAgent({ sessionID: input.sessionID, agent: AgentV2.ID.make(agent) }) + if (agent !== undefined && session.agent !== Agent.ID.make(agent)) + yield* result.switchAgent({ sessionID: input.sessionID, agent: Agent.ID.make(agent) }) if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model }) return yield* result.prompt({ @@ -610,7 +611,7 @@ const layer = Layer.effect( resume: input.resume, }) }), - shell: Effect.fn("V2Session.shell")(function* (input) { + shell: Effect.fn("Session.shell")(function* (input) { const session = yield* result.get(input.sessionID) yield* shellLocks.withLock(input.sessionID)( Effect.gen(function* () { @@ -620,7 +621,7 @@ const layer = Layer.effect( const shell = yield* Shell.Service return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 }) }).pipe(Effect.provide(locations.get(session.location))) - yield* events.publish( + yield* bus.publish( SessionEvent.Shell.Started, { sessionID: input.sessionID, @@ -643,7 +644,7 @@ const layer = Layer.effect( : missingShellOutput() return { shell: terminal.info, output } }).pipe(Effect.provide(locations.get(session.location))) - yield* events.publish(SessionEvent.Shell.Ended, { + yield* bus.publish(SessionEvent.Shell.Ended, { sessionID: input.sessionID, shell: completed.shell, output: completed.output, @@ -658,12 +659,12 @@ const layer = Layer.effect( ), ) }), - skill: Effect.fn("V2Session.skill")(function* (input) { + skill: Effect.fn("Session.skill")(function* (input) { const session = yield* result.get(input.sessionID) - const skills = yield* SkillV2.Service.pipe(Effect.provide(locations.get(session.location))) + const skills = yield* Skill.Service.pipe(Effect.provide(locations.get(session.location))) const skill = (yield* skills.list()).find((item) => item.id === input.skill) if (!skill) return yield* new SkillNotFoundError({ skill: input.skill }) - yield* events.publish( + yield* bus.publish( SessionEvent.Skill.Activated, { sessionID: input.sessionID, @@ -671,21 +672,21 @@ const layer = Layer.effect( name: skill.name, text: skill.content, }, - { id: input.id ? EventV2.ID.make(input.id.replace(/^msg_/, "evt_")) : undefined }, + { id: input.id ? Event.ID.make(input.id.replace(/^msg_/, "evt_")) : undefined }, ) if (input.resume !== false) yield* execution .resume(input.sessionID) .pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) }), - switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { + switchAgent: Effect.fn("Session.switchAgent")(function* (input) { yield* result.get(input.sessionID) - yield* events.publish(SessionEvent.AgentSelected, { + yield* bus.publish(SessionEvent.AgentSelected, { sessionID: input.sessionID, agent: input.agent, }) }), - switchModel: Effect.fn("V2Session.switchModel")(function* (input) { + switchModel: Effect.fn("Session.switchModel")(function* (input) { const session = yield* result.get(input.sessionID) if ( session.model?.providerID === input.model.providerID && @@ -693,19 +694,19 @@ const layer = Layer.effect( (session.model.variant ?? "default") === (input.model.variant ?? "default") ) return - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID: input.sessionID, model: input.model, }) }), - rename: Effect.fn("V2Session.rename")(function* (input) { + rename: Effect.fn("Session.rename")(function* (input) { yield* result.get(input.sessionID) - yield* events.publish(SessionEvent.Renamed, { + yield* bus.publish(SessionEvent.Renamed, { sessionID: input.sessionID, title: input.title, }) }), - move: Effect.fn("V2Session.move")(function* (input) { + move: Effect.fn("Session.move")(function* (input) { const current = yield* result.get(input.sessionID) const value = input.directory.trim() const expanded = @@ -730,17 +731,17 @@ const layer = Layer.effect( yield* execution.interrupt(input.sessionID) yield* execution.awaitIdle(input.sessionID) } - yield* events.publish(SessionEvent.Moved, { + yield* bus.publish(SessionEvent.Moved, { sessionID: input.sessionID, location: Location.Ref.make({ directory, workspaceID: input.workspaceID }), projectID: project.id, subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")), }) }), - compact: Effect.fn("V2Session.compact")(function* (input) { + compact: Effect.fn("Session.compact")(function* (input) { yield* result.get(input.sessionID) const inputID = input.id ?? SessionMessage.ID.create() - const admitted = yield* SessionPending.admitCompaction(db, events, { + const admitted = yield* SessionPending.admitCompaction(db, bus, { id: inputID, sessionID: input.sessionID, }).pipe( @@ -753,12 +754,12 @@ const layer = Layer.effect( yield* execution.wake(input.sessionID) return admitted }), - wait: Effect.fn("V2Session.wait")(function* (sessionID) { + wait: Effect.fn("Session.wait")(function* (sessionID) { yield* result.get(sessionID) yield* execution.awaitIdle(sessionID) }), active: execution.active, - background: Effect.fn("V2Session.background")(function* (sessionID) { + background: Effect.fn("Session.background")(function* (sessionID) { yield* result.get(sessionID) const backgrounded = yield* jobs.backgroundAll({ sessionID }) if (backgrounded.length === 0) return @@ -776,11 +777,11 @@ const layer = Layer.effect( }) .pipe(Effect.catchTag("Session.SyntheticConflictError", Effect.die)) }), - resume: Effect.fn("V2Session.resume")(function* (sessionID) { + resume: Effect.fn("Session.resume")(function* (sessionID) { yield* result.get(sessionID) yield* execution.resume(sessionID) }), - synthetic: Effect.fn("V2Session.synthetic")((input) => + synthetic: Effect.fn("Session.synthetic")((input) => Effect.uninterruptible( Effect.gen(function* () { yield* result.get(input.sessionID) @@ -794,7 +795,7 @@ const layer = Layer.effect( }, delivery: input.delivery ?? "steer", }) - const admitted = yield* SessionPending.admit(db, events, { + const admitted = yield* SessionPending.admit(db, bus, { id: inputID, sessionID: input.sessionID, input: admittedInput, @@ -816,34 +817,34 @@ const layer = Layer.effect( }), ), ), - interrupt: Effect.fn("V2Session.interrupt")((sessionID) => + interrupt: Effect.fn("Session.interrupt")((sessionID) => Effect.uninterruptible(execution.interrupt(sessionID)), ), revert: { - stage: Effect.fn("V2Session.revert.stage")(function* (input) { + stage: Effect.fn("Session.revert.stage")(function* (input) { const session = yield* result.get(input.sessionID) if ((yield* execution.active).has(input.sessionID)) return yield* new BusyError({ sessionID: input.sessionID }) return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( Effect.provideService(Database.Service, database), - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), Effect.provide(locations.get(session.location)), ) }), - clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { + clear: Effect.fn("Session.revert.clear")(function* (sessionID) { const session = yield* result.get(sessionID) if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID }) const revert = yield* SessionRevert.clear(session).pipe( - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), Effect.provide(locations.get(session.location)), ) yield* execution.wake(sessionID) return revert }), - commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { + commit: Effect.fn("Session.revert.commit")(function* (sessionID) { const session = yield* result.get(sessionID) if ((yield* execution.active).has(sessionID)) return yield* new BusyError({ sessionID }) - return yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + return yield* SessionRevert.commit(session).pipe(Effect.provideService(Bus.Service, bus)) }), }, }) @@ -872,7 +873,7 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf } } -const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* ( +const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* ( input: PromptInput.Prompt, image: Effect.Effect, ) { @@ -885,7 +886,7 @@ const resolvePrompt = Effect.fn("V2Session.resolvePrompt")(function* ( const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 -const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(function* ( +const materializeAttachment = Effect.fn("Session.materializeAttachment")(function* ( fs: FSUtil.Interface, input: PromptInput.FileAttachment, image: Effect.Effect, @@ -933,7 +934,7 @@ const materializeAttachment = Effect.fn("V2Session.materializeAttachment")(funct }) }) -const normalizeImageAttachment = Effect.fn("V2Session.normalizeImageAttachment")(function* ( +const normalizeImageAttachment = Effect.fn("Session.normalizeImageAttachment")(function* ( input: PromptInput.FileAttachment, data: Base64, mime: string, @@ -950,7 +951,7 @@ const normalizeImageAttachment = Effect.fn("V2Session.normalizeImageAttachment") return { data: Base64.make(normalized.content), mime: normalized.mime } }) -const readFileAttachment = Effect.fn("V2Session.readFileAttachment")(function* (fs: FSUtil.Interface, uri: string) { +const readFileAttachment = Effect.fn("Session.readFileAttachment")(function* (fs: FSUtil.Interface, uri: string) { const url = yield* Effect.try({ try: () => new URL(uri), catch: () => new AttachmentError({ uri, message: `Invalid attachment URI: ${uri}` }), @@ -1033,8 +1034,8 @@ export const node = makeGlobalNode({ deps: [ Job.node, Database.node, - EventV2.node, - ProjectV2.node, + Bus.node, + Project.node, SessionExecution.node, SessionStore.node, LocationServiceMap.node, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index a5b2c626b62..569675cb459 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -4,7 +4,7 @@ import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Mode import { SessionError } from "@opencode-ai/schema/session-error" import { Context, Effect, Layer, Stream } from "effect" import { Config } from "../config" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "../effect/app-node-platform" import { SessionEvent } from "./event" @@ -15,7 +15,7 @@ import { SessionRunnerModel } from "./runner/model" import { SessionSchema } from "./schema" import { toSessionError } from "./to-session-error" import { Token } from "../util/token" -import type { ModelV2 } from "../model" +import type { Info } from "../model" import { SessionUsage } from "./usage" const DEFAULT_BUFFER = 20_000 @@ -62,7 +62,7 @@ type Settings = { type Dependencies = { readonly app: App.Info - readonly events: EventV2.Interface + readonly bus: Bus.Interface readonly llm: { readonly stream: (request: LLMRequest) => Stream.Stream } @@ -74,7 +74,7 @@ export type AutoInput = { readonly session: SessionSchema.Info readonly messages: readonly SessionMessage.Info[] readonly model: Model - readonly cost: ModelV2.Info["cost"] + readonly cost: Info["cost"] } export type ManualInput = { @@ -86,7 +86,7 @@ export type ManualInput = { type Plan = { readonly session: SessionSchema.Info readonly model: Model - readonly cost: ModelV2.Info["cost"] + readonly cost: Info["cost"] readonly reason: SessionMessage.Compaction["reason"] readonly prompt: string readonly recent: string @@ -103,7 +103,7 @@ export interface Interface { readonly compactManual: (input: ManualInput) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} +export class Service extends Context.Service()("@opencode/SessionCompaction") {} const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` @@ -233,11 +233,11 @@ const make = (dependencies: Dependencies) => { readonly error: SessionError.Error readonly inputID?: SessionMessage.ID }) { - yield* dependencies.events.publish(SessionEvent.Compaction.Failed, input) + yield* dependencies.bus.publish(SessionEvent.Compaction.Failed, input) return { status: "failed" as const, error: input.error } }) const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) { - yield* dependencies.events.publish(SessionEvent.Compaction.Started, { + yield* dependencies.bus.publish(SessionEvent.Compaction.Started, { sessionID: plan.session.id, reason: plan.reason, recent: plan.recent, @@ -249,7 +249,7 @@ const make = (dependencies: Dependencies) => { let usage: SessionUsage.Recorded | undefined const recordUsage = Effect.suspend(() => usage - ? dependencies.events.publish(SessionEvent.UsageRecorded, { + ? dependencies.bus.publish(SessionEvent.UsageRecorded, { sessionID: plan.session.id, source: "compaction", ...usage, @@ -274,7 +274,7 @@ const make = (dependencies: Dependencies) => { } if (LLMEvent.is.textDelta(event)) { chunks.push(event.text) - return dependencies.events.publish(SessionEvent.Compaction.Delta, { + return dependencies.bus.publish(SessionEvent.Compaction.Delta, { sessionID: plan.session.id, text: event.text, }) @@ -316,7 +316,7 @@ const make = (dependencies: Dependencies) => { inputID: plan.inputID, }) } - yield* dependencies.events.publish(SessionEvent.Compaction.Ended, { + yield* dependencies.bus.publish(SessionEvent.Compaction.Ended, { sessionID: plan.session.id, reason: plan.reason, text: summary, @@ -395,17 +395,17 @@ const make = (dependencies: Dependencies) => { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const llm = yield* LLMClient.Service const config = yield* Config.Service const models = yield* SessionRunnerModel.Service const app = yield* App.Metadata - return make({ events, llm, models, config: settings(yield* config.entries()), app }) + return make({ bus, llm, models, config: settings(yield* config.entries()), app }) }), ) export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node, App.node], + deps: [Bus.node, llmClient, Config.node, SessionRunnerModel.node, App.node], }) diff --git a/packages/core/src/session/context.ts b/packages/core/src/session/context.ts index c2c41e32998..d4936bd2c5b 100644 --- a/packages/core/src/session/context.ts +++ b/packages/core/src/session/context.ts @@ -1,7 +1,7 @@ export * as SessionContext from "./context" import { Context, Effect, Layer } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { CodeModeInstructions } from "../codemode/instructions" import { Database } from "../database/database" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -13,7 +13,7 @@ import { McpInstructions } from "../mcp/instructions" import { PluginSupervisor } from "../plugin/supervisor" import { ReferenceInstructions } from "../reference/instructions" import { SkillInstructions } from "../skill/instructions" -import { ToolRegistry } from "../tool/registry" +import { Tool } from "../tool" import { AgentNotFoundError } from "./error" import { SessionHistory } from "./history" import { InstructionEntry } from "./instruction-entry" @@ -24,18 +24,18 @@ import { SessionStore } from "./store" export interface Selection { readonly session: SessionSchema.Info - readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info } + readonly agent: Agent.Selection & { readonly info: Agent.Info } readonly instructions: Instructions.Instructions - readonly toolSet: ToolRegistry.ToolSet + readonly tools: Tool.Snapshot } export interface Loaded { readonly session: SessionSchema.Info - readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info } + readonly agent: Agent.Selection & { readonly info: Agent.Info } readonly model: SessionRunnerModel.Resolved readonly initial: string readonly messages: ReadonlyArray - readonly toolSet: ToolRegistry.ToolSet + readonly tools: Tool.Snapshot } /** @@ -52,12 +52,12 @@ export interface Interface { } /** Location-scoped model-context loader for durable Session Steps. */ -export class Service extends Context.Service()("@opencode/v2/SessionContext") {} +export class Service extends Context.Service()("@opencode/SessionContext") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const builtins = yield* InstructionBuiltIns.Service const db = (yield* Database.Service).db const discovery = yield* InstructionDiscovery.Service @@ -69,7 +69,7 @@ const layer = Layer.effect( const referenceInstructions = yield* ReferenceInstructions.Service const skillInstructions = yield* SkillInstructions.Service const store = yield* SessionStore.Service - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const select = Effect.fn("SessionContext.select")(function* (sessionID: SessionSchema.ID) { const session = yield* store.get(sessionID) @@ -82,7 +82,7 @@ const layer = Layer.effect( if (!agent.info) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id }) const loaded = yield* Effect.all( { - toolSet: registry.snapshot(agent.info.permissions), + tools: registry.snapshot(agent.info.permissions), builtins: builtins.load(sessionID), discovery: discovery.load(), skills: skillInstructions.load(agent), @@ -97,14 +97,14 @@ const layer = Layer.effect( agent: { ...agent, info: agent.info }, instructions: Instructions.combine([ loaded.builtins, - CodeModeInstructions.make(loaded.toolSet.codeModeCatalog), + CodeModeInstructions.make(loaded.tools.codeModeCatalog), loaded.discovery, loaded.skills, loaded.references, loaded.mcp, loaded.entries, ]), - toolSet: loaded.toolSet, + tools: loaded.tools, } }) @@ -117,7 +117,7 @@ const layer = Layer.effect( model, initial: history.initial, messages: history.entries.map((entry) => entry.message), - toolSet: selection.toolSet, + tools: selection.tools, } }) @@ -129,7 +129,7 @@ export const node = makeLocationNode({ service: Service, layer, deps: [ - AgentV2.node, + Agent.node, Database.node, InstructionBuiltIns.node, InstructionDiscovery.node, @@ -141,6 +141,6 @@ export const node = makeLocationNode({ SessionRunnerModel.node, SessionStore.node, SkillInstructions.node, - ToolRegistry.node, + Tool.node, ], }) diff --git a/packages/core/src/session/execution.ts b/packages/core/src/session/execution.ts index 9cef1472529..44a90a8492d 100644 --- a/packages/core/src/session/execution.ts +++ b/packages/core/src/session/execution.ts @@ -1,7 +1,7 @@ export * as SessionExecution from "./execution" import { Cause, Context, Effect, Exit, Layer } from "effect" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { LocationServiceMap } from "../location-service-map" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { SessionEvent } from "./event" @@ -26,7 +26,7 @@ export interface Interface { } /** Routes execution from a Session ID to the runner owned by that Session's Location. */ -export class Service extends Context.Service()("@opencode/v2/SessionExecution") {} +export class Service extends Context.Service()("@opencode/SessionExecution") {} type InterruptReason = "user" | "shutdown" | "superseded" @@ -44,7 +44,7 @@ export const layer = Layer.effect( Effect.gen(function* () { const store = yield* SessionStore.Service const locations = yield* LocationServiceMap.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const reportLifecycle = (sessionID: SessionSchema.ID, effect: Effect.Effect) => effect.pipe( Effect.tapCause((cause) => @@ -65,7 +65,7 @@ export const layer = Layer.effect( started: (sessionID) => reportLifecycle( sessionID, - events.publish(SessionEvent.Execution.Started, { sessionID }, clearSuspensionOnCommit(sessionID)), + bus.publish(SessionEvent.Execution.Started, { sessionID }, clearSuspensionOnCommit(sessionID)), ), drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) { const session = yield* store.get(sessionID) @@ -86,14 +86,14 @@ export const layer = Layer.effect( Effect.gen(function* () { const outcome = terminal(exit, reason) if (outcome.type === "succeeded") { - yield* events.publish(SessionEvent.Execution.Succeeded, { sessionID }, clearSuspensionOnCommit(sessionID)) + yield* bus.publish(SessionEvent.Execution.Succeeded, { sessionID }, clearSuspensionOnCommit(sessionID)) return } if (outcome.type === "interrupted") { - yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason }) + yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason }) return } - yield* events.publish( + yield* bus.publish( SessionEvent.Execution.Failed, { sessionID, @@ -118,7 +118,7 @@ export const layer = Layer.effect( export const node = makeGlobalNode({ service: Service, layer, - deps: [SessionStore.node, LocationServiceMap.node, EventV2.node], + deps: [SessionStore.node, LocationServiceMap.node, Bus.node], }) /** Low-level compatibility layer for callers that only need durable Session recording. */ diff --git a/packages/core/src/session/execution/restart.ts b/packages/core/src/session/execution/restart.ts index d7133bb7d25..08d8bf40119 100644 --- a/packages/core/src/session/execution/restart.ts +++ b/packages/core/src/session/execution/restart.ts @@ -19,7 +19,7 @@ export interface Interface { * Restart continuity actions for the managed server. The service is inert until called: only the * managed server invokes it, so default, embedded, and stdio servers never suspend or auto-resume. */ -export class Service extends Context.Service()("@opencode/v2/SessionRestart") {} +export class Service extends Context.Service()("@opencode/SessionRestart") {} export const layer = Layer.effect( Service, diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index e1b81c22b91..9a5c1031fd8 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -34,8 +34,8 @@ export const layer = Layer.effect( const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id) ? selection.session.id.slice(4) : selection.session.id - const toolSet = selection.toolSet - const toolDefinitions = toolSet.definitions + const tools = selection.tools + const toolDefinitions = tools.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) const contextEvent = yield* hooks.trigger("session", "context", { sessionID: selection.session.id, diff --git a/packages/core/src/session/generate.ts b/packages/core/src/session/generate.ts index 381c3345185..a8f38b10c82 100644 --- a/packages/core/src/session/generate.ts +++ b/packages/core/src/session/generate.ts @@ -18,4 +18,4 @@ export interface Interface { } /** Location-scoped transient generation from Session context. */ -export class Service extends Context.Service()("@opencode/v2/SessionGenerate") {} +export class Service extends Context.Service()("@opencode/SessionGenerate") {} diff --git a/packages/core/src/session/info.ts b/packages/core/src/session/info.ts index 2c507d00589..83d0297bd24 100644 --- a/packages/core/src/session/info.ts +++ b/packages/core/src/session/info.ts @@ -1,11 +1,11 @@ import { DateTime, Schema } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Location } from "../location" -import { ModelV2 } from "../model" -import { ProjectV2 } from "../project" -import { ProviderV2 } from "../provider" +import { Model } from "../model" +import { Project } from "../project" +import { Provider } from "../provider" import { AbsolutePath, RelativePath } from "../schema" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { SessionSchema } from "./schema" import { SessionTable } from "./sql" import { SessionMessage } from "./message" @@ -17,7 +17,7 @@ const decodeRevert = Schema.decodeUnknownSync(PersistedRevert) export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info { return SessionSchema.Info.make({ id: SessionSchema.ID.make(row.id), - projectID: ProjectV2.ID.make(row.project_id), + projectID: Project.ID.make(row.project_id), title: row.title, parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined, fork: row.fork_session_id @@ -26,12 +26,12 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In messageID: row.fork_message_id ? SessionMessage.ID.make(row.fork_message_id) : undefined, } : undefined, - agent: row.agent ? AgentV2.ID.make(row.agent) : undefined, + agent: row.agent ? Agent.ID.make(row.agent) : undefined, model: row.model ? { - id: ModelV2.ID.make(row.model.id), - providerID: ProviderV2.ID.make(row.model.providerID), - variant: ModelV2.VariantID.make(row.model.variant ?? "default"), + id: Model.ID.make(row.model.id), + providerID: Provider.ID.make(row.model.providerID), + variant: Model.VariantID.make(row.model.variant ?? "default"), } : undefined, cost: Money.USD.make(row.cost), @@ -46,7 +46,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In }, location: Location.Ref.make({ directory: AbsolutePath.make(row.directory), - workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, + workspaceID: row.workspace_id ? Workspace.ID.make(row.workspace_id) : undefined, }), subpath: row.path ? RelativePath.make(row.path) : undefined, revert: row.revert ? decodeRevert(row.revert) : undefined, diff --git a/packages/core/src/session/instruction-entry.ts b/packages/core/src/session/instruction-entry.ts index 371a7337e76..5fca262da00 100644 --- a/packages/core/src/session/instruction-entry.ts +++ b/packages/core/src/session/instruction-entry.ts @@ -28,7 +28,7 @@ export interface Interface { readonly load: (sessionID: SessionSchema.ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/InstructionEntry") {} +export class Service extends Context.Service()("@opencode/InstructionEntry") {} const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2)) diff --git a/packages/core/src/session/instruction-state.ts b/packages/core/src/session/instruction-state.ts index 1d6127b6dee..364dfba8a5e 100644 --- a/packages/core/src/session/instruction-state.ts +++ b/packages/core/src/session/instruction-state.ts @@ -3,11 +3,12 @@ export * as InstructionState from "./instruction-state" import { and, asc, desc, eq, gt, inArray, lte, sql } from "drizzle-orm" import { DateTime, Effect, Option, Schema } from "effect" import type { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { EventTable } from "../event/sql" import { Instructions } from "../instructions/index" import { SessionEvent } from "./event" import { SessionMessage } from "./message" +import { Event } from "@opencode-ai/schema/event" import { SessionSchema } from "./schema" import { InstructionBlobTable, InstructionStateTable, SessionTable } from "./sql" @@ -39,11 +40,11 @@ export const observe = Effect.fn("InstructionState.observe")(function* ( export const commit = Effect.fn("InstructionState.commit")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, observation: Observation, ) { if (!observation.initial && Object.keys(observation.delta).length === 0) return - yield* events.publish( + yield* bus.publish( SessionEvent.InstructionsUpdated, { sessionID: observation.sessionID, delta: observation.delta }, { @@ -56,11 +57,11 @@ export const commit = Effect.fn("InstructionState.commit")(function* ( export const prepare = Effect.fn("InstructionState.prepare")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, instructions: Instructions.Instructions, sessionID: SessionSchema.ID, ) { - yield* commit(db, events, yield* observe(db, instructions, sessionID)) + yield* commit(db, bus, yield* observe(db, instructions, sessionID)) }) export const apply = Effect.fn("InstructionState.apply")(function* ( @@ -171,7 +172,7 @@ const assembleState = Effect.fnUntraced(function* ( result.push({ seq: update.row.seq, message: SessionMessage.System.make({ - id: SessionMessage.ID.fromEvent(EventV2.ID.make(update.row.id)), + id: SessionMessage.ID.fromEvent(Event.ID.make(update.row.id)), type: "system", text, time: { created: DateTime.makeUnsafe(update.row.created) }, @@ -310,16 +311,16 @@ function requireBlob(blobs: ReadonlyMap, hash: I return value } -const instructionEventType = EventV2.versionedType( +const instructionEventType = Bus.versionedType( SessionEvent.InstructionsUpdated.type, SessionEvent.InstructionsUpdated.durable.version, ) -const compactionEventType = EventV2.versionedType( +const compactionEventType = Bus.versionedType( SessionEvent.Compaction.Ended.type, SessionEvent.Compaction.Ended.durable.version, ) -const movedEventType = EventV2.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version) -const revertedEventType = EventV2.versionedType( +const movedEventType = Bus.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version) +const revertedEventType = Bus.versionedType( SessionEvent.RevertEvent.Committed.type, SessionEvent.RevertEvent.Committed.durable.version, ) diff --git a/packages/core/src/session/instructions.ts b/packages/core/src/session/instructions.ts index d3ad3fcbaf6..4688c79bb3f 100644 --- a/packages/core/src/session/instructions.ts +++ b/packages/core/src/session/instructions.ts @@ -3,7 +3,7 @@ export * as SessionInstructions from "./instructions" import { relative } from "path" import { Context, DateTime, Effect, Layer, Option, Ref, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "../location" import { SessionEvent } from "./event" @@ -23,12 +23,12 @@ export interface Interface { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionInstructions") {} +export class Service extends Context.Service()("@opencode/SessionInstructions") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const fs = yield* FSUtil.Service const store = yield* SessionStore.Service const location = yield* Location.Service @@ -67,12 +67,12 @@ const layer = Layer.effect( ) const readable = files.filter((file): file is { path: string; content: string } => file !== undefined) if (readable.length === 0) return - // Publish directly rather than through SessionV2.synthetic: a Location-scoped layer - // cannot depend on SessionV2 (it routes through LocationServiceMap, forming a type + // Publish directly rather than through Session.synthetic: a Location-scoped layer + // cannot depend on Session (it routes through LocationServiceMap, forming a type // cycle with this node). The durable publish is what makes the synthetic visible on // the next projected history reload. The dedup ledger lives on the synthetic message // metadata so it survives across Location layer restarts. - yield* events.publish(SessionEvent.Synthetic, { + yield* bus.publish(SessionEvent.Synthetic, { sessionID: input.sessionID, text: readable.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n"), description: `Loaded ${readable.map((file) => describePath(root, file.path)).join(", ")}`, @@ -109,5 +109,5 @@ function describePath(root: string, path: string) { export const node = makeLocationNode({ name: "session-instructions", layer, - deps: [EventV2.node, FSUtil.node, Location.node, SessionStore.node], + deps: [Bus.node, FSUtil.node, Location.node, SessionStore.node], }) diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index b62fdc8f3f7..4cf3e4f8de7 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -1,16 +1,16 @@ export * as SessionModelRequest from "./model-request" -import { LLM, Message, SystemPart, type LLMRequest, type ToolContent } from "@opencode-ai/ai" +import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai" +import type { Content } from "@opencode-ai/schema/tool" import { SessionError } from "@opencode-ai/schema/session-error" import { Cause, Context, Effect, Layer, Result } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "../app" -import { ModelV2 } from "../model" -import { PermissionV2 } from "../permission" +import { Model } from "../model" +import { Permission } from "../permission" import { PluginHooks } from "../plugin/hooks" -import { QuestionTool } from "../tool/question" -import { ToolOutputStore } from "../tool-output-store" -import { ToolRegistry } from "../tool/registry" +import { QuestionTool } from "../tool/plugin/question" +import { Tool } from "../tool" import { SessionContext } from "./context" import { SessionModelHeaders } from "./model-headers" import { MAX_STEPS_PROMPT } from "./runner/max-steps" @@ -18,16 +18,16 @@ import PROMPT_DEFAULT from "./runner/prompt/base.txt" import { toLLMMessages } from "./runner/to-llm-message" /** Failures a prepared execution can surface: infrastructure errors plus user declines resurfaced from the defect tunnel. */ -export type ExecuteError = ToolOutputStore.Error | PermissionV2.DeclinedError | QuestionTool.CancelledError +export type ExecuteError = Tool.Error | Permission.DeclinedError | QuestionTool.CancelledError // User declines dive under the leaves' blanket `mapError` as defects (the deliberate -// tunnel entered in PermissionV2.assert and the question tool), so a user's "no" can +// tunnel entered in Permission.assert and the question tool), so a user's "no" can // never become model-facing tool output. They resurface as typed failures exactly once, // here at the seam the runner executes through. -const declineDefect = (cause: Cause.Cause) => { +const declineDefect = (cause: Cause.Cause) => { const decline = cause.reasons.flatMap((reason) => Cause.isDieReason(reason) && - (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError) + (reason.defect instanceof Permission.DeclinedError || reason.defect instanceof QuestionTool.CancelledError) ? [reason.defect] : [], )[0] @@ -41,8 +41,8 @@ interface Prepared { * step-limit-violating calls fail individually through the same seam. */ readonly executeTool: ( - input: ToolRegistry.ExecuteInput, - ) => Effect.Effect + input: Parameters[0], + ) => Effect.Effect /** True when this request is the final Step; violating calls are rejected and no continuation follows. */ readonly stepLimitReached: boolean } @@ -59,7 +59,7 @@ const mimeToModality = (mime: string) => { if (mime === "application/pdf") return "pdf" } -const unsupportedMedia = (mime: string, name: string | undefined, capabilities: ModelV2.Capabilities) => { +const unsupportedMedia = (mime: string, name: string | undefined, capabilities: Model.Capabilities) => { const modality = mimeToModality(mime) if (!modality || capabilities.input.some((item) => item.startsWith(modality))) return return { @@ -68,7 +68,7 @@ const unsupportedMedia = (mime: string, name: string | undefined, capabilities: } } -export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: ModelV2.Capabilities) => +export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: Model.Capabilities) => messages.map((message) => Message.make({ ...message, @@ -81,7 +81,7 @@ export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: ...part, result: { ...part.result, - value: part.result.value.map((item: ToolContent) => { + value: part.result.value.map((item: Content) => { if (item.type !== "file") return item return unsupportedMedia(item.mime, item.name, capabilities) ?? item }), @@ -102,7 +102,7 @@ export interface Interface { } /** Location-scoped outbound model-request preparation. */ -export class Service extends Context.Service()("@opencode/v2/SessionModelRequest") {} +export class Service extends Context.Service()("@opencode/SessionModelRequest") {} export const layer = Layer.effect( Service, @@ -119,14 +119,14 @@ export const layer = Layer.effect( const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps // The final Step keeps definitions available to protocols with native "none", // preserving their prompt cache prefix. Calls are still rejected at execution. - const toolSet = input.context.toolSet + const tools = input.context.tools const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial] .filter((part) => part.length > 0) .map(SystemPart.make) const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey) const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history - const toolDefinitions = toolSet.definitions + const toolDefinitions = tools.definitions const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool])) // Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit. const contextEvent = yield* hooks.trigger("session", "context", { @@ -142,7 +142,7 @@ export const layer = Layer.effect( const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => { const registered = toolsByName.get(name) return registered - ? [Object.assign({}, registered, { description: tool.description, inputSchema: tool.input })] + ? [{ ...registered, description: tool.description, inputSchema: tool.input }] : [] }) const request = LLM.request({ @@ -158,16 +158,10 @@ export const layer = Layer.effect( }) const executeTool: Prepared["executeTool"] = (executeInput) => { if (stepLimitReached) - return Effect.succeed({ - status: "error", - error: { type: "tool.execution", message: "Tools are disabled after the maximum agent steps" }, - }) + return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" }) if (toolsByName.has(executeInput.call.name) && !Object.hasOwn(contextEvent.tools, executeInput.call.name)) - return Effect.succeed({ - status: "error", - error: { type: "tool.unknown", message: `Tool is not available for this request: ${executeInput.call.name}` }, - }) - return toolSet + return new Tool.Error({ message: `Tool is not available for this request: ${executeInput.call.name}` }) + return tools .execute(executeInput) .pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline))) } diff --git a/packages/core/src/session/pending.ts b/packages/core/src/session/pending.ts index 6753903be9b..47d74d0abff 100644 --- a/packages/core/src/session/pending.ts +++ b/packages/core/src/session/pending.ts @@ -14,7 +14,7 @@ import { } from "@opencode-ai/schema/session-pending" import { Event } from "@opencode-ai/schema/event" import type { Database } from "../database/database" -import type { EventV2 } from "../event" +import { Bus } from "../bus" import { EventTable } from "../event/sql" import { KeyedMutex } from "../effect/keyed-mutex" import { SessionEvent } from "./event" @@ -38,7 +38,7 @@ const encodeUser = Schema.encodeSync(UserData) const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData) const encodeSynthetic = Schema.encodeSync(SyntheticData) const decodeAdmittedEvent = Schema.decodeUnknownOption(SessionEvent.InputAdmitted.data) -const admittedEventType = Event.versionedType( +const admittedEventType = Bus.versionedType( SessionEvent.InputAdmitted.type, SessionEvent.InputAdmitted.durable.version, ) @@ -150,7 +150,7 @@ const promotedFromHistory = Effect.fn("SessionPending.promotedFromHistory")(func export const admit = Effect.fn("SessionPending.admit")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, request: { readonly id: SessionMessage.ID readonly sessionID: SessionSchema.ID @@ -164,7 +164,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* ( } const promoted = yield* promotedFromHistory(db, request.sessionID, request.id) if (promoted !== undefined) return promoted - return yield* events + return yield* bus .publish(SessionEvent.InputAdmitted, { inputID: request.id, sessionID: request.sessionID, @@ -198,7 +198,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* ( export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }, ) { return yield* inboxLocks.withLock(input.sessionID)( @@ -210,7 +210,7 @@ export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(funct } const pending = yield* compaction(db, input.sessionID) if (pending) return pending - return yield* events + return yield* bus .publish(SessionEvent.Compaction.Admitted, { inputID: input.id, sessionID: input.sessionID, @@ -413,7 +413,7 @@ export const equivalent = ( const publish = Effect.fn("SessionPending.publish")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, sessionID: SessionSchema.ID, rows: ReadonlyArray, ) { @@ -423,7 +423,7 @@ const publish = Effect.fn("SessionPending.publish")(function* ( (row) => { const entry = fromRow(row) if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id })) - return events + return bus .publish(SessionEvent.InputPromoted, { sessionID, inputID: entry.id, @@ -450,7 +450,7 @@ const publish = Effect.fn("SessionPending.publish")(function* ( */ export const promote = Effect.fn("SessionPending.promote")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, sessionID: SessionSchema.ID, scope: Promotable, ) { @@ -464,7 +464,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* ( .orderBy(asc(SessionPendingTable.admitted_seq)) .all() .pipe(Effect.orDie) - if (steers.length > 0 || scope === "steer") return yield* publish(db, events, sessionID, steers) + if (steers.length > 0 || scope === "steer") return yield* publish(db, bus, sessionID, steers) const queued = yield* db .select() @@ -475,7 +475,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* ( .get() .pipe(Effect.orDie) if (!queued) return 0 - const promoted = yield* publish(db, events, sessionID, [queued]) + const promoted = yield* publish(db, bus, sessionID, [queued]) const arrivedSteers = yield* db .select() .from(SessionPendingTable) @@ -483,7 +483,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* ( .orderBy(asc(SessionPendingTable.admitted_seq)) .all() .pipe(Effect.orDie) - return promoted + (yield* publish(db, events, sessionID, arrivedSteers)) + return promoted + (yield* publish(db, bus, sessionID, arrivedSteers)) }), ) }) diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index f759a3757f2..5c888137621 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -3,16 +3,16 @@ export * as SessionProjector from "./projector" import { and, asc, desc, eq, gt, gte, inArray, lt, sql } from "drizzle-orm" import { DateTime, Effect, Layer, Schema, Stream } from "effect" import { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" -import { ModelV2 } from "../model" +import { Model } from "../model" import { SessionEvent } from "./event" import { SessionV1 } from "../v1/session" import { WorkspaceTable } from "../control-plane/workspace.sql" import { SessionMessage } from "./message" import { SessionMessageUpdater } from "./message-updater" import { SessionPending } from "./pending" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { InstructionState } from "./instruction-state" import { MessageTable, PartTable, SessionPendingTable, SessionMessageTable, SessionTable } from "./sql" import type { DeepMutable } from "../schema" @@ -134,7 +134,7 @@ function applyUsage( const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* ( db: DatabaseService, - events: EventV2.Interface, + bus: Bus.Interface, sessionID: (typeof SessionEvent.Step.Ended.Type)["data"]["sessionID"], ) { const row = yield* db @@ -151,7 +151,7 @@ const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* .get() .pipe(Effect.orDie) if (!row) return - yield* events.publish(SessionEvent.UsageUpdated, { + yield* bus.publish(SessionEvent.UsageUpdated, { sessionID, cost: Money.USD.make(row.cost), tokens: { @@ -314,7 +314,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( cursor = rows.at(-1)!.seq } - yield* EventV2.reserveSequence(db, event.data.sessionID, event.data.parentSeq) + yield* Bus.reserveSequence(db, event.data.sessionID, event.data.parentSeq) yield* InstructionState.rebuild(db, event.data.sessionID) }) @@ -349,7 +349,7 @@ function run(db: DatabaseService, event: MessageEvent) { .get() .pipe( Effect.orDie, - Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(ModelV2.Ref)(row.model) : undefined)), + Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(Model.Ref)(row.model) : undefined)), ) }, getCurrentAssistant() { @@ -460,9 +460,9 @@ function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, me const layer = Layer.effectDiscard( Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const db = (yield* Database.Service).db - yield* events.project(SessionV1.Event.Created, (event) => + yield* bus.project(SessionV1.Event.Created, (event) => Effect.gen(function* () { const stored = yield* db .insert(SessionTable) @@ -482,7 +482,7 @@ const layer = Layer.effectDiscard( } }), ) - yield* events.project(SessionV1.Event.Updated, (event) => + yield* bus.project(SessionV1.Event.Updated, (event) => db .update(SessionTable) .set(sessionRow(event.data.info)) @@ -490,7 +490,7 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie), ) - yield* events.project(SessionEvent.Moved, (event) => + yield* bus.project(SessionEvent.Moved, (event) => Effect.gen(function* () { yield* db .update(SessionTable) @@ -498,7 +498,7 @@ const layer = Layer.effectDiscard( directory: event.data.location.directory, path: event.data.subpath, ...(event.data.projectID ? { project_id: event.data.projectID } : {}), - workspace_id: event.data.location.workspaceID ? WorkspaceV2.ID.make(event.data.location.workspaceID) : null, + workspace_id: event.data.location.workspaceID ? Workspace.ID.make(event.data.location.workspaceID) : null, time_updated: DateTime.toEpochMillis(event.created), }) .where(eq(SessionTable.id, event.data.sessionID)) @@ -507,13 +507,13 @@ const layer = Layer.effectDiscard( yield* InstructionState.reset(db, event.data.sessionID) }), ) - yield* events.project(SessionV1.Event.Deleted, (event) => + yield* bus.project(SessionV1.Event.Deleted, (event) => db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie), ) - yield* events.project(SessionEvent.Deleted, (event) => + yield* bus.project(SessionEvent.Deleted, (event) => db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie), ) - yield* events.project(SessionV1.Event.MessageUpdated, (event) => + yield* bus.project(SessionV1.Event.MessageUpdated, (event) => Effect.gen(function* () { const time_created = event.data.info.time.created const id = event.data.info.id @@ -527,7 +527,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionV1.Event.MessageRemoved, (event) => + yield* bus.project(SessionV1.Event.MessageRemoved, (event) => Effect.gen(function* () { const rows = yield* db .select() @@ -546,7 +546,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionV1.Event.PartRemoved, (event) => + yield* bus.project(SessionV1.Event.PartRemoved, (event) => Effect.gen(function* () { const row = yield* db .select() @@ -563,7 +563,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionV1.Event.PartUpdated, (event) => + yield* bus.project(SessionV1.Event.PartUpdated, (event) => Effect.gen(function* () { const id = event.data.part.id const messageID = event.data.part.messageID @@ -582,7 +582,7 @@ const layer = Layer.effectDiscard( if (next) yield* applyUsage(db, sessionID, next) }), ) - yield* events.project(SessionEvent.AgentSelected, (event) => + yield* bus.project(SessionEvent.AgentSelected, (event) => db .update(SessionTable) .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) }) @@ -590,7 +590,7 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie, Effect.andThen(run(db, event))), ) - yield* events.project(SessionEvent.ModelSelected, (event) => + yield* bus.project(SessionEvent.ModelSelected, (event) => Effect.gen(function* () { yield* run(db, event) yield* db @@ -601,7 +601,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionEvent.Renamed, (event) => + yield* bus.project(SessionEvent.Renamed, (event) => db .update(SessionTable) .set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.created) }) @@ -609,9 +609,9 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie), ) - yield* events.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data)) - yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event)) - yield* events.project(SessionEvent.InputPromoted, (event) => + yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data)) + yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event)) + yield* bus.project(SessionEvent.InputPromoted, (event) => Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) @@ -643,7 +643,7 @@ const layer = Layer.effectDiscard( ) }), ) - yield* events.project(SessionEvent.InputAdmitted, (event) => + yield* bus.project(SessionEvent.InputAdmitted, (event) => Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) @@ -656,7 +656,7 @@ const layer = Layer.effectDiscard( }) }), ) - yield* events.project(SessionEvent.Compaction.Admitted, (event) => + yield* bus.project(SessionEvent.Compaction.Admitted, (event) => Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) @@ -668,42 +668,42 @@ const layer = Layer.effectDiscard( }) }), ) - yield* events.project(SessionEvent.Execution.Succeeded, (event) => run(db, event)) - yield* events.project(SessionEvent.Execution.Failed, (event) => run(db, event)) - yield* events.project(SessionEvent.Execution.Interrupted, (event) => run(db, event)) - yield* events.project(SessionEvent.InstructionsUpdated, (event) => + yield* bus.project(SessionEvent.Execution.Succeeded, (event) => run(db, event)) + yield* bus.project(SessionEvent.Execution.Failed, (event) => run(db, event)) + yield* bus.project(SessionEvent.Execution.Interrupted, (event) => run(db, event)) + yield* bus.project(SessionEvent.InstructionsUpdated, (event) => InstructionState.apply(db, event.data.sessionID, event.durable.seq, event.data.delta), ) - yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) - yield* events.project(SessionEvent.Skill.Activated, (event) => run(db, event)) - yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Step.Ended, (event) => + yield* bus.project(SessionEvent.Synthetic, (event) => run(db, event)) + yield* bus.project(SessionEvent.Skill.Activated, (event) => run(db, event)) + yield* bus.project(SessionEvent.Shell.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Shell.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.Step.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Step.Ended, (event) => Effect.gen(function* () { yield* run(db, event) yield* applyUsage(db, event.data.sessionID, event.data) }), ) - yield* events.project(SessionEvent.Step.Failed, (event) => + yield* bus.project(SessionEvent.Step.Failed, (event) => Effect.gen(function* () { yield* run(db, event) if (event.data.cost !== undefined && event.data.tokens !== undefined) yield* applyUsage(db, event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens }) }), ) - yield* events.project(SessionEvent.Text.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event)) - yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event)) - yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) - yield* events.project(SessionEvent.RetryScheduled, (event) => run(db, event)) - yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event)) - yield* events.project(SessionEvent.Compaction.Ended, (event) => + yield* bus.project(SessionEvent.Text.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Text.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Input.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Called, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Success, (event) => run(db, event)) + yield* bus.project(SessionEvent.Tool.Failed, (event) => run(db, event)) + yield* bus.project(SessionEvent.Reasoning.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Reasoning.Ended, (event) => run(db, event)) + yield* bus.project(SessionEvent.RetryScheduled, (event) => run(db, event)) + yield* bus.project(SessionEvent.Compaction.Started, (event) => run(db, event)) + yield* bus.project(SessionEvent.Compaction.Ended, (event) => Effect.gen(function* () { yield* run(db, event) yield* InstructionState.advanceEpoch(db, event.data.sessionID, event.durable.seq) @@ -713,7 +713,7 @@ const layer = Layer.effectDiscard( yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID }) }), ) - yield* events.project(SessionEvent.Compaction.Failed, (event) => + yield* bus.project(SessionEvent.Compaction.Failed, (event) => Effect.gen(function* () { yield* run(db, event) if (event.durable === undefined) @@ -722,7 +722,7 @@ const layer = Layer.effectDiscard( yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID }) }), ) - yield* events.project(SessionEvent.RevertEvent.Staged, (event) => + yield* bus.project(SessionEvent.RevertEvent.Staged, (event) => Effect.gen(function* () { const revert = event.data.revert yield* db @@ -736,7 +736,7 @@ const layer = Layer.effectDiscard( .pipe(Effect.orDie) }), ) - yield* events.project(SessionEvent.RevertEvent.Cleared, (event) => + yield* bus.project(SessionEvent.RevertEvent.Cleared, (event) => db .update(SessionTable) .set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) }) @@ -744,7 +744,7 @@ const layer = Layer.effectDiscard( .run() .pipe(Effect.orDie, Effect.asVoid), ) - yield* events.project(SessionEvent.RevertEvent.Committed, (event) => + yield* bus.project(SessionEvent.RevertEvent.Committed, (event) => Effect.gen(function* () { const boundary = yield* db .select({ seq: SessionMessageTable.seq }) @@ -781,18 +781,18 @@ const layer = Layer.effectDiscard( yield* InstructionState.reset(db, event.data.sessionID) }), ) - yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe( + yield* bus.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe( Stream.runForEach((event) => { if ( event.type === SessionEvent.Step.Failed.type && (event.data.cost === undefined || event.data.tokens === undefined) ) return Effect.void - return publishSessionUsage(db, events, event.data.sessionID) + return publishSessionUsage(db, bus, event.data.sessionID) }), Effect.forkScoped({ startImmediately: true }), ) }), ) -export const node = makeGlobalNode({ name: "session-projector", layer, deps: [EventV2.node, Database.node] }) +export const node = makeGlobalNode({ name: "session-projector", layer, deps: [Bus.node, Database.node] }) diff --git a/packages/core/src/session/revert.ts b/packages/core/src/session/revert.ts index 42bc34e6943..429170e9870 100644 --- a/packages/core/src/session/revert.ts +++ b/packages/core/src/session/revert.ts @@ -3,7 +3,7 @@ export * as SessionRevert from "./revert" import { and, asc, eq, gt } from "drizzle-orm" import { Effect, Schema } from "effect" import { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { RelativePath } from "../schema" import { Snapshot } from "../snapshot" import { SessionEvent } from "./event" @@ -63,7 +63,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: { readonly files?: boolean }) { const snapshot = yield* Snapshot.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const original = input.session.revert?.snapshot ? Snapshot.ID.make(input.session.revert.snapshot) : yield* snapshot.capture() @@ -83,7 +83,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: { snapshot: original, files, } satisfies SessionSchema.Info["revert"] - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID: input.session.id, revert, }) @@ -98,16 +98,16 @@ export const clear = Effect.fn("SessionRevert.clear")(function* (session: Sessio yield* snapshot.restore({ files: new Map((session.revert.files ?? []).map((file) => [RelativePath.make(file.file), original])), }) - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.RevertEvent.Cleared, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.RevertEvent.Cleared, { sessionID: session.id, }) }) export const commit = Effect.fn("SessionRevert.commit")(function* (session: SessionSchema.Info) { if (!session.revert) return - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.RevertEvent.Committed, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.RevertEvent.Committed, { sessionID: session.id, to: session.revert.messageID, }) diff --git a/packages/core/src/session/runner/index.ts b/packages/core/src/session/runner/index.ts index 65fdb223524..db45c0cd31f 100644 --- a/packages/core/src/session/runner/index.ts +++ b/packages/core/src/session/runner/index.ts @@ -6,7 +6,6 @@ import { SessionSchema } from "../schema" import type { AgentNotFoundError, MessageDecodeError, StepFailedError, UserInterruptedError } from "../error" import { SessionRunnerModel } from "./model" import type { Instructions } from "../../instructions/index" -import type { ToolOutputStore } from "../../tool-output-store" export type RunError = | LLMError @@ -16,7 +15,6 @@ export type RunError = | StepFailedError | UserInterruptedError | Instructions.InitializationBlocked - | ToolOutputStore.Error /** Runs one local continuation from already-recorded Session history. */ export interface Interface { @@ -27,4 +25,4 @@ export interface Interface { }) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionRunner") {} +export class Service extends Context.Service()("@opencode/SessionRunner") {} diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index da722b449f8..42f871e8f70 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -3,10 +3,9 @@ export * as SessionRunnerLLM from "./llm" import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent, type ToolCall } from "@opencode-ai/ai" import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Stream } from "effect" import { Database } from "../../database/database" -import { EventV2 } from "../../event" -import { PermissionV2 } from "../../permission" -import { QuestionTool } from "../../tool/question" -import { ToolOutputStore } from "../../tool-output-store" +import { Bus } from "../../bus" +import { Permission } from "../../permission" +import { QuestionTool } from "../../tool/plugin/question" import { InstructionState } from "../instruction-state" import { SessionCompaction } from "../compaction" import { SessionContext } from "../context" @@ -38,8 +37,8 @@ const CallOutcome = Data.taggedEnum() // Declining an interactive prompt halts the drain instead of becoming model-facing tool output. const isDecline = ( error: SessionModelRequest.ExecuteError, -): error is PermissionV2.DeclinedError | QuestionTool.CancelledError => - error._tag === "PermissionV2.DeclinedError" || error._tag === "QuestionTool.CancelledError" +): error is Permission.DeclinedError | QuestionTool.CancelledError => + error._tag === "Permission.DeclinedError" || error._tag === "QuestionTool.CancelledError" /** * Classifies how the owned tool fibers ended. Interrupts abort the step; a user decline @@ -66,8 +65,8 @@ const classifyToolExits = ( // drain's error channel never carries a decline. const failure = causes.flatMap((cause) => { if (Cause.hasInterrupts(cause)) return [] - const reasons = cause.reasons.flatMap((reason): Array> => - Cause.isFailReason(reason) ? (isDecline(reason.error) ? [] : [Cause.makeFailReason(reason.error)]) : [reason], + const reasons = cause.reasons.flatMap((reason): Array> => + Cause.isFailReason(reason) ? [] : [reason], ) return reasons.length > 0 ? [Cause.fromReasons(reasons)] : [] }).at(0) @@ -75,7 +74,6 @@ const classifyToolExits = ( interrupted: causes.some(Cause.hasInterrupts), declines, failure, - infraError: failure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(failure)), } } @@ -86,7 +84,7 @@ const RESULT_MISSING = { type: "tool.result-missing", message: "Provider did not const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const llm = yield* LLMClient.Service const store = yield* SessionStore.Service const context = yield* SessionContext.Service @@ -146,7 +144,7 @@ const layer = Layer.effect( // compaction boundary, so the rebuilt step needs identity inside the new epoch. let assistantMessageID = SessionMessage.ID.create() const retry = yield* Schedule.toStepWithSleep( - SessionRunnerRetry.schedule(events, sessionID, () => assistantMessageID), + SessionRunnerRetry.schedule(bus, sessionID, () => assistantMessageID), ) /** * Consumes one retry allowance: sleeps the scheduled backoff, or publishes @@ -157,7 +155,7 @@ const layer = Layer.effect( retry(failure).pipe( Effect.as(CallOutcome.Retry({ step: failure.step })), Pull.catchDone(() => - events + bus .publish(SessionEvent.Step.Failed, { sessionID, assistantMessageID, @@ -203,8 +201,8 @@ const layer = Layer.effect( const selected = yield* context.select(sessionID) // Establish what the model knows before admitting what the user said, so // a blocked first step leaves pending inputs untouched. - yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id) - const promoted = promotable ? yield* SessionPending.promote(db, events, selected.session.id, promotable) : 0 + yield* InstructionState.prepare(db, bus, selected.instructions, selected.session.id) + const promoted = promotable ? yield* SessionPending.promote(db, bus, selected.session.id, promotable) : 0 // Promoted input opens a fresh step allowance. const currentStep = promoted > 0 ? 1 : step const loaded = yield* context.load(selected) @@ -231,7 +229,7 @@ const layer = Layer.effect( }> = [] const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber))) const startSnapshot = yield* snapshots.capture() - const publisher = createLLMEventPublisher(events, { + const publisher = createLLMEventPublisher(bus, { sessionID: session.id, agent: agent.id, // The selected catalog identity, not model.id: route-level ids are provider API @@ -260,7 +258,7 @@ const layer = Layer.effect( const publishStepEnd = (finish: NonNullable) => Effect.gen(function* () { const end = yield* captureStepEnd() - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID: session.id, assistantMessageID: yield* publisher.startAssistant(), finish: finish.finish, @@ -310,6 +308,9 @@ const layer = Layer.effect( // The fiber owns its call: it publishes its own completion, masked so a // finished execution always reaches its durable settlement. Effect.flatMap((outcome) => publisher.toolExecution(event.id, event.name, outcome)), + Effect.catchTag("Tool.Error", (error) => + publisher.failTool(event.id, toSessionError(error)).pipe(Effect.asVoid), + ), ), ).pipe(Effect.forkScoped), }) @@ -386,9 +387,8 @@ const layer = Layer.effect( yield* publisher.failAssistant(STEP_INTERRUPTED) } if (tools.failure !== undefined) { - const error = toSessionError(tools.infraError ?? Cause.squash(tools.failure)) + const error = toSessionError(Cause.squash(tools.failure)) yield* publisher.failUnsettledTools(error) - if (tools.infraError !== undefined) yield* publisher.failAssistant(error) } // Local calls have joined, so the remaining sweeps only close hosted calls the // provider promised but never resolved. @@ -413,8 +413,7 @@ const layer = Layer.effect( if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) if (tools.declines.length > 0) return yield* Effect.interrupt - if ((tools.interrupted || tools.infraError !== undefined) && tools.failure) - return yield* Effect.failCause(tools.failure) + if (tools.interrupted && tools.failure) return yield* Effect.failCause(tools.failure) if (tools.interrupted && joined._tag === "Failure") return yield* Effect.failCause(joined.cause) if (record.failure) return yield* new StepFailedError({ error: record.failure }) return CallOutcome.Completed({ @@ -450,7 +449,7 @@ const layer = Layer.effect( if (Exit.isSuccess(compacted)) return const unsettled = yield* SessionPending.compaction(db, sessionID) if (unsettled) - yield* events.publish(SessionEvent.Compaction.Failed, { + yield* bus.publish(SessionEvent.Compaction.Failed, { sessionID, reason: "manual", error: Cause.hasInterruptsOnly(compacted.cause) @@ -471,7 +470,7 @@ const layer = Layer.effect( if (message.type !== "assistant") continue for (const tool of message.content) { if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue - yield* events.publish(SessionEvent.Tool.Failed, { + yield* bus.publish(SessionEvent.Tool.Failed, { sessionID, assistantMessageID: message.id, callID: tool.id, @@ -503,7 +502,7 @@ export const node = makeLocationNode({ service: Service, layer, deps: [ - EventV2.node, + Bus.node, llmClient, SessionContext.node, SessionModelRequest.node, diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index d0902d7d674..3b842b72eb8 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -5,8 +5,8 @@ import { Model } from "@opencode-ai/ai" import { Context, Effect, Layer, Schema } from "effect" import { Catalog } from "../../catalog" import { ModelResolver } from "../../model-resolver" -import { ModelV2 } from "../../model" -import { ProviderV2 } from "../../provider" +import { Capabilities, ID, Info, Ref, VariantID } from "../../model" +import { Provider } from "../../provider" import { SessionSchema } from "../schema" export class ModelNotSelectedError extends Schema.TaggedErrorClass()( @@ -20,7 +20,7 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass()( "SessionRunnerModel.ModelUnavailableError", - { providerID: ProviderV2.ID, modelID: ModelV2.ID }, + { providerID: Provider.ID, modelID: ID }, ) { override get message() { return `Model unavailable: ${this.providerID}/${this.modelID}` @@ -38,21 +38,21 @@ export interface Interface { readonly resolve: (session: SessionSchema.Info) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionRunnerModel") {} +export class Service extends Context.Service()("@opencode/SessionRunnerModel") {} /** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */ export const resolved = ( model: Model, options: { - readonly capabilities: ModelV2.Capabilities - readonly variant?: ModelV2.VariantID - readonly cost: ModelV2.Info["cost"] + readonly capabilities: Capabilities + readonly variant?: VariantID + readonly cost: Info["cost"] }, ): Resolved => ({ model, - ref: ModelV2.Ref.make({ - id: ModelV2.ID.make(model.id), - providerID: ProviderV2.ID.make(model.provider), + ref: Ref.make({ + id: ID.make(model.id), + providerID: Provider.ID.make(model.provider), ...(options.variant === undefined ? {} : { variant: options.variant }), }), capabilities: options.capabilities, diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index 440f6779b86..99dc0ad77b5 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -1,23 +1,22 @@ import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai" import { Effect } from "effect" -import { EventV2 } from "../../event" -import { ModelV2 } from "../../model" +import { Bus } from "../../bus" +import { Model } from "../../model" import { SessionEvent } from "../event" import { SessionMessage } from "../message" import { SessionSchema } from "../schema" import { SessionError } from "@opencode-ai/schema/session-error" import { Money } from "@opencode-ai/schema/money" -import { AgentV2 } from "../../agent" +import { Agent } from "../../agent" import { Snapshot } from "../../snapshot" import { RelativePath } from "../../schema" import { SessionUsage } from "../usage" -import { Tool } from "../../tool/tool" -import type { ToolRegistry } from "../../tool/registry" +import { Tool } from "@opencode-ai/schema/tool" type Input = { readonly sessionID: SessionSchema.ID - readonly agent: AgentV2.ID - readonly model: ModelV2.Ref + readonly agent: Agent.ID + readonly model: Model.Ref readonly providerMetadataKey: string readonly snapshot?: Snapshot.ID readonly assistantMessageID: SessionMessage.ID @@ -48,12 +47,26 @@ export interface StepRecord { } /** Derives canonical model content from a provider-hosted tool result. */ -const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { +type NonEmptyContent = readonly [Tool.Content, ...Tool.Content[]] + +const nonEmpty = (content: ReadonlyArray): NonEmptyContent | undefined => + content.length > 0 ? (content as NonEmptyContent) : undefined + +const stringify = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + +const hostedContent = (result: ToolResultValue): NonEmptyContent => { if (result.type === "content") { - const content = Tool.nonEmpty(result.value) + const content = nonEmpty(result.value) if (content !== undefined) return content } - return [{ type: "text", text: Tool.stringify(result.value) }] + return [{ type: "text", text: stringify(result.value) }] } /** @@ -67,7 +80,7 @@ const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => { * order: each publishing fiber is sequential, so per-source order holds by construction, * and consumers fold by callID/ordinal rather than global position. */ -export const createLLMEventPublisher = (events: Pick, input: Input) => { +export const createLLMEventPublisher = (bus: Pick, input: Input) => { const tools = new Map< string, { @@ -76,10 +89,10 @@ export const createLLMEventPublisher = (events: Pick() - const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) => + const failureSnapshot = (tool: { readonly progress?: Tool.Metadata }) => tool.progress === undefined ? {} : { metadata: tool.progress } const assistantMessageID = input.assistantMessageID let stepStarted = false @@ -92,7 +105,7 @@ export const createLLMEventPublisher = (events: Pick Effect.gen(function* () { - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), ordinal, @@ -165,7 +178,7 @@ export const createLLMEventPublisher = (events: Pick Effect.gen(function* () { - yield* events.publish(SessionEvent.Reasoning.Ended, { + yield* bus.publish(SessionEvent.Reasoning.Ended, { sessionID: input.sessionID, assistantMessageID: yield* currentAssistantMessageID(), ordinal, @@ -179,7 +192,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (toolInput.has(event.id)) yield* endToolInput(event, event.raw) tool.settled = true - yield* events.publish(SessionEvent.Tool.Failed, { + yield* bus.publish(SessionEvent.Tool.Failed, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, @@ -263,7 +276,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`)) yield* toolInput.append(event.id, event.text) - yield* events.publish(SessionEvent.Tool.Input.Delta, { + yield* bus.publish(SessionEvent.Tool.Input.Delta, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, @@ -408,7 +421,7 @@ export const createLLMEventPublisher = (events: Pick ${event.name}`)) if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool error: ${event.id}`)) tool.settled = true - yield* events.publish(SessionEvent.Tool.Failed, { + yield* bus.publish(SessionEvent.Tool.Failed, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, @@ -495,12 +508,12 @@ export const createLLMEventPublisher = (events: Pick ${name}`)) - if (tool.settled) { - if (execution.status === "error") return - return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`)) - } + if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`)) tool.settled = true - if (execution.status === "completed") { - yield* events.publish(SessionEvent.Tool.Success, { - sessionID: input.sessionID, - assistantMessageID: tool.assistantMessageID, - callID, - content: execution.content, - ...(execution.metadata === undefined ? {} : { metadata: execution.metadata }), - executed: tool.providerExecuted, - }) - return - } - // An execution-provided snapshot wins; otherwise fall back to retained progress. - const snapshot = - execution.content !== undefined || execution.metadata !== undefined - ? { - ...(execution.content === undefined ? {} : { content: execution.content }), - ...(execution.metadata === undefined ? {} : { metadata: execution.metadata }), - } - : failureSnapshot(tool) - yield* events.publish(SessionEvent.Tool.Failed, { + const content = + typeof result.content === "string" + ? [{ type: "text" as const, text: result.content }] + : result.content === undefined + ? [] + : [...result.content] + if (content.length === 0) return yield* Effect.die(new Error(`Tool execution has no content: ${callID}`)) + yield* bus.publish(SessionEvent.Tool.Success, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID, - error: execution.error, - ...snapshot, + content: [content[0], ...content.slice(1)], + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), executed: tool.providerExecuted, }) }) diff --git a/packages/core/src/session/runner/retry.ts b/packages/core/src/session/runner/retry.ts index de94d36ec6b..3e22554d60f 100644 --- a/packages/core/src/session/runner/retry.ts +++ b/packages/core/src/session/runner/retry.ts @@ -3,7 +3,7 @@ export * as SessionRunnerRetry from "./retry" import { LLMError } from "@opencode-ai/ai" import { SessionError } from "@opencode-ai/schema/session-error" import { Data, Duration, Effect, Schedule } from "effect" -import { EventV2 } from "../../event" +import { Bus } from "../../bus" import { SessionEvent } from "../event" import { SessionMessage } from "../message" import { SessionSchema } from "../schema" @@ -41,7 +41,7 @@ const retryAfter = (failure: RetryableFailure) => { return undefined } -export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) => +export const schedule = (bus: Bus.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) => Schedule.max([Schedule.exponential("2 seconds"), Schedule.recurs(4)]).pipe( Schedule.setInputType(), Schedule.modifyDelay(({ input: failure, duration: delay }) => { @@ -49,7 +49,7 @@ export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID, return Effect.succeed(minimum === undefined ? delay : Duration.max(delay, Duration.millis(minimum))) }), Schedule.tap((metadata) => - events.publish(SessionEvent.RetryScheduled, { + bus.publish(SessionEvent.RetryScheduled, { sessionID, assistantMessageID: assistantMessageID(), attempt: metadata.attempt + 1, diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index d708563eff6..a58ced07be6 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -1,6 +1,6 @@ import { Message, ToolCallPart, ToolResultPart, type ContentPart, type ProviderMetadata } from "@opencode-ai/ai" import { Option, Schema } from "effect" -import type { ModelV2 } from "../../model" +import type { Model } from "../../model" import { SessionMessage } from "../message" import type { FileAttachment } from "@opencode-ai/schema/prompt" @@ -108,7 +108,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid } } -const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, providerMetadataKey: string) => { +const assistant = (message: SessionMessage.Assistant, model: Model.Ref, providerMetadataKey: string) => { const sameProvider = String(message.model.providerID) === String(model.providerID) const sameModel = sameProvider && String(message.model.id) === String(model.id) const reuseProviderMetadata = sameModel && message.error === undefined @@ -177,7 +177,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid ] } -function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref, providerMetadataKey: string): Message[] { +function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMetadataKey: string): Message[] { switch (message.type) { case "agent-switched": case "model-switched": @@ -239,9 +239,9 @@ ${message.recent} } } -/** Translate projected V2 Session history into canonical @opencode-ai/ai context. */ +/** Translate projected Session history into canonical @opencode-ai/ai context. */ export const toLLMMessages = ( messages: readonly SessionMessage.Info[], - model: ModelV2.Ref, + model: Model.Ref, providerMetadataKey: string = model.providerID, ) => messages.flatMap((message) => toLLMMessage(message, model, providerMetadataKey)) diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 21d0ab5f267..17654df1b38 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -6,10 +6,10 @@ import type { SessionMessage } from "./message" import type { SessionPending } from "./pending" import type { FileDiff } from "@opencode-ai/schema/file-diff" import { PermissionV1 } from "../v1/permission" -import { ProjectV2 } from "../project" +import { Project } from "../project" import type { SessionSchema } from "./schema" import type { MessageID, PartID, SessionV1 } from "../v1/session" -import { WorkspaceV2 } from "../workspace" +import { Workspace } from "../workspace" import { Timestamps } from "../database/schema.sql" import type { Instruction } from "@opencode-ai/schema/instruction" import type { Session } from "@opencode-ai/schema/session" @@ -26,10 +26,10 @@ export const SessionTable = sqliteTable( { id: text().$type().primaryKey(), project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), - workspace_id: text().$type(), + workspace_id: text().$type(), parent_id: text().$type(), fork_session_id: text().$type(), fork_message_id: text().$type(), diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts index 0018ace4a64..8a6058ca2ab 100644 --- a/packages/core/src/session/store.ts +++ b/packages/core/src/session/store.ts @@ -23,7 +23,7 @@ export interface Interface { readonly suspend: (sessionIDs: Iterable) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionStore") {} +export class Service extends Context.Service()("@opencode/SessionStore") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index d9cba66a8a8..ed795477b4c 100644 --- a/packages/core/src/session/title.ts +++ b/packages/core/src/session/title.ts @@ -2,9 +2,9 @@ export * as SessionTitle from "./title" import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai" import { Context, Effect, Layer, Stream } from "effect" -import { AgentV2 } from "../agent" +import { Agent } from "../agent" import { Database } from "../database/database" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { App } from "../app" import { llmClient } from "../effect/app-node-platform" @@ -19,11 +19,11 @@ const MAX_LENGTH = 100 type Dependencies = { readonly app: App.Info - readonly events: EventV2.Interface + readonly bus: Bus.Interface readonly llm: { readonly stream: (request: LLMRequest) => Stream.Stream } - readonly agents: AgentV2.Interface + readonly agents: Agent.Interface readonly models: SessionRunnerModel.Interface } @@ -32,7 +32,7 @@ export interface Interface { readonly generateForFirstPrompt: (session: SessionSchema.Info) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SessionTitle") {} +export class Service extends Context.Service()("@opencode/SessionTitle") {} const truncate = (value: string) => (value.length <= MAX_LENGTH ? value : `${value.slice(0, MAX_LENGTH - 3)}...`) @@ -44,7 +44,7 @@ const make = (dependencies: Dependencies) => { if (session.parentID) return const firstUser = yield* SessionHistory.firstUserMessageIfOnly(db, session.id) if (!firstUser) return - const agent = yield* dependencies.agents.get(AgentV2.ID.make("title")) + const agent = yield* dependencies.agents.get(Agent.ID.make("title")) if (!agent) return const resolved = yield* ( agent.model @@ -57,7 +57,7 @@ const make = (dependencies: Dependencies) => { let usage: SessionUsage.Recorded | undefined const recordUsage = Effect.suspend(() => usage - ? dependencies.events.publish(SessionEvent.UsageRecorded, { + ? dependencies.bus.publish(SessionEvent.UsageRecorded, { sessionID: session.id, source: "title", ...usage, @@ -96,7 +96,7 @@ const make = (dependencies: Dependencies) => { .map((line) => line.trim()) .find((line) => line.length > 0) if (!title) return - yield* dependencies.events.publish(SessionEvent.Renamed, { + yield* dependencies.bus.publish(SessionEvent.Renamed, { sessionID: session.id, title: truncate(title), }) @@ -107,13 +107,13 @@ const make = (dependencies: Dependencies) => { export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const llm = yield* LLMClient.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const models = yield* SessionRunnerModel.Service const database = yield* Database.Service const app = yield* App.Metadata - const title = make({ events, llm, agents, models, app }) + const title = make({ bus, llm, agents, models, app }) return Service.of({ generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session), }) @@ -123,5 +123,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node, App.node], + deps: [Bus.node, llmClient, Agent.node, SessionRunnerModel.node, Database.node, App.node], }) diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index fe21051d8ab..d7ccb3dc7e9 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -1,10 +1,9 @@ import { LLMError, ToolFailure } from "@opencode-ai/ai" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +import { Tool } from "@opencode-ai/schema/tool" import { SessionError } from "@opencode-ai/schema/session-error" -import { PermissionV2 } from "../permission" -import { QuestionV2 } from "../question" +import { Permission } from "../permission" +import { Question } from "../question" import { Integration } from "../integration" -import { ToolOutputStore } from "../tool-output-store" import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "./error" import { SessionRunnerModel } from "./runner/model" @@ -37,9 +36,9 @@ export function toSessionError(cause: unknown): SessionError.Error { } } } - if (cause instanceof PermissionV2.BlockedError) return { type: "permission.rejected", message: cause.message } - if (cause instanceof QuestionV2.RejectedError) return { type: "aborted", message: cause.message } - if (cause instanceof ToolFailure || cause instanceof Tool.Failure) { + if (cause instanceof Permission.BlockedError) return { type: "permission.rejected", message: cause.message } + if (cause instanceof Question.RejectedError) return { type: "aborted", message: cause.message } + if (cause instanceof ToolFailure || cause instanceof Tool.Error) { if (cause.error === undefined) return { type: "tool.execution", message: cause.message } // The canonical error is the sole model-visible representation, so a cause // with no message must not erase the tool's curated failure message. @@ -57,6 +56,5 @@ export function toSessionError(cause: unknown): SessionError.Error { ) return { type: "provider.no-route", message: cause.message } if (cause instanceof Integration.AuthorizationError) return { type: "provider.auth", message: cause.message } - if (cause instanceof ToolOutputStore.StorageError) return { type: "unknown", message: cause.message } return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) } } diff --git a/packages/core/src/session/usage.ts b/packages/core/src/session/usage.ts index 33db0926e11..dd38e948aae 100644 --- a/packages/core/src/session/usage.ts +++ b/packages/core/src/session/usage.ts @@ -3,7 +3,7 @@ export * as SessionUsage from "./usage" import type { Usage } from "@opencode-ai/ai" import { Money } from "@opencode-ai/schema/money" import type { TokenUsage } from "@opencode-ai/schema/token-usage" -import type { ModelV2 } from "../model" +import type { Model } from "../model" const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0) @@ -18,7 +18,7 @@ export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({ }) // TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract. -export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Info) { +export function calculateCost(costs: Model.Info["cost"], usage: TokenUsage.Info) { const context = usage.input + usage.cache.read + usage.cache.write const tier = costs .filter((cost) => cost.tier?.type === "context" && context > cost.tier.size) @@ -36,7 +36,7 @@ export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Inf export type Recorded = { readonly tokens: TokenUsage.Info; readonly cost: Money.USD } -export const record = (usage: Usage | undefined, costs: ModelV2.Info["cost"]): Recorded => { +export const record = (usage: Usage | undefined, costs: Model.Info["cost"]): Recorded => { const normalized = tokens(usage) return { tokens: normalized, cost: calculateCost(costs, normalized) } } diff --git a/packages/core/src/shell.ts b/packages/core/src/shell.ts index 4a2be872bf5..1c851a15406 100644 --- a/packages/core/src/shell.ts +++ b/packages/core/src/shell.ts @@ -8,7 +8,7 @@ import { Shell } from "@opencode-ai/schema/shell" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { AppProcess } from "@opencode-ai/util/process" import { Config } from "./config" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { Location } from "./location" import { Global } from "@opencode-ai/util/global" import { ShellSelect } from "./shell/select" @@ -57,12 +57,12 @@ export interface Interface { readonly remove: (id: Shell.ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Shell") {} +export class Service extends Context.Service()("@opencode/Shell") {} export const layer = (options?: ShellSelect.Options) => Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const location = yield* Location.Service const config = yield* Config.Service const global = yield* Global.Service @@ -105,7 +105,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( // Unblock any wait still pending when the command is removed before it terminated. yield* Deferred.fail(session.done, new NotFoundError({ id })) yield* Effect.promise(() => unlink(session.file).catch(() => {})) - yield* events.publish(Shell.Event.Deleted, { id }) + yield* bus.publish(Shell.Event.Deleted, { id }) }) const remove = Effect.fn("Shell.remove")(function* (id: Shell.ID) { @@ -259,7 +259,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( // the timeout-fiber interrupt below, which on the timeout path would otherwise cancel // this very fiber (finish is invoked by the timeout fiber) before waiters are resolved. yield* Deferred.succeed(session.done, session.info) - yield* events.publish(Shell.Event.Exited, { + yield* bus.publish(Shell.Event.Exited, { id, ...(exit !== undefined ? { exit } : {}), status, @@ -299,7 +299,7 @@ export const layer = (options?: ShellSelect.Options) => Layer.effect( ), ) - yield* events.publish(Shell.Event.Created, { info }) + yield* bus.publish(Shell.Event.Created, { info }) yield* Deferred.succeed(ready, session) // Hold the handle's scope open until the command terminates; closing it earlier would // release (kill) the process before its exit is observed. @@ -320,7 +320,7 @@ export function configured(options?: ShellSelect.Options) { return makeLocationNode({ service: Service, layer: layer(options), - deps: [EventV2.node, Location.node, Config.node, Global.node, AppProcess.node], + deps: [Bus.node, Location.node, Config.node, Global.node, AppProcess.node], }) } diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index c66fb4f46c4..521d3d6eb55 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -1,15 +1,15 @@ -export * as SkillV2 from "./skill" +export * as Skill from "./skill" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import path from "path" import { Context, Effect, Layer, Schema, Stream, Types } from "effect" import { FileSystem } from "@opencode-ai/schema/filesystem" import { Skill } from "@opencode-ai/schema/skill" -import { AgentV2 } from "./agent" +import { Agent } from "./agent" import { ConfigMarkdown } from "./config/markdown" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { FSUtil } from "@opencode-ai/util/fs-util" -import { PermissionV2 } from "./permission" +import { Permission } from "./permission" import { AbsolutePath } from "./schema" import { SkillDiscovery } from "./skill/discovery" import { State } from "./state" @@ -33,10 +33,10 @@ export type ID = Skill.ID export const Name = Skill.Name export type Name = Skill.Name -export const Event = Skill.Event +export { Event } from "@opencode-ai/schema/skill" -export const available = (skills: ReadonlyArray, agent: AgentV2.Info) => - skills.filter((skill) => PermissionV2.evaluate("skill", skill.id, agent.permissions).effect !== "deny") +export const available = (skills: ReadonlyArray, agent: Agent.Info) => + skills.filter((skill) => Permission.evaluate("skill", skill.id, agent.permissions).effect !== "deny") const Frontmatter = Schema.Struct({ name: Schema.String.pipe(Schema.optional), @@ -73,14 +73,14 @@ export interface Interface extends State.Transformable { readonly list: () => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Skill") {} +export class Service extends Context.Service()("@opencode/Skill") {} const layer = Layer.effect( Service, Effect.gen(function* () { const discovery = yield* SkillDiscovery.Service const fs = yield* FSUtil.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const state = State.create({ name: "skill", @@ -92,10 +92,10 @@ const layer = Layer.effect( }, list: () => draft.sources as Source[], }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(Skill.Event.Updated, {}).pipe(Effect.asVoid), }) - const load = Effect.fn("SkillV2.load")(function* (source: Source) { + const load = Effect.fn("Skill.load")(function* (source: Source) { const skills: Info[] = [] if (source.type === "embedded") { yield* Effect.logDebug("skill source loaded", { @@ -143,7 +143,7 @@ const layer = Layer.effect( }) const cache = new Map() - const invalidate = Effect.fn("SkillV2.invalidateFromWatcher")(function* (file: string) { + const invalidate = Effect.fn("Skill.invalidateFromWatcher")(function* (file: string) { const invalidated = Array.from(cache.entries()).filter(([, loaded]) => loaded.directories.some((directory) => FSUtil.contains(directory, file)), ) @@ -154,15 +154,15 @@ const layer = Layer.effect( sources: invalidated.map(([key]) => key), skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.id)), }) - yield* events.publish(Event.Updated, {}).pipe(Effect.asVoid) + yield* bus.publish(Skill.Event.Updated, {}).pipe(Effect.asVoid) }) - yield* events.subscribe(FileSystem.Event.Changed).pipe( + yield* bus.subscribe(FileSystem.Event.Changed).pipe( Stream.runForEach((event) => invalidate(event.data.file)), Effect.forkScoped({ startImmediately: true }), ) - const list = Effect.fn("SkillV2.list")(function* () { + const list = Effect.fn("Skill.list")(function* () { const skills = new Map() for (const source of state.get().sources) { const key = Source.key(source) @@ -176,7 +176,7 @@ const layer = Layer.effect( return Service.of({ transform: state.transform, reload: state.reload, - sources: Effect.fn("SkillV2.sources")(function* () { + sources: Effect.fn("Skill.sources")(function* () { return state.get().sources }), list, @@ -187,5 +187,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [SkillDiscovery.node, FSUtil.node, EventV2.node], + deps: [SkillDiscovery.node, FSUtil.node, Bus.node], }) diff --git a/packages/core/src/skill/discovery.ts b/packages/core/src/skill/discovery.ts index 074434ec61d..2a21a4c8639 100644 --- a/packages/core/src/skill/discovery.ts +++ b/packages/core/src/skill/discovery.ts @@ -67,7 +67,7 @@ export interface Interface { readonly pull: (url: string) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SkillDiscovery") {} +export class Service extends Context.Service()("@opencode/SkillDiscovery") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/skill/instructions.ts b/packages/core/src/skill/instructions.ts index 382fede3b58..382987c0249 100644 --- a/packages/core/src/skill/instructions.ts +++ b/packages/core/src/skill/instructions.ts @@ -2,13 +2,13 @@ export * as SkillInstructions from "./instructions" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Context, Effect, Layer, Schema } from "effect" -import { AgentV2 } from "../agent" -import { SkillV2 } from "../skill" +import { Agent } from "../agent" +import { Skill } from "../skill" import { Instructions } from "../instructions/index" const Summary = Schema.Struct({ - id: SkillV2.ID, - name: SkillV2.Name, + id: Skill.ID, + name: Skill.Name, description: Schema.String, }) type Summary = typeof Summary.Type @@ -57,21 +57,21 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray Effect.Effect + readonly load: (agent: Agent.Selection) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/SkillInstructions") {} +export class Service extends Context.Service()("@opencode/SkillInstructions") {} const layer = Layer.effect( Service, Effect.gen(function* () { - const skills = yield* SkillV2.Service + const skills = yield* Skill.Service return Service.of({ load: Effect.fn("SkillInstructions.load")(function* (selection) { const agent = selection.info if (!agent) return Instructions.empty - const permitted = SkillV2.available(yield* skills.list(), agent) + const permitted = Skill.available(yield* skills.list(), agent) const available = permitted .flatMap((skill) => skill.description === undefined || skill.autoinvoke === false @@ -94,4 +94,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [SkillV2.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [Skill.node] }) diff --git a/packages/core/src/snapshot.ts b/packages/core/src/snapshot.ts index 72982fbd90a..5113b3f4c6f 100644 --- a/packages/core/src/snapshot.ts +++ b/packages/core/src/snapshot.ts @@ -81,7 +81,7 @@ export interface Interface { readonly checkout: (snapshot: ID) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Snapshot") {} +export class Service extends Context.Service()("@opencode/Snapshot") {} const layer = Layer.effect( Service, diff --git a/packages/core/src/tool-output-store.ts b/packages/core/src/tool-output-store.ts deleted file mode 100644 index d59a342159f..00000000000 --- a/packages/core/src/tool-output-store.ts +++ /dev/null @@ -1,201 +0,0 @@ -export * as ToolOutputStore from "./tool-output-store" - -import path from "path" -import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" -import { Config } from "./config" -import { FSUtil } from "@opencode-ai/util/fs-util" -import { Global } from "@opencode-ai/util/global" -import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { SessionSchema } from "./session/schema" -import { Identifier } from "./util/identifier" -import type { ToolContent } from "@opencode-ai/ai" - -export const MAX_LINES = 2_000 -export const MAX_BYTES = 50 * 1024 -export const RETENTION = Duration.days(7) - -export const MANAGED_DIRECTORY = "tool-output" - -export interface BoundInput { - readonly sessionID: SessionSchema.ID - readonly callID: string - readonly content: ReadonlyArray -} - -export interface BoundResult { - readonly content: ReadonlyArray - readonly outputPaths: ReadonlyArray -} - -export class StorageError extends Schema.TaggedErrorClass()("ToolOutputStore.StorageError", { - operation: Schema.Literals(["encode", "write"]), - cause: Schema.Defect(), -}) { - override get message() { - const detail = this.cause instanceof Error ? this.cause.message : String(this.cause) - return `Failed to ${this.operation} tool output${detail ? `: ${detail}` : ""}` - } -} - -export type Error = StorageError - -export interface Interface { - readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }> - readonly bound: (input: BoundInput) => Effect.Effect - readonly cleanup: () => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/ToolOutputStore") {} - -const takePrefix = (input: string, maximumBytes: number) => { - let bytes = 0 - let content = "" - for (const char of input) { - const size = Buffer.byteLength(char, "utf-8") - if (bytes + size > maximumBytes) break - content += char - bytes += size - } - return content -} - -const takeSuffix = (input: string, maximumBytes: number) => { - let bytes = 0 - const content: string[] = [] - for (const char of Array.from(input).toReversed()) { - const size = Buffer.byteLength(char, "utf-8") - if (bytes + size > maximumBytes) break - content.unshift(char) - bytes += size - } - return content.join("") -} - -const preview = (text: string, maxLines: number, maxBytes: number) => { - const lines = text.split("\n") - const headLines = Math.ceil(maxLines / 2) - const tailLines = Math.floor(maxLines / 2) - const sampled = - lines.length <= maxLines - ? text - : [ - lines.slice(0, headLines).join("\n"), - ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : []), - ].join("\n") - if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) { - return lines.length <= maxLines - ? { head: sampled, tail: "" } - : { - head: lines.slice(0, headLines).join("\n"), - tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "", - } - } - const headBytes = Math.ceil(maxBytes / 2) - const tailBytes = Math.floor(maxBytes / 2) - return { head: takePrefix(sampled, headBytes), tail: takeSuffix(sampled, tailBytes) } -} - -const boundedPreview = (text: string, marker: string, maxLines: number, maxBytes: number) => { - const markerOnly = takePrefix(marker, maxBytes).split("\n").slice(0, maxLines).join("\n") - const markerBytes = Buffer.byteLength(marker, "utf-8") - if (maxLines <= 4 || maxBytes <= markerBytes + 4) return markerOnly - const bounded = preview(text, maxLines - 4, maxBytes - markerBytes - 4) - return bounded.tail ? `${bounded.head}\n\n${marker}\n\n${bounded.tail}` : `${bounded.head}\n\n${marker}` -} - -const lineCount = (text: string) => { - let count = 1 - for (const char of text) if (char === "\n") count++ - return count -} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* FSUtil.Service - const global = yield* Global.Service - const config = yield* Effect.serviceOption(Config.Service) - const directory = path.join(global.data, MANAGED_DIRECTORY) - const limits = Effect.fn("ToolOutputStore.limits")(function* () { - if (Option.isNone(config)) return { maxLines: MAX_LINES, maxBytes: MAX_BYTES } - const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[]))) - const configured = Object.assign( - {}, - ...entries.flatMap((entry) => (entry.type === "document" ? [entry.info.tool_output ?? {}] : [])), - ) - return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES } - }) - - const write = Effect.fn("ToolOutputStore.write")(function* (content: string) { - const file = path.join(directory, `tool_${Identifier.ascending()}`) - yield* fs.ensureDir(directory).pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause }))) - yield* fs - .writeFileString(file, content, { flag: "wx" }) - .pipe(Effect.mapError((cause) => new StorageError({ operation: "write", cause }))) - return file - }) - - const bound = Effect.fn("ToolOutputStore.bound")(function* (input: BoundInput) { - const outputLimits = yield* limits() - const media = input.content.filter((item) => item.type === "file") - const contextual = input.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("") - if ( - lineCount(contextual) <= outputLimits.maxLines && - Buffer.byteLength(contextual, "utf-8") <= outputLimits.maxBytes - ) - return { - content: input.content, - outputPaths: [], - } - - const outputPath = yield* write(contextual) - const marker = `... output truncated; full content saved to ${outputPath} ...` - - return { - content: [ - { - type: "text" as const, - text: boundedPreview(contextual, marker, outputLimits.maxLines, outputLimits.maxBytes), - }, - ...media, - ], - outputPaths: [outputPath], - } - }) - - const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () { - const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([]))) - const cutoff = Date.now() - Duration.toMillis(RETENTION) - for (const entry of entries) { - if (!entry.startsWith("tool_")) continue - const file = path.join(directory, entry) - const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.void)) - const modified = info?.mtime.pipe( - Option.map((date) => date.getTime()), - Option.getOrElse(() => 0), - ) - if (modified !== undefined && modified < cutoff) yield* fs.remove(file).pipe(Effect.catch(() => Effect.void)) - } - }) - - return Service.of({ limits, bound, cleanup }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Config.node] }) - -export const nodeWithoutConfig = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node] }) - -/** Runs retention scanning once globally rather than once per active Location. */ -export const cleanupLayer = Layer.effectDiscard( - Effect.gen(function* () { - const store = yield* Service - yield* store.cleanup().pipe(Effect.repeat(Schedule.spaced(Duration.hours(1))), Effect.forkScoped) - }), -) - -export const cleanupNode = makeGlobalNode({ - name: "tool-output-cleanup", - layer: Layer.merge(layer, cleanupLayer.pipe(Layer.provide(layer))), - deps: [FSUtil.node, Global.node], -}) diff --git a/packages/core/src/tool.ts b/packages/core/src/tool.ts new file mode 100644 index 00000000000..43bc900c46a --- /dev/null +++ b/packages/core/src/tool.ts @@ -0,0 +1,285 @@ +export * as Tool from "./tool.js" +export { CallID, Content, Error, FileContent, TextContent } from "@opencode-ai/schema/tool" +export type { Context, Metadata, Options, Result } from "@opencode-ai/schema/tool" + +import type { ToolCall, ToolDefinition } from "@opencode-ai/ai" +import { Tool } from "@opencode-ai/schema/tool" +import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect" +import { makeLocationNode } from "@opencode-ai/util/effect/app-node" +import type { Agent } from "./agent" +import { CodeModeCatalog } from "./codemode/catalog" +import { CodeModeTool } from "./codemode/tool" +import { Image } from "./image" +import { Permission } from "./permission" +import { PluginHooks } from "./plugin/hooks" +import { SessionMessage } from "./session/message" +import { SessionSchema } from "./session/schema" +import { definition, execute, normalizeContent } from "./tool/runtime" +import { Wildcard } from "./util/wildcard" + +export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { + name: Schema.String, + message: Schema.String, +}) {} + +export interface Interface { + readonly transform: ( + callback: (draft: { readonly add: (tool: Tool.Info) => void }) => void, + ) => Effect.Effect + readonly snapshot: (permissions?: Permission.Ruleset) => Effect.Effect +} + +export interface Snapshot { + readonly definitions: ReadonlyArray + readonly codeModeCatalog?: ReadonlyArray + readonly execute: (input: { + readonly sessionID: SessionSchema.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly call: ToolCall + readonly progress?: (update: Tool.Metadata) => Effect.Effect + }) => Effect.Effect }, Tool.Error> +} + +export class Service extends Context.Service()("@opencode/Tool") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const hooks = yield* PluginHooks.Service + const image = yield* Image.Service + + type NormalizedItem = Tool.Content | "decode" | "size" + const normalizeImages = Effect.fn("Tool.normalizeImages")(function* (content: ReadonlyArray) { + const normalized = yield* Effect.forEach(content, (item): Effect.Effect => { + if (item.type !== "file" || !item.mime.startsWith("image/")) return Effect.succeed(item) + const base64 = /^data:[^,]*;base64,(.*)$/s.exec(item.uri)?.[1] + if (base64 === undefined) return Effect.succeed(item) + const resource = item.name ?? `${item.mime} tool output` + return image.normalize(resource, { uri: resource, content: base64, encoding: "base64", mime: item.mime }).pipe( + Effect.map((result) => ({ + ...item, + uri: `data:${result.mime};base64,${result.content}`, + mime: result.mime, + })), + Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(item)), + Effect.catchTag("Image.DecodeError", () => Effect.succeed("decode" as const)), + Effect.catchTag("Image.SizeError", () => Effect.succeed("size" as const)), + ) + }) + const note = (reason: "decode" | "size", text: string) => { + const count = normalized.filter((item) => item === reason).length + if (count === 0) return [] + return [{ type: "text" as const, text: `[${count} image${count === 1 ? "" : "s"} omitted: ${text}]` }] + } + return [ + ...normalized.filter((item) => typeof item !== "string"), + ...note("decode", "could not be decoded."), + ...note("size", "could not be resized below the image size limit."), + ] + }) + + const local = new Map>() + const lock = Semaphore.makeUnsafe(1) + + const executeTool = Effect.fn("Tool.execute")(function* ( + tool: Tool.Info, + name: string, + input: unknown, + context: Tool.Context, + ) { + const beforeEvent: PluginHooks.Domains["tool"]["execute.before"] = { + tool: name, + sessionID: context.sessionID, + agent: context.agent, + messageID: context.messageID, + callID: context.callID, + input, + } + yield* hooks.trigger("tool", "execute.before", beforeEvent) + const execution = yield* execute(tool, beforeEvent.input, context).pipe( + Effect.map((value) => ({ value })), + Effect.catchTag("Tool.Error", (failure) => Effect.succeed({ failure })), + ) + const base = { + tool: name, + sessionID: context.sessionID, + agent: context.agent, + messageID: context.messageID, + callID: context.callID, + input: beforeEvent.input, + } + if ("failure" in execution) { + const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = { + ...base, + status: "error", + error: execution.failure, + } + yield* hooks.trigger("tool", "execute.after", afterEvent) + return yield* afterEvent.error + } + const content = yield* normalizeImages(execution.value.content) + const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = { + ...base, + status: "completed", + result: { + ...(execution.value.output === undefined ? {} : { output: execution.value.output }), + content: content.length > 0 ? content : execution.value.content, + ...(execution.value.metadata === undefined ? {} : { metadata: execution.value.metadata }), + }, + } + yield* hooks.trigger("tool", "execute.after", afterEvent) + const afterContent = yield* normalizeImages(normalizeContent(afterEvent.result.content, afterEvent.result.output)) + return { + ...(afterEvent.result.output === undefined ? {} : { output: afterEvent.result.output }), + content: afterContent, + ...(afterEvent.result.metadata === undefined ? {} : { metadata: afterEvent.result.metadata }), + } + }) + + const transform: Interface["transform"] = Effect.fn("Tool.transform")(function* (callback) { + const tools: Array = [] + yield* Effect.sync(() => callback({ add: (tool) => tools.push(tool) })) + yield* Effect.forEach( + tools.flatMap((tool) => (tool.options?.namespace === undefined ? [] : [tool.options.namespace])), + validateNamespace, + { discard: true }, + ) + const entries = normalizedEntries(tools) + yield* Effect.forEach(entries, (entry) => validateName(normalizedName(entry.tool)), { discard: true }) + const collision = entries.find( + (entry, index) => entries.findIndex((candidate) => candidate.key === entry.key) !== index, + ) + if (collision) + return yield* Effect.fail( + new RegistrationError({ + name: collision.key, + message: `Duplicate normalized tool name: ${collision.key}`, + }), + ) + const reserved = entries.find((entry) => entry.tool.options?.codemode === false && entry.key === "execute") + if (reserved) + return yield* Effect.fail( + new RegistrationError({ + name: reserved.key, + message: 'Tool name "execute" is reserved for CodeMode', + }), + ) + if (entries.length === 0) return + yield* Effect.uninterruptible( + lock.withPermit( + Effect.gen(function* () { + const token = {} + for (const entry of entries) + local.set(entry.key, [...(local.get(entry.key) ?? []), { token, tool: entry.tool }]) + yield* Effect.addFinalizer(() => + lock.withPermit( + Effect.sync(() => { + for (const entry of entries) { + const remaining = local.get(entry.key)?.filter((item) => item.token !== token) ?? [] + if (remaining.length > 0) local.set(entry.key, remaining) + else local.delete(entry.key) + } + }), + ), + ) + }), + ), + ) + }) + + return Service.of({ + transform, + snapshot: Effect.fn("Tool.snapshot")((permissions) => + lock.withPermit( + Effect.gen(function* () { + const active = new Map() + const rules = permissions ?? [] + for (const [name, entries] of local) { + const tool = entries.at(-1)?.tool + if (!tool) continue + if (whollyDisabled(tool.options?.permission ?? name, rules)) continue + active.set(name, tool) + } + const direct = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode === false)) + const codemode = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode !== false)) + const executeRule = rules.findLast((rule) => Wildcard.match("execute", rule.action)) + const codemodeEnabled = executeRule?.resource !== "*" || executeRule.effect !== "deny" + const codemodeTool = codemodeEnabled + ? CodeModeTool.create(codemode, (name, tool, input, context) => executeTool(tool, name, input, context)) + : undefined + const codeModeCatalog = codemodeEnabled ? CodeModeTool.catalog(codemode) : undefined + return { + ...(codeModeCatalog === undefined ? {} : { codeModeCatalog }), + definitions: [ + ...Array.from(direct) + .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + .map(([, tool]) => definition(tool)), + ...(codemodeTool ? [definition(codemodeTool)] : []), + ], + execute: (input: { + readonly sessionID: SessionSchema.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly call: ToolCall + readonly progress?: (update: Tool.Metadata) => Effect.Effect + }) => { + const context: Tool.Context = { + sessionID: input.sessionID, + agent: input.agent, + messageID: input.messageID, + callID: Tool.CallID.make(input.call.id), + progress: input.progress ?? (() => Effect.void), + } + if (input.call.name === "execute" && codemodeTool) + return executeTool(codemodeTool, input.call.name, input.call.input, context) + const tool = direct.get(input.call.name) + if (tool) return executeTool(tool, input.call.name, input.call.input, context) + return new Tool.Error({ message: `Unknown tool: ${input.call.name}` }) + }, + } + }), + ), + ), + }) + }), +) + +const whollyDisabled = (action: string, rules: Permission.Ruleset) => { + const rule = rules.findLast((rule) => Wildcard.match(action, rule.action)) + return rule?.resource === "*" && rule.effect === "deny" +} + +const validateName = (name: string) => + /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) + ? Effect.void + : Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` })) + +const validateNamespace = (namespace: string) => + namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment)) + ? Effect.void + : Effect.fail( + new RegistrationError({ + name: namespace, + message: `Invalid tool namespace: ${JSON.stringify(namespace)}`, + }), + ) + +const normalizedName = (tool: Tool.Info) => tool.name.replace(/[^a-zA-Z0-9_-]/g, "_") + +const effectiveName = (tool: Tool.Info) => + tool.options?.namespace === undefined + ? normalizedName(tool) + : `${tool.options.namespace.replaceAll(".", "_")}_${normalizedName(tool)}` + +const normalizedEntries = (tools: ReadonlyArray) => + tools.map((tool) => ({ + key: effectiveName(tool), + tool, + })) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [PluginHooks.node, Image.node], +}) diff --git a/packages/core/src/tool/AGENTS.md b/packages/core/src/tool/AGENTS.md index 2c1e9292bc0..a6bc1541dfc 100644 --- a/packages/core/src/tool/AGENTS.md +++ b/packages/core/src/tool/AGENTS.md @@ -1,20 +1,22 @@ # Core Tool Architecture -This folder owns Core's local tools, Location-scoped registrations, effective lookup, execution, and terminal outcomes. +`src/tool.ts` owns the Location-scoped tool service, registrations, effective lookup, execution, and terminal outcomes. This folder contains its supporting runtime modules and built-in plugins. ## Representations -- `tool.ts` defines the structural canonical `Tool.make({ description, input, output?, execute })` tool. Executors return model content and metadata alongside declared machine output. Shipped built-ins and plugin tools use the same type. -- `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers. -- `registry.ts` stores only canonical Location registrations, derives LLM definitions, executes tools, and applies generic output bounding. +- Plugin authors get schema-derived input types at the `ToolDraft.add` boundary through `Tool`. +- The heterogeneous Core registry deliberately erases registered definitions to `Tool.Info`. Use `any` at this internal boundary; do not replace it with `unknown`, JSON-value plumbing, casts, or compiled wrapper types solely to preserve type safety after registration. +- Executors return model content and metadata alongside declared machine output. Shipped built-ins and plugin tools use the same runtime shape after registration. +- `src/tool.ts` stores canonical Location registrations, derives LLM definitions, executes tools, and applies generic output bounding. +- Built-in tool plugins live in `tool/plugin`. Do not add a second executable entry type, registry-owned executor, authorization callback, output-path callback, or legacy normalization path. ## Construction -Tool schemas use `input` and `output` terminology. A tool carries schemas and executable behavior without public identity. A registration binds its name, namespace, CodeMode placement, and optional catalog permission action. +Tool schemas use `input` and `output` terminology. Each tool carries its name, options, schemas, and executable behavior in one object. -Location-scoped built-in layers acquire `PermissionV2.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context: +Location-scoped built-in layers acquire `Permission.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context: ```ts const source = { @@ -24,13 +26,11 @@ const source = { } ``` -Leaves own resolution, permission, and side-effect ordering. Translate only expected typed errors into `ToolFailure`; do not use `catchCause`, because interruption and defects must survive. User declines from `PermissionV2.assert` and question dismissals travel as defects beneath leaf `mapError` blankets and resurface as typed failures at `SessionModelRequest.executeTool`; leaves must never catch or convert them. A decline with feedback (`PermissionV2.CorrectedError`) stays typed so the leaf converts it into `ToolFailure` and the model continues. +Leaves own resolution, permission, and side-effect ordering. Translate only expected typed errors into `ToolFailure`; do not use `catchCause`, because interruption and defects must survive. User declines from `Permission.assert` and question dismissals travel as defects beneath leaf `mapError` blankets and resurface as typed failures at `SessionModelRequest.executeTool`; leaves must never catch or convert them. A decline with feedback (`Permission.CorrectedError`) stays typed so the leaf converts it into `ToolFailure` and the model continues. ## Registration -Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`. Registrations may provide a -namespace, which flattens direct model names to `_`, and default into CodeMode (`codemode` defaults true; -`codemode: false` keeps the tool on the provider's native tool list). +Built-ins, plugins, and MCP install tools through `ToolRegistry.Service.transform`, adding complete tool objects to the draft. A tool may provide a namespace, which flattens direct model names to `_`, and defaults into CodeMode (`codemode` defaults true; `codemode: false` keeps the tool on the provider's native tool list). Registrations are scoped: @@ -38,21 +38,22 @@ Registrations are scoped: - Closing any registration removes only that registration and reveals the next active one. - Each model request captures the effective tools it advertises; later registration changes affect later requests. +Type safety ends at registration. The registry validates model input and declared output at runtime and should not carry producer schema generics through storage or execution. + `ToolRegistry.Service` is Location-scoped. Do not make the registry process-global or construct a separate application-tool service for each Location. ## Permissions -The registry has no `PermissionV2.Service` dependency and performs no execution authorization. Registration options may attach a permission action solely to preserve whole-tool definition filtering. Most registrations default to their effective name; `edit`, `write`, and `patch` use the shared `edit` action. +The registry has no `Permission.Service` dependency and performs no execution authorization. Registration options may attach a permission action solely to preserve whole-tool definition filtering. Most registrations default to their effective name; `edit`, `write`, and `patch` use the shared `edit` action. Tool filtering is catalog visibility, not execution authorization. A call still executes the captured tool's leaf policy if it reaches execution. ## Output -Built-ins return complete tool responses. `ToolRegistry.ToolSet.execute` is the only local execution and generic model-output bounding boundary and owns managed retention paths. +Built-ins return complete tool responses. `Tool.Snapshot.execute` is the local execution boundary. -Producer capture limits are separate. For example, Bash keeps `AppProcess.maxOutputBytes` and accurately reports stdout/stderr capture loss, but it does not run model-output truncation or return a managed `outputPath`. +Producer capture limits remain local to producers. For example, Bash keeps `AppProcess.maxOutputBytes` and accurately reports stdout/stderr capture loss. ## Current Gaps - MCP and future Session-scoped registrations still need an explicit canonical registration design. -- The public Session result shape currently exposes managed `outputPaths`; full storage encapsulation requires a future opaque managed-output reference design. diff --git a/packages/core/src/tool/hooks.ts b/packages/core/src/tool/hooks.ts deleted file mode 100644 index 2e6cf32942a..00000000000 --- a/packages/core/src/tool/hooks.ts +++ /dev/null @@ -1,79 +0,0 @@ -export * as ToolHooks from "./hooks" - -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { State } from "../state" -import { Context, Effect, Layer, Scope } from "effect" -import type { Tool } from "./tool" - -export type BeforeEvent = Tool.ToolExecuteBeforeEvent - -/** The canonical execution outcome. Hooks never observe the raw domain output. */ -export type AfterEvent = Tool.ToolExecuteAfterEvent - -export interface Interface { - readonly hook: { - readonly before: ( - callback: (event: BeforeEvent) => Effect.Effect | void, - ) => Effect.Effect - readonly after: ( - callback: (event: AfterEvent) => Effect.Effect | void, - ) => Effect.Effect - } - readonly runBefore: (event: BeforeEvent) => Effect.Effect - readonly runAfter: (event: AfterEvent) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/ToolHooks") {} - -const layer = Layer.effect( - Service, - Effect.gen(function* () { - let beforeHooks: ((event: BeforeEvent) => Effect.Effect | void)[] = [] - let afterHooks: ((event: AfterEvent) => Effect.Effect | void)[] = [] - - const register = ( - hooks: () => ((event: Event) => Effect.Effect | void)[], - update: (hooks: ((event: Event) => Effect.Effect | void)[]) => void, - ) => - Effect.fn("ToolHooks.hook")(function* (callback: (event: Event) => Effect.Effect | void) { - const scope = yield* Scope.Scope - let active = true - update([...hooks(), callback]) - const dispose = Effect.sync(() => { - if (!active) return - active = false - update(hooks().filter((item) => item !== callback)) - }) - yield* Scope.addFinalizer(scope, dispose) - return { dispose } - }) - - const run = Effect.fnUntraced(function* ( - hooks: readonly ((event: Event) => Effect.Effect | void)[], - event: Event, - ) { - for (const hook of hooks) { - const result = hook(event) - if (Effect.isEffect(result)) yield* result - } - return event - }) - - return Service.of({ - hook: { - before: register( - () => beforeHooks, - (next) => (beforeHooks = next), - ), - after: register( - () => afterHooks, - (next) => (afterHooks = next), - ), - }, - runBefore: (event) => run(beforeHooks, event), - runAfter: (event) => run(afterHooks, event), - }) - }), -) - -export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/tool/mcp.ts b/packages/core/src/tool/mcp.ts index 24b10f79dad..d82e45ae33e 100644 --- a/packages/core/src/tool/mcp.ts +++ b/packages/core/src/tool/mcp.ts @@ -4,13 +4,11 @@ import { ToolFailure } from "@opencode-ai/ai" import { McpEvent } from "@opencode-ai/schema/mcp-event" import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { MCP } from "../mcp" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" -import { Tools } from "./tools" -import { ToolRegistry } from "./registry" +import { Permission } from "../permission" +import { Tool } from "../tool" /** * Registry namespace and permission action names for MCP tools. @@ -21,9 +19,9 @@ export const name = (server: string, tool: string) => `${namespace(server)}_${to export const layer = Layer.effectDiscard( Effect.gen(function* () { const mcp = yield* MCP.Service - const tools = yield* Tools.Service - const events = yield* EventV2.Service - const permission = yield* PermissionV2.Service + const tools = yield* Tool.Service + const bus = yield* Bus.Service + const permission = yield* Permission.Service const scope = yield* Scope.Scope const lock = Semaphore.makeUnsafe(1) let current: Scope.Closeable | undefined @@ -32,27 +30,25 @@ export const layer = Layer.effectDiscard( // registry never has a gap where MCP tools disappear mid-swap. const reconcile = lock.withPermit( Effect.gen(function* () { - const groups = new Map< - string, - { - tools: Record - codemode: boolean - } - >() - for (const tool of yield* mcp.tools()) { - const group = groups.get(tool.server) ?? { tools: {}, codemode: tool.codemode !== false } - const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema - group.tools[tool.name] = Tool.make({ - description: tool.description ?? "", - input: { - ...schema, - type: "object", - properties: schema.properties ?? {}, - additionalProperties: false, - }, - output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema, - execute: (input, context) => - Effect.gen(function* () { + const discovered = yield* mcp.tools() + const next = yield* Scope.fork(scope) + yield* tools + .transform((draft) => { + for (const tool of discovered) { + const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema + draft.add({ + name: tool.name, + options: { namespace: namespace(tool.server), codemode: tool.codemode !== false }, + description: tool.description ?? "", + input: { + ...schema, + type: "object", + properties: schema.properties ?? {}, + additionalProperties: false, + }, + output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema, + execute: (input, context) => + Effect.gen(function* () { yield* permission.assert({ action: name(tool.server, tool.name), resources: ["*"], @@ -90,31 +86,27 @@ export const layer = Layer.effectDiscard( const content = result.content.map((part) => part.type === "text" ? { type: "text" as const, text: part.text } - : { type: "file" as const, data: part.data, mime: part.mimeType }, + : { + type: "file" as const, + uri: `data:${part.mimeType};base64,${part.data}`, + mime: part.mimeType, + }, ) const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n") return { output: result.structured ?? (text === "" ? null : text), - ...(content.length === 0 ? {} : { content: content as [Tool.Content, ...Tool.Content[]] }), + ...(content.length === 0 ? {} : { content }), } - }).pipe( - Effect.mapError((error) => - error instanceof ToolFailure - ? error - : new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }), - ), - ), + }).pipe( + Effect.mapError((error) => + error instanceof ToolFailure + ? error + : new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }), + ), + ), + }) + } }) - groups.set(tool.server, group) - } - const next = yield* Scope.fork(scope) - yield* tools - .registerBatch( - Array.from(groups, ([server, group]) => ({ - tools: group.tools, - options: { namespace: namespace(server), codemode: group.codemode }, - })), - ) .pipe(Scope.provide(next), Effect.orDie) if (current) yield* Scope.close(current, Exit.void) current = next @@ -122,7 +114,7 @@ export const layer = Layer.effectDiscard( ) yield* reconcile.pipe(Effect.forkScoped) - yield* events.subscribe(McpEvent.ToolsChanged).pipe( + yield* bus.subscribe(McpEvent.ToolsChanged).pipe( Stream.runForEach(() => reconcile), Effect.forkScoped({ startImmediately: true }), ) @@ -132,5 +124,5 @@ export const layer = Layer.effectDiscard( export const node = makeLocationNode({ name: "mcp-tools", layer, - deps: [ToolRegistry.toolsNode, MCP.node, EventV2.node, PermissionV2.node], + deps: [Tool.node, MCP.node, Bus.node, Permission.node], }) diff --git a/packages/core/src/tool/edit.ts b/packages/core/src/tool/plugin/edit.ts similarity index 92% rename from packages/core/src/tool/edit.ts rename to packages/core/src/tool/plugin/edit.ts index 00b49ca0d0a..3807020ab24 100644 --- a/packages/core/src/tool/edit.ts +++ b/packages/core/src/tool/plugin/edit.ts @@ -1,21 +1,20 @@ /** - * Model-facing V2 exact-edit leaf. Relative paths resolve within the active + * Model-facing exact-edit leaf. Relative paths resolve within the active * Location. Absolute paths inside that Location are accepted, while explicit * absolute external paths retain mutation capability through a separate * external_directory approval before edit approval. */ export * as EditTool from "./edit" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" import { Effect, Schema } from "effect" -import { FileMutation } from "../file-mutation" +import { FileMutation } from "../../file-mutation" import { FSUtil } from "@opencode-ai/util/fs-util" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" export const name = "edit" @@ -78,12 +77,12 @@ export const toModelOutput = (output: Output, oldString: string, newString: stri "```", ].join("\n") -/** Deferred V2 edit behavior and UX integrations remain visible at the model-facing seam. */ +/** Deferred edit behavior and UX integrations remain visible at the model-facing seam. */ // TODO: Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review. -// TODO: Add formatter integration after V2 formatter runtime exists. -// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add formatter integration after formatter runtime exists. +// TODO: Publish watcher/file-edit events after watcher integration exists. // TODO: Add snapshots / undo after design exists. -// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. +// TODO: Add LSP notification and diagnostics after LSP runtime exists. export const Plugin = { id: "opencode.tool.edit", @@ -91,13 +90,14 @@ export const Plugin = { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service const fs = yield* FSUtil.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false, permission: "edit" }, description: "Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", input: Input, @@ -212,7 +212,6 @@ export const Plugin = { ) }, }), - { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/plugin/glob.ts similarity index 92% rename from packages/core/src/tool/glob.ts rename to packages/core/src/tool/plugin/glob.ts index 39929ad44bf..f1003ef5566 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/plugin/glob.ts @@ -1,17 +1,16 @@ export * as GlobTool from "./glob" import { ToolFailure } from "@opencode-ai/ai" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema } from "effect" import path from "path" -import { FileSystem } from "../filesystem" +import { FileSystem } from "../../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" -import { LocationMutation } from "../location-mutation" -import { Ripgrep } from "../ripgrep" -import { RelativePath } from "../schema" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { Location } from "../../location" +import { LocationMutation } from "../../location-mutation" +import { Ripgrep } from "../../ripgrep" +import { RelativePath } from "../../schema" +import { Permission } from "../../permission" export const name = "glob" @@ -47,13 +46,14 @@ export const Plugin = { const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service const mutation = yield* LocationMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: 'Search file paths using a glob pattern (examples: "**/*.ts", "src/**/*.tsx").', input: Input, @@ -130,7 +130,6 @@ export const Plugin = { ), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/plugin/grep.ts similarity index 93% rename from packages/core/src/tool/grep.ts rename to packages/core/src/tool/plugin/grep.ts index 93267b34bea..64dcf45fe79 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/plugin/grep.ts @@ -1,17 +1,16 @@ export * as GrepTool from "./grep" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" import path from "path" -import { FileSystem } from "../filesystem" +import { FileSystem } from "../../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { Ripgrep } from "../ripgrep" -import { RelativePath } from "../schema" -import { Tool } from "./tool" +import { Location } from "../../location" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" +import { Ripgrep } from "../../ripgrep" +import { RelativePath } from "../../schema" export const name = "grep" @@ -63,13 +62,14 @@ export const Plugin = { const ripgrep = yield* Ripgrep.Service const location = yield* Location.Service const mutation = yield* LocationMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: "Search file contents using regular expressions. Use it to locate specific code, symbols, or text patterns, and narrow searches with `path` or `include`. Returns matching file paths, line numbers, and line previews.", input: Input, @@ -154,7 +154,6 @@ export const Plugin = { ), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/patch.ts b/packages/core/src/tool/plugin/patch.ts similarity index 98% rename from packages/core/src/tool/patch.ts rename to packages/core/src/tool/plugin/patch.ts index c7307c069ef..ea29a4829c9 100644 --- a/packages/core/src/tool/patch.ts +++ b/packages/core/src/tool/plugin/patch.ts @@ -1,6 +1,6 @@ export * as PatchTool from "./patch" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { FileDiff } from "@opencode-ai/schema/file-diff" import { createTwoFilesPatch, diffLines } from "diff" @@ -8,11 +8,10 @@ import { Effect, Schema } from "effect" import { PlatformError } from "effect/PlatformError" import path from "path" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" +import { Location } from "../../location" import { Patch } from "@opencode-ai/util/patch" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" -import DESCRIPTION from "./patch.txt" +import { Permission } from "../../permission" +import DESCRIPTION from "../patch.txt" export const name = "patch" @@ -70,13 +69,14 @@ export const Plugin = { effect: Effect.fn("PatchTool.Plugin")(function* (ctx: PluginContext) { const fs = yield* FSUtil.Service const location = yield* Location.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false, permission: "edit" }, description: DESCRIPTION, input: Input, output: Output, @@ -310,7 +310,6 @@ export const Plugin = { ) }, }), - { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/plugin/question.ts similarity index 86% rename from packages/core/src/tool/question.ts rename to packages/core/src/tool/plugin/question.ts index 1ad52a8f1b1..f402cb350de 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/plugin/question.ts @@ -1,12 +1,11 @@ export * as QuestionTool from "./question" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { Form } from "../form" -import { PermissionV2 } from "../permission" -import { QuestionV2 } from "../question" -import { Tool } from "./tool" +import { Form } from "../../form" +import { Permission } from "../../permission" +import { Question } from "../../question" export const name = "question" @@ -22,11 +21,11 @@ Usage notes: - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label` export const Input = Schema.Struct({ - questions: Schema.NonEmptyArray(QuestionV2.Prompt).annotate({ description: "Questions to ask" }), + questions: Schema.NonEmptyArray(Question.Prompt).annotate({ description: "Questions to ask" }), }) export const Output = Schema.Struct({ - answers: Schema.Array(QuestionV2.Answer), + answers: Schema.Array(Question.Answer), }) export type Output = typeof Output.Type @@ -37,8 +36,8 @@ export class CancelledError extends Schema.TaggedErrorClass()("Q } export const toModelOutput = ( - questions: ReadonlyArray, - answers: ReadonlyArray, + questions: ReadonlyArray, + answers: ReadonlyArray, ) => { const formatted = questions .map( @@ -53,13 +52,14 @@ export const Plugin = { id: "opencode.tool.question", effect: Effect.fn("QuestionTool.Plugin")(function* (ctx: PluginContext) { const forms = yield* Form.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -91,12 +91,12 @@ export const Plugin = { .pipe(Effect.orDie), ), Effect.flatMap((state) => { - // Deliberate defect tunnel (see PermissionV2.assert): a dismissal must dodge + // Deliberate defect tunnel (see Permission.assert): a dismissal must dodge // leaf `mapError` blankets so it never becomes model-facing tool output; it // resurfaces as a typed failure at SessionModelRequest.executeTool. if (state.status === "cancelled") return Effect.die(new CancelledError()) const output = { - answers: input.questions.map((_, index): QuestionV2.Answer => { + answers: input.questions.map((_, index): Question.Answer => { const value = state.answer[`q${index}`] if (value === undefined) return [] if (typeof value === "object") return Array.from(value) @@ -111,14 +111,13 @@ export const Plugin = { }), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) }), } -function toField(question: QuestionV2.Prompt, index: number): Form.Field { +function toField(question: Question.Prompt, index: number): Form.Field { return { key: `q${index}`, title: question.header, diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/plugin/read.ts similarity index 89% rename from packages/core/src/tool/read.ts rename to packages/core/src/tool/plugin/read.ts index 5c0b9254beb..2de9757bd68 100644 --- a/packages/core/src/tool/read.ts +++ b/packages/core/src/tool/plugin/read.ts @@ -1,18 +1,17 @@ export * as ReadTool from "./read" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { dirname } from "path" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { FileSystem } from "../filesystem" +import { FileSystem } from "../../filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" -import { Location } from "../location" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { SessionInstructions } from "../session/instructions" -import { AbsolutePath } from "../schema" -import { ReadToolFileSystem } from "./read-filesystem" -import { Tool } from "./tool" +import { Location } from "../../location" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" +import { SessionInstructions } from "../../session/instructions" +import { AbsolutePath } from "../../schema" +import { ReadToolFileSystem } from "../read-filesystem" export const name = "read" const FILENAME = "AGENTS.md" @@ -34,7 +33,7 @@ export const Plugin = { effect: Effect.fn("ReadTool.Plugin")(function* (ctx: PluginContext) { const reader = yield* ReadToolFileSystem.Service const mutation = yield* LocationMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const sessionInstructions = yield* SessionInstructions.Service const fs = yield* FSUtil.Service const location = yield* Location.Service @@ -42,8 +41,9 @@ export const Plugin = { yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: "Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page. Relative paths resolve from the current location; absolute paths inside it are accepted, while external absolute paths require external_directory approval.", input: Input, @@ -119,7 +119,12 @@ export const Plugin = { ? SUPPORTED_IMAGE_MIMES.has(output.mime) ? ([ { type: "text", text: "Image read successfully" }, - { type: "file", data: output.content, mime: output.mime, name: input.path }, + { + type: "file", + uri: `data:${output.mime};base64,${output.content}`, + mime: output.mime, + name: input.path, + }, ] as const) : JSON.stringify({ ...output, content: "" }, null, 2) : JSON.stringify(output, null, 2) @@ -136,7 +141,6 @@ export const Plugin = { ) }, }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/shell.ts b/packages/core/src/tool/plugin/shell.ts similarity index 94% rename from packages/core/src/tool/shell.ts rename to packages/core/src/tool/plugin/shell.ts index 534855a78d6..f26224caeab 100644 --- a/packages/core/src/tool/shell.ts +++ b/packages/core/src/tool/plugin/shell.ts @@ -2,16 +2,16 @@ export * as ShellTool from "./shell" import path from "path" import { ToolFailure } from "@opencode-ai/ai" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Content } from "@opencode-ai/schema/tool" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Deferred, Effect, Schema, Scope } from "effect" import { FSUtil } from "@opencode-ai/util/fs-util" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { PluginRuntime } from "../plugin/runtime" -import { NonNegativeInt } from "../schema" -import { SessionSchema } from "../session/schema" -import { Shell } from "../shell" -import { Tool, type Content } from "./tool" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" +import { PluginRuntime } from "../../plugin/runtime" +import { NonNegativeInt } from "../../schema" +import { SessionSchema } from "../../session/schema" +import { Shell } from "../../shell" export const name = "shell" export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000 @@ -64,14 +64,14 @@ const modelOutput = (output: Output): string | undefined => { } /** - * Minimal V2 core shell boundary. Keep parity debt visible without pulling the + * Minimal core shell boundary. Keep parity debt visible without pulling the * legacy shell runtime into core. */ // TODO: Port tree-sitter bash / PowerShell parser-based approval reduction. // TODO: Port BashArity reusable command-prefix approvals. // TODO: Replace token-based command-argument external-directory advisories with parser-based detection. // TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows. -// TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist. +// TODO: Add plugin shell.env environment augmentation once plugin hooks exist. // TODO: Persist job status and define restart recovery before exposing remote observation. // TODO: Add HTTP job observation only after durable status, restart recovery, and authorization are defined. // TODO: Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it. @@ -104,7 +104,7 @@ export const Plugin = { const fsUtil = yield* FSUtil.Service const mutation = yield* LocationMutation.Service const shell = yield* Shell.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const notifyWhenDone = Effect.fn("ShellTool.notifyWhenDone")(function* ( sessionID: SessionSchema.ID, @@ -142,8 +142,9 @@ export const Plugin = { yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. An optional timeout may be provided in milliseconds (zero: unlimited; foreground default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Background commands default to unlimited. Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows. Background mode (background=true) launches the command asynchronously and returns immediately; you are notified when it finishes.`, input: Input, output: Output, @@ -267,7 +268,7 @@ export const Plugin = { return { ...(yield* Deferred.await(settled)), ...(warnings.length ? { warnings } : {}) } }).pipe( Effect.map((output) => { - const content: [Content, ...Content[]] = [{ type: "text", text: output.output }] + const content: Array = [{ type: "text", text: output.output }] const model = modelOutput(output) if (model) content.push({ type: "text", text: model }) return { @@ -286,7 +287,6 @@ export const Plugin = { ), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/plugin/skill.ts similarity index 87% rename from packages/core/src/tool/skill.ts rename to packages/core/src/tool/plugin/skill.ts index 48bdb0b17f7..f8a4109a63a 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/plugin/skill.ts @@ -1,23 +1,22 @@ export * as SkillTool from "./skill" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import path from "path" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" import { FSUtil } from "@opencode-ai/util/fs-util" -import { SkillV2 } from "../skill" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { Skill } from "../../skill" +import { Permission } from "../../permission" export const name = "skill" const FILE_LIMIT = 10 export const Input = Schema.Struct({ - id: SkillV2.ID.annotate({ description: "The ID of the skill from the available skills list" }), + id: Skill.ID.annotate({ description: "The ID of the skill from the available skills list" }), }) export const Output = Schema.Struct({ - name: SkillV2.Name, + name: Skill.Name, directory: Schema.String, output: Schema.String, }) @@ -27,7 +26,7 @@ export const description = [ "The skill ID must match one of the available skills in the instructions.", ].join("\n") -export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray) => { +export const toModelOutput = (skill: Skill.Info, files: ReadonlyArray) => { const directory = path.dirname(skill.location) return [ ``, @@ -53,13 +52,14 @@ export const Plugin = { id: "opencode.tool.skill", effect: Effect.fn("SkillTool.Plugin")(function* (ctx: PluginContext) { const fs = yield* FSUtil.Service - const skills = yield* SkillV2.Service - const permission = yield* PermissionV2.Service + const skills = yield* Skill.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -99,7 +99,6 @@ export const Plugin = { })), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/subagent.ts b/packages/core/src/tool/plugin/subagent.ts similarity index 95% rename from packages/core/src/tool/subagent.ts rename to packages/core/src/tool/plugin/subagent.ts index daef4e1cf39..ee7f54769a0 100644 --- a/packages/core/src/tool/subagent.ts +++ b/packages/core/src/tool/plugin/subagent.ts @@ -1,14 +1,13 @@ export * as SubagentTool from "./subagent" import { ToolFailure } from "@opencode-ai/ai" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Effect, Schema, Scope } from "effect" -import { AgentV2 } from "../agent" -import { Config } from "../config" -import { PluginRuntime } from "../plugin/runtime" -import { PermissionV2 } from "../permission" -import { SessionSchema } from "../session/schema" -import { Tool } from "./tool" +import { Agent } from "../../agent" +import { Config } from "../../config" +import { PluginRuntime } from "../../plugin/runtime" +import { Permission } from "../../permission" +import { SessionSchema } from "../../session/schema" export const name = "subagent" @@ -42,9 +41,9 @@ export const Plugin = { id: "opencode.tool.subagent", effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) { const runtime = yield* PluginRuntime.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const config = yield* Config.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const scope = yield* Scope.Scope // Concatenate the child's final completed assistant text. Distinguishes "completed with no @@ -109,8 +108,9 @@ export const Plugin = { yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -165,7 +165,7 @@ export const Plugin = { .create({ parentID: context.sessionID, title: input.description, - agent: AgentV2.ID.make(input.agent), + agent: Agent.ID.make(input.agent), model, // TODO(opencode kkdvxn): derive restricted subagent permissions from the parent // session (V1 deriveSubagentSessionPermission). MVP uses the agent's own permissions. @@ -238,7 +238,6 @@ export const Plugin = { })), ), }), - { codemode: false }, ), ) .pipe(Effect.orDie) @@ -254,7 +253,7 @@ export const Plugin = { (agent) => agent.mode !== "primary" && !agent.hidden && - PermissionV2.evaluate(name, agent.id, selected.permissions).effect !== "deny", + Permission.evaluate(name, agent.id, selected.permissions).effect !== "deny", ) .toSorted((a, b) => a.id.localeCompare(b.id)) if (available.length === 0) return diff --git a/packages/core/src/tool/webfetch.ts b/packages/core/src/tool/plugin/webfetch.ts similarity index 96% rename from packages/core/src/tool/webfetch.ts rename to packages/core/src/tool/plugin/webfetch.ts index d3f894bdbc4..b4298cfbc54 100644 --- a/packages/core/src/tool/webfetch.ts +++ b/packages/core/src/tool/plugin/webfetch.ts @@ -1,14 +1,13 @@ export * as WebFetchTool from "./webfetch" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Duration, Effect, Schema } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { Parser } from "htmlparser2" import TurndownService from "turndown" -import { PermissionV2 } from "../permission" -import { collectBoundedResponseBody } from "./http-body" -import { Tool } from "./tool" +import { Permission } from "../../permission" +import { collectBoundedResponseBody } from "../http-body" export const name = "webfetch" export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 @@ -115,13 +114,14 @@ export const Plugin = { id: "opencode.tool.webfetch", effect: Effect.fn("WebFetchTool.Plugin")(function* (ctx: PluginContext) { const http = yield* HttpClient.HttpClient - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -173,7 +173,6 @@ export const Plugin = { return { output: result, content: result.output, metadata: { contentType: result.contentType } } }).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to fetch ${input.url}`, error }))), }), - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/websearch.ts b/packages/core/src/tool/plugin/websearch.ts similarity index 94% rename from packages/core/src/tool/websearch.ts rename to packages/core/src/tool/plugin/websearch.ts index 9e3a6256bd1..98e0bc0e572 100644 --- a/packages/core/src/tool/websearch.ts +++ b/packages/core/src/tool/plugin/websearch.ts @@ -1,12 +1,12 @@ export * as WebSearchTool from "./websearch" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { Form } from "../form" -import { KV } from "../kv" -import { PermissionV2 } from "../permission" -import { WebSearch } from "../websearch" +import { Form } from "../../form" +import { KV } from "../../kv" +import { Permission } from "../../permission" +import { WebSearch } from "../../websearch" export const name = "websearch" export const NO_RESULTS = "No search results found. Please try a different query." @@ -26,15 +26,16 @@ const Output = Schema.Struct({ export const Plugin = { id: "opencode.tool.websearch", effect: Effect.fn("WebSearchTool.Plugin")(function* (ctx: PluginContext) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const forms = yield* Form.Service const kv = yield* KV.Service yield* ctx.tool .transform((draft) => draft.add( - name, { + name, + options: { codemode: false }, description, input: Input, output: Output, @@ -109,7 +110,6 @@ export const Plugin = { ), ), }, - { codemode: false }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/write.ts b/packages/core/src/tool/plugin/write.ts similarity index 82% rename from packages/core/src/tool/write.ts rename to packages/core/src/tool/plugin/write.ts index 4d86df9729c..d5e9411a527 100644 --- a/packages/core/src/tool/write.ts +++ b/packages/core/src/tool/plugin/write.ts @@ -1,18 +1,17 @@ /** - * Model-facing V2 file-write leaf. Relative paths resolve within the active + * Model-facing file-write leaf. Relative paths resolve within the active * Location. Absolute paths inside that Location are accepted, while explicit * absolute external paths retain mutation capability through a separate * external_directory approval before edit approval. */ export * as WriteTool from "./write" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { ToolFailure } from "@opencode-ai/ai" import { Effect, Schema } from "effect" -import { FileMutation } from "../file-mutation" -import { LocationMutation } from "../location-mutation" -import { PermissionV2 } from "../permission" -import { Tool } from "./tool" +import { FileMutation } from "../../file-mutation" +import { LocationMutation } from "../../location-mutation" +import { Permission } from "../../permission" export const name = "write" @@ -36,24 +35,25 @@ export type Output = typeof Output.Type export const toModelOutput = (output: Output) => `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}` -/** Deferred V2 write UX integrations remain visible at the model-facing seam. */ -// TODO: Add formatter integration after V2 formatter runtime exists. -// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +/** Deferred write UX integrations remain visible at the model-facing seam. */ +// TODO: Add formatter integration after formatter runtime exists. +// TODO: Publish watcher/file-edit events after watcher integration exists. // TODO: Add snapshots / undo after design exists. -// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. +// TODO: Add LSP notification and diagnostics after LSP runtime exists. export const Plugin = { id: "opencode.tool.write", effect: Effect.fn("WriteTool.Plugin")(function* (ctx: PluginContext) { const mutation = yield* LocationMutation.Service const files = yield* FileMutation.Service - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service yield* ctx.tool .transform((draft) => draft.add( - name, - Tool.make({ + ({ + name, + options: { codemode: false, permission: "edit" }, description: "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", input: Input, @@ -88,7 +88,6 @@ export const Plugin = { Effect.mapError((error) => new ToolFailure({ message: `Unable to write ${input.path}`, error })), ), }), - { codemode: false, permission: "edit" }, ), ) .pipe(Effect.orDie) diff --git a/packages/core/src/tool/registry.ts b/packages/core/src/tool/registry.ts deleted file mode 100644 index c5e3d1014b5..00000000000 --- a/packages/core/src/tool/registry.ts +++ /dev/null @@ -1,377 +0,0 @@ -export * as ToolRegistry from "./registry" - -import { type ToolCall, type ToolContent, type ToolDefinition } from "@opencode-ai/ai" -import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect" -import type { AgentV2 } from "../agent" -import { CodeModeCatalog } from "../codemode/catalog" -import { Image } from "../image" -import { PermissionV2 } from "../permission" -import { SessionMessage } from "../session/message" -import { SessionSchema } from "../session/schema" -import { ToolOutputStore } from "../tool-output-store" -import { Wildcard } from "../util/wildcard" -import { CodeMode } from "../codemode" -import { Tool, nonEmpty, registrationEntries, toLLMDefinition, validateName, validateNamespace } from "./tool" -import { Tools } from "./tools" -import { ToolHooks } from "./hooks" -import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { toSessionError } from "../session/to-session-error" - -export type ExecuteInput = { - readonly sessionID: SessionSchema.ID - readonly agent: AgentV2.ID - readonly messageID: SessionMessage.ID - readonly call: ToolCall - readonly progress?: (update: Progress) => Effect.Effect -} - -/** Live replacement metadata for a running tool. */ -export type Progress = Tool.Metadata - -export interface Interface { - readonly snapshot: (permissions?: PermissionV2.Ruleset) => Effect.Effect - /** Internal registration capability exposed publicly only through Tools.Service. */ - readonly register: ( - tools: Readonly>, - options?: Tools.RegisterOptions, - ) => Effect.Effect - /** Internal atomic registration capability used by plugin transforms. */ - readonly registerBatch: ( - registrations: ReadonlyArray<{ - readonly tools: Readonly> - readonly options?: Tools.RegisterOptions - }>, - ) => Effect.Effect -} - -/** - * One request-scoped snapshot pairing the Code Mode catalog and advertised - * definitions with captured tools. A model request executes exactly the tool - * values it advertised even if registration changes while it is in flight. - */ -export interface ToolSet { - readonly definitions: ReadonlyArray - readonly codeModeCatalog?: ReadonlyArray - readonly execute: (input: ExecuteInput) => Effect.Effect -} - -/** - * The canonical outcome of one local tool execution. `output` is the validated - * machine value for Code Mode and remains ephemeral; durable publication drops it. - */ -export type ToolOutcome = - | (Extract & { readonly output?: unknown }) - | Extract - -export class Service extends Context.Service()("@opencode/v2/ToolRegistry") {} - -const registryLayer = Layer.effect( - Service, - Effect.gen(function* () { - const resources = yield* ToolOutputStore.Service - const toolHooks = yield* ToolHooks.Service - const image = yield* Image.Service - const codeMode = yield* CodeMode.Service - - type NormalizedItem = ToolContent | "decode" | "size" - const normalizeImages = Effect.fn("ToolRegistry.normalizeImages")(function* (content: ReadonlyArray) { - const normalized = yield* Effect.forEach(content, (item): Effect.Effect => { - if (item.type !== "file" || !item.mime.startsWith("image/")) return Effect.succeed(item) - // RFC 2397 permits parameters between the mime and ";base64". - const base64 = /^data:[^,]*;base64,(.*)$/s.exec(item.uri)?.[1] - if (base64 === undefined) return Effect.succeed(item) - const resource = item.name ?? `${item.mime} tool output` - return image.normalize(resource, { uri: resource, content: base64, encoding: "base64", mime: item.mime }).pipe( - Effect.map((result) => ({ - ...item, - uri: `data:${result.mime};base64,${result.content}`, - mime: result.mime, - })), - Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(item)), - Effect.catchTag("Image.DecodeError", () => Effect.succeed("decode" as const)), - Effect.catchTag("Image.SizeError", () => Effect.succeed("size" as const)), - ) - }) - const note = (reason: "decode" | "size", text: string) => { - const count = normalized.filter((item) => item === reason).length - if (count === 0) return [] - return [{ type: "text" as const, text: `[${count} image${count === 1 ? "" : "s"} omitted: ${text}]` }] - } - return [ - ...normalized.filter((item) => typeof item !== "string"), - ...note("decode", "could not be decoded."), - ...note("size", "could not be resized below the image size limit."), - ] - }) - - // Invalid or oversized metadata is dropped with a warning; it never fails a - // successful side-effecting tool. - const validMetadata = Effect.fnUntraced(function* (tool: string, metadata: Tool.Metadata | undefined) { - if (metadata === undefined) return undefined - const limits = yield* resources.limits() - const valid = Tool.jsonMetadata(metadata, limits.maxBytes) - if (valid === undefined) - yield* Effect.logWarning("dropping invalid or oversized tool metadata").pipe(Effect.annotateLogs({ tool })) - return valid - }) - - type Registration = Tool.Registration - const local = new Map>() - const registrationLock = Semaphore.makeUnsafe(1) - - const executeTool = Effect.fn("ToolRegistry.executeTool")(function* (input: ExecuteInput, tool: Tool.Any) { - // Hooks fire only for hosted/local tools; provider-executed calls never reach executeTool. - const beforeEvent: ToolHooks.BeforeEvent = { - tool: input.call.name, - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - input: input.call.input, - } - yield* toolHooks.runBefore(beforeEvent) - const execution = yield* Tool.execute(tool, beforeEvent.input, { - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - progress: (metadata) => { - const progress = input.progress - if (!progress) return Effect.void - return validMetadata(input.call.name, metadata).pipe( - Effect.flatMap((valid) => (valid === undefined ? Effect.void : progress(valid))), - ) - }, - }).pipe( - Effect.map((value) => ({ value })), - Effect.catchTag("LLM.ToolFailure", (failure) => Effect.succeed({ failure: toSessionError(failure) })), - ) - - const outcome: ToolOutcome = yield* Effect.gen(function* () { - if ("failure" in execution) return { status: "error" as const, error: execution.failure } - const bounded = yield* resources.bound({ - sessionID: input.sessionID, - callID: input.call.id, - content: yield* normalizeImages(execution.value.content), - }) - const metadata = yield* validMetadata(input.call.name, execution.value.metadata) - return { - status: "completed" as const, - ...(execution.value.output === undefined ? {} : { output: execution.value.output }), - content: nonEmpty(bounded.content) ?? execution.value.content, - ...(metadata === undefined ? {} : { metadata }), - ...(bounded.outputPaths.length > 0 ? { outputPaths: bounded.outputPaths } : {}), - } - }) - - const base = { - tool: input.call.name, - sessionID: input.sessionID, - agent: input.agent, - messageID: input.messageID, - callID: input.call.id, - input: beforeEvent.input, - } - const afterEvent: ToolHooks.AfterEvent = - outcome.status === "completed" - ? { - ...base, - status: "completed", - content: outcome.content, - ...(outcome.metadata === undefined ? {} : { metadata: outcome.metadata }), - ...(outcome.outputPaths === undefined ? {} : { outputPaths: outcome.outputPaths }), - } - : { - ...base, - status: "error", - error: outcome.error, - ...(outcome.content === undefined ? {} : { content: outcome.content }), - ...(outcome.metadata === undefined ? {} : { metadata: outcome.metadata }), - ...(outcome.outputPaths === undefined ? {} : { outputPaths: outcome.outputPaths }), - } - yield* toolHooks.runAfter(afterEvent) - const afterMetadata = yield* validMetadata(input.call.name, afterEvent.metadata) - const afterContent = yield* Effect.gen(function* () { - if ( - afterEvent.content === undefined || - (outcome.status === "completed" && afterEvent.content === outcome.content) - ) - return { content: afterEvent.content, outputPaths: afterEvent.outputPaths } - const bounded = yield* resources.bound({ - sessionID: input.sessionID, - callID: input.call.id, - content: yield* normalizeImages(afterEvent.content), - }) - return { - content: nonEmpty(bounded.content), - outputPaths: - bounded.outputPaths.length === 0 - ? afterEvent.outputPaths - : Array.from(new Set([...(afterEvent.outputPaths ?? []), ...bounded.outputPaths])), - } - }) - if (afterEvent.status === "completed") - return { - status: "completed" as const, - ...(outcome.status === "completed" && outcome.output !== undefined ? { output: outcome.output } : {}), - content: afterContent.content ?? afterEvent.content, - ...(afterMetadata === undefined ? {} : { metadata: afterMetadata }), - ...(afterContent.outputPaths === undefined ? {} : { outputPaths: afterContent.outputPaths }), - } - return { - status: "error" as const, - error: afterEvent.error, - ...(afterContent.content === undefined ? {} : { content: afterContent.content }), - ...(afterMetadata === undefined ? {} : { metadata: afterMetadata }), - ...(afterContent.outputPaths === undefined ? {} : { outputPaths: afterContent.outputPaths }), - } - }) - - const registerBatch: Interface["registerBatch"] = Effect.fn("ToolRegistry.registerBatch")( - function* (registrations) { - const planned = yield* Effect.forEach(registrations, ({ tools, options }) => - Effect.gen(function* () { - if (options?.namespace !== undefined) yield* validateNamespace(options.namespace) - const entries = registrationEntries(tools, options) - yield* Effect.forEach(entries, (entry) => validateName(entry.name), { discard: true }) - const collision = entries.find( - (entry, index) => entries.findIndex((candidate) => candidate.key === entry.key) !== index, - ) - if (collision) - return yield* Effect.fail( - new Tool.RegistrationError({ - name: collision.key, - message: `Duplicate normalized tool name: ${collision.key}`, - }), - ) - const codemode = options?.codemode ?? true - const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute") - if (reserved) - return yield* Effect.fail( - new Tool.RegistrationError({ - name: reserved.key, - message: 'Tool name "execute" is reserved for CodeMode', - }), - ) - return { tools, options, entries, codemode } - }), - ) - // CodeMode registrations live in the CodeMode service; the registry keeps only direct tools. - yield* Effect.forEach( - planned.filter((plan) => plan.codemode && plan.entries.length > 0), - (plan) => codeMode.register(plan.entries), - { discard: true }, - ) - const direct = planned.filter((plan) => !plan.codemode) - if (direct.every((plan) => plan.entries.length === 0)) return - yield* Effect.uninterruptible( - registrationLock.withPermit( - Effect.gen(function* () { - const token = {} - for (const { entries } of direct) - for (const entry of entries) - local.set(entry.key, [ - ...(local.get(entry.key) ?? []), - { - token, - registration: { - tool: entry.tool, - name: entry.name, - namespace: entry.namespace, - permission: entry.permission, - }, - }, - ]) - yield* Effect.addFinalizer(() => - registrationLock.withPermit( - Effect.sync(() => { - for (const { entries } of direct) - for (const entry of entries) { - const registrations = - local.get(entry.key)?.filter((registration) => registration.token !== token) ?? [] - if (registrations.length > 0) local.set(entry.key, registrations) - else local.delete(entry.key) - } - }), - ), - ) - }), - ), - ) - }, - ) - - return Service.of({ - register: Effect.fn("ToolRegistry.register")((tools, options) => - registerBatch([ - { - tools, - ...(options === undefined ? {} : { options }), - }, - ]), - ), - registerBatch, - snapshot: Effect.fn("ToolRegistry.snapshot")((permissions) => - registrationLock.withPermit( - Effect.gen(function* () { - const direct = new Map() - const rules = permissions ?? [] - for (const [name, entries] of local) { - const registration = entries.at(-1)?.registration - if (!registration) continue - if (whollyDisabled(registration.permission, rules)) continue - direct.set(name, registration) - } - const codeModeMaterialization = yield* codeMode.materialize(permissions) - const codemodeTool = codeModeMaterialization.tool - return { - ...(codeModeMaterialization.catalog === undefined - ? {} - : { codeModeCatalog: codeModeMaterialization.catalog }), - definitions: [ - // Definitions are prompt-cache prefix bytes, so order only after effective registrations settle. - ...Array.from(direct) - .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) - .map(([name, registration]) => toLLMDefinition(name, registration.tool)), - ...(codemodeTool ? [toLLMDefinition("execute", codemodeTool)] : []), - ], - execute: (input: ExecuteInput) => { - if (input.call.name === "execute" && codemodeTool) return executeTool(input, codemodeTool) - const registration = direct.get(input.call.name) - if (registration) return executeTool(input, registration.tool) - return Effect.succeed({ - status: "error", - error: { type: "tool.unknown", message: `Unknown tool: ${input.call.name}` }, - }) - }, - } - }), - ), - ), - }) - }), -) - -const layer = Layer.effect( - Tools.Service, - Service.use((registry) => - Effect.succeed(Tools.Service.of({ register: registry.register, registerBatch: registry.registerBatch })), - ), -).pipe(Layer.provideMerge(registryLayer)) - -function whollyDisabled(action: string, rules: PermissionV2.Ruleset) { - const rule = rules.findLast((rule) => Wildcard.match(action, rule.action)) - return rule?.resource === "*" && rule.effect === "deny" -} - -export const node = makeLocationNode({ - service: Service, - layer, - deps: [CodeMode.node, ToolOutputStore.node, ToolHooks.node, Image.node], -}) - -export const toolsNode = makeLocationNode({ - service: Tools.Service, - layer, - deps: [CodeMode.node, ToolOutputStore.node, ToolHooks.node, Image.node], -}) diff --git a/packages/core/src/tool/runtime.ts b/packages/core/src/tool/runtime.ts new file mode 100644 index 00000000000..80872798e02 --- /dev/null +++ b/packages/core/src/tool/runtime.ts @@ -0,0 +1,125 @@ +import type { ToolDefinition } from "@opencode-ai/ai" +import { Tool } from "@opencode-ai/schema/tool" +import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" +import { Effect, JsonSchema, Schema } from "effect" + +export const definition = (tool: Tool.Info): ToolDefinition => ({ + name: effectiveName(tool), + description: tool.description, + inputSchema: inputJsonSchema(tool.input), + ...(tool.output === undefined ? {} : { outputSchema: outputJsonSchema(tool.output) }), +}) + +export const execute = (tool: Tool.Info, input: unknown, context: Tool.Context) => + Effect.gen(function* () { + const decoded = yield* decodeInput(tool.input, input) + const result = yield* tool.execute(decoded, context) + if (tool.output === undefined) { + if ("output" in result) return yield* Effect.die("Tool result declared output without an output schema") + return { + output: undefined, + content: normalizeContent(result.content), + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), + } + } + if (!("output" in result)) return yield* new Tool.Error({ message: "Tool did not return its declared output" }) + const output = yield* encodeOutput(tool.output, result.output) + return { + output, + content: normalizeContent(result.content, output), + ...(result.metadata === undefined ? {} : { metadata: result.metadata }), + } + }) + +const decodeInput = (schema: Tool.ValueSchema, value: unknown) => { + if (Schema.isSchema(schema)) + return Schema.decodeUnknownEffect(schema)(value).pipe( + Effect.mapError((error) => new Tool.Error({ message: `Invalid tool input: ${error.message}` })), + ) + if (isStandardSchema(schema)) return validateStandard(schema, value, "Invalid tool input") + return Effect.succeed(value) +} + +const encodeOutput = (schema: Tool.ValueSchema, value: unknown) => { + if (Schema.isSchema(schema)) + return Schema.encodeEffect(schema)(value).pipe( + Effect.mapError( + (error) => new Tool.Error({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), + ), + ) + if (isStandardSchema(schema)) + return validateStandard(schema, value, "Tool returned an invalid value for its output schema") + return Schema.decodeUnknownEffect(Schema.Json)(value).pipe( + Effect.mapError( + (error) => new Tool.Error({ message: `Tool returned a non-JSON value for its output schema: ${error.message}` }), + ), + ) +} + +const isStandardSchema = ( + schema: Tool.ValueSchema, +): schema is StandardSchemaV1 & StandardJSONSchemaV1 => "~standard" in schema + +const validateStandard = ( + schema: StandardSchemaV1 & StandardJSONSchemaV1, + value: unknown, + prefix: string, +) => + Effect.gen(function* () { + const pending = yield* Effect.try({ + try: () => schema["~standard"].validate(value), + catch: (error) => standardFailure(prefix, error), + }) + const result = + pending instanceof Promise + ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) + : pending + if (result.issues) + return yield* new Tool.Error({ + message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}`, + }) + return result.value + }) + +const standardFailure = (prefix: string, error: unknown) => + new Tool.Error({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) + +const inputJsonSchema = (schema: Tool.ValueSchema): JsonSchema.JsonSchema => { + if (isStandardSchema(schema)) + return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) +} + +const outputJsonSchema = (schema: Tool.ValueSchema): JsonSchema.JsonSchema => { + if (isStandardSchema(schema)) + return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) +} + +const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => { + const document = Schema.toJsonSchemaDocument(schema) + if (Object.keys(document.definitions).length === 0) return document.schema + return { ...document.schema, $defs: document.definitions } +} + +export const normalizeContent = (value: string | ReadonlyArray | undefined, output?: unknown) => { + if (typeof value === "string") return [{ type: "text" as const, text: value }] + if (value !== undefined && value.length > 0) return [...value] + return [{ type: "text" as const, text: stringify(output) }] +} + +const stringify = (value: unknown) => { + if (typeof value === "string") return value + try { + return JSON.stringify(value) ?? String(value) + } catch { + return String(value) + } +} + +const normalizedName = (tool: Tool.Info) => tool.name.replace(/[^a-zA-Z0-9_-]/g, "_") + +const effectiveName = (tool: Tool.Info) => + tool.options?.namespace === undefined + ? normalizedName(tool) + : `${tool.options.namespace.replaceAll(".", "_")}_${normalizedName(tool)}` diff --git a/packages/core/src/tool/tool.ts b/packages/core/src/tool/tool.ts deleted file mode 100644 index e9917f94ddc..00000000000 --- a/packages/core/src/tool/tool.ts +++ /dev/null @@ -1,90 +0,0 @@ -export * as Tool from "./tool" -export * from "@opencode-ai/plugin/v2/effect/tool" - -import type { ToolContent } from "@opencode-ai/ai" -import { - decodeInput, - encodeOutput, - type Any, - type Content, - type Context, - Failure, - type Metadata, -} from "@opencode-ai/plugin/v2/effect/tool" -import { Effect, Schema } from "effect" - -/** Non-empty canonical model content. */ -export type NonEmptyContent = readonly [ToolContent, ...ToolContent[]] - -/** - * The execution-local result of one tool call: the machine output for - * Code Mode, canonical model content, and optional UI metadata. The typed - * domain output never leaves this function. - */ -export type Execution = { - readonly output?: unknown - readonly content: NonEmptyContent - readonly metadata?: Metadata -} - -export const execute = (tool: Any, input: unknown, context: Context): Effect.Effect => - Effect.gen(function* () { - const decoded = yield* decodeInput(tool.input, input) - const result = yield* tool.execute(decoded, context) - if (tool.output === undefined) { - if ("output" in result) return yield* Effect.die("Tool result declared output without an output schema") - return { - content: contentFrom(result.content), - ...(result.metadata === undefined ? {} : { metadata: result.metadata }), - } - } - if (!("output" in result)) - return yield* Effect.fail(new Failure({ message: "Tool did not return its declared output" })) - const encoded = yield* encodeOutput(tool.output, result.output) - return { - output: encoded, - content: contentFrom(result.content, encoded), - ...(result.metadata === undefined ? {} : { metadata: result.metadata }), - } - }) - -/** Model content from the tool's projection, falling back to the stringified encoded output. */ -const contentFrom = (projected: string | ReadonlyArray | undefined, encoded?: unknown): NonEmptyContent => { - if (typeof projected === "string") return [textContent(projected)] - if (projected !== undefined) { - const mapped = nonEmpty(projected.map(toModelContent)) - if (mapped !== undefined) return mapped - } - return [textContent(stringify(encoded))] -} - -export const toModelContent = (part: Content): ToolContent => - part.type === "text" - ? { type: "text", text: part.text } - : { type: "file", uri: `data:${part.mime};base64,${part.data}`, mime: part.mime, name: part.name } - -export const nonEmpty = (content: ReadonlyArray): NonEmptyContent | undefined => - content.length > 0 ? (content as NonEmptyContent) : undefined - -const textContent = (text: string): ToolContent => ({ type: "text", text }) - -/** Human-readable text for an arbitrary value; strings pass through unchanged. */ -export const stringify = (value: unknown) => { - if (typeof value === "string") return value - try { - return JSON.stringify(value) ?? String(value) - } catch { - return String(value) - } -} - -const MetadataSchema = Schema.Record(Schema.String, Schema.Json) - -/** Defensive boundary: non-JSON or oversized metadata is dropped, never failing the producing call. */ -export const jsonMetadata = (value: unknown, maxBytes?: number): Metadata | undefined => { - if (value === undefined) return undefined - const decoded = Schema.decodeUnknownOption(MetadataSchema)(value) - if (decoded._tag === "None") return undefined - if (maxBytes !== undefined && Buffer.byteLength(JSON.stringify(decoded.value), "utf-8") > maxBytes) return undefined - return decoded.value -} diff --git a/packages/core/src/tool/tools.ts b/packages/core/src/tool/tools.ts deleted file mode 100644 index 31ecc10cdf0..00000000000 --- a/packages/core/src/tool/tools.ts +++ /dev/null @@ -1,23 +0,0 @@ -export * as Tools from "./tools" - -import { Context, Effect, Scope } from "effect" -import { Tool } from "./tool" - -export type RegisterOptions = Tool.RegisterOptions - -export interface Interface { - readonly register: ( - tools: Readonly>, - options?: Tool.RegisterOptions, - ) => Effect.Effect - /** Internal atomic registration capability used by plugin transforms. */ - readonly registerBatch: ( - registrations: ReadonlyArray<{ - readonly tools: Readonly> - readonly options?: Tool.RegisterOptions - }>, - ) => Effect.Effect -} - -/** Narrow registration-only Location capability. */ -export class Service extends Context.Service()("@opencode/v2/Tools") {} diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 61a49f57cf4..afff5bdbd5b 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -7,8 +7,8 @@ import { ConfigMCPV1 } from "./mcp" import { ConfigPermissionV1 } from "./permission" import { ConfigProviderV1 } from "./provider" import { ConfigProviderOptionsV1 } from "./provider-options" -import { ProviderV2 } from "../../provider" -import { ModelV2 } from "../../model" +import { Provider } from "../../provider" +import { Model } from "../../model" const keys = new Set([ "logLevel", @@ -242,7 +242,7 @@ function migrateProvider(info: ConfigProviderV1.Info) { return { name: info.name, env: info.env, - package: info.npm ? ProviderV2.aisdk(info.npm) : undefined, + package: info.npm ? Provider.aisdk(info.npm) : undefined, settings: info.api ? { ...options.settings, baseURL: info.api } : options.settings, headers: info.options && options.headers, body: info.options && options.body, @@ -279,8 +279,8 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) { modelID: info.id, family: info.family, name: info.name, - compatibility: ModelV2.compatibility(info.interleaved), - package: info.provider?.npm ? ProviderV2.aisdk(info.provider.npm) : undefined, + compatibility: Model.compatibility(info.interleaved), + package: info.provider?.npm ? Provider.aisdk(info.provider.npm) : undefined, settings: info.provider?.api ? { ...settings, baseURL: info.provider.api } : settings, capabilities, headers: info.headers, diff --git a/packages/core/src/v2-schema.ts b/packages/core/src/v2-schema.ts deleted file mode 100644 index 4dfd51d92bd..00000000000 --- a/packages/core/src/v2-schema.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as V2Schema from "./v2-schema" - -export { DateTimeUtcFromMillis } from "@opencode-ai/schema/schema" diff --git a/packages/core/src/vcs.ts b/packages/core/src/vcs.ts index cb61e681f7e..308520bba0c 100644 --- a/packages/core/src/vcs.ts +++ b/packages/core/src/vcs.ts @@ -21,7 +21,7 @@ export interface Interface { readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/Vcs") {} +export class Service extends Context.Service()("@opencode/Vcs") {} // Adapter seam: one working-copy implementation per VCS type, selected by the // resolved location. Locations without a supported VCS degrade to empty diff --git a/packages/core/src/websearch.ts b/packages/core/src/websearch.ts index a1ac9107cd3..7f389bffce4 100644 --- a/packages/core/src/websearch.ts +++ b/packages/core/src/websearch.ts @@ -3,7 +3,7 @@ export * as WebSearch from "./websearch" import { WebSearch } from "@opencode-ai/schema/websearch" import { Context, Effect, Layer, Schema } from "effect" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { KV } from "./kv" import { State } from "./state" @@ -13,7 +13,7 @@ export type ID = WebSearch.ID export const Provider = WebSearch.Provider export type Provider = WebSearch.Provider -export const Event = WebSearch.Event +export { Event } from "@opencode-ai/schema/websearch" export const Input = WebSearch.Input export type Input = WebSearch.Input @@ -56,7 +56,7 @@ export interface Interface extends State.Transformable { readonly query: (input: Input) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/WebSearch") {} +export class Service extends Context.Service()("@opencode/WebSearch") {} type Data = { readonly providers: Map @@ -74,7 +74,7 @@ export type Draft = { const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const kv = yield* KV.Service const decodeResults = Schema.decodeUnknownEffect(Schema.Array(Result)) const state = State.create({ @@ -86,7 +86,7 @@ const layer = Layer.effect( set: (providerID) => (draft.defaultProviderID = providerID), }, }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + finalize: () => bus.publish(WebSearch.Event.Updated, {}).pipe(Effect.asVoid), }) const requireProvider = (providers: Map, providerID: ID) => { @@ -140,5 +140,5 @@ const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [EventV2.node, KV.node], + deps: [Bus.node, KV.node], }) diff --git a/packages/core/src/wellknown.ts b/packages/core/src/wellknown.ts index 78a21e74800..db5862a9488 100644 --- a/packages/core/src/wellknown.ts +++ b/packages/core/src/wellknown.ts @@ -6,7 +6,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { isDeepStrictEqual } from "node:util" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" -import { EventV2 } from "./event" +import { Bus } from "./bus" import { KV } from "./kv" export interface Auth extends Schema.Schema.Type {} @@ -51,10 +51,10 @@ export interface Interface { readonly resolve: (entry: Entry, variables: Readonly>) => Effect.Effect } -export class Service extends Context.Service()("@opencode/v2/WellKnown") {} +export class Service extends Context.Service()("@opencode/WellKnown") {} export const Event = { - Updated: EventV2.ephemeral({ type: "wellknown.updated", schema: {} }), + Updated: Bus.ephemeral({ type: "wellknown.updated", schema: {} }), } export const inspect = Effect.fn("WellKnown.inspect")(function* (origin: string) { @@ -103,7 +103,7 @@ const layer = Layer.effect( Effect.gen(function* () { const http = yield* HttpClient.HttpClient const kv = yield* KV.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const cache = yield* Ref.make(new Map()) const lock = Semaphore.makeUnsafe(1) @@ -139,7 +139,7 @@ const layer = Layer.effect( const changed = !isDeepStrictEqual(Ref.getUnsafe(cache), next) if (!changed) return false yield* Ref.set(cache, next) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) return true }), ) @@ -160,7 +160,7 @@ const layer = Layer.effect( const origins = Schema.is(Sources)(sources) ? sources : [] yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin]))) yield* Ref.update(cache, (current) => new Map(current).set(origin, entry)) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) return entry }), ) @@ -180,7 +180,7 @@ const layer = Layer.effect( next.delete(origin) return next }) - yield* events.publish(Event.Updated, {}) + yield* bus.publish(Event.Updated, {}) }), ) }), @@ -191,4 +191,4 @@ const layer = Layer.effect( }), ) -export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, KV.node, EventV2.node] }) +export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, KV.node, Bus.node] }) diff --git a/packages/core/src/wellknown/plugin.ts b/packages/core/src/wellknown/plugin.ts index 9be129a248e..3e1f7dbed34 100644 --- a/packages/core/src/wellknown/plugin.ts +++ b/packages/core/src/wellknown/plugin.ts @@ -1,14 +1,14 @@ export * as WellKnownPlugin from "./plugin" -import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { define } from "@opencode-ai/plugin/effect/plugin" import { Effect, Stream } from "effect" -import { EventV2 } from "../event" +import { Bus } from "../bus" import { WellKnown } from "../wellknown" export const Plugin = define({ id: "opencode.wellknown", effect: Effect.fn(function* (ctx) { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const wellknown = yield* WellKnown.Service yield* wellknown.entries().pipe(Effect.orDie) yield* ctx.integration.transform((draft) => { @@ -28,7 +28,7 @@ export const Plugin = define({ }) }) }) - yield* events.subscribe(WellKnown.Event.Updated).pipe( + yield* bus.subscribe(WellKnown.Event.Updated).pipe( Stream.runForEach(() => ctx.integration.reload()), Effect.forkScoped({ startImmediately: true }), ) diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index d85bbe4ba47..4cf18c32ffd 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -1,4 +1,4 @@ -export * as WorkspaceV2 from "./workspace" +export * as Workspace from "./workspace" import { Workspace } from "@opencode-ai/schema/workspace" diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index 88bf879b602..41aeee99eca 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -1,12 +1,12 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Fiber, Layer, Scope, Stream } from "effect" import { TestClock } from "effect/testing" -import { AgentV2 } from "@opencode-ai/core/agent" -import { EventV2 } from "@opencode-ai/core/event" +import { Agent } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AgentPlugin } from "@opencode-ai/core/plugin/agent" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "./fixture/location" @@ -17,22 +17,22 @@ const testLocation = location({ directory: AbsolutePath.make("/project") }) const locationLayer = Layer.succeed(Location.Service, Location.Service.of(testLocation)) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([AgentV2.node, EventV2.node, Location.node]), [ + AppNodeBuilder.build(LayerNode.group([Agent.node, Bus.node, Location.node]), [ [Location.node, locationLayer], ]) as unknown as Layer.Layer, ) -describe("AgentV2", () => { +describe("Agent", () => { it.effect("publishes an updated event after agent changes", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const events = yield* EventV2.Service - const updated = yield* events - .subscribe(AgentV2.Event.Updated) + const agent = yield* Agent.Service + const bus = yield* Bus.Service + const updated = yield* bus + .subscribe(Agent.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* agent.transform((editor) => editor.update(AgentV2.ID.make("reviewer"), () => {})) + yield* agent.transform((editor) => editor.update(Agent.ID.make("reviewer"), () => {})) expect(yield* Fiber.join(updated)).toMatchObject([{ location: { directory: testLocation.directory } }]) }), @@ -40,17 +40,17 @@ describe("AgentV2", () => { it.effect("starts without agents", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service expect(yield* agent.list()).toEqual([]) - expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined() + expect(yield* agent.get(Agent.ID.make("build"))).toBeUndefined() }), ) it.effect("materializes replayable agent transforms", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("reviewer") + const agent = yield* Agent.Service + const id = Agent.ID.make("reviewer") yield* agent.transform((editor) => editor.update(id, (info) => { info.description = "Reviews code" @@ -65,8 +65,8 @@ describe("AgentV2", () => { it.effect("rebuilds state when a transform is replaced", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("reviewer") + const agent = yield* Agent.Service + const id = Agent.ID.make("reviewer") let description = "Old description" let hidden = true yield* agent.transform((editor) => @@ -87,8 +87,8 @@ describe("AgentV2", () => { it.effect("removes a transform when its scope closes", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("scoped") + const agent = yield* Agent.Service + const id = Agent.ID.make("scoped") const scope = yield* Scope.make() yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope)) expect(yield* agent.get(id)).toBeDefined() @@ -100,8 +100,8 @@ describe("AgentV2", () => { it.effect("applies direct agent updates", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("build") + const agent = yield* Agent.Service + const id = Agent.ID.make("build") yield* agent.transform((editor) => editor.update(id, (info) => { @@ -116,11 +116,11 @@ describe("AgentV2", () => { it.effect("creates agents with runtime defaults and supports direct removal", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service - const id = AgentV2.ID.make("custom") + const agent = yield* Agent.Service + const id = Agent.ID.make("custom") yield* agent.transform((editor) => editor.update(id, () => {})) - expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id)) + expect(yield* agent.get(id)).toEqual(Agent.Info.empty(id)) yield* agent.transform((editor) => editor.remove(id)) expect(yield* agent.get(id)).toBeUndefined() @@ -129,7 +129,7 @@ describe("AgentV2", () => { it.effect("does not ambiently opt built-in agents into bash", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* AgentPlugin.Plugin.effect( host({ agent: agentHost(agent), @@ -151,7 +151,7 @@ describe("AgentV2", () => { "summary", "title", ]) - expect((yield* agent.get(AgentV2.defaultID))?.system).toBeUndefined() + expect((yield* agent.get(Agent.defaultID))?.system).toBeUndefined() for (const item of agents) { expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false) } @@ -160,7 +160,7 @@ describe("AgentV2", () => { it.effect("denies the subagent tool for built-in subagents", () => Effect.gen(function* () { - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* AgentPlugin.Plugin.effect( host({ agent: agentHost(agent), @@ -174,11 +174,11 @@ describe("AgentV2", () => { yield* Effect.forEach(["general", "explore"], (id) => Effect.gen(function* () { - const info = yield* agent.get(AgentV2.ID.make(id)) + const info = yield* agent.get(Agent.ID.make(id)) if (!info) throw new Error(`expected built-in agent: ${id}`) expect(info.mode).toBe("subagent") expect(info.permissions).toContainEqual({ action: "subagent", resource: "*", effect: "deny" }) - expect(PermissionV2.evaluate("subagent", "*", info.permissions).effect).toBe("deny") + expect(Permission.evaluate("subagent", "*", info.permissions).effect).toBe("deny") }), ) }), diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 03f05c65347..87a02d1b3ce 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -1,7 +1,7 @@ import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart } from "@ai-sdk/provider" import { AISDK } from "@opencode-ai/core/aisdk" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { LLM, LLMError, LLMEvent, Message } from "@opencode-ai/ai" import { LLMClient, RequestExecutor } from "@opencode-ai/ai/route" import { expect } from "bun:test" @@ -11,10 +11,10 @@ import { testEffect } from "./lib/effect" const it = testEffect(AISDK.locationLayer) const model = (packageName: string, settings: Record = {}) => - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("catalog-model")), - modelID: ModelV2.ID.make("api-model"), - package: ProviderV2.aisdk(packageName), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("test-provider"), Model.ID.make("catalog-model")), + modelID: Model.ID.make("api-model"), + package: Provider.aisdk(packageName), settings, limit: { context: 100, output: 20 }, }) @@ -185,7 +185,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () => gateway: { order: ["anthropic"] }, thinking: { type: "adaptive" }, }), - modelID: ModelV2.ID.make("anthropic/claude-sonnet-5"), + modelID: Model.ID.make("anthropic/claude-sonnet-5"), }) const anthropicPrepared = yield* LLMClient.prepare( LLM.request({ model: anthropic, prompt: "Hello" }), @@ -197,7 +197,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () => const bedrock = yield* aisdk.model({ ...model("@ai-sdk/gateway", { reasoningConfig: { type: "enabled" } }), - modelID: ModelV2.ID.make("amazon/nova-2-lite"), + modelID: Model.ID.make("amazon/nova-2-lite"), }) const bedrockPrepared = yield* LLMClient.prepare( LLM.request({ model: bedrock, prompt: "Hello" }), @@ -208,7 +208,7 @@ it.effect("routes AI Gateway model options by upstream prefix", () => const fallback = yield* aisdk.model({ ...model("@ai-sdk/gateway", { reasoningEffort: "high" }), - modelID: ModelV2.ID.make("deepseek/deepseek-v4"), + modelID: Model.ID.make("deepseek/deepseek-v4"), }) const fallbackPrepared = yield* LLMClient.prepare( LLM.request({ model: fallback, prompt: "Hello" }), diff --git a/packages/core/test/event.test.ts b/packages/core/test/bus.test.ts similarity index 64% rename from packages/core/test/event.test.ts rename to packages/core/test/bus.test.ts index 5fa89af8972..447afae7e63 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/bus.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Ref, Schema, Stream } from "effect" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Event } from "@opencode-ai/schema/event" import { Session } from "@opencode-ai/schema/session" import { SessionEvent } from "@opencode-ai/schema/session-event" @@ -11,7 +11,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { eq } from "drizzle-orm" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" @@ -19,17 +19,17 @@ import { testEffect } from "./lib/effect" const locationLayer = Layer.succeed( Location.Service, Location.Service.of( - location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }), + location({ directory: AbsolutePath.make("project"), workspaceID: Workspace.ID.make("wrk_test") }), ), ) -const Message = EventV2.ephemeral({ +const Message = Bus.ephemeral({ type: "test.message", schema: { text: Schema.String, }, }) -const SyncMessage = EventV2.durable({ +const SyncMessage = Bus.durable({ type: "test.sync", durable: { version: 1, @@ -41,7 +41,7 @@ const SyncMessage = EventV2.durable({ }, }) -const SyncSent = EventV2.durable({ +const SyncSent = Bus.durable({ type: "test.sent", durable: { version: 1, @@ -53,31 +53,31 @@ const SyncSent = EventV2.durable({ }, }) -const VersionedMessageV1 = EventV2.durable({ +const VersionedMessageV1 = Bus.durable({ type: "test.versioned", durable: { version: 1, aggregate: "id" }, schema: { id: Schema.String }, }) -const VersionedMessageV2 = EventV2.durable({ +const VersionedMessageV2 = Bus.durable({ type: "test.versioned", durable: { version: 2, aggregate: "id" }, schema: { id: Schema.String }, }) -const GlobalMessage = EventV2.ephemeral({ +const GlobalMessage = Bus.ephemeral({ type: "test.global", schema: { text: Schema.String, }, }) -const CountMessage = EventV2.ephemeral({ +const CountMessage = Bus.ephemeral({ type: "test.count", schema: { count: Schema.Number, }, }) -const VersionedMessage = EventV2.durable({ +const VersionedMessage = Bus.durable({ type: "test.versioned", durable: { version: 2, @@ -96,27 +96,27 @@ const durableData = (sessionID: Session.ID, text: string) => ({ }) /** Followed log read without markers: the old `durable` stream shape. */ -const tail = (events: EventV2.Interface, input: { aggregateID: string; after?: number }) => - events.log({ ...input, follow: true }).pipe(Stream.filter((item): item is EventV2.Payload => !EventV2.isSynced(item))) +const tail = (bus: Bus.Interface, input: { aggregateID: string; after?: number }) => + bus.log({ ...input, follow: true }).pipe(Stream.filter((item): item is Event.Payload => !Bus.isSynced(item))) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, Location.node]), [[Location.node, locationLayer]]), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, Location.node]), [[Location.node, locationLayer]]), ) -const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node]))) +const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node]))) -describe("EventV2", () => { +describe("Bus", () => { it.effect("subscribes to multiple event definitions with a discriminated payload union", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service // @ts-expect-error multi-definition subscriptions require at least one definition - events.subscribe([]) - const fiber = yield* events + bus.subscribe([]) + const fiber = yield* bus .subscribe([Message, CountMessage]) .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* events.publish(Message, { text: "hello" }) - yield* events.publish(CountMessage, { count: 2 }) + yield* bus.publish(Message, { text: "hello" }) + yield* bus.publish(CountMessage, { count: 2 }) const received = Array.from(yield* Fiber.join(fiber)).map((event) => event.type === "test.message" ? event.data.text : event.data.count, @@ -127,10 +127,10 @@ describe("EventV2", () => { it.effect("publishes events with the current location", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const fiber = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const bus = yield* Bus.Service + const fiber = yield* bus.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - const event = yield* events.publish(Message, { text: "hello" }) + const event = yield* bus.publish(Message, { text: "hello" }) const received = Array.from(yield* Fiber.join(fiber)) expect(received).toEqual([event]) @@ -139,15 +139,15 @@ describe("EventV2", () => { expect(event.data).toEqual({ text: "hello" }) expect(event.location).toEqual({ directory: AbsolutePath.make("project"), - workspaceID: WorkspaceV2.ID.make("wrk_test"), + workspaceID: Workspace.ID.make("wrk_test"), }) }), ) itWithoutLocation.effect("omits location when no location is available", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const event = yield* events.publish(GlobalMessage, { text: "hello" }) + const bus = yield* Bus.Service + const event = yield* bus.publish(GlobalMessage, { text: "hello" }) expect(event).not.toHaveProperty("location") expect(event.type).toBe("test.global") @@ -156,22 +156,22 @@ describe("EventV2", () => { it.effect("publishes definition version", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const event = yield* events.publish(VersionedMessage, { id: "one", text: "hello" }) + const bus = yield* Bus.Service + const event = yield* bus.publish(VersionedMessage, { id: "one", text: "hello" }) expect(event.type).toBe("test.versioned") - expect(event.durable?.version).toBe(EventV2.Version.make(2)) + expect(event.durable?.version).toBe(Event.Version.make(2)) }), ) it.effect("selects the latest durable definition independent of declaration order", () => Effect.sync(() => { - const latest = EventV2.durable({ + const latest = Bus.durable({ type: "test.out-of-order", durable: { version: 2, aggregate: "id" }, schema: { id: Schema.String }, }) - const historical = EventV2.durable({ + const historical = Bus.durable({ type: "test.out-of-order", durable: { version: 1, aggregate: "id" }, schema: { id: Schema.String }, @@ -184,11 +184,11 @@ describe("EventV2", () => { it.effect("publishes to typed and wildcard subscriptions", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) - const wildcard = yield* events.subscribe().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const bus = yield* Bus.Service + const typed = yield* bus.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const wildcard = yield* bus.subscribe().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - const event = yield* events.publish(Message, { text: "hello" }) + const event = yield* bus.publish(Message, { text: "hello" }) expect(Array.from(yield* Fiber.join(typed))).toEqual([event]) expect(Array.from(yield* Fiber.join(wildcard))).toEqual([event]) @@ -197,16 +197,16 @@ describe("EventV2", () => { it.effect("runs projectors inline", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() - yield* events.project(SyncMessage, (event) => + const bus = yield* Bus.Service + const received = new Array() + yield* bus.project(SyncMessage, (event) => Effect.sync(() => { received.push(event) }), ) - const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" }) - yield* events.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) + const event = yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) + yield* bus.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) expect(received[0]).toEqual(event) expect(received[1]?.data).toEqual({ id: "one", text: "after unsubscribe" }) @@ -215,12 +215,12 @@ describe("EventV2", () => { it.effect("commits local operational state inside a new durable event transaction", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - const aggregateID = EventV2.ID.create() - yield* events.project(SyncMessage, () => Effect.sync(() => received.push("projector"))) + const aggregateID = Event.ID.create() + yield* bus.project(SyncMessage, () => Effect.sync(() => received.push("projector"))) - yield* events.publish( + yield* bus.publish( SyncMessage, { id: aggregateID, text: "hello" }, { commit: (seq) => Effect.sync(() => received.push(`commit:${seq}`)) }, @@ -232,16 +232,16 @@ describe("EventV2", () => { it.effect("rolls back the durable event and projector when the local commit fails", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() yield* db.run("CREATE TABLE IF NOT EXISTS event_commit_probe (value text NOT NULL)") yield* db.run("DELETE FROM event_commit_probe") - yield* events.project(SyncMessage, () => + yield* bus.project(SyncMessage, () => db.run("INSERT INTO event_commit_probe (value) VALUES ('projected')").pipe(Effect.orDie, Effect.asVoid), ) - const exit = yield* events + const exit = yield* bus .publish(SyncMessage, { id: aggregateID, text: "hello" }, { commit: () => Effect.die("commit failed") }) .pipe(Effect.exit) @@ -256,8 +256,8 @@ describe("EventV2", () => { it.effect("rejects local commit hooks on live-only events", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const exit = yield* events.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit) + const bus = yield* Bus.Service + const exit = yield* bus.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit) expect(String(exit)).toContain("Local commit hooks require a durable event") }), @@ -265,21 +265,21 @@ describe("EventV2", () => { it.effect("runs projectors before publishing to streams", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - const fiber = yield* events.subscribe().pipe( + const fiber = yield* bus.subscribe().pipe( Stream.take(1), Stream.runForEach(() => Effect.sync(() => received.push("stream"))), Effect.forkScoped, ) - yield* events.project(SyncMessage, (event) => + yield* bus.project(SyncMessage, (event) => Effect.sync(() => { received.push(event.type) }), ) yield* Effect.yieldNow - yield* events.publish(SyncMessage, { id: "one", text: "hello" }) + yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) yield* Fiber.join(fiber) expect(received).toEqual([SyncMessage.type, "stream"]) @@ -288,22 +288,22 @@ describe("EventV2", () => { it.effect("runs listeners inline after projectors", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - yield* events.project(SyncMessage, () => + yield* bus.project(SyncMessage, () => Effect.sync(() => { received.push("projector") }), ) - const unsubscribe = yield* events.listen(() => + const unsubscribe = yield* bus.listen(() => Effect.sync(() => { received.push("listener") }), ) - yield* events.publish(SyncMessage, { id: "one", text: "hello" }) + yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) yield* unsubscribe - yield* events.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) + yield* bus.publish(SyncMessage, { id: "one", text: "after unsubscribe" }) expect(received).toEqual(["projector", "listener", "projector"]) }), @@ -311,18 +311,18 @@ describe("EventV2", () => { it.effect("isolates observer defects after durable events commit", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const received = new Array() - yield* events.listen(() => { + yield* bus.listen(() => { throw new Error("listener defect") }) - yield* events.listen((event) => + yield* bus.listen((event) => Effect.sync(() => { received.push(event.type) }), ) - const event = yield* events.publish(SyncMessage, { id: "one", text: "hello" }) + const event = yield* bus.publish(SyncMessage, { id: "one", text: "hello" }) expect(received).toEqual([SyncMessage.type]) expect(event.durable?.seq).toBeNumber() @@ -331,11 +331,11 @@ describe("EventV2", () => { it.effect("notifies global listeners only after a durable event is committed", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() const observed = new Array<{ id: string; seq: number }>() - yield* events.listen((event) => + yield* bus.listen((event) => event.type !== SyncMessage.type ? Effect.void : db @@ -354,7 +354,7 @@ describe("EventV2", () => { ), ) - const event = yield* events.publish(SyncMessage, { id: aggregateID, text: "committed" }) + const event = yield* bus.publish(SyncMessage, { id: aggregateID, text: "committed" }) if (!event.durable) throw new Error("Expected durable event metadata") expect(observed).toEqual([{ id: event.id, seq: event.durable.seq }]) @@ -363,11 +363,11 @@ describe("EventV2", () => { it.effect("preserves observer interruption", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - yield* events.listen(() => Effect.interrupt) + yield* bus.listen(() => Effect.interrupt) - const exit = yield* events.publish(SyncMessage, { id: "interrupted", text: "hello" }).pipe(Effect.exit) + const exit = yield* bus.publish(SyncMessage, { id: "interrupted", text: "hello" }).pipe(Effect.exit) const committed = yield* db .select({ id: EventTable.id }) .from(EventTable) @@ -382,21 +382,21 @@ describe("EventV2", () => { it.effect("keeps live-only listener defects fail-fast", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const defect = new Error("listener defect") - yield* events.listen(() => Effect.die(defect)) + yield* bus.listen(() => Effect.die(defect)) - expect(yield* events.publish(Message, { text: "hello" }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) + expect(yield* bus.publish(Message, { text: "hello" }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) }), ) it.effect("inserts durable event rows on publish", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "first" }) + yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" }) const rows = yield* db .select() .from(EventTable) @@ -405,19 +405,19 @@ describe("EventV2", () => { .pipe(Effect.orDie) expect(rows).toHaveLength(1) - expect(rows[0]?.type).toBe(EventV2.versionedType(SyncMessage.type, 1)) + expect(rows[0]?.type).toBe(Bus.versionedType(SyncMessage.type, 1)) expect(rows[0]?.aggregate_id).toBe(aggregateID) }), ) it.effect("increments durable event seq per aggregate", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "first" }) - yield* events.publish(SyncMessage, { id: aggregateID, text: "second" }) + yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" }) + yield* bus.publish(SyncMessage, { id: aggregateID, text: "second" }) const rows = yield* db .select() .from(EventTable) @@ -431,18 +431,18 @@ describe("EventV2", () => { it.effect("replays durable aggregate events after a sequence and tails new events", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) - const fiber = yield* tail(events, { aggregateID, after: 0 }).pipe( + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) + const fiber = yield* tail(bus, { aggregateID, after: 0 }).pipe( Stream.take(2), Stream.runCollect, Effect.forkScoped, ) yield* Effect.yieldNow - yield* events.publish(DurableMessage, durableData(aggregateID, "two")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "two")) expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ [1, durableData(aggregateID, "one")], @@ -453,12 +453,12 @@ describe("EventV2", () => { it.effect("catches durable aggregate events published during replay handoff", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ [0, durableData(aggregateID, "zero")], @@ -472,7 +472,7 @@ describe("EventV2", () => { const readStarted = yield* Deferred.make() const continueRead = yield* Deferred.make() let pause = true - const eventLayer = EventV2.layerWith({ + const eventLayer = Bus.layerWith({ beforeAggregateRead: () => pause ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead))) @@ -480,13 +480,13 @@ describe("EventV2", () => { }).pipe(Layer.provide(LayerNode.compile(Database.node))) yield* Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Deferred.await(readStarted) pause = false - yield* events.publish(DurableMessage, durableData(aggregateID, "during handoff")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "during handoff")) yield* Deferred.succeed(continueRead, undefined) expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([ @@ -498,14 +498,14 @@ describe("EventV2", () => { it.effect("coalesces durable aggregate wakes while draining every committed event", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() const count = 64 - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow for (let index = 0; index < count; index++) { - yield* events.publish(DurableMessage, durableData(aggregateID, String(index))) + yield* bus.publish(DurableMessage, durableData(aggregateID, String(index))) } expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual( @@ -516,13 +516,13 @@ describe("EventV2", () => { it.effect("omits live-only events from durable aggregate streams", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const fiber = yield* tail(events, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* events.publish(Message, { text: "live only" }) - yield* events.publish(DurableMessage, durableData(aggregateID, "durable")) + yield* bus.publish(Message, { text: "live only" }) + yield* bus.publish(DurableMessage, durableData(aggregateID, "durable")) expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type]) }), @@ -530,11 +530,11 @@ describe("EventV2", () => { it.effect("uses custom sync aggregate field", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncSent, { messageID: aggregateID, text: "sent" }) + yield* bus.publish(SyncSent, { messageID: aggregateID, text: "sent" }) const rows = yield* db .select() .from(EventTable) @@ -549,19 +549,19 @@ describe("EventV2", () => { it.effect("replays durable events through projectors", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() - yield* events.project(DurableMessage, (event) => + const bus = yield* Bus.Service + const received = new Array() + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) const aggregateID = Session.ID.create() - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "hello"), @@ -574,14 +574,14 @@ describe("EventV2", () => { it.effect("replay inserts external event rows", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "replayed"), @@ -602,23 +602,23 @@ describe("EventV2", () => { "replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const envelopeAggregateID = Session.ID.create() const payloadAggregateID = Session.ID.create() - const received = new Array() - yield* events.publish(DurableMessage, durableData(payloadAggregateID, "seed")) - yield* events.project(DurableMessage, (event) => + const received = new Array() + yield* bus.publish(DurableMessage, durableData(payloadAggregateID, "seed")) + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - const exit = yield* events + const exit = yield* bus .replay({ - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID: envelopeAggregateID, data: durableData(payloadAggregateID, "replayed"), @@ -646,22 +646,22 @@ describe("EventV2", () => { it.effect("replay defects on sequence mismatch", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "first"), }) - const exit = yield* events + const exit = yield* bus .replay({ - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 5, aggregateID, data: durableData(aggregateID, "bad"), @@ -674,19 +674,19 @@ describe("EventV2", () => { it.effect("replay decodes synchronized transformed values before projection", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() const received = new Array() - yield* events.project(SessionEvent.InstructionsUpdated, (event) => + yield* bus.project(SessionEvent.InstructionsUpdated, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(SessionEvent.InstructionsUpdated.type, 2), + type: Bus.versionedType(SessionEvent.InstructionsUpdated.type, 2), seq: 0, aggregateID, data: { sessionID: aggregateID, delta: { "core/context": "0".repeat(64) } }, @@ -698,33 +698,33 @@ describe("EventV2", () => { it.effect("dispatches durable projectors by exact event version", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() const received = new Array() - yield* events.project(VersionedMessageV2, (event) => + yield* bus.project(VersionedMessageV2, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.publish(VersionedMessageV1, { id: aggregateID }) - yield* events.publish(VersionedMessageV2, { id: aggregateID }) + yield* bus.publish(VersionedMessageV1, { id: aggregateID }) + yield* bus.publish(VersionedMessageV2, { id: aggregateID }) expect(received).toHaveLength(1) - expect(received[0]?.durable.version).toBe(EventV2.Version.make(2)) + expect(received[0]?.durable.version).toBe(Event.Version.make(2)) }), ) it.effect("replay defects on unknown event type", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const exit = yield* events + const bus = yield* Bus.Service + const exit = yield* bus .replay({ - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), type: "unknown.event.1", seq: 0, - aggregateID: EventV2.ID.create(), + aggregateID: Event.ID.create(), data: {}, }) .pipe(Effect.exit) @@ -735,21 +735,21 @@ describe("EventV2", () => { it.effect("replayAll validates contiguous aggregate events", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const source = yield* events.replayAll([ + const source = yield* bus.replayAll([ { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "one"), }, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "two"), @@ -762,41 +762,41 @@ describe("EventV2", () => { it.effect("replayAll accepts later chunks after the first batch", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - const one = yield* events.replayAll([ + const one = yield* bus.replayAll([ { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "one"), }, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "two"), }, ]) - const two = yield* events.replayAll([ + const two = yield* bus.replayAll([ { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 2, aggregateID, data: durableData(aggregateID, "three"), }, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 3, aggregateID, data: durableData(aggregateID, "four"), @@ -817,22 +817,22 @@ describe("EventV2", () => { it.effect("claim fences replay owners", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) - yield* events.claim(aggregateID, "owner-a") - yield* events.project(DurableMessage, (event) => + yield* bus.publish(DurableMessage, durableData(aggregateID, "seed")) + yield* bus.claim(aggregateID, "owner-a") + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "ignored"), @@ -846,20 +846,20 @@ describe("EventV2", () => { it.effect("strict owner fences exact replay", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const id = EventV2.ID.create() + const id = Event.ID.create() const replayed = { id, created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "owned"), } - yield* events.replay(replayed, { ownerID: "owner-a" }) + yield* bus.replay(replayed, { ownerID: "owner-a" }) - const exit = yield* events.replay(replayed, { ownerID: "owner-b", strictOwner: true }).pipe(Effect.exit) + const exit = yield* bus.replay(replayed, { ownerID: "owner-b", strictOwner: true }).pipe(Effect.exit) expect(String(exit)).toContain("Replay owner mismatch") }), @@ -867,20 +867,20 @@ describe("EventV2", () => { it.effect("exact replay claims an unowned aggregate", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - const published = yield* events.publish(DurableMessage, durableData(aggregateID, "owned")) + const published = yield* bus.publish(DurableMessage, durableData(aggregateID, "owned")) const replayed = { id: published.id, created: published.created, - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: published.durable!.seq, aggregateID, data: published.data, } - yield* events.replay(replayed, { ownerID: "owner-a", strictOwner: true }) + yield* bus.replay(replayed, { ownerID: "owner-a", strictOwner: true }) const row = yield* db .select({ ownerID: EventSequenceTable.owner_id }) .from(EventSequenceTable) @@ -889,9 +889,9 @@ describe("EventV2", () => { .pipe(Effect.orDie) expect(row?.ownerID).toBe("owner-a") - const exit = yield* events + const exit = yield* bus .replay( - { ...replayed, id: EventV2.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") }, + { ...replayed, id: Event.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") }, { ownerID: "owner-b", strictOwner: true }, ) .pipe(Effect.exit) @@ -901,15 +901,15 @@ describe("EventV2", () => { it.effect("replay with owner claims an unowned sequence", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "owned"), @@ -929,27 +929,27 @@ describe("EventV2", () => { it.effect("replay claims an existing unowned sequence before fencing a different owner", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "local")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "local")) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "claimed"), }, { ownerID: "owner-1" }, ) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 2, aggregateID, data: durableData(aggregateID, "fenced"), @@ -976,13 +976,13 @@ describe("EventV2", () => { it.effect("strict replay rejects an owner conflict instead of silently skipping it", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "claimed"), @@ -990,12 +990,12 @@ describe("EventV2", () => { { ownerID: "owner-1" }, ) - const exit = yield* events + const exit = yield* bus .replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "conflict"), @@ -1010,21 +1010,21 @@ describe("EventV2", () => { it.effect("publishes accepted replay with its durable sequence and suppresses stale replay", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() - yield* events.listen((event) => Effect.sync(() => received.push(event))) + yield* bus.listen((event) => Effect.sync(() => received.push(event))) const replayed = { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "replayed"), } - yield* events.replay(replayed, { publish: true }) - yield* events.replay(replayed, { publish: true }) + yield* bus.replay(replayed, { publish: true }) + yield* bus.replay(replayed, { publish: true }) expect(received).toMatchObject([{ id: replayed.id, durable: { seq: 0, version: 1 }, data: replayed.data }]) }), @@ -1032,21 +1032,21 @@ describe("EventV2", () => { it.effect("rejects divergent stale replay without publishing it", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() const replayed = { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "original"), } - yield* events.listen((event) => Effect.sync(() => received.push(event))) - yield* events.replay(replayed, { publish: true }) + yield* bus.listen((event) => Effect.sync(() => received.push(event))) + yield* bus.replay(replayed, { publish: true }) - const exit = yield* events + const exit = yield* bus .replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true }) .pipe(Effect.exit) @@ -1057,23 +1057,23 @@ describe("EventV2", () => { it.effect("rejects an event ID reused at another aggregate position", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const id = EventV2.ID.create() - yield* events.replay({ + const id = Event.ID.create() + yield* bus.replay({ id, created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "first"), }) - const exit = yield* events + const exit = yield* bus .replay({ id, created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "second"), @@ -1086,28 +1086,28 @@ describe("EventV2", () => { it.effect("replay from a different owner leaves claimed sequence unchanged", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const aggregateID = Session.ID.create() - const received = new Array() - yield* events.listen((event) => Effect.sync(() => received.push(event))) + const received = new Array() + yield* bus.listen((event) => Effect.sync(() => received.push(event))) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "first"), }, { ownerID: "owner-1" }, ) - yield* events.replay( + yield* bus.replay( { - id: EventV2.ID.create(), + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 1, aggregateID, data: durableData(aggregateID, "ignored"), @@ -1135,13 +1135,13 @@ describe("EventV2", () => { it.effect("claim updates the event sequence owner", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const aggregateID = EventV2.ID.create() + const aggregateID = Event.ID.create() - yield* events.publish(SyncMessage, { id: aggregateID, text: "claimed" }) - yield* events.claim(aggregateID, "owner-1") - yield* events.claim(aggregateID, "owner-2") + yield* bus.publish(SyncMessage, { id: aggregateID, text: "claimed" }) + yield* bus.claim(aggregateID, "owner-1") + yield* bus.claim(aggregateID, "owner-2") const row = yield* db .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) .from(EventSequenceTable) @@ -1155,21 +1155,21 @@ describe("EventV2", () => { it.effect("remove clears durable event sequence", () => Effect.gen(function* () { - const events = yield* EventV2.Service - const received = new Array() + const bus = yield* Bus.Service + const received = new Array() const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "seed")) - yield* events.remove(aggregateID) - yield* events.project(DurableMessage, (event) => + yield* bus.publish(DurableMessage, durableData(aggregateID, "seed")) + yield* bus.remove(aggregateID) + yield* bus.project(DurableMessage, (event) => Effect.sync(() => { received.push(event) }), ) - yield* events.replay({ - id: EventV2.ID.create(), + yield* bus.replay({ + id: Event.ID.create(), created: DateTime.makeUnsafe(0), - type: EventV2.versionedType(DurableMessage.type, 1), + type: Bus.versionedType(DurableMessage.type, 1), seq: 0, aggregateID, data: durableData(aggregateID, "replayed"), @@ -1181,82 +1181,82 @@ describe("EventV2", () => { it.effect("log without follow replays events and completes with a synced marker", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) - const items = Array.from(yield* Stream.runCollect(events.log({ aggregateID }))) + const items = Array.from(yield* Stream.runCollect(bus.log({ aggregateID }))) - expect(items.map((item) => (EventV2.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - EventV2.Seq.make(1), + expect(items.map((item) => (Bus.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + Event.Seq.make(1), "log.synced", ]) - expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: EventV2.Seq.make(1) }) + expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: Event.Seq.make(1) }) }), ) it.effect("log synced marker omits seq for an empty log and keeps the cursor otherwise", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - const empty = Array.from(yield* Stream.runCollect(events.log({ aggregateID }))) - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const drained = Array.from(yield* Stream.runCollect(events.log({ aggregateID, after: 0 }))) + const empty = Array.from(yield* Stream.runCollect(bus.log({ aggregateID }))) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const drained = Array.from(yield* Stream.runCollect(bus.log({ aggregateID, after: 0 }))) expect(empty).toEqual([{ type: "log.synced", aggregateID }]) expect(empty[0]).not.toHaveProperty("seq") - expect(drained).toEqual([{ type: "log.synced", aggregateID, seq: EventV2.Seq.make(0) }]) + expect(drained).toEqual([{ type: "log.synced", aggregateID, seq: Event.Seq.make(0) }]) }), ) it.effect("log with follow emits the synced marker at the replay-to-live boundary", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const fiber = yield* events + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const fiber = yield* bus .log({ aggregateID, follow: true }) .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) const items = Array.from(yield* Fiber.join(fiber)) - expect(items.map((item) => (EventV2.isSynced(item) ? item : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - { type: "log.synced", aggregateID, seq: EventV2.Seq.make(0) }, - EventV2.Seq.make(1), + expect(items.map((item) => (Bus.isSynced(item) ? item : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + { type: "log.synced", aggregateID, seq: Event.Seq.make(0) }, + Event.Seq.make(1), ]) }), ) it.effect("log replays across configured read pages", () => Effect.gen(function* () { - const eventLayer = EventV2.layerWith({ logReadPageSize: 2 }).pipe(Layer.provide(LayerNode.compile(Database.node))) + const eventLayer = Bus.layerWith({ logReadPageSize: 2 }).pipe(Layer.provide(LayerNode.compile(Database.node))) yield* Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) - yield* events.publish(DurableMessage, durableData(aggregateID, "two")) - yield* events.publish(DurableMessage, durableData(aggregateID, "three")) - yield* events.publish(DurableMessage, durableData(aggregateID, "four")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "two")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "three")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "four")) - const items = Array.from(yield* Stream.runCollect(events.log({ aggregateID }))) + const items = Array.from(yield* Stream.runCollect(bus.log({ aggregateID }))) - expect(items.map((item) => (EventV2.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - EventV2.Seq.make(1), - EventV2.Seq.make(2), - EventV2.Seq.make(3), - EventV2.Seq.make(4), + expect(items.map((item) => (Bus.isSynced(item) ? item.type : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + Event.Seq.make(1), + Event.Seq.make(2), + Event.Seq.make(3), + Event.Seq.make(4), "log.synced", ]) - expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: EventV2.Seq.make(4) }) + expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: Event.Seq.make(4) }) }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer))) }), ) @@ -1266,7 +1266,7 @@ describe("EventV2", () => { const readStarted = yield* Deferred.make() const releaseRead = yield* Deferred.make() const firstRead = yield* Ref.make(true) - const eventLayer = EventV2.layerWith({ + const eventLayer = Bus.layerWith({ beforeAggregateRead: () => Ref.getAndSet(firstRead, false).pipe( Effect.flatMap((shouldBlock) => { @@ -1277,22 +1277,22 @@ describe("EventV2", () => { }).pipe(Layer.provide(LayerNode.compile(Database.node))) yield* Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const aggregateID = Session.ID.create() - yield* events.publish(DurableMessage, durableData(aggregateID, "zero")) - const fiber = yield* events + yield* bus.publish(DurableMessage, durableData(aggregateID, "zero")) + const fiber = yield* bus .log({ aggregateID, follow: true }) .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) yield* Deferred.await(readStarted) - yield* events.publish(DurableMessage, durableData(aggregateID, "one")) + yield* bus.publish(DurableMessage, durableData(aggregateID, "one")) yield* Deferred.succeed(releaseRead, undefined) const items = Array.from(yield* Fiber.join(fiber)) - expect(items.map((item) => (EventV2.isSynced(item) ? item : item.durable?.seq))).toEqual([ - EventV2.Seq.make(0), - { type: "log.synced", aggregateID, seq: EventV2.Seq.make(0) }, - EventV2.Seq.make(1), + expect(items.map((item) => (Bus.isSynced(item) ? item : item.durable?.seq))).toEqual([ + Event.Seq.make(0), + { type: "log.synced", aggregateID, seq: Event.Seq.make(0) }, + Event.Seq.make(1), ]) }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer))) }), @@ -1300,22 +1300,22 @@ describe("EventV2", () => { it.effect("sequences returns the latest committed seq per aggregate and omits unknown aggregates", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const first = Session.ID.create() const second = Session.ID.create() - yield* events.publish(DurableMessage, durableData(first, "zero")) - yield* events.publish(DurableMessage, durableData(first, "one")) - yield* events.publish(DurableMessage, durableData(second, "zero")) + yield* bus.publish(DurableMessage, durableData(first, "zero")) + yield* bus.publish(DurableMessage, durableData(first, "one")) + yield* bus.publish(DurableMessage, durableData(second, "zero")) - const sequences = yield* events.sequences([first, second, Session.ID.create()]) + const sequences = yield* bus.sequences([first, second, Session.ID.create()]) expect(sequences).toEqual( new Map([ - [first, EventV2.Seq.make(1)], - [second, EventV2.Seq.make(0)], + [first, Event.Seq.make(1)], + [second, Event.Seq.make(0)], ]), ) - expect(yield* events.sequences([])).toEqual(new Map()) + expect(yield* bus.sequences([])).toEqual(new Map()) }), ) }) diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 502a46b7d41..8860cd90453 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -7,10 +7,10 @@ import { Integration } from "@opencode-ai/core/integration" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" @@ -25,22 +25,22 @@ const locationLayer = Layer.succeed( Location.Service.of(location({ directory: AbsolutePath.make("test") })), ) const catalogLayer = AppNodeBuilder.build( - LayerNode.group([Catalog.node, EventV2.node, Credential.node, Integration.node]), + LayerNode.group([Catalog.node, Bus.node, Credential.node, Integration.node]), [[Location.node, locationLayer]], ) const it = testEffect(catalogLayer) -describe("CatalogV2", () => { +describe("Catalog", () => { it.effect("publishes an updated event after catalog changes", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const events = yield* EventV2.Service - const updated = yield* events + const bus = yield* Bus.Service + const updated = yield* bus .subscribe(Catalog.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) + yield* catalog.transform((editor) => editor.provider.update(Provider.ID.make("test"), () => {})) expect((yield* Fiber.join(updated)).length).toBe(1) }), @@ -55,28 +55,28 @@ describe("CatalogV2", () => { return Effect.gen(function* () { const catalog = yield* Catalog.Service const credentials = yield* Credential.Service - yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) + yield* catalog.transform((editor) => editor.provider.update(Provider.ID.make("test"), () => {})) yield* credentials.create({ integrationID, label: "First", value: Credential.Key.make({ type: "key", key: "first", metadata: { tenant: "one" } }), }) - expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined() + expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([Provider.ID.make("test")]) + expect(required(yield* catalog.provider.get(Provider.ID.make("test"))).body).toBeUndefined() yield* credentials.create({ integrationID, label: "Second", value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }), }) - expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).body).toBeUndefined() + expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([Provider.ID.make("test")]) + expect(required(yield* catalog.provider.get(Provider.ID.make("test"))).body).toBeUndefined() }).pipe(Effect.provide(localCatalogLayer)) }) it.effect("derives availability from a provider's integration", () => { const integrationID = Integration.ID.make("gateway") - const providerID = ProviderV2.ID.make("remote") + const providerID = Provider.ID.make("remote") const localCatalogLayer = Layer.fresh( AppNodeBuilder.build(LayerNode.group([Catalog.node, Credential.node, Integration.node]), [ [Location.node, locationLayer], @@ -113,7 +113,7 @@ describe("CatalogV2", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const integrations = yield* Integration.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* integrations.transform((editor) => editor.method.update({ integrationID: Integration.ID.make(providerID), @@ -135,16 +135,16 @@ describe("CatalogV2", () => { it.effect("stores provider package settings", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* catalog.transform((catalog) => catalog.provider.update(providerID, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://override.example.com" } }), ) expect(required(yield* catalog.provider.get(providerID))).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://override.example.com" }, }) }), @@ -153,23 +153,23 @@ describe("CatalogV2", () => { it.effect("uses model package settings over provider settings", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") + const providerID = Provider.ID.make("test") + const modelID = Model.ID.make("model") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://provider.example.com" } }) catalog.model.update(providerID, modelID, (model) => { - model.modelID = ModelV2.ID.make("upstream-model") - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + model.modelID = Model.ID.make("upstream-model") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") model.settings = { baseURL: "https://override.example.com" } }) }) expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({ - modelID: ModelV2.ID.make("upstream-model"), - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + modelID: Model.ID.make("upstream-model"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://override.example.com" }, }) }), @@ -178,18 +178,18 @@ describe("CatalogV2", () => { it.effect("resolves default model package settings from the provider", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") + const providerID = Provider.ID.make("test") + const modelID = Model.ID.make("model") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://provider.example.com" } }) catalog.model.update(providerID, modelID, () => {}) }) expect(required(yield* catalog.model.get(providerID, modelID))).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://provider.example.com" }, }) }), @@ -198,8 +198,8 @@ describe("CatalogV2", () => { it.effect("resolves provider and model overlay merges", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const modelID = ModelV2.ID.make("model") + const providerID = Provider.ID.make("test") + const modelID = Model.ID.make("model") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, (provider) => { provider.settings = { provider: true, shared: "provider" } @@ -223,13 +223,13 @@ describe("CatalogV2", () => { it.effect("falls back to newest available model when no default is configured", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, () => {}) - catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => { + catalog.model.update(providerID, Model.ID.make("old"), (model) => { model.time.released = 1000 }) - catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => { + catalog.model.update(providerID, Model.ID.make("new"), (model) => { model.time.released = 2000 }) }) @@ -241,9 +241,9 @@ describe("CatalogV2", () => { it.effect("uses a transform-provided default model until that transform is replaced", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") - const old = ModelV2.ID.make("old") - const newest = ModelV2.ID.make("new") + const providerID = Provider.ID.make("test") + const old = Model.ID.make("old") + const newest = Model.ID.make("new") const models = (catalog: Catalog.Draft) => { catalog.provider.update(providerID, () => {}) catalog.model.update(providerID, old, (model) => { @@ -272,10 +272,10 @@ describe("CatalogV2", () => { it.effect("ignores a configured default on a disabled provider", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const disabledProvider = ProviderV2.ID.make("disabled") - const enabledProvider = ProviderV2.ID.make("enabled") - const disabledModel = ModelV2.ID.make("configured") - const fallbackModel = ModelV2.ID.make("fallback") + const disabledProvider = Provider.ID.make("disabled") + const enabledProvider = Provider.ID.make("enabled") + const disabledModel = Model.ID.make("configured") + const fallbackModel = Model.ID.make("fallback") yield* catalog.transform((catalog) => { catalog.provider.update(disabledProvider, (provider) => { provider.disabled = true @@ -296,10 +296,10 @@ describe("CatalogV2", () => { it.effect("small model prefers small keyword candidates before cost scoring", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("test") + const providerID = Provider.ID.make("test") yield* catalog.transform((catalog) => { catalog.provider.update(providerID, () => {}) - catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => { + catalog.model.update(providerID, Model.ID.make("cheap-large"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [ @@ -314,7 +314,7 @@ describe("CatalogV2", () => { ] model.time.released = Date.now() }) - catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => { + catalog.model.update(providerID, Model.ID.make("expensive-mini"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [ diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index 9d9f8ccfd3c..0de0096b188 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -1,34 +1,32 @@ import { describe, expect } from "bun:test" -import { CodeMode } from "@opencode-ai/core/codemode" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Tool } from "@opencode-ai/core/tool/tool" +import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "./lib/effect" describe("CodeMode", () => { it.effect("owns registrations, execute, and catalog materialization", () => Effect.gen(function* () { - const codeMode = yield* CodeMode.Service - yield* codeMode.register( - Tool.registrationEntries({ - echo: Tool.make({ + const tools = yield* Tool.Service + yield* tools.transform((draft) => + draft.add({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.String, execute: ({ text }) => Effect.succeed({ output: text }), - }), }), ) - const materialized = yield* codeMode.materialize() - expect(materialized.tool).toBeDefined() - expect(materialized.catalog).toStrictEqual([ + const snapshot = yield* tools.snapshot() + expect(snapshot.definitions.some((tool) => tool.name === "execute")).toBe(true) + expect(snapshot.codeModeCatalog).toStrictEqual([ { path: "echo", description: "Echo text", signature: "tools.echo(input: {\n text: string,\n}): Promise", }, ]) - }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(CodeMode.node))), + }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(Tool.node))), ) }) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 44f46a9837a..fcb6fe730a5 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -1,9 +1,8 @@ import { describe, expect } from "bun:test" -import { CodeMode } from "@opencode-ai/core/codemode" import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { Tool } from "@opencode-ai/core/tool/tool" +import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" @@ -66,32 +65,45 @@ describe("CodeModeInstructions", () => { ) it.effect("stores a canonical sorted snapshot so registration order does not churn history", () => { - const alpha = Tool.make({ + const alpha = ({ + name: "alpha", description: "Alpha tool", input: Schema.Struct({}), output: Schema.String, execute: () => Effect.succeed({ output: "alpha" }), }) - const zeta = Tool.make({ + const zeta = ({ + name: "zeta", description: "Zeta tool", input: Schema.Struct({}), output: Schema.String, execute: () => Effect.succeed({ output: "zeta" }), }) - const layer = AppNodeBuilder.build(CodeMode.node) + const layer = AppNodeBuilder.build(Tool.node) return Effect.gen(function* () { - const codeMode = yield* CodeMode.Service + const tools = yield* Tool.Service const initialized = yield* Effect.scoped( Effect.gen(function* () { - yield* codeMode.register(Tool.registrationEntries({ zeta, alpha }, { namespace: "tools" })) - return yield* readInitial(CodeModeInstructions.make((yield* codeMode.materialize()).catalog)) + yield* tools.transform((draft) => { + draft.add({ ...zeta, options: { namespace: "tools" } }) + draft.add({ ...alpha, options: { namespace: "tools" } }) + }) + return yield* readInitial( + CodeModeInstructions.make((yield* tools.snapshot()).codeModeCatalog), + ) }), ) const reordered = yield* Effect.scoped( Effect.gen(function* () { - yield* codeMode.register(Tool.registrationEntries({ alpha, zeta }, { namespace: "tools" })) - return yield* readUpdate(CodeModeInstructions.make((yield* codeMode.materialize()).catalog), initialized) + yield* tools.transform((draft) => { + draft.add({ ...alpha, options: { namespace: "tools" } }) + draft.add({ ...zeta, options: { namespace: "tools" } }) + }) + return yield* readUpdate( + CodeModeInstructions.make((yield* tools.snapshot()).codeModeCatalog), + initialized, + ) }), ) diff --git a/packages/core/test/command.test.ts b/packages/core/test/command.test.ts index 8fb24321392..11ac774b973 100644 --- a/packages/core/test/command.test.ts +++ b/packages/core/test/command.test.ts @@ -1,27 +1,27 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { Config } from "@opencode-ai/core/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "./fixture/mcp" import { testEffect } from "./lib/effect" const it = testEffect( - AppNodeBuilder.build(CommandV2.node, [ + AppNodeBuilder.build(Command.node, [ [MCP.node, emptyMcpLayer], [Config.node, emptyConfigLayer], [Location.node, testLocationLayer], ]), ) -describe("CommandV2", () => { +describe("Command", () => { it.effect("applies command transforms and preserves later overrides", () => Effect.gen(function* () { - const command = yield* CommandV2.Service + const command = yield* Command.Service yield* command.transform((editor) => { editor.update("review", (command) => { command.template = "First" @@ -30,34 +30,34 @@ describe("CommandV2", () => { editor.update("review", (command) => { command.template = "Second" command.model = { - id: ModelV2.ID.make("claude"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), } }) }) expect(yield* command.get("review")).toEqual( - CommandV2.Info.make({ + Command.Info.make({ name: "review", template: "Second", description: "Review code", model: { - id: ModelV2.ID.make("claude"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), }, }), ) expect(yield* command.list()).toEqual([ - CommandV2.Info.make({ + Command.Info.make({ name: "review", template: "Second", description: "Review code", model: { - id: ModelV2.ID.make("claude"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), }, }), ]) @@ -66,7 +66,7 @@ describe("CommandV2", () => { it.effect("evaluates command template shell blocks", () => Effect.gen(function* () { - const command = yield* CommandV2.Service + const command = yield* Command.Service yield* command.transform((editor) => { editor.update("review", (command) => { command.template = "Output: !`echo command-output`" diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 855ea370e2f..a8b393bfb3d 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -2,26 +2,26 @@ import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate" import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" import { agentHost, host } from "../plugin/host" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([AgentV2.node, FSUtil.node, Global.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Agent.node, FSUtil.node, Global.node]))) const decode = Schema.decodeUnknownSync(Config.Info) const defaultPermissions = [ { action: "*", resource: "*", effect: "allow" }, { action: "external_directory", resource: "*", effect: "ask" }, -] satisfies PermissionV2.Ruleset +] satisfies Permission.Ruleset test("rejects named agent color tokens", () => { expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow() @@ -31,15 +31,15 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("matches POSIX paths against home-relative permissions", () => Effect.gen(function* () { const permissions = yield* loadHomePermissions("/home/test") - expect(PermissionV2.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe( + expect(Permission.evaluate("external_directory", "/home/test/p/opencode/src/*", permissions).effect).toBe( "allow", ) - expect(PermissionV2.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny") - expect(PermissionV2.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny") - expect(PermissionV2.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny") + expect(Permission.evaluate("external_directory", "/home/test/cache/files/*", permissions).effect).toBe("deny") + expect(Permission.evaluate("external_directory", "/some/~/path", permissions).effect).toBe("deny") + expect(Permission.evaluate("external_directory", "$HOMELESS/private/*", permissions).effect).toBe("deny") expect(permissions).toContainEqual({ action: "shell", resource: "$HOME/private/**", effect: "deny" }) expect(permissions).not.toContainEqual({ action: "shell", resource: "/home/test/private/**", effect: "deny" }) - expect(PermissionV2.evaluate("shell", "$HOME/private/key", permissions).effect).toBe("deny") + expect(Permission.evaluate("shell", "$HOME/private/key", permissions).effect).toBe("deny") }), ) @@ -47,9 +47,9 @@ describe("ConfigAgentPlugin.Plugin", () => { Effect.gen(function* () { const permissions = yield* loadHomePermissions("C:\\Users\\test") expect( - PermissionV2.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect, + Permission.evaluate("external_directory", "C:\\Users\\test\\p\\opencode\\src\\*", permissions).effect, ).toBe("allow") - expect(PermissionV2.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe( + expect(Permission.evaluate("external_directory", "C:\\Users\\test\\cache\\files\\*", permissions).effect).toBe( "deny", ) }), @@ -57,8 +57,8 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("applies all global permissions before agent-specific permissions", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const build = AgentV2.ID.make("build") + const agents = yield* Agent.Service + const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, (agent) => { agent.mode = "primary" @@ -119,10 +119,10 @@ describe("ConfigAgentPlugin.Plugin", () => { { action: "read", resource: "*", effect: "allow" }, { action: "bash", resource: "git *", effect: "allow" }, ]) - expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow") - expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask") + expect(Permission.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow") + expect(Permission.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask") - const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) + const reviewer = yield* agents.get(Agent.ID.make("reviewer")) if (!reviewer) throw new Error("expected configured reviewer agent") expect(reviewer).toMatchObject({ description: "Review changes", @@ -137,20 +137,20 @@ describe("ConfigAgentPlugin.Plugin", () => { { action: "edit", resource: "*", effect: "deny" }, { action: "read", resource: "*", effect: "deny" }, ]) - expect(PermissionV2.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny") - expect((yield* agents.get(AgentV2.ID.make("late")))?.permissions).toEqual([ + expect(Permission.evaluate("read", "README.md", reviewer.permissions).effect).toBe("deny") + expect((yield* agents.get(Agent.ID.make("late")))?.permissions).toEqual([ ...defaultPermissions, { action: "bash", resource: "*", effect: "ask" }, { action: "read", resource: "*", effect: "allow" }, { action: "edit", resource: "*", effect: "allow" }, ]) - expect(yield* agents.get(AgentV2.ID.make("removed"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("removed"))).toBeUndefined() }), ) it.effect("maps configured agent fields and preserves an unspecified model variant", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -194,7 +194,7 @@ describe("ConfigAgentPlugin.Plugin", () => { Effect.provideService(Config.Service, config), ) - const reviewer = yield* agents.get(AgentV2.ID.make("reviewer")) + const reviewer = yield* agents.get(Agent.ID.make("reviewer")) if (!reviewer) throw new Error("expected configured reviewer agent") expect(reviewer).toMatchObject({ system: "Review carefully.", @@ -215,8 +215,8 @@ describe("ConfigAgentPlugin.Plugin", () => { it.effect("removes a built-in agent disabled by configuration", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const build = AgentV2.ID.make("build") + const agents = yield* Agent.Service + const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, () => {})) const config = Config.Service.of({ @@ -277,7 +277,7 @@ Use native v2 fields.`, await fs.writeFile(path.join(tmp.path, "agents", "disabled.md"), "---\ndisabled: true\n---\nDisabled") await fs.writeFile(path.join(tmp.path, "modes", "plan.md"), "Make a plan.") }) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -293,21 +293,21 @@ Use native v2 fields.`, Effect.provideService(Config.Service, config), ) - expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({ + expect(yield* agents.get(Agent.ID.make("reviewer"))).toMatchObject({ model: { providerID: "openrouter", id: "openai/gpt-5" }, system: "Review carefully.", description: "Markdown description", request: { body: { temperature: 0.5 } }, permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }], }) - expect(yield* agents.get(AgentV2.ID.make("team/helper"))).toMatchObject({ system: "Help the team." }) - expect(yield* agents.get(AgentV2.ID.make("native"))).toMatchObject({ + expect(yield* agents.get(Agent.ID.make("team/helper"))).toMatchObject({ system: "Help the team." }) + expect(yield* agents.get(Agent.ID.make("native"))).toMatchObject({ system: "Use native v2 fields.", request: { headers: { "x-agent": "native" }, body: { effort: "high" } }, permissions: [...defaultPermissions, { action: "edit", resource: "*", effect: "deny" }], }) - expect(yield* agents.get(AgentV2.ID.make("disabled"))).toBeUndefined() - expect(yield* agents.get(AgentV2.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" }) + expect(yield* agents.get(Agent.ID.make("disabled"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("plan"))).toMatchObject({ system: "Make a plan.", mode: "primary" }) }), ), ), @@ -316,8 +316,8 @@ Use native v2 fields.`, function loadHomePermissions(home: string) { return Effect.gen(function* () { - const agents = yield* AgentV2.Service - const build = AgentV2.ID.make("build") + const agents = yield* Agent.Service + const build = Agent.ID.make("build") yield* agents.transform((editor) => editor.update(build, () => {})) const config = Config.Service.of({ entries: () => diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index fced5f83dbe..aa3cf491450 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -3,18 +3,18 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, PubSub, Schema, Stream } from "effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { CommandV2 } from "@opencode-ai/core/command" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Command } from "@opencode-ai/core/command" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { emptyConfigLayer, emptyMcpLayer, testLocationLayer } from "../fixture/mcp" import { tmpdir } from "../fixture/tmpdir" @@ -22,7 +22,7 @@ import { testEffect } from "../lib/effect" import { host } from "../plugin/host" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([CommandV2.node, EventV2.node, FSUtil.node]), [ + AppNodeBuilder.build(LayerNode.group([Command.node, Bus.node, FSUtil.node]), [ [MCP.node, emptyMcpLayer], [Config.node, emptyConfigLayer], [Location.node, testLocationLayer], @@ -54,9 +54,9 @@ Review files`, await fs.writeFile(path.join(tmp.path, "commands", "empty.md"), "") }) - const command = yield* CommandV2.Service - const events = yield* EventV2.Service - const update = yield* events.publish(ConfigSchema.Event.Updated, {}) + const command = yield* Command.Service + const bus = yield* Bus.Service + const update = yield* bus.publish(ConfigSchema.Event.Updated, {}) const updates = yield* PubSub.unbounded() yield* ConfigCommandPlugin.Plugin.effect( host({ @@ -84,20 +84,20 @@ Review files`, ) expect(yield* command.list()).toEqual([ - CommandV2.Info.make({ + Command.Info.make({ name: "review", template: "Review files", description: "File review", - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), model: { - providerID: ProviderV2.ID.make("anthropic"), - id: ModelV2.ID.make("claude"), - variant: ModelV2.VariantID.make("high"), + providerID: Provider.ID.make("anthropic"), + id: Model.ID.make("claude"), + variant: Model.VariantID.make("high"), }, subtask: true, }), - CommandV2.Info.make({ name: "empty", template: "" }), - CommandV2.Info.make({ name: "nested/docs", template: "Write docs" }), + Command.Info.make({ name: "empty", template: "" }), + Command.Info.make({ name: "nested/docs", template: "Write docs" }), ]) yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "commands", "review.md"), "Review again")) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index b826da794d1..5c94cc59ea6 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -15,11 +15,11 @@ import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { FSUtil } from "@opencode-ai/util/fs-util" import { Watcher } from "@opencode-ai/core/filesystem/watcher" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { WellKnown } from "@opencode-ai/core/wellknown" import { Integration } from "@opencode-ai/schema/integration" @@ -81,7 +81,7 @@ function testLayer( ), ), ) - return AppNodeBuilder.build(LayerNode.group([Config.node, EventV2.node]), [ + return AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node]), [ [Config.node, Config.configured(options)], [Location.node, locationLayer], [Global.node, Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })], @@ -194,8 +194,8 @@ describe("Config", () => { return yield* Effect.gen(function* () { const config = yield* Config.Service - const events = yield* EventV2.Service - const changed = yield* events + const bus = yield* Bus.Service + const changed = yield* bus .subscribe(ConfigSchema.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.sleep("10 millis") @@ -298,14 +298,14 @@ describe("Config", () => { return yield* Effect.gen(function* () { const config = yield* Config.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service expect(Config.latest(yield* config.entries(), "shell")).toBe("secret") - const updated = yield* events + const updated = yield* bus .subscribe(ConfigSchema.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow key = "next" - yield* events.publish(Integration.Event.ConnectionUpdated, { integrationID }) + yield* bus.publish(Integration.Event.ConnectionUpdated, { integrationID }) expect(yield* Fiber.join(updated)).toHaveLength(1) expect(Config.latest(yield* config.entries(), "shell")).toBe("next") }).pipe( @@ -331,7 +331,7 @@ describe("Config", () => { // V1 lists servers directly under `mcp`, so a file with only `$schema` + `mcp` still migrates. expect(ConfigMigrateV1.isV1({ mcp: { context7: { type: "local", command: ["npx"] } } })).toBe(true) expect(ConfigMigrateV1.isV1({ $schema: "x", mcp: { executor: { type: "remote", url: "https://x" } } })).toBe(true) - // V2 nests under `mcp.servers`, so it must not be misdetected and re-migrated. + // Current config nests under `mcp.servers`, so it must not be misdetected and re-migrated. expect(ConfigMigrateV1.isV1({ mcp: { servers: { context7: { type: "local", command: ["npx"] } } } })).toBe(false) expect(ConfigMigrateV1.isV1({ mcp: {} })).toBe(false) expect(ConfigMigrateV1.isV1({ mcp: { timeout: { execution: 1000 } } })).toBe(false) @@ -396,7 +396,7 @@ describe("Config", () => { }) expect(migrated.providers?.bedrock).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock"), settings: { region: "us-east-1", profile: "dev" }, headers: { "x-test": "1" }, body: { trace: true }, @@ -1041,7 +1041,7 @@ describe("Config", () => { }, }) expect(documents[0]?.info.providers?.openai).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/openai"), + package: Provider.aisdk("@ai-sdk/openai"), settings: { apiKey: "secret", organization: "org" }, models: { model: { @@ -1051,7 +1051,7 @@ describe("Config", () => { }, }) expect(documents[0]?.info.providers?.anthropic).toMatchObject({ - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + package: Provider.aisdk("@ai-sdk/anthropic"), models: { model: { settings: { diff --git a/packages/core/test/config/fixtures/plugin/directory-plugin.ts b/packages/core/test/config/fixtures/plugin/directory-plugin.ts index f5e15c2c00a..a9d7628e47d 100644 --- a/packages/core/test/config/fixtures/plugin/directory-plugin.ts +++ b/packages/core/test/config/fixtures/plugin/directory-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "directory-plugin", diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index 2a9dfa81bf0..c558dd869e8 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -2,21 +2,20 @@ import fs from "fs/promises" import path from "path" import { pathToFileURL } from "url" import { describe, expect } from "bun:test" -import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { Plugin } from "@opencode-ai/schema/plugin" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { Effect, Logger } from "effect" import { Database } from "../../src/database/database" @@ -24,7 +23,7 @@ import { tmpdir } from "../fixture/tmpdir" import { testEffect } from "../lib/effect" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])), ) describe("PluginSupervisor config", () => { @@ -32,7 +31,7 @@ describe("PluginSupervisor config", () => { withLocation( { plugins: ["-opencode.provider.*", "opencode.provider.openai"] }, Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service yield* ready() expect( (yield* plugins.list()).map((plugin) => plugin.id).filter((id) => id.startsWith("opencode.provider.")), @@ -54,8 +53,8 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("configured"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("configured"))).toMatchObject({ description: "Loaded from config", mode: "subagent", }) @@ -69,10 +68,10 @@ describe("PluginSupervisor config", () => { { plugins: [plugin, "-config-promise-plugin"] }, Effect.gen(function* () { yield* ready() - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service expect((yield* plugins.list()).map((item) => String(item.id))).not.toContain("config-promise-plugin") - expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined() }), ) }) @@ -83,7 +82,7 @@ describe("PluginSupervisor config", () => { { plugins: [plugin, `-${plugin}`] }, Effect.gen(function* () { yield* ready() - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service expect((yield* plugins.list()).map((item) => String(item.id))).toContain("config-promise-plugin") }), ) @@ -102,8 +101,8 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("effect-configured"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("effect-configured"))).toMatchObject({ description: "Effect plugin from config", mode: "subagent", }) @@ -133,8 +132,8 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("configured"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("configured"))).toMatchObject({ description: "Loaded after invalid plugins", }) expect(output).toEqual([ @@ -150,8 +149,8 @@ describe("PluginSupervisor config", () => { undefined, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("directory"))).toMatchObject({ + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("directory"))).toMatchObject({ description: "Loaded from plugin directory", }) }), @@ -164,26 +163,26 @@ describe("PluginSupervisor config", () => { undefined, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - const events = yield* EventV2.Service + const agents = yield* Agent.Service + const bus = yield* Bus.Service const location = yield* Location.Service - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const file = path.join(location.directory, ".opencode", "plugin", "mutable.ts") const first = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id expect(first).toBeDefined() - expect((yield* agents.get(AgentV2.ID.make("mutable")))?.description).toBe("first") + expect((yield* agents.get(Agent.ID.make("mutable")))?.description).toBe("first") yield* Effect.promise(async () => { await fs.writeFile(file, mutablePlugin("second")) const modified = new Date(Date.now() + 5_000) await fs.utimes(file, modified, modified) }) - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) yield* waitUntil( Effect.gen(function* () { const current = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id - return current === first && (yield* agents.get(AgentV2.ID.make("mutable")))?.description === "second" + return current === first && (yield* agents.get(Agent.ID.make("mutable")))?.description === "second" }), ) }), @@ -201,8 +200,8 @@ describe("PluginSupervisor config", () => { { plugins: ["-*"] }, Effect.gen(function* () { yield* ready() - const agents = yield* AgentV2.Service - expect(yield* agents.get(AgentV2.ID.make("directory"))).toBeUndefined() + const agents = yield* Agent.Service + expect(yield* agents.get(Agent.ID.make("directory"))).toBeUndefined() }), true, ), @@ -221,7 +220,7 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service const ids = (yield* registry.list()).map((plugin) => String(plugin.id)) expect(ids.indexOf("opencode.agent")).toBeLessThan(ids.indexOf("sdk-order")) expect(ids.indexOf("sdk-order")).toBeLessThan(ids.indexOf("config-promise-plugin")) @@ -231,7 +230,7 @@ describe("PluginSupervisor config", () => { const catalog = yield* Catalog.Service expect( - (yield* catalog.model.get(ProviderV2.ID.make("configured"), ModelV2.ID.make("glm-5.2")))?.variants, + (yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants, ).toEqual([ expect.objectContaining({ id: "high", headers: { custom: "true" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }), @@ -248,12 +247,12 @@ describe("PluginSupervisor config", () => { }, Effect.gen(function* () { yield* ready() - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service expect((yield* registry.list()).map((plugin) => String(plugin.id))).not.toContain("opencode.variant") const catalog = yield* Catalog.Service expect( - (yield* catalog.model.get(ProviderV2.ID.make("configured"), ModelV2.ID.make("glm-5.2")))?.variants, + (yield* catalog.model.get(Provider.ID.make("configured"), Model.ID.make("glm-5.2")))?.variants, ).toEqual([expect.objectContaining({ id: "high", headers: { custom: "true" } })]) }), ), @@ -304,7 +303,7 @@ function withLocation( } function mutablePlugin(description: string) { - const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/v2/promise/index.ts")).href + const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/index.ts")).href return ` import { Plugin } from ${JSON.stringify(plugin)} diff --git a/packages/core/test/config/policy.test.ts b/packages/core/test/config/policy.test.ts index 810f20ffd9d..a90d64ed99d 100644 --- a/packages/core/test/config/policy.test.ts +++ b/packages/core/test/config/policy.test.ts @@ -3,10 +3,10 @@ import { Config as ConfigSchema } from "@opencode-ai/schema/config" import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" import { ConfigPolicyPlugin } from "@opencode-ai/core/config/plugin/policy" -import { EventV2 } from "@opencode-ai/core/event" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Bus } from "@opencode-ai/core/bus" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Effect, Schema } from "effect" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" @@ -25,7 +25,7 @@ const policies = (...items: { effect: "allow" | "deny"; resource: string }[]) => }) const addPlugin = Effect.fn(function* (entries: () => Config.Entry[]) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ConfigPolicyPlugin.Plugin.effect(host).pipe( Effect.provideService(Config.Service, Config.Service.of({ entries: () => Effect.sync(entries) })), @@ -37,9 +37,9 @@ describe("ConfigPolicyPlugin.Plugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openai, () => {}) - catalog.provider.update(ProviderV2.ID.anthropic, () => {}) - catalog.provider.update(ProviderV2.ID.make("company-internal"), () => {}) + catalog.provider.update(Provider.ID.openai, () => {}) + catalog.provider.update(Provider.ID.anthropic, () => {}) + catalog.provider.update(Provider.ID.make("company-internal"), () => {}) }) yield* addPlugin(() => [ policies( @@ -49,38 +49,38 @@ describe("ConfigPolicyPlugin.Plugin", () => { ), ]) - expect(yield* catalog.provider.get(ProviderV2.ID.openai)).toBeUndefined() - expect(yield* catalog.provider.get(ProviderV2.ID.anthropic)).toBeDefined() - expect(yield* catalog.provider.get(ProviderV2.ID.make("company-internal"))).toBeDefined() + expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() + expect(yield* catalog.provider.get(Provider.ID.anthropic)).toBeDefined() + expect(yield* catalog.provider.get(Provider.ID.make("company-internal"))).toBeDefined() }), ) it.effect("prevents project policy from overriding user-global policy", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {})) yield* addPlugin(() => [ policies({ effect: "deny", resource: "openai" }), policies({ effect: "allow", resource: "openai" }), ]) - expect(yield* catalog.provider.get(ProviderV2.ID.openai)).toBeUndefined() + expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() }), ) it.live("reloads changed policies", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service let entries: Config.Entry[] = [policies({ effect: "deny", resource: "openai" })] - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {})) yield* addPlugin(() => entries) - expect(yield* catalog.provider.get(ProviderV2.ID.openai)).toBeUndefined() + expect(yield* catalog.provider.get(Provider.ID.openai)).toBeUndefined() entries = [policies({ effect: "allow", resource: "openai" })] - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) yield* waitUntil( - catalog.provider.get(ProviderV2.ID.openai).pipe(Effect.map((provider) => provider !== undefined)), + catalog.provider.get(Provider.ID.openai).pipe(Effect.map((provider) => provider !== undefined)), ) }), ) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 60597836f2d..152b34faff0 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -5,17 +5,17 @@ import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (config: Config.Interface) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)) }) @@ -52,8 +52,8 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("defaults custom models to agent capabilities", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("custom") - const modelID = ModelV2.ID.make("chat") + const providerID = Provider.ID.make("custom") + const modelID = Model.ID.make("chat") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -81,9 +81,9 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("preserves catalog capabilities unless config overrides them", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("custom") - const inheritedID = ModelV2.ID.make("inherited") - const overriddenID = ModelV2.ID.make("overridden") + const providerID = Provider.ID.make("custom") + const inheritedID = Model.ID.make("inherited") + const overriddenID = Model.ID.make("overridden") yield* catalog.transform((draft) => { draft.model.update(providerID, inheritedID, (model) => { model.capabilities = { tools: false, input: ["text"], output: ["text"] } @@ -132,8 +132,8 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("keeps configured model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.opencode - const modelID = ModelV2.ID.make("alpha-gpt-next") + const providerID = Provider.ID.opencode + const modelID = Model.ID.make("alpha-gpt-next") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -184,8 +184,8 @@ describe("ConfigProviderPlugin.Plugin", () => { it.effect("keeps layered model variant bodies unchanged", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.opencode - const modelID = ModelV2.ID.make("alpha-gpt-next") + const providerID = Provider.ID.opencode + const modelID = Model.ID.make("alpha-gpt-next") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -232,8 +232,8 @@ describe("ConfigProviderPlugin.Plugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service const integrations = yield* Integration.Service - const providerID = ProviderV2.ID.make("custom") - const modelID = ModelV2.ID.make("chat") + const providerID = Provider.ID.make("custom") + const modelID = Model.ID.make("chat") const config = Config.Service.of({ entries: () => Effect.succeed([ @@ -318,7 +318,7 @@ describe("ConfigProviderPlugin.Plugin", () => { const provider = required(yield* catalog.provider.get(providerID)) const model = required(yield* catalog.model.get(providerID, modelID)) - expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default")) + expect((yield* catalog.model.default())?.id).toBe(Model.ID.make("default")) expect(provider.name).toBe("Renamed") expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({ type: "env", @@ -330,7 +330,7 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(provider.settings).toEqual({ baseURL: "https://example.test" }) expect(provider.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.id).toBe(modelID) - expect(model.modelID).toBe(ModelV2.ID.make("api-chat")) + expect(model.modelID).toBe(Model.ID.make("api-chat")) expect(model.name).toBe("Last") expect(model.compatibility).toEqual({ reasoningField: "vendor_reasoning" }) expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) @@ -350,8 +350,8 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true }) expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.variants?.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("fast"), - ModelV2.VariantID.make("slow"), + Model.VariantID.make("fast"), + Model.VariantID.make("slow"), ]) expect(model.variants?.[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.variants?.[1]?.headers).toEqual({ slow: "slow" }) diff --git a/packages/core/test/config/reload.test.ts b/packages/core/test/config/reload.test.ts index d917821facb..70d21a693b1 100644 --- a/packages/core/test/config/reload.test.ts +++ b/packages/core/test/config/reload.test.ts @@ -1,22 +1,22 @@ import path from "path" import { describe, expect } from "bun:test" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { Config } from "@opencode-ai/core/config" import { ConfigAgentPlugin } from "@opencode-ai/core/config/plugin/agent" import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command" import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider" import { ConfigReferencePlugin } from "@opencode-ai/core/config/plugin/reference" import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Global } from "@opencode-ai/util/global" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Reference } from "@opencode-ai/core/reference" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { Effect, Schema } from "effect" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" @@ -28,13 +28,13 @@ const document = path.join(import.meta.dir, "opencode.json") describe("config plugin reloads", () => { it.live("reloads config-backed domains without reloading external plugins", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service - const commands = yield* CommandV2.Service - const events = yield* EventV2.Service - const plugins = yield* PluginV2.Service + const commands = yield* Command.Service + const bus = yield* Bus.Service + const plugins = yield* Plugin.Service const references = yield* Reference.Service - const skills = yield* SkillV2.Service + const skills = yield* Skill.Service const host = yield* PluginHost.make(plugins) let entries: Config.Entry[] = [config("first")] const service = Config.Service.of({ entries: () => Effect.sync(() => entries) }) @@ -47,26 +47,26 @@ describe("config plugin reloads", () => { yield* setup(ConfigReferencePlugin.Plugin.effect(host)) yield* setup(ConfigProviderPlugin.Plugin.effect(host)) - expect((yield* agents.get(AgentV2.ID.make("first")))?.description).toBe("First agent") + expect((yield* agents.get(Agent.ID.make("first")))?.description).toBe("First agent") expect((yield* commands.get("first"))?.description).toBe("First command") expect( (yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/first"), ).toBe(true) expect((yield* references.list()).map((reference) => reference.name)).toEqual(["first"]) - expect(yield* catalog.provider.get(ProviderV2.ID.make("first"))).toBeDefined() + expect(yield* catalog.provider.get(Provider.ID.make("first"))).toBeDefined() entries = [config("second")] - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) yield* waitUntil( Effect.gen(function* () { return ( - (yield* agents.get(AgentV2.ID.make("first"))) === undefined && - (yield* agents.get(AgentV2.ID.make("second")))?.description === "Second agent" && + (yield* agents.get(Agent.ID.make("first"))) === undefined && + (yield* agents.get(Agent.ID.make("second")))?.description === "Second agent" && (yield* commands.get("first")) === undefined && (yield* commands.get("second"))?.description === "Second command" && (yield* references.list()).some((reference) => reference.name === "second") && - (yield* catalog.provider.get(ProviderV2.ID.make("first"))) === undefined && - (yield* catalog.provider.get(ProviderV2.ID.make("second"))) !== undefined + (yield* catalog.provider.get(Provider.ID.make("first"))) === undefined && + (yield* catalog.provider.get(Provider.ID.make("second"))) !== undefined ) }), ) diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index 86df211cf8e..2553498987b 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -6,7 +6,7 @@ import { ConfigSkillPlugin } from "@opencode-ai/core/config/plugin/skill" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { host } from "../plugin/host" @@ -18,8 +18,8 @@ describe("ConfigSkillPlugin.Plugin", () => { it.effect("registers configured skill directories and URLs", () => Effect.gen(function* () { const directory = AbsolutePath.make("/repo/packages/app") - const sources: SkillV2.Source[] = [] - const transform = Effect.fnUntraced(function* (update: (draft: SkillV2.Draft) => void | Effect.Effect) { + const sources: Skill.Source[] = [] + const transform = Effect.fnUntraced(function* (update: (draft: Skill.Draft) => void | Effect.Effect) { const result = update({ source: (source) => { sources.push(source) @@ -61,29 +61,29 @@ describe("ConfigSkillPlugin.Plugin", () => { ) expect(sources).toEqual([ - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.claude", "skills")), }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.agents", "skills")), }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skill")), }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skills")), }), - SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), - SkillV2.DirectorySource.make({ + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/home/test", "shared-skills")), }), - SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/opt/skills") }), - SkillV2.UrlSource.make({ type: "url", url: "https://example.test/skills/" }), + Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/opt/skills") }), + Skill.UrlSource.make({ type: "url", url: "https://example.test/skills/" }), ]) }), ) diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index f98ded68f3a..f8148b66f8c 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -28,8 +28,8 @@ import deleteToolProgressEventsMigration from "@opencode-ai/core/database/migrat import canonicalToolResultsMigration from "@opencode-ai/core/database/migration/20260722170000_canonical_tool_results" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Bus } from "@opencode-ai/core/bus" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionSchema } from "@opencode-ai/core/session/schema" @@ -968,13 +968,13 @@ describe("DatabaseMigration", () => { ) const database = Layer.succeed(Database.Service, { db }) - yield* EventV2.Service.use((service) => + yield* Bus.Service.use((service) => service.publish(SessionV1.Event.Updated, { sessionID: SessionSchema.ID.make("session"), info: { id: SessionSchema.ID.make("session"), slug: "session", - projectID: ProjectV2.ID.global, + projectID: Project.ID.global, directory: "/project", title: "After", version: "test", @@ -983,7 +983,7 @@ describe("DatabaseMigration", () => { }), ).pipe( Effect.provide( - AppNodeBuilder.build(LayerNode.group([EventV2.node, SessionProjector.node]), [[Database.node, database]]), + AppNodeBuilder.build(LayerNode.group([Bus.node, SessionProjector.node]), [[Database.node, database]]), ), ) @@ -1280,7 +1280,7 @@ describe("DatabaseMigration", () => { Effect.gen(function* () { const db = yield* makeDb yield* DatabaseMigration.apply(db) - const projectID = ProjectV2.ID.make("codec_project") + const projectID = Project.ID.make("codec_project") const worktree = AbsolutePath.make("C:\\Repo\\Thing") const sandbox = AbsolutePath.make("C:\\Repo\\Thing\\sandbox") const directory = "C:\\Repo\\Thing\\packages\\api" @@ -1291,7 +1291,7 @@ describe("DatabaseMigration", () => { db .insert(ProjectTable) .values({ - id: ProjectV2.ID.make("invalid_path"), + id: Project.ID.make("invalid_path"), worktree: AbsolutePath.make("not-absolute"), sandboxes: [], time_created: 1, diff --git a/packages/core/test/event-logger.test.ts b/packages/core/test/event-logger.test.ts index ab7aca7d5b9..9678d0e0d47 100644 --- a/packages/core/test/event-logger.test.ts +++ b/packages/core/test/event-logger.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Logger } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventLogger } from "@opencode-ai/core/event-logger" import { Agent } from "@opencode-ai/schema/agent" import { Catalog } from "@opencode-ai/schema/catalog" @@ -11,7 +11,7 @@ import { Command } from "@opencode-ai/schema/command" import { Config } from "@opencode-ai/schema/config" import { McpEvent } from "@opencode-ai/schema/mcp-event" -const UnlistedUpdated = EventV2.ephemeral({ type: "test.updated", schema: {} }) +const UnlistedUpdated = Bus.ephemeral({ type: "test.updated", schema: {} }) describe("EventLogger", () => { test("logs explicitly listed updated events", async () => { @@ -21,15 +21,15 @@ describe("EventLogger", () => { }) await Effect.gen(function* () { - const events = yield* EventV2.Service - yield* events.publish(Agent.Event.Updated, {}) - yield* events.publish(Catalog.Event.Updated, {}) - yield* events.publish(Command.Event.Updated, {}) - yield* events.publish(Config.Event.Updated, {}) - yield* events.publish(McpEvent.StatusChanged, { server: "example" }) - yield* events.publish(UnlistedUpdated, {}) + const bus = yield* Bus.Service + yield* bus.publish(Agent.Event.Updated, {}) + yield* bus.publish(Catalog.Event.Updated, {}) + yield* bus.publish(Command.Event.Updated, {}) + yield* bus.publish(Config.Event.Updated, {}) + yield* bus.publish(McpEvent.StatusChanged, { server: "example" }) + yield* bus.publish(UnlistedUpdated, {}) }).pipe( - Effect.provide(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, EventLogger.node]))), + Effect.provide(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, EventLogger.node]))), Effect.provide(Logger.layer([logger])), Effect.scoped, Effect.runPromise, diff --git a/packages/core/test/filesystem/watcher.test.ts b/packages/core/test/filesystem/watcher.test.ts index 5c597d2db53..e475a8b1665 100644 --- a/packages/core/test/filesystem/watcher.test.ts +++ b/packages/core/test/filesystem/watcher.test.ts @@ -6,7 +6,7 @@ import { Deferred, Duration, Effect, Fiber, Layer, Option, Schedule, Stream } fr import { Config } from "@opencode-ai/core/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { LocationWatcher } from "@opencode-ai/core/filesystem/location-watcher" import { Watcher } from "@opencode-ai/core/filesystem/watcher" @@ -21,7 +21,7 @@ const describeWatcher = Watcher.hasNativeBinding() && !process.env.CI ? describe type WatcherEvent = { file: string; event: "add" | "change" | "unlink" } -const it = testEffect(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([FSUtil.node, Bus.node]))) const configLayer = Layer.succeed( Config.Service, @@ -66,9 +66,9 @@ function withTmp( function wait(check: (event: WatcherEvent) => boolean) { return Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const deferred = yield* Deferred.make() - const fiber = yield* events.subscribe(FileSystem.Event.Changed).pipe( + const fiber = yield* bus.subscribe(FileSystem.Event.Changed).pipe( Stream.runForEach((event) => { if (!check(event.data)) return Effect.void return Deferred.succeed(deferred, event.data).pipe(Effect.asVoid) @@ -217,7 +217,7 @@ describeWatcher("LocationWatcher", () => { it.live("cleanup stops publishing events", () => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const fs = yield* FSUtil.Service const tmp = yield* Effect.acquireRelease( Effect.promise(() => tmpdir()), @@ -229,9 +229,9 @@ describeWatcher("LocationWatcher", () => { ) const file = path.join(tmp.path, "after-dispose.txt") yield* noUpdate((event) => event.file === file, fs.writeFileString(file, "gone")).pipe( - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), ) - }).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node])))), + }).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([FSUtil.node, Bus.node])))), ) it.live("ignores .git/index changes", () => diff --git a/packages/core/test/form.test.ts b/packages/core/test/form.test.ts index 06b50f3643b..5ee579668be 100644 --- a/packages/core/test/form.test.ts +++ b/packages/core/test/form.test.ts @@ -2,12 +2,12 @@ import { describe, expect } from "bun:test" import { Deferred, Effect, Exit, Fiber } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Form } from "@opencode-ai/core/form" import { SessionSchema } from "@opencode-ai/core/session/schema" import { testEffect } from "./lib/effect" -const forms = AppNodeBuilder.build(LayerNode.group([EventV2.node, Form.node])) +const forms = AppNodeBuilder.build(LayerNode.group([Bus.node, Form.node])) const it = testEffect(forms) const formID = Form.ID.create("frm_test") @@ -22,9 +22,9 @@ describe("Form", () => { it.effect("returns a terminal cancelled state from ask", () => Effect.gen(function* () { const service = yield* Form.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const created = yield* Deferred.make() - const unsubscribe = yield* events.listen((event) => + const unsubscribe = yield* bus.listen((event) => event.type === Form.Event.Created.type ? Deferred.succeed(created, (event.data as { readonly form: Form.Info }).form).pipe(Effect.asVoid) : Effect.void, @@ -311,8 +311,8 @@ describe("Form", () => { it.effect("cleans up created forms when event publication fails", () => Effect.gen(function* () { const service = yield* Form.Service - const events = yield* EventV2.Service - const unsubscribe = yield* events.listen((event) => + const bus = yield* Bus.Service + const unsubscribe = yield* bus.listen((event) => event.type === Form.Event.Created.type ? Effect.die("create listener failed") : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -328,9 +328,9 @@ describe("Form", () => { it.effect("keeps forms pending when reply event publication fails", () => Effect.gen(function* () { const service = yield* Form.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* service.create(input) - const unsubscribe = yield* events.listen((event) => + const unsubscribe = yield* bus.listen((event) => event.type === Form.Event.Replied.type ? Effect.die("reply listener failed") : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) diff --git a/packages/core/test/generate.test.ts b/packages/core/test/generate.test.ts index 575b0199fb9..b669e60f483 100644 --- a/packages/core/test/generate.test.ts +++ b/packages/core/test/generate.test.ts @@ -6,15 +6,15 @@ import { Catalog } from "@opencode-ai/core/catalog" import { Generate } from "@opencode-ai/core/generate" import { Integration } from "@opencode-ai/core/integration" import { ModelResolver } from "@opencode-ai/core/model-resolver" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { ID, Info, Ref } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { Npm } from "@opencode-ai/util/npm" import { Effect, Layer, Stream } from "effect" import { testEffect } from "./lib/effect" -const selected = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("test-provider"), ModelV2.ID.make("gemini")), - package: ProviderV2.aisdk("@ai-sdk/google"), +const selected = Info.make({ + ...Info.default(Provider.ID.make("test-provider"), ID.make("gemini")), + package: Provider.aisdk("@ai-sdk/google"), }) const runtime = Model.make({ id: "gemini", provider: "test-provider", route: OpenAIChat.route }) @@ -89,7 +89,7 @@ it.effect("loads dynamic AI SDK models", () => const generate = yield* Generate.Service const result = yield* generate.text({ prompt: "Return exactly OK", - model: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + model: Ref.make({ providerID: selected.providerID, id: selected.id }), }) expect(result).toBe("OK") @@ -99,11 +99,11 @@ it.effect("loads dynamic AI SDK models", () => resolverIt.effect("resolves dynamic models with their catalog metadata", () => Effect.gen(function* () { const resolver = yield* ModelResolver.Service - const result = yield* resolver.resolve(ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id })) + const result = yield* resolver.resolve(Ref.make({ providerID: selected.providerID, id: selected.id })) expect(result).toEqual({ model: runtime, - ref: ModelV2.Ref.make({ providerID: selected.providerID, id: selected.id }), + ref: Ref.make({ providerID: selected.providerID, id: selected.id }), capabilities: selected.capabilities, cost: selected.cost, }) diff --git a/packages/core/test/github-copilot/models.test.ts b/packages/core/test/github-copilot/models.test.ts index 9bd07971fdf..cba3001d691 100644 --- a/packages/core/test/github-copilot/models.test.ts +++ b/packages/core/test/github-copilot/models.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test" import { CopilotModels } from "@opencode-ai/core/github-copilot/models" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" test("defensively syncs advertised Copilot models", async () => { const server = Bun.serve({ @@ -48,28 +48,28 @@ test("defensively syncs advertised Copilot models", async () => { }) try { - const existing = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + const existing = Model.Info.make({ + ...Model.Info.default(Provider.ID.githubCopilot, Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), name: "GPT-5 local", }) - const stale = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale")), - modelID: ModelV2.ID.make("stale"), + const stale = Model.Info.make({ + ...Model.Info.default(Provider.ID.githubCopilot, Model.ID.make("stale")), + modelID: Model.ID.make("stale"), }) const models = await CopilotModels.get(server.url.origin, {}, [existing, stale]) - const model = models.get(ModelV2.ID.make("gpt-5")) + const model = models.get(Model.ID.make("gpt-5")) expect(model?.name).toBe("GPT-5 local") expect(model?.settings).toMatchObject({ baseURL: server.url.origin, endpoint: "responses" }) expect(model?.cost[0]).toMatchObject({ input: 0, output: 0, cache: { read: 0, write: 0 } }) expect(model?.variants.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("low"), - ModelV2.VariantID.make("high"), + Model.VariantID.make("low"), + Model.VariantID.make("high"), ]) - expect(models.get(ModelV2.ID.make("utility"))?.enabled).toBe(false) - expect(models.has(ModelV2.ID.make("stale"))).toBe(false) - expect(models.has(ModelV2.ID.make("incomplete"))).toBe(false) + expect(models.get(Model.ID.make("utility"))?.enabled).toBe(false) + expect(models.has(Model.ID.make("stale"))).toBe(false) + expect(models.has(Model.ID.make("incomplete"))).toBe(false) } finally { await server.stop(true) } diff --git a/packages/core/test/instruction-state.test.ts b/packages/core/test/instruction-state.test.ts index 6557731fd79..4e75340d783 100644 --- a/packages/core/test/instruction-state.test.ts +++ b/packages/core/test/instruction-state.test.ts @@ -4,7 +4,8 @@ import { Effect, Schema } from "effect" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { EventTable } from "@opencode-ai/core/event/sql" import { Instructions } from "@opencode-ai/core/instructions" import { Project } from "@opencode-ai/core/project" @@ -16,7 +17,7 @@ import { SessionSchema } from "@opencode-ai/core/session/schema" import { InstructionBlobTable, InstructionStateTable, SessionTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) const source = (name: string, read: Effect.Effect) => Instructions.make({ @@ -51,7 +52,7 @@ const setup = (sessionID: SessionSchema.ID) => }) .run() .pipe(Effect.orDie) - return { db, events: yield* EventV2.Service } + return { db, events: yield* Bus.Service } }) const instructionEvents = (db: Database.Interface["db"], sessionID: SessionSchema.ID) => @@ -90,7 +91,7 @@ describe("InstructionState", () => { }), ), ]) - const published: EventV2.Payload[] = [] + const published: Event.Payload[] = [] const unsubscribe = yield* events.listen((event) => Effect.sync(() => { if (event.type === "session.instructions.updated") published.push(event) @@ -146,7 +147,7 @@ describe("InstructionState", () => { }), ), ]) - const published: EventV2.Payload[] = [] + const published: Event.Payload[] = [] const unsubscribe = yield* events.listen((event) => Effect.sync(() => { if (event.type === "session.instructions.updated") published.push(event) diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts index e5d79a53c60..a4553780a62 100644 --- a/packages/core/test/integration.test.ts +++ b/packages/core/test/integration.test.ts @@ -5,11 +5,11 @@ import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Integration } from "@opencode-ai/core/integration" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Integration.node, Credential.node, EventV2.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Integration.node, Credential.node, Bus.node]))) const failingCredentialNode = makeGlobalNode({ service: Credential.Service, layer: Layer.succeed( @@ -26,7 +26,7 @@ const failingCredentialNode = makeGlobalNode({ deps: [], }) const failingIt = testEffect( - AppNodeBuilder.build(LayerNode.group([Integration.node, EventV2.node]), [[Credential.node, failingCredentialNode]]), + AppNodeBuilder.build(LayerNode.group([Integration.node, Bus.node]), [[Credential.node, failingCredentialNode]]), ) function eventually( @@ -135,7 +135,7 @@ describe("Integration", () => { Effect.gen(function* () { const integrations = yield* Integration.Service const credentials = yield* Credential.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const integrationID = Integration.ID.make("openai") yield* integrations.transform((editor) => editor.method.update({ @@ -143,7 +143,7 @@ describe("Integration", () => { method: { type: "key", label: "API key" }, }), ) - const updated = yield* events + const updated = yield* bus .subscribe(Integration.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow diff --git a/packages/core/test/lib/image.ts b/packages/core/test/lib/image.ts index b899bc91684..72ac03fdf1e 100644 --- a/packages/core/test/lib/image.ts +++ b/packages/core/test/lib/image.ts @@ -1,7 +1,7 @@ import { Image } from "@opencode-ai/core/image" import { Effect, Layer } from "effect" -/** Passthrough resizer for tests that build ToolRegistry.node without a Location. */ +/** Passthrough resizer for tests that build Tool.node without a Location. */ export const imagePassthrough = Layer.mock(Image.Service, { normalize: (_resource, content) => Effect.succeed(content), }) diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index 7bf51cd9320..c99d2001330 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -1,23 +1,23 @@ -import { AgentV2 } from "@opencode-ai/core/agent" -import type { PermissionV2 } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" +import type { Permission } from "@opencode-ai/core/permission" import { SessionMessage } from "@opencode-ai/core/session/message" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { Tool } from "@opencode-ai/core/tool/tool" -import { Tools } from "@opencode-ai/core/tool/tools" -import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import { toSessionError } from "@opencode-ai/core/session/to-session-error" +import type { SessionError } from "@opencode-ai/schema/session-error" +import { Tool } from "@opencode-ai/core/tool" +import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin" import { Effect, type Scope } from "effect" import { host } from "../plugin/host" export const toolIdentity = { - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_tool_test"), } -export const toolDefinitions = (registry: ToolRegistry.Interface, permissions?: PermissionV2.Ruleset) => +export const toolDefinitions = (registry: Tool.Interface, permissions?: Permission.Ruleset) => registry.snapshot(permissions).pipe(Effect.map((toolSet) => toolSet.definitions)) export function waitForTool( - registry: ToolRegistry.Interface, + registry: Tool.Interface, name: string, remaining = 1000, ): Effect.Effect { @@ -33,10 +33,10 @@ export function waitForTool( } export function waitForCodeModeTool( - registry: ToolRegistry.Interface, + registry: Tool.Interface, path: string, remaining = 1000, -): Effect.Effect { +): Effect.Effect { return Effect.gen(function* () { const toolSet = yield* registry.snapshot() if (toolSet.codeModeCatalog?.some((tool) => tool.path === path)) return toolSet @@ -59,9 +59,9 @@ export const registerToolPlugin = ( readonly effect: (context: PluginContext) => Effect.Effect }, overrides: Parameters[0] = {}, -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { - const tools = yield* Tools.Service + const tools = yield* Tool.Service const context = host({ ...overrides, session: { @@ -69,29 +69,31 @@ export const registerToolPlugin = ( }, tool: { transform: (callback) => - Effect.gen(function* () { - const registrations: Array<{ - readonly name: string - readonly tool: Tool.Any - readonly options?: Tool.RegisterOptions - }> = [] - callback({ - add: (name, tool, options) => { - registrations.push({ name, tool, ...(options ? { options } : {}) }) - }, - }) - yield* Effect.forEach( - registrations, - (registration) => tools.register({ [registration.name]: registration.tool }, registration.options), - { discard: true }, - ).pipe(Effect.orDie) - return { dispose: Effect.void } - }), + tools + .transform((draft) => callback({ add: (tool) => draft.add(tool) })) + .pipe(Effect.orDie, Effect.as({ dispose: Effect.void })), hook: () => Effect.die("registerToolPlugin does not support tool hooks"), }, }) yield* plugin.effect(context) }) -export const executeTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput) => - registry.snapshot().pipe(Effect.flatMap((toolSet) => toolSet.execute(input))) +export interface ToolExecution { + readonly status: "completed" | "error" + readonly output?: any + readonly content?: ReadonlyArray + readonly metadata?: Tool.Metadata + readonly error?: SessionError.Error +} + +export const executeTool = ( + registry: Tool.Interface, + input: Parameters[0], +): Effect.Effect => + registry.snapshot().pipe( + Effect.flatMap((tools) => tools.execute(input)), + Effect.map((result) => ({ status: "completed" as const, ...result }) satisfies ToolExecution), + Effect.catchTag("Tool.Error", (error) => + Effect.succeed({ status: "error" as const, error: toSessionError(error) } satisfies ToolExecution), + ), + ) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 1a198371ece..bf897a6b1a6 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -2,36 +2,35 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Config } from "@opencode-ai/schema/config" -import { Plugin } from "@opencode-ai/schema/plugin" import { Money } from "@opencode-ai/schema/money" import { DateTime, Deferred, Effect, Equal, Fiber, Hash, RcMap, Schema, Stream } from "effect" -import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { Location } from "@opencode-ai/core/location" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProjectV2 } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { toolDefinitions, waitForTool } from "./lib/tool" import { Database } from "../src/database/database" -import { EventV2 } from "../src/event" +import { Bus } from "../src/bus" import { Reference } from "../src/reference" -import { ToolRegistry } from "../src/tool/registry" +import { Tool } from "../src/tool" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, LocationServiceMap.node]))) const itWithSdk = testEffect( - AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])), + AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node])), ) describe("LocationServiceMap", () => { @@ -44,7 +43,7 @@ describe("LocationServiceMap", () => { Effect.gen(function* () { const sdk = yield* SdkPlugins.Service const locations = yield* LocationServiceMap.Service - const id = AgentV2.ID.make("persistent-sdk-agent") + const id = Agent.ID.make("persistent-sdk-agent") const plugin = EffectPlugin.define({ id: "persistent-sdk-plugin", effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})), @@ -55,7 +54,7 @@ describe("LocationServiceMap", () => { const read = Effect.gen(function* () { const supervisor = yield* PluginSupervisor.Service yield* supervisor.flush - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service return yield* agents.get(id) }) @@ -101,7 +100,7 @@ describe("LocationServiceMap", () => { ) const explorer = yield* Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service return yield* agents.resolve("explore") }).pipe(Effect.provide(context)) @@ -193,8 +192,8 @@ describe("LocationServiceMap", () => { const context = yield* locations.contextEffect(Location.Ref.make({ directory: AbsolutePath.make(dir.path) })) yield* Deferred.await(firstStarted) - const events = yield* EventV2.Service - const updated = yield* events.subscribe(Config.Event.Updated).pipe( + const bus = yield* Bus.Service + const updated = yield* bus.subscribe(Config.Event.Updated).pipe( Stream.filter((event) => event.location?.directory === dir.path), Stream.runHead, Effect.forkChild({ startImmediately: true }), @@ -235,11 +234,11 @@ describe("LocationServiceMap", () => { Effect.provide(context), Effect.forkChild({ startImmediately: true }), ) - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* Effect.forEach( Array.from({ length: 5 }), - () => events.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))), + () => bus.publish(SdkPlugins.Updated, {}).pipe(Effect.andThen(Effect.sleep("50 millis"))), { discard: true }, ) expect(flushFiber.pollUnsafe()).toBeUndefined() @@ -270,7 +269,7 @@ describe("LocationServiceMap", () => { yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(context)) expect(activations.count).toBe(1) - yield* EventV2.Service.use((events) => events.publish(Config.Event.Updated, {})).pipe(Effect.provide(context)) + yield* Bus.Service.use((bus) => bus.publish(Config.Event.Updated, {})).pipe(Effect.provide(context)) yield* Effect.sleep("200 millis") expect(activations.count).toBe(1) @@ -370,7 +369,7 @@ describe("LocationServiceMap", () => { fs.writeFile(path.join(dir.path, "opencode.json"), JSON.stringify({ plugins: ["-*", "opencode.agent"] })), ) const plugins = yield* Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service yield* (yield* PluginSupervisor.Service).flush return yield* plugins.list() }).pipe( @@ -396,7 +395,7 @@ describe("LocationServiceMap", () => { const file = path.join(dir.path, "opencode.json") yield* Effect.promise(() => fs.writeFile(file, JSON.stringify({ plugins: ["-*", "opencode.agent"] }))) yield* Effect.gen(function* () { - const registry = yield* PluginV2.Service + const registry = yield* Plugin.Service const supervisor = yield* PluginSupervisor.Service yield* supervisor.flush expect((yield* registry.list()).map((plugin) => String(plugin.id))).toEqual(["opencode.agent"]) @@ -449,25 +448,25 @@ describe("LocationServiceMap", () => { Effect.scoped( Effect.gen(function* () { const locations = yield* LocationServiceMap.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const firstRef = Location.Ref.make({ directory: AbsolutePath.make(first.path) }) const secondRef = Location.Ref.make({ directory: AbsolutePath.make(second.path) }) const firstContext = yield* locations.contextEffect(firstRef) const secondContext = yield* locations.contextEffect(secondRef) const received = { first: 0, second: 0 } - yield* events.subscribe(Config.Event.Updated).pipe( + yield* bus.subscribe(Config.Event.Updated).pipe( Stream.runForEach(() => Effect.sync(() => received.first++)), Effect.provideContext(firstContext), Effect.forkScoped({ startImmediately: true }), ) - yield* events.subscribe(Config.Event.Updated).pipe( + yield* bus.subscribe(Config.Event.Updated).pipe( Stream.runForEach(() => Effect.sync(() => received.second++)), Effect.provideContext(secondContext), Effect.forkScoped({ startImmediately: true }), ) yield* Effect.sleep("10 millis") - yield* events.publish(Config.Event.Updated, {}, { location: firstRef }) + yield* bus.publish(Config.Event.Updated, {}, { location: firstRef }) yield* Effect.sleep("10 millis") expect(received).toEqual({ first: 1, second: 0 }) @@ -538,12 +537,12 @@ describe("LocationServiceMap", () => { ).pipe( Effect.flatMap(([blocked, allowed]) => Effect.gen(function* () { - const update = (directory: string, providerID: ProviderV2.ID) => + const update = (directory: string, providerID: Provider.ID) => Effect.gen(function* () { yield* Reference.Service const catalog = yield* Catalog.Service yield* catalog.transform((editor) => editor.provider.update(providerID, () => {})) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service // Tool plugins register during the forked PluginSupervisor boot; wait for // every expected tool rather than relying on batch ordering. yield* Effect.forEach( @@ -573,8 +572,8 @@ describe("LocationServiceMap", () => { ), ) - const blockedID = ProviderV2.ID.make("blocked-location") - const allowedID = ProviderV2.ID.make("allowed-location") + const blockedID = Provider.ID.make("blocked-location") + const allowedID = Provider.ID.make("allowed-location") const blockedState = yield* update(blocked.path, blockedID) expect(blockedState.providers.some((provider) => provider.id === blockedID)).toBe(true) expect(blockedState.providers.some((provider) => provider.id === allowedID)).toBe(false) @@ -641,13 +640,13 @@ describe("LocationServiceMap", () => { ) const failure = yield* SessionRunnerModel.Service.use((models) => models.resolve( - SessionV2.Info.make({ - id: SessionV2.ID.make("ses_unavailable_model"), - projectID: ProjectV2.ID.global, + Session.Info.make({ + id: Session.ID.make("ses_unavailable_model"), + projectID: Project.ID.global, title: "test", model: { - id: ModelV2.ID.make("chat"), - providerID: ProviderV2.ID.make("unavailable"), + id: Model.ID.make("chat"), + providerID: Provider.ID.make("unavailable"), }, cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, @@ -678,25 +677,25 @@ describe("LocationServiceMap", () => { const resolved = yield* Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((editor) => { - editor.provider.update(ProviderV2.ID.make("aliased"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai") + editor.provider.update(Provider.ID.make("aliased"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai") }) - editor.model.update(ProviderV2.ID.make("aliased"), ModelV2.ID.make("fast"), (model) => { + editor.model.update(Provider.ID.make("aliased"), Model.ID.make("fast"), (model) => { // Catalog id and package model id intentionally differ, like gpt-5.5-fast -> gpt-5.5. - model.modelID = ModelV2.ID.make("base") - model.variants = [{ id: ModelV2.VariantID.make("high") }] + model.modelID = Model.ID.make("base") + model.variants = [{ id: Model.VariantID.make("high") }] }) }) const models = yield* SessionRunnerModel.Service return yield* models.resolve( - SessionV2.Info.make({ - id: SessionV2.ID.make("ses_aliased_model"), - projectID: ProjectV2.ID.global, + Session.Info.make({ + id: Session.ID.make("ses_aliased_model"), + projectID: Project.ID.global, title: "test", model: { - id: ModelV2.ID.make("fast"), - providerID: ProviderV2.ID.make("aliased"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("fast"), + providerID: Provider.ID.make("aliased"), + variant: Model.VariantID.make("high"), }, cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, @@ -707,10 +706,10 @@ describe("LocationServiceMap", () => { }).pipe(Effect.provide(LocationServiceMap.Service.get(location))) expect(resolved.ref).toEqual( - ModelV2.Ref.make({ - id: ModelV2.ID.make("fast"), - providerID: ProviderV2.ID.make("aliased"), - variant: ModelV2.VariantID.make("high"), + Model.Ref.make({ + id: Model.ID.make("fast"), + providerID: Provider.ID.make("aliased"), + variant: Model.VariantID.make("high"), }), ) expect(String(resolved.model.id)).toBe("base") @@ -726,7 +725,7 @@ describe("LocationServiceMap", () => { ).pipe( Effect.flatMap((dir) => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const reviewer = EffectPlugin.define({ id: "reviewer", effect: (ctx) => @@ -741,7 +740,7 @@ describe("LocationServiceMap", () => { }) yield* plugins.activate([{ ...reviewer, version: "1" }]) - expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({ + expect(yield* (yield* Agent.Service).get(Agent.ID.make("reviewer"))).toMatchObject({ description: "Reviews code", mode: "subagent", }) diff --git a/packages/core/test/location.test.ts b/packages/core/test/location.test.ts index d012a0463f9..b51c3c5bd83 100644 --- a/packages/core/test/location.test.ts +++ b/packages/core/test/location.test.ts @@ -4,10 +4,10 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { testEffect } from "./lib/effect" -const workspaceID = WorkspaceV2.ID.make("wrk_test") +const workspaceID = Workspace.ID.make("wrk_test") const ref = { directory: AbsolutePath.make("/repo/packages/app"), workspaceID } const projectLayer = Layer.succeed( Project.Service, diff --git a/packages/core/test/mcp-instructions.test.ts b/packages/core/test/mcp-instructions.test.ts index 77b49363548..44efd76b241 100644 --- a/packages/core/test/mcp-instructions.test.ts +++ b/packages/core/test/mcp-instructions.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { MCP } from "@opencode-ai/core/mcp/index" import { McpInstructions } from "@opencode-ai/core/mcp/instructions" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { McpTool } from "@opencode-ai/core/tool/mcp" import { it } from "./lib/effect" import { readInitial, readUpdate } from "./lib/instructions" -const build = AgentV2.ID.make("build") +const build = Agent.ID.make("build") -const selection = (permissions: PermissionV2.Ruleset = []) => { - const info = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions }) +const selection = (permissions: Permission.Ruleset = []) => { + const info = Agent.Info.make({ ...Agent.Info.empty(build), permissions }) return { id: info.id, info } } diff --git a/packages/core/test/mcp.test.ts b/packages/core/test/mcp.test.ts index 8b948c9cc9b..b33fc860d75 100644 --- a/packages/core/test/mcp.test.ts +++ b/packages/core/test/mcp.test.ts @@ -16,18 +16,18 @@ import { Config } from "@opencode-ai/core/config" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" import { MCP } from "@opencode-ai/core/mcp/index" import { MCPClient } from "@opencode-ai/core/mcp/client" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { McpTool } from "@opencode-ai/core/tool/mcp" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Tool } from "@opencode-ai/core/tool" import { Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" @@ -35,8 +35,8 @@ import { imagePassthrough } from "./lib/image" import { location } from "./fixture/location" import { executeTool, toolDefinitions, toolIdentity, waitForCodeModeTool, waitForTool } from "./lib/tool" -let assertion: Deferred.Deferred | undefined -let decision: Effect.Effect = Effect.void +let assertion: Deferred.Deferred | undefined +let decision: Effect.Effect = Effect.void let calls = 0 type ResourcePage = { @@ -182,14 +182,14 @@ function resourceMcpLayer( }), ), Layer.succeed(Location.Service, Location.Service.of(location({ directory }))), - Layer.mock(EventV2.Service, { + Layer.mock(Bus.Service, { subscribe: () => Stream.never, publish: (definition, data) => { const event = { - id: EventV2.ID.create(), + id: Event.ID.create(), type: definition.type, data, - } as EventV2.Payload + } as Event.Payload if (event.type !== Form.Event.Created.type || !onFormCreated) return Effect.succeed(event) return onFormCreated(Schema.decodeUnknownSync(Form.Event.Created.data)(data).form).pipe(Effect.as(event)) }, @@ -285,7 +285,7 @@ const mcp = Layer.mock(MCP.Service, { }) }), }) -const permissions = Layer.mock(PermissionV2.Service, { +const permissions = Layer.mock(Permission.Service, { assert: (input) => Effect.gen(function* () { if (!assertion) return yield* Effect.die("Permission test is not initialized") @@ -293,13 +293,12 @@ const permissions = Layer.mock(PermissionV2.Service, { yield* decision }), }) -const events = Layer.mock(EventV2.Service, { subscribe: () => Stream.never }) +const events = Layer.mock(Bus.Service, { subscribe: () => Stream.never }) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, McpTool.node]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, McpTool.node]), [ [MCP.node, mcp], - [PermissionV2.node, permissions], - [EventV2.node, events], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permissions], + [Bus.node, events], [Image.node, imagePassthrough], ]), ) @@ -801,7 +800,7 @@ test("serializes concurrent MCP lifecycle operations", async () => { it.effect("advertises MCP output schemas to Code Mode", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const toolSet = yield* waitForCodeModeTool(registry, "demo.search") const execute = toolSet.definitions.find((tool) => tool.name === "execute") @@ -818,7 +817,7 @@ it.effect("advertises MCP output schemas to Code Mode", () => it.effect("advertises MCP tools directly when Code Mode is disabled for the server", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, "direct_lookup") const definitions = yield* toolDefinitions(registry) const execute = definitions.find((tool) => tool.name === "execute") @@ -832,38 +831,35 @@ it.effect("advertises MCP tools directly when Code Mode is disabled for the serv // success whose text happens to describe an error. it.effect("fails the call when MCP reports isError", () => Effect.gen(function* () { - assertion = yield* Deferred.make() + assertion = yield* Deferred.make() decision = Effect.void - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, "direct_fail") const execution = yield* executeTool(registry, { - sessionID: SessionV2.ID.make("ses_mcp_is_error"), + sessionID: Session.ID.make("ses_mcp_is_error"), ...toolIdentity, call: { type: "tool-call", id: "call_mcp_is_error", name: "direct_fail", input: {} }, }) expect(execution).toMatchObject({ status: "error", error: { message: "search index unavailable" } }) - expect(execution.content).toBeUndefined() }), ) // Baseline (PLAN.md step 1): mixed MCP text and media content must reach the model intact. it.effect("preserves MCP text and media content for the model", () => Effect.gen(function* () { - assertion = yield* Deferred.make() + assertion = yield* Deferred.make() decision = Effect.void - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, "direct_media") const execution = yield* executeTool(registry, { - sessionID: SessionV2.ID.make("ses_mcp_media"), + sessionID: Session.ID.make("ses_mcp_media"), ...toolIdentity, call: { type: "tool-call", id: "call_mcp_media", name: "direct_media", input: {} }, }) - expect(execution.status).toBe("completed") - if (execution.status !== "completed") return expect(execution.output).toBe("rendered chart") expect(execution.content).toMatchObject([ { type: "text", text: "rendered chart" }, @@ -875,14 +871,14 @@ it.effect("preserves MCP text and media content for the model", () => it.effect("waits for permission before calling an MCP tool", () => Effect.gen(function* () { calls = 0 - assertion = yield* Deferred.make() + assertion = yield* Deferred.make() const permission = yield* Deferred.make() decision = Deferred.await(permission) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const toolSet = yield* waitForCodeModeTool(registry, "demo.search") const fiber = yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_mcp_permission"), + sessionID: Session.ID.make("ses_mcp_permission"), ...toolIdentity, call: { type: "tool-call", @@ -896,7 +892,7 @@ it.effect("waits for permission before calling an MCP tool", () => resources: ["*"], save: ["*"], metadata: {}, - sessionID: SessionV2.ID.make("ses_mcp_permission"), + sessionID: Session.ID.make("ses_mcp_permission"), agent: toolIdentity.agent, source: { type: "tool", @@ -915,13 +911,13 @@ it.effect("waits for permission before calling an MCP tool", () => it.effect("does not call MCP when permission is blocked", () => Effect.gen(function* () { calls = 0 - assertion = yield* Deferred.make() - decision = Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "demo_search", resources: ["*"] })) - const registry = yield* ToolRegistry.Service + assertion = yield* Deferred.make() + decision = Effect.fail(new Permission.BlockedError({ rules: [], permission: "demo_search", resources: ["*"] })) + const registry = yield* Tool.Service const toolSet = yield* waitForCodeModeTool(registry, "demo.search") const execution = yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_mcp_blocked"), + sessionID: Session.ID.make("ses_mcp_blocked"), ...toolIdentity, call: { type: "tool-call", @@ -930,7 +926,6 @@ it.effect("does not call MCP when permission is blocked", () => input: { code: "return await tools.demo.search({})" }, }, }) - expect(execution.status).toBe("completed") expect(execution.content).toEqual([{ type: "text", text: "Unable to execute demo_search" }]) expect(execution.metadata).toEqual({ toolCalls: [{ tool: "demo.search", status: "error" }], diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 9eedc4265b6..547893a088f 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -5,25 +5,25 @@ import { Effect } from "effect" import { Headers } from "effect/unstable/http" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Compatibility, ID, Info, VariantID } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { ModelResolver } from "@opencode-ai/core/model-resolver" import { it } from "./lib/effect" interface ModelOptions { readonly modelID?: string - readonly compatibility?: ModelV2.Compatibility - readonly settings?: ModelV2.Info["settings"] - readonly headers?: ModelV2.Info["headers"] - readonly body?: ModelV2.Info["body"] - readonly variants?: ModelV2.Info["variants"] + readonly compatibility?: Compatibility + readonly settings?: Info["settings"] + readonly headers?: Info["headers"] + readonly body?: Info["body"] + readonly variants?: Info["variants"] } const model = (packageName: string | undefined, options: ModelOptions = {}) => - ModelV2.Info.make({ - id: ModelV2.ID.make("test-model"), - modelID: ModelV2.ID.make(options.modelID ?? "api-test-model"), - providerID: ProviderV2.ID.make("test-provider"), + Info.make({ + id: ID.make("test-model"), + modelID: ID.make(options.modelID ?? "api-test-model"), + providerID: Provider.ID.make("test-provider"), name: "Test model", compatibility: options.compatibility, package: packageName, @@ -42,12 +42,12 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) => describe("ModelResolver", () => { it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) const resolved = yield* ModelResolver.fromCatalogModel(catalog) - expect(catalog.id).toBe(ModelV2.ID.make("test-model")) + expect(catalog.id).toBe(ID.make("test-model")) expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) expect(resolved.route).toMatchObject({ id: "openai-responses", @@ -65,7 +65,7 @@ describe("ModelResolver", () => { it.effect("keeps catalog apiKey credentials out of provider JSON", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { apiKey: "secret", baseURL: "https://openai.example/v1" }, }), ) @@ -79,7 +79,7 @@ describe("ModelResolver", () => { it.effect("treats an empty configured API key as omitted", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { apiKey: "", baseURL: "https://openai.example/v1" }, }), ) @@ -98,7 +98,7 @@ describe("ModelResolver", () => { it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { + model(Provider.aisdk("@ai-sdk/openai-compatible"), { compatibility: { reasoningField: "vendor_reasoning" }, settings: { apiKey: "settings-secret", @@ -128,11 +128,11 @@ describe("ModelResolver", () => { it.effect("overlays selected OpenAI variant settings and bodies", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, variants: [ { - id: ModelV2.VariantID.make("high"), + id: VariantID.make("high"), settings: { reasoningEffort: "high" }, headers: { "x-variant": "high" }, body: { @@ -143,7 +143,7 @@ describe("ModelResolver", () => { }, ], }) - const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) + const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high")) expect(resolved.route.defaults.headers).toMatchObject({ "x-test": "header", "x-variant": "high" }) expect(resolved.route.defaults.http?.body).toEqual({ @@ -160,18 +160,18 @@ describe("ModelResolver", () => { it.effect("overlays selected OpenAI-compatible variant bodies", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai-compatible"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai-compatible"), { settings: { baseURL: "https://compatible.example/v1" }, variants: [ { - id: ModelV2.VariantID.make("high"), + id: VariantID.make("high"), settings: {}, headers: {}, body: { store: false, reasoning_effort: "high" }, }, ], }) - const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) + const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -183,10 +183,10 @@ describe("ModelResolver", () => { it.effect("rejects an explicit unavailable variant during model resolution", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/openai"), { + const catalog = model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }) - const failure = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("unknown")).pipe(Effect.flip) + const failure = yield* ModelResolver.resolveModel(catalog, VariantID.make("unknown")).pipe(Effect.flip) expect(failure).toMatchObject({ _tag: "SessionRunnerModel.VariantUnavailableError", @@ -200,18 +200,18 @@ describe("ModelResolver", () => { it.effect("overlays selected Anthropic variant settings", () => Effect.gen(function* () { - const catalog = model(ProviderV2.aisdk("@ai-sdk/anthropic"), { + const catalog = model(Provider.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, variants: [ { - id: ModelV2.VariantID.make("high"), + id: VariantID.make("high"), settings: { thinking: { type: "enabled", budgetTokens: 12000 } }, headers: {}, body: {}, }, ], }) - const resolved = yield* ModelResolver.resolveModel(catalog, ModelV2.VariantID.make("high")) + const resolved = yield* ModelResolver.resolveModel(catalog, VariantID.make("high")) expect(resolved.route.defaults.http?.body).toEqual({ custom_extension: { enabled: true }, @@ -225,7 +225,7 @@ describe("ModelResolver", () => { it.effect("maps catalog Anthropic AI SDK models into native routes", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/anthropic"), { + model(Provider.aisdk("@ai-sdk/anthropic"), { settings: { baseURL: "https://anthropic.example/v1" }, }), ) @@ -241,7 +241,7 @@ describe("ModelResolver", () => { it.effect("uses resolved credentials for bearer auth", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -265,7 +265,7 @@ describe("ModelResolver", () => { Effect.gen(function* () { const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { apiKey: "configured-secret", baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -288,7 +288,7 @@ describe("ModelResolver", () => { it.effect("does not project OAuth account metadata into the request body", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -310,7 +310,7 @@ describe("ModelResolver", () => { it.effect("routes ChatGPT OAuth credentials to the codex backend", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -395,7 +395,7 @@ describe("ModelResolver", () => { it.effect("maps legacy OpenAI organization and project settings to headers", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { organization: "org_123", project: "proj_123" }, }), ) @@ -410,7 +410,7 @@ describe("ModelResolver", () => { it.effect("routes ChatGPT OAuth credentials without an account id to the codex backend", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -441,7 +441,7 @@ describe("ModelResolver", () => { it.effect("keeps non-ChatGPT OAuth credentials on the configured endpoint", () => Effect.gen(function* () { const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, headers: {}, body: {}, @@ -473,7 +473,7 @@ describe("ModelResolver", () => { it.effect("loads dynamic native provider packages through the injected package loader", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) @@ -510,7 +510,7 @@ describe("ModelResolver", () => { it.effect("maps OAuth credentials to native provider auth settings", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) @@ -549,12 +549,12 @@ describe("ModelResolver", () => { it.effect("loads arbitrary AISDK packages through the injected AISDK loader", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) const resolved = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/google"), { + model(Provider.aisdk("@ai-sdk/google"), { modelID: "gemini-api-model", settings: { project: "test" }, headers: { "x-aisdk": "header" }, @@ -568,7 +568,7 @@ describe("ModelResolver", () => { id: "test-model", modelID: "gemini-api-model", providerID: "test-provider", - package: ProviderV2.aisdk("@ai-sdk/google"), + package: Provider.aisdk("@ai-sdk/google"), settings: { project: "test", apiKey: "fallback-secret" }, headers: { "x-aisdk": "header" }, body: { custom: true }, @@ -589,7 +589,7 @@ describe("ModelResolver", () => { it.effect("rejects AISDK packages without an available loader", () => Effect.gen(function* () { const failure = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/google"), { + model(Provider.aisdk("@ai-sdk/google"), { settings: { baseURL: "https://google.example/v1" }, }), ).pipe(Effect.flip) @@ -607,12 +607,12 @@ describe("ModelResolver", () => { it.effect("drops an empty API key before loading an AISDK package", () => Effect.gen(function* () { const native = yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/openai"), { + model(Provider.aisdk("@ai-sdk/openai"), { settings: { baseURL: "https://openai.example/v1" }, }), ) yield* ModelResolver.fromCatalogModel( - model(ProviderV2.aisdk("@ai-sdk/google"), { + model(Provider.aisdk("@ai-sdk/google"), { settings: { apiKey: "", baseURL: "https://google.example/v1" }, }), undefined, @@ -629,7 +629,7 @@ describe("ModelResolver", () => { it.effect("reports whether a catalog model declares a provider package", () => Effect.sync(() => { - expect(ModelResolver.supported(model(ProviderV2.aisdk("@ai-sdk/openai")))).toBe(true) + expect(ModelResolver.supported(model(Provider.aisdk("@ai-sdk/openai")))).toBe(true) expect(ModelResolver.supported(model("@opencode-ai/ai/providers/custom"))).toBe(true) expect(ModelResolver.supported(model(undefined))).toBe(false) }), diff --git a/packages/core/test/model.test.ts b/packages/core/test/model.test.ts index fe97acc25aa..0c545eeeefd 100644 --- a/packages/core/test/model.test.ts +++ b/packages/core/test/model.test.ts @@ -1,23 +1,23 @@ import { describe, expect, test } from "bun:test" import { Schema } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" -const decode = Schema.decodeUnknownSync(ModelV2.Ref) +const decode = Schema.decodeUnknownSync(Model.Ref) -describe("ModelV2.Ref", () => { +describe("Model.Ref", () => { test("accepts a model selection without a variant", () => { expect(decode({ id: "claude-sonnet", providerID: "anthropic" })).toEqual({ - id: ModelV2.ID.make("claude-sonnet"), - providerID: ProviderV2.ID.make("anthropic"), + id: Model.ID.make("claude-sonnet"), + providerID: Provider.ID.make("anthropic"), }) }) test("preserves an explicit model variant", () => { expect(decode({ id: "claude-sonnet", providerID: "anthropic", variant: "high" })).toEqual({ - id: ModelV2.ID.make("claude-sonnet"), - providerID: ProviderV2.ID.make("anthropic"), - variant: ModelV2.VariantID.make("high"), + id: Model.ID.make("claude-sonnet"), + providerID: Provider.ID.make("anthropic"), + variant: Model.VariantID.make("high"), }) }) }) diff --git a/packages/core/test/models.test.ts b/packages/core/test/models.test.ts index 1d8bd4ae184..e848f65c98d 100644 --- a/packages/core/test/models.test.ts +++ b/packages/core/test/models.test.ts @@ -6,9 +6,9 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Global } from "@opencode-ai/util/global" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { ModelsDev } from "@opencode-ai/core/models-dev" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { it } from "./lib/effect" import { readFile, rm, writeFile, utimes, mkdir } from "fs/promises" import path from "path" @@ -16,10 +16,10 @@ import path from "path" const cacheFile = path.join(Global.Path.cache, "models.json") test("normalizes permissive interleaved values to compatibility", () => { - expect(ModelV2.compatibility("reasoning_text")).toEqual({ reasoningField: "reasoning_text" }) - expect(ModelV2.compatibility({ field: "vendor_reasoning" })).toEqual({ reasoningField: "vendor_reasoning" }) - expect(ModelV2.compatibility(true)).toBeUndefined() - expect(ModelV2.compatibility(false)).toBeUndefined() + expect(Model.compatibility("reasoning_text")).toEqual({ reasoningField: "reasoning_text" }) + expect(Model.compatibility({ field: "vendor_reasoning" })).toEqual({ reasoningField: "vendor_reasoning" }) + expect(Model.compatibility(true)).toBeUndefined() + expect(Model.compatibility(false)).toBeUndefined() }) const fixture = { @@ -47,15 +47,15 @@ const fixture = { const fixtureSnapshot = [ { info: { - id: ProviderV2.ID.make("acme"), + id: Provider.ID.make("acme"), name: "Acme", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), }, models: [ { - id: ModelV2.ID.make("acme-1"), - modelID: ModelV2.ID.make("acme-1"), - providerID: ProviderV2.ID.make("acme"), + id: Model.ID.make("acme-1"), + modelID: Model.ID.make("acme-1"), + providerID: Provider.ID.make("acme"), name: "Acme One", compatibility: { reasoningField: "vendor_reasoning" }, family: undefined, @@ -109,15 +109,15 @@ const fixture2 = { const fixture2Snapshot = [ { info: { - id: ProviderV2.ID.make("beta"), + id: Provider.ID.make("beta"), name: "Beta", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), }, models: [ { - id: ModelV2.ID.make("beta-1"), - modelID: ModelV2.ID.make("beta-1"), - providerID: ProviderV2.ID.make("beta"), + id: Model.ID.make("beta-1"), + modelID: Model.ID.make("beta-1"), + providerID: Provider.ID.make("beta"), name: "Beta One", family: undefined, package: undefined, diff --git a/packages/core/test/move-session.test.ts b/packages/core/test/move-session.test.ts index 952bd292413..9a37f143b16 100644 --- a/packages/core/test/move-session.test.ts +++ b/packages/core/test/move-session.test.ts @@ -8,13 +8,13 @@ import { MoveSession } from "@opencode-ai/core/control-plane/move-session" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionTable } from "@opencode-ai/core/session/sql" @@ -40,10 +40,10 @@ const it = testEffect( LayerNode.group([ MoveSession.node, Database.node, - EventV2.node, + Bus.node, ProjectDirectories.node, Project.node, - SessionV2.node, + Session.node, SessionProjector.node, SessionStore.node, ]), @@ -86,7 +86,7 @@ describe("MoveSession", () => { yield* Effect.promise(() => fs.writeFile(path.join(source, "untracked.txt"), "new\n")) const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id - const sessionID = SessionV2.ID.make("ses_move") + const sessionID = Session.ID.make("ses_move") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -142,7 +142,7 @@ describe("MoveSession", () => { yield* Effect.promise(() => fs.writeFile(path.join(source, "untracked.txt"), "new\n")) const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id - const sessionID = SessionV2.ID.make("ses_move_nested") + const sessionID = Session.ID.make("ses_move_nested") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -164,7 +164,7 @@ describe("MoveSession", () => { .run() .pipe(Effect.orDie) - const missing = yield* SessionV2.Service.use((service) => + const missing = yield* Session.Service.use((service) => service.move({ sessionID, directory: abs("packages") }).pipe(Effect.flip), ) expect(missing._tag).toBe("Session.DestinationNotFoundError") @@ -202,7 +202,7 @@ describe("MoveSession", () => { const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id const destinationProjectID = (yield* Project.Service.use((service) => service.resolve(destination))).id - const sessionID = SessionV2.ID.make("ses_move_project") + const sessionID = Session.ID.make("ses_move_project") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -224,7 +224,7 @@ describe("MoveSession", () => { .run() .pipe(Effect.orDie) - yield* SessionV2.Service.use((service) => + yield* Session.Service.use((service) => service.move({ sessionID, directory: destination }), ) @@ -266,7 +266,7 @@ describe("MoveSession", () => { yield* Effect.promise(() => fs.writeFile(path.join(source, "untracked.txt"), "unrelated\n")) const projectID = (yield* Project.Service.use((service) => service.resolve(source))).id - const sessionID = SessionV2.ID.make("ses_move_nested_checkout") + const sessionID = Session.ID.make("ses_move_nested_checkout") const { db } = yield* Database.Service yield* db .insert(ProjectTable) diff --git a/packages/core/test/permission.test.ts b/packages/core/test/permission.test.ts index 6abf9c47ca6..7e7ac0392c6 100644 --- a/packages/core/test/permission.test.ts +++ b/packages/core/test/permission.test.ts @@ -1,19 +1,19 @@ import { describe, expect } from "bun:test" import { Cause, Deferred, Effect, Fiber, Layer } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { PermissionTable } from "@opencode-ai/core/permission/sql" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { eq } from "drizzle-orm" @@ -28,17 +28,17 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionStore.node, PermissionSaved.node, - AgentV2.node, - PermissionV2.node, + Agent.node, + Permission.node, ]), [[Location.node, current]], ), ) -function setup(rules: PermissionV2.Ruleset = []) { +function setup(rules: Permission.Ruleset = []) { return Effect.gen(function* () { const { db } = yield* Database.Service yield* db @@ -50,7 +50,7 @@ function setup(rules: PermissionV2.Ruleset = []) { yield* db .insert(SessionTable) .values({ - id: SessionV2.ID.make("ses_test"), + id: Session.ID.make("ses_test"), project_id: Project.ID.global, slug: "test", directory: "/project", @@ -65,35 +65,35 @@ function setup(rules: PermissionV2.Ruleset = []) { }) } -function setRules(rules: PermissionV2.Ruleset) { +function setRules(rules: Permission.Ruleset) { return Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("test"), (agent) => { + editor.update(Agent.ID.make("test"), (agent) => { agent.permissions = [...rules] }), ) }) } -function assertion(input: Partial = {}) { +function assertion(input: Partial = {}) { return { - id: PermissionV2.ID.create("per_test"), - sessionID: SessionV2.ID.make("ses_test"), + id: Permission.ID.create("per_test"), + sessionID: Session.ID.make("ses_test"), action: "read", resources: ["src/index.ts"], ...input, - } satisfies PermissionV2.AssertInput + } satisfies Permission.AssertInput } function waitForRequest() { return Effect.gen(function* () { - const service = yield* PermissionV2.Service - const events = yield* EventV2.Service - const asked = yield* Deferred.make() - const unsubscribe = yield* events.listen((event) => - event.type === PermissionV2.Event.Asked.type - ? Deferred.succeed(asked, event.data as PermissionV2.Request).pipe(Effect.asVoid) + const service = yield* Permission.Service + const bus = yield* Bus.Service + const asked = yield* Deferred.make() + const unsubscribe = yield* bus.listen((event) => + event.type === Permission.Event.Asked.type + ? Deferred.succeed(asked, event.data as Permission.Request).pipe(Effect.asVoid) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -103,53 +103,53 @@ function waitForRequest() { }) } -describe("PermissionV2", () => { +describe("Permission", () => { it.effect("returns the evaluated effect and only queues prompts", () => Effect.gen(function* () { yield* setup([{ action: "read", resource: "*", effect: "allow" }]) - const service = yield* PermissionV2.Service - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "allow" }) + const service = yield* Permission.Service + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "allow" }) expect(yield* service.list()).toEqual([]) yield* setRules([{ action: "read", resource: "*", effect: "deny" }]) - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "deny" }) + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "deny" }) expect(yield* service.list()).toEqual([]) yield* setRules([]) - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "ask" }) - expect(yield* service.get(PermissionV2.ID.create("per_test"))).toBeDefined() + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "ask" }) + expect(yield* service.get(Permission.ID.create("per_test"))).toBeDefined() }), ) it.effect("evaluates against an explicit provider-turn agent", () => Effect.gen(function* () { yield* setup([{ action: "read", resource: "*", effect: "allow" }]) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.permissions.push({ action: "read", resource: "*", effect: "deny" }) }), ) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" }) - expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" }) + expect(yield* service.ask(assertion({ agent: Agent.ID.make("reviewer") }))).toMatchObject({ effect: "deny" }) yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.permissions = [] }), ) - expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "ask" }) - expect(yield* service.get(PermissionV2.ID.create("per_test"))).not.toHaveProperty("agent") + expect(yield* service.ask(assertion({ agent: Agent.ID.make("reviewer") }))).toMatchObject({ effect: "ask" }) + expect(yield* service.get(Permission.ID.create("per_test"))).not.toHaveProperty("agent") }), ) it.effect("allows and denies from explicit rules without asking", () => Effect.gen(function* () { yield* setup([{ action: "read", resource: "*", effect: "allow" }]) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service yield* service.assert(assertion()) yield* setRules([{ action: "read", resource: "*", effect: "deny" }]) const blocked = yield* service.assert(assertion()).pipe(Effect.flip) - expect(blocked).toBeInstanceOf(PermissionV2.BlockedError) + expect(blocked).toBeInstanceOf(Permission.BlockedError) expect(yield* service.list()).toEqual([]) }), ) @@ -160,7 +160,7 @@ describe("PermissionV2", () => { { action: "*", resource: "*", effect: "deny" }, { action: "read", resource: "*", effect: "allow" }, ]) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion({ resources: ["tool_123"] }))).toMatchObject({ effect: "allow" }) expect( @@ -176,19 +176,19 @@ describe("PermissionV2", () => { yield* db .update(SessionTable) .set({ agent: null }) - .where(eq(SessionTable.id, SessionV2.ID.make("ses_test"))) + .where(eq(SessionTable.id, Session.ID.make("ses_test"))) .run() .pipe(Effect.orDie) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.permissions = [{ action: "custom", resource: "*", effect: "allow" }] }), ) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion({ action: "custom", resources: ["*"] }))).toEqual({ - id: PermissionV2.ID.create("per_test"), + id: Permission.ID.create("per_test"), effect: "allow", }) expect(yield* service.list()).toEqual([]) @@ -202,17 +202,17 @@ describe("PermissionV2", () => { yield* db .update(SessionTable) .set({ agent: null }) - .where(eq(SessionTable.id, SessionV2.ID.make("ses_test"))) + .where(eq(SessionTable.id, Session.ID.make("ses_test"))) .run() .pipe(Effect.orDie) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => { - editor.remove(AgentV2.ID.make("test")) - editor.remove(AgentV2.ID.make("build")) + editor.remove(Agent.ID.make("test")) + editor.remove(Agent.ID.make("build")) }) - const service = yield* PermissionV2.Service - expect(yield* service.ask(assertion())).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "deny" }) + const service = yield* Permission.Service + expect(yield* service.ask(assertion())).toEqual({ id: Permission.ID.create("per_test"), effect: "deny" }) expect(yield* service.list()).toEqual([]) }), ) @@ -220,13 +220,13 @@ describe("PermissionV2", () => { it.effect("evaluates bash with the normal configured-rule semantics", () => Effect.gen(function* () { yield* setup([{ action: "*", resource: "*", effect: "allow" }]) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service const bash = assertion({ action: "bash", resources: ["pwd"] }) - expect(yield* service.ask(bash)).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "allow" }) + expect(yield* service.ask(bash)).toEqual({ id: Permission.ID.create("per_test"), effect: "allow" }) yield* setRules([]) - expect(yield* service.ask(bash)).toEqual({ id: PermissionV2.ID.create("per_test"), effect: "ask" }) - expect(yield* service.get(PermissionV2.ID.create("per_test"))).toBeDefined() + expect(yield* service.ask(bash)).toEqual({ id: Permission.ID.create("per_test"), effect: "ask" }) + expect(yield* service.get(Permission.ID.create("per_test"))).toBeDefined() }), ) @@ -236,16 +236,16 @@ describe("PermissionV2", () => { const saved = yield* PermissionSaved.Service yield* saved.add({ projectID: Project.ID.global, action: "bash", resources: ["pwd"] }) - const service = yield* PermissionV2.Service + const service = yield* Permission.Service expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toEqual({ - id: PermissionV2.ID.create("per_test"), + id: Permission.ID.create("per_test"), effect: "allow", }) expect(yield* service.list()).toEqual([]) yield* setRules([{ action: "bash", resource: "*", effect: "deny" }]) expect(yield* service.ask(assertion({ action: "bash", resources: ["pwd"] }))).toEqual({ - id: PermissionV2.ID.create("per_test"), + id: Permission.ID.create("per_test"), effect: "deny", }) }), @@ -257,7 +257,7 @@ describe("PermissionV2", () => { const { service, fiber, request } = yield* waitForRequest() expect(yield* service.list()).toEqual([request]) expect(yield* service.forSession(request.sessionID)).toEqual([request]) - expect(yield* service.forSession(SessionV2.ID.make("ses_other"))).toEqual([]) + expect(yield* service.forSession(Session.ID.make("ses_other"))).toEqual([]) expect(yield* service.get(request.id)).toEqual(request) yield* service.reply({ requestID: request.id, reply: "once" }) yield* Fiber.join(fiber) @@ -277,7 +277,7 @@ describe("PermissionV2", () => { if (exit._tag === "Failure") expect( exit.cause.reasons.some( - (reason) => Cause.isDieReason(reason) && reason.defect instanceof PermissionV2.DeclinedError, + (reason) => Cause.isDieReason(reason) && reason.defect instanceof Permission.DeclinedError, ), ).toBe(true) expect(yield* service.list()).toEqual([]) @@ -287,12 +287,12 @@ describe("PermissionV2", () => { it.effect("stores and removes saved resources for a project", () => Effect.gen(function* () { yield* setup() - const service = yield* PermissionV2.Service - const asked = yield* Deferred.make() - const events = yield* EventV2.Service - const unsubscribe = yield* events.listen((event) => - event.type === PermissionV2.Event.Asked.type - ? Deferred.succeed(asked, event.data as PermissionV2.Request).pipe(Effect.asVoid) + const service = yield* Permission.Service + const asked = yield* Deferred.make() + const bus = yield* Bus.Service + const unsubscribe = yield* bus.listen((event) => + event.type === Permission.Event.Asked.type + ? Deferred.succeed(asked, event.data as Permission.Request).pipe(Effect.asVoid) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -308,7 +308,7 @@ describe("PermissionV2", () => { const saved = yield* PermissionSaved.Service const id = (yield* saved.list())[0]!.id expect(yield* saved.list()).toEqual([{ id, projectID: Project.ID.global, action: "read", resource: "src/*" }]) - yield* service.assert(assertion({ id: PermissionV2.ID.create("per_next"), resources: ["src/next.ts"] })) + yield* service.assert(assertion({ id: Permission.ID.create("per_next"), resources: ["src/next.ts"] })) yield* saved.remove(id) expect(yield* saved.list()).toEqual([]) }), diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index ca191a8b56d..e2f0b88a94f 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -1,16 +1,14 @@ import { describe, expect } from "bun:test" import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect" -import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" -import { Plugin } from "@opencode-ai/schema/plugin" -import { AgentV2 } from "@opencode-ai/core/agent" -import { EventV2 } from "@opencode-ai/core/event" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Agent } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" -import { Tool } from "@opencode-ai/core/tool/tool" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Tool } from "@opencode-ai/core/tool" import { testEffect } from "./lib/effect" import { PluginTestLayer } from "./plugin/fixture" @@ -20,11 +18,11 @@ class Secret extends Context.Service()("@opencode/test/PluginSec const versioned = (plugin: EffectPlugin.Plugin, version = "1") => ({ ...plugin, version }) -describe("PluginV2", () => { +describe("Plugin", () => { it.live("exposes public events through the plugin context", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const events = yield* EventV2.Service + const plugins = yield* Plugin.Service + const bus = yield* Bus.Service const host = yield* PluginHost.make(plugins) const received = yield* host.event.subscribe().pipe( Stream.filter((event) => event.type === "config.updated"), @@ -33,7 +31,7 @@ describe("PluginV2", () => { ) yield* Effect.sleep("10 millis") - yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* bus.publish(ConfigSchema.Event.Updated, {}) expect((yield* Fiber.join(received)).valueOrUndefined?.type).toBe("config.updated") }), @@ -41,12 +39,12 @@ describe("PluginV2", () => { it.effect("replaces plugins by ID and version", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service - const events = yield* EventV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service + const bus = yield* Bus.Service let description = "first" let updates = 0 - const unsubscribe = yield* events.listen((event) => + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { if (event.type === Plugin.Event.Updated.type) updates++ }), @@ -67,19 +65,19 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(managed(), "1")]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("first") description = "second" yield* plugins.activate([versioned(managed(), "2")]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second") description = "third" yield* plugins.activate([versioned(managed(), "2")]) expect(updates).toBe(2) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second") yield* plugins.activate([]) - expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined() expect(updates).toBe(3) yield* unsubscribe }), @@ -87,7 +85,7 @@ describe("PluginV2", () => { it.effect("rejects duplicate IDs before replacing active plugins", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const active = Plugin.ID.make("active") const duplicate = "duplicate" yield* plugins.activate([{ id: active, version: "1", effect: () => Effect.void }]) @@ -106,8 +104,8 @@ describe("PluginV2", () => { it.effect("skips failed plugins and loads the rest", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service let fail = true const good = EffectPlugin.define({ id: "good", @@ -130,7 +128,7 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(good), versioned(bad)]) expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("good") }]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("loaded") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("loaded") fail = false yield* plugins.activate([versioned(good), versioned(bad, "2")]) @@ -140,8 +138,8 @@ describe("PluginV2", () => { it.effect("restores the previous plugin when its replacement fails", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service const previous = EffectPlugin.define({ id: "managed", effect: (ctx) => @@ -170,14 +168,14 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(replacement, "2")]) expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("managed") }]) - expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("previous") + expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("previous") }), ) it.effect("deactivates a plugin when replacement and restoration fail", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const agents = yield* AgentV2.Service + const plugins = yield* Plugin.Service + const agents = yield* Agent.Service let loads = 0 const previous = EffectPlugin.define({ id: "managed", @@ -202,13 +200,13 @@ describe("PluginV2", () => { yield* plugins.activate([versioned(replacement, "2")]) expect(yield* plugins.list()).toEqual([]) - expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined() }), ) it.effect("closes the previous generation in reverse order", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service const closed: string[] = [] yield* plugins.activate( ["first", "second"].map((id) => ({ @@ -226,7 +224,7 @@ describe("PluginV2", () => { it.effect("isolates plugins from ambient services", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const plugins = yield* Plugin.Service let visible = true const plugin = EffectPlugin.define({ id: "isolated", @@ -245,22 +243,22 @@ describe("PluginV2", () => { it.effect("registers location tools through the plugin context", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service const plugin = EffectPlugin.define({ id: "tool-plugin", effect: (ctx) => ctx.tool .transform((draft) => draft.add( - "plugin_tool", - Tool.make({ + ({ + name: "plugin_tool", + options: { codemode: false }, description: "Plugin tool", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), execute: () => Effect.succeed({ output: { ok: true } }), }), - { codemode: false }, ), ) .pipe(Effect.orDie), @@ -276,10 +274,12 @@ describe("PluginV2", () => { it.effect("namespaces tool names and routes codemode registrations through execute", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service - const tool = (description: string) => - Tool.make({ + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service + const tool = (name: string, description: string, options?: Tool.Options) => + ({ + name, + options, description, input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), @@ -290,9 +290,9 @@ describe("PluginV2", () => { effect: (ctx) => ctx.tool .transform((draft) => { - draft.add("plain", tool("Plain"), { codemode: false }) - draft.add("look/up", tool("Lookup"), { namespace: "context7", codemode: false }) - draft.add("search", tool("Search"), { namespace: "context7" }) + draft.add(tool("plain", "Plain", { codemode: false })) + draft.add(tool("look/up", "Lookup", { namespace: "context7", codemode: false })) + draft.add(tool("search", "Search", { namespace: "context7" })) }) .pipe(Effect.orDie), }) @@ -309,8 +309,8 @@ describe("PluginV2", () => { it.effect("fires before/after tool hooks with mutable events around execution", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service const executed: unknown[] = [] const seen: { before?: unknown @@ -324,15 +324,15 @@ describe("PluginV2", () => { yield* ctx.tool .transform((draft) => draft.add( - "echo", - Tool.make({ + ({ + name: "echo", + options: { codemode: false }, description: "Echo", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ output: { text } })), }), - { codemode: false }, ), ) .pipe(Effect.orDie) @@ -352,12 +352,15 @@ describe("PluginV2", () => { seen.after = { input: event.input, status: event.status, - content: event.content, - metadata: event.metadata, + content: event.status === "completed" ? event.result.content : undefined, + metadata: event.status === "completed" ? event.result.metadata : event.error.metadata, } if (event.status !== "completed") return - event.content = [{ type: "text", text: "after-mutated" }] - event.metadata = { rewritten: true } + event.result = { + ...event.result, + content: [{ type: "text", text: "after-mutated" }], + metadata: { rewritten: true }, + } }), ) .pipe(Effect.asVoid) @@ -365,7 +368,7 @@ describe("PluginV2", () => { yield* ctx.tool .hook("execute.after", (event) => Effect.sync(() => { - if (event.status === "completed") (event.content as unknown as unknown[]).splice(0) + if (event.status === "completed" && Array.isArray(event.result.content)) event.result.content.splice(0) }), ) .pipe(Effect.asVoid) @@ -376,8 +379,8 @@ describe("PluginV2", () => { const toolSet = yield* registry.snapshot() const execution = yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_hooks"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_hooks"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_hooks"), call: { type: "tool-call", id: "call-hooks", name: "echo", input: { text: "original" } }, }) diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index 65848dbd722..f5dc96a8cea 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Location } from "@opencode-ai/core/location" import { CommandPlugin } from "@opencode-ai/core/plugin/command" @@ -15,12 +15,12 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory }, { projectDirectory: project })), ) -const it = testEffect(AppNodeBuilder.build(CommandV2.node, [[Location.node, locationLayer]])) +const it = testEffect(AppNodeBuilder.build(Command.node, [[Location.node, locationLayer]])) describe("CommandPlugin.Plugin", () => { it.effect("registers built-in init and review commands", () => Effect.gen(function* () { - const command = yield* CommandV2.Service + const command = yield* Command.Service yield* CommandPlugin.Plugin.effect( host({ command: { diff --git a/packages/core/test/plugin/fixture.ts b/packages/core/test/plugin/fixture.ts index fc56071558d..82ba8419d6f 100644 --- a/packages/core/test/plugin/fixture.ts +++ b/packages/core/test/plugin/fixture.ts @@ -1,26 +1,25 @@ -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AISDK } from "@opencode-ai/core/aisdk" import { Catalog } from "@opencode-ai/core/catalog" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { Config } from "@opencode-ai/core/config" import { Credential } from "@opencode-ai/core/credential" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/util/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" import { Npm } from "@opencode-ai/util/npm" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { Reference } from "@opencode-ai/core/reference" -import { SkillV2 } from "@opencode-ai/core/skill" -import { ToolHooks } from "@opencode-ai/core/tool/hooks" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Skill } from "@opencode-ai/core/skill" +import { Tool } from "@opencode-ai/core/tool" import { WebSearch } from "@opencode-ai/core/websearch" import { Effect, Layer } from "effect" import { tempLocationLayer } from "../fixture/location" @@ -41,21 +40,21 @@ export const PluginTestLayer = AppNodeBuilder.build( Location.node, Npm.node, Credential.node, - EventV2.node, + Bus.node, Form.node, LayerNodePlatform.httpClient, - PluginV2.node, - AgentV2.node, + Plugin.node, + Agent.node, AISDK.node, Catalog.node, - CommandV2.node, + Command.node, Integration.node, PluginRuntime.node, PluginHooks.node, Reference.node, - SkillV2.node, - ToolHooks.node, - ToolRegistry.toolsNode, + Skill.node, + PluginHooks.node, + Tool.node, WebSearch.node, ]), [ diff --git a/packages/core/test/plugin/fixtures/config-effect-plugin.ts b/packages/core/test/plugin/fixtures/config-effect-plugin.ts index e607b658e35..37546379b1b 100644 --- a/packages/core/test/plugin/fixtures/config-effect-plugin.ts +++ b/packages/core/test/plugin/fixtures/config-effect-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ diff --git a/packages/core/test/plugin/fixtures/config-promise-plugin.ts b/packages/core/test/plugin/fixtures/config-promise-plugin.ts index 0fef8fbc655..91f4a1b176e 100644 --- a/packages/core/test/plugin/fixtures/config-promise-plugin.ts +++ b/packages/core/test/plugin/fixtures/config-promise-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "config-promise-plugin", diff --git a/packages/core/test/plugin/fixtures/failing-plugin.ts b/packages/core/test/plugin/fixtures/failing-plugin.ts index dac49b410d7..f6db2fb411d 100644 --- a/packages/core/test/plugin/fixtures/failing-plugin.ts +++ b/packages/core/test/plugin/fixtures/failing-plugin.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ diff --git a/packages/core/test/plugin/fixtures/variant-source-plugin.ts b/packages/core/test/plugin/fixtures/variant-source-plugin.ts index 5dae699f6e5..08045a29000 100644 --- a/packages/core/test/plugin/fixtures/variant-source-plugin.ts +++ b/packages/core/test/plugin/fixtures/variant-source-plugin.ts @@ -1,5 +1,6 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Plugin } from "@opencode-ai/plugin/effect" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { Effect } from "effect" export default Plugin.define({ @@ -8,14 +9,14 @@ export default Plugin.define({ ctx.catalog .transform((catalog) => { catalog.provider.update("configured", (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") }) catalog.model.update("configured", "glm-5.2", (model) => { - model.modelID = "glm-5.2" - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + model.modelID = Model.ID.make("glm-5.2") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") model.variants = [ { - id: "high", + id: Model.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {}, diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index b7218ae690b..4087a38f9cd 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -1,21 +1,15 @@ -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Plugin } from "@opencode-ai/plugin/effect" +import type { IntegrationMethodRegistration } from "@opencode-ai/plugin/effect/integration" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { WebSearch } from "@opencode-ai/core/websearch" -import type { - CredentialOAuth, - IntegrationCommandMethod, - IntegrationEnvMethod, - IntegrationKeyMethod, - IntegrationOAuthMethod, -} from "@opencode-ai/sdk/v2/types" import { Effect, Stream } from "effect" type Overrides = Partial> & { @@ -41,7 +35,6 @@ export function host(overrides: Overrides = {}): Plugin.Context { get: () => Effect.die("unused catalog.provider.get"), }, model: { - get: () => Effect.die("unused catalog.model.get"), list: () => Effect.die("unused catalog.model.list"), default: () => Effect.die("unused catalog.model.default"), }, @@ -116,9 +109,22 @@ export function host(overrides: Overrides = {}): Plugin.Context { } } -export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] { +export function agentHost(agent: Agent.Interface): Plugin.Context["agent"] { return { - get: (id) => agent.get(AgentV2.ID.make(id)).pipe(Effect.map((value) => value && agentInfo(value))), + get: (input) => + agent.get(input.agentID).pipe( + Effect.flatMap((value) => + value + ? Effect.succeed({ + location: new Location.Info({ + directory: AbsolutePath.make("/"), + project: { id: Project.ID.make("test"), directory: AbsolutePath.make("/") }, + }), + data: agentInfo(value), + }) + : Effect.fail(new Error(`Agent not found: ${input.agentID}`)), + ), + ), list: () => Effect.die("unused agent.list"), reload: agent.reload, transform: (callback) => @@ -126,17 +132,17 @@ export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] { callback({ list: () => draft.list().map(agentInfo), get: (id) => { - const value = draft.get(AgentV2.ID.make(id)) + const value = draft.get(Agent.ID.make(id)) return value && agentInfo(value) }, - default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), + default: (id) => draft.default(id === undefined ? undefined : Agent.ID.make(id)), update: (id, update) => - draft.update(AgentV2.ID.make(id), (value) => { + draft.update(Agent.ID.make(id), (value) => { const current = agentInfo(value) update(current) - Object.assign(value, current, { id: AgentV2.ID.make(current.id) }) + Object.assign(value, current, { id: Agent.ID.make(current.id) }) }), - remove: (id) => draft.remove(AgentV2.ID.make(id)), + remove: (id) => draft.remove(Agent.ID.make(id)), }), ), } @@ -149,10 +155,6 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog get: () => Effect.die("unused catalog.provider.get"), }, model: { - get: (providerID, modelID) => - catalog.model - .get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)) - .pipe(Effect.map((value) => value && modelInfo(value))), list: () => Effect.die("unused catalog.model.list"), default: () => Effect.die("unused catalog.model.default"), }, @@ -167,7 +169,7 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])), })), get: (id) => { - const value = draft.provider.get(ProviderV2.ID.make(id)) + const value = draft.provider.get(Provider.ID.make(id)) return ( value && { provider: providerInfo(value.provider), @@ -176,41 +178,41 @@ export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog ) }, update: (id, update) => - draft.provider.update(ProviderV2.ID.make(id), (value) => { + draft.provider.update(Provider.ID.make(id), (value) => { const current = providerInfo(value) update(current) - Object.assign(value, current, { id: ProviderV2.ID.make(current.id) }) + Object.assign(value, current, { id: Provider.ID.make(current.id) }) }), - remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), + remove: (id) => draft.provider.remove(Provider.ID.make(id)), }, model: { get: (providerID, modelID) => { - const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)) + const value = draft.model.get(Provider.ID.make(providerID), Model.ID.make(modelID)) return value && modelInfo(value) }, update: (providerID, modelID, update) => - draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => { + draft.model.update(Provider.ID.make(providerID), Model.ID.make(modelID), (value) => { const current = modelInfo(value) update(current) Object.assign(value, current, { - id: ModelV2.ID.make(current.id), - providerID: ProviderV2.ID.make(current.providerID), - family: current.family === undefined ? undefined : ModelV2.Family.make(current.family), + id: Model.ID.make(current.id), + providerID: Provider.ID.make(current.providerID), + family: current.family === undefined ? undefined : Model.Family.make(current.family), variants: current.variants?.map((variant) => ({ ...variant, - id: ModelV2.VariantID.make(variant.id), + id: Model.VariantID.make(variant.id), })), }) }), remove: (providerID, modelID) => - draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.remove(Provider.ID.make(providerID), Model.ID.make(modelID)), default: { get: () => { const value = draft.model.default.get() return value && { providerID: value.providerID, modelID: value.modelID } }, set: (providerID, modelID) => - draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + draft.model.default.set(Provider.ID.make(providerID), Model.ID.make(modelID)), }, }, }), @@ -370,7 +372,7 @@ export function webSearchHost(websearch: WebSearch.Interface): Plugin.Context["w } } -function oauthCredential(value: CredentialOAuth) { +function oauthCredential(value: Credential.OAuth) { return Credential.OAuth.make({ ...value, methodID: Integration.MethodID.make(value.methodID) }) } @@ -390,7 +392,7 @@ function method(value: Integration.Method) { } function internalMethod( - value: IntegrationOAuthMethod | IntegrationCommandMethod | IntegrationKeyMethod | IntegrationEnvMethod, + value: IntegrationMethodRegistration["method"], ): Integration.Method { if (value.type === "env") return value if (value.type === "key") return value @@ -407,7 +409,7 @@ function internalMethod( } } -function agentInfo(value: AgentV2.Info) { +function agentInfo(value: Agent.Info) { return { ...value, model: value.model && { ...value.model }, @@ -420,7 +422,7 @@ function agentInfo(value: AgentV2.Info) { } } -function providerInfo(value: ProviderV2.MutableInfo) { +function providerInfo(value: Provider.MutableInfo) { return { ...value, settings: value.settings && { ...value.settings }, @@ -429,7 +431,7 @@ function providerInfo(value: ProviderV2.MutableInfo) { } } -function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) { +function modelInfo(value: Model.Info | Model.MutableInfo) { return { ...value, settings: value.settings && { ...value.settings }, diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index b08e6792c27..80e929bf3c7 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -6,12 +6,12 @@ import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { ModelsDev } from "@opencode-ai/core/models-dev" import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" @@ -21,7 +21,7 @@ const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })), ) -const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, EventV2.node]), [ +const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, Bus.node]), [ [Location.node, locationLayer], ]) const it = testEffect(layer) @@ -33,8 +33,8 @@ describe("ModelsDevPlugin", () => { Effect.gen(function* () { const integrations = yield* Integration.Service const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.make("acme") - const modelID = ModelV2.ID.make("gpt-5.4") + const providerID = Provider.ID.make("acme") + const modelID = Model.ID.make("gpt-5.4") const models = ModelsDev.Service.of({ get: () => Effect.succeed([ @@ -42,7 +42,7 @@ describe("ModelsDevPlugin", () => { info: { id: providerID, name: "Acme", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://api.acme.test/v1" }, }, environment: [], @@ -52,7 +52,7 @@ describe("ModelsDevPlugin", () => { modelID, providerID, name: "GPT-5.4", - family: ModelV2.Family.make("gpt"), + family: Model.Family.make("gpt"), capabilities: { tools: true, input: [], output: [] }, variants: [], time: { released: Date.parse("2026-01-01") }, @@ -89,12 +89,12 @@ describe("ModelsDevPlugin", () => { limit: { context: 1_050_000, input: 922_000, output: 128_000 }, }, { - id: ModelV2.ID.make("gpt-5.4-fast"), + id: Model.ID.make("gpt-5.4-fast"), modelID, providerID, name: "GPT-5.4 Fast", - family: ModelV2.Family.make("gpt"), - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + family: Model.Family.make("gpt"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://api.acme.test/v1" }, headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, @@ -146,8 +146,8 @@ describe("ModelsDevPlugin", () => { }), ).pipe(Effect.provideService(ModelsDev.Service, models)) - const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4")) - const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast")) + const base = yield* catalog.model.get(providerID, Model.ID.make("gpt-5.4")) + const fast = yield* catalog.model.get(providerID, Model.ID.make("gpt-5.4-fast")) expect(base?.variants).toEqual([]) expect(base?.body).toBeUndefined() @@ -156,7 +156,7 @@ describe("ModelsDevPlugin", () => { modelID: "gpt-5.4", providerID: "acme", name: "GPT-5.4 Fast", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: "https://api.acme.test/v1" }, headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, @@ -231,13 +231,13 @@ describe("ModelsDevPlugin", () => { }), ) - const model = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning")) + const model = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning")) expect(model?.variants?.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("low"), - ModelV2.VariantID.make("high"), + Model.VariantID.make("low"), + Model.VariantID.make("high"), ]) expect(model?.variants).toContainEqual({ - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningEffort: "low", reasoningSummary: "auto", @@ -245,7 +245,7 @@ describe("ModelsDevPlugin", () => { }, }) expect(model?.variants).toContainEqual({ - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningEffort: "high", reasoningSummary: "auto", @@ -253,7 +253,7 @@ describe("ModelsDevPlugin", () => { }, }) - const mode = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-high")) + const mode = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning-high")) expect(mode).toMatchObject({ id: "gpt-reasoning-high", name: "GPT Reasoning High", @@ -261,229 +261,229 @@ describe("ModelsDevPlugin", () => { body: { service_tier: "priority" }, }) expect(mode?.variants?.map((variant) => variant.id)).toEqual([ - ModelV2.VariantID.make("low"), - ModelV2.VariantID.make("high"), + Model.VariantID.make("low"), + Model.VariantID.make("high"), ]) - const pro = yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-reasoning-pro")) + const pro = yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-reasoning-pro")) expect(pro).toMatchObject({ id: "gpt-reasoning-pro", body: { reasoning: { mode: "pro" } }, }) - const budgetModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-budget")) + const budgetModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-budget")) expect(budgetModel?.variants).toContainEqual({ - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinking: { type: "enabled", budgetTokens: 16000 } }, }) expect(budgetModel?.variants).toContainEqual({ - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinking: { type: "enabled", budgetTokens: 31999 } }, }) - const anthropicEffortModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4.7")) + const anthropicEffortModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4.7")) expect(anthropicEffortModel?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { thinking: { type: "adaptive", display: "summarized" }, effort: "low" }, }, ]) - const anthropicToggleModel = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-toggle")) + const anthropicToggleModel = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-toggle")) expect(anthropicToggleModel?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { thinking: { type: "adaptive", display: "summarized" } }, }, ]) - const opus45 = yield* catalog.model.get(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-opus-4-5")) + const opus45 = yield* catalog.model.get(Provider.ID.anthropic, Model.ID.make("claude-opus-4-5")) expect(opus45?.variants).toEqual([ - { id: ModelV2.VariantID.make("low"), settings: { effort: "low" } }, - { id: ModelV2.VariantID.make("high"), settings: { effort: "high" } }, + { id: Model.VariantID.make("low"), settings: { effort: "low" } }, + { id: Model.VariantID.make("high"), settings: { effort: "high" } }, ]) - const grok = yield* catalog.model.get(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4.5")) + const grok = yield* catalog.model.get(Provider.ID.make("xai"), Model.ID.make("grok-4.5")) expect(grok?.variants).toEqual( ["low", "medium", "high"].map((id) => ({ - id: ModelV2.VariantID.make(id), + id: Model.VariantID.make(id), settings: { reasoningEffort: id }, })), ) - const minimax = yield* catalog.model.get(ProviderV2.ID.make("opencode-go"), ModelV2.ID.make("minimax-m3")) + const minimax = yield* catalog.model.get(Provider.ID.make("opencode-go"), Model.ID.make("minimax-m3")) expect(minimax?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, + { id: Model.VariantID.make("none"), settings: { thinking: { type: "disabled" } } }, { - id: ModelV2.VariantID.make("thinking"), + id: Model.VariantID.make("thinking"), settings: { thinking: { type: "adaptive", display: "summarized" } }, }, ]) - const toggle = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-only")) + const toggle = yield* catalog.model.get(Provider.ID.make("alibaba"), Model.ID.make("toggle-only")) expect(toggle?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, - { id: ModelV2.VariantID.make("thinking"), settings: { enableThinking: true } }, + { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, + { id: Model.VariantID.make("thinking"), settings: { enableThinking: true } }, ]) - const combined = yield* catalog.model.get(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("toggle-budget")) + const combined = yield* catalog.model.get(Provider.ID.make("alibaba"), Model.ID.make("toggle-budget")) expect(combined?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, + { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { enableThinking: true, thinkingBudget: 8000 }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { enableThinking: true, thinkingBudget: 16000 }, }, ]) - const gateway = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("alibaba/qwen-toggle")) + const gateway = yield* catalog.model.get(Provider.ID.make("vercel"), Model.ID.make("alibaba/qwen-toggle")) expect(gateway?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { enableThinking: false } }, + { id: Model.VariantID.make("none"), settings: { enableThinking: false } }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { enableThinking: true, thinkingBudget: 8000 }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { enableThinking: true, thinkingBudget: 16000 }, }, ]) - const gatewayNova = yield* catalog.model.get(ProviderV2.ID.make("vercel"), ModelV2.ID.make("amazon/nova-2-lite")) + const gatewayNova = yield* catalog.model.get(Provider.ID.make("vercel"), Model.ID.make("amazon/nova-2-lite")) expect(gatewayNova?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, }, ]) const gatewayFallback = yield* catalog.model.get( - ProviderV2.ID.make("vercel"), - ModelV2.ID.make("deepseek/deepseek-toggle"), + Provider.ID.make("vercel"), + Model.ID.make("deepseek/deepseek-toggle"), ) expect(gatewayFallback?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningEffort: "low" }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningEffort: "high" }, }, ]) const openrouter = yield* catalog.model.get( - ProviderV2.ID.make("openrouter"), - ModelV2.ID.make("openrouter-toggle"), + Provider.ID.make("openrouter"), + Model.ID.make("openrouter-toggle"), ) expect(openrouter?.variants).toEqual([ - { id: ModelV2.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, - { id: ModelV2.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, + { id: Model.VariantID.make("none"), settings: { reasoning: { enabled: false } } }, + { id: Model.VariantID.make("thinking"), settings: { reasoning: { enabled: true } } }, ]) - const google = yield* catalog.model.get(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini-2.5-flash")) + const google = yield* catalog.model.get(Provider.ID.make("google"), Model.ID.make("gemini-2.5-flash")) expect(google?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }, }, ]) const vertex = yield* catalog.model.get( - ProviderV2.ID.make("google-vertex"), - ModelV2.ID.make("gemini-2.5-flash-lite"), + Provider.ID.make("google-vertex"), + Model.ID.make("gemini-2.5-flash-lite"), ) expect(vertex?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }, }, ]) const bedrock = yield* catalog.model.get( - ProviderV2.ID.make("amazon-bedrock"), - ModelV2.ID.make("amazon.nova-2-lite-v1:0"), + Provider.ID.make("amazon-bedrock"), + Model.ID.make("amazon.nova-2-lite-v1:0"), ) expect(bedrock?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { additionalModelRequestFields: { reasoningConfig: { type: "disabled" } } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "low" } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }, }, ]) - const sapGemini = yield* catalog.model.get(ProviderV2.ID.make("sap-ai-core"), ModelV2.ID.make("gemini-2.5-flash")) + const sapGemini = yield* catalog.model.get(Provider.ID.make("sap-ai-core"), Model.ID.make("gemini-2.5-flash")) expect(sapGemini?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { modelParams: { thinkingConfig: { includeThoughts: false, thinkingBudget: 0 } } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } } }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } } }, }, ]) - const sapNova = yield* catalog.model.get(ProviderV2.ID.make("sap-ai-core"), ModelV2.ID.make("amazon--nova-lite")) + const sapNova = yield* catalog.model.get(Provider.ID.make("sap-ai-core"), Model.ID.make("amazon--nova-lite")) expect(sapNova?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "disabled" } } }, }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { modelParams: { additionalModelRequestFields: { output_config: { effort: "low" } } }, }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { additionalModelRequestFields: { output_config: { effort: "high" } } }, }, @@ -491,31 +491,31 @@ describe("ModelsDevPlugin", () => { ]) const sapCohere = yield* catalog.model.get( - ProviderV2.ID.make("sap-ai-core"), - ModelV2.ID.make("cohere--command-a-reasoning"), + Provider.ID.make("sap-ai-core"), + Model.ID.make("cohere--command-a-reasoning"), ) expect(sapCohere?.variants).toEqual([ { - id: ModelV2.VariantID.make("none"), + id: Model.VariantID.make("none"), settings: { modelParams: { thinking: { type: "disabled" } } }, }, { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { modelParams: { reasoning_effort: "low" } }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { reasoning_effort: "high" } }, }, ]) const sapAnthropicEffort = yield* catalog.model.get( - ProviderV2.ID.make("sap-ai-core"), - ModelV2.ID.make("anthropic--claude-4.7-opus"), + Provider.ID.make("sap-ai-core"), + Model.ID.make("anthropic--claude-4.7-opus"), ) expect(sapAnthropicEffort?.variants).toEqual([ { - id: ModelV2.VariantID.make("low"), + id: Model.VariantID.make("low"), settings: { modelParams: { additionalModelRequestFields: { @@ -528,12 +528,12 @@ describe("ModelsDevPlugin", () => { ]) const sapAnthropicBudget = yield* catalog.model.get( - ProviderV2.ID.make("sap-ai-core"), - ModelV2.ID.make("anthropic--claude-4-sonnet"), + Provider.ID.make("sap-ai-core"), + Model.ID.make("anthropic--claude-4-sonnet"), ) expect(sapAnthropicBudget?.variants).toEqual([ { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 8000 } }, @@ -541,7 +541,7 @@ describe("ModelsDevPlugin", () => { }, }, { - id: ModelV2.VariantID.make("max"), + id: Model.VariantID.make("max"), settings: { modelParams: { additionalModelRequestFields: { thinking: { type: "enabled", budget_tokens: 16000 } }, diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 97e30386f2b..2730680ccd4 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -1,24 +1,21 @@ import { describe, expect } from "bun:test" import { Message, SystemPart } from "@opencode-ai/ai" import { DateTime, Effect, Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginHost } from "@opencode-ai/core/plugin/host" import { PluginPromise } from "@opencode-ai/core/plugin/promise" import { WebSearch } from "@opencode-ai/core/websearch" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionPending } from "@opencode-ai/core/session/pending" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { Plugin } from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" -import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" -import { Model } from "@opencode-ai/schema/model" -import { Provider } from "@opencode-ai/schema/provider" +import { Tool } from "@opencode-ai/core/tool" +import { Provider } from "@opencode-ai/core/provider" +import { define } from "@opencode-ai/plugin/promise/plugin" +import type { SessionHooks } from "@opencode-ai/plugin/effect/session" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" import { host as testHost } from "./host" @@ -35,7 +32,7 @@ describe("fromPromise", () => { }) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-session-generate", setup: async (ctx) => { expect(await ctx.session.generate({ sessionID: "ses_generate", prompt: "Summarize" })).toEqual({ @@ -67,7 +64,7 @@ describe("fromPromise", () => { SessionPending.Synthetic.make({ admittedSeq: 1, id: SessionMessage.ID.make(input.id), - sessionID: SessionV2.ID.make(input.sessionID), + sessionID: Session.ID.make(input.sessionID), timeCreated: DateTime.makeUnsafe(0), type: "synthetic", data: { @@ -82,7 +79,7 @@ describe("fromPromise", () => { }) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-session-synthetic", setup: async (ctx) => { await ctx.session.synthetic(input) @@ -101,10 +98,10 @@ describe("fromPromise", () => { it.effect("forwards standard client reads", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) const seen: string[] = [] - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-client-reads", setup: async (ctx) => { const results = await Promise.all([ @@ -128,31 +125,36 @@ describe("fromPromise", () => { }), ) - it.effect("forwards direct agent and model reads", () => + it.effect("forwards direct agent and model list reads", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("reviewer"), (agent) => { + draft.update(Agent.ID.make("reviewer"), (agent) => { agent.description = "Reviews code" }), ) yield* catalog.transform((draft) => - draft.model.update(ProviderV2.ID.make("test"), ModelV2.ID.make("alias"), (model) => { - model.modelID = ModelV2.ID.make("gpt-5") + draft.model.update(Provider.ID.make("test"), Model.ID.make("alias"), (model) => { + model.modelID = Model.ID.make("gpt-5") }), ) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-direct-reads", setup: async (ctx) => { - expect(await ctx.agent.get("reviewer")).toMatchObject({ description: "Reviews code" }) - expect(await ctx.agent.get("missing")).toBeUndefined() - expect(await ctx.catalog.model.get("test", "alias")).toMatchObject({ modelID: "gpt-5" }) - expect(await ctx.catalog.model.get("test", "missing")).toBeUndefined() + expect((await ctx.agent.get({ agentID: Agent.ID.make("reviewer") })).data).toMatchObject({ + description: "Reviews code", + }) + await expect(ctx.agent.get({ agentID: Agent.ID.make("missing") })).rejects.toThrow("Agent not found: missing") + const models = (await ctx.catalog.model.list()).data + expect(models.find((model) => model.providerID === "test" && model.id === "alias")).toMatchObject({ + modelID: "gpt-5", + }) + expect(models.find((model) => model.providerID === "test" && model.id === "missing")).toBeUndefined() }, }), ).effect(host) @@ -161,11 +163,11 @@ describe("fromPromise", () => { it.effect("loads a promise plugin and registers a transform hook", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const plugin = yield* PluginV2.Service + const agents = yield* Agent.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-example", setup: async (ctx) => { expect(ctx.options.mode).toBe("strict") @@ -181,7 +183,7 @@ describe("fromPromise", () => { const adapted = PluginPromise.fromPromise(promisePlugin) yield* adapted.effect({ ...host, options: { mode: "strict" } }) - expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({ + expect(yield* agents.get(Agent.ID.make("reviewer"))).toMatchObject({ description: "Reviews code", mode: "subagent", }) @@ -190,11 +192,11 @@ describe("fromPromise", () => { it.effect("forwards session context hooks", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const hooks = yield* PluginHooks.Service const host = yield* PluginHost.make(plugin) yield* PluginPromise.fromPromise( - Plugin.define({ + define({ id: "promise-session-context", setup: async (ctx) => { await ctx.session.hook("context", (event) => { @@ -205,8 +207,8 @@ describe("fromPromise", () => { }), ).effect(host) const event: SessionHooks["context"] = { - sessionID: SessionV2.ID.make("ses_promise_session_context"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_promise_session_context"), + agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }), system: [SystemPart.make("Initial")], messages: [Message.user("Hello")], @@ -222,11 +224,11 @@ describe("fromPromise", () => { it.effect("disposes a hook registration on request", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service - const plugin = yield* PluginV2.Service + const agents = yield* Agent.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-dispose", setup: async (ctx) => { const registration = await ctx.agent.transform((draft) => { @@ -241,16 +243,16 @@ describe("fromPromise", () => { const adapted = PluginPromise.fromPromise(promisePlugin) yield* adapted.effect(host) - expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined() + expect(yield* agents.get(Agent.ID.make("temp"))).toBeUndefined() }), ) it.effect("registers a standalone web search provider", () => Effect.gen(function* () { const websearch = yield* WebSearch.Service - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-websearch", setup: async (ctx) => { await ctx.websearch.transform((draft) => { @@ -279,10 +281,10 @@ describe("fromPromise", () => { it.effect("runs the setup cleanup when the plugin scope closes", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) const events: string[] = [] - const promisePlugin = Plugin.define({ + const promisePlugin = define({ id: "promise-cleanup", setup: async () => { events.push("setup") @@ -306,17 +308,18 @@ describe("fromPromise", () => { it.effect("constructs plain Promise tool definitions in the host", () => Effect.gen(function* () { - const plugins = yield* PluginV2.Service - const registry = yield* ToolRegistry.Service + const plugins = yield* Plugin.Service + const registry = yield* Tool.Service const host = yield* PluginHost.make(plugins) - const progress: ToolRegistry.Progress[] = [] - const promisePlugin = Plugin.define({ + const progress: Tool.Metadata[] = [] + const promisePlugin = define({ id: "promise-tool", setup: async (ctx) => { await ctx.tool.transform((tools) => { tools.add( - "hello", - Tool.make({ + { + name: "hello", + options: { codemode: false }, description: "Hello", input: Schema.Struct({ name: Schema.String }), output: Schema.String, @@ -324,8 +327,7 @@ describe("fromPromise", () => { await context.progress({ phase: "greeting" }) return { output: `Hello, ${name}!` } }, - }), - { codemode: false }, + }, ) }) }, @@ -337,8 +339,8 @@ describe("fromPromise", () => { expect(toolSet.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" })) expect( yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_promise_tool"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_promise_tool"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_promise_tool"), progress: (update) => Effect.sync(() => progress.push(update)), call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } }, diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index 39f46c49a2c..44d91b036a4 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -3,18 +3,18 @@ import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AmazonBedrockPlugin.effect(host) @@ -83,9 +83,9 @@ describe("AmazonBedrockPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const bedrock = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock"), + const bedrock = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.amazonBedrock), + package: Provider.aisdk("@ai-sdk/amazon-bedrock"), settings: { endpoint: "https://bedrock.example" }, }) catalog.provider.update(bedrock.id, (item) => { @@ -94,8 +94,8 @@ describe("AmazonBedrockPlugin", () => { }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)) - expect(result.package).toBe(ProviderV2.aisdk("@ai-sdk/amazon-bedrock")) + const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock)) + expect(result.package).toBe(Provider.aisdk("@ai-sdk/amazon-bedrock")) expect(result.settings).toEqual({ baseURL: "https://bedrock.example" }) }), ) @@ -103,14 +103,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("prefers endpoint over baseURL for SDK base URL", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -129,14 +129,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses baseURL as SDK base URL", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -164,14 +164,14 @@ describe("AmazonBedrockPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock" }, @@ -185,14 +185,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses config region over AWS_REGION for SDK base URL", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock", region: "eu-west-1" }, @@ -205,14 +205,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses AWS_REGION for SDK base URL when config region is absent", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock" }, @@ -225,14 +225,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("defaults SDK region to us-east-1", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { name: "amazon-bedrock" }, @@ -245,15 +245,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("loads bearer token option into env and uses bearer auth", () => withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -275,15 +275,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("prefers bearer token env over bearer token option", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -305,14 +305,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("creates Mantle SDK with GPT-5 OpenAI base path", () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), - modelID: ModelV2.ID.make("openai.gpt-5.5"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-5.5")), + modelID: Model.ID.make("openai.gpt-5.5"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), package: "@ai-sdk/amazon-bedrock/mantle", options: { @@ -332,24 +332,24 @@ describe("AmazonBedrockPlugin", () => { it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), - modelID: ModelV2.ID.make("openai.gpt-5.5"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-5.5")), + modelID: Model.ID.make("openai.gpt-5.5"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), sdk: fakeSelectorSdk(calls), options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), - modelID: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/mantle"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-oss-safeguard-120b")), + modelID: Model.ID.make("openai.gpt-oss-safeguard-120b"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), }), sdk: fakeSelectorSdk(calls), options: { region: "us-east-1" }, @@ -360,14 +360,14 @@ describe("AmazonBedrockPlugin", () => { it.effect("ignores other Bedrock provider subpaths", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("@ai-sdk/amazon-bedrock/anthropic"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("@ai-sdk/amazon-bedrock/anthropic"), }), package: "@ai-sdk/amazon-bedrock/anthropic", options: { name: "amazon-bedrock" }, @@ -387,15 +387,15 @@ describe("AmazonBedrockPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/amazon-bedrock", options: { @@ -419,51 +419,51 @@ describe("AmazonBedrockPlugin", () => { it.effect("applies legacy cross-region inference prefixes", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "eu-west-1" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("global.anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("global.anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "eu-west-1" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "ap-northeast-1" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "ap-southeast-2" }, @@ -481,15 +481,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("uses AWS_REGION for language prefixes when region option is absent", () => withEnv({ AWS_REGION: "eu-west-1" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: {}, @@ -501,7 +501,7 @@ describe("AmazonBedrockPlugin", () => { it.effect("applies the full legacy cross-region prefix matrix", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] const cases = [ @@ -573,10 +573,10 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() for (const item of cases) { yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), - modelID: ModelV2.ID.make(item.modelID), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make(item.modelID)), + modelID: Model.ID.make(item.modelID), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: item.region }, @@ -588,15 +588,15 @@ describe("AmazonBedrockPlugin", () => { it.effect("ignores non-Bedrock providers for language selection", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), - modelID: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("anthropic.claude-sonnet-4-5")), + modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, options: { region: "eu-west-1" }, diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index 0c0bcba461e..de9008b10f1 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -2,18 +2,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AnthropicPlugin.effect(host) @@ -29,9 +29,9 @@ describe("AnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.anthropic), - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.anthropic), + package: Provider.aisdk("@ai-sdk/anthropic"), headers: { Existing: "1" }, }) catalog.provider.update(item.id, (draft) => { @@ -40,32 +40,32 @@ describe("AnthropicPlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.["anthropic-beta"]).toBe( + expect(required(yield* catalog.provider.get(Provider.ID.anthropic)).headers?.["anthropic-beta"]).toBe( "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", ) - expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).headers?.Existing).toBe("1") + expect(required(yield* catalog.provider.get(Provider.ID.anthropic)).headers?.Existing).toBe("1") }), ) it.effect("ignores non-Anthropic providers", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.openai, () => {})) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined() + expect(required(yield* catalog.provider.get(Provider.ID.openai)).headers?.["anthropic-beta"]).toBeUndefined() }), ) it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-anthropic"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), + package: Provider.aisdk("@ai-sdk/anthropic"), }), package: "@ai-sdk/anthropic", options: { name: "custom-anthropic", apiKey: "test" }, @@ -76,14 +76,14 @@ describe("AnthropicPlugin", () => { it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), - package: ProviderV2.aisdk("@ai-sdk/anthropic"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.anthropic, Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), + package: Provider.aisdk("@ai-sdk/anthropic"), }), package: "@ai-sdk/anthropic", options: { name: "anthropic", apiKey: "test" }, diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index 5b9ca4bf86f..c53bb4a524a 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -3,18 +3,18 @@ import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AzureCognitiveServicesPlugin.effect(host) @@ -65,12 +65,12 @@ describe("AzureCognitiveServicesPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => { - item.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("azure-cognitive-services"), (item) => { + item.package = Provider.aisdk("@ai-sdk/openai-compatible") }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))) + const result = required(yield* catalog.provider.get(Provider.ID.make("azure-cognitive-services"))) expect(result).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://cognitive.cognitiveservices.azure.com/openai" }, @@ -85,12 +85,12 @@ describe("AzureCognitiveServicesPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.make("azure-cognitive-services")), package: "aisdk:@ai-sdk/openai-compatible", }) - const openai = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), + const openai = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), package: "aisdk:test-provider", }) catalog.provider.update(azure.id, (item) => { @@ -103,8 +103,8 @@ describe("AzureCognitiveServicesPlugin", () => { }) }) yield* addPlugin() - const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))) - const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai)) + const azure = required(yield* catalog.provider.get(Provider.ID.make("azure-cognitive-services"))) + const openai = required(yield* catalog.provider.get(Provider.ID.openai)) expect(azure.settings?.baseURL).toBeUndefined() expect(azure).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible" }) expect(openai.settings?.baseURL).toBeUndefined() @@ -115,14 +115,14 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("selects chat only for completion URLs", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -134,23 +134,23 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("uses the legacy Azure selector order and provider guard", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) const ignored = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -163,33 +163,33 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("falls back from responses to messages, chat, then languageModel", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] const sdk = fakeSelectorSdk(calls) yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), - modelID: ModelV2.ID.make("messages-deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("messages-deployment")), + modelID: Model.ID.make("messages-deployment"), package: "aisdk:test-provider", }), sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), - modelID: ModelV2.ID.make("chat-deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("chat-deployment")), + modelID: Model.ID.make("chat-deployment"), package: "aisdk:test-provider", }), sdk: { chat: sdk.chat, languageModel: sdk.languageModel }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), - modelID: ModelV2.ID.make("language-deployment"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("azure-cognitive-services"), Model.ID.make("language-deployment")), + modelID: Model.ID.make("language-deployment"), package: "aisdk:test-provider", }), sdk: { languageModel: sdk.languageModel }, diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 227e3bfa783..85f405e6cbe 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -3,18 +3,18 @@ import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* AzurePlugin.effect(host) @@ -65,12 +65,12 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.azure, (item) => { - item.package = ProviderV2.aisdk("@ai-sdk/azure") + catalog.provider.update(Provider.ID.azure, (item) => { + item.package = Provider.aisdk("@ai-sdk/azure") }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env") + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-env") }), ), ) @@ -80,20 +80,20 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.azure), - package: ProviderV2.aisdk("@ai-sdk/azure"), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.azure), + package: Provider.aisdk("@ai-sdk/azure"), settings: { resourceName: "from-config" }, }) catalog.provider.update(azure.id, (item) => { item.package = azure.package item.settings = { resourceName: "from-config" } }) - catalog.provider.update(ProviderV2.ID.openai, () => {}) + catalog.provider.update(Provider.ID.openai, () => {}) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-config") - expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.resourceName).toBeUndefined() + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-config") + expect(required(yield* catalog.provider.get(Provider.ID.openai)).settings?.resourceName).toBeUndefined() }), ), ) @@ -103,9 +103,9 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.azure), - package: ProviderV2.aisdk("@ai-sdk/azure"), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.azure), + package: Provider.aisdk("@ai-sdk/azure"), settings: { resourceName: "" }, }) catalog.provider.update(azure.id, (item) => { @@ -114,7 +114,7 @@ describe("AzurePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env") + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-env") }), ), ) @@ -124,9 +124,9 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.azure), - package: ProviderV2.aisdk("@ai-sdk/azure"), + const azure = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.azure), + package: Provider.aisdk("@ai-sdk/azure"), settings: { resourceName: " " }, }) catalog.provider.update(azure.id, (item) => { @@ -135,7 +135,7 @@ describe("AzurePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).settings?.resourceName).toBe("from-env") + expect(required(yield* catalog.provider.get(Provider.ID.azure)).settings?.resourceName).toBe("from-env") }), ), ) @@ -143,14 +143,14 @@ describe("AzurePlugin", () => { it.effect("allows configured baseURL without resourceName", () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/azure", options: { name: "azure", baseURL: "https://proxy.example.com/openai" }, @@ -167,10 +167,10 @@ describe("AzurePlugin", () => { yield* addPlugin() const exit = yield* aisdk .runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/azure", options: { name: "azure" }, @@ -183,15 +183,15 @@ describe("AzurePlugin", () => { it.effect("selects chat only for completion URLs", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true }, @@ -202,15 +202,15 @@ describe("AzurePlugin", () => { it.effect("selects chat from per-call useCompletionUrls", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true }, @@ -221,15 +221,15 @@ describe("AzurePlugin", () => { it.effect("ignores model useCompletionUrls when per-call option is unset", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), body: { useCompletionUrls: true }, }), sdk: fakeSelectorSdk(calls), @@ -241,24 +241,24 @@ describe("AzurePlugin", () => { it.effect("uses the legacy Azure selector order and provider guard", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, }) const ignored = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), - modelID: ModelV2.ID.make("deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("deployment")), + modelID: Model.ID.make("deployment"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, @@ -270,7 +270,7 @@ describe("AzurePlugin", () => { it.effect("falls back through the legacy Azure selector order", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] const make = (method: string) => (id: string) => { @@ -279,19 +279,19 @@ describe("AzurePlugin", () => { } yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), - modelID: ModelV2.ID.make("messages-deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("messages-deployment")), + modelID: Model.ID.make("messages-deployment"), + package: Provider.aisdk("test-provider"), }), sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") }, options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), - modelID: ModelV2.ID.make("language-deployment"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.azure, Model.ID.make("language-deployment")), + modelID: Model.ID.make("language-deployment"), + package: Provider.aisdk("test-provider"), }), sdk: { languageModel: make("languageModel") }, options: {}, diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 6722f48996f..bf13f452d4d 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -14,7 +14,7 @@ const cerebrasOptions: Record[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CerebrasPlugin.effect(host) @@ -35,13 +35,13 @@ describe("CerebrasPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => { - item.package = ProviderV2.aisdk("@ai-sdk/cerebras") + catalog.provider.update(Provider.ID.make("cerebras"), (item) => { + item.package = Provider.aisdk("@ai-sdk/cerebras") item.headers = { ...item.headers, Existing: "1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("cerebras")))?.headers).toEqual({ Existing: "1", "X-Cerebras-3rd-Party-Integration": "opencode", }) @@ -51,25 +51,25 @@ describe("CerebrasPlugin", () => { it.effect("ignores non-Cerebras providers", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("groq"), () => {})) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("groq")))?.headers).toBeUndefined() }), ) it.effect("creates a bundled Cerebras SDK with the model provider ID as the SDK name", () => Effect.gen(function* () { cerebrasOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("custom-cerebras"), - ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("custom-cerebras"), + Model.ID.make("llama-4-scout-17b-16e-instruct"), ), - modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider", }), package: "@ai-sdk/cerebras", @@ -83,16 +83,16 @@ describe("CerebrasPlugin", () => { it.effect("preserves an explicit bundled Cerebras SDK name option", () => Effect.gen(function* () { cerebrasOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("custom-cerebras"), - ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("custom-cerebras"), + Model.ID.make("llama-4-scout-17b-16e-instruct"), ), - modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider", }), package: "@ai-sdk/cerebras", @@ -105,16 +105,16 @@ describe("CerebrasPlugin", () => { it.effect("ignores non-Cerebras SDK packages", () => Effect.gen(function* () { cerebrasOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("custom-cerebras"), - ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("custom-cerebras"), + Model.ID.make("llama-4-scout-17b-16e-instruct"), ), - modelID: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), + modelID: Model.ID.make("llama-4-scout-17b-16e-instruct"), package: "aisdk:test-provider", }), package: "@ai-sdk/groq", diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index bd9d9e80cd2..2dab4d93baa 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -1,18 +1,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CloudflareAIGatewayPlugin.effect(host) @@ -112,13 +112,13 @@ describe("CloudflareAIGatewayPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -133,14 +133,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -178,14 +178,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -208,14 +208,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -246,14 +246,14 @@ describe("CloudflareAIGatewayPlugin", () => { () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -278,14 +278,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv({ CLOUDFLARE_API_TOKEN: undefined, CF_AIG_TOKEN: "cf-aig-token" }), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -301,14 +301,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_GATEWAY_ID: undefined }), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -325,14 +325,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv({ CLOUDFLARE_API_TOKEN: undefined, CF_AIG_TOKEN: undefined }), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -355,14 +355,14 @@ describe("CloudflareAIGatewayPlugin", () => { () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -379,17 +379,17 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("cloudflare-ai-gateway"), - ModelV2.ID.make("anthropic/claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("cloudflare-ai-gateway"), + Model.ID.make("anthropic/claude-sonnet-4-5"), ), - modelID: ModelV2.ID.make("anthropic/claude-sonnet-4-5"), + modelID: Model.ID.make("anthropic/claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "ai-gateway-provider", @@ -411,14 +411,14 @@ describe("CloudflareAIGatewayPlugin", () => { withEnv(cloudflareEnv(), () => Effect.gen(function* () { resetCalls() - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-ai-gateway"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 5f4e3bc2760..7179786dc72 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -14,7 +14,7 @@ import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* CloudflareWorkersAIPlugin.effect(host) @@ -82,20 +82,20 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("maps account ID to endpoint URL and creates an OpenAI-compatible SDK", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = ProviderV2.aisdk("test-provider") + catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { + provider.package = Provider.aisdk("test-provider") }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai"))) const sdk = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: provider.package, settings: provider.settings, }), @@ -116,13 +116,13 @@ describe("CloudflareWorkersAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = ProviderV2.aisdk("test-provider") + catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { + provider.package = Provider.aisdk("test-provider") provider.settings = { ...provider.settings, baseURL: "https://proxy.example/v1" } }), ) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({ + expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({ package: "aisdk:test-provider", settings: { baseURL: "https://proxy.example/v1" }, }) @@ -133,13 +133,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("allows a configured baseURL without account ID", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, }), @@ -156,13 +156,13 @@ describe("CloudflareWorkersAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { - provider.package = ProviderV2.aisdk("test-provider") + catalog.provider.update(Provider.ID.make("cloudflare-workers-ai"), (provider) => { + provider.package = Provider.aisdk("test-provider") provider.settings = { ...provider.settings, accountId: "configured-acct" } }), ) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))).toMatchObject({ + expect(required(yield* catalog.provider.get(Provider.ID.make("cloudflare-workers-ai")))).toMatchObject({ package: "aisdk:test-provider", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1" }, }) @@ -173,13 +173,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("uses env API key over auth or configured API key and keeps the Cloudflare User-Agent", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://proxy.example/v1" }, }), @@ -202,13 +202,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("expands account ID vars in endpoint URLs", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1" }, }), @@ -227,14 +227,14 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("selects languageModel with the API model ID", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("@cf/api-model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("alias")), + modelID: Model.ID.make("@cf/api-model"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -248,13 +248,13 @@ describe("CloudflareWorkersAIPlugin", () => { it.effect("does not create an SDK for non OpenAI-compatible packages", () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - modelID: ModelV2.ID.make("@cf/model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("@cf/model")), + modelID: Model.ID.make("@cf/model"), package: "aisdk:@ai-sdk/anthropic", settings: { baseURL: "https://proxy.example/v1" }, }), diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index 341878e6ded..437c64bbf4d 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -7,11 +7,11 @@ import path from "path" import { fileURLToPath } from "url" import { AISDK } from "@opencode-ai/core/aisdk" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -29,7 +29,7 @@ function npmEntrypoint(entrypoint?: string) { } const addPlugin = Effect.fn(function* (npm?: Npm.Interface) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* DynamicProviderPlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm ?? (yield* Npm.Service))) }) @@ -52,10 +52,10 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), - package: ProviderV2.aisdk(fixtureProvider), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), + package: Provider.aisdk(fixtureProvider), }), package: fixtureProvider, options: { name: "custom", marker: "dynamic" }, @@ -71,10 +71,10 @@ describe("DynamicProviderPlugin", () => { const sdk = { marker: "existing" } yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), - package: ProviderV2.aisdk(fixtureProvider), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), + package: Provider.aisdk(fixtureProvider), }), package: fixtureProvider, options: { name: "custom", marker: "dynamic" }, @@ -89,10 +89,10 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), - package: ProviderV2.aisdk(fixtureProvider), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-provider"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), + package: Provider.aisdk(fixtureProvider), }), package: fixtureProvider, options: { name: "custom-provider", marker: "dynamic" }, @@ -106,9 +106,9 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(npmEntrypoint(fixtureProviderPath)) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), - modelID: ModelV2.ID.make("test-model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("npm-provider"), Model.ID.make("test-model")), + modelID: Model.ID.make("test-model"), package: "aisdk:fixture-provider", }), package: "fixture-provider", @@ -124,9 +124,9 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin(npmEntrypoint()) const exit = yield* aisdk .language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("missing-entrypoint"), Model.ID.make("alias")), + modelID: Model.ID.make("alias"), package: "aisdk:fixture-provider", }), ) @@ -142,9 +142,9 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const exit = yield* aisdk .language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("bad-import"), Model.ID.make("alias")), + modelID: Model.ID.make("alias"), package: "aisdk:file:///missing/provider-factory.js", }), ) @@ -156,15 +156,15 @@ describe("DynamicProviderPlugin", () => { itWithAISDK.live("wraps missing provider factory exports as AISDK init errors", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n") yield* addPlugin(npmEntrypoint(tmp.entrypoint)) const exit = yield* aisdk .language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("alias"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("missing-factory"), Model.ID.make("alias")), + modelID: Model.ID.make("alias"), package: "aisdk:fixture-provider", }), ) @@ -176,14 +176,14 @@ describe("DynamicProviderPlugin", () => { itWithAISDK.effect("uses the model modelID for the default language model", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const language = yield* aisdk.language( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("test-model-api"), - package: ProviderV2.aisdk(fixtureProvider), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("alias")), + modelID: Model.ID.make("test-model-api"), + package: Provider.aisdk(fixtureProvider), }), ) expect(language).toMatchObject({ modelID: "test-model-api", options: { name: "custom" } }) diff --git a/packages/core/test/plugin/provider-factory.test.ts b/packages/core/test/plugin/provider-factory.test.ts index d51d4900b49..28856ad6d0b 100644 --- a/packages/core/test/plugin/provider-factory.test.ts +++ b/packages/core/test/plugin/provider-factory.test.ts @@ -1,8 +1,8 @@ import { expect } from "bun:test" import { Effect } from "effect" import { AISDK } from "@opencode-ai/core/aisdk" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba" import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere" @@ -13,11 +13,11 @@ import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral" import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity" import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai" import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" -const modelID = ModelV2.ID.make("test-model") +const modelID = Model.ID.make("test-model") const options = { name: "custom-provider", apiKey: "test", baseURL: "https://example.test" } const providers = [ { id: "alibaba", plugin: AlibabaPlugin, package: "@ai-sdk/alibaba", provider: "alibaba.chat" }, @@ -36,14 +36,14 @@ const it = testEffect(PluginTestLayer) providers.forEach((item) => it.effect(`${item.id} loads only its exact package`, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* item.plugin.effect(host) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make(item.id), modelID), + const model = Model.Info.make({ + ...Model.Info.default(Provider.ID.make(item.id), modelID), modelID, - package: ProviderV2.aisdk(item.package), + package: Provider.aisdk(item.package), }) const matched = yield* aisdk.runSDK({ model, package: item.package, options }) const ignored = yield* aisdk.runSDK({ model, package: `${item.package}/unsupported`, options }) diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index b39e06ac7be..0f70aa628bf 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -3,11 +3,11 @@ import { App } from "@opencode-ai/core/app" import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { copilotBaseURL, copilotFetch, GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Integration } from "@opencode-ai/core/integration" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" @@ -16,7 +16,7 @@ import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GithubCopilotPlugin.effect(host) @@ -94,22 +94,22 @@ describe("GithubCopilotPlugin", () => { it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", options: { name: "github-copilot" }, }) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/github-copilot", @@ -122,14 +122,14 @@ describe("GithubCopilotPlugin", () => { it.effect("selects languageModel when responses and chat are absent", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), - modelID: ModelV2.ID.make("claude-sonnet-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("claude-sonnet-4")), + modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, @@ -141,14 +141,14 @@ describe("GithubCopilotPlugin", () => { it.effect("selects languageModel with the API model ID when responses and chat are absent", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("claude-sonnet-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("alias")), + modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, @@ -160,50 +160,50 @@ describe("GithubCopilotPlugin", () => { it.effect("uses responses for gpt-5 models except gpt-5-mini", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), - modelID: ModelV2.ID.make("gpt-5.1-codex"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5.1-codex")), + modelID: Model.ID.make("gpt-5.1-codex"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), - modelID: ModelV2.ID.make("gpt-4o"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-4o")), + modelID: Model.ID.make("gpt-4o"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), - modelID: ModelV2.ID.make("gpt-5-mini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-mini")), + modelID: Model.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), - modelID: ModelV2.ID.make("gpt-5-mini-2025-08-07"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-mini-2025-08-07")), + modelID: Model.ID.make("gpt-5-mini-2025-08-07"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -221,14 +221,14 @@ describe("GithubCopilotPlugin", () => { it.effect("uses advertised Copilot endpoint metadata before model ID fallbacks", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("mai-code-1-flash-picker")), - modelID: ModelV2.ID.make("mai-code-1-flash-picker"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("mai-code-1-flash-picker")), + modelID: Model.ID.make("mai-code-1-flash-picker"), package: "aisdk:test-provider", settings: { endpoint: "responses" }, }), @@ -236,9 +236,9 @@ describe("GithubCopilotPlugin", () => { options: { endpoint: "responses" }, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", settings: { endpoint: "chat" }, }), @@ -251,32 +251,32 @@ describe("GithubCopilotPlugin", () => { it.effect("uses the API model ID when selecting responses or chat", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("default")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), - modelID: ModelV2.ID.make("gpt-5-mini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("small")), + modelID: Model.ID.make("gpt-5-mini"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), options: {}, }) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), - modelID: ModelV2.ID.make("claude-sonnet-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("github-copilot"), Model.ID.make("sonnet")), + modelID: Model.ID.make("claude-sonnet-4"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), @@ -290,12 +290,12 @@ describe("GithubCopilotPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {}) - catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.provider.update(Provider.ID.make("github-copilot"), () => {}) + catalog.model.update(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))) + required(yield* catalog.model.get(Provider.ID.make("github-copilot"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(false) }), @@ -305,12 +305,12 @@ describe("GithubCopilotPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {}) - catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.provider.update(Provider.ID.make("custom-copilot"), () => {}) + catalog.model.update(Provider.ID.make("custom-copilot"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))) + required(yield* catalog.model.get(Provider.ID.make("custom-copilot"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(true) }), @@ -318,14 +318,14 @@ describe("GithubCopilotPlugin", () => { it.effect("ignores non-Copilot providers", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("openai"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), package: "aisdk:test-provider", }), sdk: fakeSelectorSdk(calls), diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index ac12ebfc9d6..a549ff18b22 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -14,7 +14,7 @@ const gitlabSDKOptions: Record[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GitLabPlugin.effect(host) @@ -64,13 +64,13 @@ describe("GitLabPlugin", () => { () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "gitlab-ai-provider", @@ -102,13 +102,13 @@ describe("GitLabPlugin", () => { () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "gitlab-ai-provider", @@ -128,13 +128,13 @@ describe("GitLabPlugin", () => { () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "gitlab-ai-provider", @@ -170,13 +170,13 @@ describe("GitLabPlugin", () => { it.effect("ignores non-GitLab SDK packages", () => Effect.gen(function* () { gitlabSDKOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai", @@ -189,14 +189,14 @@ describe("GitLabPlugin", () => { it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), - modelID: ModelV2.ID.make("duo-workflow-custom"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("duo-workflow-custom")), + modelID: Model.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, settings: { workflowRef: "ref", workflowDefinition: "definition" }, @@ -223,14 +223,14 @@ describe("GitLabPlugin", () => { it.effect("uses exact static workflow model ids when the provider recognizes them", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), - modelID: ModelV2.ID.make("duo-workflow-exact"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("duo-workflow-exact")), + modelID: Model.ID.make("duo-workflow-exact"), package: "aisdk:test-provider", }), sdk: { @@ -251,14 +251,14 @@ describe("GitLabPlugin", () => { it.effect("uses provider feature flags instead of model settings feature flags", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), - modelID: ModelV2.ID.make("duo-workflow-custom"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("duo-workflow-custom")), + modelID: Model.ID.make("duo-workflow-custom"), package: "aisdk:test-provider", headers: {}, settings: { featureFlags: { request_flag: true } }, @@ -278,14 +278,14 @@ describe("GitLabPlugin", () => { it.effect("uses agenticChat with provider aiGatewayHeaders and feature flags for normal models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - modelID: ModelV2.ID.make("claude"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("gitlab"), Model.ID.make("claude")), + modelID: Model.ID.make("claude"), package: "aisdk:test-provider", headers: { h: "v" }, settings: {}, diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 3b6726d3730..4762e9b34c9 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -3,18 +3,18 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* definition.effect(host) @@ -63,15 +63,15 @@ describe("GoogleVertexAnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic") + catalog.provider.update(Provider.ID.make("google-vertex-anthropic"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex/anthropic") }), ) yield* addPlugin(GoogleVertexAnthropicPlugin) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( "cloud-project", ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( "cloud-location", ) }), @@ -83,16 +83,16 @@ describe("GoogleVertexAnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex/anthropic") + catalog.provider.update(Provider.ID.make("google-vertex-anthropic"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex/anthropic") provider.settings = { ...provider.settings, project: "configured-project", location: "configured-location" } }), ) yield* addPlugin(GoogleVertexAnthropicPlugin) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.project).toBe( "configured-project", ) - expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( + expect((yield* catalog.provider.get(Provider.ID.make("google-vertex-anthropic")))?.settings?.location).toBe( "configured-location", ) }), @@ -111,16 +111,16 @@ describe("GoogleVertexAnthropicPlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("google-vertex-anthropic"), - ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("google-vertex-anthropic"), + Model.ID.make("claude-sonnet-4-5"), ), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -138,16 +138,16 @@ describe("GoogleVertexAnthropicPlugin", () => { { GOOGLE_CLOUD_PROJECT: "project", GOOGLE_CLOUD_LOCATION: "cloud-location", VERTEX_LOCATION: "vertex-location" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default( - ProviderV2.ID.make("google-vertex-anthropic"), - ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default( + Provider.ID.make("google-vertex-anthropic"), + Model.ID.make("claude-sonnet-4-5"), ), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -162,13 +162,13 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -182,13 +182,13 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("keeps configured baseURL for google-vertex Anthropic models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", @@ -198,25 +198,25 @@ describe("GoogleVertexAnthropicPlugin", () => { }), ) - it.effect("selects google-vertex Anthropic language models through V2 plugins", () => + it.effect("selects google-vertex Anthropic language models through plugins", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin) const sdkResult = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), - modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make(" claude-sonnet-4-5 ")), + modelID: Model.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), package: "@ai-sdk/google-vertex/anthropic", options: { name: "google-vertex", project: "project", location: "us" }, }) const languageResult = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), - modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make(" claude-sonnet-4-5 ")), + modelID: Model.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), sdk: sdkResult.sdk, @@ -232,14 +232,14 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), - modelID: ModelV2.ID.make(" claude-sonnet-4-5 "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex-anthropic"), Model.ID.make(" claude-sonnet-4-5 ")), + modelID: Model.ID.make(" claude-sonnet-4-5 "), package: "aisdk:test-provider", }), sdk: { languageModel: selector(calls) }, @@ -251,14 +251,14 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("ignores non Vertex Anthropic providers for language selection", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), - modelID: ModelV2.ID.make("claude-sonnet-4-5"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("claude-sonnet-4-5")), + modelID: Model.ID.make("claude-sonnet-4-5"), package: "aisdk:test-provider", }), sdk: { languageModel: selector(calls) }, diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 15949b49454..d7b89e01067 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -2,11 +2,11 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import type { LanguageModelV3 } from "@ai-sdk/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -16,7 +16,7 @@ const googleAuthOptions: Record[] = [] const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GoogleVertexPlugin.effect(host) @@ -91,14 +91,14 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.opencode, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.opencode, (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://opencode.ai/zen/v1" } }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode)) + const provider = required(yield* catalog.provider.get(Provider.ID.opencode)) expect(provider.settings).toEqual({ baseURL: "https://opencode.ai/zen/v1" }) }), ) @@ -117,8 +117,8 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -127,7 +127,7 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider.settings?.project).toBe("google-cloud-project") expect(provider.settings?.location).toBe("google-vertex-location") expect(provider).toMatchObject({ @@ -155,12 +155,12 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { vertexOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -169,11 +169,11 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), package: "@ai-sdk/google-vertex", @@ -208,8 +208,8 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -219,7 +219,7 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider.settings?.project).toBe("config-project") expect(provider.settings?.location).toBe("global") expect(provider).toMatchObject({ @@ -234,8 +234,8 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: @@ -245,7 +245,7 @@ describe("GoogleVertexPlugin", () => { }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider).toMatchObject({ package: "aisdk:@ai-sdk/openai-compatible", settings: { baseURL: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu" }, @@ -267,13 +267,13 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/google-vertex") + catalog.provider.update(Provider.ID.make("google-vertex"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/google-vertex") provider.settings = { ...provider.settings, project: "config-project" } }), ) yield* addPlugin() - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("google-vertex"))) expect(provider.settings?.project).toBe("config-project") expect(provider.settings?.location).toBe("us-central1") }), @@ -289,13 +289,13 @@ describe("GoogleVertexPlugin", () => { () => Effect.gen(function* () { vertexOptions.length = 0 - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google-vertex", }), package: "@ai-sdk/google-vertex", @@ -313,7 +313,7 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { googleAuthOptions.length = 0 const fetchCalls: { input: Parameters[0]; init?: RequestInit }[] = [] - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.hook.sdk((evt) => @@ -338,9 +338,9 @@ describe("GoogleVertexPlugin", () => { Effect.void, () => aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/openai-compatible", }), package: "@ai-sdk/openai-compatible", @@ -361,14 +361,14 @@ describe("GoogleVertexPlugin", () => { it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), - modelID: ModelV2.ID.make(" gemini-2.5-pro "), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google-vertex"), Model.ID.make(" gemini-2.5-pro ")), + modelID: Model.ID.make(" gemini-2.5-pro "), package: "aisdk:test-provider", }), sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index 2dea12a2e66..655c525ea02 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -1,18 +1,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* GooglePlugin.effect(host) @@ -21,13 +21,13 @@ const addPlugin = Effect.fn(function* () { describe("GooglePlugin", () => { it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), package: "@ai-sdk/google", @@ -40,13 +40,13 @@ describe("GooglePlugin", () => { it.effect("ignores non-Google SDK packages", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), - modelID: ModelV2.ID.make("gemini"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("google"), Model.ID.make("gemini")), + modelID: Model.ID.make("gemini"), package: "aisdk:@ai-sdk/google", }), package: "@ai-sdk/google-vertex", @@ -58,13 +58,13 @@ describe("GooglePlugin", () => { it.effect("uses default languageModel loading with provider ID parity", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdkEvent = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("gemini-api"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("alias")), + modelID: Model.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", }), package: "@ai-sdk/google", @@ -87,9 +87,9 @@ describe("GooglePlugin", () => { yield* addPlugin() const resolved = yield* aisdk.model( - ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("gemini-api"), + Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-google"), Model.ID.make("alias")), + modelID: Model.ID.make("gemini-api"), package: "aisdk:@ai-sdk/google", settings: { apiKey: "test" }, }), diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index e4af57cda6b..e45cf39a3bd 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* KiloPlugin.effect(host) }) @@ -26,20 +26,20 @@ describe("KiloPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("kilo"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.openrouter, () => {}) + catalog.provider.update(Provider.ID.openrouter, () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toBeUndefined() }), ) @@ -47,20 +47,20 @@ describe("KiloPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("kilo"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer") - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("x-title") - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source") + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).not.toHaveProperty("http-referer") + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).not.toHaveProperty("x-title") + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).not.toHaveProperty("X-Source") }), ) @@ -68,18 +68,18 @@ describe("KiloPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => { - provider.package = ProviderV2.aisdk("kilo") + catalog.provider.update(Provider.ID.make("kilo"), (provider) => { + provider.package = Provider.aisdk("kilo") }) - catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("custom-kilo"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.kilo.ai/api/gateway" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.headers).toBeUndefined() - expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("kilo")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("custom-kilo")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 9ff6d2566e7..11455d3b628 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -2,18 +2,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) const integration = yield* Integration.Service yield* LLMGatewayPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration)) @@ -33,21 +33,21 @@ describe("LLMGatewayPlugin", () => { editor.update(Integration.ID.make("openrouter"), () => {}) }) yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.llmgateway.io/v1" } provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.openrouter, () => {}) + catalog.provider.update(Provider.ID.openrouter, () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-Source": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toBeUndefined() }), ) @@ -59,16 +59,16 @@ describe("LLMGatewayPlugin", () => { editor.update(Integration.ID.make("llmgateway"), () => {}) }) yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => { + catalog.provider.update(Provider.ID.make("llmgateway"), (provider) => { provider.disabled = true - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://api.llmgateway.io/v1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true) - expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.disabled).toBe(true) + expect((yield* catalog.provider.get(Provider.ID.make("llmgateway")))?.headers).toBeUndefined() }), ) }) diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index 47f78fd0228..278e4f99c14 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* NvidiaPlugin.effect(host) }) @@ -26,21 +26,21 @@ describe("NvidiaPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.openrouter, () => {}) + catalog.provider.update(Provider.ID.openrouter, () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toBeUndefined() }), ) @@ -48,14 +48,14 @@ describe("NvidiaPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "OpenCode", @@ -67,15 +67,15 @@ describe("NvidiaPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("nvidia"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { baseURL: "https://integrate.api.nvidia.com/v1" } provider.headers = { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", "X-BILLING-INVOKE-ORIGIN": "CustomOrigin", diff --git a/packages/core/test/plugin/provider-openai-compatible.test.ts b/packages/core/test/plugin/provider-openai-compatible.test.ts index e1cf1ed6c86..407d7d357c4 100644 --- a/packages/core/test/plugin/provider-openai-compatible.test.ts +++ b/packages/core/test/plugin/provider-openai-compatible.test.ts @@ -1,18 +1,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* OpenAICompatiblePlugin.effect(host) @@ -21,22 +21,22 @@ const addPlugin = Effect.fn(function* () { describe("OpenAICompatiblePlugin", () => { it.effect("preserves explicit includeUsage false and defaults it to true", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const defaulted = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", options: { name: "custom" }, }) const disabled = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -49,13 +49,13 @@ describe("OpenAICompatiblePlugin", () => { it.effect("defaults includeUsage for OpenAI-compatible package matches", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "file:///tmp/@ai-sdk/openai-compatible-provider.js", @@ -67,7 +67,7 @@ describe("OpenAICompatiblePlugin", () => { it.effect("uses the provider ID as the OpenAI-compatible provider name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const observed: string[] = [] yield* addPlugin() @@ -77,9 +77,9 @@ describe("OpenAICompatiblePlugin", () => { }), ) yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-provider"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -98,9 +98,9 @@ describe("OpenAICompatiblePlugin", () => { }) yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), - modelID: ModelV2.ID.make("model"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("cloudflare-workers-ai"), Model.ID.make("model")), + modelID: Model.ID.make("model"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index 741966eebed..75b6385c021 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -6,18 +6,18 @@ import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) const integrations = yield* Integration.Service @@ -63,14 +63,14 @@ describe("OpenAIPlugin", () => { it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-openai"), Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/openai", options: { name: "custom-openai", apiKey: "test" }, @@ -81,14 +81,14 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI SDK packages", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/openai-compatible", options: { name: "openai" }, @@ -99,15 +99,15 @@ describe("OpenAIPlugin", () => { it.effect("uses the Responses API for language models", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("alias")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, @@ -119,15 +119,15 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI providers", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), - modelID: ModelV2.ID.make("gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.anthropic, Model.ID.make("gpt-5")), + modelID: Model.ID.make("gpt-5"), + package: Provider.aisdk("test-provider"), }), sdk: fakeSelectorSdk(calls), options: {}, @@ -141,20 +141,20 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("@ai-sdk/openai"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("@ai-sdk/openai"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5"))).enabled).toBe(true) expect( - required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled, + required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5-chat-latest"))).enabled, ).toBe(false) }), ) @@ -164,14 +164,14 @@ describe("OpenAIPlugin", () => { const catalog = yield* Catalog.Service const credentials = yield* Credential.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("@ai-sdk/openai"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("@ai-sdk/openai"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => { + catalog.model.update(item.id, Model.ID.make("gpt-5.5"), (model) => { model.cost = [ { input: Money.USDPerMillionTokens.make(1), @@ -183,14 +183,14 @@ describe("OpenAIPlugin", () => { }, ] }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5-pro"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.4-pro"), (model) => { - model.modelID = ModelV2.ID.make("gpt-5.4") + catalog.model.update(item.id, Model.ID.make("gpt-5.5-pro"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.4-pro"), (model) => { + model.modelID = Model.ID.make("gpt-5.4") model.body = { reasoning: { mode: "pro" } } }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.6"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.6-sol"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.6"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.6-sol"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) yield* credentials.create({ integrationID: Integration.ID.make("openai"), @@ -205,20 +205,20 @@ describe("OpenAIPlugin", () => { }) yield* addPlugin() - const eligible = required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5"))) + const eligible = required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))) expect(eligible.cost).toEqual([]) expect(eligible.enabled).toBe(true) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5-pro"))).enabled).toBe( + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5-pro"))).enabled).toBe( false, ) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.4-pro"))).enabled).toBe( + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.4-pro"))).enabled).toBe( false, ) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6"))).enabled).toBe(false) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.6-sol"))).enabled).toBe( + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6"))).enabled).toBe(false) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.6-sol"))).enabled).toBe( true, ) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-4.1"))).enabled).toBe(false) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(false) }), ) @@ -227,15 +227,15 @@ describe("OpenAIPlugin", () => { const catalog = yield* Catalog.Service const credentials = yield* Credential.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("@ai-sdk/openai"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("@ai-sdk/openai"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), () => {}) - catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5.5"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-4.1"), () => {}) }) yield* credentials.create({ integrationID: Integration.ID.make("openai"), @@ -243,8 +243,8 @@ describe("OpenAIPlugin", () => { }) yield* addPlugin() - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5.5"))).enabled).toBe(true) - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-4.1"))).enabled).toBe(true) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-5.5"))).enabled).toBe(true) + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("gpt-4.1"))).enabled).toBe(true) }), ) @@ -252,18 +252,18 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")), - package: ProviderV2.aisdk("test-provider"), + const item = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.make("custom-openai")), + package: Provider.aisdk("test-provider"), }) catalog.provider.update(item.id, (draft) => { draft.package = item.package }) - catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.model.update(item.id, Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))) + required(yield* catalog.model.get(Provider.ID.make("custom-openai"), Model.ID.make("gpt-5-chat-latest"))) .enabled, ).toBe(true) }), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index f0b9fff8d7a..e8f8323dbf9 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -3,25 +3,25 @@ import { Money } from "@opencode-ai/schema/money" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Credential } from "@opencode-ai/core/credential" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const integration = yield* Integration.Service yield* OpencodePlugin.effect(host).pipe( - Effect.provideService(EventV2.Service, events), + Effect.provideService(Bus.Service, bus), Effect.provideService(Integration.Service, integration), ) }) @@ -215,18 +215,18 @@ describe("OpencodePlugin", () => { const credentials = yield* Credential.Service const catalog = yield* Catalog.Service yield* catalog.transform((draft) => { - draft.provider.update(ProviderV2.ID.make("remote"), () => {}) - draft.model.update(ProviderV2.ID.make("remote"), ModelV2.ID.make("model"), (model) => { + draft.provider.update(Provider.ID.make("remote"), () => {}) + draft.model.update(Provider.ID.make("remote"), Model.ID.make("model"), (model) => { model.variants = [ { - id: ModelV2.VariantID.make("custom"), + id: Model.VariantID.make("custom"), settings: {}, headers: { "x-custom": "true" }, body: { custom: true }, }, ] }) - draft.model.update(ProviderV2.ID.make("remote"), ModelV2.ID.make("stale"), () => {}) + draft.model.update(Provider.ID.make("remote"), Model.ID.make("stale"), () => {}) }) yield* credentials.create({ integrationID: Integration.ID.make("opencode"), @@ -240,44 +240,44 @@ describe("OpencodePlugin", () => { yield* addPlugin() expect(authorization).toEqual(["Bearer secret"]) - const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("remote"))) + const provider = required(yield* catalog.provider.get(Provider.ID.make("remote"))) expect(provider).toMatchObject({ name: "Remote", integrationID: "opencode", - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: `${server.url.origin}/v1`, custom: "value" }, headers: { "x-org-id": "org" }, }) expect(yield* (yield* Integration.Service).get(Integration.ID.make("remote"))).toBeUndefined() - const model = required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("model"))) + const model = required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("model"))) expect(model).toMatchObject({ name: "Remote Model", family: "remote", capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }], limit: { context: 1000, output: 100 }, - package: ProviderV2.aisdk("@ai-sdk/openai-compatible"), + package: Provider.aisdk("@ai-sdk/openai-compatible"), settings: { baseURL: `${server.url.origin}/v1`, custom: "value", temperature: 0.5 }, headers: { "x-org-id": "org" }, }) expect(model.variants).toEqual([ { - id: ModelV2.VariantID.make("custom"), + id: Model.VariantID.make("custom"), settings: {}, headers: { "x-custom": "true" }, body: { custom: true }, }, { - id: ModelV2.VariantID.make("high"), + id: Model.VariantID.make("high"), settings: { temperature: 0.2 }, headers: {}, }, ]) expect( - required(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("disabled"))).enabled, + required(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("disabled"))).enabled, ).toBe(false) - expect(yield* catalog.model.get(ProviderV2.ID.make("remote"), ModelV2.ID.make("stale"))).toBeDefined() + expect(yield* catalog.model.get(Provider.ID.make("remote"), Model.ID.make("stale"))).toBeDefined() }), ({ server }) => Effect.promise(() => server.stop(true)), ), @@ -288,14 +288,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -304,8 +304,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(false) }), ), ) @@ -315,14 +315,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("free")), - modelID: ModelV2.ID.make("free"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("free")), + modelID: Model.ID.make("free"), + package: Provider.aisdk("test-provider"), cost: cost(0), }) catalog.provider.update(provider.id, () => {}) @@ -331,8 +331,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("free"))).enabled).toBe(true) }), ), ) @@ -342,14 +342,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("output-only")), - modelID: ModelV2.ID.make("output-only"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("output-only")), + modelID: Model.ID.make("output-only"), + package: Provider.aisdk("test-provider"), cost: cost(0, 1), }) catalog.provider.update(provider.id, () => {}) @@ -358,8 +358,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("public") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe( + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("public") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("output-only"))).enabled).toBe( true, ) }), @@ -371,14 +371,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -387,8 +387,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined() - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBeUndefined() + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -405,14 +405,14 @@ describe("OpencodePlugin", () => { }) }) yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -421,8 +421,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBeUndefined() - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBeUndefined() + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -432,15 +432,15 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.opencode), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.opencode), + package: Provider.aisdk("test-provider"), settings: { apiKey: "configured" }, }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, (draft) => { @@ -452,8 +452,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).settings?.apiKey).toBe("configured") - expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.opencode)).settings?.apiKey).toBe("configured") + expect(required(yield* catalog.model.get(Provider.ID.opencode, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -463,14 +463,14 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = ProviderV2.Info.make({ - ...ProviderV2.Info.empty(ProviderV2.ID.openai), - package: ProviderV2.aisdk("test-provider"), + const provider = Provider.Info.make({ + ...Provider.Info.empty(Provider.ID.openai), + package: Provider.aisdk("test-provider"), }) - const model = ModelV2.Info.make({ - ...ModelV2.Info.default(provider.id, ModelV2.ID.make("paid")), - modelID: ModelV2.ID.make("paid"), - package: ProviderV2.aisdk("test-provider"), + const model = Model.Info.make({ + ...Model.Info.default(provider.id, Model.ID.make("paid")), + modelID: Model.ID.make("paid"), + package: Provider.aisdk("test-provider"), cost: cost(1), }) catalog.provider.update(provider.id, () => {}) @@ -479,8 +479,8 @@ describe("OpencodePlugin", () => { }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).settings?.apiKey).toBeUndefined() - expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true) + expect(required(yield* catalog.provider.get(Provider.ID.openai)).settings?.apiKey).toBeUndefined() + expect(required(yield* catalog.model.get(Provider.ID.openai, Model.ID.make("paid"))).enabled).toBe(true) }), ), ) @@ -488,17 +488,17 @@ describe("OpencodePlugin", () => { it.effect("prefers gpt-5-nano as the opencode small model", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - const providerID = ProviderV2.ID.opencode + const providerID = Provider.ID.opencode yield* catalog.transform((catalog) => { catalog.provider.update(providerID, () => {}) - catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => { + catalog.model.update(providerID, Model.ID.make("cheap-mini"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [...cost(1, 1)] model.time.released = Date.now() }) - catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => { + catalog.model.update(providerID, Model.ID.make("gpt-5-nano"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] model.cost = [...cost(10, 10)] @@ -508,7 +508,7 @@ describe("OpencodePlugin", () => { const selected = yield* catalog.model.small(providerID) - expect(selected?.id).toBe(ModelV2.ID.make("gpt-5-nano")) + expect(selected?.id).toBe(Model.ID.make("gpt-5-nano")) }), ) }) diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 63520e01d3f..c2e3109f16f 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -2,19 +2,19 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* OpenRouterPlugin.effect(host) @@ -29,34 +29,34 @@ describe("OpenRouterPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { - provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider") + catalog.provider.update(Provider.ID.openrouter, (provider) => { + provider.package = Provider.aisdk("@openrouter/ai-sdk-provider") provider.headers = { Existing: "value" } }) - catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {}) + catalog.provider.update(Provider.ID.make("nvidia"), () => {}) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.openrouter))?.headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", }) - expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("nvidia")))?.headers).toBeUndefined() }), ) it.effect("creates an SDK only for the OpenRouter package", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openrouter, Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@ai-sdk/openai-compatible", options: { name: "openrouter" }, @@ -64,10 +64,10 @@ describe("OpenRouterPlugin", () => { expect(ignored.sdk).toBeUndefined() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), - modelID: ModelV2.ID.make("openai/gpt-5"), - package: ProviderV2.aisdk("test-provider"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom"), Model.ID.make("openai/gpt-5")), + modelID: Model.ID.make("openai/gpt-5"), + package: Provider.aisdk("test-provider"), }), package: "@openrouter/ai-sdk-provider", options: { name: "custom" }, @@ -80,21 +80,21 @@ describe("OpenRouterPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { - provider.package = ProviderV2.aisdk("@openrouter/ai-sdk-provider") + catalog.provider.update(Provider.ID.openrouter, (provider) => { + provider.package = Provider.aisdk("@openrouter/ai-sdk-provider") }) - catalog.provider.update(ProviderV2.ID.openai, () => {}) - catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {}) - catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5"), () => {}) - catalog.model.update(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"), () => {}) + catalog.provider.update(Provider.ID.openai, () => {}) + catalog.model.update(Provider.ID.openrouter, Model.ID.make("openai/gpt-5-chat"), () => {}) + catalog.model.update(Provider.ID.openrouter, Model.ID.make("openai/gpt-5"), () => {}) + catalog.model.update(Provider.ID.openai, Model.ID.make("openai/gpt-5-chat"), () => {}) }) yield* addPlugin() - expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe( + expect((yield* catalog.model.get(Provider.ID.openrouter, Model.ID.make("openai/gpt-5-chat")))?.enabled).toBe( false, ) - expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")))?.enabled).toBe(true) - expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe(true) + expect((yield* catalog.model.get(Provider.ID.openrouter, Model.ID.make("openai/gpt-5")))?.enabled).toBe(true) + expect((yield* catalog.model.get(Provider.ID.openai, Model.ID.make("openai/gpt-5-chat")))?.enabled).toBe(true) }), ) @@ -102,12 +102,12 @@ describe("OpenRouterPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {}) - catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {}) + catalog.provider.update(Provider.ID.make("custom-openrouter"), () => {}) + catalog.model.update(Provider.ID.make("custom-openrouter"), Model.ID.make("gpt-5-chat-latest"), () => {}) }) yield* addPlugin() expect( - (yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"))) + (yield* catalog.model.get(Provider.ID.make("custom-openrouter"), Model.ID.make("gpt-5-chat-latest"))) ?.enabled, ).toBe(true) }), diff --git a/packages/core/test/plugin/provider-sap-ai-core.test.ts b/packages/core/test/plugin/provider-sap-ai-core.test.ts index 09d99867f2c..5524c93af05 100644 --- a/packages/core/test/plugin/provider-sap-ai-core.test.ts +++ b/packages/core/test/plugin/provider-sap-ai-core.test.ts @@ -1,12 +1,12 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { Npm } from "@opencode-ai/util/npm" import { SapAICorePlugin } from "@opencode-ai/core/plugin/provider/sap-ai-core" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -19,7 +19,7 @@ const npm = Npm.Service.of({ }) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* SapAICorePlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm)) @@ -47,10 +47,10 @@ function withEnv(vars: Record, effect: () = } function model(providerID: string) { - return ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), - modelID: ModelV2.ID.make("sap-model"), - package: ProviderV2.aisdk(fixtureProvider), + return Model.Info.make({ + ...Model.Info.default(Provider.ID.make(providerID), Model.ID.make("sap-model")), + modelID: Model.ID.make("sap-model"), + package: Provider.aisdk(fixtureProvider), }) } @@ -60,7 +60,7 @@ describe("SapAICorePlugin", () => { { AICORE_SERVICE_KEY: undefined, AICORE_DEPLOYMENT_ID: "deployment", AICORE_RESOURCE_GROUP: "resource-group" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ @@ -83,7 +83,7 @@ describe("SapAICorePlugin", () => { }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ @@ -102,7 +102,7 @@ describe("SapAICorePlugin", () => { { AICORE_SERVICE_KEY: undefined, AICORE_DEPLOYMENT_ID: "deployment", AICORE_RESOURCE_GROUP: "resource-group" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ @@ -118,7 +118,7 @@ describe("SapAICorePlugin", () => { it.effect("uses the callable SDK for language selection", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = Object.assign((modelID: string) => ({ modelID, provider: "callable" }), { @@ -136,7 +136,7 @@ describe("SapAICorePlugin", () => { { AICORE_SERVICE_KEY: undefined, AICORE_DEPLOYMENT_ID: "deployment", AICORE_RESOURCE_GROUP: "resource-group" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = yield* aisdk.runSDK({ diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index 749f4c5520f..f72c19f90f3 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -1,19 +1,19 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, it as bun_it } from "bun:test" import { Effect } from "effect" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* SnowflakeCortexPlugin.effect(host) @@ -53,13 +53,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("ignores non-snowflake-cortex providers", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), - modelID: ModelV2.ID.make("gpt-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("openai"), Model.ID.make("gpt-4")), + modelID: Model.ID.make("gpt-4"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai", @@ -72,13 +72,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("creates SDK for snowflake-cortex using SNOWFLAKE_CORTEX_PAT env var", () => withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -92,13 +92,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("falls back to options.apiKey when SNOWFLAKE_CORTEX_PAT env var is absent", () => withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -116,13 +116,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("uses SNOWFLAKE_CORTEX_TOKEN env var", () => withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -136,13 +136,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("falls back to options.token when no Snowflake env token is set", () => withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", @@ -160,13 +160,13 @@ describe("SnowflakeCortexPlugin", () => { it.effect("sets includeUsage on the SDK options", () => withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - modelID: ModelV2.ID.make("claude-sonnet-4-6"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("snowflake-cortex"), Model.ID.make("claude-sonnet-4-6")), + modelID: Model.ID.make("claude-sonnet-4-6"), package: "aisdk:test-provider", }), package: "@ai-sdk/openai-compatible", diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 29820b714c5..bf4998ae7d3 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -2,18 +2,18 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) yield* VercelPlugin.effect(host) @@ -24,13 +24,13 @@ describe("VercelPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/vercel") + catalog.provider.update(Provider.ID.make("vercel"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/vercel") provider.headers = { ...provider.headers, Existing: "1" } }) }) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).toEqual({ + expect((yield* catalog.provider.get(Provider.ID.make("vercel")))?.headers).toEqual({ Existing: "1", "http-referer": "https://opencode.ai/", "x-title": "opencode", @@ -42,25 +42,25 @@ describe("VercelPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => - catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/vercel") + catalog.provider.update(Provider.ID.make("vercel"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/vercel") }), ) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer") - expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title") + expect((yield* catalog.provider.get(Provider.ID.make("vercel")))?.headers).not.toHaveProperty("HTTP-Referer") + expect((yield* catalog.provider.get(Provider.ID.make("vercel")))?.headers).not.toHaveProperty("X-Title") }), ) it.effect("creates @ai-sdk/vercel SDKs for custom provider IDs", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const aisdk = yield* AISDK.Service yield* addPlugin() const event = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), - modelID: ModelV2.ID.make("v0-1.0-md"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-vercel"), Model.ID.make("v0-1.0-md")), + modelID: Model.ID.make("v0-1.0-md"), package: "aisdk:@ai-sdk/vercel", }), package: "@ai-sdk/vercel", @@ -74,9 +74,9 @@ describe("VercelPlugin", () => { it.effect("ignores non-Vercel providers", () => Effect.gen(function* () { const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {})) + yield* catalog.transform((catalog) => catalog.provider.update(Provider.ID.make("gateway"), () => {})) yield* addPlugin() - expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.headers).toBeUndefined() + expect((yield* catalog.provider.get(Provider.ID.make("gateway")))?.headers).toBeUndefined() }), ) }) diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index 4b3e672c1ed..429ccdba194 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -3,18 +3,18 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Integration } from "@opencode-ai/core/integration" -import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Model } from "@opencode-ai/core/model" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* XAIPlugin.effect(host) }) @@ -61,9 +61,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), package: "@ai-sdk/openai-compatible", @@ -71,9 +71,9 @@ describe("XAIPlugin", () => { }) const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), package: "@ai-sdk/xai", @@ -91,9 +91,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("custom-xai"), Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), package: "@ai-sdk/xai", @@ -111,9 +111,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.make("xai"), Model.ID.make("alias")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), sdk: fakeSelectorSdk(calls), @@ -132,9 +132,9 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: ModelV2.Info.make({ - ...ModelV2.Info.default(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), - modelID: ModelV2.ID.make("grok-4"), + model: Model.Info.make({ + ...Model.Info.default(Provider.ID.openai, Model.ID.make("grok-4")), + modelID: Model.ID.make("grok-4"), package: "aisdk:@ai-sdk/xai", }), sdk: fakeSelectorSdk(calls), diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index 8f3bdd343a0..de599148dbc 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -1,18 +1,18 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderPlugins } from "@opencode-ai/core/plugin/provider" import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { - const plugin = yield* PluginV2.Service + const plugin = yield* Plugin.Service const host = yield* PluginHost.make(plugin) yield* ZenmuxPlugin.effect(host) }) @@ -31,13 +31,13 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))) + const result = required(yield* catalog.provider.get(Provider.ID.make("zenmux"))) expect(result.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" }) expect(Object.keys(required(result.headers)).sort()).toEqual(["HTTP-Referer", "X-Title"]) }), @@ -47,15 +47,15 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } provider.headers = { ...provider.headers, Existing: "value" } }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({ + expect(required(yield* catalog.provider.get(Provider.ID.make("zenmux"))).headers).toEqual({ Existing: "value", "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode", @@ -67,15 +67,15 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.make("zenmux"), (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") provider.settings = { ...provider.settings, baseURL: "https://zenmux.ai/api/v1" } provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" } }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).headers).toEqual({ + expect(required(yield* catalog.provider.get(Provider.ID.make("zenmux"))).headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) @@ -86,13 +86,13 @@ describe("ZenmuxPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.openrouter, (provider) => { + catalog.provider.update(Provider.ID.openrouter, (provider) => { provider.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" } }) }) yield* addPlugin() - expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).headers).toEqual({ + expect(required(yield* catalog.provider.get(Provider.ID.openrouter)).headers).toEqual({ "HTTP-Referer": "https://example.com/", "X-Title": "custom-title", }) diff --git a/packages/core/test/plugin/skill.test.ts b/packages/core/test/plugin/skill.test.ts index cd9a9b3d82a..7ce387f97fa 100644 --- a/packages/core/test/plugin/skill.test.ts +++ b/packages/core/test/plugin/skill.test.ts @@ -7,17 +7,17 @@ import { Location } from "@opencode-ai/core/location" import { Effect } from "effect" import { SkillPlugin } from "@opencode-ai/core/plugin/skill" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { host } from "./host" -const it = testEffect(AppNodeBuilder.build(SkillV2.node)) +const it = testEffect(AppNodeBuilder.build(Skill.node)) describe("SkillPlugin.Plugin", () => { it.effect("registers built-in skills", () => Effect.gen(function* () { - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* SkillPlugin.Plugin.effect( host({ app: { name: "test", version: "1.2.3", channel: "beta" }, diff --git a/packages/core/test/plugin/system-prompt.test.ts b/packages/core/test/plugin/system-prompt.test.ts index 724ab681b11..1c9c67d9c5c 100644 --- a/packages/core/test/plugin/system-prompt.test.ts +++ b/packages/core/test/plugin/system-prompt.test.ts @@ -1,13 +1,13 @@ import { describe, expect, test } from "bun:test" import { SystemPart } from "@opencode-ai/ai" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginHost } from "@opencode-ai/core/plugin/host" import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt" -import { SessionV2 } from "@opencode-ai/core/session" -import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session" +import { Session } from "@opencode-ai/core/session" +import type { SessionHooks } from "@opencode-ai/plugin/effect/session" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" import { Effect } from "effect" @@ -19,13 +19,15 @@ import PROMPT_DEFAULT from "../../src/session/runner/prompt/base.txt" const it = testEffect(PluginTestLayer) const fallback = PROMPT_DEFAULT const makeHost = Effect.gen(function* () { - const plugins = yield* PluginV2.Service + const agents = yield* Agent.Service + const plugins = yield* Plugin.Service + yield* agents.transform((draft) => draft.update(Agent.ID.make("build"), () => {})) return yield* PluginHost.make(plugins) }) const context = (id: string, system = fallback): SessionHooks["context"] => ({ - sessionID: SessionV2.ID.make("ses_system_prompt"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_system_prompt"), + agent: Agent.ID.make("build"), model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }), system: [SystemPart.make(system)], messages: [], @@ -33,7 +35,7 @@ const context = (id: string, system = fallback): SessionHooks["context"] => ({ }) describe("SystemPromptPlugin", () => { - test("uses V2 vocabulary in the Meta prompt", () => { + test("uses current vocabulary in the Meta prompt", () => { expect(PROMPT_META).toContain("webfetch tool") expect(PROMPT_META).toContain("subagent tool") expect(PROMPT_META).toContain("shell tool") @@ -92,10 +94,10 @@ describe("SystemPromptPlugin", () => { it.effect("preserves an explicit agent system prompt", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const hooks = yield* PluginHooks.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.system = "Custom agent prompt" }), ) @@ -111,6 +113,21 @@ describe("SystemPromptPlugin", () => { }), ) + it.effect("skips the hook when agent lookup fails", () => + Effect.gen(function* () { + const agents = yield* Agent.Service + const hooks = yield* PluginHooks.Service + const pluginHost = yield* makeHost + yield* SystemPromptPlugin.OpenAIPlugin.effect(pluginHost) + yield* agents.transform((draft) => draft.remove(Agent.ID.make("build"))) + const event = context("gpt-5") + + yield* hooks.trigger("session", "context", event) + + expect(event.system[0]?.text).toBe(fallback) + }), + ) + it.effect("allows one model-lab prompt plugin to be enabled independently", () => Effect.gen(function* () { const hooks = yield* PluginHooks.Service diff --git a/packages/core/test/plugin/variant.test.ts b/packages/core/test/plugin/variant.test.ts index 5e87748dc4a..1df1082fb66 100644 --- a/packages/core/test/plugin/variant.test.ts +++ b/packages/core/test/plugin/variant.test.ts @@ -3,9 +3,9 @@ import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { VariantPlugin } from "@opencode-ai/core/plugin/variant" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { Effect, Layer } from "effect" import { location } from "../fixture/location" @@ -23,17 +23,17 @@ describe("VariantPlugin", () => { Effect.gen(function* () { const service = yield* Catalog.Service yield* service.transform((catalog) => { - catalog.provider.update(ProviderV2.ID.opencode, (provider) => { - provider.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.provider.update(Provider.ID.opencode, (provider) => { + provider.package = Provider.aisdk("@ai-sdk/openai-compatible") }) - catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { - model.modelID = ModelV2.ID.make("glm-5.2") - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") + catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => { + model.modelID = Model.ID.make("glm-5.2") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") }) }) yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) - expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([ + expect((yield* service.model.get(Provider.ID.opencode, Model.ID.make("glm-5.2")))?.variants).toEqual([ expect.objectContaining({ id: "high", settings: { reasoningEffort: "high" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }), ]) @@ -44,15 +44,15 @@ describe("VariantPlugin", () => { Effect.gen(function* () { const service = yield* Catalog.Service yield* service.transform((catalog) => { - catalog.model.update(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2"), (model) => { - model.modelID = ModelV2.ID.make("glm-5.2") - model.package = ProviderV2.aisdk("@ai-sdk/openai-compatible") - model.variants = [{ id: ModelV2.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }] + catalog.model.update(Provider.ID.opencode, Model.ID.make("glm-5.2"), (model) => { + model.modelID = Model.ID.make("glm-5.2") + model.package = Provider.aisdk("@ai-sdk/openai-compatible") + model.variants = [{ id: Model.VariantID.make("high"), settings: {}, headers: { custom: "true" }, body: {} }] }) }) yield* VariantPlugin.Plugin.effect(host({ catalog: catalogHost(service) })) - expect((yield* service.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("glm-5.2")))?.variants).toEqual([ + expect((yield* service.model.get(Provider.ID.opencode, Model.ID.make("glm-5.2")))?.variants).toEqual([ expect.objectContaining({ id: "high", headers: { custom: "true" } }), expect.objectContaining({ id: "max", settings: { reasoningEffort: "max" } }), ]) diff --git a/packages/core/test/plugin/websearch-fixture.ts b/packages/core/test/plugin/websearch-fixture.ts index 406e093249c..61046aefc84 100644 --- a/packages/core/test/plugin/websearch-fixture.ts +++ b/packages/core/test/plugin/websearch-fixture.ts @@ -4,7 +4,7 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Credential } from "@opencode-ai/core/credential" import { Config } from "@opencode-ai/core/config" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Form } from "@opencode-ai/core/form" import { Integration } from "@opencode-ai/core/integration" import { WebSearch } from "@opencode-ai/core/websearch" @@ -42,7 +42,7 @@ const http = Layer.succeed( export const webSearchIntegrationTest = testEffect( Layer.merge( AppNodeBuilder.build( - LayerNode.group([Integration.node, Credential.node, EventV2.node, Form.node, WebSearch.node]), + LayerNode.group([Integration.node, Credential.node, Bus.node, Form.node, WebSearch.node]), [[Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))]], ), http, diff --git a/packages/core/test/project-copy.test.ts b/packages/core/test/project-copy.test.ts index d85d13dcc3b..c574a9b0415 100644 --- a/packages/core/test/project-copy.test.ts +++ b/packages/core/test/project-copy.test.ts @@ -9,7 +9,7 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AbsolutePath } from "@opencode-ai/core/schema" import { Git } from "@opencode-ai/core/git" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Project } from "@opencode-ai/core/project" import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectCopy } from "@opencode-ai/core/project/copy" @@ -18,7 +18,7 @@ import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, EventV2.node, ProjectDirectories.node])), + AppNodeBuilder.build(LayerNode.group([ProjectCopy.node, Database.node, Bus.node, ProjectDirectories.node])), ) function abs(input: string) { @@ -116,14 +116,14 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const temp = yield* Effect.promise(() => fs.realpath(path.dirname(input.root.path))) const parent = abs(path.join(temp, path.basename(input.root.path) + "-copy-created")) const target = abs(path.join(parent, "copy")) yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(parent, { recursive: true, force: true })).pipe(Effect.ignore), ) - const fiber = yield* events + const fiber = yield* bus .subscribe(ProjectCopy.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow @@ -278,8 +278,8 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const events = yield* EventV2.Service - const event = yield* events.subscribe(ProjectCopy.Event.Updated).pipe( + const bus = yield* Bus.Service + const event = yield* bus.subscribe(ProjectCopy.Event.Updated).pipe( Stream.take(1), Stream.runCollect, Effect.forkScoped, @@ -300,7 +300,7 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const target = abs(`${input.root.path}-copy-external`) yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(target, { recursive: true, force: true })).pipe(Effect.ignore), @@ -311,7 +311,7 @@ describe("ProjectCopy", () => { .values({ project_id: input.projectID, directory: target }) .run() .pipe(Effect.orDie) - const fiber = yield* events + const fiber = yield* bus .subscribe(ProjectCopy.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow diff --git a/packages/core/test/project.test.ts b/packages/core/test/project.test.ts index dc505465c72..44d36fd85f8 100644 --- a/packages/core/test/project.test.ts +++ b/packages/core/test/project.test.ts @@ -5,25 +5,25 @@ import path from "path" import { Effect, Layer, Schema } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Database } from "@opencode-ai/core/database/database" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" import { Hash } from "@opencode-ai/util/hash" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" -const it = testEffect(Layer.merge(AppNodeBuilder.build(ProjectV2.node), AppNodeBuilder.build(Database.node))) +const it = testEffect(Layer.merge(AppNodeBuilder.build(Project.node), AppNodeBuilder.build(Database.node))) -describe("ProjectV2.list", () => { +describe("Project.list", () => { it.effect("returns complete projects ordered by recent update", () => Effect.gen(function* () { const db = (yield* Database.Service).db - const project = yield* ProjectV2.Service + const project = yield* Project.Service yield* db .insert(ProjectTable) .values([ { - id: ProjectV2.ID.make("older"), + id: Project.ID.make("older"), worktree: abs("/older"), vcs: "git", name: "Older", @@ -34,7 +34,7 @@ describe("ProjectV2.list", () => { time_updated: 1, }, { - id: ProjectV2.ID.make("newer"), + id: Project.ID.make("newer"), worktree: abs("/newer"), sandboxes: [], time_created: 2, @@ -46,13 +46,13 @@ describe("ProjectV2.list", () => { expect(yield* project.list()).toEqual([ { - id: ProjectV2.ID.make("newer"), + id: Project.ID.make("newer"), worktree: abs("/newer"), time: { created: 2, updated: 2, initialized: 3 }, sandboxes: [], }, { - id: ProjectV2.ID.make("older"), + id: Project.ID.make("older"), worktree: abs("/older"), vcs: "git", name: "Older", @@ -67,7 +67,7 @@ describe("ProjectV2.list", () => { }) function remoteID(remote: string) { - return ProjectV2.ID.make(Hash.fast(`git-remote:${remote}`)) + return Project.ID.make(Hash.fast(`git-remote:${remote}`)) } function abs(value: string) { @@ -92,18 +92,18 @@ async function rootCommit(dir: string) { return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim() } -describe("ProjectV2.resolve", () => { +describe("Project.resolve", () => { it.live("returns global for non-git directory", () => Effect.gen(function* () { const tmp = yield* Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make("global")) + expect(result.id).toBe(Project.ID.make("global")) expect(path.resolve(result.directory)).toBe(path.parse(tmp.path).root) expect(result.previous).toBeUndefined() expect(result.vcs).toBeUndefined() @@ -117,11 +117,11 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path)) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make("global")) + expect(result.id).toBe(Project.ID.make("global")) expect(result.directory).toBe(yield* real(tmp.path)) expect(result.previous).toBeUndefined() expect(result.vcs?.type).toBe("git") @@ -135,11 +135,11 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) expect(result.directory).toBe(yield* real(tmp.path)) expect(result.previous).toBeUndefined() expect(result.vcs?.type).toBe("git") @@ -153,12 +153,12 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) expect(result.id).toBe(remoteID("github.com/Acme/App")) - expect(result.id).not.toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) expect(result.directory).toBe(yield* real(tmp.path)) expect(result.vcs?.type).toBe("git") }), @@ -176,7 +176,7 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" })) yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const a = yield* project.resolve(abs(ssh.path)) const b = yield* project.resolve(abs(https.path)) @@ -193,11 +193,11 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.id).toBe(ProjectV2.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) + expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))) }), ) @@ -209,11 +209,11 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id")) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) - expect(result.previous).toBe(ProjectV2.ID.make("old-id")) + expect(result.previous).toBe(Project.ID.make("old-id")) expect(result.id).toBe(remoteID("github.com/owner/repo")) }), ) @@ -225,7 +225,7 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service yield* project.resolve(abs(tmp.path)) @@ -241,7 +241,7 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true })) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b"))) @@ -270,13 +270,13 @@ describe("ProjectV2.resolve", () => { .quiet() await fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true }) }) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b"))) expect(result.vcs?.type).toBe("hg") expect(result.directory).toBe(abs(tmp.path)) - expect(result.id).not.toBe(ProjectV2.ID.make("global")) + expect(result.id).not.toBe(Project.ID.make("global")) expect(result.previous).toBeUndefined() }), ) @@ -289,7 +289,7 @@ describe("ProjectV2.resolve", () => { ) yield* Effect.promise(() => initRepo(tmp.path, { commit: true })) yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".hg"))) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) @@ -304,12 +304,12 @@ describe("ProjectV2.resolve", () => { (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ) yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".hg"))) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(tmp.path)) expect(result.vcs?.type).toBe("hg") - expect(result.id).toBe(ProjectV2.ID.make("global")) + expect(result.id).toBe(Project.ID.make("global")) }), ) @@ -326,12 +326,12 @@ describe("ProjectV2.resolve", () => { yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" })) yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id")) yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet()) - const project = yield* ProjectV2.Service + const project = yield* Project.Service const result = yield* project.resolve(abs(worktree)) expect(result.directory).toBe(yield* real(worktree)) - expect(result.previous).toBe(ProjectV2.ID.make("old-id")) + expect(result.previous).toBe(Project.ID.make("old-id")) expect(result.id).toBe(remoteID("github.com/owner/repo")) expect(result.vcs?.type).toBe("git") }), diff --git a/packages/core/test/pty/pty-session.test.ts b/packages/core/test/pty/pty-session.test.ts index d11f36fc0bf..05f6dbc588f 100644 --- a/packages/core/test/pty/pty-session.test.ts +++ b/packages/core/test/pty/pty-session.test.ts @@ -3,7 +3,7 @@ import { Cause, Deferred, Effect, Exit, Layer, Queue } from "effect" import { Config } from "@opencode-ai/core/config" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { Pty } from "@opencode-ai/core/pty" import type { PtyID } from "@opencode-ai/core/pty/schema" @@ -19,7 +19,7 @@ const locationLayer = Layer.succeed( ) const configLayer = Layer.mock(Config.Service)({ entries: () => Effect.succeed([]) }) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ + AppNodeBuilder.build(LayerNode.group([Pty.node, Bus.node]), [ [Config.node, configLayer], [Location.node, locationLayer], ]), @@ -27,7 +27,7 @@ const it = testEffect( const ptyTest = process.platform === "win32" ? it.live.skip : it.live const subscribePtyEvents = Effect.fn("PtySessionTest.subscribePtyEvents")(function* () { - const source = yield* EventV2.Service + const source = yield* Bus.Service const events = yield* Queue.unbounded() const unsubscribe = yield* source.listen((event) => { if (event.type === Pty.Event.Created.type) @@ -206,7 +206,7 @@ describe("pty", () => { const configuredShell = process.platform === "win32" ? undefined : Bun.which("bash") const configuredIt = testEffect( - AppNodeBuilder.build(LayerNode.group([Pty.node, EventV2.node]), [ + AppNodeBuilder.build(LayerNode.group([Pty.node, Bus.node]), [ [ Config.node, Layer.mock(Config.Service)({ diff --git a/packages/core/test/pty/ticket.test.ts b/packages/core/test/pty/ticket.test.ts index c478e721dba..563264ec71e 100644 --- a/packages/core/test/pty/ticket.test.ts +++ b/packages/core/test/pty/ticket.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { PtyID } from "@opencode-ai/core/pty/schema" import { PtyTicket } from "@opencode-ai/core/pty/ticket" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { testEffect } from "../lib/effect" const it = testEffect(LayerNode.compile(PtyTicket.node)) @@ -50,10 +50,10 @@ describe("PTY websocket tickets", () => { Effect.gen(function* () { const tickets = yield* PtyTicket.Service const ptyID = PtyID.ascending() - const workspaceID = WorkspaceV2.ID.ascending() + const workspaceID = Workspace.ID.ascending() const issued = yield* tickets.issue({ ptyID, workspaceID }) - expect(yield* tickets.consume({ ptyID, workspaceID: WorkspaceV2.ID.ascending(), ticket: issued.ticket })).toBe( + expect(yield* tickets.consume({ ptyID, workspaceID: Workspace.ID.ascending(), ticket: issued.ticket })).toBe( false, ) expect(yield* tickets.consume({ ptyID, workspaceID, ticket: issued.ticket })).toBe(true) diff --git a/packages/core/test/question.test.ts b/packages/core/test/question.test.ts index d60288cc796..cd3e0369b2a 100644 --- a/packages/core/test/question.test.ts +++ b/packages/core/test/question.test.ts @@ -2,30 +2,31 @@ import { describe, expect } from "bun:test" import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { EventV2 } from "@opencode-ai/core/event" -import { QuestionV2 } from "@opencode-ai/core/question" -import { SessionV2 } from "@opencode-ai/core/session" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" +import { Question } from "@opencode-ai/core/question" +import { Session } from "@opencode-ai/core/session" import { testEffect } from "./lib/effect" -const questions = AppNodeBuilder.build(LayerNode.group([EventV2.node, QuestionV2.node])) +const questions = AppNodeBuilder.build(LayerNode.group([Bus.node, Question.node])) const it = testEffect(questions) -const sessionID = SessionV2.ID.make("ses_question_test") -const question: QuestionV2.Info = { +const sessionID = Session.ID.make("ses_question_test") +const question: Question.Info = { question: "Which option?", header: "Option", options: [{ label: "One", description: "First option" }], } -const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* ( - service: QuestionV2.Interface, - input: QuestionV2.AskInput, +const waitForAsk = Effect.fn("QuestionTest.waitForAsk")(function* ( + service: Question.Interface, + input: Question.AskInput, ) { - const events = yield* EventV2.Service - const asked = yield* Deferred.make() - const unsubscribe = yield* events.listen((event) => - event.type === QuestionV2.Event.Asked.type - ? Deferred.succeed(asked, event.data as QuestionV2.Request).pipe(Effect.asVoid) + const bus = yield* Bus.Service + const asked = yield* Deferred.make() + const unsubscribe = yield* bus.listen((event) => + event.type === Question.Event.Asked.type + ? Deferred.succeed(asked, event.data as Question.Request).pipe(Effect.asVoid) : Effect.void, ) yield* Effect.addFinalizer(() => unsubscribe) @@ -33,15 +34,15 @@ const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* ( return { fiber, request: yield* Deferred.await(asked) } }) -describe("QuestionV2", () => { +describe("Question", () => { it.effect("publishes lifecycle events and settles a pending reply", () => Effect.gen(function* () { - const service = yield* QuestionV2.Service - const events = yield* EventV2.Service - const published: EventV2.Payload[] = [] - const unsubscribe = yield* events.listen((event) => + const service = yield* Question.Service + const bus = yield* Bus.Service + const published: Event.Payload[] = [] + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { - if (event.type.startsWith("question.v2.")) published.push(event) + if (event.type.startsWith("question.")) published.push(event) }), ) yield* Effect.addFinalizer(() => unsubscribe) @@ -54,20 +55,20 @@ describe("QuestionV2", () => { expect(yield* Fiber.join(fiber)).toEqual([["One"]]) expect(yield* service.list()).toEqual([]) expect(published.map((event) => [event.type, event.data])).toEqual([ - [QuestionV2.Event.Asked.type, request], - [QuestionV2.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }], + [Question.Event.Asked.type, request], + [Question.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }], ]) }), ) it.effect("publishes rejection, fails the ask, and rejects unknown IDs", () => Effect.gen(function* () { - const service = yield* QuestionV2.Service - const events = yield* EventV2.Service - const published: EventV2.Payload[] = [] - const unsubscribe = yield* events.listen((event) => + const service = yield* Question.Service + const bus = yield* Bus.Service + const published: Event.Payload[] = [] + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { - if (event.type === QuestionV2.Event.Rejected.type) published.push(event) + if (event.type === Question.Event.Rejected.type) published.push(event) }), ) yield* Effect.addFinalizer(() => unsubscribe) @@ -76,15 +77,15 @@ describe("QuestionV2", () => { yield* service.reject(request.id) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError") + if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("Question.RejectedError") expect(published.map((event) => event.data)).toEqual([{ sessionID, requestID: request.id }]) - const unknown = QuestionV2.ID.ascending("que_unknown") + const unknown = Question.ID.ascending("que_unknown") expect(yield* service.reply({ requestID: unknown, answers: [] }).pipe(Effect.flip)).toEqual( - new QuestionV2.NotFoundError({ requestID: unknown }), + new Question.NotFoundError({ requestID: unknown }), ) expect(yield* service.reject(unknown).pipe(Effect.flip)).toEqual( - new QuestionV2.NotFoundError({ requestID: unknown }), + new Question.NotFoundError({ requestID: unknown }), ) }), ) @@ -93,21 +94,21 @@ describe("QuestionV2", () => { Effect.gen(function* () { const firstScope = yield* Scope.make() const secondScope = yield* Scope.make() - const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), QuestionV2.Service) - const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), QuestionV2.Service) + const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), Question.Service) + const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), Question.Service) const fiber = yield* first.ask({ sessionID, questions: [question] }).pipe(Effect.forkScoped) yield* Effect.yieldNow const request = (yield* first.list())[0]! expect(yield* second.list()).toEqual([]) expect(yield* second.reply({ requestID: request.id, answers: [["One"]] }).pipe(Effect.flip)).toEqual( - new QuestionV2.NotFoundError({ requestID: request.id }), + new Question.NotFoundError({ requestID: request.id }), ) yield* Scope.close(firstScope, Exit.void) const exit = yield* Fiber.await(fiber) expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError") + if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("Question.RejectedError") yield* Scope.close(secondScope, Exit.void) }), ) diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index 3ad15fc730a..b8f46d91aff 100644 --- a/packages/core/test/session-compact.test.ts +++ b/packages/core/test/session-compact.test.ts @@ -5,14 +5,14 @@ import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -31,10 +31,10 @@ const model = Model.make({ route: OpenAIChat.route.with({ limits: { context: 10_000, output: 1_000 } }), }) const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), @@ -62,7 +62,7 @@ const locations = Layer.effect( LocationServiceMap.Service, LayerMap.make( () => - // The test only needs the compaction location service used by SessionV2.compact. + // The test only needs the compaction location service used by Session.compact. // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion SessionCompaction.layer.pipe( Layer.provide(client), @@ -73,25 +73,25 @@ const locations = Layer.effect( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ [LocationServiceMap.node, locations], - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) -describe("SessionV2.compact", () => { +describe("Session.compact", () => { it.effect("durably admits and coalesces manual compaction", () => Effect.gen(function* () { requests = [] - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const created = yield* session.create({ location }) const messageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID: created.id, inputID: messageID, input: { @@ -100,7 +100,7 @@ describe("SessionV2.compact", () => { delivery: "steer", }, }) - yield* events.publish(SessionEvent.InputPromoted, { + yield* bus.publish(SessionEvent.InputPromoted, { sessionID: created.id, inputID: messageID, }) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 4111ac222fc..89f1f18d39e 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -6,7 +6,7 @@ import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" @@ -15,7 +15,7 @@ import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { App } from "@opencode-ai/core/app" @@ -78,7 +78,7 @@ const models = Layer.mock(SessionRunnerModel.Service)({ }) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionCompaction.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, SessionCompaction.node]), [ [llmClient, client], [Config.node, config], @@ -136,10 +136,10 @@ it.effect("manual compaction summarizes short context instead of no-op", () => requests = [] const db = (yield* Database.Service).db const compaction = yield* SessionCompaction.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service - const sessionID = SessionV2.ID.make("ses_manual_compaction") - const parentID = SessionV2.ID.make("ses_manual_compaction_parent") + const sessionID = Session.ID.make("ses_manual_compaction") + const parentID = Session.ID.make("ses_manual_compaction_parent") const userMessage = { id: SessionMessage.ID.create(), type: "user" as const, @@ -174,7 +174,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () => ), ) - const delta = yield* events + const delta = yield* bus .subscribe(SessionEvent.Compaction.Delta) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow @@ -215,9 +215,9 @@ it.effect("manual compaction summarizes short context instead of no-op", () => .all() .pipe(Effect.orDie), ).toEqual([ - { type: EventV2.versionedType(SessionEvent.Compaction.Started.type, 1) }, - { type: EventV2.versionedType(SessionEvent.UsageRecorded.type, 1) }, - { type: EventV2.versionedType(SessionEvent.Compaction.Ended.type, 1) }, + { type: Bus.versionedType(SessionEvent.Compaction.Started.type, 1) }, + { type: Bus.versionedType(SessionEvent.UsageRecorded.type, 1) }, + { type: Bus.versionedType(SessionEvent.Compaction.Ended.type, 1) }, ]) }), ) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 422d5892300..ae048e228b1 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -2,20 +2,20 @@ import { describe, expect } from "bun:test" import path from "path" import { DateTime, Effect, Layer, Stream } from "effect" import { Money } from "@opencode-ai/schema/money" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { asc, eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Model } from "@opencode-ai/core/model" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionV1 } from "@opencode-ai/core/v1/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" @@ -24,42 +24,42 @@ import { SessionPending } from "@opencode-ai/core/session/pending" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { testEffect } from "./lib/effect" import { tmpdir } from "./fixture/tmpdir" const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -const id = SessionV2.ID.create() +const id = Session.ID.create() /** Public session events from a `log` read, without synced markers. */ -const logEvents = (session: SessionV2.Interface, sessionID: SessionV2.ID, follow?: boolean) => +const logEvents = (session: Session.Interface, sessionID: Session.ID, follow?: boolean) => session .log({ sessionID, follow }) - .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !EventV2.isSynced(item))) + .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !Bus.isSynced(item))) -const assertCreateInputTypes = (session: SessionV2.Interface) => { +const assertCreateInputTypes = (session: Session.Interface) => { // @ts-expect-error location or parentID is required. session.create({}) // @ts-expect-error child sessions inherit their parent's location. - session.create({ parentID: SessionV2.ID.create(), location }) + session.create({ parentID: Session.ID.create(), location }) } void assertCreateInputTypes @@ -70,10 +70,10 @@ function withTmp(f: (directory: string) => Effect.Effect) { ).pipe(Effect.flatMap((tmp) => f(tmp.path))) } -describe("SessionV2.create", () => { +describe("Session.create", () => { it.effect("creates a fresh projected session when the ID is omitted", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const first = yield* session.create({ location }) const second = yield* session.create({ location }) @@ -85,7 +85,7 @@ describe("SessionV2.create", () => { it.effect("returns the original session when the ID is retried", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { id, location } const first = yield* session.create(input) @@ -98,18 +98,18 @@ describe("SessionV2.create", () => { it.effect("stores supplied immutable create attributes", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const workspaceID = WorkspaceV2.ID.make("wrk_test") - const model = ModelV2.Ref.make({ - id: ModelV2.ID.make("sonnet"), - providerID: ProviderV2.ID.anthropic, - variant: ModelV2.VariantID.make("fast"), + const session = yield* Session.Service + const workspaceID = Workspace.ID.make("wrk_test") + const model = Model.Ref.make({ + id: Model.ID.make("sonnet"), + providerID: Provider.ID.anthropic, + variant: Model.VariantID.make("fast"), }) expect( yield* session.create({ location: Location.Ref.make({ directory: location.directory, workspaceID }), - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model, }), ).toMatchObject({ location: { directory: location.directory, workspaceID }, agent: "build", model }) @@ -118,7 +118,7 @@ describe("SessionV2.create", () => { it.effect("inherits location from an existing parent when omitted", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location }) const child = yield* session.create({ parentID: parent.id, title: "child" }) @@ -128,18 +128,18 @@ describe("SessionV2.create", () => { it.effect("rejects child creation when the parent does not exist", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const missing = SessionV2.ID.create() + const session = yield* Session.Service + const missing = Session.ID.create() expect(yield* Effect.flip(session.create({ parentID: missing, title: "child" }))).toEqual( - new SessionV2.NotFoundError({ sessionID: missing }), + new Session.NotFoundError({ sessionID: missing }), ) }), ) it.effect("filters root sessions before applying the page limit", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const staleRoot = yield* session.create({ location, title: "stale root" }) const root = yield* session.create({ location, title: "root" }) @@ -173,7 +173,7 @@ describe("SessionV2.create", () => { it.effect("filters direct child sessions by parent ID", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location, title: "parent" }) const child = yield* session.create({ parentID: parent.id, title: "child" }) yield* session.create({ location, title: "other root" }) @@ -186,8 +186,8 @@ describe("SessionV2.create", () => { it.effect("forks a session by replaying a durable fork event into copied projected rows", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location, title: "Parent" }) const admitted = yield* session.prompt({ @@ -195,9 +195,9 @@ describe("SessionV2.create", () => { text: "First", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") const forked = yield* session.fork({ sessionID: parent.id }) const parentContext = yield* session.context(parent.id) @@ -232,13 +232,13 @@ describe("SessionV2.create", () => { text: "Parent changed", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") yield* session.prompt({ sessionID: forked.id, text: "Child continues", resume: false, }) - yield* SessionPending.promote(db, events, forked.id, "steer") + yield* SessionPending.promote(db, bus, forked.id, "steer") expect((yield* session.context(parent.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) expect((yield* session.context(forked.id)).map((message) => message.type)).toEqual(["user", "synthetic", "user"]) @@ -254,8 +254,8 @@ describe("SessionV2.create", () => { it.effect("forks before the selected boundary message", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const parent = yield* session.create({ location }) const first = yield* session.prompt({ @@ -263,22 +263,22 @@ describe("SessionV2.create", () => { text: "First", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") const second = yield* session.prompt({ sessionID: parent.id, text: "Second", resume: false, }) - yield* SessionPending.promote(db, events, parent.id, "steer") + yield* SessionPending.promote(db, bus, parent.id, "steer") const assistantMessageID = SessionMessage.ID.create() - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) - yield* events.publish(SessionEvent.Step.Started, { + const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) + yield* bus.publish(SessionEvent.Step.Started, { sessionID: parent.id, assistantMessageID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model, }) - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID: parent.id, assistantMessageID, finish: "stop", @@ -308,15 +308,15 @@ describe("SessionV2.create", () => { it.effect("returns the existing Session when one ID is reused with different create arguments", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ id, location }) const changed = [ { id, location: Location.Ref.make({ directory: AbsolutePath.make("/other") }) }, - { id, location, agent: AgentV2.ID.make("build") }, + { id, location, agent: Agent.ID.make("build") }, { id, location, - model: ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }), + model: Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }), }, ] @@ -329,7 +329,7 @@ describe("SessionV2.create", () => { it.effect("returns one recorded session to concurrent exact retries", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { id, location } const created = yield* Effect.all([session.create(input), session.create(input)], { concurrency: "unbounded" }) @@ -341,7 +341,7 @@ describe("SessionV2.create", () => { it.effect("returns the current Session projection after updates", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const input = { id, location } const created = yield* session.create(input) @@ -354,12 +354,12 @@ describe("SessionV2.create", () => { it.effect("returns the current Session projection after projected updates", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const input = { id, location } const created = yield* session.create(input) - yield* events.publish(SessionV1.Event.Updated, { + yield* bus.publish(SessionV1.Event.Updated, { sessionID: id, info: SessionV1.SessionInfo.make({ id, @@ -379,19 +379,19 @@ describe("SessionV2.create", () => { it.effect("persists creation through the existing legacy created event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const created = yield* session.create({ location }) expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), - ).toMatchObject([{ type: EventV2.versionedType(SessionV1.Event.Created.type, 1) }]) + ).toMatchObject([{ type: Bus.versionedType(SessionV1.Event.Created.type, 1) }]) }), ) it.effect("persists caller-ID creation through the existing created event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const created = yield* session.create({ id, location }) @@ -403,10 +403,10 @@ describe("SessionV2.create", () => { }), ) - it.effect("omits legacy creation rows from the V2 Session event stream", () => + it.effect("omits legacy creation rows from the Session event stream", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const created = yield* session.create({ location }) yield* session.prompt({ @@ -414,7 +414,7 @@ describe("SessionV2.create", () => { text: "Hello", resume: false, }) - yield* SessionPending.promote(db, events, created.id, "steer") + yield* SessionPending.promote(db, bus, created.id, "steer") expect( Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(2), Stream.runCollect)), @@ -431,10 +431,10 @@ describe("SessionV2.create", () => { it.effect("replays one prompt lifecycle into a fresh target database", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const sourceEvents = yield* EventV2.Service + const session = yield* Session.Service + const sourceEvents = yield* Bus.Service const sourceDb = (yield* Database.Service).db - const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location }) + const created = yield* session.create({ id: Session.ID.make("ses_fresh_target_replay"), location }) const admitted = yield* session.prompt({ sessionID: created.id, text: "Replay lifecycle", @@ -462,22 +462,22 @@ describe("SessionV2.create", () => { ) const targetDatabase = Database.layer({ path: path.join(tmp.path, "target.sqlite") }) const targetLayer = AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node]), [[Database.node, targetDatabase]], ) yield* Effect.gen(function* () { const db = (yield* Database.Service).db - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service yield* db .insert(ProjectTable) - .values({ id: ProjectV2.ID.global, worktree: location.directory, sandboxes: [] }) + .values({ id: Project.ID.global, worktree: location.directory, sandboxes: [] }) .run() .pipe(Effect.orDie) expect(yield* store.get(created.id)).toBeUndefined() - expect(yield* events.replayAll(serialized.slice(0, 2))).toBe(created.id) + expect(yield* bus.replayAll(serialized.slice(0, 2))).toBe(created.id) expect(yield* SessionPending.find(db, admitted.id)).toMatchObject({ id: admitted.id, sessionID: created.id, @@ -488,7 +488,7 @@ describe("SessionV2.create", () => { }) expect(yield* store.context(created.id)).toEqual([]) - expect(yield* events.replayAll(serialized.slice(2))).toBe(created.id) + expect(yield* bus.replayAll(serialized.slice(2))).toBe(created.id) expect(yield* SessionPending.find(db, admitted.id)).toBeUndefined() expect(yield* store.context(created.id)).toMatchObject([ { id: admitted.id, type: "user", text: "Replay lifecycle" }, @@ -502,9 +502,9 @@ describe("SessionV2.create", () => { .all() .pipe(Effect.orDie)).map((event) => [event.seq, event.type]), ).toEqual([ - [0, EventV2.versionedType(SessionV1.Event.Created.type, 1)], - [1, EventV2.versionedType(SessionEvent.InputAdmitted.type, 1)], - [2, EventV2.versionedType(SessionEvent.InputPromoted.type, 1)], + [0, Bus.versionedType(SessionV1.Event.Created.type, 1)], + [1, Bus.versionedType(SessionEvent.InputAdmitted.type, 1)], + [2, Bus.versionedType(SessionEvent.InputPromoted.type, 1)], ]) }).pipe(Effect.provide(Layer.fresh(targetLayer))) }), @@ -512,8 +512,8 @@ describe("SessionV2.create", () => { it.effect("does not mask unrelated created projector defects", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const event = yield* EventV2.Service + const session = yield* Session.Service + const event = yield* Bus.Service const defect = new Error("unrelated projector defect") yield* event.project(SessionV1.Event.Created, () => Effect.die(defect)) @@ -524,7 +524,7 @@ describe("SessionV2.create", () => { it.live("runs a shell command and projects the started/ended shell message", () => withTmp((directory) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), }) @@ -544,7 +544,7 @@ describe("SessionV2.create", () => { it.live("still emits shell ended for a failing command", () => withTmp((directory) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }), }) @@ -562,10 +562,10 @@ describe("SessionV2.create", () => { it.effect("switches the selected agent through the durable Session event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("plan") }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("plan") }) expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" }) expect( @@ -576,11 +576,11 @@ describe("SessionV2.create", () => { it.effect("rejects an agent switch for a missing Session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const missing = SessionV2.ID.make("ses_missing_agent_switch") + const session = yield* Session.Service + const missing = Session.ID.make("ses_missing_agent_switch") expect( - yield* session.switchAgent({ sessionID: missing, agent: AgentV2.ID.make("plan") }).pipe( + yield* session.switchAgent({ sessionID: missing, agent: Agent.ID.make("plan") }).pipe( Effect.flip, Effect.map((error) => error._tag), ), @@ -590,28 +590,28 @@ describe("SessionV2.create", () => { it.effect("switches the selected model through the durable Session event", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) - const model = ModelV2.Ref.make({ - id: ModelV2.ID.make("sonnet"), - providerID: ProviderV2.ID.anthropic, - variant: ModelV2.VariantID.make("high"), + const model = Model.Ref.make({ + id: Model.ID.make("sonnet"), + providerID: Provider.ID.anthropic, + variant: Model.VariantID.make("high"), }) yield* session.switchModel({ sessionID: created.id, model }) expect(yield* session.get(created.id)).toMatchObject({ model }) - const events = Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)) - expect(events).toMatchObject([{ type: "session.model.selected" }]) - expect(events[0]?.data).toEqual({ sessionID: created.id, model }) + const bus = Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)) + expect(bus).toMatchObject([{ type: "session.model.selected" }]) + expect(bus[0]?.data).toEqual({ sessionID: created.id, model }) }), ) it.effect("ignores a model switch when the selected model is unchanged", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }) + const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }) yield* session.switchModel({ sessionID: created.id, model }) yield* session.switchModel({ sessionID: created.id, model }) @@ -626,13 +626,13 @@ describe("SessionV2.create", () => { it.effect("treats an omitted variant as the default variant", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }) + const session = yield* Session.Service + const model = Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }) const created = yield* session.create({ location, model }) yield* session.switchModel({ sessionID: created.id, - model: ModelV2.Ref.make({ ...model, variant: ModelV2.VariantID.make("default") }), + model: Model.Ref.make({ ...model, variant: Model.VariantID.make("default") }), }) const { db } = yield* Database.Service @@ -644,14 +644,14 @@ describe("SessionV2.create", () => { it.effect("rejects a model switch for a missing Session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const missing = SessionV2.ID.make("ses_missing_model_switch") + const session = yield* Session.Service + const missing = Session.ID.make("ses_missing_model_switch") expect( yield* session .switchModel({ sessionID: missing, - model: ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }), + model: Model.Ref.make({ id: Model.ID.make("sonnet"), providerID: Provider.ID.anthropic }), }) .pipe( Effect.flip, diff --git a/packages/core/test/session-error.test.ts b/packages/core/test/session-error.test.ts index 825910f7a48..0b0ed9d905b 100644 --- a/packages/core/test/session-error.test.ts +++ b/packages/core/test/session-error.test.ts @@ -15,8 +15,8 @@ import { UnknownProviderReason, ToolFailure, } from "@opencode-ai/ai" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +import { Permission } from "@opencode-ai/core/permission" +import { Tool } from "@opencode-ai/schema/tool" import { toSessionError } from "@opencode-ai/core/session/to-session-error" import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry" @@ -56,7 +56,7 @@ describe("toSessionError", () => { }) test("preserves the permission rejection type without exposing internal fields", () => { - const blocked = new PermissionV2.BlockedError({ rules: [], permission: "external_directory", resources: [] }) + const blocked = new Permission.BlockedError({ rules: [], permission: "external_directory", resources: [] }) expect(toSessionError(blocked)).toEqual({ type: "permission.rejected", message: "Permission denied: external_directory", @@ -65,7 +65,7 @@ describe("toSessionError", () => { type: "permission.rejected", message: "Permission denied: external_directory", }) - expect(toSessionError(new Tool.Failure({ message: "failed" }))).toEqual({ + expect(toSessionError(new Tool.Error({ message: "failed" }))).toEqual({ type: "tool.execution", message: "failed", }) diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 9ff3cc0c54d..09920eae7c9 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -3,24 +3,23 @@ import { LLMError, TransportReason } from "@opencode-ai/ai" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionRestart } from "@opencode-ai/core/session/execution/restart" import { UserInterruptedError } from "@opencode-ai/core/session/error" import { SessionRunner } from "@opencode-ai/core/session/runner" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { Context, Deferred, Effect, Exit, Fiber, Layer, LayerMap, Scope } from "effect" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionStore.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionStore.node]))) describe("SessionExecution lifecycle", () => { test("classifies success and typed failure terminals", () => { @@ -36,11 +35,6 @@ describe("SessionExecution lifecycle", () => { ), ), ).toEqual({ type: "failed", error: { type: "provider.transport", message: "Disconnected" } }) - const storage = new ToolOutputStore.StorageError({ operation: "encode", cause: new Error("invalid output") }) - expect(SessionExecution.terminal(Exit.fail(storage))).toEqual({ - type: "failed", - error: { type: "unknown", message: storage.message }, - }) }) test("defaults owner-scope interruption to shutdown and preserves explicit reasons", () => { @@ -58,8 +52,8 @@ describe("SessionExecution lifecycle", () => { Effect.gen(function* () { const database = yield* Database.Service const store = yield* SessionStore.Service - const first = SessionV2.ID.make("ses_recover_first") - const second = SessionV2.ID.make("ses_recover_second") + const first = Session.ID.make("ses_recover_first") + const second = Session.ID.make("ses_recover_second") yield* seedSessions(database, [first, second], { time_suspended: Date.now() }) expect(yield* store.consumeSuspended(first)).toBe(true) @@ -72,8 +66,8 @@ describe("SessionExecution lifecycle", () => { it.effect("suspension survives teardown interruption and clears when a drain finishes on its own", () => Effect.gen(function* () { const database = yield* Database.Service - const interrupted = SessionV2.ID.make("ses_suspend_interrupted") - const completed = SessionV2.ID.make("ses_suspend_completed") + const interrupted = Session.ID.make("ses_suspend_interrupted") + const completed = Session.ID.make("ses_suspend_completed") yield* seedSessions(database, [interrupted, completed]) const draining = yield* Deferred.make() @@ -108,11 +102,11 @@ describe("SessionExecution lifecycle", () => { it.effect("starts every suspended execution without waiting for earlier drains to finish", () => Effect.gen(function* () { const database = yield* Database.Service - const sessionIDs = Array.from({ length: 5 }, (_, index) => SessionV2.ID.make(`ses_resume_concurrent_${index}`)) + const sessionIDs = Array.from({ length: 5 }, (_, index) => Session.ID.make(`ses_resume_concurrent_${index}`)) yield* seedSessions(database, sessionIDs, { time_suspended: Date.now() }) const fourStarted = yield* Deferred.make() - const started: SessionV2.ID[] = [] + const started: Session.ID[] = [] const scope = yield* Scope.make() yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void)) const context = yield* buildExecution(scope, ({ sessionID }) => @@ -133,8 +127,8 @@ describe("SessionExecution lifecycle", () => { it.effect("resumes each suspended Session at most once", () => Effect.gen(function* () { const database = yield* Database.Service - const first = SessionV2.ID.make("ses_resume_first") - const second = SessionV2.ID.make("ses_resume_second") + const first = Session.ID.make("ses_resume_first") + const second = Session.ID.make("ses_resume_second") yield* seedSessions(database, [first, second], { time_suspended: Date.now() }) const drained: string[] = [] @@ -157,7 +151,7 @@ describe("SessionExecution lifecycle", () => { function seedSessions( database: Database.Service["Service"], - sessionIDs: ReadonlyArray, + sessionIDs: ReadonlyArray, values: { time_suspended?: number } = {}, ) { return Effect.gen(function* () { @@ -199,7 +193,7 @@ function suspensions(database: Database.Service["Service"]) { function buildExecution(scope: Scope.Closeable, drain: SessionRunner.Interface["drain"]) { return Effect.gen(function* () { const database = yield* Database.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service const runner = Layer.succeed(SessionRunner.Service, SessionRunner.Service.of({ drain })) const locations = Layer.effect( @@ -215,7 +209,7 @@ function buildExecution(scope: Scope.Closeable, drain: SessionRunner.Interface[" SessionRestart.layer.pipe( Layer.provideMerge(SessionExecution.layer), Layer.provide(Layer.succeed(Database.Service, database)), - Layer.provide(Layer.succeed(EventV2.Service, events)), + Layer.provide(Layer.succeed(Bus.Service, bus)), Layer.provide(Layer.succeed(SessionStore.Service, store)), Layer.provide(locations), ), diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index a6910f826f7..ea12b528f24 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -1,22 +1,22 @@ import { expect } from "bun:test" import { LLMClient, LLMEvent, LLMResponse, Model, SystemPart, ToolDefinition, type LLMRequest } from "@opencode-ai/ai" import { OpenAIChat } from "@opencode-ai/ai/protocols" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery" import { Instructions } from "@opencode-ai/core/instructions" import { InstructionBuiltIns } from "@opencode-ai/core/instructions/builtins" import { Location } from "@opencode-ai/core/location" import { McpInstructions } from "@opencode-ai/core/mcp/instructions" -import { ModelV2 } from "@opencode-ai/core/model" +import { ID } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { ReferenceInstructions } from "@opencode-ai/core/reference/instructions" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionEvent } from "@opencode-ai/core/session/event" @@ -38,7 +38,7 @@ import { SessionStore } from "@opencode-ai/core/session/store" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Tool } from "@opencode-ai/core/tool" import { asc, eq } from "drizzle-orm" import { Effect, Layer, Schema, Stream } from "effect" import { testEffect } from "./lib/effect" @@ -98,7 +98,7 @@ const skills = Layer.mock(SkillInstructions.Service, { load: () => Effect.succee const references = Layer.mock(ReferenceInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const mcp = Layer.mock(McpInstructions.Service, { load: () => Effect.succeed(Instructions.empty) }) const plugins = Layer.mock(PluginSupervisor.Service, { flush: Effect.void }) -const tools = Layer.mock(ToolRegistry.Service, { +const tools = Layer.mock(Tool.Service, { snapshot: () => Effect.succeed({ codeModeCatalog: [ @@ -111,18 +111,17 @@ const tools = Layer.mock(ToolRegistry.Service, { definitions: [ToolDefinition.make({ name: "lookup", description: "Lookup", inputSchema: { type: "object" } })], execute: () => Effect.die(new Error("unused")), }), - register: () => Effect.die(new Error("unused")), - registerBatch: () => Effect.die(new Error("unused")), + transform: () => Effect.die(new Error("unused")), }) const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, InstructionBuiltIns.node, PluginHooks.node, SessionGenerateNode.node, @@ -136,7 +135,7 @@ const it = testEffect( [ReferenceInstructions.node, references], [McpInstructions.node, mcp], [PluginSupervisor.node, plugins], - [ToolRegistry.node, tools], + [Tool.node, tools], [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], ], ), @@ -144,8 +143,8 @@ const it = testEffect( const durableState = (db: Database.Interface["db"], sessionID: SessionSchema.ID) => Effect.all({ - sequence: EventV2.latestSequence(db, sessionID), - events: db + sequence: Bus.latestSequence(db, sessionID), + bus: db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, sessionID)) @@ -185,11 +184,11 @@ const userTexts = (request: LLMRequest) => const setup = Effect.gen(function* () { const { db } = yield* Database.Service - const events = yield* EventV2.Service - const agents = yield* AgentV2.Service + const bus = yield* Bus.Service + const agents = yield* Agent.Service const instructionBuiltIns = yield* InstructionBuiltIns.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" }), ) @@ -207,71 +206,71 @@ const setup = Effect.gen(function* () { directory: "/project", title: "Generate test", version: "test", - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), }) .run() .pipe(Effect.orDie) - return { db, events, instructions: yield* instructionBuiltIns.load(sessionID) } + return { db, bus, instructions: yield* instructionBuiltIns.load(sessionID) } }) it.effect("generates from fresh settled Session context without durable mutation", () => Effect.gen(function* () { requests.length = 0 instruction = "Initial context" - const { db, events, instructions } = yield* setup - yield* InstructionState.prepare(db, events, instructions, sessionID) + const { db, bus, instructions } = yield* setup + yield* InstructionState.prepare(db, bus, instructions, sessionID) const existing = SessionMessage.ID.create() - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: existing, input: { type: "user", data: { text: "Existing durable context" }, delivery: "steer" }, }) - yield* events.publish(SessionEvent.InputPromoted, { sessionID, inputID: existing }) + yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: existing }) const settledAssistant = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: settledAssistant, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("generate-model"), providerID: ProviderV2.ID.make("test") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("generate-model"), providerID: Provider.ID.make("test") }, }) - yield* events.publish(SessionEvent.Text.Started, { + yield* bus.publish(SessionEvent.Text.Started, { sessionID, assistantMessageID: settledAssistant, ordinal: 0, }) - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID, assistantMessageID: settledAssistant, ordinal: 0, text: "Settled partial answer", }) const activeAssistant = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: activeAssistant, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("generate-model"), providerID: ProviderV2.ID.make("test") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("generate-model"), providerID: Provider.ID.make("test") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID: activeAssistant, callID: "active-call", name: "echo", }) - yield* events.publish(SessionEvent.Tool.Input.Ended, { + yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID, assistantMessageID: activeAssistant, callID: "active-call", text: "{}", }) - yield* events.publish(SessionEvent.Tool.Called, { + yield* bus.publish(SessionEvent.Tool.Called, { sessionID, assistantMessageID: activeAssistant, callID: "active-call", input: {}, executed: false, }) - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: SessionMessage.ID.create(), input: { type: "user", data: { text: "Queued input must remain invisible" }, delivery: "queue" }, diff --git a/packages/core/test/session-instructions.test.ts b/packages/core/test/session-instructions.test.ts index 9d40f95f4b5..bee32501ec8 100644 --- a/packages/core/test/session-instructions.test.ts +++ b/packages/core/test/session-instructions.test.ts @@ -3,22 +3,22 @@ import fs from "fs/promises" import path from "path" import { DateTime, Effect, Layer } from "effect" import { Message } from "@opencode-ai/ai" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Image } from "@opencode-ai/core/image" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { ModelV2 } from "@opencode-ai/core/model" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { ProjectV2 } from "@opencode-ai/core/project" -import { ProviderV2 } from "@opencode-ai/core/provider" -import { ReadTool } from "@opencode-ai/core/tool/read" +import { Model } from "@opencode-ai/core/model" +import { Permission } from "@opencode-ai/core/permission" +import { Project } from "@opencode-ai/core/project" +import { Provider } from "@opencode-ai/core/provider" +import { ReadTool } from "@opencode-ai/core/tool/plugin/read" import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionExecution } from "@opencode-ai/core/session/execution" @@ -26,11 +26,10 @@ import { SessionInstructions } from "@opencode-ai/core/session/instructions" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionStore } from "@opencode-ai/core/session/store" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" -import { ToolHooks } from "@opencode-ai/core/tool/hooks" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { PluginHooks } from "@opencode-ai/core/plugin/hooks" +import { Tool } from "@opencode-ai/core/tool" import { tempLocationLayer } from "./fixture/location" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { testEffect } from "./lib/effect" @@ -40,11 +39,11 @@ const readToolNode = makeLocationNode({ name: "test/read-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)), deps: [ - ToolRegistry.toolsNode, + Tool.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, - PermissionV2.node, + Permission.node, SessionInstructions.node, FSUtil.node, Location.node, @@ -52,17 +51,17 @@ const readToolNode = makeLocationNode({ }) const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: () => Effect.void, ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -77,41 +76,39 @@ const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]]) const testLayer = AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - SessionV2.node, + Session.node, Location.node, FSUtil.node, LocationMutation.node, ReadToolFileSystem.node, readToolNode, - ToolRegistry.node, - ToolRegistry.toolsNode, - ToolHooks.node, + Tool.node, + Tool.node, + PluginHooks.node, SessionInstructions.node, Global.node, - ToolOutputStore.node, Image.node, ]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], [Location.node, tempLocationLayer], - [PermissionV2.node, permission], + [Permission.node, permission], [Config.node, config], [Image.node, imageLayer], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ], ) as unknown as Layer.Layer const it = testEffect(testLayer) const identity = { - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_nearby"), } -const readCall = (sessionID: SessionV2.ID, id: string, readPath: string): ToolRegistry.ExecuteInput => ({ +const readCall = (sessionID: Session.ID, id: string, readPath: string): Parameters[0] => ({ sessionID, ...identity, call: { type: "tool-call", id, name: "read", input: { path: readPath } }, @@ -120,7 +117,7 @@ const readCall = (sessionID: SessionV2.ID, id: string, readPath: string): ToolRe const writeAgents = (file: string, content: string) => Effect.promise(() => fs.writeFile(file, content)) const mkdir = (dir: string) => Effect.promise(() => fs.mkdir(dir, { recursive: true })) -const synthetics = (sessionID: SessionV2.ID) => +const synthetics = (sessionID: Session.ID) => Effect.gen(function* () { const store = yield* SessionStore.Service return (yield* store.context(sessionID)).filter((message) => message.type === "synthetic") @@ -128,10 +125,10 @@ const synthetics = (sessionID: SessionV2.ID) => // Seed a prior synthetic message with an instruction dedup ledger, simulating a prior turn // after the Location layer was reopened (in-memory set empty). -const seedSynthetic = (sessionID: SessionV2.ID, paths: string[]) => +const seedSynthetic = (sessionID: Session.ID, paths: string[]) => Effect.gen(function* () { - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.Synthetic, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.Synthetic, { sessionID, text: `Instructions from: ${paths[0]}\nprior`, description: `Loaded ${paths[0]}`, @@ -157,8 +154,8 @@ describe("SessionInstructions", () => { yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "sub", "deep", "file.txt"), "file content")) yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "sub", "other", "file2.txt"), "file content 2")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // A read deep under sub/ discovers deep and sub AGENTS.md, walking up to but @@ -201,8 +198,8 @@ describe("SessionInstructions", () => { yield* writeAgents(subPath, "sub-instructions") yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "sub", "file.txt"), "content")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // Seed the durable history with a prior synthetic that already claims sub's AGENTS.md @@ -230,8 +227,8 @@ describe("SessionInstructions", () => { yield* writeAgents(pkgPath, "pkg-instructions") yield* Effect.promise(() => fs.writeFile(path.resolve(dir, "packages", "foo", "file.txt"), "content")) - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // Listing packages/foo/ discovers its own AGENTS.md, walking up to but excluding @@ -263,8 +260,8 @@ describe("SessionInstructions", () => { yield* writeAgents(rootPath, "root-instructions") yield* writeAgents(subPath, "sub-instructions") - const session = yield* SessionV2.Service - const registry = yield* ToolRegistry.Service + const session = yield* Session.Service + const registry = yield* Tool.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id // The walk starts and stops at the Location root: the root AGENTS.md is searched but @@ -283,7 +280,7 @@ describe("SessionInstructions", () => { yield* mkdir(path.resolve(dir, "sub")) yield* writeAgents(subPath, "sub-instructions") - const session = yield* SessionV2.Service + const session = yield* Session.Service const sessionInstructions = yield* SessionInstructions.Service const sessionID = (yield* session.create({ location: Location.Ref.make({ directory: dir }) })).id @@ -299,7 +296,7 @@ describe("SessionInstructions", () => { test("toLLMMessages does not forward synthetic metadata to the provider", () => { const created = DateTime.makeUnsafe(0) - const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) + const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) const synthetic = SessionMessage.Synthetic.make({ id: SessionMessage.ID.make("msg_synthetic"), type: "synthetic", diff --git a/packages/core/test/session-log.test.ts b/packages/core/test/session-log.test.ts index 870f6acf02a..f223542cafb 100644 --- a/packages/core/test/session-log.test.ts +++ b/packages/core/test/session-log.test.ts @@ -1,15 +1,16 @@ import { describe, expect } from "bun:test" import { Effect, Fiber, Layer, Schema, Stream } from "effect" import { Database } from "@opencode-ai/core/database/database" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { Location } from "@opencode-ai/core/location" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionStore } from "@opencode-ai/core/session/store" @@ -17,35 +18,35 @@ import { SessionTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -describe("SessionV2.log", () => { +describe("Session.log", () => { it.effect("replays public session events and marks synced at the aggregate watermark", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const created = yield* session.create({ location }) yield* session.rename({ sessionID: created.id, title: "session.renamed" }) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id }))) - const watermark = (yield* events.sequences([created.id])).get(created.id) + const watermark = (yield* bus.sequences([created.id])).get(created.id) // Session creation commits a non-public durable event, so the marker's // seq covers more of the aggregate than the public events emitted. @@ -56,7 +57,7 @@ describe("SessionV2.log", () => { it.effect("continues with live public events when following", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const created = yield* session.create({ location }) const fiber = yield* session .log({ sessionID: created.id, follow: true }) @@ -72,52 +73,52 @@ describe("SessionV2.log", () => { it.effect("fails with NotFound for an unknown session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const error = yield* Effect.flip(Stream.runCollect(session.log({ sessionID: SessionV2.ID.create() }))) + const session = yield* Session.Service + const error = yield* Effect.flip(Stream.runCollect(session.log({ sessionID: Session.ID.create() }))) expect(error._tag).toBe("Session.NotFoundError") }), ) it.effect("reads across undecodable gaps in aggregate order and marks the true log position", () => Effect.gen(function* () { - const GapEvent = EventV2.durable({ + const GapEvent = Bus.durable({ type: "test.session.log.gap", durable: { aggregate: "sessionID", version: 1 }, - schema: { sessionID: SessionV2.ID, value: Schema.String }, + schema: { sessionID: Session.ID, value: Schema.String }, }) - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const created = yield* session.create({ location }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("one") }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("one") }) // Not in the durable manifest, so reads must skip it without failing. - yield* events.publish(GapEvent, { sessionID: created.id, value: "filtered" }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("two") }) - yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("three") }) + yield* bus.publish(GapEvent, { sessionID: created.id, value: "filtered" }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("two") }) + yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("three") }) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id, after: 1 }))) expect( - items.map((item): number | string | undefined => (EventV2.isSynced(item) ? item.type : item.durable?.seq)), + items.map((item): number | string | undefined => (Bus.isSynced(item) ? item.type : item.durable?.seq)), ).toEqual([3, 4, "log.synced"]) - expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: EventV2.Seq.make(4) }) + expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: Event.Seq.make(4) }) }), ) it.effect("completes with a bare synced marker for a migrated Session with no event sequence", () => Effect.gen(function* () { const db = (yield* Database.Service).db - const session = yield* SessionV2.Service - const sessionID = SessionV2.ID.make("ses_empty_log") + const session = yield* Session.Service + const sessionID = Session.ID.make("ses_empty_log") yield* db .insert(ProjectTable) - .values({ id: ProjectV2.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) .onConflictDoNothing() .run() yield* db .insert(SessionTable) .values({ id: sessionID, - project_id: ProjectV2.ID.global, + project_id: Project.ID.global, slug: "empty-log", directory: "/project", title: "Empty log", diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 18bdcdbd62e..9891a0b9622 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -2,17 +2,18 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Fiber, Option, Schema, Stream } from "effect" import { asc, eq, sql } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { EventTable } from "@opencode-ai/core/event/sql" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { Money } from "@opencode-ai/schema/money" @@ -31,14 +32,14 @@ import { import { testEffect } from "./lib/effect" import { Snapshot } from "@opencode-ai/core/snapshot" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) -const sessionsLayer = AppNodeBuilder.build(SessionV2.node, [[SessionExecution.node, SessionExecution.noopLayer]]) -const sessionID = SessionV2.ID.make("ses_projector_test") +const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) +const sessionsLayer = AppNodeBuilder.build(Session.node, [[SessionExecution.node, SessionExecution.noopLayer]]) +const sessionID = Session.ID.make("ses_projector_test") const created = DateTime.makeUnsafe(0) -const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } -const previousModel = { ...model, variant: ModelV2.VariantID.make("medium") } +const model = { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") } +const previousModel = { ...model, variant: Model.VariantID.make("medium") } const encodeMessage = Schema.encodeSync(SessionMessage.Info) -const build = AgentV2.defaultID +const build = Agent.defaultID const assistantRow = ( id: SessionMessage.ID, @@ -75,11 +76,11 @@ describe("SessionProjector", () => { version: "test", }) .run() - const events = yield* EventV2.Service + const bus = yield* Bus.Service const inputID = SessionMessage.ID.make("msg_manual_compaction") - yield* SessionPending.admitCompaction(db, events, { id: inputID, sessionID }) + yield* SessionPending.admitCompaction(db, bus, { id: inputID, sessionID }) - yield* events.publish(SessionEvent.Compaction.Failed, { + yield* bus.publish(SessionEvent.Compaction.Failed, { sessionID, reason: "auto", error: { type: "compaction.failed", message: "Auto compaction failed" }, @@ -143,7 +144,7 @@ describe("SessionProjector", () => { yield* SessionMessageUpdater.update( SessionMessageUpdater.memory(state), SessionEvent.Compaction.Delta.make({ - id: EventV2.ID.make("evt_delta"), + id: Event.ID.make("evt_delta"), type: "session.compaction.delta", created, data: { sessionID, text: "summary" }, @@ -213,8 +214,8 @@ describe("SessionProjector", () => { current_values: {}, }) .run() - const events = yield* EventV2.Service - yield* events.publish(SessionEvent.RevertEvent.Staged, { + const bus = yield* Bus.Service + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), files: [] }, }) @@ -223,13 +224,13 @@ describe("SessionProjector", () => { snapshot: "tree", files: [], }) - yield* events.publish(SessionEvent.RevertEvent.Cleared, { sessionID }) + yield* bus.publish(SessionEvent.RevertEvent.Cleared, { sessionID }) expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toBeNull() - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary, files: [] }, }) - yield* events.publish(SessionEvent.RevertEvent.Committed, { + yield* bus.publish(SessionEvent.RevertEvent.Committed, { sessionID, to: boundary, }) @@ -269,36 +270,36 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: SessionMessage.ID.make("msg_first"), input: { type: "user", data: { text: "first" }, delivery: "steer" }, }) - yield* events.publish( + yield* bus.publish( SessionEvent.InputPromoted, { sessionID, inputID: SessionMessage.ID.make("msg_first"), }, - { id: EventV2.ID.make("evt_z") }, + { id: Event.ID.make("evt_z") }, ) - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: SessionMessage.ID.make("msg_second"), input: { type: "user", data: { text: "second" }, delivery: "steer" }, }) - yield* events.publish( + yield* bus.publish( SessionEvent.InputPromoted, { sessionID, inputID: SessionMessage.ID.make("msg_second"), }, - { id: EventV2.ID.make("evt_a") }, + { id: Event.ID.make("evt_a") }, ) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const firstPage = yield* sessions.messages({ sessionID, limit: 1, order: "asc" }) expect(firstPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["first"]) const secondPage = yield* sessions.messages({ @@ -342,16 +343,16 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const id = SessionMessage.ID.make("msg_admitted") - const admitted = yield* SessionPending.admit(db, events, { + const admitted = yield* SessionPending.admit(db, bus, { id, sessionID, input: { type: "user", data: { text: "promote me" }, delivery: "steer" }, }) if (!admitted) return yield* Effect.die("Prompt admission failed") - const event = yield* events.publish(SessionEvent.InputPromoted, { + const event = yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: id, }) @@ -386,22 +387,22 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service - yield* events.publish(SessionEvent.AgentSelected, { + yield* bus.publish(SessionEvent.AgentSelected, { sessionID, agent: build, }) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, model, }) - yield* events.publish(SessionEvent.Synthetic, { + yield* bus.publish(SessionEvent.Synthetic, { sessionID, text: "synthetic context", metadata: { source: "projector-test" }, }) - yield* events.publish(SessionEvent.Shell.Started, { + yield* bus.publish(SessionEvent.Shell.Started, { sessionID, shell: Shell.Info.make({ id: Shell.ID.make("sh_projector"), @@ -414,7 +415,7 @@ describe("SessionProjector", () => { time: { started: 0 }, }), }) - yield* events.publish(SessionEvent.Shell.Ended, { + yield* bus.publish(SessionEvent.Shell.Ended, { sessionID, shell: Shell.Info.make({ id: Shell.ID.make("sh_projector"), @@ -429,12 +430,12 @@ describe("SessionProjector", () => { }), output: { output: "/project", cursor: 8, size: 8, truncated: false }, }) - yield* events.publish(SessionEvent.Compaction.Started, { + yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "recent context", }) - yield* events.publish(SessionEvent.Compaction.Delta, { + yield* bus.publish(SessionEvent.Compaction.Delta, { sessionID, text: "partial", }) @@ -454,7 +455,7 @@ describe("SessionProjector", () => { .all() .pipe(Effect.orDie), ).toEqual([{ data: expect.objectContaining({ status: "running", summary: "", recent: "recent context" }) }]) - yield* events.publish(SessionEvent.Compaction.Ended, { + yield* bus.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", text: "summary", @@ -525,7 +526,7 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const id = SessionMessage.ID.make("msg_creator_collision") const { id: _, type, ...data } = encodeMessage({ id, type: "synthetic", text: "existing", time: { created } }) yield* db @@ -533,7 +534,7 @@ describe("SessionProjector", () => { .values({ id, session_id: sessionID, type, seq: 0, time_created: 0, data }) .run() - const exit = yield* events + const exit = yield* bus .publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: id, @@ -594,11 +595,11 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const first = SessionMessage.ID.make("msg_retry_first") const second = SessionMessage.ID.make("msg_retry_second") - yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: build, model }) - yield* events.publish(SessionEvent.RetryScheduled, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: build, model }) + yield* bus.publish(SessionEvent.RetryScheduled, { sessionID, assistantMessageID: first, attempt: 2, @@ -619,15 +620,15 @@ describe("SessionProjector", () => { retry: { attempt: 2, at: DateTime.makeUnsafe(2_000), error: { type: "provider.transport" } }, }) - yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: build, model }) - yield* events.publish(SessionEvent.RetryScheduled, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: build, model }) + yield* bus.publish(SessionEvent.RetryScheduled, { sessionID, assistantMessageID: second, attempt: 3, at: 6_000, error: { type: "provider.internal", message: "Unavailable" }, }) - yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) + yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) const rows = yield* db .select() @@ -661,7 +662,7 @@ describe("SessionProjector", () => { }) .run() .pipe(Effect.orDie) - const events = yield* EventV2.Service + const bus = yield* Bus.Service const suspended = () => db .select({ timeSuspended: SessionTable.time_suspended }) @@ -670,10 +671,10 @@ describe("SessionProjector", () => { .get() .pipe(Effect.orDie) - yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) + yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) expect((yield* suspended())?.timeSuspended).toBeNull() - yield* events.publish(SessionEvent.Execution.Started, { sessionID }) + yield* bus.publish(SessionEvent.Execution.Started, { sessionID }) expect((yield* suspended())?.timeSuspended).toBeNull() }), ) @@ -707,7 +708,7 @@ describe("SessionProjector", () => { .run() .pipe(Effect.orDie) - const service = yield* EventV2.Service + const service = yield* Bus.Service const usageUpdated = yield* service .subscribe(SessionEvent.UsageUpdated) .pipe(Stream.runHead, Effect.forkScoped({ startImmediately: true })) @@ -787,7 +788,7 @@ describe("SessionProjector", () => { .run() .pipe(Effect.orDie) - const service = yield* EventV2.Service + const service = yield* Bus.Service yield* service.publish(SessionEvent.Text.Started, { sessionID, assistantMessageID: SessionMessage.ID.make("msg_assistant_completed"), diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 8cab48f14a1..9f30efe9c06 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -6,18 +6,18 @@ import path from "path" import { pathToFileURL } from "url" import { eq } from "drizzle-orm" import { Database } from "@opencode-ai/core/database/database" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { SessionEvent } from "@opencode-ai/core/session/event" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" @@ -29,10 +29,10 @@ import type { LocationServices } from "@opencode-ai/core/location-services" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" -const executionCalls: SessionV2.ID[] = [] -const interruptCalls: SessionV2.ID[] = [] -const wakeCalls: SessionV2.ID[] = [] -const activeSessions = new Set() +const executionCalls: Session.ID[] = [] +const interruptCalls: Session.ID[] = [] +const wakeCalls: Session.ID[] = [] +const activeSessions = new Set() const execution = Layer.succeed( SessionExecution.Service, SessionExecution.Service.of({ @@ -63,14 +63,14 @@ const locations = Layer.effect( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ [SessionExecution.node, execution], [LocationServiceMap.node, locations], ], ), ) -const sessionID = SessionV2.ID.make("ses_prompt_test") +const sessionID = Session.ID.make("ses_prompt_test") const messageID = SessionMessage.ID.create() const setup = Effect.gen(function* () { @@ -130,8 +130,8 @@ const assistantRow = (id: SessionMessage.ID, seq: number) => { SessionMessage.Assistant.make({ id, type: "assistant", - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + agent: Agent.ID.make("build"), + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [], time: { created: DateTime.makeUnsafe(0) }, }), @@ -139,18 +139,18 @@ const assistantRow = (id: SessionMessage.ID, seq: number) => { return { id, session_id: sessionID, type, seq, time_created: 0, data } } -describe("SessionV2.prompt", () => { +describe("Session.prompt", () => { it.effect("exposes the execution registry", () => Effect.gen(function* () { activeSessions.add(sessionID) - expect(Array.from(yield* (yield* SessionV2.Service).active)).toEqual([sessionID]) + expect(Array.from(yield* (yield* Session.Service).active)).toEqual([sessionID]) }).pipe(Effect.ensuring(Effect.sync(() => activeSessions.clear()))), ) it.effect("delegates execution continuation through SessionExecution", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 yield* session.resume(sessionID) @@ -162,7 +162,7 @@ describe("SessionV2.prompt", () => { it.effect("delegates process-local interruption through SessionExecution", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service interruptCalls.length = 0 yield* session.interrupt(sessionID) @@ -173,18 +173,18 @@ describe("SessionV2.prompt", () => { it.effect("delegates interruption without requiring a recorded Session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service interruptCalls.length = 0 - yield* session.interrupt(SessionV2.ID.make("ses_missing")) - expect(interruptCalls).toEqual([SessionV2.ID.make("ses_missing")]) + yield* session.interrupt(Session.ID.make("ses_missing")) + expect(interruptCalls).toEqual([Session.ID.make("ses_missing")]) }), ) it.effect("durably admits one user message before transcript promotion", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const message = yield* session.prompt({ sessionID, @@ -207,8 +207,8 @@ describe("SessionV2.prompt", () => { it.effect("commits a staged revert before admitting a new prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const boundary = yield* session.prompt({ @@ -216,10 +216,10 @@ describe("SessionV2.prompt", () => { text: "boundary", resume: false, }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") const stale = SessionMessage.ID.make("msg_stale_assistant") yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie) - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary.id, files: [] }, }) @@ -240,16 +240,16 @@ describe("SessionV2.prompt", () => { it.effect("holds synthetic input behind a staged revert and discards it when committed", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const boundary = yield* session.prompt({ sessionID, text: "boundary", resume: false, }) - yield* SessionPending.promote(db, events, sessionID, "steer") - yield* events.publish(SessionEvent.RevertEvent.Staged, { + yield* SessionPending.promote(db, bus, sessionID, "steer") + yield* bus.publish(SessionEvent.RevertEvent.Staged, { sessionID, revert: { messageID: boundary.id, files: [] }, }) @@ -269,7 +269,7 @@ describe("SessionV2.prompt", () => { it.effect("resolves attachment MIME before admission", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" @@ -298,7 +298,7 @@ describe("SessionV2.prompt", () => { it.effect("materializes selected source file content", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const directory = import.meta.dir const source = path.join(directory, "session-prompt.test.ts") const sourceUri = pathToFileURL(source) @@ -329,7 +329,7 @@ describe("SessionV2.prompt", () => { it.effect("materializes directories as directory attachments", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = pathToFileURL(import.meta.dir).href const message = yield* session.prompt({ @@ -354,7 +354,7 @@ describe("SessionV2.prompt", () => { it.effect("materializes local image content before admission", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const directory = yield* Effect.acquireRelease( Effect.promise(() => mkdtemp(path.join(tmpdir(), "opencode-session-prompt-"))), (directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })), @@ -389,7 +389,7 @@ describe("SessionV2.prompt", () => { it.effect("sniffs data URL content instead of trusting its declared MIME", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = `data:video/mp2t;base64,${Buffer.from("export const value = 1\n").toString("base64")}` const message = yield* session.prompt({ @@ -413,7 +413,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects malformed base64 data URLs", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const uri = "data:image/png;base64,not-base64" const error = yield* session @@ -436,19 +436,19 @@ describe("SessionV2.prompt", () => { it.effect("streams durable Session events after an aggregate sequence", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service - const publicEvents = (input: { sessionID: SessionV2.ID; after?: number }) => + const publicEvents = (input: { sessionID: Session.ID; after?: number }) => session .log({ ...input, follow: true }) - .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !EventV2.isSynced(item))) + .pipe(Stream.filter((item): item is SessionEvent.DurableEvent => !Bus.isSynced(item))) const fiber = yield* publicEvents({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow yield* session.prompt({ sessionID, text: "First", resume: false }) yield* session.prompt({ sessionID, text: "Second", resume: false }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") const streamed = Array.from(yield* Fiber.join(fiber)) expect(streamed.map((event): [number | undefined, string] => [event.durable?.seq, event.type])).toEqual([ @@ -468,7 +468,7 @@ describe("SessionV2.prompt", () => { it.effect("resumes through a recorded message without appending another prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const message = yield* session.prompt({ sessionID, text: "Fix the failing tests", @@ -489,7 +489,7 @@ describe("SessionV2.prompt", () => { it.effect("records distinct messages when the ID is omitted", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, text: "Fix the failing tests", resume: false } const first = yield* session.prompt(input) @@ -504,7 +504,7 @@ describe("SessionV2.prompt", () => { it.effect("returns the original recorded message when the ID is retried", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, id: messageID, @@ -524,7 +524,7 @@ describe("SessionV2.prompt", () => { it.effect("wakes execution when an exact prompt retry recovers a committed message", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, id: messageID, @@ -544,7 +544,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects reuse of one ID with a different prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ sessionID, @@ -569,7 +569,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects reuse of one ID with a different delivery mode", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ id: messageID, @@ -594,7 +594,7 @@ describe("SessionV2.prompt", () => { it.effect("returns one recorded message to concurrent exact retries", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { sessionID, id: messageID, @@ -607,7 +607,7 @@ describe("SessionV2.prompt", () => { expect(messages[1]).toEqual(messages[0]) expect(yield* session.messages({ sessionID })).toEqual([]) expect(yield* admittedCount).toBe(1) - expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) + expect(yield* eventCount(Bus.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) }), ) @@ -615,8 +615,8 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const { db } = yield* Database.Service - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service yield* session.prompt({ id: messageID, sessionID, @@ -625,11 +625,11 @@ describe("SessionV2.prompt", () => { }) yield* Effect.all( - [SessionPending.promote(db, events, sessionID, "steer"), SessionPending.promote(db, events, sessionID, "steer")], + [SessionPending.promote(db, bus, sessionID, "steer"), SessionPending.promote(db, bus, sessionID, "steer")], { concurrency: "unbounded" }, ) - expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1) + expect(yield* eventCount(Bus.versionedType(SessionEvent.InputPromoted.type, 1))).toBe(1) expect(yield* admitted(messageID)).toBeUndefined() expect(yield* session.messages({ sessionID })).toMatchObject([ { id: messageID, type: "user", text: "Promote once" }, @@ -641,8 +641,8 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const { db } = yield* Database.Service - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service wakeCalls.length = 0 yield* session.prompt({ id: messageID, @@ -659,7 +659,7 @@ describe("SessionV2.prompt", () => { .all() .pipe(Effect.orDie) - yield* events.remove(sessionID) + yield* bus.remove(sessionID) yield* db .delete(SessionPendingTable) .where(eq(SessionPendingTable.session_id, sessionID)) @@ -670,7 +670,7 @@ describe("SessionV2.prompt", () => { .where(eq(SessionMessageTable.session_id, sessionID)) .run() .pipe(Effect.orDie) - yield* events.replayAll( + yield* bus.replayAll( recorded.map((event) => ({ id: event.id, created: DateTime.makeUnsafe(event.created), @@ -700,8 +700,8 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const { db } = yield* Database.Service - const session = yield* SessionV2.Service - const other = SessionV2.ID.make("ses_prompt_other") + const session = yield* Session.Service + const other = Session.ID.make("ses_prompt_other") yield* db .insert(SessionTable) .values({ @@ -727,7 +727,7 @@ describe("SessionV2.prompt", () => { it.effect("rejects a prompt ID already used by visible Session history", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const { id: _, @@ -762,7 +762,7 @@ describe("SessionV2.prompt", () => { it.effect("starts execution by default after recording the prompt", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 @@ -776,7 +776,7 @@ describe("SessionV2.prompt", () => { it.effect("starts execution when resume is explicitly true", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 @@ -794,7 +794,7 @@ describe("SessionV2.prompt", () => { it.effect("only records the prompt when resume is false", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service executionCalls.length = 0 wakeCalls.length = 0 @@ -808,7 +808,7 @@ describe("SessionV2.prompt", () => { it.effect("treats prompt metadata as durable retry identity", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const input = { id: messageID, sessionID, @@ -830,8 +830,8 @@ describe("SessionV2.prompt", () => { it.effect("durably admits synthetic input before transcript promotion", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const input = yield* session.synthetic({ @@ -855,7 +855,7 @@ describe("SessionV2.prompt", () => { }, }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") expect(yield* session.messages({ sessionID })).toMatchObject([ { @@ -872,15 +872,15 @@ describe("SessionV2.prompt", () => { it.effect("reconciles exact synthetic retries and rejects conflicting reuse", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const database = yield* Database.Service const input = { id: messageID, sessionID, text: "Completed", resume: false } const entries = yield* Effect.all([session.synthetic(input), session.synthetic(input)], { concurrency: "unbounded", }) - yield* SessionPending.promote(database.db, events, sessionID, "steer") + yield* SessionPending.promote(database.db, bus, sessionID, "steer") const promotedRetry = yield* session.synthetic(input) const failure = yield* session.synthetic({ ...input, text: "Different completion" }).pipe(Effect.flip) @@ -888,15 +888,15 @@ describe("SessionV2.prompt", () => { expect(promotedRetry).toMatchObject({ id: messageID, type: "synthetic", data: { text: "Completed" } }) expect(failure).toMatchObject({ _tag: "Session.SyntheticConflictError", sessionID, inputID: messageID }) expect(yield* admittedCount).toBe(0) - expect(yield* eventCount(EventV2.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) + expect(yield* eventCount(Bus.versionedType(SessionEvent.InputAdmitted.type, 1))).toBe(1) }), ) it.effect("keeps queued input pending until the idle boundary", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const input = yield* session.synthetic({ @@ -909,11 +909,11 @@ describe("SessionV2.prompt", () => { expect(input.delivery).toBe("queue") expect(yield* SessionPending.has(db, sessionID, "input")).toBe(true) expect( - yield* SessionPending.promote(db, events, sessionID, "steer"), + yield* SessionPending.promote(db, bus, sessionID, "steer"), ).toBe(0) expect(yield* session.messages({ sessionID })).toEqual([]) expect( - yield* SessionPending.promote(db, events, sessionID, "input"), + yield* SessionPending.promote(db, bus, sessionID, "input"), ).toBe(1) expect(yield* SessionPending.has(db, sessionID, "input")).toBe(false) expect(yield* session.messages({ sessionID })).toMatchObject([ @@ -925,8 +925,8 @@ describe("SessionV2.prompt", () => { it.effect("promotes prompt and synthetic steers in admission order", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service yield* session.prompt({ @@ -941,7 +941,7 @@ describe("SessionV2.prompt", () => { resume: false, }) - yield* SessionPending.promote(db, events, sessionID, "steer") + yield* SessionPending.promote(db, bus, sessionID, "steer") expect( (yield* session.messages({ sessionID, order: "asc" })).map((message) => @@ -952,11 +952,11 @@ describe("SessionV2.prompt", () => { ) }) -describe("SessionV2.pending", () => { +describe("Session.pending", () => { it.effect("fails for an unknown session", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - expect(yield* session.pending(SessionV2.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({ + const session = yield* Session.Service + expect(yield* session.pending(Session.ID.make("ses_missing")).pipe(Effect.flip)).toMatchObject({ _tag: "Session.NotFoundError", }) }), @@ -965,8 +965,8 @@ describe("SessionV2.pending", () => { it.effect("lists admitted work in admission order until promotion", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service - const events = yield* EventV2.Service + const session = yield* Session.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service const first = yield* session.prompt({ sessionID, text: "First steer", resume: false }) @@ -985,12 +985,12 @@ describe("SessionV2.pending", () => { ]) expect( - yield* SessionPending.promote(db, events, sessionID, "input"), + yield* SessionPending.promote(db, bus, sessionID, "input"), ).toBe(2) expect(yield* session.pending(sessionID)).toMatchObject([{ id: queued.id, type: "synthetic" }]) expect( - yield* SessionPending.promote(db, events, sessionID, "input"), + yield* SessionPending.promote(db, bus, sessionID, "input"), ).toBe(1) expect(yield* session.pending(sessionID)).toEqual([]) }), @@ -999,7 +999,7 @@ describe("SessionV2.pending", () => { it.effect("lists an unhandled compaction barrier until it settles", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const { db } = yield* Database.Service const barrier = yield* session.compact({ sessionID }) diff --git a/packages/core/test/session-remove.test.ts b/packages/core/test/session-remove.test.ts index 8d61af76c85..65728a7b7ce 100644 --- a/packages/core/test/session-remove.test.ts +++ b/packages/core/test/session-remove.test.ts @@ -3,40 +3,40 @@ import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionStore } from "@opencode-ai/core/session/store" import { testEffect } from "./lib/effect" const projects = Layer.succeed( - ProjectV2.Service, - ProjectV2.Service.of({ + Project.Service, + Project.Service.of({ list: () => Effect.succeed([]), - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), directories: () => Effect.succeed([]), commit: () => Effect.void, }), ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -describe("SessionV2.remove", () => { +describe("Session.remove", () => { it.effect("removes a session and its children", () => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location }) const child = yield* session.create({ parentID: parent.id }) @@ -50,8 +50,8 @@ describe("SessionV2.remove", () => { it.effect("fails when the session does not exist", () => Effect.gen(function* () { - const session = yield* SessionV2.Service - const sessionID = SessionV2.ID.make("ses_missing") + const session = yield* Session.Service + const sessionID = Session.ID.make("ses_missing") expect(yield* Effect.result(session.remove(sessionID))).toMatchObject({ _tag: "Failure", diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index de33bcd69dc..5e5b0e5ddbc 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -1,18 +1,18 @@ import { describe, expect, test } from "bun:test" import { Message } from "@opencode-ai/ai" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { SessionMessage } from "@opencode-ai/core/session/message" import { AgentAttachment, Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Shell } from "@opencode-ai/schema/shell" import { DateTime } from "effect" const created = DateTime.makeUnsafe(0) const id = (value: string) => SessionMessage.ID.make(`msg_${value}`) -const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }) -const build = AgentV2.defaultID +const model = Model.Ref.make({ id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }) +const build = Agent.defaultID describe("toLLMMessages", () => { test("omits empty assistant turns", () => { @@ -21,7 +21,7 @@ describe("toLLMMessages", () => { id: id(value), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content, time: { created, completed: created }, }) @@ -45,7 +45,7 @@ describe("toLLMMessages", () => { expect(messages.map((message) => message.id)).toEqual([id("text"), id("reasoning")]) }) - test("maps every top-level V2 Session message type", () => { + test("maps every top-level Session message type", () => { const file = FileAttachment.make({ data: Base64.make("aGVsbG8="), mime: "image/png", @@ -63,7 +63,7 @@ describe("toLLMMessages", () => { SessionMessage.ModelSelected.make({ id: id("model"), type: "model-switched", - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, time: { created }, }), SessionMessage.System.make({ @@ -345,7 +345,7 @@ Recent work id: id("assistant"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantText.make({ type: "text", text: "Checking" }), SessionMessage.AssistantReasoning.make({ @@ -495,7 +495,7 @@ Recent work id: id("assistant-openai-reasoning"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -519,7 +519,7 @@ Recent work }) test("replays flat state under an OpenCode hosted model's route key", () => { - const opencode = ModelV2.Ref.make({ id: ModelV2.ID.make("claude-fable-5"), providerID: ProviderV2.ID.opencode }) + const opencode = Model.Ref.make({ id: Model.ID.make("claude-fable-5"), providerID: Provider.ID.opencode }) const messages = toLLMMessages( [ SessionMessage.Assistant.make({ @@ -553,7 +553,7 @@ Recent work id: id("assistant-failed"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -651,7 +651,7 @@ Recent work id: id("assistant-old-model"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("old-model"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -745,7 +745,7 @@ Recent work id: id("assistant-alias"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("fast"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantReasoning.make({ type: "reasoning", @@ -756,7 +756,7 @@ Recent work time: { created, completed: created }, }), ], - ModelV2.Ref.make({ id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") }), + Model.Ref.make({ id: Model.ID.make("fast"), providerID: Provider.ID.make("provider") }), ) expect(messages[0]?.content).toEqual([ @@ -775,7 +775,7 @@ Recent work id: id("assistant-phase"), type: "assistant", agent: build, - model: { id: ModelV2.ID.make("old"), providerID: ProviderV2.ID.make("provider") }, + model: { id: Model.ID.make("old"), providerID: Provider.ID.make("provider") }, content: [ SessionMessage.AssistantText.make({ type: "text", @@ -787,7 +787,7 @@ Recent work time: { created, completed: created }, }), ], - ModelV2.Ref.make({ id: ModelV2.ID.make("new"), providerID: ProviderV2.ID.make("provider") }), + Model.Ref.make({ id: Model.ID.make("new"), providerID: Provider.ID.make("provider") }), ) expect(messages[0]?.content).toEqual([ diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index 70bc1532821..a430cb212a6 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -6,16 +6,16 @@ import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventTable } from "@opencode-ai/core/event/sql" import { Job } from "@opencode-ai/core/job" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Permission } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Snapshot } from "@opencode-ai/core/snapshot" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionTitle } from "@opencode-ai/core/session/title" @@ -25,8 +25,7 @@ import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator import { SessionRunner } from "@opencode-ai/core/session/runner" import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Tool } from "@opencode-ai/core/tool" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { Location } from "@opencode-ai/core/location" @@ -56,8 +55,8 @@ const cassette = HttpRecorder.layerFetch(cassetteName, { directory: cassetteDire const executor = RequestExecutor.layer.pipe(Layer.provide(cassette)) const client = LLMClient.layer.pipe(Layer.provide(executor)) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: () => Effect.die("unused"), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -116,15 +115,14 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [ReferenceInstructions.node, referenceInstructions], [McpInstructions.node, mcpInstructions], [Config.node, config], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], [PluginSupervisor.node, pluginSupervisor], ]) const execution = Layer.effect( SessionExecution.Service, Effect.gen(function* () { const sessionRunner = yield* SessionRunner.Service - const coordinator = yield* SessionRunCoordinator.make({ + const coordinator = yield* SessionRunCoordinator.make({ drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }), }) return SessionExecution.Service.of({ @@ -140,13 +138,13 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, Catalog.node, PluginHooks.node, - ToolRegistry.node, + Tool.node, SessionRunnerModel.node, InstructionBuiltIns.node, InstructionDiscovery.node, @@ -155,13 +153,12 @@ const it = testEffect( Config.node, Snapshot.node, SessionRunnerLLM.node, - SessionV2.node, + Session.node, ]), [ [LayerNodePlatform.llmClient, client], - [PermissionV2.node, permission], + [Permission.node, permission], [Catalog.node, promptCatalog], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [SessionRunnerModel.node, models], [InstructionBuiltIns.node, systemContext], [InstructionDiscovery.node, instructionContext], @@ -175,16 +172,16 @@ const it = testEffect( ], ), ) -const sessionID = SessionV2.ID.make("ses_runner_recorded") +const sessionID = Session.ID.make("ses_runner_recorded") describe("SessionRunnerLLM recorded", () => { - it.effect("executes one recorded V2 prompt through the recorded HTTP transport", () => + it.effect("executes one recorded prompt through the recorded HTTP transport", () => Effect.gen(function* () { - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service const hooks = yield* PluginHooks.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" agent.permissions.push({ action: "execute", resource: "*", effect: "deny" }) }), @@ -215,7 +212,7 @@ describe("SessionRunnerLLM recorded", () => { .onConflictDoNothing() .run() .pipe(Effect.orDie) - const session = yield* SessionV2.Service + const session = yield* Session.Service const prompt = yield* session.prompt({ sessionID, text: "Say hello in one short sentence.", diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 23b607a70e0..82c6a869159 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -2,29 +2,30 @@ import { expect, test } from "bun:test" import { Cause, Effect, Exit, Schema } from "effect" import { LLMEvent } from "@opencode-ai/ai" import { Money } from "@opencode-ai/schema/money" -import { EventV2 } from "@opencode-ai/core/event" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" +import { Agent } from "@opencode-ai/core/agent" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" -import { SessionV2 } from "@opencode-ai/core/session" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Session } from "@opencode-ai/core/session" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { RelativePath } from "@opencode-ai/core/schema" import { Snapshot } from "@opencode-ai/core/snapshot" import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publish-llm-event" -const sessionID = SessionV2.ID.make("ses_tool_event_test") +const sessionID = Session.ID.make("ses_tool_event_test") const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" const capture = (providerMetadataKey = "anthropic", options?: { readonly interruptProgress?: boolean }) => { const published: Array<{ readonly type: string; readonly data: unknown }> = [] - const events: Pick = { + const bus: Pick = { publish: (definition, data) => { const publish = Effect.sync(() => { - const event = { id: EventV2.ID.create(), type: definition.type, data } as EventV2.Payload + const event = { id: Event.ID.create(), type: definition.type, data } as Event.Payload published.push({ type: definition.durable - ? EventV2.versionedType(definition.type, definition.durable.version) + ? Bus.versionedType(definition.type, definition.durable.version) : definition.type, data, }) @@ -37,12 +38,12 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru } return { published, - publisher: createLLMEventPublisher(events, { + publisher: createLLMEventPublisher(bus, { sessionID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model: { - id: ModelV2.ID.make("model"), - providerID: ProviderV2.ID.opencode, + id: Model.ID.make("model"), + providerID: Provider.ID.opencode, }, providerMetadataKey, assistantMessageID: SessionMessage.ID.create(), @@ -68,7 +69,6 @@ test("local tool success serializes media base64 once through canonical content" await Effect.runPromise(publisher.publish(call)) await Effect.runPromise( publisher.toolExecution(call.id, call.name, { - status: "completed", output: { type: "media", mime: "image/png" }, content: [ { type: "text", text: "Image read successfully" }, @@ -227,10 +227,7 @@ test("binary failure emits no success event", async () => { const { published, publisher } = capture() await Effect.runPromise(publisher.publish(call)) await Effect.runPromise( - publisher.toolExecution(call.id, call.name, { - status: "error", - error: { type: "tool.execution", message: "Cannot read binary file" }, - }), + publisher.failTool(call.id, { type: "tool.execution", message: "Cannot read binary file" }), ) expect(published.some((event) => event.type === "session.tool.success.2")).toBe(false) expect(published.some((event) => event.type === "session.tool.failed.2")).toBe(true) diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index b924f15fb7b..37a32fa4c7a 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -1,35 +1,16 @@ import { describe, expect } from "bun:test" -import { Tool } from "@opencode-ai/core/tool/tool" -import { AgentV2 } from "@opencode-ai/core/agent" -import type { PermissionV2 } from "@opencode-ai/core/permission" +import { Agent } from "@opencode-ai/core/agent" +import type { Permission } from "@opencode-ai/core/permission" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { Image } from "@opencode-ai/core/image" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { Tool } from "@opencode-ai/core/tool" +import type { Info } from "@opencode-ai/schema/tool" import { executeTool, toolDefinitions } from "./lib/tool" import { Cause, Deferred, Effect, Exit, Fiber, Layer, Option, Schema, SchemaGetter, SchemaIssue, Scope } from "effect" import { testEffect } from "./lib/effect" -const bounds: ToolOutputStore.BoundInput[] = [] -const retentionFailure = new ToolOutputStore.StorageError({ operation: "write", cause: new Error("disk full") }) -const outputStore = Layer.mock(ToolOutputStore.Service, { - limits: () => Effect.succeed({ maxLines: ToolOutputStore.MAX_LINES, maxBytes: ToolOutputStore.MAX_BYTES }), - bound: (input) => { - if (input.callID === "call-retention-failure") return Effect.fail(retentionFailure) - return Effect.sync(() => bounds.push(input)).pipe( - Effect.as( - input.callID === "call-bounded" - ? { - content: [{ type: "text" as const, text: "bounded reference" }], - outputPaths: ["/managed/generic"], - } - : { content: input.content, outputPaths: [] }, - ), - ) - }, -}) const imageStore = Layer.mock(Image.Service, { normalize: (resource, content) => { if (resource === "corrupt.png") return Effect.fail(new Image.DecodeError({ resource })) @@ -48,43 +29,53 @@ const imageStore = Layer.mock(Image.Service, { return Effect.succeed({ ...content, content: "bm9ybWFsaXplZA==", mime: "image/jpeg" }) }, }) -const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [ - [ToolOutputStore.node, outputStore], - [Image.node, imageStore], -]) +const registryLayer = AppNodeBuilder.build(Tool.node, [[Image.node, imageStore]]) const it = testEffect(registryLayer) const identity = { - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_registry"), } -const sessionID = SessionV2.ID.make("ses_registry") -const call = (name: string, id = `call-${name}`): ToolRegistry.ExecuteInput => ({ +const sessionID = Session.ID.make("ses_registry") +const call = (name: string, id = `call-${name}`): Parameters[0] => ({ sessionID, ...identity, call: { type: "tool-call", id, name, input: { text: name } }, }) -const make = () => - Tool.make({ +const make = (): Info => + ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: ({ text }) => Effect.succeed({ output: { text }, content: text }), }) -const constant = (text: string) => - Tool.make({ +const constant = (text: string): Info => + ({ + name: "constant", description: "Return text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: () => Effect.succeed({ output: { text }, content: text }), }) -describe("ToolRegistry", () => { +const transform = ( + service: Tool.Interface, + tools: Readonly>, + options?: Tool.Options, +) => + service.transform((draft) => + Object.entries(tools).forEach(([name, tool]) => + draft.add({ ...tool, name, options: { ...tool.options, ...options } }), + ), + ) + +describe("Tool", () => { it.effect("rejects invalid dotted namespaces", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - const error = yield* service.register({ echo: make() }, { namespace: "slack..admin" }).pipe(Effect.flip) + const service = yield* Tool.Service + const error = yield* transform(service, { echo: make() }, { namespace: "slack..admin" }).pipe(Effect.flip) expect(error).toBeInstanceOf(Tool.RegistrationError) expect(error.message).toBe('Invalid tool namespace: "slack..admin"') @@ -94,12 +85,11 @@ describe("ToolRegistry", () => { it.effect("rejects invalid and colliding normalized names", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - const invalid = yield* service.register({ "123": make() }, { codemode: false }).pipe(Effect.flip) + const service = yield* Tool.Service + const invalid = yield* transform(service, { "123": make() }, { codemode: false }).pipe(Effect.flip) expect(invalid.message).toBe("Invalid tool name: 123") - const collision = yield* service - .register({ "echo.tool": make(), echo_tool: make() }, { codemode: false }) + const collision = yield* transform(service, { "echo.tool": make(), echo_tool: make() }, { codemode: false }) .pipe(Effect.flip) expect(collision.message).toBe("Duplicate normalized tool name: echo_tool") expect((yield* service.snapshot()).definitions.map((tool) => tool.name)).toEqual(["execute"]) @@ -108,12 +98,12 @@ describe("ToolRegistry", () => { it.effect("validates a registration batch before installing any tools", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const error = yield* service - .registerBatch([ - { tools: { first: make() }, options: { codemode: false } }, - { tools: { second: make() }, options: { namespace: "invalid..namespace", codemode: false } }, - ]) + .transform((draft) => { + draft.add({ ...make(), name: "first", options: { codemode: false } }) + draft.add({ ...make(), name: "second", options: { namespace: "invalid..namespace", codemode: false } }) + }) .pipe(Effect.flip) expect(error).toBeInstanceOf(Tool.RegistrationError) @@ -123,24 +113,26 @@ describe("ToolRegistry", () => { it.effect("canonicalizes effective definitions and keeps Code Mode last", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const tool = make() - const capture = (registrations: Parameters[0]) => + const capture = (tools: ReadonlyArray) => Effect.scoped( Effect.gen(function* () { - yield* service.registerBatch(registrations) + yield* service.transform((draft) => tools.forEach(draft.add)) return (yield* service.snapshot()).definitions }), ) const first = yield* capture([ - { tools: { zeta: tool, alpha: tool }, options: { codemode: false } }, - { tools: { beta: tool }, options: { namespace: "alpha", codemode: false } }, - { tools: { echo: tool } }, + { ...tool, name: "zeta", options: { codemode: false } }, + { ...tool, name: "alpha", options: { codemode: false } }, + { ...tool, name: "beta", options: { namespace: "alpha", codemode: false } }, + { ...tool, name: "echo" }, ]) const second = yield* capture([ - { tools: { echo: tool } }, - { tools: { beta: tool }, options: { namespace: "alpha", codemode: false } }, - { tools: { alpha: tool, zeta: tool }, options: { codemode: false } }, + { ...tool, name: "echo" }, + { ...tool, name: "beta", options: { namespace: "alpha", codemode: false } }, + { ...tool, name: "alpha", options: { codemode: false } }, + { ...tool, name: "zeta", options: { codemode: false } }, ]) expect(first).toEqual(second) @@ -150,7 +142,7 @@ describe("ToolRegistry", () => { it.effect("keeps execute available without Code Mode tools unless explicitly denied", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const available = yield* service.snapshot() expect(available.definitions.map((tool) => tool.name)).toEqual(["execute"]) @@ -164,10 +156,10 @@ describe("ToolRegistry", () => { it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ question: make(), bash: make() }, { codemode: false }) - yield* service.register({ edit: make(), write: make() }, { codemode: false, permission: "edit" }) - const names = (permissions: PermissionV2.Ruleset) => + const service = yield* Tool.Service + yield* transform(service, { question: make(), bash: make() }, { codemode: false }) + yield* transform(service, { edit: make(), write: make() }, { codemode: false, permission: "edit" }) + const names = (permissions: Permission.Ruleset) => toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name))) expect(yield* names([{ action: "question", resource: "*", effect: "deny" }])).toEqual([ @@ -198,10 +190,10 @@ describe("ToolRegistry", () => { it.effect("keeps permission options isolated between registrations", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const shared = make() - yield* service.register({ first: shared }, { codemode: false }) - yield* service.register({ second: shared }, { codemode: false, permission: "edit" }) + yield* transform(service, { first: shared }, { codemode: false }) + yield* transform(service, { second: shared }, { codemode: false, permission: "edit" }) expect( (yield* toolDefinitions(service, [{ action: "edit", resource: "*", effect: "deny" }])).map((tool) => tool.name), @@ -211,9 +203,9 @@ describe("ToolRegistry", () => { it.effect("removes a scoped registration", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const scope = yield* Scope.make() - yield* service.register({ echo: make() }, { codemode: false }).pipe(Scope.provide(scope)) + yield* transform(service, { echo: make() }, { codemode: false }).pipe(Scope.provide(scope)) expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo", "execute"]) yield* Scope.close(scope, Exit.void) expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["execute"]) @@ -222,11 +214,10 @@ describe("ToolRegistry", () => { it.effect("preserves an interrupted registration until its scope closes", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const scope = yield* Scope.make() const registered = yield* Deferred.make() - const fiber = yield* service - .register({ echo: make() }, { codemode: false }) + const fiber = yield* transform(service, { echo: make() }, { codemode: false }) .pipe( Effect.andThen(Deferred.succeed(registered, undefined)), Effect.andThen(Effect.never), @@ -244,14 +235,15 @@ describe("ToolRegistry", () => { it.effect("returns model errors without swallowing interruption or defects", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register( + const service = yield* Tool.Service + yield* transform(service, { - failed: Tool.make({ + failed: ({ + name: "failed", description: "Failed", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), - execute: () => Effect.fail(new Tool.Failure({ message: "Denied" })), + execute: () => Effect.fail(new Tool.Error({ message: "Denied" })), }), }, { codemode: false }, @@ -269,11 +261,12 @@ describe("ToolRegistry", () => { ...identity, call: { type: "tool-call", id: "missing", name: "missing", input: {} }, }), - ).toEqual({ status: "error", error: { type: "tool.unknown", message: "Unknown tool: missing" } }) + ).toEqual({ status: "error", error: { type: "tool.execution", message: "Unknown tool: missing" } }) - yield* service.register( + yield* transform(service, { - defect: Tool.make({ + defect: ({ + name: "defect", description: "Defect", input: Schema.Struct({}), output: Schema.Struct({}), @@ -297,22 +290,9 @@ describe("ToolRegistry", () => { }), ) - it.effect("propagates retention failures through execution", () => - Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ echo: make() }, { codemode: false }) - const toolSet = yield* service.snapshot() - const exit = yield* toolSet.execute(call("echo", "call-retention-failure")).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBe(retentionFailure) - expect(retentionFailure.message).toBe("Failed to write tool output: disk full") - }), - ) - it.effect("exposes execution only through a snapshot", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service expect("definitions" in service).toBe(false) expect("execute" in service).toBe(false) expect("settle" in service).toBe(false) @@ -322,11 +302,12 @@ describe("ToolRegistry", () => { it.effect("passes complete call identity to tool execution", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const contexts: Tool.Context[] = [] - yield* service.register( + yield* transform(service, { - context: Tool.make({ + context: ({ + name: "context", description: "Context", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), @@ -341,37 +322,19 @@ describe("ToolRegistry", () => { ...identity, call: { type: "tool-call", id: "call-context", name: "context", input: {} }, }) - expect(contexts).toEqual([{ sessionID, ...identity, callID: "call-context", progress: expect.any(Function) }]) - }), - ) - - it.effect("encodes output and applies generic execution bounding", () => - Effect.gen(function* () { - bounds.length = 0 - const service = yield* ToolRegistry.Service - yield* service.register({ bounded: make() }, { codemode: false }) - expect( - yield* executeTool(service, { - sessionID, - ...identity, - call: { type: "tool-call", id: "call-bounded", name: "bounded", input: { text: "complete" } }, - }), - ).toEqual({ - status: "completed", - output: { text: "complete" }, - content: [{ type: "text", text: "bounded reference" }], - outputPaths: ["/managed/generic"], - }) - expect(bounds).toHaveLength(1) + expect(contexts).toEqual([ + { sessionID, ...identity, callID: Tool.CallID.make("call-context"), progress: expect.any(Function) }, + ]) }), ) it.effect("normalizes image tool output at execution and drops unresizable images", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register( + const service = yield* Tool.Service + yield* transform(service, { - snapshot: Tool.make({ + snapshot: ({ + name: "snapshot", description: "Return images", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -379,9 +342,14 @@ describe("ToolRegistry", () => { Effect.succeed({ output: { text }, content: [ - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "frame.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "too-large.png" }, - { type: "file", data: "aW1hZ2U=", mime: "image/png", name: "corrupt.png" }, + { type: "file", uri: "data:image/png;base64,aW1hZ2U=", mime: "image/png", name: "frame.png" }, + { + type: "file", + uri: "data:image/png;base64,aW1hZ2U=", + mime: "image/png", + name: "too-large.png", + }, + { type: "file", uri: "data:image/png;base64,aW1hZ2U=", mime: "image/png", name: "corrupt.png" }, { type: "text", text }, ], }), @@ -402,10 +370,11 @@ describe("ToolRegistry", () => { it.effect("publishes progress metadata unchanged", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register( + const service = yield* Tool.Service + yield* transform(service, { - progressive: Tool.make({ + progressive: ({ + name: "progressive", description: "Emit image progress", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -416,7 +385,7 @@ describe("ToolRegistry", () => { { codemode: false }, ) - const updates: ToolRegistry.Progress[] = [] + const updates: Tool.Metadata[] = [] yield* executeTool(service, { ...call("progressive"), progress: (update) => @@ -430,7 +399,7 @@ describe("ToolRegistry", () => { it.effect("enforces transformed codecs at execution and projection boundaries", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const executed: string[] = [] const Transformed = Schema.Boolean.pipe( Schema.decodeTo(Schema.String, { @@ -438,9 +407,10 @@ describe("ToolRegistry", () => { encode: SchemaGetter.transform((value) => value === "yes"), }), ) - yield* service.register( + yield* transform(service, { - transformed: Tool.make({ + transformed: ({ + name: "transformed", description: "Transform values", input: Schema.Struct({ value: Transformed }), output: Schema.Struct({ value: Transformed }), @@ -476,9 +446,10 @@ describe("ToolRegistry", () => { }) expect(executed).toEqual(["yes"]) - yield* service.register( + yield* transform(service, { - invalid_output: Tool.make({ + invalid_output: ({ + name: "invalid_output", description: "Return invalid output", input: Schema.Struct({}), output: Schema.Struct({ @@ -513,12 +484,12 @@ describe("ToolRegistry", () => { it.effect("executes the tool advertised in a model request", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const scope = yield* Scope.make() - yield* service.register({ echo: constant("advertised") }, { codemode: false }).pipe(Scope.provide(scope)) + yield* transform(service, { echo: constant("advertised") }, { codemode: false }).pipe(Scope.provide(scope)) const request = yield* service.snapshot() yield* Scope.close(scope, Exit.void) - yield* service.register({ echo: constant("replacement") }, { codemode: false }) + yield* transform(service, { echo: constant("replacement") }, { codemode: false }) expect((yield* request.execute(call("echo"))).content).toEqual([{ type: "text", text: "advertised" }]) expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "replacement" }]) @@ -527,10 +498,10 @@ describe("ToolRegistry", () => { it.effect("reveals the previous registration after an overlay closes", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ echo: constant("base") }, { codemode: false }) + const service = yield* Tool.Service + yield* transform(service, { echo: constant("base") }, { codemode: false }) const overlay = yield* Scope.make() - yield* service.register({ echo: constant("overlay") }, { codemode: false }).pipe(Scope.provide(overlay)) + yield* transform(service, { echo: constant("overlay") }, { codemode: false }).pipe(Scope.provide(overlay)) expect((yield* executeTool(service, call("echo"))).content).toEqual([{ type: "text", text: "overlay" }]) yield* Scope.close(overlay, Exit.void) @@ -540,12 +511,12 @@ describe("ToolRegistry", () => { it.effect("executes and reports progress for codemode tools advertised in a model request", () => Effect.gen(function* () { - const service = yield* ToolRegistry.Service + const service = yield* Tool.Service const executed: string[] = [] const scope = yield* Scope.make() - yield* service - .register({ - echo: Tool.make({ + yield* transform(service, { + echo: ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -563,8 +534,9 @@ describe("ToolRegistry", () => { expect(execute?.description).toContain("confined Code Mode runtime") expect(execute?.description).not.toContain("Echo text") yield* Scope.close(scope, Exit.void) - yield* service.register({ - echo: Tool.make({ + yield* transform(service, { + echo: ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -572,7 +544,7 @@ describe("ToolRegistry", () => { }), }) - const progress: ToolRegistry.Progress[] = [] + const progress: Tool.Metadata[] = [] const execution = yield* toolSet.execute({ ...call("execute"), call: { @@ -584,10 +556,11 @@ describe("ToolRegistry", () => { progress: (update) => Effect.sync(() => progress.push(update)), }) - expect(execution).toMatchObject({ status: "completed", content: [{ type: "text" }] }) + expect(execution).toMatchObject({ content: [{ type: "text" }] }) expect(executed).toEqual(["old:request"]) expect(progress).toEqual([ { toolCalls: [{ tool: "echo", status: "running", input: { text: "request" } }] }, + { stage: "old" }, { toolCalls: [{ tool: "echo", status: "completed", input: { text: "request" } }] }, ]) }), diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 1f96f41cbc7..2c4876235cc 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -21,15 +21,16 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { App } from "@opencode-ai/core/app" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { EventTable } from "@opencode-ai/core/event/sql" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { Form } from "@opencode-ai/core/form" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Snapshot } from "@opencode-ai/core/snapshot" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -42,18 +43,15 @@ import { SessionRunner } from "@opencode-ai/core/session/runner" import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionUsage } from "@opencode-ai/core/session/usage" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { CodeMode } from "@opencode-ai/core/codemode" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { PluginHooks } from "@opencode-ai/core/plugin/hooks" import { SystemPromptPlugin } from "@opencode-ai/core/plugin/system-prompt" -import { QuestionTool } from "@opencode-ai/core/tool/question" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { AgentV2 } from "@opencode-ai/core/agent" +import { QuestionTool } from "@opencode-ai/core/tool/plugin/question" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { ConfigCompaction } from "@opencode-ai/core/config/compaction" -import { Tool } from "@opencode-ai/core/tool/tool" -import { ToolHooks } from "@opencode-ai/core/tool/hooks" +import { Tool } from "@opencode-ai/core/tool" +import type { Info } from "@opencode-ai/schema/tool" import { InstructionStateTable, SessionPendingTable, @@ -68,9 +66,9 @@ import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { ReferenceInstructions } from "@opencode-ai/core/reference/instructions" import { McpInstructions } from "@opencode-ai/core/mcp/instructions" -import { ModelV2 } from "@opencode-ai/core/model" +import { ID } from "@opencode-ai/core/model" import { Location } from "@opencode-ai/core/location" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Scope, Stream } from "effect" import { TestClock } from "effect/testing" import { asc, eq } from "drizzle-orm" @@ -103,14 +101,14 @@ const client = Layer.succeed( responseStream = undefined return stream } - const events = streamFailure + const bus = streamFailure ? Stream.fail(streamFailure) : Stream.fromIterable(responses === undefined ? response : (responses.shift() ?? [])) - if (!streamGate) return events + if (!streamGate) return bus return Stream.unwrap( (streamStarted ? Deferred.succeed(streamStarted, undefined) : Effect.void).pipe( Effect.andThen(Deferred.await(streamGate)), - Effect.as(events), + Effect.as(bus), ), ) }) as unknown as LLMClientShape["stream"], @@ -213,14 +211,15 @@ test("does not apply an ineligible tier without base pricing", () => { const authorizations: Tool.Context[] = [] const executions: string[] = [] -const permissionFail = Tool.make({ +const permissionFail = ({ + name: "permission_fail", description: "Reject a permission", input: Schema.Struct({}), output: Schema.Struct({}), execute: () => new ToolFailure({ message: "Permission denied: edit", - error: new PermissionV2.BlockedError({ + error: new Permission.BlockedError({ rules: [], permission: "edit", resources: ["src/index.ts"], @@ -228,8 +227,8 @@ const permissionFail = Tool.make({ }), }) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: () => Effect.die("unused"), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -238,11 +237,22 @@ const permission = Layer.succeed( list: () => Effect.die("unused"), }), ) +const transformTools = ( + registry: Tool.Interface, + tools: Readonly>, + options?: Tool.Options, +) => + registry.transform((draft) => + Object.entries(tools).forEach(([name, tool]) => + draft.add({ ...tool, name, options: { ...tool.options, ...options } }), + ), + ) const echo = Layer.effectDiscard( - ToolRegistry.Service.use((registry) => - registry.register( + Tool.Service.use((registry) => + transformTools(registry, { - echo: Tool.make({ + echo: ({ + name: "echo", description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), @@ -259,7 +269,8 @@ const echo = Layer.effectDiscard( return { output: { text }, content: text } }).pipe(Effect.ensuring(Effect.sync(() => activeToolExecutions--))), }), - defect: Tool.make({ + defect: ({ + name: "defect", description: "Fail unexpectedly", input: Schema.Struct({}), output: Schema.Struct({}), @@ -268,9 +279,8 @@ const echo = Layer.effectDiscard( Effect.andThen(Effect.die("unexpected tool defect")), ), }), - // The wrapped ToolOutputStore below fails bound for this call ID with a - // typed StorageError, exercising the infrastructure failure channel. - storefail: Tool.make({ + storefail: ({ + name: "storefail", description: "Produce output that cannot be persisted", input: Schema.Struct({}), output: Schema.Struct({}), @@ -281,7 +291,7 @@ const echo = Layer.effectDiscard( ), ), ) -const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] }) +const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [Tool.node] }) let modelResolveHook = Effect.void let currentModel = model const models = Layer.mock(SessionRunnerModel.Service)({ @@ -301,7 +311,7 @@ let systemBaseline = "Initial context" let systemRemoved = false let systemUnavailable = false let systemLoadHook = Effect.void -const skillBaselines = new Map() +const skillBaselines = new Map() const systemContext = Layer.mock(InstructionBuiltIns.Service, { load: () => Effect.sync(() => @@ -369,12 +379,6 @@ const pluginSupervisor = Layer.succeed( flush: Effect.suspend(() => pluginFlushHook), }), ) -let codeModeMaterializations: ReadonlyArray = [] -let codeModeMaterializationCount = 0 -const codeMode = Layer.mock(CodeMode.Service, { - register: () => Effect.void, - materialize: () => Effect.sync(() => codeModeMaterializations[codeModeMaterializationCount++] ?? {}), -}) const promptCatalog = Layer.mock(Catalog.Service, { provider: { get: () => Effect.succeed(undefined), @@ -389,15 +393,6 @@ const promptCatalog = Layer.mock(Catalog.Service, { small: () => Effect.succeed(undefined), }, }) -// Pass-through bounding that fails "call-storefail" with a typed StorageError so -// runner tests can exercise the infrastructure failure channel deterministically. -const toolOutputStore = Layer.mock(ToolOutputStore.Service, { - limits: () => Effect.succeed({ maxLines: ToolOutputStore.MAX_LINES, maxBytes: ToolOutputStore.MAX_BYTES }), - bound: (input) => - input.callID === "call-storefail" - ? Effect.fail(new ToolOutputStore.StorageError({ operation: "write", cause: new Error("disk full") })) - : Effect.succeed({ content: input.content, outputPaths: [] }), -}) const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Snapshot.node, Snapshot.noopLayer], [LayerNodePlatform.llmClient, client], @@ -407,18 +402,16 @@ const runnerLayer = AppNodeBuilder.build(SessionRunnerLLM.node, [ [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], [SkillInstructions.node, skillInstructions], [ReferenceInstructions.node, referenceInstructions], - [PermissionV2.node, permission], + [Permission.node, permission], [Config.node, config], [McpInstructions.node, mcpInstructions], - [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], - [CodeMode.node, codeMode], ]) const execution = Layer.effect( SessionExecution.Service, Effect.gen(function* () { const sessionRunner = yield* SessionRunner.Service - const coordinator = yield* SessionRunCoordinator.make({ + const coordinator = yield* SessionRunCoordinator.make({ drain: (sessionID, force) => sessionRunner.drain({ sessionID, force }), }) return SessionExecution.Service.of({ @@ -434,15 +427,15 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, Form.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, Catalog.node, - ToolRegistry.node, - ToolRegistry.toolsNode, - ToolHooks.node, + Tool.node, + Tool.node, + PluginHooks.node, PluginHooks.node, echoNode, SessionRunnerModel.node, @@ -455,11 +448,11 @@ const it = testEffect( Snapshot.node, SessionRunnerLLM.node, SessionExecution.node, - SessionV2.node, + Session.node, ]), [ [LayerNodePlatform.llmClient, client], - [PermissionV2.node, permission], + [Permission.node, permission], [Catalog.node, promptCatalog], [SessionRunnerModel.node, models], [InstructionBuiltIns.node, systemContext], @@ -470,17 +463,15 @@ const it = testEffect( [Snapshot.node, Snapshot.noopLayer], [SessionExecution.node, execution], [Config.node, config], - [ToolOutputStore.node, toolOutputStore], [PluginSupervisor.node, pluginSupervisor], - [CodeMode.node, codeMode], ], ), ) -const sessionID = SessionV2.ID.make("ses_runner_test") -const otherSessionID = SessionV2.ID.make("ses_runner_other") -const admit = (session: SessionV2.Interface, text: string) => session.prompt({ sessionID, text, resume: false }) +const sessionID = Session.ID.make("ses_runner_test") +const otherSessionID = Session.ID.make("ses_runner_other") +const admit = (session: Session.Interface, text: string) => session.prompt({ sessionID, text, resume: false }) -const insertSession = (id: SessionV2.ID) => +const insertSession = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service yield* db @@ -500,7 +491,7 @@ const insertSession = (id: SessionV2.ID) => const setup = Effect.gen(function* () { const { db } = yield* Database.Service - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service const catalog = yield* Catalog.Service const hooks = yield* PluginHooks.Service const pluginHost = host({ @@ -521,8 +512,6 @@ const setup = Effect.gen(function* () { systemLoadHook = Effect.void modelResolveHook = Effect.void pluginFlushHook = Effect.void - codeModeMaterializations = [] - codeModeMaterializationCount = 0 currentModel = model skillBaselines.clear() responses = undefined @@ -537,7 +526,7 @@ const setup = Effect.gen(function* () { activeToolExecutions = 0 maxActiveToolExecutions = 0 yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("build"), (agent) => { + draft.update(Agent.ID.make("build"), (agent) => { agent.mode = "primary" }), ) @@ -548,7 +537,7 @@ const setup = Effect.gen(function* () { .run() .pipe(Effect.orDie) yield* insertSession(sessionID) - return yield* SessionV2.Service + return yield* Session.Service }) const providerUnavailable = () => @@ -589,7 +578,7 @@ const messageTexts = (request: LLMRequest, role: "user" | "system") => const userTexts = (request: LLMRequest) => messageTexts(request, "user") const systemTexts = (request: LLMRequest) => messageTexts(request, "system") -const recordedEventTypes = (id: SessionV2.ID) => +const recordedEventTypes = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service return yield* db @@ -604,7 +593,7 @@ const recordedEventTypes = (id: SessionV2.ID) => ) }) -const recordedStepSettlementEvents = (id: SessionV2.ID, assistantMessageID: SessionMessage.ID) => +const recordedStepSettlementEvents = (id: Session.ID, assistantMessageID: SessionMessage.ID) => Effect.gen(function* () { const { db } = yield* Database.Service const settlementTypes = new Set([ @@ -635,10 +624,10 @@ const requireAssistant = (messages: readonly SessionMessage.Info[]) => { return assistant } -const replaySessionProjection = (id: SessionV2.ID) => +const replaySessionProjection = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const recorded = yield* db .select() .from(EventTable) @@ -647,11 +636,11 @@ const replaySessionProjection = (id: SessionV2.ID) => .all() .pipe(Effect.orDie) - yield* events.remove(id) + yield* bus.remove(id) yield* db.delete(InstructionStateTable).where(eq(InstructionStateTable.session_id, id)).run().pipe(Effect.orDie) yield* db.delete(SessionPendingTable).where(eq(SessionPendingTable.session_id, id)).run().pipe(Effect.orDie) yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, id)).run().pipe(Effect.orDie) - yield* events.replayAll( + yield* bus.replayAll( recorded.map((event) => ({ id: event.id, created: DateTime.makeUnsafe(event.created), @@ -666,7 +655,7 @@ const replaySessionProjection = (id: SessionV2.ID) => type FragmentKind = "text" | "reasoning" | "tool input" type FragmentFixture = { - readonly delta: EventV2.Definition + readonly delta: Event.Definition readonly completeEvents: LLMEvent[] readonly partialEvents: LLMEvent[] readonly expectedAssistant: unknown @@ -746,8 +735,8 @@ const verifyEphemeralDeltas = (kind: FragmentKind) => const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks) const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant] yield* admit(session, prompt) - const events = yield* EventV2.Service - const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped) + const bus = yield* Bus.Service + const live = yield* bus.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow response = fixture.completeEvents @@ -757,7 +746,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) => const deltas = yield* db .select({ type: EventTable.type }) .from(EventTable) - .where(eq(EventTable.type, EventV2.versionedType(fixture.delta.type, 1))) + .where(eq(EventTable.type, Bus.versionedType(fixture.delta.type, 1))) .all() .pipe(Effect.orDie) expect(Array.from(yield* Fiber.join(live))).toHaveLength(32) @@ -834,92 +823,6 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) => }) describe("SessionRunnerLLM", () => { - it.effect("uses one Code Mode materialization per request for instructions and execution", () => - Effect.gen(function* () { - const executed: string[] = [] - const execute = (name: string) => - Tool.make({ - description: `Execute ${name}`, - input: Schema.Struct({}), - output: Schema.String, - execute: () => Effect.sync(() => executed.push(name)).pipe(Effect.as({ output: name })), - }) - const catalog = (name: string) => [ - { - path: `catalog.${name.toLowerCase()}`, - description: `Code Mode catalog ${name}`, - signature: `tools.catalog.${name.toLowerCase()}(input: {}): Promise`, - }, - ] - const session = yield* setup - codeModeMaterializations = [ - { catalog: catalog("A"), tool: execute("A") }, - { catalog: catalog("B"), tool: execute("B") }, - { catalog: catalog("C"), tool: execute("C") }, - { catalog: catalog("D"), tool: execute("D") }, - ] - yield* admit(session, "Use Code Mode") - responses = [reply.tool("call-execute", "execute", {}), reply.stop()] - - yield* session.resume(sessionID) - - expect(requests).toHaveLength(2) - expect(codeModeMaterializationCount).toBe(2) - expect(requests[0]?.system.some((part) => part.text.includes("Code Mode catalog A"))).toBe(true) - expect(requests[0]?.system.some((part) => part.text.includes("Code Mode catalog B"))).toBe(false) - expect(requests[0]?.tools.find((tool) => tool.name === "execute")?.description).toBe("Execute A") - expect(executed).toEqual(["A"]) - expect(requests[1]?.tools.find((tool) => tool.name === "execute")?.description).toBe("Execute B") - expect( - requests[1]?.messages.some( - (message) => - message.role === "system" && - message.content.some((part) => part.type === "text" && part.text.includes("Code Mode catalog B")), - ), - ).toBe(true) - }), - ) - - it.effect("advertises execute and durable guidance for an empty Code Mode catalog", () => - Effect.gen(function* () { - const session = yield* setup - const empty = { - catalog: [], - tool: Tool.make({ - description: "Execute Code Mode", - input: Schema.Struct({ code: Schema.String }), - output: Schema.String, - execute: () => Effect.succeed({ output: "unused" }), - }), - } - codeModeMaterializations = [empty, empty, {}] - yield* admit(session, "Continue without Code Mode tools") - response = reply.stop() - - yield* session.resume(sessionID) - yield* admit(session, "Still no Code Mode tools") - yield* session.resume(sessionID) - yield* admit(session, "Code Mode denied") - yield* session.resume(sessionID) - - expect(requests).toHaveLength(3) - expect(requests[0]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) - expect(requests[0]?.system.some((part) => part.text.includes("Do not call `execute`"))).toBe(true) - expect(requests[1]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail", "execute"]) - expect(requests[1]?.messages.filter((message) => message.role === "system")).toEqual([]) - expect(requests[2]?.tools.map((tool) => tool.name)).toEqual(["defect", "echo", "storefail"]) - expect( - requests[2]?.messages.some( - (message) => - message.role === "system" && - message.content.some( - (part) => part.type === "text" && part.text.includes("Code Mode tools are no longer available"), - ), - ), - ).toBe(true) - }), - ) - it.effect("applies session context hooks without exposing unavailable tools", () => Effect.gen(function* () { const session = yield* setup @@ -963,11 +866,12 @@ describe("SessionRunnerLLM", () => { it.effect("advertises and executes a location registered tool", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const contexts: Tool.Context[] = [] - yield* registry.register( + yield* transformTools(registry, { - location_context: Tool.make({ + location_context: ({ + name: "location_context", description: "Read application context", input: Schema.Struct({ query: Schema.String }), output: Schema.Struct({ answer: Schema.String }), @@ -983,8 +887,8 @@ describe("SessionRunnerLLM", () => { ) yield* admit(session, "Use application context") responses = [reply.tool("call-location", "location_context", { query: "hello" }), []] - const events = yield* EventV2.Service - const progressFiber = yield* events.subscribe(SessionEvent.Tool.Progress).pipe( + const bus = yield* Bus.Service + const progressFiber = yield* bus.subscribe(SessionEvent.Tool.Progress).pipe( Stream.filter((event) => event.data.sessionID === sessionID && event.data.callID === "call-location"), Stream.take(1), Stream.runCollect, @@ -997,9 +901,9 @@ describe("SessionRunnerLLM", () => { expect(contexts).toEqual([ { sessionID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), messageID: expect.stringMatching(/^msg_/), - callID: "call-location", + callID: Tool.CallID.make("call-location"), progress: expect.any(Function), }, ]) @@ -1023,14 +927,17 @@ describe("SessionRunnerLLM", () => { it.effect("prefers failure outcome metadata over retained progress", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - const hooks = yield* ToolHooks.Service - yield* hooks.hook.after((event) => { - if (event.status === "error") event.metadata = { phase: "failed" } + const registry = yield* Tool.Service + const hooks = yield* PluginHooks.Service + yield* hooks.register("tool", "execute.after", (event) => { + if (event.status === "error") + event.error = new Tool.Error({ message: event.error.message, metadata: { phase: "failed" } }) + return Effect.void }) - yield* registry.register( + yield* transformTools(registry, { - failing_progress: Tool.make({ + failing_progress: ({ + name: "failing_progress", description: "Report progress and fail", input: Schema.Struct({}), output: Schema.Struct({}), @@ -1072,13 +979,13 @@ describe("SessionRunnerLLM", () => { it.effect("executes the tool advertised before a registry reload", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const scope = yield* Scope.make() const executions: string[] = [] - yield* registry - .register( + yield* transformTools(registry, { - reloaded: Tool.make({ + reloaded: ({ + name: "reloaded", description: "Record the advertised tool", input: Schema.Struct({}), output: Schema.Struct({ value: Schema.String }), @@ -1105,9 +1012,10 @@ describe("SessionRunnerLLM", () => { const run = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) yield* Scope.close(scope, Exit.void) - yield* registry.register( + yield* transformTools(registry, { - reloaded: Tool.make({ + reloaded: ({ + name: "reloaded", description: "Record the replacement tool", input: Schema.Struct({}), output: Schema.Struct({ value: Schema.String }), @@ -1182,7 +1090,7 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("streams one request with registry definitions from chronological V2 user history", () => + it.effect("streams one request with registry definitions from chronological user history", () => Effect.gen(function* () { const session = yield* setup yield* admit(session, "First") @@ -1204,9 +1112,9 @@ describe("SessionRunnerLLM", () => { it.effect("marks the initial instruction sync as baseline metadata", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - const instructionEvents: EventV2.Payload[] = [] - const unsubscribe = yield* events.listen((event) => + const bus = yield* Bus.Service + const instructionEvents: Event.Payload[] = [] + const unsubscribe = yield* bus.listen((event) => Effect.sync(() => { if (event.type === "session.instructions.updated") instructionEvents.push(event) }), @@ -1260,12 +1168,12 @@ describe("SessionRunnerLLM", () => { it.effect("interrupts a source Location runner after a Session moves", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service const { db } = yield* Database.Service yield* admit(session, "First") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.Moved, { + yield* bus.publish(SessionEvent.Moved, { sessionID, location: Location.Ref.make({ directory: AbsolutePath.make("/moved") }), }) @@ -1313,16 +1221,16 @@ describe("SessionRunnerLLM", () => { expect(systemTexts(requests.at(-1)!)).toContain("Latest context") const { db } = yield* Database.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service const recorded = yield* db .select() .from(EventTable) .where(eq(EventTable.aggregate_id, forked.id)) .orderBy(asc(EventTable.seq)) .all() - yield* events.remove(forked.id) + yield* bus.remove(forked.id) yield* db.delete(SessionTable).where(eq(SessionTable.id, forked.id)).run() - yield* events.replayAll( + yield* bus.replayAll( recorded.map((event) => ({ id: event.id, created: DateTime.makeUnsafe(event.created), @@ -1455,9 +1363,9 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route }) - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "" agent.mode = "primary" }), @@ -1477,9 +1385,9 @@ describe("SessionRunnerLLM", () => { it.effect("includes the effective default agent system before durable context", () => Effect.gen(function* () { const session = yield* setup - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "Build agent instructions" agent.mode = "primary" }), @@ -1496,17 +1404,17 @@ describe("SessionRunnerLLM", () => { it.effect("uses the configured default agent system for omitted-agent sessions", () => Effect.gen(function* () { const session = yield* setup - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => { - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "Build agent instructions" agent.mode = "primary" }) - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.system = "Reviewer instructions" agent.mode = "primary" }) - editor.default(AgentV2.ID.make("reviewer")) + editor.default(Agent.ID.make("reviewer")) }) yield* admit(session, "First") @@ -1521,9 +1429,9 @@ describe("SessionRunnerLLM", () => { it.effect("uses only the agent prompt and initial instructions as system parts", () => Effect.gen(function* () { const session = yield* setup - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.system = "Build agent instructions" agent.mode = "primary" }), @@ -1541,9 +1449,9 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { const session = yield* setup const { db } = yield* Database.Service - const agent = yield* AgentV2.Service + const agent = yield* Agent.Service yield* agent.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.system = "Reviewer instructions" agent.mode = "primary" }), @@ -1574,7 +1482,7 @@ describe("SessionRunnerLLM", () => { .where(eq(SessionTable.id, sessionID)) .run() .pipe(Effect.orDie) - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ sessionID, text: "Inspect files", resume: false }) requests.length = 0 @@ -1595,7 +1503,7 @@ describe("SessionRunnerLLM", () => { yield* setup const release = yield* Deferred.make() pluginFlushHook = Deferred.await(release) - const session = yield* SessionV2.Service + const session = yield* Session.Service yield* session.prompt({ sessionID, text: "Wait for plugins", resume: false }) requests.length = 0 @@ -1615,21 +1523,21 @@ describe("SessionRunnerLLM", () => { it.effect("updates selected-agent skill instructions after an agent switch", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - const agents = yield* AgentV2.Service + const bus = yield* Bus.Service + const agents = yield* Agent.Service yield* agents.transform((draft) => - draft.update(AgentV2.ID.make("reviewer"), (agent) => { + draft.update(Agent.ID.make("reviewer"), (agent) => { agent.mode = "primary" }), ) - skillBaselines.set(AgentV2.ID.make("build"), "Build skills") + skillBaselines.set(Agent.ID.make("build"), "Build skills") yield* admit(session, "First") yield* session.resume(sessionID) - skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills") - yield* events.publish(SessionEvent.AgentSelected, { + skillBaselines.set(Agent.ID.make("reviewer"), "Reviewer skills") + yield* bus.publish(SessionEvent.AgentSelected, { sessionID, - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), }) yield* admit(session, "Second") yield* session.resume(sessionID) @@ -1645,17 +1553,17 @@ describe("SessionRunnerLLM", () => { it.effect("keeps the sampled agent when selection changes during observation", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - skillBaselines.set(AgentV2.ID.make("build"), "Build skills") - skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills") + const bus = yield* Bus.Service + skillBaselines.set(Agent.ID.make("build"), "Build skills") + skillBaselines.set(Agent.ID.make("reviewer"), "Reviewer skills") let switched = false systemLoadHook = Effect.suspend(() => { if (switched) return Effect.void switched = true - return events + return bus .publish(SessionEvent.AgentSelected, { sessionID, - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), }) .pipe(Effect.asVoid) }) @@ -1672,15 +1580,15 @@ describe("SessionRunnerLLM", () => { it.effect("keeps the sampled model when selection changes during model resolution", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service let switched = false modelResolveHook = Effect.suspend(() => { if (switched) return Effect.void switched = true - return events + return bus .publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) .pipe(Effect.asVoid) }) @@ -1806,16 +1714,16 @@ describe("SessionRunnerLLM", () => { it.effect("keeps initial instructions and chronological updates after a model switch", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) systemBaseline = "Changed context" yield* admit(session, "Second") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) systemBaseline = "Replacement context" yield* admit(session, "Third") @@ -1844,13 +1752,13 @@ describe("SessionRunnerLLM", () => { it.effect("preserves instruction values while a source is temporarily unavailable", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) systemUnavailable = true yield* admit(session, "Second") @@ -1871,16 +1779,16 @@ describe("SessionRunnerLLM", () => { it.effect("moves the epoch at compaction and narrates later changes", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.Compaction.Started, { + yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", }) - yield* events.publish(SessionEvent.Compaction.Ended, { + yield* bus.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", text: "summary", @@ -1987,7 +1895,7 @@ describe("SessionRunnerLLM", () => { }) expect( (yield* recordedEventTypes(sessionID)).filter( - (type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1), + (type) => type === Bus.versionedType(SessionEvent.Compaction.Failed.type, 1), ), ).toHaveLength(1) }), @@ -1996,7 +1904,7 @@ describe("SessionRunnerLLM", () => { it.effect("explains when manual compaction has no history", () => Effect.gen(function* () { yield* setup - const session = yield* SessionV2.Service + const session = yield* Session.Service const compaction = yield* session.compact({ sessionID }) modelResolveHook = Effect.die("model resolution should not run") @@ -2011,7 +1919,7 @@ describe("SessionRunnerLLM", () => { }) expect( (yield* recordedEventTypes(sessionID)).filter( - (type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1), + (type) => type === Bus.versionedType(SessionEvent.Compaction.Failed.type, 1), ), ).toHaveLength(1) }), @@ -2126,7 +2034,7 @@ describe("SessionRunnerLLM", () => { }) expect( (yield* recordedEventTypes(sessionID)).filter( - (type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1), + (type) => type === Bus.versionedType(SessionEvent.Compaction.Failed.type, 1), ), ).toHaveLength(1) }), @@ -2404,19 +2312,19 @@ describe("SessionRunnerLLM", () => { it.effect("uses epoch values after compaction while a source is unavailable", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "First") yield* session.resume(sessionID) systemBaseline = "Changed context" yield* admit(session, "Second") yield* session.resume(sessionID) - yield* events.publish(SessionEvent.Compaction.Started, { + yield* bus.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", }) - yield* events.publish(SessionEvent.Compaction.Ended, { + yield* bus.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", text: "summary", @@ -2574,7 +2482,7 @@ describe("SessionRunnerLLM", () => { it.effect("reloads a model switch before a tool-driven continuation step", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Echo this") responses = [reply.tool("call-echo", "echo", { text: "hello" }), reply.stop()] @@ -2583,9 +2491,9 @@ describe("SessionRunnerLLM", () => { toolExecutionsReady = 1 const run = yield* Effect.forkChild(session.resume(sessionID)) yield* Deferred.await(toolExecutionsStarted) - yield* events.publish(SessionEvent.ModelSelected, { + yield* bus.publish(SessionEvent.ModelSelected, { sessionID, - model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, + model: { id: ID.make("replacement"), providerID: Provider.ID.make("fake") }, }) systemBaseline = "Replacement context" yield* Deferred.succeed(toolExecutionGate, undefined) @@ -3193,29 +3101,29 @@ describe("SessionRunnerLLM", () => { it.effect("durably fails local tools left running by a prior process before continuing", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Recover interrupted tool") - yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") + yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID, callID: "call-interrupted", name: "echo", }) - yield* events.publish(SessionEvent.Tool.Input.Ended, { + yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID, assistantMessageID, callID: "call-interrupted", text: '{"text":"stale"}', }) - yield* events.publish(SessionEvent.Tool.Called, { + yield* bus.publish(SessionEvent.Tool.Called, { sessionID, assistantMessageID, callID: "call-interrupted", @@ -3250,29 +3158,29 @@ describe("SessionRunnerLLM", () => { it.effect("durably fails hosted tools left running by a prior process before continuing inline", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Recover interrupted hosted tool") - yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") + yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID, callID: "call-hosted-interrupted", name: "web_search", }) - yield* events.publish(SessionEvent.Tool.Input.Ended, { + yield* bus.publish(SessionEvent.Tool.Input.Ended, { sessionID, assistantMessageID, callID: "call-hosted-interrupted", text: '{"query":"stale"}', }) - yield* events.publish(SessionEvent.Tool.Called, { + yield* bus.publish(SessionEvent.Tool.Called, { sessionID, assistantMessageID, callID: "call-hosted-interrupted", @@ -3301,17 +3209,17 @@ describe("SessionRunnerLLM", () => { it.effect("durably fails pending tool input left by a prior process before continuing", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* admit(session, "Recover interrupted tool input") - yield* SessionPending.promote((yield* Database.Service).db, events, sessionID, "steer") + yield* SessionPending.promote((yield* Database.Service).db, bus, sessionID, "steer") const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), - model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, + agent: Agent.ID.make("build"), + model: { id: ID.make("fake-model"), providerID: Provider.ID.make("fake") }, }) - yield* events.publish(SessionEvent.Tool.Input.Started, { + yield* bus.publish(SessionEvent.Tool.Input.Started, { sessionID, assistantMessageID, callID: "call-pending-interrupted", @@ -3351,10 +3259,10 @@ describe("SessionRunnerLLM", () => { it.effect("retries inbox input after prompt projection rolls back", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service + const bus = yield* Bus.Service const defect = new Error("fail after prompt promotion") let fail = true - yield* events.project(SessionEvent.InputPromoted, () => (fail ? Effect.die(defect) : Effect.void)) + yield* bus.project(SessionEvent.InputPromoted, () => (fail ? Effect.die(defect) : Effect.void)) yield* admit(session, "Recover promoted input") expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) @@ -3372,8 +3280,8 @@ describe("SessionRunnerLLM", () => { it.effect("does not strand a committed promotion when a post-commit listener defects", () => Effect.gen(function* () { const session = yield* setup - const events = yield* EventV2.Service - yield* events.listen((event) => + const bus = yield* Bus.Service + yield* bus.listen((event) => event.type === SessionEvent.InputPromoted.type ? Effect.die("fail after prompt promotion commits") : Effect.void, @@ -3408,7 +3316,7 @@ describe("SessionRunnerLLM", () => { it.effect("adds the parent session header to child model requests", () => Effect.gen(function* () { const session = yield* setup - const parentID = SessionV2.ID.make("ses_runner_parent") + const parentID = Session.ID.make("ses_runner_parent") const { db } = yield* Database.Service yield* db .update(SessionTable) @@ -3460,8 +3368,8 @@ describe("SessionRunnerLLM", () => { it.effect("bounds 64-character session prompt cache keys", () => Effect.gen(function* () { const session = yield* setup - const longSessionID = SessionV2.ID.make(`ses_${"a".repeat(64)}`) - const otherLongSessionID = SessionV2.ID.make(`ses_${"b".repeat(64)}`) + const longSessionID = Session.ID.make(`ses_${"a".repeat(64)}`) + const otherLongSessionID = Session.ID.make(`ses_${"b".repeat(64)}`) yield* insertSession(longSessionID) yield* insertSession(otherLongSessionID) yield* session.prompt({ @@ -3583,16 +3491,17 @@ describe("SessionRunnerLLM", () => { it.effect("returns tool-wrapped policy blocks to the model and continues", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - blocked: Tool.make({ + blocked: ({ + name: "blocked", description: "Fail because policy blocked execution", input: Schema.Struct({}), output: Schema.Struct({}), execute: () => - Effect.fail(new PermissionV2.BlockedError({ rules: [], permission: "blocked", resources: ["*"] })).pipe( - Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })), + Effect.fail(new Permission.BlockedError({ rules: [], permission: "blocked", resources: ["*"] })).pipe( + Effect.mapError(() => new Tool.Error({ message: "Permission blocked" })), ), }), }, @@ -3621,14 +3530,15 @@ describe("SessionRunnerLLM", () => { it.effect("interrupts runner continuation when permission approval is declined", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - declined: Tool.make({ + declined: ({ + name: "declined", description: "Fail because the user declined approval", input: Schema.Struct({}), output: Schema.Struct({}), - execute: () => Effect.die(new PermissionV2.DeclinedError()), + execute: () => Effect.die(new Permission.DeclinedError()), }), }, { codemode: false }, @@ -3661,16 +3571,17 @@ describe("SessionRunnerLLM", () => { it.effect("returns permission corrections to the model and continues", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - corrected: Tool.make({ + corrected: ({ + name: "corrected", description: "Fail with user correction feedback", input: Schema.Struct({}), output: Schema.Struct({}), execute: () => - Effect.fail(new PermissionV2.CorrectedError({ feedback: "Use another tool" })).pipe( - Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })), + Effect.fail(new Permission.CorrectedError({ feedback: "Use another tool" })).pipe( + Effect.mapError(() => new Tool.Error({ message: "Use another tool" })), ), }), }, @@ -3734,8 +3645,8 @@ describe("SessionRunnerLLM", () => { it.effect("returns configured permission denials to the model and continues", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register({ permissionfail: permissionFail }, { codemode: false }) + const registry = yield* Tool.Service + yield* transformTools(registry, { permissionfail: permissionFail }, { codemode: false }) yield* admit(session, "Reject permission") responses = [ reply.tool("call-permission", "permissionfail", {}), @@ -3772,10 +3683,11 @@ describe("SessionRunnerLLM", () => { it.effect("interrupts runner continuation when a question is cancelled", () => Effect.gen(function* () { const session = yield* setup - const registry = yield* ToolRegistry.Service - yield* registry.register( + const registry = yield* Tool.Service + yield* transformTools(registry, { - question: Tool.make({ + question: ({ + name: "question", description: "Ask the user", input: Schema.Struct({}), output: Schema.Struct({}), @@ -3976,9 +3888,9 @@ describe("SessionRunnerLLM", () => { it.effect("forces a text response on an agent's configured final step", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4012,9 +3924,9 @@ describe("SessionRunnerLLM", () => { it.effect("resets the configured step allowance when steering input promotes", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4136,15 +4048,15 @@ describe("SessionRunnerLLM", () => { toolExecutionsStarted = undefined const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.success.2", "session.step.failed.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) }), ) @@ -4307,9 +4219,9 @@ describe("SessionRunnerLLM", () => { it.effect("retries a model call without consuming the logical agent step", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4685,9 +4597,9 @@ describe("SessionRunnerLLM", () => { it.effect("does not continue malformed tool input past the agent step limit", () => Effect.gen(function* () { const session = yield* setup - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("build"), (agent) => { + editor.update(Agent.ID.make("build"), (agent) => { agent.steps = 2 }), ) @@ -4793,14 +4705,14 @@ describe("SessionRunnerLLM", () => { const context = yield* session.context(sessionID) const assistant = requireAssistant(context) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.failed.1", ]) - expect(events[2]?.data.error).toMatchObject({ type: "unknown", message: "unexpected tool defect" }) + expect(bus[2]?.data.error).toMatchObject({ type: "unknown", message: "unexpected tool defect" }) }), ) @@ -4830,15 +4742,15 @@ describe("SessionRunnerLLM", () => { expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider did not return a tool result") const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.failed.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) yield* replaySessionProjection(sessionID) @@ -4868,15 +4780,15 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.ended.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) }), ) @@ -4904,8 +4816,8 @@ describe("SessionRunnerLLM", () => { toolExecutionGate = undefined const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => ({ type: event.type, callID: event.data.callID }))).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => ({ type: event.type, callID: event.data.callID }))).toEqual([ { type: "session.step.started.1", callID: undefined }, { type: "session.tool.called.1", callID: "call-local-raw-failure" }, { type: "session.tool.called.1", callID: "call-hosted-raw-failure-pair" }, @@ -4914,7 +4826,7 @@ describe("SessionRunnerLLM", () => { { type: "session.step.failed.1", callID: undefined }, ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) }), ) @@ -4932,15 +4844,15 @@ describe("SessionRunnerLLM", () => { expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) expect(requests).toHaveLength(1) const assistant = requireAssistant(yield* session.context(sessionID)) - const events = yield* recordedStepSettlementEvents(sessionID, assistant.id) - expect(events.map((event) => event.type)).toEqual([ + const bus = yield* recordedStepSettlementEvents(sessionID, assistant.id) + expect(bus.map((event) => event.type)).toEqual([ "session.step.started.1", "session.tool.called.1", "session.tool.failed.2", "session.step.failed.1", ]) expect( - events.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), + bus.filter((event) => event.type.startsWith("session.step.") && event.type !== "session.step.started.1"), ).toHaveLength(1) yield* replaySessionProjection(sessionID) expect(yield* session.context(sessionID)).toMatchObject([ diff --git a/packages/core/test/session-skill.test.ts b/packages/core/test/session-skill.test.ts index decf33a9a31..2ab4a655519 100644 --- a/packages/core/test/session-skill.test.ts +++ b/packages/core/test/session-skill.test.ts @@ -4,30 +4,30 @@ import { Effect, Layer, LayerMap } from "effect" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionStore } from "@opencode-ai/core/session/store" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { testEffect } from "./lib/effect" const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -const projects = Layer.mock(ProjectV2.Service, { - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), +const projects = Layer.mock(Project.Service, { + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), }) -const skills = Layer.mock(SkillV2.Service, { +const skills = Layer.mock(Skill.Service, { list: () => Effect.succeed([ - SkillV2.Info.make({ - id: SkillV2.ID.make("effect"), - name: SkillV2.Name.make("Effect"), + Skill.Info.make({ + id: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), description: "Effect guidance", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Use Effect", @@ -45,23 +45,23 @@ const locations = Layer.effect( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ [LocationServiceMap.node, locations], - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, SessionExecution.noopLayer], ], ), ) -describe("SessionV2.skill", () => { +describe("Session.skill", () => { it.effect("projects the caller-supplied message ID", () => Effect.gen(function* () { - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const session = yield* sessions.create({ location }) const id = SessionMessage.ID.make("msg_caller_skill") - yield* sessions.skill({ id, sessionID: session.id, skill: SkillV2.ID.make("effect"), resume: false }) + yield* sessions.skill({ id, sessionID: session.id, skill: Skill.ID.make("effect"), resume: false }) expect(yield* sessions.messages({ sessionID: session.id })).toContainEqual( expect.objectContaining({ id, type: "skill", skill: "effect", name: "Effect", text: "Use Effect" }), diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index ce7f7f89bd3..b69abb77bc9 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -1,12 +1,12 @@ import { expect } from "bun:test" import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/ai" import { OpenAIChat } from "@opencode-ai/ai/protocols" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" @@ -14,7 +14,7 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { SessionTitle } from "@opencode-ai/core/session/title" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { App } from "@opencode-ai/core/app" @@ -77,10 +77,10 @@ const it = testEffect( AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, SessionProjector.node, SessionStore.node, - AgentV2.node, + Agent.node, SessionTitle.node, ]), [ @@ -90,7 +90,7 @@ const it = testEffect( ), ) -const insertSession = (id: SessionV2.ID) => +const insertSession = (id: Session.ID) => Effect.gen(function* () { const { db } = yield* Database.Service yield* db @@ -114,16 +114,16 @@ const insertSession = (id: SessionV2.ID) => .pipe(Effect.orDie) }) -const prompt = (sessionID: SessionV2.ID, text: string) => +const prompt = (sessionID: Session.ID, text: string) => Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const messageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.InputAdmitted, { + yield* bus.publish(SessionEvent.InputAdmitted, { sessionID, inputID: messageID, input: { type: "user", data: { text }, delivery: "steer" }, }) - yield* events.publish(SessionEvent.InputPromoted, { + yield* bus.publish(SessionEvent.InputPromoted, { sessionID, inputID: messageID, }) @@ -132,15 +132,15 @@ const prompt = (sessionID: SessionV2.ID, text: string) => it.effect("generates a title from the sole user message and renames the session", () => Effect.gen(function* () { requests = [] - const agentService = yield* AgentV2.Service + const agentService = yield* Agent.Service yield* agentService.transform((editor) => { - editor.update(AgentV2.ID.make("title"), (agent) => { + editor.update(Agent.ID.make("title"), (agent) => { agent.mode = "primary" agent.hidden = true agent.system = "You are a title generator." }) }) - const sessionID = SessionV2.ID.make("ses_title_generate") + const sessionID = Session.ID.make("ses_title_generate") yield* insertSession(sessionID) yield* prompt(sessionID, "Help me debug the failing build") @@ -171,15 +171,15 @@ it.effect("generates a title from the sole user message and renames the session" it.effect("does not generate once a second user message exists", () => Effect.gen(function* () { requests = [] - const agentService = yield* AgentV2.Service + const agentService = yield* Agent.Service yield* agentService.transform((editor) => { - editor.update(AgentV2.ID.make("title"), (agent) => { + editor.update(Agent.ID.make("title"), (agent) => { agent.mode = "primary" agent.hidden = true agent.system = "You are a title generator." }) }) - const sessionID = SessionV2.ID.make("ses_title_second_message") + const sessionID = Session.ID.make("ses_title_second_message") yield* insertSession(sessionID) yield* prompt(sessionID, "First message") yield* prompt(sessionID, "Second message") @@ -200,15 +200,15 @@ it.effect("does not generate once a second user message exists", () => it.effect("does not generate for a child session", () => Effect.gen(function* () { requests = [] - const agentService = yield* AgentV2.Service + const agentService = yield* Agent.Service yield* agentService.transform((editor) => { - editor.update(AgentV2.ID.make("title"), (agent) => { + editor.update(Agent.ID.make("title"), (agent) => { agent.mode = "primary" agent.hidden = true agent.system = "You are a title generator." }) }) - const sessionID = SessionV2.ID.make("ses_title_child") + const sessionID = Session.ID.make("ses_title_child") const { db } = yield* Database.Service yield* db .insert(ProjectTable) @@ -221,7 +221,7 @@ it.effect("does not generate for a child session", () => .values({ id: sessionID, project_id: Project.ID.global, - parent_id: SessionV2.ID.make("ses_title_parent"), + parent_id: Session.ID.make("ses_title_parent"), slug: sessionID, directory: "/project", title: "Child session - fake", @@ -246,7 +246,7 @@ it.effect("does not generate for a child session", () => it.effect("does not generate when the title agent is removed", () => Effect.gen(function* () { requests = [] - const sessionID = SessionV2.ID.make("ses_title_no_agent") + const sessionID = Session.ID.make("ses_title_no_agent") yield* insertSession(sessionID) yield* prompt(sessionID, "Help me debug the failing build") diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index 414e260e984..fc57c92c52d 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -3,33 +3,33 @@ import { asc, eq } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" import { Database } from "@opencode-ai/core/database/database" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Agent } from "@opencode-ai/core/agent" import { EventTable } from "@opencode-ai/core/event/sql" -import { ModelV2 } from "@opencode-ai/core/model" +import { Model } from "@opencode-ai/core/model" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql" import { testEffect } from "./lib/effect" -const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, EventV2.node, SessionProjector.node]))) +const it = testEffect(LayerNode.compile(LayerNode.group([Database.node, Bus.node, SessionProjector.node]))) const timestamp = DateTime.makeUnsafe(1) -const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") } +const model = { id: Model.ID.make("model"), providerID: Provider.ID.make("provider") } const content = (text: string) => [{ type: "text" as const, text }] as const -describe("Tool.Progress", () => { +describe("Tool.Metadata", () => { it.effect("keeps progress live-only and terminal settlements durable", () => Effect.gen(function* () { const { db } = yield* Database.Service - const service = yield* EventV2.Service - const sessionID = SessionV2.ID.make("ses_tool_progress_projector") + const service = yield* Bus.Service + const sessionID = Session.ID.make("ses_tool_progress_projector") yield* db .insert(ProjectTable) .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) @@ -52,7 +52,7 @@ describe("Tool.Progress", () => { yield* service.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("build"), + agent: Agent.ID.make("build"), model, }) const readAssistant = Effect.gen(function* () { @@ -144,9 +144,9 @@ describe("Tool.Progress", () => { .orderBy(asc(EventTable.seq)) .all() .pipe(Effect.orDie) - expect(rows.map((row) => row.type)).not.toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1)) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 2)) - expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 2)) + expect(rows.map((row) => row.type)).not.toContain(Bus.versionedType(SessionEvent.Tool.Progress.type, 1)) + expect(rows.map((row) => row.type)).toContain(Bus.versionedType(SessionEvent.Tool.Success.type, 2)) + expect(rows.map((row) => row.type)).toContain(Bus.versionedType(SessionEvent.Tool.Failed.type, 2)) }), ) }) diff --git a/packages/core/test/session-wait.test.ts b/packages/core/test/session-wait.test.ts index b7c69c47800..b3861a29c3e 100644 --- a/packages/core/test/session-wait.test.ts +++ b/packages/core/test/session-wait.test.ts @@ -3,40 +3,40 @@ import { Effect, Layer } from "effect" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Job } from "@opencode-ai/core/job" import { Location } from "@opencode-ai/core/location" -import { ProjectV2 } from "@opencode-ai/core/project" +import { Project } from "@opencode-ai/core/project" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionStore } from "@opencode-ai/core/session/store" import { testEffect } from "./lib/effect" const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) -const awaited: SessionV2.ID[] = [] -const projects = Layer.mock(ProjectV2.Service, { - resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }), +const awaited: Session.ID[] = [] +const projects = Layer.mock(Project.Service, { + resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory }), }) const execution = Layer.mock(SessionExecution.Service, { awaitIdle: (sessionID) => Effect.sync(() => awaited.push(sessionID)), }) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]), + LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]), [ - [ProjectV2.node, projects], + [Project.node, projects], [SessionExecution.node, execution], ], ), ) -describe("SessionV2.wait", () => { +describe("Session.wait", () => { it.effect("delegates to SessionExecution.awaitIdle", () => Effect.gen(function* () { awaited.length = 0 - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const session = yield* sessions.create({ location }) yield* sessions.wait(session.id) diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index 937cd3e1f04..1e22da6c289 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -1,9 +1,7 @@ import { expect, test } from "bun:test" import { Schema } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" -import { ModelV2 } from "@opencode-ai/core/model" -import { SessionV2 } from "@opencode-ai/core/session" -import { Agent } from "@opencode-ai/schema/agent" +import { Agent } from "@opencode-ai/core/agent" +import { Session } from "@opencode-ai/core/session" import { Location } from "@opencode-ai/schema/location" import { Model } from "@opencode-ai/schema/model" import { Provider } from "@opencode-ai/schema/provider" @@ -11,7 +9,6 @@ import { Project } from "@opencode-ai/schema/project" import { ProjectDirectories } from "@opencode-ai/schema/project-directories" import { PermissionV1 } from "@opencode-ai/schema/permission-v1" import { Prompt } from "@opencode-ai/schema/prompt" -import { Session } from "@opencode-ai/schema/session" import { SessionPending } from "@opencode-ai/schema/session-pending" import { SessionMessage } from "@opencode-ai/schema/session-message" import { Workspace } from "@opencode-ai/schema/workspace" @@ -26,9 +23,10 @@ import { Pty } from "@opencode-ai/schema/pty" import { Reference } from "@opencode-ai/schema/reference" import { Skill } from "@opencode-ai/schema/skill" import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema" -import { ProviderV2 } from "@opencode-ai/core/provider" test("Core reuses the canonical shared schemas", async () => { + const schemaAgent = await import("@opencode-ai/schema/agent") + const schemaSession = await import("@opencode-ai/schema/session") const [ coreCommand, coreConnection, @@ -37,16 +35,17 @@ test("Core reuses the canonical shared schemas", async () => { coreIntegration, coreLocation, coreLLM, + coreModel, corePermission, corePermissionV1, coreProjectCopy, corePty, coreProject, + coreProvider, coreReference, coreSessionPending, coreSessionMessage, coreSkill, - coreV2Schema, coreSchema, coreWorkspace, ] = await Promise.all([ @@ -57,25 +56,26 @@ test("Core reuses the canonical shared schemas", async () => { import("@opencode-ai/core/integration"), import("@opencode-ai/core/location"), import("@opencode-ai/ai"), + import("@opencode-ai/core/model"), import("@opencode-ai/core/permission"), import("@opencode-ai/core/v1/permission"), import("@opencode-ai/core/project/copy"), import("@opencode-ai/core/pty"), import("@opencode-ai/core/project/schema"), + import("@opencode-ai/core/provider"), import("@opencode-ai/core/reference"), import("@opencode-ai/core/session/pending"), import("@opencode-ai/core/session/message"), import("@opencode-ai/core/skill"), - import("@opencode-ai/core/v2-schema"), import("@opencode-ai/core/schema"), import("@opencode-ai/core/workspace"), ]) const schemas = [ - [AgentV2.ID, Agent.ID], - [AgentV2.Name, Agent.Name], - [AgentV2.Color, Agent.Color], - [AgentV2.Info, Agent.Info], + [Agent.ID, schemaAgent.Agent.ID], + [Agent.Name, schemaAgent.Agent.Name], + [Agent.Color, schemaAgent.Agent.Color], + [Agent.Info, schemaAgent.Agent.Info], [coreCommand.Info, Command.Info], [coreConnection.CredentialInfo, Connection.CredentialInfo], [coreConnection.EnvInfo, Connection.EnvInfo], @@ -102,19 +102,16 @@ test("Core reuses the canonical shared schemas", async () => { [coreLocation.Ref, Location.Ref], [coreLLM.ProviderMetadata, LLM.ProviderMetadata], [coreLLM.FinishReason, LLM.FinishReason], - [coreLLM.ToolTextContent, LLM.ToolTextContent], - [coreLLM.ToolFileContent, LLM.ToolFileContent], - [coreLLM.ToolContent, LLM.ToolContent], - [ModelV2.ID, Model.ID], - [ModelV2.VariantID, Model.VariantID], - [ModelV2.Ref, Model.Ref], - [ModelV2.Family, Model.Family], - [ModelV2.Capabilities, Model.Capabilities], - [ModelV2.Cost, Model.Cost], - [ModelV2.Info, Model.Info], - [ProviderV2.ID, Provider.ID], - [ProviderV2.Request, Provider.Request], - [ProviderV2.Info, Provider.Info], + [coreModel.ID, Model.ID], + [coreModel.VariantID, Model.VariantID], + [coreModel.Ref, Model.Ref], + [coreModel.Family, Model.Family], + [coreModel.Capabilities, Model.Capabilities], + [coreModel.Cost, Model.Cost], + [coreModel.Info, Model.Info], + [coreProvider.ID, Provider.ID], + [coreProvider.Request, Provider.Request], + [coreProvider.Info, Provider.Info], [corePermission.Effect, Permission.Effect], [corePermission.Rule, Permission.Rule], [corePermission.Ruleset, Permission.Ruleset], @@ -130,9 +127,9 @@ test("Core reuses the canonical shared schemas", async () => { [coreReference.LocalSource, Reference.LocalSource], [coreReference.GitSource, Reference.GitSource], [coreReference.Source, Reference.Source], - [SessionV2.ID, Session.ID], - [SessionV2.Info, Session.Info], - [SessionV2.ListAnchor, Session.ListAnchor], + [Session.ID, schemaSession.Session.ID], + [Session.Info, schemaSession.Session.Info], + [Session.ListAnchor, schemaSession.Session.ListAnchor], [coreSessionPending.Delivery, SessionPending.Delivery], [coreSessionPending.Message, SessionPending.Message], [coreSessionPending.User, SessionPending.User], @@ -162,18 +159,17 @@ test("Core reuses the canonical shared schemas", async () => { [coreSkill.EmbeddedSource, Skill.EmbeddedSource], [coreSkill.Source, Skill.Source], [coreSkill.Info, Skill.Info], - [coreV2Schema.DateTimeUtcFromMillis, DateTimeUtcFromMillis], [coreSchema.optional, optional], [coreSchema.statics, statics], [coreWorkspace.ID, Workspace.ID], ] for (const [core, shared] of schemas) expect(core).toBe(shared) - expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test"))) - expect(Model.Info.default(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( - ModelV2.Info.default(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), + expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(Agent.Info.empty(Agent.ID.make("test"))) + expect(coreModel.Info.default(coreProvider.ID.make("test"), coreModel.ID.make("model"))).toEqual( + Model.Info.default(Provider.ID.make("test"), Model.ID.make("model")), ) - expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test"))) + expect(coreProvider.Info.empty(coreProvider.ID.make("test"))).toEqual(Provider.Info.empty(Provider.ID.make("test"))) expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe( "directory:/tmp", ) diff --git a/packages/core/test/skill.test.ts b/packages/core/test/skill.test.ts index 06df3dc8395..07e29ff3780 100644 --- a/packages/core/test/skill.test.ts +++ b/packages/core/test/skill.test.ts @@ -2,13 +2,13 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Deferred, Effect, Fiber, Layer, Stream } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" import { FileSystem } from "@opencode-ai/schema/filesystem" import { tmpdir } from "./fixture/tmpdir" @@ -26,7 +26,7 @@ const discovery = Layer.succeed( }), ) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([SkillV2.node, AgentV2.node, EventV2.node]), [[SkillDiscovery.node, discovery]]), + AppNodeBuilder.build(LayerNode.group([Skill.node, Agent.node, Bus.node]), [[SkillDiscovery.node, discovery]]), ) function write(directory: string, name: string, description: string) { @@ -42,9 +42,9 @@ description: ${description} function waitForSkillUpdate() { return Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const deferred = yield* Deferred.make() - const fiber = yield* events.subscribe(SkillV2.Event.Updated).pipe( + const fiber = yield* bus.subscribe(Skill.Event.Updated).pipe( Stream.runForEach(() => Deferred.succeed(deferred, undefined).pipe(Effect.asVoid)), Effect.forkScoped, ) @@ -53,10 +53,10 @@ function waitForSkillUpdate() { }) } -describe("SkillV2", () => { +describe("Skill", () => { it.live("publishes updates when skill sources change", () => Effect.gen(function* () { - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* Effect.acquireUseRelease( waitForSkillUpdate(), @@ -88,7 +88,7 @@ describe("SkillV2", () => { await fs.writeFile(path.join(first, "foo.md"), "---\nslash: true\n---\n# foo") }) - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => { editor.source({ type: "directory", path: AbsolutePath.make(first) }) editor.source({ type: "directory", path: AbsolutePath.make(first) }) @@ -104,16 +104,16 @@ describe("SkillV2", () => { { type: "directory", path: AbsolutePath.make(second) }, ]) expect(yield* skill.list()).toEqual([ - SkillV2.Info.make({ - id: SkillV2.ID.make("foo"), - name: SkillV2.Name.make("foo"), + Skill.Info.make({ + id: Skill.ID.make("foo"), + name: Skill.Name.make("foo"), slash: true, location: AbsolutePath.make(path.join(first, "foo.md")), content: "# foo", }), { - id: SkillV2.ID.make("review"), - name: SkillV2.Name.make("review"), + id: Skill.ID.make("review"), + name: Skill.Name.make("review"), description: "Second", location: AbsolutePath.make(path.join(second, "review", "SKILL.md")), content: "# review", @@ -138,20 +138,20 @@ describe("SkillV2", () => { pulls = 0 urls.set("https://example.test/skills/", [AbsolutePath.make(tmp.path)]) - const agents = yield* AgentV2.Service + const agents = yield* Agent.Service yield* agents.transform((editor) => - editor.update(AgentV2.ID.make("reviewer"), (agent) => { + editor.update(Agent.ID.make("reviewer"), (agent) => { agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" }) }), ) - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => editor.source({ type: "url", url: "https://example.test/skills/" })) - expect((yield* skill.list()).map((item) => item.name)).toEqual([SkillV2.Name.make("deploy")]) - expect((yield* skill.list()).map((item) => item.name)).toEqual([SkillV2.Name.make("deploy")]) + expect((yield* skill.list()).map((item) => item.name)).toEqual([Skill.Name.make("deploy")]) + expect((yield* skill.list()).map((item) => item.name)).toEqual([Skill.Name.make("deploy")]) expect(pulls).toBe(1) - expect(SkillV2.available(yield* skill.list(), (yield* agents.get(AgentV2.ID.make("reviewer")))!)).toEqual([]) + expect(Skill.available(yield* skill.list(), (yield* agents.get(Agent.ID.make("reviewer")))!)).toEqual([]) }), ), ), @@ -179,13 +179,13 @@ metadata: ) }) - const skill = yield* SkillV2.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => editor.source({ type: "directory", path: AbsolutePath.make(tmp.path) })) expect(yield* skill.list()).toEqual([ { - id: SkillV2.ID.make("manual"), - name: SkillV2.Name.make("manual"), + id: Skill.ID.make("manual"), + name: Skill.Name.make("manual"), description: "Manual only", slash: true, autoinvoke: false, @@ -210,8 +210,8 @@ metadata: await write(tmp.path, "deploy", "Initial deploy") }) - const events = yield* EventV2.Service - const skill = yield* SkillV2.Service + const bus = yield* Bus.Service + const skill = yield* Skill.Service yield* skill.transform((editor) => editor.source({ type: "directory", path: AbsolutePath.make(tmp.path) })) expect((yield* skill.list()).find((item) => item.name === "deploy")?.description).toBe("Initial deploy") @@ -223,7 +223,7 @@ metadata: yield* Effect.acquireUseRelease( waitForSkillUpdate(), ({ deferred }) => - events + bus .publish(FileSystem.Event.Changed, { file, event: "change" }) .pipe(Effect.andThen(Deferred.await(deferred)), Effect.timeout("1 second")), ({ fiber }) => Fiber.interrupt(fiber), diff --git a/packages/core/test/skill/instructions.test.ts b/packages/core/test/skill/instructions.test.ts index 9ca431bc6b5..a062c10f6ad 100644 --- a/packages/core/test/skill/instructions.test.ts +++ b/packages/core/test/skill/instructions.test.ts @@ -1,53 +1,53 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { SkillInstructions } from "@opencode-ai/core/skill/instructions" import { it } from "../lib/effect" import { readInitial, readUpdate } from "../lib/instructions" -const build = AgentV2.ID.make("build") -const effect = SkillV2.Info.make({ - id: SkillV2.ID.make("effect"), - name: SkillV2.Name.make("Effect"), +const build = Agent.ID.make("build") +const effect = Skill.Info.make({ + id: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), description: "Build applications with Effect", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Effect guidance", }) -const hidden = SkillV2.Info.make({ - id: SkillV2.ID.make("hidden"), - name: SkillV2.Name.make("Hidden"), +const hidden = Skill.Info.make({ + id: Skill.ID.make("hidden"), + name: Skill.Name.make("Hidden"), location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")), content: "Undescribed guidance", }) -const denied = SkillV2.Info.make({ - id: SkillV2.ID.make("denied"), - name: SkillV2.Name.make("Denied"), +const denied = Skill.Info.make({ + id: Skill.ID.make("denied"), + name: Skill.Name.make("Denied"), description: "Must not be advertised", location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")), content: "Denied guidance", }) -const manual = SkillV2.Info.make({ - id: SkillV2.ID.make("manual"), - name: SkillV2.Name.make("Manual"), +const manual = Skill.Info.make({ + id: Skill.ID.make("manual"), + name: Skill.Name.make("Manual"), description: "Load only when explicitly selected", autoinvoke: false, location: AbsolutePath.make(path.resolve("/skills/manual/SKILL.md")), content: "Manual guidance", }) -const layer = (list: () => SkillV2.Info[]) => +const layer = (list: () => Skill.Info[]) => AppNodeBuilder.build(SkillInstructions.node, [ - [SkillV2.node, Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })], + [Skill.node, Layer.mock(Skill.Service, { list: () => Effect.succeed(list()) })], ]) describe("SkillInstructions", () => { it.effect("renders described agent skills and updates the complete available list", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [{ action: "skill", resource: "denied", effect: "deny" }], }) let skills = [hidden, denied, manual, effect] @@ -80,10 +80,10 @@ describe("SkillInstructions", () => { }) it.effect("announces added and removed skills as deltas without restating the list", () => { - const agent = AgentV2.Info.make(AgentV2.Info.empty(build)) - const debugging = SkillV2.Info.make({ - id: SkillV2.ID.make("debugging"), - name: SkillV2.Name.make("Debugging"), + const agent = Agent.Info.make(Agent.Info.empty(build)) + const debugging = Skill.Info.make({ + id: Skill.ID.make("debugging"), + name: Skill.Name.make("Debugging"), description: "Diagnose hard bugs", location: AbsolutePath.make(path.resolve("/skills/debugging/SKILL.md")), content: "Debugging guidance", @@ -117,13 +117,13 @@ describe("SkillInstructions", () => { }) it.effect("restates the full skill list when a description changes", () => { - const agent = AgentV2.Info.make(AgentV2.Info.empty(build)) + const agent = Agent.Info.make(Agent.Info.empty(build)) let skills = [effect] return Effect.gen(function* () { const instructions = yield* SkillInstructions.Service const initialized = yield* instructions.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(readInitial)) - skills = [SkillV2.Info.make({ ...effect, description: "Build applications with Effect v4" })] + skills = [Skill.Info.make({ ...effect, description: "Build applications with Effect v4" })] expect( yield* instructions .load({ id: agent.id, info: agent }) @@ -137,8 +137,8 @@ describe("SkillInstructions", () => { }) it.effect("omits instructions when the selected agent denies all skills", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [{ action: "skill", resource: "*", effect: "deny" }], }) return Effect.gen(function* () { @@ -148,8 +148,8 @@ describe("SkillInstructions", () => { }) it.effect("omits instructions when a resource-specific denial follows the global denial", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "hidden", effect: "deny" }, @@ -162,8 +162,8 @@ describe("SkillInstructions", () => { }) it.effect("retains specifically allowed skills after a global denial", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "effect", effect: "allow" }, @@ -178,8 +178,8 @@ describe("SkillInstructions", () => { }) it.effect("omits instructions when a specifically allowed skill is denied again", () => { - const agent = AgentV2.Info.make({ - ...AgentV2.Info.empty(build), + const agent = Agent.Info.make({ + ...Agent.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, { action: "skill", resource: "effect", effect: "allow" }, diff --git a/packages/core/test/tool-edit.test.ts b/packages/core/test/tool-edit.test.ts index 8eaa9c3fbb8..a22ba21f393 100644 --- a/packages/core/test/tool-edit.test.ts +++ b/packages/core/test/tool-edit.test.ts @@ -8,12 +8,11 @@ import { FileMutation } from "@opencode-ai/core/file-mutation" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { EditTool } from "@opencode-ai/core/tool/edit" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { EditTool } from "@opencode-ai/core/tool/plugin/edit" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -23,25 +22,25 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const editToolNode = makeLocationNode({ name: "test/edit-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(EditTool.Plugin)), - deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], + deps: [Tool.node, LocationMutation.node, FileMutation.node, FSUtil.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_edit_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_edit_tool_test") +const assertions: Permission.AssertInput[] = [] const writes: string[] = [] let reads = 0 let denyAction: string | undefined let afterRead = (_target: string, _content: Uint8Array): Effect.Effect => Effect.void const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -90,19 +89,19 @@ const filesystem = Layer.effect( }), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) -const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { +const withTool = (directory: string, body: (registry: Tool.Interface) => Effect.Effect) => { const activeLocation = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) })), ) return Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( AppNodeBuilder.build( LayerNode.group([ - ToolRegistry.node, - ToolRegistry.toolsNode, + Tool.node, + Tool.node, LocationMutation.node, FileMutation.node, editToolNode, @@ -110,8 +109,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte [ [FSUtil.node, filesystem], [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], ], ), ), diff --git a/packages/core/test/tool-execute.test.ts b/packages/core/test/tool-execute.test.ts index 1175c2ea430..d679f317fe6 100644 --- a/packages/core/test/tool-execute.test.ts +++ b/packages/core/test/tool-execute.test.ts @@ -1,21 +1,26 @@ import { expect, test } from "bun:test" -import { ExecuteTool } from "@opencode-ai/core/tool/execute" -import { Tool } from "@opencode-ai/core/tool/tool" +import { CodeModeTool } from "@opencode-ai/core/codemode/tool" +import { Tool } from "@opencode-ai/core/tool" +import { execute } from "@opencode-ai/core/tool/runtime" import { Agent } from "@opencode-ai/schema/agent" import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "@opencode-ai/schema/session-message" -import { Deferred, Effect, Fiber, Schema } from "effect" +import type { Info } from "@opencode-ai/schema/tool" +import { Effect, Schema } from "effect" const context = { sessionID: Session.ID.make("ses_execute"), agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_execute"), - callID: "call_execute", + callID: Tool.CallID.make("call_execute"), progress: () => Effect.void, } +const createCodeMode = (tools: ReadonlyMap) => + CodeModeTool.create(tools, (_, tool, input, context) => execute(tool, input, context)) + test("execute describes invariant Code Mode behavior", () => { - expect(ExecuteTool.create(new Map()).description).toBe( + expect(createCodeMode(new Map()).description).toBe( [ "Run JavaScript to orchestrate tool calls and compose their results through `{ code }` in a confined Code Mode runtime.", "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.", @@ -28,109 +33,92 @@ test("execute describes invariant Code Mode behavior", () => { }) test("canonical execution distinguishes declared, model-only, and raw schema outputs", async () => { - const declared = Tool.make({ + const declared: Info = ({ + name: "declared", description: "Declared", input: Schema.Struct({ value: Schema.String }), output: Schema.Struct({ value: Schema.String }), execute: ({ value }) => Effect.succeed({ output: { value } }), }) - const modelOnly = Tool.make({ + const modelOnlyInput = Schema.Struct({}) + const modelOnly = ({ + name: "model_only", description: "Model only", - input: Schema.Struct({}), + input: modelOnlyInput, execute: () => Effect.succeed({ content: "visible only", metadata: { kind: "model" } }), - }) - const raw = Tool.make({ + }) satisfies Info + const raw: Info = ({ + name: "raw", description: "Raw", input: {}, output: {}, execute: (input) => Effect.succeed({ output: input, content: "raw" }), }) - expect(await Effect.runPromise(Tool.execute(declared, { value: "encoded" }, context))).toEqual({ + expect(await Effect.runPromise(execute(declared, { value: "encoded" }, context))).toEqual({ output: { value: "encoded" }, content: [{ type: "text", text: '{"value":"encoded"}' }], }) - expect(await Effect.runPromise(Tool.execute(modelOnly, {}, context))).toEqual({ + expect(await Effect.runPromise(execute(modelOnly, {}, context))).toEqual({ + output: undefined, content: [{ type: "text", text: "visible only" }], metadata: { kind: "model" }, }) - expect(await Effect.runPromise(Tool.execute(raw, { unchecked: true }, context))).toEqual({ + expect(await Effect.runPromise(execute(raw, { unchecked: true }, context))).toEqual({ output: { unchecked: true }, content: [{ type: "text", text: "raw" }], }) }) test("declared outputs cannot bypass validation and raw outputs stay JSON-compatible", async () => { - const missing: Tool.Any = { + const missing: Info = { + name: "missing", description: "Missing output", input: Schema.Struct({}), output: Schema.String, execute: () => Effect.succeed({ content: "not an output" }), } - const invalid: Tool.Any = { + const invalid: Info = { + name: "invalid", description: "Invalid raw output", input: {}, output: {}, execute: () => Effect.succeed({ output: 1n, content: "not JSON" }), } - expect((await Effect.runPromiseExit(Tool.execute(missing, {}, context))).toString()).toContain( + expect((await Effect.runPromiseExit(execute(missing, {}, context))).toString()).toContain( "Tool did not return its declared output", ) - expect((await Effect.runPromiseExit(Tool.execute(invalid, {}, context))).toString()).toContain( + expect((await Effect.runPromiseExit(execute(invalid, {}, context))).toString()).toContain( "Tool returned a non-JSON value", ) }) -test("execute preserves successful results with visible unhandled rejections", async () => { - const child = Tool.make({ - description: "Always fail", - input: Schema.Struct({}), - output: Schema.String, - execute: () => Effect.fail(new Tool.Failure({ message: "Lookup refused" })), - }) - const execute = ExecuteTool.create(new Map([["fail", { tool: child, name: "fail", permission: "fail" }]])) - const result = await Effect.runPromise(Tool.execute(execute, { code: `tools.fail({}); return "done"` }, context)) - - expect(result.metadata).toEqual({ toolCalls: [{ tool: "fail", status: "error" }] }) - expect(result.content).toEqual([ - { - type: "text", - text: [ - "done", - "", - "Warnings:", - "- [ToolFailure] Unhandled rejection from an un-awaited promise: Lookup refused", - ].join("\n"), - }, - ]) -}) - test("execute supports callable namespace tools", async () => { - const callable = Tool.make({ + const callable: Info = ({ + name: "admin", description: "Administer Slack", input: Schema.Struct({}), output: Schema.String, + options: { namespace: "slack" }, execute: () => Effect.succeed({ output: "admin" }), }) - const child = Tool.make({ + const child: Info = ({ + name: "create", description: "Create a Slack resource", input: Schema.Struct({}), output: Schema.String, + options: { namespace: "slack.admin" }, execute: () => Effect.succeed({ output: "created" }), }) - const execute = ExecuteTool.create( + const codeMode = createCodeMode( new Map([ - ["slack_admin", { tool: callable, name: "admin", namespace: "slack", permission: "slack_admin" }], - [ - "slack_admin_create", - { tool: child, name: "create", namespace: "slack.admin", permission: "slack_admin_create" }, - ], + ["slack_admin", callable], + ["slack_admin_create", child], ]), ) const result = await Effect.runPromise( - Tool.execute( - execute, + codeMode.execute( { code: "return [await tools.slack.admin({}), await tools.slack.admin.create({})]" }, context, ), @@ -144,46 +132,3 @@ test("execute supports callable namespace tools", async () => { }) expect(result.content).toEqual([{ type: "text", text: '[\n "admin",\n "created"\n]' }]) }) - -test("execute marks every admitted child call failed when interrupted", async () => { - const child = Tool.make({ - description: "Wait forever", - input: Schema.Struct({ id: Schema.Number }), - output: Schema.String, - execute: () => Effect.never, - }) - const execute = ExecuteTool.create(new Map([["wait", { tool: child, name: "wait", permission: "wait" }]])) - const updates: Tool.Metadata[] = [] - - await Effect.runPromise( - Effect.gen(function* () { - const started = yield* Deferred.make() - const fiber = yield* Tool.execute( - execute, - { code: "return await Promise.all([tools.wait({ id: 1 }), tools.wait({ id: 2 })])" }, - { - ...context, - progress: (update) => - Effect.gen(function* () { - updates.push(update) - if (updates.length > 1) return - yield* Deferred.succeed(started, undefined) - yield* Effect.never - }), - }, - ).pipe(Effect.forkChild) - yield* Deferred.await(started) - yield* Effect.yieldNow - yield* Effect.yieldNow - yield* Fiber.interrupt(fiber) - }), - ) - - expect(updates[0]).toEqual({ toolCalls: [{ tool: "wait", status: "running", input: { id: 1 } }] }) - expect(updates.at(-1)).toEqual({ - toolCalls: [ - { tool: "wait", status: "error", input: { id: 1 } }, - { tool: "wait", status: "error", input: { id: 2 } }, - ], - }) -}) diff --git a/packages/core/test/tool-output-store.test.ts b/packages/core/test/tool-output-store.test.ts deleted file mode 100644 index 2e22402e104..00000000000 --- a/packages/core/test/tool-output-store.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { describe, expect } from "bun:test" -import path from "path" -import { Cause, Effect, Exit, Fiber, Layer, Option } from "effect" -import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { FSUtil } from "@opencode-ai/util/fs-util" -import { Global } from "@opencode-ai/util/global" -import { Config } from "@opencode-ai/core/config" -import { ConfigToolOutput } from "@opencode-ai/core/config/tool-output" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { testEffect } from "./lib/effect" -import { tmpdir } from "./fixture/tmpdir" - -const sessionID = SessionV2.ID.make("ses_tool_output_store") - -const withStore = ( - body: (input: { root: string; store: ToolOutputStore.Interface; fs: FSUtil.Interface }) => Effect.Effect, - config?: Config.Info, -) => - Effect.acquireUseRelease( - Effect.promise(() => tmpdir()), - (tmp) => { - const global = Global.layerWith({ data: tmp.path }) - const configured = config - ? Layer.succeed( - Config.Service, - Config.Service.of({ - entries: () => Effect.succeed([new Config.Document({ type: "document", info: config })]), - }), - ) - : Layer.empty - - const store = AppNodeBuilder.build(LayerNode.group([ToolOutputStore.node, FSUtil.node]), [ - [Global.node, global], - [Config.node, configured], - ]) - return Effect.gen(function* () { - return yield* body({ root: tmp.path, store: yield* ToolOutputStore.Service, fs: yield* FSUtil.Service }) - }).pipe(Effect.provide(store)) - }, - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ) - -const it = testEffect(Layer.empty) - -describe("ToolOutputStore", () => { - it.live("bounds the provider-facing text channel with one managed file", () => - withStore(({ store, fs }) => - Effect.gen(function* () { - const first = "HEAD-" + "x".repeat(30_000) - const second = "y".repeat(30_000) + "-TAIL" - const result = yield* store.bound({ - sessionID, - callID: "call-aggregate", - content: [ - { type: "text", text: first }, - { type: "text", text: second }, - ], - }) - expect(result.outputPaths).toHaveLength(1) - expect(yield* fs.readFileString(result.outputPaths[0])).toBe(first + second) - if (result.content[0]?.type !== "text") throw new Error("expected text preview") - expect(Buffer.byteLength(result.content[0].text)).toBeLessThanOrEqual(ToolOutputStore.MAX_BYTES) - }), - ), - ) - - it.live("preserves native media without applying an execution media limit", () => - withStore(({ store }) => - Effect.gen(function* () { - const data = "a".repeat(6 * 1024 * 1024) - const result = yield* store.bound({ - sessionID, - callID: "call-file", - content: [{ type: "file", uri: `data:image/png;base64,${data}`, mime: "image/png", name: "pixel.png" }], - }) - expect(result.outputPaths).toEqual([]) - expect(result.content).toHaveLength(1) - expect(result.content[0]).toEqual({ - type: "file", - uri: `data:image/png;base64,${data}`, - mime: "image/png", - name: "pixel.png", - }) - }), - ), - ) - - it.live("preserves native media when bounding text", () => - withStore(({ store, fs }) => - Effect.gen(function* () { - const text = "x".repeat(ToolOutputStore.MAX_BYTES + 1) - const media = { - type: "file" as const, - uri: "data:image/png;base64,aGVsbG8=", - mime: "image/png", - name: "pixel.png", - } - const result = yield* store.bound({ - sessionID, - callID: "call-text-and-media", - content: [{ type: "text", text }, media], - }) - - expect(result.content[1]).toEqual(media) - expect(yield* fs.readFileString(result.outputPaths[0])).toBe(text) - }), - ), - ) - - it.live("returns content within the limits unchanged", () => - withStore(({ store }) => - Effect.gen(function* () { - const text = "x".repeat(30_000) - const content = [{ type: "text" as const, text }] - expect(yield* store.bound({ sessionID, callID: "call-duplicated", content })).toEqual({ - content, - outputPaths: [], - }) - }), - ), - ) - - it.live("fails oversized execution when complete retention cannot be written", () => - withStore(({ root, store, fs }) => - Effect.gen(function* () { - yield* fs.writeFileString(path.join(root, "tool-output"), "not a directory") - const exit = yield* store - .bound({ - sessionID, - callID: "call-lossy", - content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }], - }) - .pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) - expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))?._tag).toBe("ToolOutputStore.StorageError") - }), - ), - ) - - it.live("preserves interruption while retaining complete output", () => - Effect.gen(function* () { - const root = yield* Effect.promise(() => tmpdir()) - const blockedFilesystem = Layer.effect( - FSUtil.Service, - Effect.gen(function* () { - const fs = yield* FSUtil.Service - return FSUtil.Service.of({ - ...fs, - ensureDir: () => Effect.void, - writeFileString: () => Effect.never, - }) - }), - ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) - const store = AppNodeBuilder.build(ToolOutputStore.nodeWithoutConfig, [ - [Global.node, Global.layerWith({ data: root.path })], - [FSUtil.node, blockedFilesystem], - ]) - const exit = yield* Effect.gen(function* () { - const service = yield* ToolOutputStore.Service - const fiber = yield* service - .bound({ - sessionID, - callID: "call-interrupted", - content: [{ type: "text", text: "x".repeat(ToolOutputStore.MAX_BYTES + 1) }], - }) - .pipe(Effect.forkChild) - yield* Fiber.interrupt(fiber) - return yield* Fiber.await(fiber) - }).pipe(Effect.provide(store)) - expect(Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)).toBe(true) - yield* Effect.promise(() => root[Symbol.asyncDispose]()) - }), - ) - - it.live("honors configured limits", () => - withStore( - ({ store }) => - Effect.gen(function* () { - expect(yield* store.limits()).toEqual({ maxLines: 2, maxBytes: 1_000 }) - const result = yield* store.bound({ - sessionID, - callID: "call-config", - content: [{ type: "text", text: "one\ntwo\nthree" }], - }) - expect(result.outputPaths).toHaveLength(1) - }), - new Config.Info({ tool_output: new ConfigToolOutput.Info({ max_lines: 2, max_bytes: 1_000 }) }), - ), - ) - - it.live("cleans expired managed files and preserves unrelated files", () => - withStore(({ root, store, fs }) => - Effect.gen(function* () { - const old = path.join(root, "tool-output", "tool_old") - const recent = path.join(root, "tool-output", "tool_recent") - const unrelated = path.join(root, "tool-output", "keep.txt") - yield* fs.ensureDir(path.join(root, "tool-output")) - yield* fs.writeFileString(old, "old") - yield* fs.writeFileString(recent, "recent") - yield* fs.writeFileString(unrelated, "keep") - const expired = new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000) - yield* fs.utimes(old, expired, expired) - yield* store.cleanup() - expect(yield* fs.exists(old)).toBe(false) - expect(yield* fs.exists(recent)).toBe(true) - expect(yield* fs.exists(unrelated)).toBe(true) - }), - ), - ) -}) diff --git a/packages/core/test/tool-patch.test.ts b/packages/core/test/tool-patch.test.ts index e573edb19b4..471bf9e795c 100644 --- a/packages/core/test/tool-patch.test.ts +++ b/packages/core/test/tool-patch.test.ts @@ -7,12 +7,11 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { PatchTool } from "@opencode-ai/core/tool/patch" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { PatchTool } from "@opencode-ai/core/tool/plugin/patch" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -22,11 +21,11 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const patchToolNode = makeLocationNode({ name: "test/patch-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(PatchTool.Plugin)), - deps: [ToolRegistry.toolsNode, FSUtil.node, Location.node, PermissionV2.node], + deps: [Tool.node, FSUtil.node, Location.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_patch_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_patch_tool_test") +const assertions: Permission.AssertInput[] = [] let denyAction: string | undefined let failRemoveTarget: string | undefined let failRemoveErrorTarget: string | undefined @@ -36,8 +35,8 @@ let editApproved = false let afterEditApproval = (): Effect.Effect => Effect.void const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => { assertions.push(input) @@ -47,7 +46,7 @@ const permission = Layer.succeed( Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -120,7 +119,7 @@ const filesystem = Layer.effect( const withTool = ( directory: string, - body: (registry: ToolRegistry.Interface) => Effect.Effect, + body: (registry: Tool.Interface) => Effect.Effect, projectDirectory = directory, ) => { const activeLocation = Layer.succeed( @@ -130,14 +129,13 @@ const withTool = ( ), ) return Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, patchToolNode]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, patchToolNode]), [ [FSUtil.node, filesystem], [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], ]), ), ) @@ -157,7 +155,7 @@ const exists = (target: string) => ), ) const it = testEffect(Layer.empty) -const withTempTool = (body: (directory: string, registry: ToolRegistry.Interface) => Effect.Effect) => +const withTempTool = (body: (directory: string, registry: Tool.Interface) => Effect.Effect) => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => { @@ -196,7 +194,7 @@ describe("PatchTool", () => { text: "Success. Updated the following files:\nA nested/new.txt\nM update.txt\nD remove.txt", }, ]) - const modelText = settled.content[0]?.type === "text" ? settled.content[0].text : "" + const modelText = settled.content?.[0]?.type === "text" ? settled.content[0].text : "" if (process.platform === "win32") expect(modelText).not.toContain("\\") expect(settled.output).toMatchObject({ applied: [ diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index f63e7f6d2d7..8cbda1118d5 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -3,19 +3,18 @@ import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Form } from "@opencode-ai/core/form" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { QuestionTool } from "@opencode-ai/core/tool/question" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Permission } from "@opencode-ai/core/permission" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { QuestionTool } from "@opencode-ai/core/tool/plugin/question" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" import { imagePassthrough } from "./lib/image" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" -const sessionID = SessionV2.ID.make("ses_question_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_question_tool_test") +const assertions: Permission.AssertInput[] = [] let captured: Form.CreateInput | undefined let reject = false let deny = false @@ -30,14 +29,14 @@ const questionInput = { ], } const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( deny ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -78,14 +77,13 @@ const form = Layer.succeed( const questionToolNode = makeLocationNode({ name: "test/question-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(QuestionTool.Plugin)), - deps: [ToolRegistry.toolsNode, PermissionV2.node, Form.node], + deps: [Tool.node, Permission.node, Form.node], }) const it = testEffect( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, questionToolNode]), [ - [PermissionV2.node, permission], + AppNodeBuilder.build(LayerNode.group([Tool.node, questionToolNode]), [ + [Permission.node, permission], [Form.node, form], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [Image.node, imagePassthrough], ]), ) @@ -95,7 +93,7 @@ describe("QuestionTool", () => { Effect.gen(function* () { captured = undefined deny = true - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( (yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).map( @@ -126,7 +124,7 @@ describe("QuestionTool", () => { captured = undefined reject = false deny = false - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const questions = [ { question: "What should happen?", @@ -204,7 +202,7 @@ describe("QuestionTool", () => { captured = undefined reject = false deny = false - const registryService = yield* ToolRegistry.Service + const registryService = yield* Tool.Service yield* executeTool(registryService, { sessionID, @@ -234,7 +232,7 @@ describe("QuestionTool", () => { captured = undefined reject = true deny = false - const registryService = yield* ToolRegistry.Service + const registryService = yield* Tool.Service const fiber = yield* executeTool(registryService, { sessionID, ...toolIdentity, diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index a176b3b0c87..3e8427c447b 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -9,15 +9,14 @@ import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { Image } from "@opencode-ai/core/image" -import { PermissionV2 } from "@opencode-ai/core/permission" -import { SessionV2 } from "@opencode-ai/core/session" +import { Permission } from "@opencode-ai/core/permission" +import { Session } from "@opencode-ai/core/session" import { AbsolutePath } from "@opencode-ai/core/schema" import { Global } from "@opencode-ai/util/global" import { LocationMutation } from "@opencode-ai/core/location-mutation" import { location } from "./fixture/location" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { ReadTool } from "@opencode-ai/core/tool/read" +import { Tool } from "@opencode-ai/core/tool" +import { ReadTool } from "@opencode-ai/core/tool/plugin/read" import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { SessionInstructions } from "@opencode-ai/core/session/instructions" @@ -28,18 +27,18 @@ const readToolNode = makeLocationNode({ name: "test/read-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)), deps: [ - ToolRegistry.toolsNode, + Tool.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, - PermissionV2.node, + Permission.node, SessionInstructions.node, FSUtil.node, Location.node, ], }) -const assertions: PermissionV2.AssertInput[] = [] +const assertions: Permission.AssertInput[] = [] const missingPath = "__missing_read_target__.txt" const missingAbsolutePath = path.join(process.cwd(), missingPath) const readCalls: { @@ -76,8 +75,8 @@ const reader = Layer.succeed( ) let allow = true const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => { assertions.push(input) @@ -86,7 +85,7 @@ const permission = Layer.succeed( allow ? Effect.void : Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -159,20 +158,19 @@ const unavailableImage = Layer.succeed( Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }), ) const readLayer = (imageLayer: Layer.Layer) => - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, readToolNode]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, readToolNode]), [ [ReadToolFileSystem.node, reader], - [PermissionV2.node, permission], + [Permission.node, permission], [Config.node, config], [Image.node, imageLayer], [LocationMutation.node, mutation], [FSUtil.node, testFileSystem], [Location.node, locationLayer], [Global.node, Global.layerWith({ data: Global.Path.data })], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ]) const it = testEffect(readLayer(imageLayer)) const itWithoutResizer = testEffect(readLayer(unavailableImage)) -const sessionID = SessionV2.ID.make("ses_read_tool_test") +const sessionID = Session.ID.make("ses_read_tool_test") describe("ReadTool", () => { beforeEach(() => { @@ -195,7 +193,7 @@ describe("ReadTool", () => { it.effect("registers, authorizes, and reads through the location filesystem", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["read", "execute"]) expect( @@ -229,7 +227,7 @@ describe("ReadTool", () => { it.effect("asks for external_directory approval before reading an external absolute path", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const external = path.join(path.parse(process.cwd()).root, "external-read", "notes.txt") expect( @@ -261,7 +259,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const execution = yield* executeTool(registry, { sessionID, @@ -313,7 +311,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const settled = yield* executeTool(registry, { sessionID, @@ -321,7 +319,6 @@ describe("ReadTool", () => { call: { type: "tool-call", id: "call-large-image", name: "read", input: { path: "large.png" } }, }) - expect(settled.outputPaths).toBeUndefined() expect(settled.status).toBe("completed") if (settled.status !== "completed") return expect(settled.output).toMatchObject({ @@ -347,7 +344,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -371,7 +368,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -412,7 +409,7 @@ describe("ReadTool", () => { }), }), ] - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -451,7 +448,7 @@ describe("ReadTool", () => { }), }), ] - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const result = yield* executeTool(registry, { sessionID, ...toolIdentity, @@ -460,7 +457,7 @@ describe("ReadTool", () => { expect(result.status).toBe("completed") if (result.status !== "completed") return - const media = result.content[1] + const media = result.content?.[1] expect(media?.type).toBe("file") if (media?.type !== "file") return const resized = photon.PhotonImage.new_from_byteslice(Buffer.from(media.uri.split(",")[1] ?? "", "base64")) @@ -490,7 +487,7 @@ describe("ReadTool", () => { }), }), ] - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -518,7 +515,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "image/png", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -536,7 +533,7 @@ describe("ReadTool", () => { it.effect("returns expected filesystem failures to the model", () => Effect.gen(function* () { readFailure = new ReadToolFileSystem.BinaryFileError({ resource: "archive.dat" }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -559,7 +556,7 @@ describe("ReadTool", () => { it.effect("preserves unexpected filesystem defects", () => Effect.gen(function* () { resolveFailure = new Error("unexpected") - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( Exit.isFailure( @@ -576,7 +573,7 @@ describe("ReadTool", () => { it.effect("does not read when permission is denied", () => Effect.gen(function* () { allow = false - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -591,7 +588,7 @@ describe("ReadTool", () => { it.effect("returns missing paths as model-visible tool failures", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -610,7 +607,7 @@ describe("ReadTool", () => { it.effect("lists a bounded directory page through read", () => Effect.gen(function* () { resolvedType = "directory" - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -633,7 +630,7 @@ describe("ReadTool", () => { Effect.gen(function* () { allow = false resolvedType = "directory" - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -648,7 +645,7 @@ describe("ReadTool", () => { it.effect("preserves unexpected resolution defects", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service resolveFailure = new Error("missing") expect( @@ -675,7 +672,7 @@ describe("ReadTool", () => { truncated: true, next: 3, }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -707,7 +704,7 @@ describe("ReadTool", () => { encoding: "base64", mime: "application/octet-stream", } - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { diff --git a/packages/plugin/test/tool.test.ts b/packages/core/test/tool-schema.test.ts similarity index 68% rename from packages/plugin/test/tool.test.ts rename to packages/core/test/tool-schema.test.ts index 19017cab20e..72e053ab8bb 100644 --- a/packages/plugin/test/tool.test.ts +++ b/packages/core/test/tool-schema.test.ts @@ -1,18 +1,20 @@ import { expect, test } from "bun:test" import { Effect, Schema } from "effect" -import * as Tool from "../src/v2/effect/tool" +import type { Info } from "@opencode-ai/schema/tool" +import { Tool } from "../src/tool" +import { definition, execute } from "../src/tool/runtime" -test("tools remain valid across separate module instances", async () => { - const ForeignTool = await import(`${new URL("../src/v2/effect/tool.ts", import.meta.url).href}?foreign`) +test("tools are structural values", async () => { const config = { + name: "foreign", description: "Foreign tool", input: Schema.Struct({ value: Schema.String }), output: Schema.Struct({ ok: Schema.Boolean }), execute: () => Effect.succeed({ output: { ok: true } }), } - const tool = ForeignTool.make(config) + const tool: Info = config - expect(Tool.toLLMDefinition("foreign", tool)).toEqual({ + expect(definition(tool)).toEqual({ name: "foreign", description: "Foreign tool", inputSchema: { @@ -28,15 +30,14 @@ test("tools remain valid across separate module instances", async () => { additionalProperties: false, }, }) - expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: "input" }))).toEqual({ value: "input" }) }) test("portable schemas validate and describe typed tools", async () => { - const input: Tool.StandardSchemaType<{ count: string }, { count: number }> = { + const input = { "~standard": { version: 1, vendor: "test", - validate: (value) => { + validate: (value: unknown) => { if (typeof value !== "object" || value === null || !("count" in value) || typeof value.count !== "string") return { issues: [{ message: "count must be numeric" }] } const count = Number(value.count) @@ -48,41 +49,41 @@ test("portable schemas validate and describe typed tools", async () => { }, }, } - const output: Tool.StandardSchemaType = { + const output = { "~standard": { version: 1, vendor: "test", - validate: (value) => ({ value: String(value) }), + validate: (value: unknown) => ({ value: String(value) }), jsonSchema: { input: () => ({ type: "number" }), output: () => ({ type: "string" }), }, }, } - const tool = Tool.make({ + const tool: Info = ({ + name: "portable", description: "Portable tool", input, output, execute: ({ count }) => Effect.succeed({ output: count + 1 }), }) - expect(Tool.toLLMDefinition("portable", tool)).toEqual({ + expect(definition(tool)).toEqual({ name: "portable", description: "Portable tool", inputSchema: { type: "object", properties: { count: { type: "string" } } }, outputSchema: { type: "string" }, }) - const decoded = await Effect.runPromise(Tool.decodeInput(tool.input, { count: "41" })) - expect(decoded).toEqual({ count: 41 }) - expect(await Effect.runPromise(Tool.encodeOutput(tool.output, 42))).toBe("42") + const result = await Effect.runPromise(execute(tool, { count: "41" }, {} as Tool.Context)) + expect(result.output).toBe("42") }) test("portable schema failures become tool failures", async () => { - const input: Tool.StandardSchemaType = { + const input = { "~standard": { version: 1, vendor: "test", - validate: () => ({ issues: [{ message: "expected a string" }] }), + validate: (_value: unknown) => ({ issues: [{ message: "expected a string" }] }), jsonSchema: { input: () => ({ type: "string" }), output: () => ({ type: "string" }), @@ -90,14 +91,26 @@ test("portable schema failures become tool failures", async () => { }, } - const error = await Effect.runPromiseExit(Tool.decodeInput(input, 1)) + const error = await Effect.runPromiseExit( + execute( + { + name: "invalid", + description: "Invalid", + input, + execute: () => Effect.succeed({ content: "unused" }), + }, + 1, + {} as Tool.Context, + ), + ) expect(error.toString()).toContain("Invalid tool input: expected a string") }) test("canonical results carry metadata with typed output", async () => { const input = Schema.Struct({ value: Schema.String }) const output = Schema.Struct({ value: Schema.String, internal: Schema.Boolean }) - const tool = Tool.make({ + const tool: Info = ({ + name: "annotated", description: "Annotated tool", input, output, @@ -112,16 +125,21 @@ test("canonical results carry metadata with typed output", async () => { }) test("raw JSON schemas are render-only and omitted output means model-only", async () => { - const tool = Tool.make({ + const input = { type: "object", properties: { value: { type: "string" } } } + const tool: Info = ({ + name: "raw", description: "Raw tool", - input: { type: "object", properties: { value: { type: "string" } } }, + input, execute: (input) => Effect.succeed({ content: JSON.stringify(input) }), }) - expect(Tool.toLLMDefinition("raw", tool)).toEqual({ + expect(definition(tool)).toEqual({ name: "raw", description: "Raw tool", inputSchema: { type: "object", properties: { value: { type: "string" } } }, }) - expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: 1 }))).toEqual({ value: 1 }) + expect(await Effect.runPromise(execute(tool, { value: 1 }, {} as Tool.Context))).toEqual({ + output: undefined, + content: [{ type: "text", text: '{"value":1}' }], + }) }) diff --git a/packages/core/test/tool-search.test.ts b/packages/core/test/tool-search.test.ts index 8b688a23ffd..abe59a75dfb 100644 --- a/packages/core/test/tool-search.test.ts +++ b/packages/core/test/tool-search.test.ts @@ -9,14 +9,13 @@ import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { GlobTool } from "@opencode-ai/core/tool/glob" -import { GrepTool } from "@opencode-ai/core/tool/grep" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Session } from "@opencode-ai/core/session" +import { GlobTool } from "@opencode-ai/core/tool/plugin/glob" +import { GrepTool } from "@opencode-ai/core/tool/plugin/grep" +import { Tool } from "@opencode-ai/core/tool" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" @@ -26,47 +25,40 @@ const globToolNode = makeLocationNode({ name: "test/glob-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GlobTool.Plugin)), deps: [ - ToolRegistry.toolsNode, + Tool.node, FSUtil.node, Ripgrep.node, Location.node, LocationMutation.node, - PermissionV2.node, + Permission.node, ], }) const grepToolNode = makeLocationNode({ name: "test/grep-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(GrepTool.Plugin)), - deps: [ - ToolRegistry.toolsNode, - FSUtil.node, - Ripgrep.node, - Location.node, - LocationMutation.node, - PermissionV2.node, - ], + deps: [Tool.node, FSUtil.node, Ripgrep.node, Location.node, LocationMutation.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_search_tool_test") +const sessionID = Session.ID.make("ses_search_tool_test") const withTools = ( directory: string, - body: (registry: ToolRegistry.Interface) => Effect.Effect, - assertions?: PermissionV2.AssertInput[], + body: (registry: Tool.Interface) => Effect.Effect, + assertions?: Permission.AssertInput[], ) => Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, globToolNode, grepToolNode]), [ + AppNodeBuilder.build(LayerNode.group([Tool.node, globToolNode, grepToolNode]), [ [ Location.node, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), ], [ - PermissionV2.node, + Permission.node, Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => { assertions?.push(input) @@ -79,7 +71,6 @@ const withTools = ( }), ), ], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], ]), ), ) @@ -236,7 +227,7 @@ describe("search tools", () => { Effect.acquireUseRelease( Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), ([active, outside]) => { - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "needle\n")).pipe( Effect.andThen( withTools( @@ -327,7 +318,7 @@ describe("search tools", () => { Effect.acquireUseRelease( Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), ([active, outside]) => { - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] return Effect.promise(() => fs.writeFile(path.join(outside.path, "outside.txt"), "outside\n")).pipe( Effect.andThen( withTools( @@ -359,7 +350,7 @@ describe("search tools", () => { Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), ([active, outside]) => { if (process.platform === "win32") return Effect.void - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] return Effect.promise(async () => { await fs.writeFile(path.join(outside.path, "outside.txt"), "outside\n") await fs.symlink(outside.path, path.join(active.path, "linked")) diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 69af78d5693..b64058a65b8 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -9,48 +9,47 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { filesystem } from "@opencode-ai/util/effect/app-node-platform" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Job } from "@opencode-ai/core/job" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionStore } from "@opencode-ai/core/session/store" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { Shell } from "@opencode-ai/core/shell" import { Shell as ShellSchema } from "@opencode-ai/schema/shell" -import { ShellTool } from "@opencode-ai/core/tool/shell" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { ShellTool } from "@opencode-ai/core/tool/plugin/shell" +import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { toolIdentity, executeTool, toolDefinitions, waitForTool } from "./lib/tool" -const sessionID = SessionV2.ID.make("ses_shell_tool_test") -const sessionModel = ModelV2.Ref.make({ id: ModelV2.ID.make("test"), providerID: ProviderV2.ID.make("test") }) -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_shell_tool_test") +const sessionModel = Model.Ref.make({ id: Model.ID.make("test"), providerID: Provider.ID.make("test") }) +const assertions: Permission.AssertInput[] = [] let denyAction: string | undefined -let afterPermission = (_input: PermissionV2.AssertInput): Effect.Effect => Effect.void +let afterPermission = (_input: Permission.AssertInput): Effect.Effect => Effect.void const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen(Effect.suspend(() => afterPermission(input))), Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -78,30 +77,30 @@ const executionNode = makeGlobalNode({ layer: Layer.effect( SessionExecution.Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service - const complete = Effect.fn("ShellTest.complete")(function* (id: SessionV2.ID) { + const complete = Effect.fn("ShellTest.complete")(function* (id: Session.ID) { const session = yield* store.get(id) if (!session) return const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID: id, assistantMessageID, - agent: session.agent ?? AgentV2.ID.make("code"), + agent: session.agent ?? Agent.ID.make("code"), model: sessionModel, }) - yield* events.publish(SessionEvent.Text.Started, { + yield* bus.publish(SessionEvent.Text.Started, { sessionID: id, assistantMessageID, ordinal: 0, }) - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID: id, assistantMessageID, ordinal: 0, text: "ok", }) - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID: id, assistantMessageID, finish: "stop", @@ -118,16 +117,15 @@ const executionNode = makeGlobalNode({ }) }), ), - deps: [EventV2.node, SessionStore.node], + deps: [Bus.node, SessionStore.node], }) const layer = AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, Job.node, - ToolOutputStore.cleanupNode, - SessionV2.node, + Session.node, SessionExecution.node, PluginRuntime.providerNode, LocationServiceMap.node, @@ -137,7 +135,7 @@ const layer = AppNodeBuilder.build( ]), [ [SessionExecution.node, executionNode], - [PermissionV2.node, permission], + [Permission.node, permission], ], ) @@ -174,9 +172,9 @@ const progressOverflowCommand = (bytes: number, release: string) => ? `[Console]::Out.Write(('x' * ${bytes})); while (!(Test-Path -LiteralPath '${release}')) { Start-Sleep -Milliseconds 50 }` : `head -c ${bytes} /dev/zero | tr '\\0' 'x'; while [ ! -e '${release}' ]; do sleep 0.05; done` -const withSession = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => +const withSession = (directory: string, body: (registry: Tool.Interface) => Effect.Effect) => Effect.gen(function* () { - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const location = Location.Ref.make({ directory: AbsolutePath.make(directory) }) yield* sessions.create({ id: sessionID, @@ -187,7 +185,7 @@ const withSession = (directory: string, body: (registry: ToolRegistry.I const locations = yield* LocationServiceMap.Service const locationLayer = locations.get(location) return yield* Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service yield* waitForTool(registry, ShellTool.name) return yield* body(registry) }).pipe(Effect.provide(locationLayer), Effect.ensuring(locations.invalidate(location))) @@ -466,7 +464,7 @@ describe("ShellTool", () => { reset() return withSession(tmp.path, (registry) => Effect.gen(function* () { - const updates: ToolRegistry.Progress[] = [] + const updates: Tool.Metadata[] = [] yield* executeTool(registry, { ...call({ command: steadyProgressCommand }, "call-steady-progress"), progress: (update) => Effect.sync(() => updates.push(update)), @@ -511,8 +509,8 @@ describe("ShellTool", () => { reset() return withSession(tmp.path, (registry) => Effect.gen(function* () { - const events = yield* EventV2.Service - const admitted = yield* events.subscribe(SessionEvent.InputAdmitted).pipe( + const bus = yield* Bus.Service + const admitted = yield* bus.subscribe(SessionEvent.InputAdmitted).pipe( Stream.filter((event) => event.data.sessionID === sessionID && event.data.input.type === "synthetic"), Stream.runHead, Effect.forkScoped({ startImmediately: true }), diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index eab58f75d71..842b5ea6aae 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -4,13 +4,12 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { SkillV2 } from "@opencode-ai/core/skill" -import { SkillTool } from "@opencode-ai/core/tool/skill" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Session } from "@opencode-ai/core/session" +import { Skill } from "@opencode-ai/core/skill" +import { SkillTool } from "@opencode-ai/core/tool/plugin/skill" +import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { Image } from "@opencode-ai/core/image" import { it } from "./lib/effect" @@ -22,10 +21,10 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const skillToolNode = makeLocationNode({ name: "test/skill-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(SkillTool.Plugin)), - deps: [ToolRegistry.toolsNode, FSUtil.node, SkillV2.node, PermissionV2.node], + deps: [Tool.node, FSUtil.node, Skill.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_skill_tool_test") +const sessionID = Session.ID.make("ses_skill_tool_test") describe("SkillTool", () => { it.live("lists available skills, authorizes the selected ID, and loads model-facing content", () => @@ -43,25 +42,25 @@ describe("SkillTool", () => { Promise.all([fs.writeFile(location, "unused"), fs.writeFile(reference, "reference")]), ) - const info: SkillV2.Info = { - id: SkillV2.ID.make("effect"), - name: SkillV2.Name.make("Effect"), + const info: Skill.Info = { + id: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), description: "Use Effect", location: AbsolutePath.make(location), content: "# Effect\n\nGuidance", } let current = [info] - const assertions: PermissionV2.AssertInput[] = [] + const assertions: Permission.AssertInput[] = [] let deny = false const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( deny ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -78,8 +77,8 @@ describe("SkillTool", () => { }), ) const skills = Layer.succeed( - SkillV2.Service, - SkillV2.Service.of({ + Skill.Service, + Skill.Service.of({ transform: (_transform) => Effect.die("unused"), reload: () => Effect.die("unused"), sources: () => Effect.die("unused"), @@ -87,17 +86,16 @@ describe("SkillTool", () => { }), ) const skillToolLayer = AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, skillToolNode]), + LayerNode.group([Tool.node, skillToolNode]), [ - [PermissionV2.node, permission], - [SkillV2.node, skills], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], + [Skill.node, skills], [Image.node, imagePassthrough], ], ) return yield* Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect((yield* toolDefinitions(registry))[0]).toMatchObject({ name: "skill", description: SkillTool.description, @@ -151,9 +149,9 @@ describe("SkillTool", () => { error: { type: "permission.rejected", message: "Permission denied: skill" }, }) deny = false - const flat = SkillV2.Info.make({ - id: SkillV2.ID.make("public"), - name: SkillV2.Name.make("Public"), + const flat = Skill.Info.make({ + id: Skill.ID.make("public"), + name: Skill.Name.make("Public"), description: "Public guidance", location: AbsolutePath.make(path.join(tmp.path, "public.md")), content: "Public", diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index 03705eee217..73299769f6b 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -6,15 +6,15 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProviderV2 } from "@opencode-ai/core/provider" +import { Model } from "@opencode-ai/core/model" +import { Provider } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Job } from "@opencode-ai/core/job" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionExecution } from "@opencode-ai/core/session/execution" import { SessionPending } from "@opencode-ai/core/session/pending" @@ -23,30 +23,29 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionStore } from "@opencode-ai/core/session/store" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" -import { SubagentTool } from "@opencode-ai/core/tool/subagent" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { SubagentTool } from "@opencode-ai/core/tool/plugin/subagent" +import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" import { executeTool, toolIdentity, waitForTool } from "./lib/tool" const childText = "child final response" -const childModel = ModelV2.Ref.make({ id: ModelV2.ID.make("child"), providerID: ProviderV2.ID.make("test") }) -const parentModel = ModelV2.Ref.make({ id: ModelV2.ID.make("parent"), providerID: ProviderV2.ID.make("test") }) +const childModel = Model.Ref.make({ id: Model.ID.make("child"), providerID: Provider.ID.make("test") }) +const parentModel = Model.Ref.make({ id: Model.ID.make("parent"), providerID: Provider.ID.make("test") }) const tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } const outputSessionID = (value: unknown) => - Schema.decodeUnknownSync(Schema.Struct({ sessionID: SessionV2.ID }))(value).sessionID + Schema.decodeUnknownSync(Schema.Struct({ sessionID: Session.ID }))(value).sessionID const executionNode = makeGlobalNode({ service: SessionExecution.Service, layer: Layer.effect( SessionExecution.Service, Effect.gen(function* () { - const events = yield* EventV2.Service + const bus = yield* Bus.Service const store = yield* SessionStore.Service - const completed = new Set() - const complete = Effect.fn("SubagentTest.complete")(function* (sessionID: SessionV2.ID) { + const completed = new Set() + const complete = Effect.fn("SubagentTest.complete")(function* (sessionID: Session.ID) { if (completed.has(sessionID)) return if ((yield* store.get(sessionID))?.title.includes("fail")) { yield* new SessionRunnerModel.ModelNotSelectedError({ sessionID }) @@ -54,24 +53,24 @@ const executionNode = makeGlobalNode({ } completed.add(sessionID) const assistantMessageID = SessionMessage.ID.create() - yield* events.publish(SessionEvent.Step.Started, { + yield* bus.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: AgentV2.ID.make("reviewer"), + agent: Agent.ID.make("reviewer"), model: childModel, }) - yield* events.publish(SessionEvent.Text.Started, { + yield* bus.publish(SessionEvent.Text.Started, { sessionID, assistantMessageID, ordinal: 0, }) - yield* events.publish(SessionEvent.Text.Ended, { + yield* bus.publish(SessionEvent.Text.Ended, { sessionID, assistantMessageID, ordinal: 0, text: childText, }) - yield* events.publish(SessionEvent.Step.Ended, { + yield* bus.publish(SessionEvent.Step.Ended, { sessionID, assistantMessageID, finish: "stop", @@ -88,16 +87,15 @@ const executionNode = makeGlobalNode({ }) }), ), - deps: [EventV2.node, SessionStore.node], + deps: [Bus.node, SessionStore.node], }) const layer = AppNodeBuilder.build( LayerNode.group([ Database.node, - EventV2.node, + Bus.node, Job.node, - ToolOutputStore.cleanupNode, - SessionV2.node, + Session.node, SessionExecution.node, PluginRuntime.providerNode, LocationServiceMap.node, @@ -111,21 +109,21 @@ const withSubagent = (location: Location.Ref) => Effect.gen(function* () { const locations = yield* LocationServiceMap.Service yield* PluginSupervisor.Service.use((supervisor) => supervisor.flush).pipe(Effect.provide(locations.get(location))) - yield* AgentV2.Service.use((agents) => + yield* Agent.Service.use((agents) => agents.transform((draft) => { // The caller identity used by executeTool; subagent permission asserts against it. draft.update(toolIdentity.agent, (agent) => { agent.mode = "primary" agent.permissions.push({ action: "*", resource: "*", effect: "allow" }) }) - draft.update(AgentV2.ID.make("reviewer"), (agent) => { + draft.update(Agent.ID.make("reviewer"), (agent) => { agent.mode = "subagent" agent.model = childModel }) - draft.update(AgentV2.ID.make("fallback"), (agent) => { + draft.update(Agent.ID.make("fallback"), (agent) => { agent.mode = "subagent" }) - draft.update(AgentV2.ID.make("primary"), (agent) => { + draft.update(Agent.ID.make("primary"), (agent) => { agent.mode = "primary" }) }), @@ -141,12 +139,12 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const session = yield* SessionV2.Service + const session = yield* Session.Service const parent = yield* session.create({ location }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) expect((yield* registry.snapshot()).definitions.map((tool) => tool.name)).toContain(SubagentTool.name) expect( @@ -177,12 +175,12 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const root = yield* sessions.create({ location }) const parent = yield* sessions.create({ parentID: root.id, title: "parent" }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) expect( @@ -220,12 +218,12 @@ describe("SubagentTool", () => { Bun.write(path.join(dir.path, "opencode.json"), JSON.stringify({ experimental: { subagent_depth: 2 } })), ) const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const root = yield* sessions.create({ location }) const parent = yield* sessions.create({ parentID: root.id, title: "parent", model: parentModel }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) const settled = yield* executeTool(registry, { @@ -262,13 +260,13 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const parent = yield* sessions.create({ location, model: parentModel }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) - const progress: ToolRegistry.Progress[] = [] + const progress: Tool.Metadata[] = [] const settled = yield* executeTool(registry, { sessionID: parent.id, @@ -325,11 +323,11 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const parent = yield* sessions.create({ location }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) expect( @@ -363,14 +361,14 @@ describe("SubagentTool", () => { Effect.flatMap((dir) => Effect.gen(function* () { const location = Location.Ref.make({ directory: AbsolutePath.make(dir.path) }) - const sessions = yield* SessionV2.Service + const sessions = yield* Session.Service const parent = yield* sessions.create({ location }) yield* withSubagent(parent.location) const locations = yield* LocationServiceMap.Service - const registry = yield* ToolRegistry.Service.pipe(Effect.provide(locations.get(parent.location))) + const registry = yield* Tool.Service.pipe(Effect.provide(locations.get(parent.location))) yield* waitForTool(registry, SubagentTool.name) - const events = yield* EventV2.Service - const admitted = yield* events.subscribe(SessionEvent.InputAdmitted).pipe( + const bus = yield* Bus.Service + const admitted = yield* bus.subscribe(SessionEvent.InputAdmitted).pipe( Stream.filter((event) => event.data.sessionID === parent.id && event.data.input.type === "synthetic"), Stream.take(1), Stream.runCollect, @@ -406,7 +404,7 @@ describe("SubagentTool", () => { }, }) const database = yield* Database.Service - yield* SessionPending.promote(database.db, events, parent.id, "steer") + yield* SessionPending.promote(database.db, bus, parent.id, "steer") const synthetic = (yield* sessions.context(parent.id)).filter((message) => message.type === "synthetic") expect(synthetic).toHaveLength(1) expect(synthetic[0]?.text).toContain(` }> = [] -const assertions: PermissionV2.AssertInput[] = [] +const assertions: Permission.AssertInput[] = [] let respond = (_request: HttpClientRequest.HttpClientRequest) => Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } })) @@ -38,8 +37,8 @@ const http = Layer.succeed( ), ) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -49,9 +48,8 @@ const permission = Layer.succeed( }), ) const toolLayer = (replacements: LayerNode.Replacements = []) => - AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, webFetchToolNode]), [ - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + AppNodeBuilder.build(LayerNode.group([Tool.node, webFetchToolNode]), [ + [Permission.node, permission], [Image.node, imagePassthrough], ...replacements, ]) @@ -89,7 +87,7 @@ describe("WebFetchTool registration", () => { it.effect("registers and fetches an ordinary hostname HTTP URL without rewriting it", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = "http://example.com/public" expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch", "execute"]) @@ -109,7 +107,7 @@ describe("WebFetchTool registration", () => { it.effect("accepts localhost URLs with the same requested-URL permission check", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = "http://localhost/private" expect(yield* executeTool(registry, call({ url, format: "text" }))).toMatchObject({ @@ -137,7 +135,7 @@ describe("WebFetchTool registration", () => { (server) => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = new URL("/redirect", server.url).toString() expect(yield* executeTool(registry, call({ url, format: "text" }))).toMatchObject({ @@ -155,7 +153,7 @@ describe("WebFetchTool registration", () => { it.effect("rejects non-HTTP schemes before permission or transport", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service // toSessionError unwraps the "Unable to fetch " ToolFailure to its cause message. expect(yield* executeTool(registry, call({ url: "file:///etc/passwd", format: "text" }))).toEqual({ @@ -176,7 +174,7 @@ describe("WebFetchTool registration", () => { headers: { "content-type": "text/html; charset=utf-8" }, }), ) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "markdown" }))).toMatchObject({ status: "completed", @@ -198,7 +196,7 @@ describe("WebFetchTool registration", () => { headers: { "content-type": "text/html" }, }), ) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const url = "https://1.1.1.1/deep-html" expect(yield* executeTool(registry, call({ url, format: "markdown" }))).toMatchObject({ @@ -211,7 +209,7 @@ describe("WebFetchTool registration", () => { it.effect("rejects declared and streamed oversized bodies", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service respond = () => Effect.succeed( new Response("small", { @@ -243,7 +241,7 @@ describe("WebFetchTool registration", () => { it.effect("keeps images and files unsupported until typed outcomes can carry attachments", () => Effect.gen(function* () { reset() - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service respond = () => Effect.succeed(new Response("png", { headers: { "content-type": "image/png" } })) expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({ status: "error", @@ -268,7 +266,7 @@ describe("WebFetchTool registration", () => { ? new Response("challenge", { status: 403, headers: { "cf-mitigated": "challenge" } }) : new Response("ok", { headers: { "content-type": "text/plain" } }), ) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toMatchObject({ status: "completed", @@ -284,7 +282,7 @@ describe("WebFetchTool registration", () => { Effect.gen(function* () { reset() respond = () => Effect.never - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const fiber = yield* executeTool( registry, call({ url: "https://1.1.1.1/slow", format: "text", timeout: 1 }), diff --git a/packages/core/test/tool-websearch.test.ts b/packages/core/test/tool-websearch.test.ts index 6c188968086..9bd6815f22a 100644 --- a/packages/core/test/tool-websearch.test.ts +++ b/packages/core/test/tool-websearch.test.ts @@ -2,14 +2,13 @@ import { beforeEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { Form } from "@opencode-ai/core/form" import { KV } from "@opencode-ai/core/kv" import { WebSearch } from "@opencode-ai/core/websearch" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { WebSearchTool } from "@opencode-ai/core/tool/websearch" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { WebSearchTool } from "@opencode-ai/core/tool/plugin/websearch" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" @@ -25,11 +24,11 @@ const webSearchToolNode = makeLocationNode({ yield* registerToolPlugin(WebSearchTool.Plugin, { websearch: webSearchHost(websearch) }) }), ), - deps: [ToolRegistry.toolsNode, PermissionV2.node, WebSearch.node, Form.node, KV.node], + deps: [Tool.node, Permission.node, WebSearch.node, Form.node, KV.node], }) -const sessionID = SessionV2.ID.make("ses_websearch_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_websearch_test") +const assertions: Permission.AssertInput[] = [] const queries: WebSearch.Input[] = [] let result = new WebSearch.Response({ providerID: WebSearch.ID.make("exa"), @@ -46,8 +45,8 @@ beforeEach(() => { }) const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)), ask: () => Effect.die("unused"), reply: () => Effect.die("unused"), @@ -92,13 +91,12 @@ const kv = Layer.succeed( ) const it = testEffect( AppNodeBuilder.build( - LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, WebSearch.node, webSearchToolNode]), + LayerNode.group([Tool.node, WebSearch.node, webSearchToolNode]), [ - [PermissionV2.node, permission], + [Permission.node, permission], [WebSearch.node, websearch], [Form.node, form], [KV.node, kv], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], [Image.node, imagePassthrough], ], ), @@ -107,7 +105,7 @@ const it = testEffect( describe("WebSearchTool registration", () => { it.effect("asserts permission before delegating to WebSearch", () => Effect.gen(function* () { - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["websearch", "execute"]) expect( @@ -155,7 +153,7 @@ describe("WebSearchTool registration", () => { }, ], }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { @@ -190,7 +188,7 @@ describe("WebSearchTool registration", () => { it.effect("uses the concise no-results fallback", () => Effect.gen(function* () { result = new WebSearch.Response({ providerID: WebSearch.ID.make("exa"), results: [] }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service expect( yield* executeTool(registry, { diff --git a/packages/core/test/tool-write.test.ts b/packages/core/test/tool-write.test.ts index f786fc93480..cd2781189f3 100644 --- a/packages/core/test/tool-write.test.ts +++ b/packages/core/test/tool-write.test.ts @@ -8,12 +8,11 @@ import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { FSUtil } from "@opencode-ai/util/fs-util" import { Location } from "@opencode-ai/core/location" import { LocationMutation } from "@opencode-ai/core/location-mutation" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" -import { WriteTool } from "@opencode-ai/core/tool/write" +import { Session } from "@opencode-ai/core/session" +import { Tool } from "@opencode-ai/core/tool" +import { WriteTool } from "@opencode-ai/core/tool/plugin/write" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -23,23 +22,23 @@ import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from " const writeToolNode = makeLocationNode({ name: "test/write-tool-plugin", layer: Layer.effectDiscard(registerToolPlugin(WriteTool.Plugin)), - deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, PermissionV2.node], + deps: [Tool.node, LocationMutation.node, FileMutation.node, Permission.node], }) -const sessionID = SessionV2.ID.make("ses_write_tool_test") -const assertions: PermissionV2.AssertInput[] = [] +const sessionID = Session.ID.make("ses_write_tool_test") +const assertions: Permission.AssertInput[] = [] const writes: string[] = [] let denyAction: string | undefined const permission = Layer.succeed( - PermissionV2.Service, - PermissionV2.Service.of({ + Permission.Service, + Permission.Service.of({ assert: (input) => Effect.sync(() => assertions.push(input)).pipe( Effect.andThen( input.action === denyAction ? Effect.fail( - new PermissionV2.BlockedError({ + new Permission.BlockedError({ rules: [], permission: input.action, resources: input.resources, @@ -74,19 +73,19 @@ const filesystem = Layer.effect( }), ).pipe(Layer.provide(LayerNode.compile(FSUtil.node))) -const withTool = (directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect) => { +const withTool = (directory: string, body: (registry: Tool.Interface) => Effect.Effect) => { const activeLocation = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) })), ) return Effect.gen(function* () { - return yield* body(yield* ToolRegistry.Service) + return yield* body(yield* Tool.Service) }).pipe( Effect.provide( AppNodeBuilder.build( LayerNode.group([ - ToolRegistry.node, - ToolRegistry.toolsNode, + Tool.node, + Tool.node, LocationMutation.node, FileMutation.node, writeToolNode, @@ -94,8 +93,7 @@ const withTool = (directory: string, body: (registry: ToolRegistry.Inte [ [FSUtil.node, filesystem], [Location.node, activeLocation], - [PermissionV2.node, permission], - [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + [Permission.node, permission], ], ), ), diff --git a/packages/core/test/websearch.test.ts b/packages/core/test/websearch.test.ts index 104c6286060..73c19b1c593 100644 --- a/packages/core/test/websearch.test.ts +++ b/packages/core/test/websearch.test.ts @@ -2,12 +2,12 @@ import { describe, expect } from "bun:test" import { Effect, Exit, Scope } from "effect" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { KV } from "@opencode-ai/core/kv" import { WebSearch } from "@opencode-ai/core/websearch" import { testEffect } from "./lib/effect" -const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, EventV2.node, KV.node]))) +const it = testEffect(AppNodeBuilder.build(LayerNode.group([WebSearch.node, Bus.node, KV.node]))) const register = (id: string) => Effect.gen(function* () { diff --git a/packages/core/test/wellknown.test.ts b/packages/core/test/wellknown.test.ts index 266e96f7546..9a2e4e10677 100644 --- a/packages/core/test/wellknown.test.ts +++ b/packages/core/test/wellknown.test.ts @@ -3,12 +3,12 @@ import { Effect, Fiber, Stream } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { KV } from "@opencode-ai/core/kv" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { WellKnown } from "@opencode-ai/core/wellknown" import { testEffect } from "./lib/effect" const it = testEffect(FetchHttpClient.layer) -const serviceIt = testEffect(LayerNode.compile(LayerNode.group([WellKnown.node, KV.node, EventV2.node]))) +const serviceIt = testEffect(LayerNode.compile(LayerNode.group([WellKnown.node, KV.node, Bus.node]))) it.live("loads embedded and remote configuration", () => Effect.acquireUseRelease( @@ -66,8 +66,8 @@ serviceIt.live("persists sources in one KV value", () => Effect.gen(function* () { const wellknown = yield* WellKnown.Service const kv = yield* KV.Service - const events = yield* EventV2.Service - const changed = yield* events + const bus = yield* Bus.Service + const changed = yield* bus .subscribe(WellKnown.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true })) const entry = yield* wellknown.add(`${server.url.origin}/`) @@ -102,11 +102,11 @@ serviceIt.live("refreshes changed manifests", () => ({ server, update }) => Effect.gen(function* () { const wellknown = yield* WellKnown.Service - const events = yield* EventV2.Service + const bus = yield* Bus.Service yield* wellknown.add(server.url.origin) expect(yield* wellknown.refresh()).toBe(false) - const changed = yield* events + const changed = yield* bus .subscribe(WellKnown.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true })) update() diff --git a/packages/http-recorder/README.md b/packages/http-recorder/README.md index 84c539081ec..10f828b8c47 100644 --- a/packages/http-recorder/README.md +++ b/packages/http-recorder/README.md @@ -9,13 +9,13 @@ Use it for provider integrations, retries, polling, multi-step flows, and any te ## Install ```sh -bun add effect@4.0.0-beta.83 -bun add -d @opencode-ai/http-recorder @effect/vitest@4.0.0-beta.83 vitest@^4 +bun add effect@4.0.0-beta.101 +bun add -d @opencode-ai/http-recorder @effect/vitest@4.0.0-beta.101 vitest@^4 ``` The package supports Node.js 22+ and Bun. It is not intended for browsers, workers, or Deno. -Effect `4.0.0-beta.83` currently contains unresolved symbols in its published declarations. Until those upstream declarations are fixed, TypeScript consumers need: +Effect `4.0.0-beta.101` currently contains unresolved symbols in its published declarations. Until those upstream declarations are fixed, TypeScript consumers need: ```json { diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index fd00a153cea..792f03c019b 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -52,7 +52,7 @@ "typescript": "catalog:" }, "dependencies": { - "@effect/platform-node-shared": "4.0.0-beta.98" + "@effect/platform-node-shared": "4.0.0-beta.101" }, "peerDependencies": { "effect": "catalog:" diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 4de8068d56c..90ce130d1d7 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -35,6 +35,19 @@ export type Contract = { readonly groups: ReadonlyArray } +export type EffectTypeReference = { + readonly schema: Schema.Top + readonly name: string + readonly import: string +} + +export type EffectOutputType = { + readonly name: string + readonly import: string +} + +type ResolvedEffectTypeReference = Omit & { readonly ast: SchemaAST.AST } + export class GenerationError extends Schema.TaggedErrorClass()("GenerationError", { reason: Schema.String, }) { @@ -280,9 +293,13 @@ export function emitEffect(contract: Contract): Output { export function emitEffectImported( contract: Contract, options: - | { readonly module: string; readonly api: string } - | { readonly module: string; readonly group: string } - | { readonly module: string; readonly endpoints: Readonly> }, + | { readonly module: string; readonly api: string; readonly shapeModule?: string } + | { readonly module: string; readonly group: string; readonly shapeModule?: string } + | { + readonly module: string + readonly endpoints: Readonly> + readonly shapeModule?: string + }, ): Output { return { operations: operations(contract.groups), @@ -292,11 +309,19 @@ export function emitEffectImported( export function emitEffectShape( contract: Contract, - options: { readonly module: string; readonly api: string }, + options?: { + readonly typeReferences?: ReadonlyArray + readonly outputTypes?: Readonly> + }, ): Output { return { operations: operations(contract.groups), - files: [{ path: "api.ts", content: renderEffectShape(contract.groups, options) }], + files: [ + { + path: "api.ts", + content: renderEffectShape(contract.groups, options?.typeReferences ?? [], options?.outputTypes), + }, + ], } } @@ -332,28 +357,31 @@ export function emitPromise( } } -function renderEffectShape(groups: ReadonlyArray, options: { readonly module: string; readonly api: string }) { +function renderEffectShape( + groups: ReadonlyArray, + typeReferences: ReadonlyArray, + outputTypes?: Readonly>, +) { + const references = effectTypeReferences(typeReferences) + const imports = new Set() const endpointTypes = groups.map((group, groupIndex) => { - const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]` const endpoints = group.endpoints.map((endpoint, endpointIndex) => { const prefix = `Endpoint${groupIndex}_${endpointIndex}` - const request = - endpoint.operation.inputMode === "none" - ? "" - : `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(endpoint.endpoint.identifier)}]>[0]` const input = endpoint.input - .map( - (field) => - `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}]${isOpaquePayload(endpoint) && field.source === "payload" ? "" : `[${JSON.stringify(field.name)}]`}`, - ) + .map((field) => { + const schema = effectInputSchema(endpoint, field) + if (schema === undefined) { + throw new GenerationError({ reason: `Missing Effect input schema: ${endpoint.group}.${endpoint.endpoint.identifier}.${field.name}` }) + } + return `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${effectType(schema, references, imports)}` + }) .join("; ") const inputType = endpoint.operation.inputMode === "none" ? "" : `export type ${prefix}Input = { ${input} }` - const rawOutput = `EffectValue>` - const outputType = isStreamSchema(endpoint.successes[0]) - ? `export type ${prefix}Output = StreamValue<${rawOutput}>` - : `export type ${prefix}Output = ${endpoint.unwrapData ? `(${rawOutput})["data"]` : rawOutput}` + const output = effectOutputSchema(endpoint) + const override = outputTypes?.[clientOperationKey(group, endpoint)] + if (override !== undefined) imports.add(override.import) + const outputType = `export type ${prefix}Output = ${override?.name ?? (output === undefined ? "void" : effectType(output, references, imports))}` return [ - request, endpoint.operation.inputMode === "none" ? "" : inputType, outputType, `export type ${groupShapeTypeName(group, endpoint)} = (${endpoint.operation.inputMode === "none" ? "" : `input${endpoint.operation.inputMode === "optional" ? "?" : ""}: ${prefix}Input`}) => ${endpoint.operation.success === "stream" ? `Stream.Stream<${prefix}Output, E>` : `Effect.Effect<${prefix}Output, E>`}`, @@ -383,12 +411,7 @@ function renderEffectShape(groups: ReadonlyArray, options: { readonly mod ) return `// Generated by @opencode-ai/httpapi-codegen. Do not edit. import type { Effect, Stream } from "effect" -import type { HttpApiClient } from "effect/unstable/httpapi" -import type { ${options.api} } from ${JSON.stringify(options.module)} - -type RawClient = HttpApiClient.ForApi -type EffectValue = A extends Effect.Effect ? Success : never -type StreamValue = A extends Stream.Stream ? Success : never +${[...imports].join("\n")} ${endpointTypes.join("\n\n")} @@ -398,6 +421,112 @@ ${clientFields.join("\n")} ` } +function effectTypeReferences(input: ReadonlyArray) { + const names = new Map() + const asts = new Map() + const brands = new Map() + for (const reference of input) { + const value = { name: reference.name, import: reference.import, ast: reference.schema.ast } + const document = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.fromASTs([Schema.toType(reference.schema).ast]), + ) + const name = document.codes[0]?.Type + const type = + name === undefined + ? undefined + : (document.references.nonRecursives.find((item) => item.$ref === name)?.code.Type ?? name) + if (type?.includes("Brand.Brand<") && !brands.has(type)) brands.set(type, value) + if (SchemaAST.resolveIdentifier(reference.schema.ast) !== undefined || type?.includes("Brand.Brand<")) { + asts.set(reference.schema.ast, value) + asts.set(Schema.toType(reference.schema).ast, value) + } + if (name === undefined || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue + const previous = names.get(name) + if (previous !== undefined) { + if (previous.ast !== reference.schema.ast) { + throw new GenerationError({ reason: `Conflicting Effect type reference: ${name}` }) + } + continue + } + names.set(name, value) + } + return { names, asts, brands } +} + +function effectType( + schema: Schema.Top, + references: ReturnType, + imports: Set, +) { + const projected = Schema.toType(schema) + const direct = references.asts.get(schema.ast) ?? references.asts.get(projected.ast) + if (direct !== undefined) { + imports.add(direct.import) + return direct.name + } + const document = SchemaRepresentation.toCodeDocument(SchemaRepresentation.fromASTs([projected.ast])) + const source = new Map(document.references.nonRecursives.map((reference) => [reference.$ref, reference.code.Type])) + const expand = (type: string, seen = new Set()): string => { + for (const [name, value] of source) { + const pattern = new RegExp( + `(? property.name === field.name) + return property === undefined ? undefined : Schema.make(property.type) +} + +function effectOutputSchema(endpoint: Endpoint): Schema.Top | undefined { + const schema = endpoint.successes[0] + if (HttpApiSchema.isNoContent(schema.ast)) return undefined + if (isStreamSchema(schema)) { + if (schema._tag === "StreamUint8Array") return Schema.Uint8Array + return schema.sseMode === "data" ? streamDataSchema(schema) : Schema.make(schema.events.ast) + } + if (!endpoint.unwrapData) return schema + const ast = Schema.toType(schema).ast + if (!SchemaAST.isObjects(ast)) return undefined + const data = ast.propertySignatures.find((property) => property.name === "data") + return data === undefined ? undefined : Schema.make(data.type) +} + function groupShapeName(group: Group) { return `${identifierPart(group.identifier)}Api` } @@ -474,9 +603,13 @@ function renderEffectFiles(groups: ReadonlyArray): Output["files"] { function renderImportedEffectFiles( groups: ReadonlyArray, options: - | { readonly module: string; readonly api: string } - | { readonly module: string; readonly group: string } - | { readonly module: string; readonly endpoints: Readonly> }, + | { readonly module: string; readonly api: string; readonly shapeModule?: string } + | { readonly module: string; readonly group: string; readonly shapeModule?: string } + | { + readonly module: string + readonly endpoints: Readonly> + readonly shapeModule?: string + }, ): Output["files"] { const adapters = groups.map((group, groupIndex) => { const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]` @@ -514,7 +647,21 @@ function renderImportedEffectFiles( // produces one object containing a union value. The shapes are equivalent but TypeScript cannot correlate them. const rawCall = `raw[${JSON.stringify(item.endpoint.identifier)}]({ ${request} }${isOpaquePayload(item) ? ` as ${prefix}Request` : ""})` const mapped = `${rawCall}.pipe(Effect.mapError(mapClientError)${item.unwrapData ? ", Effect.map((value) => value.data)" : ""})` - return `${item.operation.inputMode === "none" ? "" : `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\ntype ${prefix}Input = { ${input} }\n`}const ${prefix} = (raw: ${rawGroup}) => (${argument}) => ${item.operation.success === "stream" ? `Stream.unwrap(${rawCall}.pipe(Effect.mapError(mapClientError), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError)))))` : mapped}` + const result = + item.operation.success === "stream" + ? `Stream.unwrap(${rawCall}.pipe(Effect.mapError(mapClientError), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError)))))` + : mapped + const output = + options.shapeModule === undefined + ? result + : `${item.operation.success === "stream" ? "preserveStream" : "preserveEffect"}<${prefix}Output>()(${result})` + const declarations = + options.shapeModule === undefined && item.operation.inputMode !== "none" + ? `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\ntype ${prefix}Input = { ${input} }\n` + : isOpaquePayload(item) + ? `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\n` + : "" + return `${declarations}const ${prefix} = (raw: ${rawGroup}) => (${argument}) => ${output}` }) const fields = renderClientTree( group.endpoints, @@ -542,7 +689,21 @@ function renderImportedEffectFiles( ? `import { ${api} } from ${JSON.stringify(options.module)}` : `import { HttpApi, HttpApiClient${"endpoints" in options ? ", HttpApiGroup" : ""} } from "effect/unstable/httpapi"\nimport { ${projection.imports.join(", ")} } from ${JSON.stringify(options.module)}` const httpApiImport = projection === undefined ? 'import { HttpApiClient } from "effect/unstable/httpapi"\n' : "" - const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\nimport { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` + const shapeTypes = groups.flatMap((group, groupIndex) => + group.endpoints.flatMap((endpoint, endpointIndex) => [ + ...(endpoint.operation.inputMode === "none" ? [] : [`Endpoint${groupIndex}_${endpointIndex}Input`]), + `Endpoint${groupIndex}_${endpointIndex}Output`, + ]), + ) + const shapeImport = + options.shapeModule === undefined + ? "" + : `import type { ${shapeTypes.join(", ")} } from ${JSON.stringify(options.shapeModule)}\n` + const preserve = + options.shapeModule === undefined + ? "" + : `const preserveEffect = () => (effect: Effect.Effect) => effect\n${usesStream ? "const preserveStream = () => (stream: Stream.Stream) => stream\n" : ""}\n` + const client = `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` return [ { path: "client-error.ts", diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 7374bd54903..32a400d471c 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -96,6 +96,65 @@ describe("HttpApiCodegen.generate", () => { ) }) + test("generates Effect API types from schemas instead of the imported API", () => { + const Info = Schema.Struct({ id: Schema.String }).annotate({ identifier: "Session.Info" }) + const output = emitEffectShape( + compileContract( + api( + HttpApiEndpoint.get("get", "/session/:id", { + params: { id: Schema.String }, + success: Schema.Struct({ data: Info }), + }), + ), + ), + { + typeReferences: [ + { + schema: Info, + name: "Session.Info", + import: 'import type { Session } from "@example/schema/session"', + }, + ], + }, + ) + const source = output.files[0]?.content + + expect(source).toContain('import type { Session } from "@example/schema/session"') + expect(source).toContain('export type Endpoint0_0Input = { readonly "id": string }') + expect(source).toContain("export type Endpoint0_0Output = Session.Info") + expect(source).not.toContain("HttpApiClient") + expect(source).not.toContain("@example/api") + }) + + test("allows composed Effect outputs to use an authoritative named type", () => { + const output = emitEffectShape( + compileContract(api(HttpApiEndpoint.get("events", "/event", { success: Schema.Unknown }))), + { + outputTypes: { + "session.events": { + name: "OpenCodeEvent", + import: 'import type { OpenCodeEvent } from "@example/protocol/event"', + }, + }, + }, + ) + const source = output.files[0]?.content + + expect(source).toContain('import type { OpenCodeEvent } from "@example/protocol/event"') + expect(source).toContain("export type Endpoint0_0Output = OpenCodeEvent") + }) + + test("exposes an imported Effect client through its generated shape", () => { + const output = emitEffectImported( + compileContract(api(HttpApiEndpoint.get("get", "/session", { success: Schema.String }))), + { module: "@example/api", api: "Api", shapeModule: "../api" }, + ) + const source = output.files.find((file) => file.path === "client.ts")?.content + + expect(source).toContain('import type { Endpoint0_0Output } from "../api"') + expect(source).toContain("preserveEffect()") + }) + test("projects imported endpoint constants into a generated API", () => { const output = emitEffectImported( compileContract( @@ -220,7 +279,7 @@ describe("HttpApiCodegen.generate", () => { '"instructions": { "list": Endpoint0_0(raw), "put": Endpoint0_1(raw), "remove": Endpoint0_2(raw) }', ) - const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" }) + const shape = emitEffectShape(contract) const apiShape = shape.files.find((file) => file.path === "api.ts")?.content expect(apiShape).toContain('readonly "instructions": { readonly "list": SessionInstructionsListOperation') expect(apiShape).toContain('readonly "put": SessionInstructionsPutOperation') @@ -1033,7 +1092,7 @@ describe("HttpApiCodegen.generate", () => { const contract = compileContract(source) const effect = emitEffect(contract) const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" }) - const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" }) + const shape = emitEffectShape(contract) const promise = emitPromise(contract) expect(effect.operations[0]).toMatchObject({ @@ -1042,7 +1101,9 @@ describe("HttpApiCodegen.generate", () => { }) expect(effect.files.find((file) => file.path === "session.ts")?.content).toContain('payload: input["payload"]') expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain('payload: input["payload"]') - expect(shape.files[0]?.content).toContain('Endpoint0_0Request["payload"]') + expect(shape.files[0]?.content).toContain( + 'readonly "payload": { readonly "type": "local", readonly "command": ReadonlyArray }', + ) expect(promise.files.find((file) => file.path === "types.ts")?.content).toContain( 'readonly "payload": { readonly "type": "local", readonly "command": ReadonlyArray } | { readonly "type": "remote", readonly "url": string }', ) diff --git a/packages/plugin/AGENTS.md b/packages/plugin/AGENTS.md new file mode 100644 index 00000000000..0d98cecc888 --- /dev/null +++ b/packages/plugin/AGENTS.md @@ -0,0 +1,6 @@ +# Plugin Package Guide + +- The plugin package has two versions: Effect and Promise. +- In the Effect version, every domain must extend the corresponding Effect API client interface from `@opencode-ai/client/effect/api`. +- Do not redefine functions that already exist on the Effect API client interface. +- Plugin domains add only the additional functions that make sense in the plugin context. diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 942de6460a6..9973af86154 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -10,15 +10,11 @@ "build": "tsc -p tsconfig.build.json" }, "exports": { - ".": "./src/index.ts", - "./tool": "./src/tool.ts", - "./tui": "./src/tui.ts", - "./v2/effect": "./src/v2/effect/index.ts", - "./v2/effect/*": "./src/v2/effect/*.ts", - "./v2/tui": "./src/v2/tui/index.ts", - "./v2/tui/*": "./src/v2/tui/*.ts", - "./v2": "./src/v2/promise/index.ts", - "./v2/*": "./src/v2/promise/*.ts" + ".": "./src/promise/index.ts", + "./effect": "./src/effect/index.ts", + "./tui": "./src/tui/index.ts", + "./v1": "./src/v1/index.ts", + "./*": "./src/*.ts" }, "files": [ "dist" @@ -29,7 +25,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "1.18.5", - "@standard-schema/spec": "^1.1.0", + "@standard-schema/spec": "catalog:", "effect": "catalog:", "zod": "catalog:" }, diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/README.md similarity index 91% rename from packages/plugin/src/v2/promise/README.md rename to packages/plugin/src/README.md index c7ffbfd251c..2063922f85a 100644 --- a/packages/plugin/src/v2/promise/README.md +++ b/packages/plugin/src/README.md @@ -1,6 +1,6 @@ # OpenCode V2 Promise Plugin API -The Promise plugin API at `@opencode-ai/plugin/v2` is the async/await equivalent of `@opencode-ai/plugin/v2/effect`. It grants plugins the same two in-process capabilities: +The Promise plugin API at `@opencode-ai/plugin` is the async/await equivalent of `@opencode-ai/plugin/effect`. It grants plugins the same two in-process capabilities: - `hook` installs behavior at an OpenCode extension point. - `reload` reruns every transform hook for a stateful domain. @@ -10,7 +10,7 @@ The only difference from the Effect API is the async boundary: hook callbacks, h ## Defining A Plugin ```ts -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "example", @@ -99,17 +99,16 @@ supplies the tool's name and options separately: ```ts import { Schema } from "effect" -import { Tool } from "@opencode-ai/plugin/v2/tool" await ctx.tool.transform((tools) => { tools.add( "echo", - Tool.make({ + { description: "Echo text", input: Schema.Struct({ text: Schema.String }), output: Schema.Struct({ text: Schema.String }), execute: async ({ text }) => ({ output: { text }, content: text }), - }), + }, ) }) ``` diff --git a/packages/plugin/src/v2/app.ts b/packages/plugin/src/app.ts similarity index 100% rename from packages/plugin/src/v2/app.ts rename to packages/plugin/src/app.ts diff --git a/packages/plugin/src/v2/effect/PLAN.md b/packages/plugin/src/effect/PLAN.md similarity index 98% rename from packages/plugin/src/v2/effect/PLAN.md rename to packages/plugin/src/effect/PLAN.md index 12cac7deea4..286ba2e9861 100644 --- a/packages/plugin/src/v2/effect/PLAN.md +++ b/packages/plugin/src/effect/PLAN.md @@ -7,7 +7,7 @@ This document describes the agreed target design for the V2 plugin system. It is ## Goals - Internal and external plugins use the same public plugin API. -- Effect plugins import `@opencode-ai/plugin/v2/effect`, not `@opencode-ai/core`. +- Effect plugins import `@opencode-ai/plugin/effect`, not `@opencode-ai/core`. - Public domain values use generated `@opencode-ai/sdk` types. - Core may retain branded IDs, decoded Effect schemas, and internal service types. - Plugins may register replayable domain transforms and runtime hooks imperatively during setup. @@ -208,7 +208,7 @@ type EventMap = { } ``` -Core resolves the public event type string to its internal event definition and delegates to `EventV2.Service.subscribe`. +Core resolves the public event type string to its internal event definition and delegates to `Event.Service.subscribe`. ## Domain State Model @@ -304,7 +304,7 @@ export const ModelsDevPlugin = define({ effect: (ctx) => Effect.gen(function* () { const modelsDev = yield* ModelsDev.Service - const event = yield* EventV2.Service + const event = yield* Event.Service yield* ctx.integration.transform( Effect.fn(function* (integration) { @@ -424,7 +424,7 @@ The Effect implementation remains the canonical runtime. Promise and embedding w ### 1. Define Public Contracts -- Define `PluginHost` domain capabilities in `@opencode-ai/plugin/v2/effect`. +- Define `PluginHost` domain capabilities in `@opencode-ai/plugin/effect`. - Define SDK-typed editors for agent, catalog, command, integration, reference, skill, and tool. - Define typed runtime hook maps per domain. - Define `Registration`. @@ -483,7 +483,7 @@ The Effect implementation remains the canonical runtime. Promise and embedding w ### 8. Add Event Adapter - Build the SDK event discriminant map. -- Resolve public type strings to internal EventV2 definitions. +- Resolve public type strings to internal Event definitions. - Return typed Effect streams. ### 9. Verification diff --git a/packages/plugin/src/v2/effect/README.md b/packages/plugin/src/effect/README.md similarity index 98% rename from packages/plugin/src/v2/effect/README.md rename to packages/plugin/src/effect/README.md index 8ce7ddb4145..492513338d4 100644 --- a/packages/plugin/src/v2/effect/README.md +++ b/packages/plugin/src/effect/README.md @@ -8,7 +8,7 @@ The Effect plugin API grants plugins two in-process capabilities: ## Defining A Plugin ```ts -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ diff --git a/packages/plugin/src/effect/agent.ts b/packages/plugin/src/effect/agent.ts new file mode 100644 index 00000000000..81bd1d03141 --- /dev/null +++ b/packages/plugin/src/effect/agent.ts @@ -0,0 +1,17 @@ +import type { AgentApi } from "@opencode-ai/client/effect/api" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Effect, Types } from "effect" +import type { Transform } from "./registration.js" + +export interface AgentDraft { + list(): readonly Types.DeepMutable[] + get(id: string): Types.DeepMutable | undefined + default(id: string | undefined): void + update(id: string, update: (agent: Types.DeepMutable) => void): void + remove(id: string): void +} + +export interface AgentDomain extends AgentApi { + readonly transform: Transform + readonly reload: () => Effect.Effect +} diff --git a/packages/plugin/src/v2/effect/aisdk.ts b/packages/plugin/src/effect/aisdk.ts similarity index 100% rename from packages/plugin/src/v2/effect/aisdk.ts rename to packages/plugin/src/effect/aisdk.ts diff --git a/packages/plugin/src/v2/effect/catalog.ts b/packages/plugin/src/effect/catalog.ts similarity index 51% rename from packages/plugin/src/v2/effect/catalog.ts rename to packages/plugin/src/effect/catalog.ts index 2d2f6777c6b..42395e0eb6d 100644 --- a/packages/plugin/src/v2/effect/catalog.ts +++ b/packages/plugin/src/effect/catalog.ts @@ -1,26 +1,24 @@ -import type { ModelInfo, ProviderV2Info } from "@opencode-ai/sdk/v2/types" import type { CatalogApi } from "@opencode-ai/client/effect/api" -import type { Model } from "@opencode-ai/schema/model" -import type { Effect } from "effect" +import { Model } from "@opencode-ai/schema/model" +import { Provider } from "@opencode-ai/schema/provider" +import type { Effect, Types } from "effect" import type { Transform } from "./registration.js" -type CatalogModel = ModelInfo & { compatibility?: Model.Compatibility } - export interface CatalogProviderRecord { - readonly provider: ProviderV2Info - readonly models: ReadonlyMap + readonly provider: Types.DeepMutable + readonly models: ReadonlyMap> } export interface CatalogDraft { readonly provider: { list(): readonly CatalogProviderRecord[] get(providerID: string): CatalogProviderRecord | undefined - update(providerID: string, update: (provider: ProviderV2Info) => void): void + update(providerID: string, update: (provider: Types.DeepMutable) => void): void remove(providerID: string): void } readonly model: { - get(providerID: string, modelID: string): CatalogModel | undefined - update(providerID: string, modelID: string, update: (model: CatalogModel) => void): void + get(providerID: string, modelID: string): Types.DeepMutable | undefined + update(providerID: string, modelID: string, update: (model: Types.DeepMutable) => void): void remove(providerID: string, modelID: string): void readonly default: { get(): { providerID: string; modelID: string } | undefined @@ -30,11 +28,6 @@ export interface CatalogDraft { } export interface CatalogDomain extends CatalogApi { - readonly model: CatalogApi["model"] & { - readonly get: (providerID: string, modelID: string) => Effect.Effect - } readonly transform: Transform readonly reload: () => Effect.Effect } - -type ModelGetOutput = Effect.Success["model"]["list"]>>["data"][number] diff --git a/packages/plugin/src/v2/effect/command.ts b/packages/plugin/src/effect/command.ts similarity index 89% rename from packages/plugin/src/v2/effect/command.ts rename to packages/plugin/src/effect/command.ts index 6e4764a4855..59be027df91 100644 --- a/packages/plugin/src/v2/effect/command.ts +++ b/packages/plugin/src/effect/command.ts @@ -1,5 +1,5 @@ -import type { CommandInfo } from "@opencode-ai/sdk/v2/types" import type { CommandApi } from "@opencode-ai/client/effect/api" +import type { CommandInfo } from "@opencode-ai/client" import type { Effect } from "effect" import type { Transform } from "./registration.js" diff --git a/packages/plugin/src/v2/effect/event.ts b/packages/plugin/src/effect/event.ts similarity index 100% rename from packages/plugin/src/v2/effect/event.ts rename to packages/plugin/src/effect/event.ts diff --git a/packages/plugin/src/v2/effect/index.ts b/packages/plugin/src/effect/index.ts similarity index 100% rename from packages/plugin/src/v2/effect/index.ts rename to packages/plugin/src/effect/index.ts diff --git a/packages/plugin/src/v2/effect/integration.ts b/packages/plugin/src/effect/integration.ts similarity index 80% rename from packages/plugin/src/v2/effect/integration.ts rename to packages/plugin/src/effect/integration.ts index 2cbfe24612a..d4c9c76b276 100644 --- a/packages/plugin/src/v2/effect/integration.ts +++ b/packages/plugin/src/effect/integration.ts @@ -1,19 +1,19 @@ import type { ConnectionInfo, - CredentialOAuth, - CredentialValue, IntegrationCommandMethod, IntegrationEnvMethod, - IntegrationInputs, IntegrationKeyMethod, IntegrationMethod, IntegrationOAuthMethod, - IntegrationRef, -} from "@opencode-ai/sdk/v2/types" +} from "@opencode-ai/client" import type { IntegrationApi } from "@opencode-ai/client/effect/api" +import { Credential } from "@opencode-ai/schema/credential" import type { Effect, Scope } from "effect" import type { Transform } from "./registration.js" +type IntegrationInputs = Record +type IntegrationRef = { id: string; name: string } + export type IntegrationOAuthAuthorization = { readonly url: string readonly instructions: string @@ -21,19 +21,19 @@ export type IntegrationOAuthAuthorization = { } & ( | { readonly mode: "auto" - readonly callback: Effect.Effect + readonly callback: Effect.Effect } | { readonly mode: "code" - readonly callback: (code: string) => Effect.Effect + readonly callback: (code: string) => Effect.Effect } ) export type IntegrationOAuthMethodRegistration = { readonly integrationID: string readonly method: IntegrationOAuthMethod readonly authorize: (inputs: IntegrationInputs) => Effect.Effect - readonly refresh?: (credential: CredentialOAuth) => Effect.Effect - readonly label?: (credential: CredentialOAuth) => string | undefined + readonly refresh?: (credential: Credential.OAuth) => Effect.Effect + readonly label?: (credential: Credential.OAuth) => string | undefined } export type IntegrationMethodRegistration = | IntegrationOAuthMethodRegistration @@ -67,6 +67,6 @@ export interface IntegrationDomain extends Omit, "wellkn readonly reload: () => Effect.Effect readonly connection: { readonly active: (integrationID: string) => Effect.Effect - readonly resolve: (connection: ConnectionInfo) => Effect.Effect + readonly resolve: (connection: ConnectionInfo) => Effect.Effect } } diff --git a/packages/plugin/src/v2/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts similarity index 100% rename from packages/plugin/src/v2/effect/plugin.ts rename to packages/plugin/src/effect/plugin.ts diff --git a/packages/plugin/src/v2/effect/reference.ts b/packages/plugin/src/effect/reference.ts similarity index 95% rename from packages/plugin/src/v2/effect/reference.ts rename to packages/plugin/src/effect/reference.ts index 085ae0de5e8..f9f92a9cf96 100644 --- a/packages/plugin/src/v2/effect/reference.ts +++ b/packages/plugin/src/effect/reference.ts @@ -1,4 +1,4 @@ -import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types" +import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/client" import type { ReferenceApi } from "@opencode-ai/client/effect/api" import type { Effect } from "effect" import type { Transform } from "./registration.js" diff --git a/packages/plugin/src/v2/effect/registration.ts b/packages/plugin/src/effect/registration.ts similarity index 100% rename from packages/plugin/src/v2/effect/registration.ts rename to packages/plugin/src/effect/registration.ts diff --git a/packages/plugin/src/v2/effect/session.ts b/packages/plugin/src/effect/session.ts similarity index 100% rename from packages/plugin/src/v2/effect/session.ts rename to packages/plugin/src/effect/session.ts diff --git a/packages/plugin/src/v2/effect/skill.ts b/packages/plugin/src/effect/skill.ts similarity index 71% rename from packages/plugin/src/v2/effect/skill.ts rename to packages/plugin/src/effect/skill.ts index 32daf0fb0a5..26439bcfc91 100644 --- a/packages/plugin/src/v2/effect/skill.ts +++ b/packages/plugin/src/effect/skill.ts @@ -1,11 +1,11 @@ -import type { SkillSource } from "@opencode-ai/sdk/v2/types" import type { SkillApi } from "@opencode-ai/client/effect/api" +import { Skill } from "@opencode-ai/schema/skill" import type { Effect } from "effect" import type { Transform } from "./registration.js" export interface SkillDraft { - source(source: SkillSource): void - list(): readonly SkillSource[] + source(source: Skill.Source): void + list(): readonly Skill.Source[] } export interface SkillDomain extends SkillApi { diff --git a/packages/plugin/src/effect/tool.ts b/packages/plugin/src/effect/tool.ts new file mode 100644 index 00000000000..3672496d620 --- /dev/null +++ b/packages/plugin/src/effect/tool.ts @@ -0,0 +1,45 @@ +import { Tool } from "@opencode-ai/schema/tool" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Session } from "@opencode-ai/schema/session" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { Hooks, Transform } from "./registration.js" + +interface ToolDraft { + add< + Input extends Tool.ValueSchema, + Output extends Tool.ValueSchema | undefined, + >(tool: Tool.Info): void +} + +export interface ToolHooks { + readonly "execute.before": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + input: unknown + } + readonly "execute.after": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + readonly input: unknown + } & ( + | { + readonly status: "completed" + result: Tool.Result + } + | { + readonly status: "error" + error: Tool.Error + } + ) +} + +export interface ToolDomain { + readonly transform: Transform + readonly hook: Hooks +} diff --git a/packages/plugin/src/v2/effect/websearch.ts b/packages/plugin/src/effect/websearch.ts similarity index 100% rename from packages/plugin/src/v2/effect/websearch.ts rename to packages/plugin/src/effect/websearch.ts diff --git a/packages/plugin/src/example-workspace.ts b/packages/plugin/src/example-workspace.ts deleted file mode 100644 index 9253284507f..00000000000 --- a/packages/plugin/src/example-workspace.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Plugin } from "@opencode-ai/plugin" -import { mkdir, rm } from "node:fs/promises" - -export const FolderWorkspacePlugin: Plugin = async ({ experimental_workspace }) => { - experimental_workspace.register("folder", { - name: "Folder", - description: "Create a blank folder", - configure(config) { - const rand = "" + Math.random() - - return { - ...config, - directory: `/tmp/folder/folder-${rand}`, - } - }, - async create(config) { - if (!config.directory) return - await mkdir(config.directory, { recursive: true }) - }, - async remove(config) { - await rm(config.directory!, { recursive: true, force: true }) - }, - target(config) { - return { - type: "local", - directory: config.directory!, - } - }, - }) - - return {} -} - -export default FolderWorkspacePlugin diff --git a/packages/plugin/src/example.ts b/packages/plugin/src/example.ts deleted file mode 100644 index bf79ab511e4..00000000000 --- a/packages/plugin/src/example.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Plugin } from "./index.js" -import { tool } from "./tool.js" - -export const ExamplePlugin: Plugin = async (_ctx) => { - return { - tool: { - mytool: tool({ - description: "This is a custom tool", - args: { - foo: tool.schema.string().describe("foo"), - }, - async execute(args) { - return `Hello ${args.foo}!` - }, - }), - }, - } -} diff --git a/packages/plugin/src/v2/options.ts b/packages/plugin/src/options.ts similarity index 100% rename from packages/plugin/src/v2/options.ts rename to packages/plugin/src/options.ts diff --git a/packages/plugin/src/promise/agent.ts b/packages/plugin/src/promise/agent.ts new file mode 100644 index 00000000000..178d2bac339 --- /dev/null +++ b/packages/plugin/src/promise/agent.ts @@ -0,0 +1,17 @@ +import type { AgentApi } from "@opencode-ai/client/promise/api" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Transform } from "./registration.js" +import type { DeepMutable } from "./types.js" + +export interface AgentDraft { + list(): readonly DeepMutable[] + get(id: string): DeepMutable | undefined + default(id: string | undefined): void + update(id: string, update: (agent: DeepMutable) => void): void + remove(id: string): void +} + +export interface AgentDomain extends AgentApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/v2/promise/aisdk.ts b/packages/plugin/src/promise/aisdk.ts similarity index 100% rename from packages/plugin/src/v2/promise/aisdk.ts rename to packages/plugin/src/promise/aisdk.ts diff --git a/packages/plugin/src/promise/catalog.ts b/packages/plugin/src/promise/catalog.ts new file mode 100644 index 00000000000..49f9b0b48d7 --- /dev/null +++ b/packages/plugin/src/promise/catalog.ts @@ -0,0 +1,33 @@ +import type { CatalogApi } from "@opencode-ai/client/promise/api" +import type { Model } from "@opencode-ai/schema/model" +import type { Provider } from "@opencode-ai/schema/provider" +import type { Transform } from "./registration.js" +import type { DeepMutable } from "./types.js" + +export interface CatalogProviderRecord { + readonly provider: DeepMutable + readonly models: ReadonlyMap> +} + +export interface CatalogDraft { + readonly provider: { + list(): readonly CatalogProviderRecord[] + get(providerID: string): CatalogProviderRecord | undefined + update(providerID: string, update: (provider: DeepMutable) => void): void + remove(providerID: string): void + } + readonly model: { + get(providerID: string, modelID: string): DeepMutable | undefined + update(providerID: string, modelID: string, update: (model: DeepMutable) => void): void + remove(providerID: string, modelID: string): void + readonly default: { + get(): { providerID: string; modelID: string } | undefined + set(providerID: string, modelID: string): void + } + } +} + +export interface CatalogDomain extends CatalogApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/promise/command.ts b/packages/plugin/src/promise/command.ts new file mode 100644 index 00000000000..cc9de3f3cb9 --- /dev/null +++ b/packages/plugin/src/promise/command.ts @@ -0,0 +1,15 @@ +import type { CommandApi } from "@opencode-ai/client/promise/api" +import type { CommandInfo } from "@opencode-ai/client" +import type { Transform } from "./registration.js" + +export interface CommandDraft { + list(): readonly CommandInfo[] + get(name: string): CommandInfo | undefined + update(name: string, update: (command: CommandInfo) => void): void + remove(name: string): void +} + +export interface CommandDomain extends CommandApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/v2/promise/event.ts b/packages/plugin/src/promise/event.ts similarity index 100% rename from packages/plugin/src/v2/promise/event.ts rename to packages/plugin/src/promise/event.ts diff --git a/packages/plugin/src/v2/promise/index.ts b/packages/plugin/src/promise/index.ts similarity index 100% rename from packages/plugin/src/v2/promise/index.ts rename to packages/plugin/src/promise/index.ts diff --git a/packages/plugin/src/promise/integration.ts b/packages/plugin/src/promise/integration.ts new file mode 100644 index 00000000000..8e598fc8dcd --- /dev/null +++ b/packages/plugin/src/promise/integration.ts @@ -0,0 +1,64 @@ +import type { + ConnectionInfo, + IntegrationCommandMethod, + IntegrationEnvMethod, + IntegrationKeyMethod, + IntegrationMethod, + IntegrationOAuthMethod, +} from "@opencode-ai/client" +import type { IntegrationApi } from "@opencode-ai/client/promise/api" +import { Credential } from "@opencode-ai/schema/credential" +import type { Transform } from "./registration.js" + +type IntegrationInputs = Record +type IntegrationRef = { id: string; name: string } + +export type IntegrationOAuthAuthorization = { + readonly url: string + readonly instructions: string + readonly expiresAt?: number +} & ( + | { + readonly mode: "auto" + readonly callback: Promise + } + | { + readonly mode: "code" + readonly callback: (code: string) => Promise + } +) + +export type IntegrationOAuthMethodRegistration = { + readonly integrationID: string + readonly method: IntegrationOAuthMethod + readonly authorize: (inputs: IntegrationInputs) => Promise + readonly refresh?: (credential: Credential.OAuth) => Promise + readonly label?: (credential: Credential.OAuth) => string | undefined +} + +export type IntegrationMethodRegistration = + | IntegrationOAuthMethodRegistration + | { readonly integrationID: string; readonly method: IntegrationCommandMethod } + | { readonly integrationID: string; readonly method: IntegrationKeyMethod } + | { readonly integrationID: string; readonly method: IntegrationEnvMethod } + +export interface IntegrationDraft { + list(): readonly IntegrationRef[] + get(id: string): IntegrationRef | undefined + update(id: string, update: (integration: IntegrationRef) => void): void + remove(id: string): void + readonly method: { + list(integrationID: string): readonly IntegrationMethod[] + update(input: IntegrationMethodRegistration): void + remove(integrationID: string, method: IntegrationMethod): void + } +} + +export interface IntegrationDomain extends Omit { + readonly transform: Transform + readonly reload: () => Promise + readonly connection: { + readonly active: (integrationID: string) => Promise + readonly resolve: (connection: ConnectionInfo) => Promise + } +} diff --git a/packages/plugin/src/v2/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts similarity index 100% rename from packages/plugin/src/v2/promise/plugin.ts rename to packages/plugin/src/promise/plugin.ts diff --git a/packages/plugin/src/promise/reference.ts b/packages/plugin/src/promise/reference.ts new file mode 100644 index 00000000000..7f5c43bf67c --- /dev/null +++ b/packages/plugin/src/promise/reference.ts @@ -0,0 +1,14 @@ +import type { ReferenceApi } from "@opencode-ai/client/promise/api" +import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/client" +import type { Transform } from "./registration.js" + +export interface ReferenceDraft { + add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void + remove(name: string): void + list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[] +} + +export interface ReferenceDomain extends ReferenceApi { + readonly transform: Transform + readonly reload: () => Promise +} diff --git a/packages/plugin/src/v2/promise/registration.ts b/packages/plugin/src/promise/registration.ts similarity index 100% rename from packages/plugin/src/v2/promise/registration.ts rename to packages/plugin/src/promise/registration.ts diff --git a/packages/plugin/src/v2/promise/session.ts b/packages/plugin/src/promise/session.ts similarity index 100% rename from packages/plugin/src/v2/promise/session.ts rename to packages/plugin/src/promise/session.ts diff --git a/packages/plugin/src/v2/promise/skill.ts b/packages/plugin/src/promise/skill.ts similarity index 61% rename from packages/plugin/src/v2/promise/skill.ts rename to packages/plugin/src/promise/skill.ts index cbc6459a339..e237bc788ff 100644 --- a/packages/plugin/src/v2/promise/skill.ts +++ b/packages/plugin/src/promise/skill.ts @@ -1,8 +1,11 @@ import type { SkillApi } from "@opencode-ai/client/promise/api" -import type { SkillDraft } from "../effect/skill.js" +import type { Skill } from "@opencode-ai/schema/skill" import type { Transform } from "./registration.js" -export type { SkillDraft } +export interface SkillDraft { + source(source: Skill.Source): void + list(): readonly Skill.Source[] +} export interface SkillDomain extends SkillApi { readonly transform: Transform diff --git a/packages/plugin/src/promise/tool.ts b/packages/plugin/src/promise/tool.ts new file mode 100644 index 00000000000..4070a4a6007 --- /dev/null +++ b/packages/plugin/src/promise/tool.ts @@ -0,0 +1,62 @@ +export { CallID, Error } from "@opencode-ai/schema/tool" +export type { Metadata, Options, Result } from "@opencode-ai/schema/tool" + +import { Tool } from "@opencode-ai/schema/tool" +import type { Agent } from "@opencode-ai/schema/agent" +import type { Session } from "@opencode-ai/schema/session" +import type { SessionMessage } from "@opencode-ai/schema/session-message" +import type { Hooks, Transform } from "./registration.js" + +export interface ToolContext extends Omit { + readonly progress: (update: Tool.Metadata) => Promise +} + +export type Info< + Input extends Tool.ValueSchema = Tool.ValueSchema, + Output extends Tool.ValueSchema | undefined = Tool.ValueSchema | undefined, +> = Omit, "execute"> & { + readonly execute: ( + input: Parameters["execute"]>[0], + context: ToolContext, + ) => Promise> +} + +interface ToolDraft { + add< + Input extends Tool.ValueSchema, + Output extends Tool.ValueSchema | undefined, + >(tool: Info): void +} + +interface ToolHooks { + readonly "execute.before": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + input: unknown + } + readonly "execute.after": { + readonly tool: string + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: Tool.CallID + readonly input: unknown + } & ( + | { + readonly status: "completed" + result: Tool.Result + } + | { + readonly status: "error" + error: Tool.Error + } + ) +} + +export interface ToolDomain { + readonly transform: Transform + readonly hook: Hooks +} diff --git a/packages/plugin/src/promise/types.ts b/packages/plugin/src/promise/types.ts new file mode 100644 index 00000000000..418fa6c2439 --- /dev/null +++ b/packages/plugin/src/promise/types.ts @@ -0,0 +1,9 @@ +export type DeepMutable = A extends (...args: never[]) => unknown + ? A + : A extends ReadonlyMap + ? Map, DeepMutable> + : A extends ReadonlyArray + ? DeepMutable[] + : A extends object + ? { -readonly [K in keyof A]: DeepMutable } + : A diff --git a/packages/plugin/src/v2/promise/websearch.ts b/packages/plugin/src/promise/websearch.ts similarity index 100% rename from packages/plugin/src/v2/promise/websearch.ts rename to packages/plugin/src/promise/websearch.ts diff --git a/packages/plugin/src/v2/tui/context.ts b/packages/plugin/src/tui/context.ts similarity index 97% rename from packages/plugin/src/v2/tui/context.ts rename to packages/plugin/src/tui/context.ts index 8d7c99003a8..34cb863536c 100644 --- a/packages/plugin/src/v2/tui/context.ts +++ b/packages/plugin/src/tui/context.ts @@ -10,8 +10,8 @@ import type { OpenCodeClient, OpenCodeEvent, PermissionSavedInfo, - PermissionV2Request, - ProviderV2Info, + PermissionRequest, + ProviderInfo, ReferenceInfo, SessionInfo, SessionMessageInfo, @@ -55,7 +55,7 @@ export interface Data { invalidate(sessionID: string): void } readonly permission: { - list(sessionID: string): PermissionV2Request[] | undefined + list(sessionID: string): PermissionRequest[] | undefined sync(sessionID: string): Promise invalidate(sessionID: string): void } @@ -90,7 +90,7 @@ export interface Data { readonly resource: LocationCollection } readonly model: LocationCollection - readonly provider: LocationCollection + readonly provider: LocationCollection readonly reference: LocationCollection readonly skill: LocationCollection } diff --git a/packages/plugin/src/v2/tui/index.ts b/packages/plugin/src/tui/index.ts similarity index 100% rename from packages/plugin/src/v2/tui/index.ts rename to packages/plugin/src/tui/index.ts diff --git a/packages/plugin/src/v2/tui/plugin.ts b/packages/plugin/src/tui/plugin.ts similarity index 100% rename from packages/plugin/src/v2/tui/plugin.ts rename to packages/plugin/src/tui/plugin.ts diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/v1/index.ts similarity index 100% rename from packages/plugin/src/index.ts rename to packages/plugin/src/v1/index.ts diff --git a/packages/plugin/src/shell.ts b/packages/plugin/src/v1/shell.ts similarity index 100% rename from packages/plugin/src/shell.ts rename to packages/plugin/src/v1/shell.ts diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/v1/tool.ts similarity index 100% rename from packages/plugin/src/tool.ts rename to packages/plugin/src/v1/tool.ts diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/v1/tui.ts similarity index 100% rename from packages/plugin/src/tui.ts rename to packages/plugin/src/v1/tui.ts diff --git a/packages/plugin/src/v2/effect/agent.ts b/packages/plugin/src/v2/effect/agent.ts deleted file mode 100644 index e14777af779..00000000000 --- a/packages/plugin/src/v2/effect/agent.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { AgentApi } from "@opencode-ai/client/effect/api" -import type { AgentInfo } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transform } from "./registration.js" - -export interface AgentDraft { - list(): readonly AgentInfo[] - get(id: string): AgentInfo | undefined - default(id: string | undefined): void - update(id: string, update: (agent: AgentInfo) => void): void - remove(id: string): void -} - -export interface AgentDomain extends AgentApi { - readonly get: (id: string) => Effect.Effect - readonly transform: Transform - readonly reload: () => Effect.Effect -} - -type AgentGetOutput = Effect.Success["list"]>>["data"][number] diff --git a/packages/plugin/src/v2/effect/filesystem.ts b/packages/plugin/src/v2/effect/filesystem.ts deleted file mode 100644 index d242b2a692f..00000000000 --- a/packages/plugin/src/v2/effect/filesystem.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { FileSystemEntry } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" - -export interface FileSystem { - read(input: { readonly path: string }): Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> - list(input?: { readonly path?: string }): Effect.Effect - find(input: { - readonly query: string - readonly type?: "file" | "directory" - readonly limit?: number - }): Effect.Effect - glob(input: { - readonly pattern: string - readonly path?: string - readonly limit?: number - }): Effect.Effect -} diff --git a/packages/plugin/src/v2/effect/internal/tool.ts b/packages/plugin/src/v2/effect/internal/tool.ts deleted file mode 100644 index abc92c6a7d2..00000000000 --- a/packages/plugin/src/v2/effect/internal/tool.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { Agent } from "@opencode-ai/schema/agent" -import { LLM } from "@opencode-ai/schema/llm" -import { Session } from "@opencode-ai/schema/session" -import { SessionError } from "@opencode-ai/schema/session-error" -import { SessionMessage } from "@opencode-ai/schema/session-message" -import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" -import { Effect, JsonSchema, Schema } from "effect" -import type { Hooks, Transform } from "../registration.js" - -// Tools - -/** A JSON-compatible value. Tool metadata and encoded outputs must be JSON. */ -export type JsonValue = typeof Schema.Json.Type - -/** Compact JSON metadata for tool-specific UI and client behavior. */ -export type Metadata = Readonly> - -export interface Context { - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly progress: (update: Progress) => Effect.Effect -} - -/** Live replacement metadata for a running tool. */ -export type Progress = Metadata - -export type StandardSchemaType = StandardSchemaV1 & - StandardJSONSchemaV1 -export type SchemaType = Schema.Codec | StandardSchemaType | JsonSchema.JsonSchema -type IsAny = 0 extends 1 & A ? true : false -export type InputValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : unknown -export type OutputValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : unknown -export type EncodedValue = - IsAny extends true - ? any - : S extends Schema.Codec - ? A - : S extends StandardSchemaV1 - ? A - : unknown - -type ToolDefinition = { - readonly name: string - readonly description: string - readonly inputSchema: JsonSchema.JsonSchema - readonly outputSchema?: JsonSchema.JsonSchema -} - -export class Failure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { - message: Schema.String, - error: Schema.optional(Schema.Defect()), -}) {} - -export class RegistrationError extends Schema.TaggedErrorClass()("Tool.RegistrationError", { - name: Schema.String, - message: Schema.String, -}) {} - -export type Content = - | { readonly type: "text"; readonly text: string } - | { readonly type: "file"; readonly data: string; readonly mime: string; readonly name?: string } - -/** Model-facing tool content: plain text or non-empty rich content. */ -export type ModelOutput = string | readonly [Content, ...Content[]] - -type BaseTool> = { - readonly description: string - readonly input: Input -} - -export type Response> = { - readonly output: OutputValue - readonly content?: ModelOutput - readonly metadata?: Metadata -} - -export type ContentResponse = { - readonly content: ModelOutput - readonly metadata?: Metadata -} - -export type Tool< - Input extends SchemaType, - Output extends SchemaType | undefined = undefined, -> = BaseTool & - (Output extends SchemaType - ? { - readonly output: Output - readonly execute: (input: InputValue, context: Context) => Effect.Effect, Failure> - } - : { - readonly output?: undefined - readonly execute: (input: InputValue, context: Context) => Effect.Effect - }) - -export type Any = BaseTool & { - readonly output?: SchemaType - readonly execute: (input: any, context: Context) => Effect.Effect | ContentResponse, Failure> -} - -export function make, Output extends SchemaType>( - config: Tool, -): Tool -export function make>(config: Tool): Tool -export function make(config: Any): Any -export function make(config: Any): Any { - return config -} - -// Registration - -export interface RegisterOptions { - readonly namespace?: string - /** Defaults to true. False exposes the tool directly to the provider. */ - readonly codemode?: boolean - /** Permission action used for whole-tool visibility filtering. */ - readonly permission?: string -} - -export interface Registration { - readonly tool: Any - readonly name: string - readonly namespace?: string - readonly permission: string -} - -export const validateName = (name: string) => - /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name) - ? Effect.void - : Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` })) - -export const registrationEntries = ( - tools: Readonly>, - options?: RegisterOptions, -): Array => - Object.entries(tools).map(([name, tool]) => { - const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_") - const key = - options?.namespace === undefined ? normalized : `${options.namespace.replaceAll(".", "_")}_${normalized}` - return { - key, - name: normalized, - namespace: options?.namespace, - tool, - permission: options?.permission ?? key, - } - }) - -export const validateNamespace = (namespace: string) => - namespace.split(".").every((segment) => /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(segment)) - ? Effect.void - : Effect.fail( - new RegistrationError({ name: namespace, message: `Invalid tool namespace: ${JSON.stringify(namespace)}` }), - ) - -export const toLLMDefinition = (name: string, tool: Any): ToolDefinition => ({ - name, - description: tool.description, - inputSchema: inputJsonSchema(tool.input), - ...(tool.output === undefined ? {} : { outputSchema: outputJsonSchema(tool.output) }), -}) - -// Schema interpretation - -export function decodeInput(schema: SchemaType, value: unknown): Effect.Effect { - if (Schema.isSchema(schema)) - return Schema.decodeUnknownEffect(schema)(value).pipe( - Effect.mapError((error) => new Failure({ message: `Invalid tool input: ${error.message}` })), - ) - if (isStandardSchema(schema)) return validateStandard(schema, value, "Invalid tool input") - return Effect.succeed(value) -} - -export function encodeOutput(schema: SchemaType, value: unknown): Effect.Effect { - if (Schema.isSchema(schema)) - return Schema.encodeEffect(schema)(value).pipe( - Effect.mapError( - (error) => new Failure({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), - ), - ) - if (isStandardSchema(schema)) - return validateStandard(schema, value, "Tool returned an invalid value for its output schema") - return Schema.decodeUnknownEffect(Schema.Json)(value).pipe( - Effect.mapError( - (error) => new Failure({ message: `Tool returned a non-JSON value for its output schema: ${error.message}` }), - ), - ) -} - -function isStandardSchema(schema: SchemaType): schema is StandardSchemaType { - return "~standard" in schema -} - -function validateStandard(schema: StandardSchemaType, value: unknown, prefix: string): Effect.Effect { - return Effect.gen(function* () { - const pending = yield* Effect.try({ - try: () => schema["~standard"].validate(value), - catch: (error) => standardFailure(prefix, error), - }) - const result = - pending instanceof Promise - ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) - : pending - if (result.issues) - return yield* Effect.fail( - new Failure({ message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}` }), - ) - return result.value - }) -} - -function standardFailure(prefix: string, error: unknown) { - return new Failure({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) -} - -function inputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { - if (isStandardSchema(schema)) - return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema - return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) -} - -function outputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { - if (isStandardSchema(schema)) - return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema - return Schema.isSchema(schema) ? toJsonSchema(schema) : (schema as JsonSchema.JsonSchema) -} - -function toJsonSchema(schema: Schema.Top): JsonSchema.JsonSchema { - const document = Schema.toJsonSchemaDocument(schema) - if (Object.keys(document.definitions).length === 0) return document.schema - return { ...document.schema, $defs: document.definitions } -} - -// Plugin events - -export interface ToolExecuteBeforeEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - input: unknown -} - -type ToolHookBase = { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly input: unknown -} - -export const ExecuteAfterOutcome = Schema.Union([ - Schema.Struct({ - status: Schema.Literal("completed"), - content: Schema.NonEmptyArray(LLM.ToolContent), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Json)), - outputPaths: Schema.optional(Schema.Array(Schema.String)), - }), - Schema.Struct({ - status: Schema.Literal("error"), - error: SessionError.Error, - content: Schema.optional(Schema.NonEmptyArray(LLM.ToolContent)), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Json)), - outputPaths: Schema.optional(Schema.Array(Schema.String)), - }), -]).pipe(Schema.toTaggedUnion("status")) - -type Mutable = { -readonly [K in keyof A]: A[K] } -type HookOutcome = Omit, "status"> & Pick - -/** The bounded terminal outcome exposed to tool hooks. */ -export type Outcome = typeof ExecuteAfterOutcome.Type extends infer A - ? A extends { readonly status: string } - ? HookOutcome - : never - : never - -/** - * The canonical execution outcome as seen by `execute.after` hooks. Hooks - * observe bounded model content, optional metadata, and managed output paths; - * they never observe the raw domain output. - */ -export type ToolExecuteAfterEvent = ToolHookBase & Outcome - -export interface ToolDraft { - add(name: string, tool: Any, options?: RegisterOptions): void -} - -export interface ToolHooks { - readonly "execute.before": ToolExecuteBeforeEvent - readonly "execute.after": ToolExecuteAfterEvent -} - -export interface ToolDomain { - readonly transform: Transform - readonly hook: Hooks -} diff --git a/packages/plugin/src/v2/effect/location.ts b/packages/plugin/src/v2/effect/location.ts deleted file mode 100644 index bc546a3b175..00000000000 --- a/packages/plugin/src/v2/effect/location.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Location { - readonly directory: string - readonly project: { - readonly directory: string - } -} diff --git a/packages/plugin/src/v2/effect/npm.ts b/packages/plugin/src/v2/effect/npm.ts deleted file mode 100644 index 4cb96c32d17..00000000000 --- a/packages/plugin/src/v2/effect/npm.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Effect } from "effect" - -export interface Npm { - add(pkg: string): Effect.Effect< - { - readonly directory: string - readonly entrypoint?: string - }, - unknown - > -} diff --git a/packages/plugin/src/v2/effect/path.ts b/packages/plugin/src/v2/effect/path.ts deleted file mode 100644 index f9045cc32d0..00000000000 --- a/packages/plugin/src/v2/effect/path.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Path { - readonly home: string - readonly data: string - readonly cache: string - readonly config: string - readonly state: string - readonly temp: string -} diff --git a/packages/plugin/src/v2/effect/tool.ts b/packages/plugin/src/v2/effect/tool.ts deleted file mode 100644 index cbc634f8e5b..00000000000 --- a/packages/plugin/src/v2/effect/tool.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as Tool from "./internal/tool.js" -export * from "./internal/tool.js" diff --git a/packages/plugin/src/v2/promise/agent.ts b/packages/plugin/src/v2/promise/agent.ts deleted file mode 100644 index af5a1f68505..00000000000 --- a/packages/plugin/src/v2/promise/agent.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AgentApi } from "@opencode-ai/client/promise/api" -import type { AgentDraft } from "../effect/agent.js" -import type { Transform } from "./registration.js" - -export type { AgentDraft } - -export interface AgentDomain extends AgentApi { - readonly get: (id: string) => Promise - readonly transform: Transform - readonly reload: () => Promise -} - -type AgentGetOutput = Awaited>["data"][number] diff --git a/packages/plugin/src/v2/promise/catalog.ts b/packages/plugin/src/v2/promise/catalog.ts deleted file mode 100644 index 26cc25caa3b..00000000000 --- a/packages/plugin/src/v2/promise/catalog.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { CatalogApi } from "@opencode-ai/client/promise/api" -import type { CatalogDraft, CatalogProviderRecord } from "../effect/catalog.js" -import type { Transform } from "./registration.js" - -export type { CatalogDraft, CatalogProviderRecord } - -export interface CatalogDomain extends CatalogApi { - readonly model: CatalogApi["model"] & { - readonly get: (providerID: string, modelID: string) => Promise - } - readonly transform: Transform - readonly reload: () => Promise -} - -type ModelGetOutput = Awaited>["data"][number] diff --git a/packages/plugin/src/v2/promise/command.ts b/packages/plugin/src/v2/promise/command.ts deleted file mode 100644 index 2c675e28561..00000000000 --- a/packages/plugin/src/v2/promise/command.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CommandApi } from "@opencode-ai/client/promise/api" -import type { CommandDraft } from "../effect/command.js" -import type { Transform } from "./registration.js" - -export type { CommandDraft } - -export interface CommandDomain extends CommandApi { - readonly transform: Transform - readonly reload: () => Promise -} diff --git a/packages/plugin/src/v2/promise/integration.ts b/packages/plugin/src/v2/promise/integration.ts deleted file mode 100644 index 6b121afc8bc..00000000000 --- a/packages/plugin/src/v2/promise/integration.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IntegrationApi } from "@opencode-ai/client/promise/api" -import type { IntegrationDraft, IntegrationMethodRegistration } from "../effect/integration.js" -import type { - CredentialOAuth, - CredentialValue, - IntegrationEnvMethod, - IntegrationInputs, - IntegrationKeyMethod, - IntegrationOAuthMethod, -} from "@opencode-ai/sdk/v2/types" -import type { Transform } from "./registration.js" - -export type { IntegrationDraft, IntegrationMethodRegistration } - -export type IntegrationOAuthAuthorization = { - readonly url: string - readonly instructions: string - readonly expiresAt?: number -} & ( - | { - readonly mode: "auto" - readonly callback: Promise - } - | { - readonly mode: "code" - readonly callback: (code: string) => Promise - } -) - -export interface IntegrationDomain extends Omit { - readonly transform: Transform - readonly reload: () => Promise - readonly connection: { - readonly active: (integrationID: string) => Promise - readonly resolve: ( - connection: import("@opencode-ai/sdk/v2/types").ConnectionInfo, - ) => Promise - } -} diff --git a/packages/plugin/src/v2/promise/internal/tool.ts b/packages/plugin/src/v2/promise/internal/tool.ts deleted file mode 100644 index 3d5def019ba..00000000000 --- a/packages/plugin/src/v2/promise/internal/tool.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { Hooks, Transform } from "../registration.js" - -export type Context = Omit & { - readonly progress: (update: import("../../effect/internal/tool.js").Progress) => Promise -} -export type SchemaType = import("../../effect/internal/tool.js").SchemaType -export type Content = import("../../effect/internal/tool.js").Content -export type Metadata = import("../../effect/internal/tool.js").Metadata -export type ModelOutput = import("../../effect/internal/tool.js").ModelOutput - -export type Tool, Output extends SchemaType | undefined = undefined> = Omit< - import("../../effect/internal/tool.js").Tool, - "execute" -> & { - readonly execute: ( - input: import("../../effect/internal/tool.js").InputValue, - context: Context, - ) => Promise< - Output extends SchemaType - ? import("../../effect/internal/tool.js").Response - : import("../../effect/internal/tool.js").ContentResponse - > -} - -export type Any = Omit & { - readonly execute: ( - input: any, - context: Context, - ) => Promise< - import("../../effect/internal/tool.js").Response | import("../../effect/internal/tool.js").ContentResponse - > -} - -export function make, Output extends SchemaType>( - tool: Tool, -): Tool -export function make>(tool: Tool): Tool -export function make(tool: Any): Any -export function make(tool: Any): Any { - return tool -} - -export type ToolExecuteBeforeEvent = import("../../effect/internal/tool.js").ToolExecuteBeforeEvent -export type ToolExecuteAfterEvent = import("../../effect/internal/tool.js").ToolExecuteAfterEvent -export type RegisterOptions = import("../../effect/internal/tool.js").RegisterOptions - -export interface ToolDraft { - add, Output extends SchemaType>( - name: string, - tool: Tool, - options?: RegisterOptions, - ): void - add>(name: string, tool: Tool, options?: RegisterOptions): void -} - -export interface ToolHooks { - readonly "execute.before": ToolExecuteBeforeEvent - readonly "execute.after": ToolExecuteAfterEvent -} - -export interface ToolDomain { - readonly transform: Transform - readonly hook: Hooks -} diff --git a/packages/plugin/src/v2/promise/reference.ts b/packages/plugin/src/v2/promise/reference.ts deleted file mode 100644 index 05542b2baf7..00000000000 --- a/packages/plugin/src/v2/promise/reference.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ReferenceApi } from "@opencode-ai/client/promise/api" -import type { ReferenceDraft } from "../effect/reference.js" -import type { Transform } from "./registration.js" - -export type { ReferenceDraft } - -export interface ReferenceDomain extends ReferenceApi { - readonly transform: Transform - readonly reload: () => Promise -} diff --git a/packages/plugin/src/v2/promise/tool.ts b/packages/plugin/src/v2/promise/tool.ts deleted file mode 100644 index cbc634f8e5b..00000000000 --- a/packages/plugin/src/v2/promise/tool.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as Tool from "./internal/tool.js" -export * from "./internal/tool.js" diff --git a/packages/plugin/test/contract-identity.test.ts b/packages/plugin/test/contract-identity.test.ts index 264fcaa2f8d..f93d70b48df 100644 --- a/packages/plugin/test/contract-identity.test.ts +++ b/packages/plugin/test/contract-identity.test.ts @@ -10,9 +10,9 @@ import { Reference } from "@opencode-ai/schema/reference" import { Skill } from "@opencode-ai/schema/skill" import { WebSearch } from "@opencode-ai/schema/websearch" -const Plugin = await import("../src/v2/effect/index") -const PromisePlugin = await import("../src/v2/promise/index") -const TuiPlugin = await import("../src/v2/tui/index") +const Plugin = await import("../src/effect/index") +const PromisePlugin = await import("../src/promise/index") +const TuiPlugin = await import("../src/tui/index") test.each([ ["effect", Plugin], @@ -43,7 +43,7 @@ test.each([ ]) }) -test("tui entrypoint exposes the V2 plugin definition", () => { +test("tui entrypoint exposes the plugin definition", () => { const plugin = TuiPlugin.Plugin.define({ id: "demo", setup() {} }) expect(plugin.id).toBe("demo") }) diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 1c8b3dc9412..b7cc24d7091 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -346,6 +346,122 @@ "summary": "List agents" } }, + "/api/agent/{agentID}": { + "get": { + "tags": [ + "agent" + ], + "operationId": "v2.agent.get", + "parameters": [ + { + "name": "agentID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "location", + "in": "query", + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "workspace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/Location.Info" + }, + "data": { + "$ref": "#/components/schemas/Agent.Info" + } + }, + "required": [ + "location", + "data" + ], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "AgentNotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentNotFoundError" + } + } + } + } + }, + "description": "Retrieve a single currently registered agent.", + "summary": "Get agent" + } + }, "/api/plugin": { "get": { "tags": [ @@ -4279,7 +4395,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } } }, @@ -4396,7 +4512,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } }, "required": [ @@ -7466,7 +7582,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7664,7 +7780,7 @@ ] }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -7762,7 +7878,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" }, "agent": { "anyOf": [ @@ -7818,7 +7934,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7916,7 +8032,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } }, "required": [ @@ -8061,7 +8177,7 @@ "type": "object", "properties": { "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" }, "message": { "anyOf": [ @@ -10456,7 +10572,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10527,7 +10643,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10667,7 +10783,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "$ref": "#/components/schemas/Question.Reply" } } }, @@ -11981,7 +12097,7 @@ "Agent.Color": { "type": "string" }, - "PermissionV2.Effect": { + "Permission.Effect": { "type": "string", "enum": [ "allow", @@ -11989,7 +12105,7 @@ "ask" ] }, - "PermissionV2.Rule": { + "Permission.Rule": { "type": "object", "properties": { "action": { @@ -11999,7 +12115,7 @@ "type": "string" }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -12009,10 +12125,10 @@ ], "additionalProperties": false }, - "PermissionV2.Ruleset": { + "Permission.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" + "$ref": "#/components/schemas/Permission.Rule" } }, "Agent.Info": { @@ -12059,7 +12175,7 @@ ] }, "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "$ref": "#/components/schemas/Permission.Ruleset" } }, "required": [ @@ -12072,6 +12188,29 @@ ], "additionalProperties": false }, + "AgentNotFoundError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": [ + "AgentNotFoundError" + ] + }, + "agentID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "_tag", + "agentID", + "message" + ], + "additionalProperties": false + }, "Plugin.Info": { "type": "object", "properties": { @@ -18537,7 +18676,7 @@ ], "additionalProperties": false }, - "ProviderV2.Info": { + "Provider.Info": { "type": "object", "properties": { "id": { @@ -21466,7 +21605,7 @@ ], "additionalProperties": false }, - "PermissionV2.Source": { + "Permission.Source": { "anyOf": [ { "type": "object", @@ -21493,7 +21632,7 @@ } ] }, - "PermissionV2.Request": { + "Permission.Request": { "type": "object", "properties": { "id": { @@ -21531,7 +21670,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -21589,7 +21728,7 @@ ], "additionalProperties": false }, - "PermissionV2.Reply": { + "Permission.Reply": { "type": "string", "enum": [ "once", @@ -21935,7 +22074,7 @@ ], "additionalProperties": false }, - "PermissionAction": { + "PermissionV1.Action": { "type": "string", "enum": [ "allow", @@ -21943,7 +22082,7 @@ "ask" ] }, - "PermissionRule": { + "PermissionV1.Rule": { "type": "object", "properties": { "permission": { @@ -21953,7 +22092,7 @@ "type": "string" }, "action": { - "$ref": "#/components/schemas/PermissionAction" + "$ref": "#/components/schemas/PermissionV1.Action" } }, "required": [ @@ -21963,10 +22102,10 @@ ], "additionalProperties": false }, - "PermissionRuleset": { + "PermissionV1.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionRule" + "$ref": "#/components/schemas/PermissionV1.Rule" } }, "SessionV1.Info": { @@ -22155,7 +22294,7 @@ "additionalProperties": false }, "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "$ref": "#/components/schemas/PermissionV1.Ruleset" }, "revert": { "type": "object", @@ -22455,10 +22594,10 @@ ], "additionalProperties": false }, - "JSONSchema": { + "SessionV1.JSONSchema": { "type": "object" }, - "OutputFormat": { + "SessionV1.OutputFormat": { "anyOf": [ { "type": "object", @@ -22485,7 +22624,7 @@ ] }, "schema": { - "$ref": "#/components/schemas/JSONSchema" + "$ref": "#/components/schemas/SessionV1.JSONSchema" }, "retryCount": { "anyOf": [ @@ -22518,7 +22657,7 @@ } ] }, - "UserMessage": { + "SessionV1.UserMessage": { "type": "object", "properties": { "id": { @@ -22563,7 +22702,7 @@ "format": { "anyOf": [ { - "$ref": "#/components/schemas/OutputFormat" + "$ref": "#/components/schemas/SessionV1.OutputFormat" }, { "type": "null" @@ -22985,7 +23124,7 @@ ], "additionalProperties": false }, - "AssistantMessage": { + "SessionV1.AssistantMessage": { "type": "object", "properties": { "id": { @@ -23218,13 +23357,13 @@ ], "additionalProperties": false }, - "Message": { + "SessionV1.Message": { "anyOf": [ { - "$ref": "#/components/schemas/UserMessage" + "$ref": "#/components/schemas/SessionV1.UserMessage" }, { - "$ref": "#/components/schemas/AssistantMessage" + "$ref": "#/components/schemas/SessionV1.AssistantMessage" } ] }, @@ -23294,7 +23433,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/SessionV1.Message" } }, "required": [ @@ -23403,7 +23542,7 @@ ], "additionalProperties": false }, - "TextPart": { + "SessionV1.TextPart": { "type": "object", "properties": { "id": { @@ -23518,7 +23657,7 @@ ], "additionalProperties": false }, - "SubtaskPart": { + "SessionV1.SubtaskPart": { "type": "object", "properties": { "id": { @@ -23605,7 +23744,7 @@ ], "additionalProperties": false }, - "ReasoningPart": { + "SessionV1.ReasoningPart": { "type": "object", "properties": { "id": { @@ -23694,7 +23833,7 @@ ], "additionalProperties": false }, - "FilePartSourceText": { + "SessionV1.FilePartSourceText": { "type": "object", "properties": { "value": { @@ -23714,11 +23853,11 @@ ], "additionalProperties": false }, - "FileSource": { + "SessionV1.FileSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23737,7 +23876,7 @@ ], "additionalProperties": false }, - "Range": { + "SessionV1.Range": { "type": "object", "properties": { "start": { @@ -23799,11 +23938,11 @@ ], "additionalProperties": false }, - "SymbolSource": { + "SessionV1.SymbolSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23815,7 +23954,7 @@ "type": "string" }, "range": { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/SessionV1.Range" }, "name": { "type": "string" @@ -23839,11 +23978,11 @@ ], "additionalProperties": false }, - "ResourceSource": { + "SessionV1.ResourceSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23866,20 +24005,20 @@ ], "additionalProperties": false }, - "FilePartSource": { + "SessionV1.FilePartSource": { "anyOf": [ { - "$ref": "#/components/schemas/FileSource" + "$ref": "#/components/schemas/SessionV1.FileSource" }, { - "$ref": "#/components/schemas/SymbolSource" + "$ref": "#/components/schemas/SessionV1.SymbolSource" }, { - "$ref": "#/components/schemas/ResourceSource" + "$ref": "#/components/schemas/SessionV1.ResourceSource" } ] }, - "FilePart": { + "SessionV1.FilePart": { "type": "object", "properties": { "id": { @@ -23931,7 +24070,7 @@ "source": { "anyOf": [ { - "$ref": "#/components/schemas/FilePartSource" + "$ref": "#/components/schemas/SessionV1.FilePartSource" }, { "type": "null" @@ -23949,7 +24088,7 @@ ], "additionalProperties": false }, - "ToolStatePending": { + "SessionV1.ToolStatePending": { "type": "object", "properties": { "status": { @@ -23972,7 +24111,7 @@ ], "additionalProperties": false }, - "ToolStateRunning": { + "SessionV1.ToolStateRunning": { "type": "object", "properties": { "status": { @@ -24029,7 +24168,7 @@ ], "additionalProperties": false }, - "ToolStateCompleted": { + "SessionV1.ToolStateCompleted": { "type": "object", "properties": { "status": { @@ -24096,7 +24235,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" } }, { @@ -24115,7 +24254,7 @@ ], "additionalProperties": false }, - "ToolStateError": { + "SessionV1.ToolStateError": { "type": "object", "properties": { "status": { @@ -24175,23 +24314,23 @@ ], "additionalProperties": false }, - "ToolState": { + "SessionV1.ToolState": { "anyOf": [ { - "$ref": "#/components/schemas/ToolStatePending" + "$ref": "#/components/schemas/SessionV1.ToolStatePending" }, { - "$ref": "#/components/schemas/ToolStateRunning" + "$ref": "#/components/schemas/SessionV1.ToolStateRunning" }, { - "$ref": "#/components/schemas/ToolStateCompleted" + "$ref": "#/components/schemas/SessionV1.ToolStateCompleted" }, { - "$ref": "#/components/schemas/ToolStateError" + "$ref": "#/components/schemas/SessionV1.ToolStateError" } ] }, - "ToolPart": { + "SessionV1.ToolPart": { "type": "object", "properties": { "id": { @@ -24231,7 +24370,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/ToolState" + "$ref": "#/components/schemas/SessionV1.ToolState" }, "metadata": { "anyOf": [ @@ -24255,7 +24394,7 @@ ], "additionalProperties": false }, - "StepStartPart": { + "SessionV1.StepStartPart": { "type": "object", "properties": { "id": { @@ -24307,7 +24446,7 @@ ], "additionalProperties": false }, - "StepFinishPart": { + "SessionV1.StepFinishPart": { "type": "object", "properties": { "id": { @@ -24415,7 +24554,7 @@ ], "additionalProperties": false }, - "SnapshotPart": { + "SessionV1.SnapshotPart": { "type": "object", "properties": { "id": { @@ -24461,7 +24600,7 @@ ], "additionalProperties": false }, - "PatchPart": { + "SessionV1.PatchPart": { "type": "object", "properties": { "id": { @@ -24514,7 +24653,7 @@ ], "additionalProperties": false }, - "AgentPart": { + "SessionV1.AgentPart": { "type": "object", "properties": { "id": { @@ -24597,7 +24736,7 @@ ], "additionalProperties": false }, - "RetryPart": { + "SessionV1.RetryPart": { "type": "object", "properties": { "id": { @@ -24670,7 +24809,7 @@ ], "additionalProperties": false }, - "CompactionPart": { + "SessionV1.CompactionPart": { "type": "object", "properties": { "id": { @@ -24741,43 +24880,43 @@ ], "additionalProperties": false }, - "Part": { + "SessionV1.Part": { "anyOf": [ { - "$ref": "#/components/schemas/TextPart" + "$ref": "#/components/schemas/SessionV1.TextPart" }, { - "$ref": "#/components/schemas/SubtaskPart" + "$ref": "#/components/schemas/SessionV1.SubtaskPart" }, { - "$ref": "#/components/schemas/ReasoningPart" + "$ref": "#/components/schemas/SessionV1.ReasoningPart" }, { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" }, { - "$ref": "#/components/schemas/ToolPart" + "$ref": "#/components/schemas/SessionV1.ToolPart" }, { - "$ref": "#/components/schemas/StepStartPart" + "$ref": "#/components/schemas/SessionV1.StepStartPart" }, { - "$ref": "#/components/schemas/StepFinishPart" + "$ref": "#/components/schemas/SessionV1.StepFinishPart" }, { - "$ref": "#/components/schemas/SnapshotPart" + "$ref": "#/components/schemas/SessionV1.SnapshotPart" }, { - "$ref": "#/components/schemas/PatchPart" + "$ref": "#/components/schemas/SessionV1.PatchPart" }, { - "$ref": "#/components/schemas/AgentPart" + "$ref": "#/components/schemas/SessionV1.AgentPart" }, { - "$ref": "#/components/schemas/RetryPart" + "$ref": "#/components/schemas/SessionV1.RetryPart" }, { - "$ref": "#/components/schemas/CompactionPart" + "$ref": "#/components/schemas/SessionV1.CompactionPart" } ] }, @@ -24847,7 +24986,7 @@ ] }, "part": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/SessionV1.Part" }, "time": { "type": "number" @@ -25472,7 +25611,7 @@ ], "additionalProperties": false }, - "permission.v2.asked": { + "permission.asked": { "type": "object", "properties": { "id": { @@ -25492,7 +25631,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.asked" + "permission.asked" ] }, "location": { @@ -25536,7 +25675,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -25556,7 +25695,7 @@ ], "additionalProperties": false }, - "permission.v2.replied": { + "permission.replied": { "type": "object", "properties": { "id": { @@ -25576,7 +25715,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.replied" + "permission.replied" ] }, "location": { @@ -25602,7 +25741,7 @@ ] }, "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" } }, "required": [ @@ -26327,7 +26466,7 @@ ], "additionalProperties": false }, - "QuestionV2.Option": { + "Question.Option": { "type": "object", "properties": { "label": { @@ -26345,7 +26484,7 @@ ], "additionalProperties": false }, - "QuestionV2.Info": { + "Question.Info": { "type": "object", "properties": { "question": { @@ -26359,7 +26498,7 @@ "options": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Option" + "$ref": "#/components/schemas/Question.Option" }, "description": "Available choices" }, @@ -26377,7 +26516,7 @@ ], "additionalProperties": false }, - "QuestionV2.Tool": { + "Question.Tool": { "type": "object", "properties": { "messageID": { @@ -26393,7 +26532,7 @@ ], "additionalProperties": false }, - "question.v2.asked": { + "question.asked": { "type": "object", "properties": { "id": { @@ -26413,7 +26552,7 @@ "type": { "type": "string", "enum": [ - "question.v2.asked" + "question.asked" ] }, "location": { @@ -26441,12 +26580,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -26465,13 +26604,13 @@ ], "additionalProperties": false }, - "QuestionV2.Answer": { + "Question.Answer": { "type": "array", "items": { "type": "string" } }, - "question.v2.replied": { + "question.replied": { "type": "object", "properties": { "id": { @@ -26491,7 +26630,7 @@ "type": { "type": "string", "enum": [ - "question.v2.replied" + "question.replied" ] }, "location": { @@ -26519,7 +26658,7 @@ "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" } } }, @@ -26539,7 +26678,7 @@ ], "additionalProperties": false }, - "question.v2.rejected": { + "question.rejected": { "type": "object", "properties": { "id": { @@ -26559,7 +26698,7 @@ "type": { "type": "string", "enum": [ - "question.v2.rejected" + "question.rejected" ] }, "location": { @@ -28068,483 +28207,6 @@ ], "additionalProperties": false }, - "permission.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "anyOf": [ - { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "permission.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow selecting multiple choices" - }, - "custom": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow typing a custom answer (default: true)" - } - }, - "required": [ - "question", - "header", - "options" - ], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg" - } - ] - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - "question.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionTool" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.rejected" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - } - }, - "required": [ - "sessionID", - "requestID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, "session.error": { "type": "object", "properties": { @@ -28865,10 +28527,10 @@ "$ref": "#/components/schemas/reference.updated" }, { - "$ref": "#/components/schemas/permission.v2.asked" + "$ref": "#/components/schemas/permission.asked" }, { - "$ref": "#/components/schemas/permission.v2.replied" + "$ref": "#/components/schemas/permission.replied" }, { "$ref": "#/components/schemas/plugin.added" @@ -28910,13 +28572,13 @@ "$ref": "#/components/schemas/shell.deleted" }, { - "$ref": "#/components/schemas/question.v2.asked" + "$ref": "#/components/schemas/question.asked" }, { - "$ref": "#/components/schemas/question.v2.replied" + "$ref": "#/components/schemas/question.replied" }, { - "$ref": "#/components/schemas/question.v2.rejected" + "$ref": "#/components/schemas/question.rejected" }, { "$ref": "#/components/schemas/form.created" @@ -28963,21 +28625,6 @@ { "$ref": "#/components/schemas/mcp.resources.changed" }, - { - "$ref": "#/components/schemas/permission.asked" - }, - { - "$ref": "#/components/schemas/permission.replied" - }, - { - "$ref": "#/components/schemas/question.asked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, { "$ref": "#/components/schemas/session.error" }, @@ -29153,7 +28800,7 @@ ], "additionalProperties": false }, - "QuestionV2.Request": { + "Question.Request": { "type": "object", "properties": { "id": { @@ -29175,12 +28822,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -29190,13 +28837,13 @@ ], "additionalProperties": false }, - "QuestionV2.Reply": { + "Question.Reply": { "type": "object", "properties": { "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" }, "description": "User answers in order of questions (each answer is an array of selected labels)" } diff --git a/packages/protocol/src/errors.ts b/packages/protocol/src/errors.ts index a678fb73461..7b7cde045b6 100644 --- a/packages/protocol/src/errors.ts +++ b/packages/protocol/src/errors.ts @@ -62,6 +62,15 @@ export class ProviderNotFoundError extends Schema.TaggedErrorClass()( + "AgentNotFoundError", + { + agentID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} + export class SessionNotFoundError extends Schema.TaggedErrorClass()( "SessionNotFoundError", { diff --git a/packages/protocol/src/groups/agent.ts b/packages/protocol/src/groups/agent.ts index cfae58533a3..cdfaaf5a72f 100644 --- a/packages/protocol/src/groups/agent.ts +++ b/packages/protocol/src/groups/agent.ts @@ -3,6 +3,7 @@ import { Location } from "@opencode-ai/schema/location" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { LocationQuery, locationQueryOpenApi } from "./location.js" +import { AgentNotFoundError } from "../errors.js" export const AgentGroup = HttpApiGroup.make("server.agent") .add( @@ -19,4 +20,20 @@ export const AgentGroup = HttpApiGroup.make("server.agent") }), ), ) + .add( + HttpApiEndpoint.get("agent.get", "/api/agent/:agentID", { + params: { agentID: Agent.ID }, + query: LocationQuery, + success: Location.response(Agent.Info), + error: AgentNotFoundError, + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.agent.get", + summary: "Get agent", + description: "Retrieve a single currently registered agent.", + }), + ), + ) .annotateMerge(OpenApi.annotations({ title: "agent" })) diff --git a/packages/schema/package.json b/packages/schema/package.json index 445991c4e2b..04551ef505f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -24,6 +24,7 @@ "typecheck": "tsgo --noEmit" }, "dependencies": { + "@standard-schema/spec": "catalog:", "effect": "catalog:" }, "devDependencies": { diff --git a/packages/schema/src/event-manifest.ts b/packages/schema/src/event-manifest.ts index a9c9aaa52e9..084c41dd7ae 100644 --- a/packages/schema/src/event-manifest.ts +++ b/packages/schema/src/event-manifest.ts @@ -12,18 +12,16 @@ import { FileSystemV1 } from "./filesystem-v1.js" import { Form } from "./form.js" import { InstallationEvent } from "./installation-event.js" import { Integration } from "./integration.js" -import { LegacyEvent } from "./legacy-event.js" +import { LegacyEventV1 } from "./legacy-event.js" import { LspEvent } from "./lsp-event.js" import { McpEvent } from "./mcp-event.js" import { ModelsDev } from "./models-dev.js" import { Permission } from "./permission.js" -import { PermissionV1 } from "./permission-v1.js" import { Plugin } from "./plugin.js" import { Project } from "./project.js" import { ProjectDirectories } from "./project-directories.js" import { Pty } from "./pty.js" import { Question } from "./question.js" -import { QuestionV1 } from "./question-v1.js" import { Reference } from "./reference.js" import { ServerEvent } from "./server-event.js" import { Shell } from "./shell.js" @@ -81,11 +79,7 @@ export const ServerDefinitions = Event.inventory( ...VcsEvent.Definitions, McpEvent.StatusChanged, McpEvent.ResourcesChanged, - // Shared transitional: V1 contracts the current TUI still consumes during - // the migration (permission.asked/replied, question.asked, session.error). - // Remove when the TUI moves to the current permission/question surfaces. - ...PermissionV1.Event.Definitions, - ...QuestionV1.Event.Definitions, + // Shared transitional event retained until the TUI moves to the current session error surface. SessionV1.Error, ) export const Server = Event.latest(ServerDefinitions) @@ -98,14 +92,12 @@ export const Definitions = Event.inventory( ...InstallationEvent.Definitions, ...featureDefinitions, ...LspEvent.Definitions, - ...PermissionV1.Event.Definitions, ...TuiEvent.Definitions, ...McpEvent.Definitions, - ...LegacyEvent.Definitions, + ...LegacyEventV1.Definitions, ...FileSystemV1.Event.Definitions, ...Project.Event.Definitions, ...SessionStatusEvent.Definitions, - ...QuestionV1.Event.Definitions, ...SessionCompactionEvent.Definitions, ...VcsEvent.Definitions, ...WorkspaceEvent.Definitions, diff --git a/packages/schema/src/llm.ts b/packages/schema/src/llm.ts index 2bc1d93bd8f..3d4e1514321 100644 --- a/packages/schema/src/llm.ts +++ b/packages/schema/src/llm.ts @@ -10,22 +10,3 @@ export type ProviderMetadata = Schema.Schema.Type export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"]) export type FinishReason = typeof FinishReason.Type - -export interface ToolTextContent extends Schema.Schema.Type {} -export const ToolTextContent = Schema.Struct({ - type: Schema.Literal("text"), - text: Schema.String, -}).annotate({ identifier: "Tool.TextContent" }) - -export interface ToolFileContent extends Schema.Schema.Type {} -export const ToolFileContent = Schema.Struct({ - type: Schema.Literal("file"), - uri: Schema.String, - mime: Schema.String, - name: optional(Schema.String), -}).annotate({ identifier: "Tool.FileContent" }) - -export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "LLM.ToolContent" }) -export type ToolContent = Schema.Schema.Type diff --git a/packages/schema/src/permission.ts b/packages/schema/src/permission.ts index ca79081eda2..e7e7fc7baeb 100644 --- a/packages/schema/src/permission.ts +++ b/packages/schema/src/permission.ts @@ -8,7 +8,7 @@ import { SessionID } from "./session-id.js" import { statics } from "./schema.js" export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( - Schema.brand("PermissionV2.ID"), + Schema.brand("Permission.ID"), statics((schema) => ({ create: (id?: string) => schema.make(id ?? "per_" + ascending()) })), ) export type ID = typeof ID.Type @@ -19,7 +19,7 @@ export const Source = Schema.Union([ messageID: Schema.String, callID: Schema.String, }), -]).annotate({ identifier: "PermissionV2.Source" }) +]).annotate({ identifier: "Permission.Source" }) export type Source = typeof Source.Type const RequestFields = { @@ -34,15 +34,15 @@ const RequestFields = { export const Request = Schema.Struct({ id: ID, ...RequestFields, -}).annotate({ identifier: "PermissionV2.Request" }) +}).annotate({ identifier: "Permission.Request" }) export interface Request extends Schema.Schema.Type {} -export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "PermissionV2.Reply" }) +export const Reply = Schema.Literals(["once", "always", "reject"]).annotate({ identifier: "Permission.Reply" }) export type Reply = typeof Reply.Type -const Asked = ephemeral({ type: "permission.v2.asked", schema: Request.fields }) +const Asked = ephemeral({ type: "permission.asked", schema: Request.fields }) const Replied = ephemeral({ - type: "permission.v2.replied", + type: "permission.replied", schema: { sessionID: SessionID, requestID: ID, @@ -51,7 +51,7 @@ const Replied = ephemeral({ }) export const Event = { Asked, Replied, Definitions: inventory(Asked, Replied) } -export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) +export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "Permission.Effect" }) export type Effect = typeof Effect.Type export interface Rule extends Schema.Schema.Type {} @@ -59,7 +59,7 @@ export const Rule = Schema.Struct({ action: Schema.String, resource: Schema.String, effect: Effect, -}).annotate({ identifier: "PermissionV2.Rule" }) +}).annotate({ identifier: "Permission.Rule" }) -export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" }) +export const Ruleset = Schema.Array(Rule).annotate({ identifier: "Permission.Ruleset" }) export type Ruleset = typeof Ruleset.Type diff --git a/packages/schema/src/provider.ts b/packages/schema/src/provider.ts index 3e98b16990a..69a8d0212ff 100644 --- a/packages/schema/src/provider.ts +++ b/packages/schema/src/provider.ts @@ -5,7 +5,7 @@ import { Integration } from "./integration.js" import { optional, statics } from "./schema.js" export const ID = Schema.String.pipe( - Schema.brand("ProviderV2.ID"), + Schema.brand("Provider.ID"), statics((schema) => ({ opencode: schema.make("opencode"), anthropic: schema.make("anthropic"), @@ -26,19 +26,19 @@ export const Package = Schema.String export type Package = typeof Package.Type export const Overlays = { - settings: Schema.Record(Schema.String, Schema.Json).pipe(optional), + settings: Schema.Record(Schema.String, Schema.Any).pipe(optional), headers: Schema.Record(Schema.String, Schema.String).pipe(optional), - body: Schema.Record(Schema.String, Schema.Json).pipe(optional), + body: Schema.Record(Schema.String, Schema.Any).pipe(optional), } -export const Settings = Schema.Record(Schema.String, Schema.Json).annotate({ identifier: "Provider.Settings" }) +export const Settings = Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "Provider.Settings" }) export type Settings = typeof Settings.Type export interface Request extends Schema.Schema.Type {} export const Request = Schema.Struct({ settings: Settings.pipe(Schema.withConstructorDefault(Effect.succeed({}))), headers: Schema.Record(Schema.String, Schema.String), - body: Schema.Record(Schema.String, Schema.Json), + body: Schema.Record(Schema.String, Schema.Any), }).annotate({ identifier: "Provider.Request" }) export interface Info extends Schema.Schema.Type {} @@ -50,7 +50,7 @@ export const Info = Schema.Struct({ package: Package, ...Overlays, }) - .annotate({ identifier: "ProviderV2.Info" }) + .annotate({ identifier: "Provider.Info" }) .pipe( statics(() => ({ empty: (id: ID): Info => ({ id, name: id, package: "" }), diff --git a/packages/schema/src/question.ts b/packages/schema/src/question.ts index 617ad2a8950..d427cd1eaec 100644 --- a/packages/schema/src/question.ts +++ b/packages/schema/src/question.ts @@ -8,7 +8,7 @@ import { SessionID } from "./session-id.js" import { statics } from "./schema.js" export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( - Schema.brand("QuestionV2.ID"), + Schema.brand("Question.ID"), statics((schema) => { const create = () => schema.make("que_" + ascending()) return { @@ -22,7 +22,7 @@ export type ID = typeof ID.Type export const Option = Schema.Struct({ label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }), description: Schema.String.annotate({ description: "Explanation of choice" }), -}).annotate({ identifier: "QuestionV2.Option" }) +}).annotate({ identifier: "Question.Option" }) export interface Option extends Schema.Schema.Type {} const base = { @@ -37,16 +37,16 @@ export const Info = Schema.Struct({ custom: Schema.Boolean.pipe(optional).annotate({ description: "Allow typing a custom answer (default: true)", }), -}).annotate({ identifier: "QuestionV2.Info" }) +}).annotate({ identifier: "Question.Info" }) export interface Info extends Schema.Schema.Type {} -export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" }) +export const Prompt = Schema.Struct(base).annotate({ identifier: "Question.Prompt" }) export interface Prompt extends Schema.Schema.Type {} export const Tool = Schema.Struct({ messageID: Schema.String, callID: Schema.String, -}).annotate({ identifier: "QuestionV2.Tool" }) +}).annotate({ identifier: "Question.Tool" }) export interface Tool extends Schema.Schema.Type {} export const Request = Schema.Struct({ @@ -54,22 +54,22 @@ export const Request = Schema.Struct({ sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Tool.pipe(optional), -}).annotate({ identifier: "QuestionV2.Request" }) +}).annotate({ identifier: "Question.Request" }) export interface Request extends Schema.Schema.Type {} -export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" }) +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "Question.Answer" }) export type Answer = typeof Answer.Type export const Reply = Schema.Struct({ answers: Schema.Array(Answer).annotate({ description: "User answers in order of questions (each answer is an array of selected labels)", }), -}).annotate({ identifier: "QuestionV2.Reply" }) +}).annotate({ identifier: "Question.Reply" }) export interface Reply extends Schema.Schema.Type {} -const Asked = ephemeral({ type: "question.v2.asked", schema: Request.fields }) +const Asked = ephemeral({ type: "question.asked", schema: Request.fields }) const Replied = ephemeral({ - type: "question.v2.replied", + type: "question.replied", schema: { sessionID: SessionID, requestID: ID, @@ -77,7 +77,7 @@ const Replied = ephemeral({ }, }) const Rejected = ephemeral({ - type: "question.v2.rejected", + type: "question.rejected", schema: { sessionID: SessionID, requestID: ID, diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 3a31643bbbf..6a9258a549b 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -3,7 +3,8 @@ export * as SessionEvent from "./session-event.js" import { Schema } from "effect" import { optional } from "./schema.js" import { Event } from "./event.js" -import { FinishReason, ToolContent } from "./llm.js" +import { FinishReason } from "./llm.js" +import { Content } from "./tool.js" import { Model } from "./model.js" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema.js" import { FileAttachment } from "./prompt.js" @@ -429,7 +430,7 @@ export namespace Tool { }, schema: { ...ToolBase, - content: Schema.NonEmptyArray(ToolContent), + content: Schema.NonEmptyArray(Content), metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), @@ -451,7 +452,7 @@ export namespace Tool { schema: { ...ToolBase, error: SessionError.Error, - content: Schema.NonEmptyArray(ToolContent).pipe(optional), + content: Schema.NonEmptyArray(Content).pipe(optional), metadata: Schema.Record(Schema.String, Schema.Json).pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 4e0858e4055..c6b8fa4acd1 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -2,7 +2,7 @@ export * as SessionMessage from "./session-message.js" import { Schema } from "effect" import { optional } from "./schema.js" -import { ToolContent } from "./llm.js" +import { Content } from "./tool.js" import { Model } from "./model.js" import { Prompt } from "./prompt.js" import { DateTimeUtcFromMillis, PositiveInt, RelativePath, statics } from "./schema.js" @@ -117,7 +117,7 @@ export interface ToolStateCompleted extends Schema.Schema.Type> + +export const CallID = Schema.String.pipe(Schema.brand("Tool.CallID")) +export type CallID = typeof CallID.Type + +export interface Context { + readonly sessionID: Session.ID + readonly agent: Agent.ID + readonly messageID: SessionMessage.ID + readonly callID: CallID + readonly progress: (update: Metadata) => Effect.Effect +} + +export interface Options { + readonly namespace?: string + readonly codemode?: boolean + readonly permission?: string +} + +export type ValueSchema = + | Schema.Codec + | (StandardSchemaV1 & StandardJSONSchemaV1) + | JsonSchema.JsonSchema + +type InputValue = 0 extends 1 & S + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : unknown +type OutputValue = S extends undefined + ? never + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : any + +export class Error extends Schema.TaggedErrorClass()("Tool.Error", { + message: Schema.String, + error: Schema.optional(Schema.Defect()), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}) {} + +export interface TextContent extends Schema.Schema.Type {} +export const TextContent = Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, +}).annotate({ identifier: "Tool.TextContent" }) + +export interface FileContent extends Schema.Schema.Type {} +export const FileContent = Schema.Struct({ + type: Schema.Literal("file"), + uri: Schema.String, + mime: Schema.String, + name: Schema.optional(Schema.String), +}).annotate({ identifier: "Tool.FileContent" }) + +export const Content = Schema.Union([TextContent, FileContent]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Tool.Content" }) +export type Content = Schema.Schema.Type + +export interface Result | undefined = ValueSchema | undefined> { + readonly output?: OutputValue + readonly content?: string | ReadonlyArray + readonly metadata?: Metadata +} + +export type Info< + Input extends ValueSchema = ValueSchema, + Output extends ValueSchema | undefined = ValueSchema | undefined, +> = { + readonly name: string + readonly input: Input + readonly description: string + readonly execute: (input: InputValue, context: Context) => Effect.Effect, Error> + readonly output?: Output + readonly options?: Options +} diff --git a/packages/schema/src/v1/legacy-event.ts b/packages/schema/src/v1/legacy-event.ts index ac22fff5e39..8c36a696752 100644 --- a/packages/schema/src/v1/legacy-event.ts +++ b/packages/schema/src/v1/legacy-event.ts @@ -1,4 +1,4 @@ -export * as LegacyEvent from "./legacy-event.js" +export * as LegacyEventV1 from "./legacy-event.js" import { Schema } from "effect" import { ephemeral, inventory } from "../event.js" diff --git a/packages/schema/src/v1/permission.ts b/packages/schema/src/v1/permission.ts index 1faa46a5083..c7d2e71b3da 100644 --- a/packages/schema/src/v1/permission.ts +++ b/packages/schema/src/v1/permission.ts @@ -8,20 +8,20 @@ import { statics } from "../schema.js" import { SessionID } from "../session-id.js" export const ID = Schema.String.check(Schema.isStartsWith("per")).pipe( - Schema.brand("PermissionID"), + Schema.brand("PermissionV1.ID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "per_" + ascending()) })), ) export type ID = typeof ID.Type -export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionAction" }) +export const Action = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV1.Action" }) export type Action = typeof Action.Type export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action }).annotate({ - identifier: "PermissionRule", + identifier: "PermissionV1.Rule", }) export type Rule = typeof Rule.Type -export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionRuleset" }) +export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV1.Ruleset" }) export type Ruleset = typeof Ruleset.Type export const Request = Schema.Struct({ @@ -32,29 +32,29 @@ export const Request = Schema.Struct({ metadata: Schema.Record(Schema.String, Schema.Unknown), always: Schema.Array(Schema.String), tool: Schema.optional(Schema.Struct({ messageID: Schema.String, callID: Schema.String })), -}).annotate({ identifier: "PermissionRequest" }) +}).annotate({ identifier: "PermissionV1.Request" }) export type Request = typeof Request.Type export const Reply = Schema.Literals(["once", "always", "reject"]) export type Reply = typeof Reply.Type export const ReplyBody = Schema.Struct({ reply: Reply, message: Schema.optional(Schema.String) }).annotate({ - identifier: "PermissionReplyBody", + identifier: "PermissionV1.ReplyBody", }) export type ReplyBody = typeof ReplyBody.Type export const Approval = Schema.Struct({ projectID: Project.ID, patterns: Schema.Array(Schema.String) }).annotate({ - identifier: "PermissionApproval", + identifier: "PermissionV1.Approval", }) export type Approval = typeof Approval.Type export const AskInput = Schema.Struct({ ...Request.fields, id: Schema.optional(ID), ruleset: Ruleset }).annotate({ - identifier: "PermissionAskInput", + identifier: "PermissionV1.AskInput", }) export type AskInput = typeof AskInput.Type export const ReplyInput = Schema.Struct({ requestID: ID, ...ReplyBody.fields }).annotate({ - identifier: "PermissionReplyInput", + identifier: "PermissionV1.ReplyInput", }) export type ReplyInput = typeof ReplyInput.Type diff --git a/packages/schema/src/v1/question.ts b/packages/schema/src/v1/question.ts index da47f37ffd7..dfd6dc8ee21 100644 --- a/packages/schema/src/v1/question.ts +++ b/packages/schema/src/v1/question.ts @@ -8,14 +8,14 @@ import { SessionID } from "../session-id.js" import { SessionV1 } from "./session.js" export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( - Schema.brand("QuestionID"), + Schema.brand("QuestionV1.ID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })), ) export const Option = Schema.Struct({ label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }), description: Schema.String.annotate({ description: "Explanation of choice" }), -}).annotate({ identifier: "QuestionOption" }) +}).annotate({ identifier: "QuestionV1.Option" }) const base = { question: Schema.String.annotate({ description: "Complete question" }), @@ -27,32 +27,32 @@ const base = { export const Info = Schema.Struct({ ...base, custom: Schema.optional(Schema.Boolean).annotate({ description: "Allow typing a custom answer (default: true)" }), -}).annotate({ identifier: "QuestionInfo" }) -export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" }) +}).annotate({ identifier: "QuestionV1.Info" }) +export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV1.Prompt" }) export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Schema.String }).annotate({ - identifier: "QuestionTool", + identifier: "QuestionV1.Tool", }) export const Request = Schema.Struct({ id: ID, sessionID: SessionID, questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), tool: Schema.optional(Tool), -}).annotate({ identifier: "QuestionRequest" }) -export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) +}).annotate({ identifier: "QuestionV1.Request" }) +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV1.Answer" }) export const Reply = Schema.Struct({ answers: Schema.Array(Answer).annotate({ description: "User answers in order of questions (each answer is an array of selected labels)", }), -}).annotate({ identifier: "QuestionReply" }) +}).annotate({ identifier: "QuestionV1.Reply" }) export const Replied = Schema.Struct({ sessionID: SessionID, requestID: ID, answers: Schema.Array(Answer), }).annotate({ - identifier: "QuestionReplied", + identifier: "QuestionV1.Replied", }) export const Rejected = Schema.Struct({ sessionID: SessionID, requestID: ID }).annotate({ - identifier: "QuestionRejected", + identifier: "QuestionV1.Rejected", }) const Asked = ephemeral({ type: "question.asked", schema: Request.fields }) diff --git a/packages/schema/src/v1/session.ts b/packages/schema/src/v1/session.ts index 3932cae88a2..9da5efaf66c 100644 --- a/packages/schema/src/v1/session.ts +++ b/packages/schema/src/v1/session.ts @@ -15,13 +15,13 @@ import { FileDiff } from "../file-diff.js" const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( - Schema.brand("MessageID"), + Schema.brand("SessionV1.MessageID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })), ) export type MessageID = typeof MessageID.Type export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( - Schema.brand("PartID"), + Schema.brand("SessionV1.PartID"), statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })), ) export type PartID = typeof PartID.Type @@ -68,13 +68,13 @@ export class OutputFormatText extends Schema.Class("OutputForm export class OutputFormatJsonSchema extends Schema.Class("OutputFormatJsonSchema")({ type: Schema.Literal("json_schema"), - schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }), + schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "SessionV1.JSONSchema" }), retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))), }) {} export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({ discriminator: "type", - identifier: "OutputFormat", + identifier: "SessionV1.OutputFormat", }) export type OutputFormat = Schema.Schema.Type @@ -88,7 +88,7 @@ export const SnapshotPart = Schema.Struct({ ...partBase, type: Schema.Literal("snapshot"), snapshot: Schema.String, -}).annotate({ identifier: "SnapshotPart" }) +}).annotate({ identifier: "SessionV1.SnapshotPart" }) export type SnapshotPart = Types.DeepMutable> export const PatchPart = Schema.Struct({ @@ -96,7 +96,7 @@ export const PatchPart = Schema.Struct({ type: Schema.Literal("patch"), hash: Schema.String, files: Schema.Array(Schema.String), -}).annotate({ identifier: "PatchPart" }) +}).annotate({ identifier: "SessionV1.PatchPart" }) export type PatchPart = Types.DeepMutable> export const TextPart = Schema.Struct({ @@ -112,7 +112,7 @@ export const TextPart = Schema.Struct({ }), ), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}).annotate({ identifier: "TextPart" }) +}).annotate({ identifier: "SessionV1.TextPart" }) export type TextPart = Types.DeepMutable> export const ReasoningPart = Schema.Struct({ @@ -124,7 +124,7 @@ export const ReasoningPart = Schema.Struct({ start: NonNegativeInt, end: Schema.optional(NonNegativeInt), }), -}).annotate({ identifier: "ReasoningPart" }) +}).annotate({ identifier: "SessionV1.ReasoningPart" }) export type ReasoningPart = Types.DeepMutable> const filePartSourceBase = { @@ -132,20 +132,20 @@ const filePartSourceBase = { value: Schema.String, start: Schema.Finite, end: Schema.Finite, - }).annotate({ identifier: "FilePartSourceText" }), + }).annotate({ identifier: "SessionV1.FilePartSourceText" }), } export const Range = Schema.Struct({ start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), -}).annotate({ identifier: "Range" }) +}).annotate({ identifier: "SessionV1.Range" }) export type Range = typeof Range.Type export const FileSource = Schema.Struct({ ...filePartSourceBase, type: Schema.Literal("file"), path: Schema.String, -}).annotate({ identifier: "FileSource" }) +}).annotate({ identifier: "SessionV1.FileSource" }) export const SymbolSource = Schema.Struct({ ...filePartSourceBase, @@ -154,18 +154,18 @@ export const SymbolSource = Schema.Struct({ range: Range, name: Schema.String, kind: NonNegativeInt, -}).annotate({ identifier: "SymbolSource" }) +}).annotate({ identifier: "SessionV1.SymbolSource" }) export const ResourceSource = Schema.Struct({ ...filePartSourceBase, type: Schema.Literal("resource"), clientName: Schema.String, uri: Schema.String, -}).annotate({ identifier: "ResourceSource" }) +}).annotate({ identifier: "SessionV1.ResourceSource" }) export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({ discriminator: "type", - identifier: "FilePartSource", + identifier: "SessionV1.FilePartSource", }) export const FilePart = Schema.Struct({ @@ -175,7 +175,7 @@ export const FilePart = Schema.Struct({ filename: Schema.optional(Schema.String), url: Schema.String, source: Schema.optional(FilePartSource), -}).annotate({ identifier: "FilePart" }) +}).annotate({ identifier: "SessionV1.FilePart" }) export type FilePart = Types.DeepMutable> export const AgentPart = Schema.Struct({ @@ -189,7 +189,7 @@ export const AgentPart = Schema.Struct({ end: NonNegativeInt, }), ), -}).annotate({ identifier: "AgentPart" }) +}).annotate({ identifier: "SessionV1.AgentPart" }) export type AgentPart = Types.DeepMutable> export const CompactionPart = Schema.Struct({ @@ -198,7 +198,7 @@ export const CompactionPart = Schema.Struct({ auto: Schema.Boolean, overflow: Schema.optional(Schema.Boolean), tail_start_id: Schema.optional(MessageID), -}).annotate({ identifier: "CompactionPart" }) +}).annotate({ identifier: "SessionV1.CompactionPart" }) export type CompactionPart = Types.DeepMutable> export const SubtaskPart = Schema.Struct({ @@ -214,7 +214,7 @@ export const SubtaskPart = Schema.Struct({ }), ), command: Schema.optional(Schema.String), -}).annotate({ identifier: "SubtaskPart" }) +}).annotate({ identifier: "SessionV1.SubtaskPart" }) export type SubtaskPart = Types.DeepMutable> export const RetryPart = Schema.Struct({ @@ -225,7 +225,7 @@ export const RetryPart = Schema.Struct({ time: Schema.Struct({ created: NonNegativeInt, }), -}).annotate({ identifier: "RetryPart" }) +}).annotate({ identifier: "SessionV1.RetryPart" }) export type RetryPart = Omit>, "error"> & { error: APIError } @@ -234,7 +234,7 @@ export const StepStartPart = Schema.Struct({ ...partBase, type: Schema.Literal("step-start"), snapshot: Schema.optional(Schema.String), -}).annotate({ identifier: "StepStartPart" }) +}).annotate({ identifier: "SessionV1.StepStartPart" }) export type StepStartPart = Types.DeepMutable> export const StepFinishPart = Schema.Struct({ @@ -253,14 +253,14 @@ export const StepFinishPart = Schema.Struct({ write: Schema.Finite, }), }), -}).annotate({ identifier: "StepFinishPart" }) +}).annotate({ identifier: "SessionV1.StepFinishPart" }) export type StepFinishPart = Types.DeepMutable> export const ToolStatePending = Schema.Struct({ status: Schema.Literal("pending"), input: Schema.Record(Schema.String, Schema.Any), raw: Schema.String, -}).annotate({ identifier: "ToolStatePending" }) +}).annotate({ identifier: "SessionV1.ToolStatePending" }) export type ToolStatePending = Types.DeepMutable> export const ToolStateRunning = Schema.Struct({ @@ -271,7 +271,7 @@ export const ToolStateRunning = Schema.Struct({ time: Schema.Struct({ start: NonNegativeInt, }), -}).annotate({ identifier: "ToolStateRunning" }) +}).annotate({ identifier: "SessionV1.ToolStateRunning" }) export type ToolStateRunning = Types.DeepMutable> export const ToolStateCompleted = Schema.Struct({ @@ -286,7 +286,7 @@ export const ToolStateCompleted = Schema.Struct({ compacted: Schema.optional(NonNegativeInt), }), attachments: Schema.optional(Schema.Array(FilePart)), -}).annotate({ identifier: "ToolStateCompleted" }) +}).annotate({ identifier: "SessionV1.ToolStateCompleted" }) export type ToolStateCompleted = Types.DeepMutable> export const ToolStateError = Schema.Struct({ @@ -298,7 +298,7 @@ export const ToolStateError = Schema.Struct({ start: NonNegativeInt, end: NonNegativeInt, }), -}).annotate({ identifier: "ToolStateError" }) +}).annotate({ identifier: "SessionV1.ToolStateError" }) export type ToolStateError = Types.DeepMutable> export const ToolState = Schema.Union([ @@ -308,7 +308,7 @@ export const ToolState = Schema.Union([ ToolStateError, ]).annotate({ discriminator: "status", - identifier: "ToolState", + identifier: "SessionV1.ToolState", }) export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError @@ -319,7 +319,7 @@ export const ToolPart = Schema.Struct({ tool: Schema.String, state: ToolState, metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}).annotate({ identifier: "ToolPart" }) +}).annotate({ identifier: "SessionV1.ToolPart" }) export type ToolPart = Omit>, "state"> & { state: ToolState } @@ -351,7 +351,7 @@ export const User = Schema.Struct({ }), system: Schema.optional(Schema.String), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), -}).annotate({ identifier: "UserMessage" }) +}).annotate({ identifier: "SessionV1.UserMessage" }) export type User = Types.DeepMutable> export const Part = Schema.Union([ @@ -367,7 +367,7 @@ export const Part = Schema.Union([ AgentPart, RetryPart, CompactionPart, -]).annotate({ discriminator: "type", identifier: "Part" }) +]).annotate({ discriminator: "type", identifier: "SessionV1.Part" }) export type Part = | TextPart | SubtaskPart @@ -407,7 +407,7 @@ export const TextPartInput = Schema.Struct({ }), ), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}).annotate({ identifier: "TextPartInput" }) +}).annotate({ identifier: "SessionV1.TextPartInput" }) export type TextPartInput = Types.DeepMutable> export const FilePartInput = Schema.Struct({ @@ -417,7 +417,7 @@ export const FilePartInput = Schema.Struct({ filename: Schema.optional(Schema.String), url: Schema.String, source: Schema.optional(FilePartSource), -}).annotate({ identifier: "FilePartInput" }) +}).annotate({ identifier: "SessionV1.FilePartInput" }) export type FilePartInput = Types.DeepMutable> export const AgentPartInput = Schema.Struct({ @@ -431,7 +431,7 @@ export const AgentPartInput = Schema.Struct({ end: NonNegativeInt, }), ), -}).annotate({ identifier: "AgentPartInput" }) +}).annotate({ identifier: "SessionV1.AgentPartInput" }) export type AgentPartInput = Types.DeepMutable> export const SubtaskPartInput = Schema.Struct({ @@ -447,7 +447,7 @@ export const SubtaskPartInput = Schema.Struct({ }), ), command: Schema.optional(Schema.String), -}).annotate({ identifier: "SubtaskPartInput" }) +}).annotate({ identifier: "SessionV1.SubtaskPartInput" }) export type SubtaskPartInput = Types.DeepMutable> export const Assistant = Schema.Struct({ @@ -482,12 +482,12 @@ export const Assistant = Schema.Struct({ structured: Schema.optional(Schema.Any), variant: Schema.optional(Schema.String), finish: Schema.optional(Schema.String), -}).annotate({ identifier: "AssistantMessage" }) +}).annotate({ identifier: "SessionV1.AssistantMessage" }) export type Assistant = Omit>, "error"> & { error?: AssistantError } -export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" }) +export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "SessionV1.Message" }) export type Info = User | Assistant export const WithParts = Schema.Struct({ diff --git a/packages/schema/src/workspace-id.ts b/packages/schema/src/workspace-id.ts index 913fc8e2a24..ad2ed5a7129 100644 --- a/packages/schema/src/workspace-id.ts +++ b/packages/schema/src/workspace-id.ts @@ -3,7 +3,7 @@ import { ascending } from "./identifier.js" import { statics } from "./schema.js" export const WorkspaceID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( - Schema.brand("WorkspaceV2.ID"), + Schema.brand("Workspace.ID"), statics((schema) => { const create = () => schema.make("wrk_" + ascending()) return { diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index 46784ee27d5..f9b379e8e08 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -84,14 +84,14 @@ describe("contract hygiene", () => { test("model defaults and provider overlays preserve public invariants", () => { const id = Model.ID.make("model") expect(Model.Info.default(Provider.ID.make("provider"), id)).toMatchObject({ modelID: id, variants: [] }) - expect(() => + expect( Schema.decodeUnknownSync(Provider.Info)({ id: "provider", name: "Provider", package: "native", - settings: { invalid: 1n }, - }), - ).toThrow() + settings: { arbitrary: 1n }, + }).settings, + ).toEqual({ arbitrary: 1n }) }) test("current ID constructors expose create", () => { @@ -137,15 +137,19 @@ describe("contract hygiene", () => { expect(new Set(identifiers).size).toBe(identifiers.length) }) - test("current source avoids Any and mutable contract wrappers", async () => { + test("current source limits Any to provider options and avoids mutable contract wrappers", async () => { const files = [...new Bun.Glob("*.ts").scanSync(new URL("../src", import.meta.url).pathname)].filter( (file) => !file.endsWith("-v1.ts"), ) - const source = await Promise.all( - files.map((file) => Bun.file(new URL(`../src/${file}`, import.meta.url)).text()), - ).then((values) => values.join("\n")) + const sources = await Promise.all( + files.map(async (file) => ({ file, source: await Bun.file(new URL(`../src/${file}`, import.meta.url)).text() })), + ) + const source = sources.map((item) => item.source).join("\n") - expect(source).not.toContain("Schema.Any") + expect(sources.filter((item) => item.file !== "provider.ts").map((item) => item.source).join("\n")).not.toContain( + "Schema.Any", + ) + expect(sources.find((item) => item.file === "provider.ts")?.source.match(/Schema\.Any/g)).toHaveLength(4) expect(source).not.toContain("Schema.mutable") }) diff --git a/packages/schema/test/legacy-event.test.ts b/packages/schema/test/legacy-event.test.ts index c9c8f56e49a..b8723de3ccb 100644 --- a/packages/schema/test/legacy-event.test.ts +++ b/packages/schema/test/legacy-event.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { LegacyEvent } from "../src/legacy-event.js" +import { LegacyEventV1 } from "../src/legacy-event.js" import { PermissionV1 } from "../src/permission-v1.js" import { QuestionV1 } from "../src/question-v1.js" import { Project } from "../src/project.js" @@ -36,7 +36,7 @@ describe("legacy public event schemas", () => { QuestionV1.Event.Replied.type, QuestionV1.Event.Rejected.type, Project.Event.Updated.type, - LegacyEvent.CommandExecuted.type, + LegacyEventV1.CommandExecuted.type, ]).toEqual([ "message.part.delta", "session.diff", diff --git a/packages/schema/test/v1-isolation.test.ts b/packages/schema/test/v1-isolation.test.ts index f605e74ce40..f38b5b76fcb 100644 --- a/packages/schema/test/v1-isolation.test.ts +++ b/packages/schema/test/v1-isolation.test.ts @@ -1,18 +1,18 @@ import { expect, test } from "bun:test" -import { LegacyEvent } from "../src/legacy-event.js" +import { LegacyEventV1 } from "../src/legacy-event.js" import { PermissionV1 } from "../src/permission-v1.js" import { QuestionV1 } from "../src/question-v1.js" import { SessionV1 } from "../src/session-v1.js" -import { LegacyEvent as IsolatedLegacyEvent } from "../src/v1/legacy-event.js" -import { PermissionV1 as IsolatedPermissionV1 } from "../src/v1/permission.js" -import { QuestionV1 as IsolatedQuestionV1 } from "../src/v1/question.js" -import { SessionV1 as IsolatedSessionV1 } from "../src/v1/session.js" +const isolatedLegacyEvent = await import("../src/v1/legacy-event.js") +const isolatedPermission = await import("../src/v1/permission.js") +const isolatedQuestion = await import("../src/v1/question.js") +const isolatedSession = await import("../src/v1/session.js") test("compatibility entrypoints preserve isolated V1 schema identity", () => { - expect(LegacyEvent).toBe(IsolatedLegacyEvent) - expect(PermissionV1).toBe(IsolatedPermissionV1) - expect(QuestionV1).toBe(IsolatedQuestionV1) - expect(SessionV1).toBe(IsolatedSessionV1) + expect(LegacyEventV1).toBe(isolatedLegacyEvent.LegacyEventV1) + expect(PermissionV1).toBe(isolatedPermission.PermissionV1) + expect(QuestionV1).toBe(isolatedQuestion.QuestionV1) + expect(SessionV1).toBe(isolatedSession.SessionV1) }) test("current source does not import the V1 subtree directly", async () => { diff --git a/packages/sdk-next/src/tool.ts b/packages/sdk-next/src/tool.ts index 978ea6d0cbf..af878df5fd5 100644 --- a/packages/sdk-next/src/tool.ts +++ b/packages/sdk-next/src/tool.ts @@ -1,2 +1,3 @@ -export { Failure, RegistrationError, make } from "@opencode-ai/plugin/v2/effect/tool" -export type { Any, Content, Context, Tool } from "@opencode-ai/plugin/v2/effect/tool" +export { RegistrationError } from "@opencode-ai/core/tool" +export { Error } from "@opencode-ai/schema/tool" +export type { Context, Info } from "@opencode-ai/schema/tool" diff --git a/packages/sdk-next/test/contract-identity.test.ts b/packages/sdk-next/test/contract-identity.test.ts index 0f662af27b0..afba8f58add 100644 --- a/packages/sdk-next/test/contract-identity.test.ts +++ b/packages/sdk-next/test/contract-identity.test.ts @@ -1,9 +1,5 @@ import { expect, test } from "bun:test" -import { AgentV2 } from "@opencode-ai/core/agent" import { Location as CoreLocation } from "@opencode-ai/core/location" -import { ModelV2 } from "@opencode-ai/core/model" -import { ProjectV2 } from "@opencode-ai/core/project" -import { SessionV2 } from "@opencode-ai/core/session" import { SessionPending as CoreSessionPending } from "@opencode-ai/core/session/pending" import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message" import { Agent } from "@opencode-ai/schema/agent" @@ -21,6 +17,10 @@ import { ClientApi, groupNames, promiseOmitEndpoints } from "@opencode-ai/protoc import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" const SDK = await import("../src/index") +const CoreAgent = await import("@opencode-ai/core/agent") +const CoreModel = await import("@opencode-ai/core/model") +const CoreProject = await import("@opencode-ai/core/project") +const CoreSession = await import("@opencode-ai/core/session") test("re-exports canonical contracts directly from Schema", () => { expect(SDK.Agent).toBe(Agent) @@ -59,13 +59,13 @@ test("re-exports canonical contracts directly from Schema", () => { }) test("Core and Server reuse the authoritative Schema and Protocol values", () => { - expect(AgentV2.ID).toBe(Agent.ID) + expect(CoreAgent.ID).toBe(Agent.ID) expect(CoreLocation.Ref).toBe(Location.Ref) - expect(ModelV2.Ref).toBe(Model.Ref) - expect(SessionV2.Info).toBe(Session.Info) - expect(ProjectV2.Current).toBe(Project.Current) - expect(ProjectV2.Directory).toBe(Project.Directory) - expect(ProjectV2.Directories).toBe(Project.Directories) + expect(CoreModel.Ref).toBe(Model.Ref) + expect(CoreSession.Info).toBe(Session.Info) + expect(CoreProject.Current).toBe(Project.Current) + expect(CoreProject.Directory).toBe(Project.Directory) + expect(CoreProject.Directories).toBe(Project.Directories) expect(CoreSessionPending.Message).toBe(SessionPending.Message) expect(CoreSessionPending.User).toBe(SessionPending.User) expect(CoreSessionPending.Synthetic).toBe(SessionPending.Synthetic) diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index bff122bcad7..2f229c4efeb 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/packages/sdk-next/test/embedded.test.ts @@ -72,8 +72,8 @@ it.live( yield* ctx.tool .transform((draft) => draft.add( - "bootstrap_sdk_tool", - fixture.sdk.Tool.make({ + ({ + name: "bootstrap_sdk_tool", description: "Marks the initial Location plugin generation", input: Schema.Struct({}), output: Schema.Void, @@ -99,8 +99,8 @@ it.live( yield* ctx.tool .transform((draft) => draft.add( - "late_sdk_tool", - fixture.sdk.Tool.make({ + ({ + name: "late_sdk_tool", description: "Tool registered after Location boot", input: Schema.Struct({}), output: Schema.Void, @@ -220,8 +220,8 @@ it.live( ctx.tool .transform((draft) => draft.add( - "embedded_tool", - fixture.sdk.Tool.make({ + ({ + name: "embedded_tool", description: "Embedded test tool", input: Schema.Struct({}), output: Schema.Struct({ ok: Schema.Boolean }), diff --git a/packages/server/src/event-feed.ts b/packages/server/src/event-feed.ts index 7b668df5b42..d7891eadd12 100644 --- a/packages/server/src/event-feed.ts +++ b/packages/server/src/event-feed.ts @@ -1,6 +1,7 @@ export * as EventFeed from "./event-feed" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { isOpenCodeEvent, OpenCodeEvent } from "@opencode-ai/protocol/groups/event" import { Cause, Context, Effect, Layer, Queue, Schema, Scope, Stream } from "effect" @@ -12,7 +13,7 @@ export class SubscriberOverflowError extends Schema.TaggedErrorClass()("EventFeed.EncodingError", { - eventID: EventV2.ID, + eventID: Event.ID, eventType: Schema.String, cause: Schema.Defect(), }) {} @@ -32,7 +33,7 @@ export function frame(event: OpenCodeEvent) { } export const make = Effect.fn("EventFeed.make")(function* ( - observe: (subscriber: EventV2.Subscriber) => Effect.Effect, + observe: (subscriber: Bus.Subscriber) => Effect.Effect, options?: { readonly capacity?: number; readonly encode?: (event: OpenCodeEvent) => string }, ) { const capacity = options?.capacity ?? SubscriberCapacity @@ -46,7 +47,7 @@ export const make = Effect.fn("EventFeed.make")(function* ( for (const subscriber of current) Queue.failCauseUnsafe(subscriber, Cause.fail(error)) }) - const publish = Effect.fnUntraced(function* (event: EventV2.Payload) { + const publish = Effect.fnUntraced(function* (event: Event.Payload) { if (!isOpenCodeEvent(event)) return if (subscribers.size === 0) return const encoded = yield* Effect.try({ @@ -84,7 +85,7 @@ export const make = Effect.fn("EventFeed.make")(function* ( export const layer = Layer.effect( Service, Effect.gen(function* () { - const events = yield* EventV2.Service - return yield* make(events.listen) + const bus = yield* Bus.Service + return yield* make(bus.listen) }), ) diff --git a/packages/server/src/handlers/agent.ts b/packages/server/src/handlers/agent.ts index 8d7dcb8b3c8..d395b01bb39 100644 --- a/packages/server/src/handlers/agent.ts +++ b/packages/server/src/handlers/agent.ts @@ -1,13 +1,27 @@ -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" import { response } from "../location" +import { AgentNotFoundError } from "@opencode-ai/protocol/errors" export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) => - handlers.handle("agent.list", () => - Effect.gen(function* () { - return yield* response(AgentV2.Service.use((agent) => agent.list())) - }), - ), + handlers + .handle("agent.list", () => + Effect.gen(function* () { + return yield* response(Agent.Service.use((agent) => agent.list())) + }), + ) + .handle( + "agent.get", + Effect.fn(function* (ctx) { + const agent = yield* Agent.Service.use((service) => service.get(ctx.params.agentID)) + if (!agent) + return yield* new AgentNotFoundError({ + agentID: ctx.params.agentID, + message: `Agent not found: ${ctx.params.agentID}`, + }) + return yield* response(Effect.succeed(agent)) + }), + ), ) diff --git a/packages/server/src/handlers/command.ts b/packages/server/src/handlers/command.ts index bf41e79f835..8e3f01b3c70 100644 --- a/packages/server/src/handlers/command.ts +++ b/packages/server/src/handlers/command.ts @@ -1,8 +1,8 @@ -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" import { response } from "../location" export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) => - handlers.handle("command.list", () => response(CommandV2.Service.use((command) => command.list()))), + handlers.handle("command.list", () => response(Command.Service.use((command) => command.list()))), ) diff --git a/packages/server/src/handlers/event.ts b/packages/server/src/handlers/event.ts index feeba74288d..a0d1b81cd3b 100644 --- a/packages/server/src/handlers/event.ts +++ b/packages/server/src/handlers/event.ts @@ -1,4 +1,5 @@ -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { Effect, Stream } from "effect" import { HttpServerResponse } from "effect/unstable/http" import { HttpApiBuilder } from "effect/unstable/httpapi" @@ -11,7 +12,7 @@ export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) return handlers.handleRaw("event.subscribe", () => Effect.gen(function* () { const connected = { - id: EventV2.ID.create(), + id: Event.ID.create(), type: "server.connected", data: {}, } as const diff --git a/packages/server/src/handlers/message.ts b/packages/server/src/handlers/message.ts index 40e2ba1077f..8bff2191b06 100644 --- a/packages/server/src/handlers/message.ts +++ b/packages/server/src/handlers/message.ts @@ -1,5 +1,5 @@ import { SessionMessage } from "@opencode-ai/core/session/message" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Effect, Schema } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" @@ -26,7 +26,7 @@ const cursor = { export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handlers) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service return handlers.handle( "session.messages", diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index cbc49180f64..53aea5b310e 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -1,5 +1,5 @@ import { Location } from "@opencode-ai/core/location" -import { PermissionV2 } from "@opencode-ai/core/permission" +import { Permission } from "@opencode-ai/core/permission" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" @@ -7,7 +7,7 @@ import { Api } from "../api" import { PermissionNotFoundError, SessionNotFoundError } from "@opencode-ai/protocol/errors" import { response } from "../location" -function missingRequest(id: PermissionV2.ID) { +function missingRequest(id: Permission.ID) { return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` }) } @@ -17,14 +17,14 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "permission.request.list", Effect.fn(function* () { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service return yield* response(permission.list()) }), ) .handle( "session.permission.create", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service return { data: yield* permission .ask({ @@ -53,14 +53,14 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "session.permission.list", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service return { data: yield* permission.forSession(ctx.params.sessionID) } }), ) .handle( "session.permission.get", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const request = yield* permission.get(ctx.params.requestID) if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID) return { data: request } @@ -69,12 +69,12 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", .handle( "session.permission.reply", Effect.fn(function* (ctx) { - const permission = yield* PermissionV2.Service + const permission = yield* Permission.Service const request = yield* permission.get(ctx.params.requestID) if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID) yield* permission .reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message }) - .pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID))) + .pipe(Effect.catchTag("Permission.NotFoundError", () => missingRequest(ctx.params.requestID))) return HttpApiSchema.NoContent.make() }), ) diff --git a/packages/server/src/handlers/plugin.ts b/packages/server/src/handlers/plugin.ts index dd8da6a3d93..0a7f200f9ef 100644 --- a/packages/server/src/handlers/plugin.ts +++ b/packages/server/src/handlers/plugin.ts @@ -1,4 +1,4 @@ -import { PluginV2 } from "@opencode-ai/core/plugin" +import { Plugin } from "@opencode-ai/core/plugin" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" @@ -7,7 +7,7 @@ import { response } from "../location" export const PluginHandler = HttpApiBuilder.group(Api, "server.plugin", (handlers) => handlers.handle("plugin.list", () => Effect.gen(function* () { - return yield* response(PluginV2.Service.use((plugin) => plugin.list())) + return yield* response(Plugin.Service.use((plugin) => plugin.list())) }), ), ) diff --git a/packages/server/src/handlers/question.ts b/packages/server/src/handlers/question.ts index d8011dd2697..7229de0ae58 100644 --- a/packages/server/src/handlers/question.ts +++ b/packages/server/src/handlers/question.ts @@ -1,22 +1,22 @@ -import { QuestionV2 } from "@opencode-ai/core/question" +import { Question } from "@opencode-ai/core/question" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { Api } from "../api" import { QuestionNotFoundError } from "@opencode-ai/protocol/errors" import { response } from "../location" -function missingRequest(id: QuestionV2.ID) { +function missingRequest(id: Question.ID) { return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` }) } export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (handlers) => Effect.gen(function* () { const withOwnedQuestion = Effect.fnUntraced(function* ( - sessionID: QuestionV2.Request["sessionID"], - requestID: QuestionV2.ID, - use: (question: QuestionV2.Interface) => Effect.Effect, + sessionID: Question.Request["sessionID"], + requestID: Question.ID, + use: (question: Question.Interface) => Effect.Effect, ) { - const question = yield* QuestionV2.Service + const question = yield* Question.Service const request = (yield* question.list()).find((request) => request.id === requestID) if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID) return yield* use(question) @@ -26,14 +26,14 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han .handle( "question.request.list", Effect.fn(function* () { - const question = yield* QuestionV2.Service + const question = yield* Question.Service return yield* response(question.list()) }), ) .handle( "session.question.list", Effect.fn(function* (ctx) { - const question = yield* QuestionV2.Service + const question = yield* Question.Service const requests = yield* question.list() return { data: requests.filter((request) => request.sessionID === ctx.params.sessionID) } }), @@ -44,7 +44,7 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) => question .reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers }) - .pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))), + .pipe(Effect.catchTag("Question.NotFoundError", () => missingRequest(ctx.params.requestID))), ) return HttpApiSchema.NoContent.make() }), @@ -55,7 +55,7 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) => question .reject(ctx.params.requestID) - .pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))), + .pipe(Effect.catchTag("Question.NotFoundError", () => missingRequest(ctx.params.requestID))), ) return HttpApiSchema.NoContent.make() }), diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 02d1c8d80ab..796116da01b 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -1,4 +1,4 @@ -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { InstructionEntry } from "@opencode-ai/core/session/instruction-entry" import { DateTime, Effect, Stream } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" @@ -23,7 +23,7 @@ const DefaultSessionsLimit = 50 export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) => Effect.gen(function* () { - const session = yield* SessionV2.Service + const session = yield* Session.Service return handlers .handle( diff --git a/packages/server/src/handlers/skill.ts b/packages/server/src/handlers/skill.ts index 8ffeaca8ea2..2b22851253d 100644 --- a/packages/server/src/handlers/skill.ts +++ b/packages/server/src/handlers/skill.ts @@ -1,8 +1,8 @@ -import { SkillV2 } from "@opencode-ai/core/skill" +import { Skill } from "@opencode-ai/core/skill" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" import { response } from "../location" export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) => - handlers.handle("skill.list", () => response(SkillV2.Service.use((skill) => skill.list()))), + handlers.handle("skill.list", () => response(Skill.Service.use((skill) => skill.list()))), ) diff --git a/packages/server/src/location.ts b/packages/server/src/location.ts index 6cc90f3262e..7b23b5e4899 100644 --- a/packages/server/src/location.ts +++ b/packages/server/src/location.ts @@ -1,7 +1,7 @@ import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { AbsolutePath } from "@opencode-ai/core/schema" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { Effect, Layer } from "effect" import { HttpServerRequest } from "effect/unstable/http" import { HttpApiMiddleware } from "effect/unstable/httpapi" @@ -34,7 +34,7 @@ export function requestRef(request: HttpServerRequest.HttpServerRequest): Locati (request.headers["x-opencode-directory"] ? decode(request.headers["x-opencode-directory"]) : process.cwd()) return Location.Ref.make({ directory: AbsolutePath.make(directory), - workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined, + workspaceID: workspaceID ? Workspace.ID.make(workspaceID) : undefined, }) } diff --git a/packages/server/src/middleware/form-location.ts b/packages/server/src/middleware/form-location.ts index f6b93b118a5..96333dc1c64 100644 --- a/packages/server/src/middleware/form-location.ts +++ b/packages/server/src/middleware/form-location.ts @@ -2,9 +2,9 @@ import { Database } from "@opencode-ai/core/database/database" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors" import { eq } from "drizzle-orm" import { Effect, Layer, Schema } from "effect" @@ -19,7 +19,7 @@ export class FormLocationMiddleware extends HttpApiMiddleware.Service< error: [InvalidRequestError, SessionNotFoundError], }) {} -const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID) +const decodeSessionID = Schema.decodeUnknownEffect(Session.ID) export const formLocationLayer = Layer.effect( FormLocationMiddleware, @@ -65,7 +65,7 @@ export const formLocationLayer = Layer.effect( locations.get( Location.Ref.make({ directory: AbsolutePath.make(row.directory), - workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined, + workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined, }), ), ), diff --git a/packages/server/src/middleware/session-location.ts b/packages/server/src/middleware/session-location.ts index 86fa80f75c1..d34076e5ce4 100644 --- a/packages/server/src/middleware/session-location.ts +++ b/packages/server/src/middleware/session-location.ts @@ -2,9 +2,9 @@ import { Database } from "@opencode-ai/core/database/database" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" -import { WorkspaceV2 } from "@opencode-ai/core/workspace" +import { Workspace } from "@opencode-ai/core/workspace" import { eq } from "drizzle-orm" import { Effect, Layer, Schema } from "effect" import { HttpRouter } from "effect/unstable/http" @@ -19,7 +19,7 @@ export class SessionLocationMiddleware extends HttpApiMiddleware.Service< error: [InvalidRequestError, SessionNotFoundError], }) {} -const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID) +const decodeSessionID = Schema.decodeUnknownEffect(Session.ID) export const sessionLocationLayer = Layer.effect( SessionLocationMiddleware, @@ -56,7 +56,7 @@ export const sessionLocationLayer = Layer.effect( locations.get( Location.Ref.make({ directory: AbsolutePath.make(row.directory), - workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined, + workspaceID: row.workspaceID ? Workspace.ID.make(row.workspaceID) : undefined, }), ), ), diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts index edffdbd4f80..f2d0d68df73 100644 --- a/packages/server/src/routes.ts +++ b/packages/server/src/routes.ts @@ -3,18 +3,18 @@ import { App } from "@opencode-ai/core/app" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { httpClient } from "@opencode-ai/util/effect/app-node-platform" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { EventLogger } from "@opencode-ai/core/event-logger" import { FileSystemSearch } from "@opencode-ai/core/filesystem/search" import { Observability } from "@opencode-ai/util/observability" import { Credential } from "@opencode-ai/core/credential" import { Config } from "@opencode-ai/core/config" -import { CommandV2 } from "@opencode-ai/core/command" +import { Command } from "@opencode-ai/core/command" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { Pty } from "@opencode-ai/core/pty" import { Project } from "@opencode-ai/core/project" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { Shell } from "@opencode-ai/core/shell" import { Job } from "@opencode-ai/core/job" import { MCP } from "@opencode-ai/core/mcp/index" @@ -25,7 +25,6 @@ import { ModelsDev } from "@opencode-ai/core/models-dev" import { SessionRestart } from "@opencode-ai/core/session/execution/restart" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" -import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" import { WellKnown } from "@opencode-ai/core/wellknown" import { Watcher } from "@opencode-ai/core/filesystem/watcher" import { HttpRouter } from "effect/unstable/http" @@ -45,13 +44,12 @@ import type { ServerOptions } from "./options" const applicationServices = LayerNode.group([ Database.node, - EventV2.node, + Bus.node, EventLogger.node, httpClient, - ToolOutputStore.cleanupNode, Job.node, Project.node, - SessionV2.node, + Session.node, PluginRuntime.providerNode, SdkPlugins.node, PermissionSaved.node, @@ -99,7 +97,7 @@ function makeRoutes( }), ], [InstructionDiscovery.node, InstructionDiscovery.configured({ project: options.config?.project })], - [CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })], + [Command.node, Command.configured({ gitbash: options.windows?.gitbash })], [Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })], [Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })], [ diff --git a/packages/server/test/event-feed.test.ts b/packages/server/test/event-feed.test.ts index 29129c735c2..897fcf35832 100644 --- a/packages/server/test/event-feed.test.ts +++ b/packages/server/test/event-feed.test.ts @@ -1,38 +1,39 @@ import { describe, expect, test } from "bun:test" -import { AgentV2 } from "@opencode-ai/core/agent" -import { EventV2 } from "@opencode-ai/core/event" +import { Agent } from "@opencode-ai/core/agent" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { OpenCodeEvent } from "@opencode-ai/protocol/groups/event" import { DateTime, Deferred, Effect, Exit, Fiber, Option, Schema, Stream } from "effect" import { it } from "../../core/test/lib/effect" import { EventFeed } from "../src/event-feed" -const Internal = EventV2.ephemeral({ type: "test.internal", schema: { value: Schema.String } }) +const Internal = Bus.ephemeral({ type: "test.internal", schema: { value: Schema.String } }) -const event = (id: string): EventV2.Payload => ({ - id: EventV2.ID.make(`evt_${id}`), +const event = (id: string): Event.Payload => ({ + id: Event.ID.make(`evt_${id}`), created: DateTime.makeUnsafe(Date.now()), - type: AgentV2.Event.Updated.type, + type: Agent.Event.Updated.type, data: {}, }) -const internal = (value: string): EventV2.Payload => ({ - id: EventV2.ID.create(), +const internal = (value: string): Event.Payload => ({ + id: Event.ID.create(), created: DateTime.makeUnsafe(Date.now()), type: Internal.type, data: { value }, }) function makeSource() { - let subscriber: EventV2.Subscriber | undefined + let subscriber: Bus.Subscriber | undefined return { - observe: (next: EventV2.Subscriber) => + observe: (next: Bus.Subscriber) => Effect.sync(() => { subscriber = next return Effect.sync(() => { if (subscriber === next) subscriber = undefined }) }), - publish: (event: EventV2.Payload) => Effect.suspend(() => (subscriber ? subscriber(event) : Effect.void)), + publish: (event: Event.Payload) => Effect.suspend(() => (subscriber ? subscriber(event) : Effect.void)), } } @@ -62,8 +63,8 @@ describe("EventFeed", () => { yield* source.publish(event("example")) expect([Array.from(yield* Fiber.join(left)), Array.from(yield* Fiber.join(right))]).toEqual([ - [AgentV2.Event.Updated.type], - [AgentV2.Event.Updated.type], + [Agent.Event.Updated.type], + [Agent.Event.Updated.type], ]) expect(encodes).toBe(1) }), @@ -123,7 +124,7 @@ describe("EventFeed", () => { yield* source.publish(internal("two")) yield* source.publish(event("public")) - expect(Array.from(yield* stream.pipe(Stream.take(1), Stream.runCollect))).toEqual([AgentV2.Event.Updated.type]) + expect(Array.from(yield* stream.pipe(Stream.take(1), Stream.runCollect))).toEqual([Agent.Event.Updated.type]) }), ) @@ -132,7 +133,7 @@ describe("EventFeed", () => { const source = makeSource() const feed = yield* EventFeed.make(source.observe, { encode: (event) => { - if (event.id === EventV2.ID.make("evt_bad")) throw new Error("invalid event") + if (event.id === Event.ID.make("evt_bad")) throw new Error("invalid event") return event.id }, }) diff --git a/packages/simulation/src/backend/simulated-provider.ts b/packages/simulation/src/backend/simulated-provider.ts index 3bb9014df8d..84b36d5878b 100644 --- a/packages/simulation/src/backend/simulated-provider.ts +++ b/packages/simulation/src/backend/simulated-provider.ts @@ -1,6 +1,6 @@ import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" -import { Plugin } from "@opencode-ai/plugin/v2/effect" -import { Tool } from "@opencode-ai/plugin/v2/effect/tool" +import { Tool } from "@opencode-ai/core/tool" +import { Plugin } from "@opencode-ai/plugin/effect" import { createHash } from "node:crypto" import { Cause, @@ -452,10 +452,10 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( name: string, input: unknown, context: Tool.Context, - ): Effect.Effect, Tool.Failure> => + ): Effect.Effect => Effect.gen(function* () { const encoded = yield* Schema.decodeUnknownEffect(Schema.Json)(input).pipe( - Effect.mapError((error) => new Tool.Failure({ message: `Simulated tool input is not JSON: ${error.message}` })), + Effect.mapError((error) => new Tool.Error({ message: `Simulated tool input is not JSON: ${error.message}` })), ) const invocation = yield* Effect.uninterruptibleMask((restore) => attachmentLock @@ -465,7 +465,7 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( const current = yield* Ref.get(state) if (current.generation !== registrationGeneration) yield* Effect.fail( - new Tool.Failure({ message: `Simulated tool registration is no longer active: ${name}` }), + new Tool.Error({ message: `Simulated tool registration is no longer active: ${name}` }), ) const id = `tool_${current.counter + 1}` const completion = yield* Deferred.make() @@ -526,9 +526,20 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( output: invocation.output.structured, ...(invocation.output.content.length === 0 ? {} - : { content: invocation.output.content as [Tool.Content, ...Tool.Content[]] }), + : { + content: invocation.output.content.map((part) => + part.type === "text" + ? part + : { + type: "file" as const, + uri: `data:${part.mime};base64,${part.data}`, + mime: part.mime, + ...(part.name === undefined ? {} : { name: part.name }), + }, + ), + }), } - return yield* Effect.fail(new Tool.Failure({ message: invocation.message })) + return yield* new Tool.Error({ message: invocation.message }) }) yield* plugins.register( @@ -552,8 +563,12 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( .transform((draft) => { for (const registration of nextRegistrations) draft.add( - registration.name, - Tool.make({ + { + name: registration.name, + options: + registration.permission === undefined + ? registration.options + : { ...registration.options, permission: registration.permission }, description: registration.description, input: registration.inputSchema, output: registration.outputSchema ?? {}, @@ -564,10 +579,7 @@ const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* ( input, context, ), - }), - registration.permission === undefined - ? registration.options - : { ...registration.options, permission: registration.permission }, + }, ) }) .pipe(Scope.provide(nextScope)), diff --git a/packages/simulation/test/simulated-provider.test.ts b/packages/simulation/test/simulated-provider.test.ts index 82238bae028..39a6509c188 100644 --- a/packages/simulation/test/simulated-provider.test.ts +++ b/packages/simulation/test/simulated-provider.test.ts @@ -2,22 +2,22 @@ import { expect, test } from "bun:test" import { mkdir, mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" -import { AgentV2 } from "@opencode-ai/core/agent" +import { Agent } from "@opencode-ai/core/agent" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-services" import { SdkPlugins } from "@opencode-ai/core/plugin/sdk" import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" import { AbsolutePath } from "@opencode-ai/core/schema" -import { SessionV2 } from "@opencode-ai/core/session" +import { Session } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" -import { ToolRegistry } from "@opencode-ai/core/tool/registry" -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Tool } from "@opencode-ai/core/tool" +import { Plugin } from "@opencode-ai/plugin/effect" import { Deferred, Effect, Fiber, Layer, Queue, Stream } from "effect" import type { Scope } from "effect/Scope" import { SimulatedProvider } from "../src/backend/simulated-provider" @@ -264,7 +264,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(messages)).toMatchObject({ id: 1, result: { attached: true } }) - const registry = yield* ToolRegistry.Service + const registry = yield* Tool.Service const toolSet = yield* registry.snapshot() expect(toolSet.definitions).toContainEqual( expect.objectContaining({ name: "lookup", description: "Look up a value" }), @@ -272,17 +272,17 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { expect( (yield* registry.snapshot([{ action: "simulate_lookup", resource: "*", effect: "deny" }])).definitions, ).not.toContainEqual(expect.objectContaining({ name: "lookup" })) - const secondaryToolSet = yield* ToolRegistry.Service.use((secondaryRegistry) => + const secondaryToolSet = yield* Tool.Service.use((secondaryRegistry) => secondaryRegistry.snapshot(), ).pipe(Effect.provide(secondary)) expect(secondaryToolSet.definitions).toContainEqual( expect.objectContaining({ name: "lookup", description: "Look up a value" }), ) - const progress: ToolRegistry.Progress[] = [] + const progress: Tool.Metadata[] = [] const executeCall = (callID: string, query: string) => toolSet.execute({ - sessionID: SessionV2.ID.make("ses_simulated_tools"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_simulated_tools"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), progress: (update) => Effect.sync(() => progress.push(update)), call: { @@ -390,13 +390,12 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(messages)).toMatchObject({ id: 23, result: { ok: true } }) expect(yield* Fiber.join(successful)).toMatchObject({ - status: "completed", output: { answer: 42 }, content: [{ type: "text", text: "42" }], }) expect(progress).toEqual([{ phase: "searching" }]) - const failed = yield* executeCall("call_failure", "missing").pipe(Effect.forkScoped) + const failed = yield* executeCall("call_failure", "missing").pipe(Effect.exit, Effect.forkScoped) const failedInvocation = yield* takeToolInvocation(messages) const failedID = requireString(requireRecord(failedInvocation.params).id) socket.send( @@ -408,10 +407,9 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { }), ) expect(yield* Queue.take(messages)).toMatchObject({ id: 4, result: { ok: true } }) - expect(yield* Fiber.join(failed)).toMatchObject({ - status: "error", - error: { message: "lookup failed" }, - }) + const failedExit = yield* Fiber.join(failed) + expect(failedExit).toMatchObject({ _tag: "Failure" }) + expect(failedExit.toString()).toContain("lookup failed") const concurrent = [ yield* executeCall("call_first", "first").pipe(Effect.forkScoped), @@ -443,12 +441,10 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { expect(yield* Queue.take(messages)).toMatchObject({ id, result: { ok: true } }) } expect(yield* Fiber.join(concurrent[0])).toMatchObject({ - status: "completed", output: "first result", content: [{ type: "text", text: "first result" }], }) expect(yield* Fiber.join(concurrent[1])).toMatchObject({ - status: "completed", output: "second result", content: [{ type: "text", text: "second result" }], }) @@ -564,7 +560,6 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 10, result: { ok: true } }) expect(yield* Fiber.join(replayed)).toMatchObject({ - status: "completed", output: "replayed result", content: [{ type: "text", text: "replayed result" }], }) @@ -585,7 +580,6 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 27, result: { ok: true } }) expect(yield* Fiber.join(preserved)).toMatchObject({ - status: "completed", output: "preserved", content: [{ type: "text", text: "preserved" }], }) @@ -607,7 +601,7 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { const replacedNames = replaced.definitions.map((definition) => definition.name) expect(replacedNames).toEqual(expect.arrayContaining(["github_search", "web_search"])) expect(replacedNames).not.toContain("lookup") - const secondaryReplaced = yield* ToolRegistry.Service.use((secondaryRegistry) => + const secondaryReplaced = yield* Tool.Service.use((secondaryRegistry) => secondaryRegistry.snapshot(), ).pipe(Effect.provide(secondary)) const secondaryNames = secondaryReplaced.definitions.map((definition) => definition.name) @@ -615,8 +609,8 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { expect(secondaryNames).not.toContain("lookup") const routed = yield* replaced .execute({ - sessionID: SessionV2.ID.make("ses_simulated_tools"), - agent: AgentV2.ID.make("build"), + sessionID: Session.ID.make("ses_simulated_tools"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), call: { type: "tool-call", @@ -642,14 +636,13 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { ) expect(yield* Queue.take(replacementMessages)).toMatchObject({ id: 12, result: { ok: true } }) expect(yield* Fiber.join(routed)).toMatchObject({ - status: "completed", output: "routed", content: [{ type: "text", text: "routed" }], }) - expect( - yield* toolSet.execute({ - sessionID: SessionV2.ID.make("ses_simulated_tools"), - agent: AgentV2.ID.make("build"), + const stale = yield* toolSet + .execute({ + sessionID: Session.ID.make("ses_simulated_tools"), + agent: Agent.ID.make("build"), messageID: SessionMessage.ID.make("msg_simulated_tools"), call: { type: "tool-call", @@ -657,11 +650,10 @@ test("controls arbitrary tools through scoped SDK overlays", async () => { name: "lookup", input: { query: "stale" }, }, - }), - ).toMatchObject({ - status: "error", - error: { message: expect.stringContaining("no longer active") }, - }) + }) + .pipe(Effect.exit) + expect(stale).toMatchObject({ _tag: "Failure" }) + expect(stale.toString()).toContain("no longer active") expect(activations).toBe(2) }).pipe(Effect.provide(primary)) }).pipe(Effect.provide(toolLifecycleLayer(endpoint)), Effect.scoped), @@ -708,7 +700,7 @@ const toolLifecycleLayer = (endpoint: string) => { deps: [SdkPlugins.node], }) return AppNodeBuilder.build( - LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node, provider]), + LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node, provider]), [[Config.node, Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))]], ) } diff --git a/packages/tui/src/attention.ts b/packages/tui/src/attention.ts index 7eac2e06469..57272243ee9 100644 --- a/packages/tui/src/attention.ts +++ b/packages/tui/src/attention.ts @@ -9,7 +9,7 @@ import type { TuiAttentionSoundName, TuiAttentionSoundPack, TuiAttentionSoundPackInfo, -} from "@opencode-ai/plugin/tui" +} from "@opencode-ai/plugin/v1/tui" import { AttentionSoundName, type Config } from "./config" import { Schema } from "effect" import stripAnsi from "strip-ansi" diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 724d67830e4..f0e313f9214 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -14,8 +14,8 @@ import type { McpServer, ModelInfo, PermissionSavedInfo, - PermissionV2Request, - ProviderV2Info, + PermissionRequest, + ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionMessageAssistant, @@ -29,7 +29,7 @@ import type { OpenCodeEvent, WebSearchProvider, } from "@opencode-ai/client" -import type { Plugin } from "@opencode-ai/plugin/v2/tui" +import type { Plugin } from "@opencode-ai/plugin/tui" import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "./helper" import { useClient } from "./client" @@ -55,7 +55,7 @@ type LocationData = { resource?: McpResource[] } model?: ModelInfo[] - provider?: ProviderV2Info[] + provider?: ProviderInfo[] reference?: ReferenceInfo[] websearch?: WebSearchProvider[] // Currently running shell commands for this location, keyed by shell id. Entries are removed @@ -75,7 +75,7 @@ type Store = { message: Record pending: Record input: Record - permission: Record + permission: Record // Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel. form: Record } @@ -808,14 +808,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.append(draft, index, failed) }) break - case "permission.v2.asked": + case "permission.asked": if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id)) break setStore("session", "permission", event.data.sessionID, [ ...(store.session.permission[event.data.sessionID] ?? []), event.data, ]) break - case "permission.v2.replied": + case "permission.replied": setStore( "session", "permission", diff --git a/packages/tui/src/context/keymap.tsx b/packages/tui/src/context/keymap.tsx index e8da0ca909a..e29c6fa2f3a 100644 --- a/packages/tui/src/context/keymap.tsx +++ b/packages/tui/src/context/keymap.tsx @@ -1,4 +1,4 @@ -import type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/v2/tui/context" +import type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/tui/context" import { InputRenderable, TextareaRenderable, type KeyEvent, type Renderable } from "@opentui/core" import { stringifyKeyStroke, type Binding, type CommandContext } from "@opentui/keymap" import { @@ -155,7 +155,7 @@ function Provider(props: ParentProps<{ config?: KeymapConfig }>) { ) } -export type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/v2/tui/context" +export type { KeymapCommand, KeymapLayer } from "@opencode-ai/plugin/tui/context" export interface Keymap { /** Dispatches a reachable command by ID. */ diff --git a/packages/tui/src/feature-plugins/builtins.ts b/packages/tui/src/feature-plugins/builtins.ts index 1e051ae4d2a..e3c1783f58e 100644 --- a/packages/tui/src/feature-plugins/builtins.ts +++ b/packages/tui/src/feature-plugins/builtins.ts @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/v1/tui" import type { PluginRuntime } from "../plugin/runtime" import Notifications from "./system/notifications" import PluginManager from "./system/plugins" diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index e8d39ee82f4..60623ef61b9 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Match, Show, Switch } from "solid-js" import { useTerminalDimensions } from "@opentui/solid" import { useTuiApp, useTuiPaths } from "../../context/runtime" diff --git a/packages/tui/src/feature-plugins/sidebar/context.tsx b/packages/tui/src/feature-plugins/sidebar/context.tsx index aaec364aa39..52655add518 100644 --- a/packages/tui/src/feature-plugins/sidebar/context.tsx +++ b/packages/tui/src/feature-plugins/sidebar/context.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" import { contextUsage } from "../../util/session" diff --git a/packages/tui/src/feature-plugins/sidebar/footer.tsx b/packages/tui/src/feature-plugins/sidebar/footer.tsx index 49f5c25a484..adde20e115f 100644 --- a/packages/tui/src/feature-plugins/sidebar/footer.tsx +++ b/packages/tui/src/feature-plugins/sidebar/footer.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" import { useTuiPaths } from "../../context/runtime" import { useTheme } from "../../context/theme" diff --git a/packages/tui/src/feature-plugins/sidebar/lsp.tsx b/packages/tui/src/feature-plugins/sidebar/lsp.tsx index 63369e34481..5eedcc12b21 100644 --- a/packages/tui/src/feature-plugins/sidebar/lsp.tsx +++ b/packages/tui/src/feature-plugins/sidebar/lsp.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { useTheme } from "../../context/theme" function View() { diff --git a/packages/tui/src/feature-plugins/sidebar/mcp.tsx b/packages/tui/src/feature-plugins/sidebar/mcp.tsx index 17f21c2b6c3..842da4c3109 100644 --- a/packages/tui/src/feature-plugins/sidebar/mcp.tsx +++ b/packages/tui/src/feature-plugins/sidebar/mcp.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js" import { useTheme } from "../../context/theme" diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index 380a5035bda..d7dbbd957e6 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -1,7 +1,7 @@ /** @jsxImportSource @opentui/solid */ import type { FileDiffInfo } from "@opencode-ai/client" -import { Plugin } from "@opencode-ai/plugin/v2/tui" -import type { KeymapCommand, Route } from "@opencode-ai/plugin/v2/tui/context" +import { Plugin } from "@opencode-ai/plugin/tui" +import type { KeymapCommand, Route } from "@opencode-ai/plugin/tui/context" import { TextAttributes, type BorderSides, diff --git a/packages/tui/src/feature-plugins/system/notifications.ts b/packages/tui/src/feature-plugins/system/notifications.ts index 871f8e6eecd..6592b843b9b 100644 --- a/packages/tui/src/feature-plugins/system/notifications.ts +++ b/packages/tui/src/feature-plugins/system/notifications.ts @@ -1,5 +1,5 @@ import type { OpenCodeEvent } from "@opencode-ai/client" -import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiAttentionSoundName, TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import type { BuiltinTuiPlugin } from "../builtins" const id = "internal:notifications" diff --git a/packages/tui/src/feature-plugins/system/plugins.tsx b/packages/tui/src/feature-plugins/system/plugins.tsx index 02c916e6ff0..c04ac640a23 100644 --- a/packages/tui/src/feature-plugins/system/plugins.tsx +++ b/packages/tui/src/feature-plugins/system/plugins.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/v1/tui" import type { BuiltinTuiPlugin } from "../builtins" import { useTerminalDimensions } from "@opentui/solid" import { fileURLToPath } from "url" diff --git a/packages/tui/src/feature-plugins/system/scrap.tsx b/packages/tui/src/feature-plugins/system/scrap.tsx index 1660a892f89..1a21a4ca514 100644 --- a/packages/tui/src/feature-plugins/system/scrap.tsx +++ b/packages/tui/src/feature-plugins/system/scrap.tsx @@ -1,4 +1,4 @@ -import { Plugin } from "@opencode-ai/plugin/v2/tui" +import { Plugin } from "@opencode-ai/plugin/tui" import { useTerminalDimensions } from "@opentui/solid" import { Keymap } from "../../context/keymap" import { useTheme } from "../../context/theme" diff --git a/packages/tui/src/feature-plugins/system/which-key.tsx b/packages/tui/src/feature-plugins/system/which-key.tsx index 732b3a96c3a..2617d27af42 100644 --- a/packages/tui/src/feature-plugins/system/which-key.tsx +++ b/packages/tui/src/feature-plugins/system/which-key.tsx @@ -4,7 +4,7 @@ import { useTerminalDimensions } from "@opentui/solid" import { createEffect, createMemo, createSignal, For, Show } from "solid-js" import { Keymap } from "../../context/keymap" import type { ActiveKey } from "@opentui/keymap" -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import type { BuiltinTuiPlugin } from "../builtins" const command = { diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index 95e9719f869..6f1fdf56c75 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -18,7 +18,7 @@ import type { EventSubscribeOutput, OpenCodeClient, - PermissionV2Request, + PermissionRequest, SessionMessageAssistantTool, SessionMessageInfo, } from "@opencode-ai/client/promise" @@ -168,14 +168,14 @@ function sourceKey(messageID: string, callID: string) { return `${messageID}\u0000${callID}` } -function permissionTool(request: PermissionV2Request, tools: Map) { +function permissionTool(request: PermissionRequest, tools: Map) { if (request.source?.type !== "tool") return request const tool = tools.get(sourceKey(request.source.messageID, request.source.callID)) return tool ? { ...request, tool } : request } function blockerCategory(event: V2Event): "permission" | "form" | undefined { - if (event.type === "permission.v2.asked" || event.type === "permission.v2.replied") return "permission" + if (event.type === "permission.asked" || event.type === "permission.replied") return "permission" if (event.type === "form.created" || event.type === "form.replied" || event.type === "form.cancelled") return "form" } @@ -436,7 +436,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac const resolvePermissionTools = async ( sdk: OpenCodeClient, child: ChildState, - permissions: PermissionV2Request[], + permissions: PermissionRequest[], epoch: number, signal = input.signal, ) => { @@ -859,13 +859,13 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac notifyDetail(child) return } - if (event.type === "permission.v2.asked") { + if (event.type === "permission.asked") { if (!child.permissions.some((item) => item.id === event.data.id)) child.permissions.push(permissionTool(event.data, child.toolSources)) input.emit() return } - if (event.type === "permission.v2.replied") { + if (event.type === "permission.replied") { child.permissions = child.permissions.filter((item) => item.id !== event.data.requestID) input.emit() return diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index e0e767ca56d..5ba7870d5c1 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -3,7 +3,7 @@ import type { FormInfo, LocationRef, OpenCodeClient, - PermissionV2Request, + PermissionRequest, SessionMessageAssistantTool, SessionMessageInfo, SessionPendingInfo, @@ -295,7 +295,7 @@ function permissionSourceKey(messageID: string, callID: string) { return streamPartKey(messageID, callID) } -function permissionTool(request: PermissionV2Request, tools: Map) { +function permissionTool(request: PermissionRequest, tools: Map) { if (request.source?.type !== "tool") return request const tool = tools.get(permissionSourceKey(request.source.messageID, request.source.callID)) return tool ? { ...request, tool } : request @@ -715,7 +715,7 @@ export async function createSessionTransport(input: StreamInput): Promise { const pending = new Set( @@ -1098,13 +1098,13 @@ export async function createSessionTransport(input: StreamInput): Promise item.id === event.data.id)) state.permissions.push(permissionTool(event.data, state.toolSources)) syncBlockers() return } - if (event.type === "permission.v2.replied") { + if (event.type === "permission.replied") { state.permissions = state.permissions.filter((item) => item.id !== event.data.requestID) pruneToolSources() syncBlockers() diff --git a/packages/tui/src/mini/theme.ts b/packages/tui/src/mini/theme.ts index 3c5afed16e6..5a1fd22f4aa 100644 --- a/packages/tui/src/mini/theme.ts +++ b/packages/tui/src/mini/theme.ts @@ -6,7 +6,7 @@ // the run footer + scrollback color model. Falls back to a hardcoded dark-mode // palette if detection fails. import { RGBA, SyntaxStyle, type CliRenderer, type ColorInput, type TerminalColors } from "@opentui/core" -import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" +import type { TuiThemeCurrent } from "@opencode-ai/plugin/v1/tui" import { ansiToRgba } from "../theme/color" import { resolveThemeColors } from "../theme/resolve" import { terminalMode } from "../theme/system" diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index 2ee854010bf..5f0e2f79e1b 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -17,7 +17,7 @@ import type { OpenCodeClient, LocationGetOutput, LocationRef, - PermissionV2Request, + PermissionRequest, ReferenceListOutput, SessionMessageAssistantTool, } from "@opencode-ai/client/promise" @@ -254,7 +254,7 @@ export type MiniToolPart = { state: MiniToolState } -export type MiniPermissionRequest = PermissionV2Request & { +export type MiniPermissionRequest = PermissionRequest & { tool?: SessionMessageAssistantTool } diff --git a/packages/tui/src/plugin/api.ts b/packages/tui/src/plugin/api.ts index 9ab6192f63d..1a9335dd6f6 100644 --- a/packages/tui/src/plugin/api.ts +++ b/packages/tui/src/plugin/api.ts @@ -1,4 +1,4 @@ -import type { TuiRouteDefinition } from "@opencode-ai/plugin/tui" +import type { TuiRouteDefinition } from "@opencode-ai/plugin/v1/tui" import { createSignal } from "solid-js" type RouteEntry = { diff --git a/packages/tui/src/plugin/command-shim.ts b/packages/tui/src/plugin/command-shim.ts index 36e7548fcfa..6141cbebd54 100644 --- a/packages/tui/src/plugin/command-shim.ts +++ b/packages/tui/src/plugin/command-shim.ts @@ -1,5 +1,5 @@ // Legacy `api.command` bridge for v1 plugins; remove in v2. -import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiCommand, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import { TuiKeybind } from "../config/keybind" import type { DialogContext } from "../ui/dialog" diff --git a/packages/tui/src/plugin/context.tsx b/packages/tui/src/plugin/context.tsx index 29777342c8d..a42434b5d6a 100644 --- a/packages/tui/src/plugin/context.tsx +++ b/packages/tui/src/plugin/context.tsx @@ -1,4 +1,4 @@ -import type { Plugin } from "@opencode-ai/plugin/v2/tui" +import type { Plugin } from "@opencode-ai/plugin/tui" import { batch, createContext, @@ -13,7 +13,7 @@ import { import path from "path" import { stat } from "fs/promises" import { fileURLToPath, pathToFileURL } from "url" -import type { Context, Page, Slot } from "@opencode-ai/plugin/v2/tui/context" +import type { Context, Page, Slot } from "@opencode-ai/plugin/tui/context" import { createStore, produce, reconcile as reconcileStore } from "solid-js/store" import { useConfig } from "../config" import { useClient } from "../context/client" diff --git a/packages/tui/src/plugin/runtime.tsx b/packages/tui/src/plugin/runtime.tsx index 4e8fa51f67d..0d248545cdc 100644 --- a/packages/tui/src/plugin/runtime.tsx +++ b/packages/tui/src/plugin/runtime.tsx @@ -3,7 +3,7 @@ import type { TuiPluginInstallOptions, TuiPluginInstallResult, TuiPluginStatus, -} from "@opencode-ai/plugin/tui" +} from "@opencode-ai/plugin/v1/tui" import { createContext, createSignal, useContext, type JSX, type ParentProps } from "solid-js" import { createPluginRoutes } from "./api" import { createSlots, type HostSlots } from "./slots" diff --git a/packages/tui/src/plugin/slots.tsx b/packages/tui/src/plugin/slots.tsx index 8c1da1c7ebd..a40037532b4 100644 --- a/packages/tui/src/plugin/slots.tsx +++ b/packages/tui/src/plugin/slots.tsx @@ -1,4 +1,4 @@ -import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@opencode-ai/plugin/tui" +import type { TuiPluginApi, TuiSlotContext, TuiSlotMap, TuiSlotProps } from "@opencode-ai/plugin/v1/tui" import { createSlot, createSolidSlotRegistry, type JSX, type SolidPlugin } from "@opentui/solid" import { createSignal } from "solid-js" import { isRecord } from "../util/record" diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index a6a746d881b..e70c2917f02 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -3,7 +3,7 @@ import { createMemo, For, Match, Show, Switch } from "solid-js" import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { useTheme } from "../../context/theme" -import type { PermissionV2Request } from "@opencode-ai/client" +import type { PermissionRequest } from "@opencode-ai/client" import { useClient } from "../../context/client" import { SplitBorder } from "../../ui/border" import { useData } from "../../context/data" @@ -107,7 +107,7 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) { ) } -export function PermissionPrompt(props: { request: PermissionV2Request; directory?: string }) { +export function PermissionPrompt(props: { request: PermissionRequest; directory?: string }) { const client = useClient() const data = useData() const [store, setStore] = createStore({ diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index 1fa253b0fdc..f717496defe 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import Notifications from "../../../../src/feature-plugins/system/notifications" import type { OpenCodeEvent, PermissionAsked, QuestionAsked } from "@opencode-ai/client" -import type { TuiAttentionNotifyInput, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiAttentionNotifyInput, TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import { createTuiPluginApi } from "../../../fixture/tui-plugin" type Session = NonNullable> @@ -95,10 +95,10 @@ function permission(id: string, sessionID = "session"): PermissionAsked["data"] return { id, sessionID, - permission: "edit", - patterns: [], + action: "edit", + resources: [], metadata: {}, - always: [], + save: [], } } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 60a55785429..b94dae7e809 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -3,7 +3,8 @@ import { expect, test } from "bun:test" import { testRender } from "@opentui/solid" import type { OpenCodeEvent } from "@opencode-ai/client" import { SessionMessage } from "@opencode-ai/core/session/message" -import { EventV2 } from "@opencode-ai/core/event" +import { Bus } from "@opencode-ai/core/bus" +import { Event } from "@opencode-ai/schema/event" import { createEffect, onMount, type ParentProps } from "solid-js" import { ConfigProvider } from "../../../src/config" import { ClientProvider, useClient } from "../../../src/context/client" @@ -879,7 +880,7 @@ test("removes committed revert messages from local state", async () => { try { for (const [seq, inputID] of ["msg_001", "msg_002", "msg_003"].entries()) { emitEvent(events, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: seq, type: "session.input.admitted", durable: durable(sessionID, seq), @@ -889,7 +890,7 @@ test("removes committed revert messages from local state", async () => { await wait(() => data.session.message.list(sessionID).length === 3) emitEvent(events, { - id: EventV2.ID.create(), + id: Event.ID.create(), created: 3, type: "session.revert.committed", durable: durable(sessionID, 3), @@ -1812,7 +1813,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_asked_1", created: 0, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_1", sessionID: "ses_1", @@ -1823,7 +1824,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_asked_2", created: 0, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_2", sessionID: "ses_1", @@ -1836,7 +1837,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_replied_1", created: 0, - type: "permission.v2.replied", + type: "permission.replied", data: { sessionID: "ses_1", requestID: "per_1", reply: "once" }, }) await wait(() => data.session.permission.list("ses_1")?.length === 1) @@ -1845,7 +1846,7 @@ test("adds and dismisses permission requests from live events", async () => { emitEvent(events, { id: "evt_permission_replied_2", created: 0, - type: "permission.v2.replied", + type: "permission.replied", data: { sessionID: "ses_1", requestID: "per_2", reply: "reject" }, }) await wait(() => data.session.permission.list("ses_1")?.length === 0) @@ -2570,7 +2571,7 @@ test("skips initial instruction state and projects later updates with their mess await wait(() => sync.session.message.list("session-1")?.some((message) => message.time.created === 1)) expect(sync.session.message.list("session-1")).toHaveLength(1) expect(sync.session.message.list("session-1")?.[0]).toMatchObject({ - id: SessionMessage.ID.fromEvent(EventV2.ID.make("evt_instructions_2")), + id: SessionMessage.ID.fromEvent(Event.ID.make("evt_instructions_2")), type: "system", text: "Instructions updated: core/date", time: { created: 1 }, diff --git a/packages/tui/test/cli/tui/diff-viewer.test.tsx b/packages/tui/test/cli/tui/diff-viewer.test.tsx index f09e49aa0a9..c54df1f229a 100644 --- a/packages/tui/test/cli/tui/diff-viewer.test.tsx +++ b/packages/tui/test/cli/tui/diff-viewer.test.tsx @@ -10,7 +10,7 @@ import type { Page, Route, Slot, -} from "@opencode-ai/plugin/v2/tui/context" +} from "@opencode-ai/plugin/tui/context" import { ThemeProvider } from "../../../src/context/theme" import { ConfigProvider } from "../../../src/config" import { TuiKeybind } from "../../../src/config/keybind" diff --git a/packages/tui/test/fixture/tui-plugin.ts b/packages/tui/test/fixture/tui-plugin.ts index 23dbf526ae4..b4e46280831 100644 --- a/packages/tui/test/fixture/tui-plugin.ts +++ b/packages/tui/test/fixture/tui-plugin.ts @@ -1,4 +1,4 @@ -import type { TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPluginApi } from "@opencode-ai/plugin/v1/tui" import { RGBA } from "@opentui/core" import { createTuiResolvedConfig } from "./tui-runtime" diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index b4ce61ef8d1..6b18cff9f09 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -8,7 +8,7 @@ import { type FormInfo, type MessageListOutput, type OpenCodeClient, - type PermissionV2Request, + type PermissionRequest, } from "@opencode-ai/client/promise" import { createSessionTransport } from "../../src/mini/stream-v2.transport" import type { StreamCommit } from "../../src/mini/types" @@ -126,7 +126,7 @@ function sdk(input: { forms?: Record globals?: FormInfo[] globalLocation?: { directory: string; workspaceID?: string } - permissions?: Record + permissions?: Record pending?: Record>> wait?: () => Promise }) { @@ -330,7 +330,7 @@ describe("V2 mini transport", () => { ], time: { created: 1 }, } - const permission: PermissionV2Request = { + const permission: PermissionRequest = { id: "per_child_startup", sessionID: "ses_child", action: "shell", @@ -1038,7 +1038,7 @@ describe("V2 mini transport", () => { events.push({ id: "evt_permission", created: 0, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_1", sessionID: "ses_1", action: "read", resources: ["/tmp/file"] }, }) @@ -2998,7 +2998,7 @@ describe("V2 mini transport", () => { events.push({ id: "evt_child_permission", created: 7, - type: "permission.v2.asked", + type: "permission.asked", data: { id: "per_child", sessionID: "ses_child_progress", diff --git a/packages/util/src/fs-util.ts b/packages/util/src/fs-util.ts index 46d408a8271..affeedebe3f 100644 --- a/packages/util/src/fs-util.ts +++ b/packages/util/src/fs-util.ts @@ -46,7 +46,7 @@ export namespace FSUtil { readonly globMatch: (pattern: string, filepath: string) => boolean } - export class Service extends Context.Service()("@opencode/FileSystem") {} + export class Service extends Context.Service()("@opencode/FSUtil") {} export const use = serviceUse(Service) diff --git a/packages/www/content/docs/build/plugins.mdx b/packages/www/content/docs/build/plugins.mdx index 6522c55906d..7d36320556d 100644 --- a/packages/www/content/docs/build/plugins.mdx +++ b/packages/www/content/docs/build/plugins.mdx @@ -121,7 +121,7 @@ package version or a local dependency when no watched file changed. Export the result of `Plugin.define` as the module default: ```ts title=".opencode/plugins/reviewer.ts" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.reviewer", @@ -203,7 +203,7 @@ other sources. Here's an example that keeps models synced from a remote source: ```js title=".opencode/plugins/remote-models.js" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.remote-models", @@ -248,13 +248,13 @@ mutable fields: | `ctx.aisdk.hook("language", callback)` | `language`, after inspecting `model`, `sdk`, and `options` | | `ctx.session.hook("request", callback)` | `system`, `messages`, and the `tools` record immediately before model dispatch | | `ctx.tool.hook("execute.before", callback)` | `input`, before the selected tool executes | -| `ctx.tool.hook("execute.after", callback)` | Terminal `content`, `metadata`, and `outputPaths`; `error` on failure | +| `ctx.tool.hook("execute.after", callback)` | Terminal `result` on success or `error` on failure | For example, remove a tool from selected model requests and normalize another tool's input: ```ts title=".opencode/plugins/guards.ts" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.guards", @@ -278,13 +278,11 @@ handle expected errors inside the callback. ### Add a tool -Create an executable tool with `Tool.make`, then register it with a name -and registration options. Define its input with JSON Schema and use an async -executor: +Register a structural tool definition with a name and registration options. +Define its input with JSON Schema and use an async executor: ```js title=".opencode/plugins/greeting.js" -import { Plugin } from "@opencode-ai/plugin/v2" -import { Tool } from "@opencode-ai/plugin/v2/tool" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.greeting", @@ -292,7 +290,7 @@ export default Plugin.define({ await ctx.tool.transform((tools) => { tools.add( "greeting", - Tool.make({ + { description: "Create a greeting", input: { type: "object", @@ -315,7 +313,7 @@ export default Plugin.define({ content: text, } }, - }), + }, ) }) }, @@ -342,7 +340,7 @@ without `output` returns model-visible `content` instead. ### Add a command ```js title=".opencode/plugins/review-command.js" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.review-command", @@ -360,7 +358,7 @@ export default Plugin.define({ ### Set the default model ```js title=".opencode/plugins/default-model.js" -import { Plugin } from "@opencode-ai/plugin/v2" +import { Plugin } from "@opencode-ai/plugin" export default Plugin.define({ id: "acme.default-model", @@ -410,7 +408,7 @@ being resolved. ## Effect OpenCode provides a first-class Effect API for plugins through the -`@opencode-ai/plugin/v2/effect` entrypoint. Install `effect` alongside the +`@opencode-ai/plugin/effect` entrypoint. Install `effect` alongside the plugin package and export an `effect` function instead of `setup`: ```sh @@ -418,7 +416,7 @@ bun add @opencode-ai/plugin@next effect ``` ```ts title=".opencode/plugins/reviewer-effect.ts" -import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/plugin/effect" import { Effect } from "effect" export default Plugin.define({ @@ -440,7 +438,6 @@ fibers, and registrations are released when the plugin reloads or unloads. OpenCode does not expose its private Core services to the plugin; use the capabilities on `ctx`. -Typed tools can use `Schema` from `effect` and `Tool.make` from -`@opencode-ai/plugin/v2/effect/tool`. Effect and Promise plugins use the same -`tools.add(name, tool, options?)` registration shape. Effect executors +Typed tools can use `Schema` from `effect`. Effect and Promise plugins use the +same `tools.add(name, tool, options?)` registration shape. Effect executors return an Effect and may fail with the typed tool failure channel. diff --git a/packages/www/openapi.json b/packages/www/openapi.json index 1c8b3dc9412..0508db24281 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -4279,7 +4279,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } } }, @@ -4396,7 +4396,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } }, "required": [ @@ -7466,7 +7466,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7664,7 +7664,7 @@ ] }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -7762,7 +7762,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" }, "agent": { "anyOf": [ @@ -7818,7 +7818,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7916,7 +7916,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } }, "required": [ @@ -8061,7 +8061,7 @@ "type": "object", "properties": { "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" }, "message": { "anyOf": [ @@ -10456,7 +10456,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10527,7 +10527,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10667,7 +10667,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "$ref": "#/components/schemas/Question.Reply" } } }, @@ -11981,7 +11981,7 @@ "Agent.Color": { "type": "string" }, - "PermissionV2.Effect": { + "Permission.Effect": { "type": "string", "enum": [ "allow", @@ -11989,7 +11989,7 @@ "ask" ] }, - "PermissionV2.Rule": { + "Permission.Rule": { "type": "object", "properties": { "action": { @@ -11999,7 +11999,7 @@ "type": "string" }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -12009,10 +12009,10 @@ ], "additionalProperties": false }, - "PermissionV2.Ruleset": { + "Permission.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" + "$ref": "#/components/schemas/Permission.Rule" } }, "Agent.Info": { @@ -12059,7 +12059,7 @@ ] }, "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "$ref": "#/components/schemas/Permission.Ruleset" } }, "required": [ @@ -18537,7 +18537,7 @@ ], "additionalProperties": false }, - "ProviderV2.Info": { + "Provider.Info": { "type": "object", "properties": { "id": { @@ -21466,7 +21466,7 @@ ], "additionalProperties": false }, - "PermissionV2.Source": { + "Permission.Source": { "anyOf": [ { "type": "object", @@ -21493,7 +21493,7 @@ } ] }, - "PermissionV2.Request": { + "Permission.Request": { "type": "object", "properties": { "id": { @@ -21531,7 +21531,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -21589,7 +21589,7 @@ ], "additionalProperties": false }, - "PermissionV2.Reply": { + "Permission.Reply": { "type": "string", "enum": [ "once", @@ -21935,7 +21935,7 @@ ], "additionalProperties": false }, - "PermissionAction": { + "PermissionV1.Action": { "type": "string", "enum": [ "allow", @@ -21943,7 +21943,7 @@ "ask" ] }, - "PermissionRule": { + "PermissionV1.Rule": { "type": "object", "properties": { "permission": { @@ -21953,7 +21953,7 @@ "type": "string" }, "action": { - "$ref": "#/components/schemas/PermissionAction" + "$ref": "#/components/schemas/PermissionV1.Action" } }, "required": [ @@ -21963,10 +21963,10 @@ ], "additionalProperties": false }, - "PermissionRuleset": { + "PermissionV1.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionRule" + "$ref": "#/components/schemas/PermissionV1.Rule" } }, "SessionV1.Info": { @@ -22155,7 +22155,7 @@ "additionalProperties": false }, "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "$ref": "#/components/schemas/PermissionV1.Ruleset" }, "revert": { "type": "object", @@ -22455,10 +22455,10 @@ ], "additionalProperties": false }, - "JSONSchema": { + "SessionV1.JSONSchema": { "type": "object" }, - "OutputFormat": { + "SessionV1.OutputFormat": { "anyOf": [ { "type": "object", @@ -22485,7 +22485,7 @@ ] }, "schema": { - "$ref": "#/components/schemas/JSONSchema" + "$ref": "#/components/schemas/SessionV1.JSONSchema" }, "retryCount": { "anyOf": [ @@ -22518,7 +22518,7 @@ } ] }, - "UserMessage": { + "SessionV1.UserMessage": { "type": "object", "properties": { "id": { @@ -22563,7 +22563,7 @@ "format": { "anyOf": [ { - "$ref": "#/components/schemas/OutputFormat" + "$ref": "#/components/schemas/SessionV1.OutputFormat" }, { "type": "null" @@ -22985,7 +22985,7 @@ ], "additionalProperties": false }, - "AssistantMessage": { + "SessionV1.AssistantMessage": { "type": "object", "properties": { "id": { @@ -23218,13 +23218,13 @@ ], "additionalProperties": false }, - "Message": { + "SessionV1.Message": { "anyOf": [ { - "$ref": "#/components/schemas/UserMessage" + "$ref": "#/components/schemas/SessionV1.UserMessage" }, { - "$ref": "#/components/schemas/AssistantMessage" + "$ref": "#/components/schemas/SessionV1.AssistantMessage" } ] }, @@ -23294,7 +23294,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/SessionV1.Message" } }, "required": [ @@ -23403,7 +23403,7 @@ ], "additionalProperties": false }, - "TextPart": { + "SessionV1.TextPart": { "type": "object", "properties": { "id": { @@ -23518,7 +23518,7 @@ ], "additionalProperties": false }, - "SubtaskPart": { + "SessionV1.SubtaskPart": { "type": "object", "properties": { "id": { @@ -23605,7 +23605,7 @@ ], "additionalProperties": false }, - "ReasoningPart": { + "SessionV1.ReasoningPart": { "type": "object", "properties": { "id": { @@ -23694,7 +23694,7 @@ ], "additionalProperties": false }, - "FilePartSourceText": { + "SessionV1.FilePartSourceText": { "type": "object", "properties": { "value": { @@ -23714,11 +23714,11 @@ ], "additionalProperties": false }, - "FileSource": { + "SessionV1.FileSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23737,7 +23737,7 @@ ], "additionalProperties": false }, - "Range": { + "SessionV1.Range": { "type": "object", "properties": { "start": { @@ -23799,11 +23799,11 @@ ], "additionalProperties": false }, - "SymbolSource": { + "SessionV1.SymbolSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23815,7 +23815,7 @@ "type": "string" }, "range": { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/SessionV1.Range" }, "name": { "type": "string" @@ -23839,11 +23839,11 @@ ], "additionalProperties": false }, - "ResourceSource": { + "SessionV1.ResourceSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23866,20 +23866,20 @@ ], "additionalProperties": false }, - "FilePartSource": { + "SessionV1.FilePartSource": { "anyOf": [ { - "$ref": "#/components/schemas/FileSource" + "$ref": "#/components/schemas/SessionV1.FileSource" }, { - "$ref": "#/components/schemas/SymbolSource" + "$ref": "#/components/schemas/SessionV1.SymbolSource" }, { - "$ref": "#/components/schemas/ResourceSource" + "$ref": "#/components/schemas/SessionV1.ResourceSource" } ] }, - "FilePart": { + "SessionV1.FilePart": { "type": "object", "properties": { "id": { @@ -23931,7 +23931,7 @@ "source": { "anyOf": [ { - "$ref": "#/components/schemas/FilePartSource" + "$ref": "#/components/schemas/SessionV1.FilePartSource" }, { "type": "null" @@ -23949,7 +23949,7 @@ ], "additionalProperties": false }, - "ToolStatePending": { + "SessionV1.ToolStatePending": { "type": "object", "properties": { "status": { @@ -23972,7 +23972,7 @@ ], "additionalProperties": false }, - "ToolStateRunning": { + "SessionV1.ToolStateRunning": { "type": "object", "properties": { "status": { @@ -24029,7 +24029,7 @@ ], "additionalProperties": false }, - "ToolStateCompleted": { + "SessionV1.ToolStateCompleted": { "type": "object", "properties": { "status": { @@ -24096,7 +24096,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" } }, { @@ -24115,7 +24115,7 @@ ], "additionalProperties": false }, - "ToolStateError": { + "SessionV1.ToolStateError": { "type": "object", "properties": { "status": { @@ -24175,23 +24175,23 @@ ], "additionalProperties": false }, - "ToolState": { + "SessionV1.ToolState": { "anyOf": [ { - "$ref": "#/components/schemas/ToolStatePending" + "$ref": "#/components/schemas/SessionV1.ToolStatePending" }, { - "$ref": "#/components/schemas/ToolStateRunning" + "$ref": "#/components/schemas/SessionV1.ToolStateRunning" }, { - "$ref": "#/components/schemas/ToolStateCompleted" + "$ref": "#/components/schemas/SessionV1.ToolStateCompleted" }, { - "$ref": "#/components/schemas/ToolStateError" + "$ref": "#/components/schemas/SessionV1.ToolStateError" } ] }, - "ToolPart": { + "SessionV1.ToolPart": { "type": "object", "properties": { "id": { @@ -24231,7 +24231,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/ToolState" + "$ref": "#/components/schemas/SessionV1.ToolState" }, "metadata": { "anyOf": [ @@ -24255,7 +24255,7 @@ ], "additionalProperties": false }, - "StepStartPart": { + "SessionV1.StepStartPart": { "type": "object", "properties": { "id": { @@ -24307,7 +24307,7 @@ ], "additionalProperties": false }, - "StepFinishPart": { + "SessionV1.StepFinishPart": { "type": "object", "properties": { "id": { @@ -24415,7 +24415,7 @@ ], "additionalProperties": false }, - "SnapshotPart": { + "SessionV1.SnapshotPart": { "type": "object", "properties": { "id": { @@ -24461,7 +24461,7 @@ ], "additionalProperties": false }, - "PatchPart": { + "SessionV1.PatchPart": { "type": "object", "properties": { "id": { @@ -24514,7 +24514,7 @@ ], "additionalProperties": false }, - "AgentPart": { + "SessionV1.AgentPart": { "type": "object", "properties": { "id": { @@ -24597,7 +24597,7 @@ ], "additionalProperties": false }, - "RetryPart": { + "SessionV1.RetryPart": { "type": "object", "properties": { "id": { @@ -24670,7 +24670,7 @@ ], "additionalProperties": false }, - "CompactionPart": { + "SessionV1.CompactionPart": { "type": "object", "properties": { "id": { @@ -24741,43 +24741,43 @@ ], "additionalProperties": false }, - "Part": { + "SessionV1.Part": { "anyOf": [ { - "$ref": "#/components/schemas/TextPart" + "$ref": "#/components/schemas/SessionV1.TextPart" }, { - "$ref": "#/components/schemas/SubtaskPart" + "$ref": "#/components/schemas/SessionV1.SubtaskPart" }, { - "$ref": "#/components/schemas/ReasoningPart" + "$ref": "#/components/schemas/SessionV1.ReasoningPart" }, { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" }, { - "$ref": "#/components/schemas/ToolPart" + "$ref": "#/components/schemas/SessionV1.ToolPart" }, { - "$ref": "#/components/schemas/StepStartPart" + "$ref": "#/components/schemas/SessionV1.StepStartPart" }, { - "$ref": "#/components/schemas/StepFinishPart" + "$ref": "#/components/schemas/SessionV1.StepFinishPart" }, { - "$ref": "#/components/schemas/SnapshotPart" + "$ref": "#/components/schemas/SessionV1.SnapshotPart" }, { - "$ref": "#/components/schemas/PatchPart" + "$ref": "#/components/schemas/SessionV1.PatchPart" }, { - "$ref": "#/components/schemas/AgentPart" + "$ref": "#/components/schemas/SessionV1.AgentPart" }, { - "$ref": "#/components/schemas/RetryPart" + "$ref": "#/components/schemas/SessionV1.RetryPart" }, { - "$ref": "#/components/schemas/CompactionPart" + "$ref": "#/components/schemas/SessionV1.CompactionPart" } ] }, @@ -24847,7 +24847,7 @@ ] }, "part": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/SessionV1.Part" }, "time": { "type": "number" @@ -25472,7 +25472,7 @@ ], "additionalProperties": false }, - "permission.v2.asked": { + "permission.asked": { "type": "object", "properties": { "id": { @@ -25492,7 +25492,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.asked" + "permission.asked" ] }, "location": { @@ -25536,7 +25536,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -25556,7 +25556,7 @@ ], "additionalProperties": false }, - "permission.v2.replied": { + "permission.replied": { "type": "object", "properties": { "id": { @@ -25576,7 +25576,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.replied" + "permission.replied" ] }, "location": { @@ -25602,7 +25602,7 @@ ] }, "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" } }, "required": [ @@ -26327,7 +26327,7 @@ ], "additionalProperties": false }, - "QuestionV2.Option": { + "Question.Option": { "type": "object", "properties": { "label": { @@ -26345,7 +26345,7 @@ ], "additionalProperties": false }, - "QuestionV2.Info": { + "Question.Info": { "type": "object", "properties": { "question": { @@ -26359,7 +26359,7 @@ "options": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Option" + "$ref": "#/components/schemas/Question.Option" }, "description": "Available choices" }, @@ -26377,7 +26377,7 @@ ], "additionalProperties": false }, - "QuestionV2.Tool": { + "Question.Tool": { "type": "object", "properties": { "messageID": { @@ -26393,7 +26393,7 @@ ], "additionalProperties": false }, - "question.v2.asked": { + "question.asked": { "type": "object", "properties": { "id": { @@ -26413,7 +26413,7 @@ "type": { "type": "string", "enum": [ - "question.v2.asked" + "question.asked" ] }, "location": { @@ -26441,12 +26441,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -26465,13 +26465,13 @@ ], "additionalProperties": false }, - "QuestionV2.Answer": { + "Question.Answer": { "type": "array", "items": { "type": "string" } }, - "question.v2.replied": { + "question.replied": { "type": "object", "properties": { "id": { @@ -26491,7 +26491,7 @@ "type": { "type": "string", "enum": [ - "question.v2.replied" + "question.replied" ] }, "location": { @@ -26519,7 +26519,7 @@ "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" } } }, @@ -26539,7 +26539,7 @@ ], "additionalProperties": false }, - "question.v2.rejected": { + "question.rejected": { "type": "object", "properties": { "id": { @@ -26559,7 +26559,7 @@ "type": { "type": "string", "enum": [ - "question.v2.rejected" + "question.rejected" ] }, "location": { @@ -28068,483 +28068,6 @@ ], "additionalProperties": false }, - "permission.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "anyOf": [ - { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "permission.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow selecting multiple choices" - }, - "custom": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow typing a custom answer (default: true)" - } - }, - "required": [ - "question", - "header", - "options" - ], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg" - } - ] - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - "question.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionTool" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.rejected" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - } - }, - "required": [ - "sessionID", - "requestID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, "session.error": { "type": "object", "properties": { @@ -28865,10 +28388,10 @@ "$ref": "#/components/schemas/reference.updated" }, { - "$ref": "#/components/schemas/permission.v2.asked" + "$ref": "#/components/schemas/permission.asked" }, { - "$ref": "#/components/schemas/permission.v2.replied" + "$ref": "#/components/schemas/permission.replied" }, { "$ref": "#/components/schemas/plugin.added" @@ -28910,13 +28433,13 @@ "$ref": "#/components/schemas/shell.deleted" }, { - "$ref": "#/components/schemas/question.v2.asked" + "$ref": "#/components/schemas/question.asked" }, { - "$ref": "#/components/schemas/question.v2.replied" + "$ref": "#/components/schemas/question.replied" }, { - "$ref": "#/components/schemas/question.v2.rejected" + "$ref": "#/components/schemas/question.rejected" }, { "$ref": "#/components/schemas/form.created" @@ -28963,21 +28486,6 @@ { "$ref": "#/components/schemas/mcp.resources.changed" }, - { - "$ref": "#/components/schemas/permission.asked" - }, - { - "$ref": "#/components/schemas/permission.replied" - }, - { - "$ref": "#/components/schemas/question.asked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, { "$ref": "#/components/schemas/session.error" }, @@ -29153,7 +28661,7 @@ ], "additionalProperties": false }, - "QuestionV2.Request": { + "Question.Request": { "type": "object", "properties": { "id": { @@ -29175,12 +28683,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -29190,13 +28698,13 @@ ], "additionalProperties": false }, - "QuestionV2.Reply": { + "Question.Reply": { "type": "object", "properties": { "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" }, "description": "User answers in order of questions (each answer is an array of selected labels)" } diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 1c8b3dc9412..0508db24281 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -4279,7 +4279,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } } }, @@ -4396,7 +4396,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/ProviderV2.Info" + "$ref": "#/components/schemas/Provider.Info" } }, "required": [ @@ -7466,7 +7466,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7664,7 +7664,7 @@ ] }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -7762,7 +7762,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" }, "agent": { "anyOf": [ @@ -7818,7 +7818,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } } }, @@ -7916,7 +7916,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/PermissionV2.Request" + "$ref": "#/components/schemas/Permission.Request" } }, "required": [ @@ -8061,7 +8061,7 @@ "type": "object", "properties": { "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" }, "message": { "anyOf": [ @@ -10456,7 +10456,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10527,7 +10527,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Request" + "$ref": "#/components/schemas/Question.Request" } } }, @@ -10667,7 +10667,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionV2.Reply" + "$ref": "#/components/schemas/Question.Reply" } } }, @@ -11981,7 +11981,7 @@ "Agent.Color": { "type": "string" }, - "PermissionV2.Effect": { + "Permission.Effect": { "type": "string", "enum": [ "allow", @@ -11989,7 +11989,7 @@ "ask" ] }, - "PermissionV2.Rule": { + "Permission.Rule": { "type": "object", "properties": { "action": { @@ -11999,7 +11999,7 @@ "type": "string" }, "effect": { - "$ref": "#/components/schemas/PermissionV2.Effect" + "$ref": "#/components/schemas/Permission.Effect" } }, "required": [ @@ -12009,10 +12009,10 @@ ], "additionalProperties": false }, - "PermissionV2.Ruleset": { + "Permission.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionV2.Rule" + "$ref": "#/components/schemas/Permission.Rule" } }, "Agent.Info": { @@ -12059,7 +12059,7 @@ ] }, "permissions": { - "$ref": "#/components/schemas/PermissionV2.Ruleset" + "$ref": "#/components/schemas/Permission.Ruleset" } }, "required": [ @@ -18537,7 +18537,7 @@ ], "additionalProperties": false }, - "ProviderV2.Info": { + "Provider.Info": { "type": "object", "properties": { "id": { @@ -21466,7 +21466,7 @@ ], "additionalProperties": false }, - "PermissionV2.Source": { + "Permission.Source": { "anyOf": [ { "type": "object", @@ -21493,7 +21493,7 @@ } ] }, - "PermissionV2.Request": { + "Permission.Request": { "type": "object", "properties": { "id": { @@ -21531,7 +21531,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -21589,7 +21589,7 @@ ], "additionalProperties": false }, - "PermissionV2.Reply": { + "Permission.Reply": { "type": "string", "enum": [ "once", @@ -21935,7 +21935,7 @@ ], "additionalProperties": false }, - "PermissionAction": { + "PermissionV1.Action": { "type": "string", "enum": [ "allow", @@ -21943,7 +21943,7 @@ "ask" ] }, - "PermissionRule": { + "PermissionV1.Rule": { "type": "object", "properties": { "permission": { @@ -21953,7 +21953,7 @@ "type": "string" }, "action": { - "$ref": "#/components/schemas/PermissionAction" + "$ref": "#/components/schemas/PermissionV1.Action" } }, "required": [ @@ -21963,10 +21963,10 @@ ], "additionalProperties": false }, - "PermissionRuleset": { + "PermissionV1.Ruleset": { "type": "array", "items": { - "$ref": "#/components/schemas/PermissionRule" + "$ref": "#/components/schemas/PermissionV1.Rule" } }, "SessionV1.Info": { @@ -22155,7 +22155,7 @@ "additionalProperties": false }, "permission": { - "$ref": "#/components/schemas/PermissionRuleset" + "$ref": "#/components/schemas/PermissionV1.Ruleset" }, "revert": { "type": "object", @@ -22455,10 +22455,10 @@ ], "additionalProperties": false }, - "JSONSchema": { + "SessionV1.JSONSchema": { "type": "object" }, - "OutputFormat": { + "SessionV1.OutputFormat": { "anyOf": [ { "type": "object", @@ -22485,7 +22485,7 @@ ] }, "schema": { - "$ref": "#/components/schemas/JSONSchema" + "$ref": "#/components/schemas/SessionV1.JSONSchema" }, "retryCount": { "anyOf": [ @@ -22518,7 +22518,7 @@ } ] }, - "UserMessage": { + "SessionV1.UserMessage": { "type": "object", "properties": { "id": { @@ -22563,7 +22563,7 @@ "format": { "anyOf": [ { - "$ref": "#/components/schemas/OutputFormat" + "$ref": "#/components/schemas/SessionV1.OutputFormat" }, { "type": "null" @@ -22985,7 +22985,7 @@ ], "additionalProperties": false }, - "AssistantMessage": { + "SessionV1.AssistantMessage": { "type": "object", "properties": { "id": { @@ -23218,13 +23218,13 @@ ], "additionalProperties": false }, - "Message": { + "SessionV1.Message": { "anyOf": [ { - "$ref": "#/components/schemas/UserMessage" + "$ref": "#/components/schemas/SessionV1.UserMessage" }, { - "$ref": "#/components/schemas/AssistantMessage" + "$ref": "#/components/schemas/SessionV1.AssistantMessage" } ] }, @@ -23294,7 +23294,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Message" + "$ref": "#/components/schemas/SessionV1.Message" } }, "required": [ @@ -23403,7 +23403,7 @@ ], "additionalProperties": false }, - "TextPart": { + "SessionV1.TextPart": { "type": "object", "properties": { "id": { @@ -23518,7 +23518,7 @@ ], "additionalProperties": false }, - "SubtaskPart": { + "SessionV1.SubtaskPart": { "type": "object", "properties": { "id": { @@ -23605,7 +23605,7 @@ ], "additionalProperties": false }, - "ReasoningPart": { + "SessionV1.ReasoningPart": { "type": "object", "properties": { "id": { @@ -23694,7 +23694,7 @@ ], "additionalProperties": false }, - "FilePartSourceText": { + "SessionV1.FilePartSourceText": { "type": "object", "properties": { "value": { @@ -23714,11 +23714,11 @@ ], "additionalProperties": false }, - "FileSource": { + "SessionV1.FileSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23737,7 +23737,7 @@ ], "additionalProperties": false }, - "Range": { + "SessionV1.Range": { "type": "object", "properties": { "start": { @@ -23799,11 +23799,11 @@ ], "additionalProperties": false }, - "SymbolSource": { + "SessionV1.SymbolSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23815,7 +23815,7 @@ "type": "string" }, "range": { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/SessionV1.Range" }, "name": { "type": "string" @@ -23839,11 +23839,11 @@ ], "additionalProperties": false }, - "ResourceSource": { + "SessionV1.ResourceSource": { "type": "object", "properties": { "text": { - "$ref": "#/components/schemas/FilePartSourceText" + "$ref": "#/components/schemas/SessionV1.FilePartSourceText" }, "type": { "type": "string", @@ -23866,20 +23866,20 @@ ], "additionalProperties": false }, - "FilePartSource": { + "SessionV1.FilePartSource": { "anyOf": [ { - "$ref": "#/components/schemas/FileSource" + "$ref": "#/components/schemas/SessionV1.FileSource" }, { - "$ref": "#/components/schemas/SymbolSource" + "$ref": "#/components/schemas/SessionV1.SymbolSource" }, { - "$ref": "#/components/schemas/ResourceSource" + "$ref": "#/components/schemas/SessionV1.ResourceSource" } ] }, - "FilePart": { + "SessionV1.FilePart": { "type": "object", "properties": { "id": { @@ -23931,7 +23931,7 @@ "source": { "anyOf": [ { - "$ref": "#/components/schemas/FilePartSource" + "$ref": "#/components/schemas/SessionV1.FilePartSource" }, { "type": "null" @@ -23949,7 +23949,7 @@ ], "additionalProperties": false }, - "ToolStatePending": { + "SessionV1.ToolStatePending": { "type": "object", "properties": { "status": { @@ -23972,7 +23972,7 @@ ], "additionalProperties": false }, - "ToolStateRunning": { + "SessionV1.ToolStateRunning": { "type": "object", "properties": { "status": { @@ -24029,7 +24029,7 @@ ], "additionalProperties": false }, - "ToolStateCompleted": { + "SessionV1.ToolStateCompleted": { "type": "object", "properties": { "status": { @@ -24096,7 +24096,7 @@ { "type": "array", "items": { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" } }, { @@ -24115,7 +24115,7 @@ ], "additionalProperties": false }, - "ToolStateError": { + "SessionV1.ToolStateError": { "type": "object", "properties": { "status": { @@ -24175,23 +24175,23 @@ ], "additionalProperties": false }, - "ToolState": { + "SessionV1.ToolState": { "anyOf": [ { - "$ref": "#/components/schemas/ToolStatePending" + "$ref": "#/components/schemas/SessionV1.ToolStatePending" }, { - "$ref": "#/components/schemas/ToolStateRunning" + "$ref": "#/components/schemas/SessionV1.ToolStateRunning" }, { - "$ref": "#/components/schemas/ToolStateCompleted" + "$ref": "#/components/schemas/SessionV1.ToolStateCompleted" }, { - "$ref": "#/components/schemas/ToolStateError" + "$ref": "#/components/schemas/SessionV1.ToolStateError" } ] }, - "ToolPart": { + "SessionV1.ToolPart": { "type": "object", "properties": { "id": { @@ -24231,7 +24231,7 @@ "type": "string" }, "state": { - "$ref": "#/components/schemas/ToolState" + "$ref": "#/components/schemas/SessionV1.ToolState" }, "metadata": { "anyOf": [ @@ -24255,7 +24255,7 @@ ], "additionalProperties": false }, - "StepStartPart": { + "SessionV1.StepStartPart": { "type": "object", "properties": { "id": { @@ -24307,7 +24307,7 @@ ], "additionalProperties": false }, - "StepFinishPart": { + "SessionV1.StepFinishPart": { "type": "object", "properties": { "id": { @@ -24415,7 +24415,7 @@ ], "additionalProperties": false }, - "SnapshotPart": { + "SessionV1.SnapshotPart": { "type": "object", "properties": { "id": { @@ -24461,7 +24461,7 @@ ], "additionalProperties": false }, - "PatchPart": { + "SessionV1.PatchPart": { "type": "object", "properties": { "id": { @@ -24514,7 +24514,7 @@ ], "additionalProperties": false }, - "AgentPart": { + "SessionV1.AgentPart": { "type": "object", "properties": { "id": { @@ -24597,7 +24597,7 @@ ], "additionalProperties": false }, - "RetryPart": { + "SessionV1.RetryPart": { "type": "object", "properties": { "id": { @@ -24670,7 +24670,7 @@ ], "additionalProperties": false }, - "CompactionPart": { + "SessionV1.CompactionPart": { "type": "object", "properties": { "id": { @@ -24741,43 +24741,43 @@ ], "additionalProperties": false }, - "Part": { + "SessionV1.Part": { "anyOf": [ { - "$ref": "#/components/schemas/TextPart" + "$ref": "#/components/schemas/SessionV1.TextPart" }, { - "$ref": "#/components/schemas/SubtaskPart" + "$ref": "#/components/schemas/SessionV1.SubtaskPart" }, { - "$ref": "#/components/schemas/ReasoningPart" + "$ref": "#/components/schemas/SessionV1.ReasoningPart" }, { - "$ref": "#/components/schemas/FilePart" + "$ref": "#/components/schemas/SessionV1.FilePart" }, { - "$ref": "#/components/schemas/ToolPart" + "$ref": "#/components/schemas/SessionV1.ToolPart" }, { - "$ref": "#/components/schemas/StepStartPart" + "$ref": "#/components/schemas/SessionV1.StepStartPart" }, { - "$ref": "#/components/schemas/StepFinishPart" + "$ref": "#/components/schemas/SessionV1.StepFinishPart" }, { - "$ref": "#/components/schemas/SnapshotPart" + "$ref": "#/components/schemas/SessionV1.SnapshotPart" }, { - "$ref": "#/components/schemas/PatchPart" + "$ref": "#/components/schemas/SessionV1.PatchPart" }, { - "$ref": "#/components/schemas/AgentPart" + "$ref": "#/components/schemas/SessionV1.AgentPart" }, { - "$ref": "#/components/schemas/RetryPart" + "$ref": "#/components/schemas/SessionV1.RetryPart" }, { - "$ref": "#/components/schemas/CompactionPart" + "$ref": "#/components/schemas/SessionV1.CompactionPart" } ] }, @@ -24847,7 +24847,7 @@ ] }, "part": { - "$ref": "#/components/schemas/Part" + "$ref": "#/components/schemas/SessionV1.Part" }, "time": { "type": "number" @@ -25472,7 +25472,7 @@ ], "additionalProperties": false }, - "permission.v2.asked": { + "permission.asked": { "type": "object", "properties": { "id": { @@ -25492,7 +25492,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.asked" + "permission.asked" ] }, "location": { @@ -25536,7 +25536,7 @@ "type": "object" }, "source": { - "$ref": "#/components/schemas/PermissionV2.Source" + "$ref": "#/components/schemas/Permission.Source" } }, "required": [ @@ -25556,7 +25556,7 @@ ], "additionalProperties": false }, - "permission.v2.replied": { + "permission.replied": { "type": "object", "properties": { "id": { @@ -25576,7 +25576,7 @@ "type": { "type": "string", "enum": [ - "permission.v2.replied" + "permission.replied" ] }, "location": { @@ -25602,7 +25602,7 @@ ] }, "reply": { - "$ref": "#/components/schemas/PermissionV2.Reply" + "$ref": "#/components/schemas/Permission.Reply" } }, "required": [ @@ -26327,7 +26327,7 @@ ], "additionalProperties": false }, - "QuestionV2.Option": { + "Question.Option": { "type": "object", "properties": { "label": { @@ -26345,7 +26345,7 @@ ], "additionalProperties": false }, - "QuestionV2.Info": { + "Question.Info": { "type": "object", "properties": { "question": { @@ -26359,7 +26359,7 @@ "options": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Option" + "$ref": "#/components/schemas/Question.Option" }, "description": "Available choices" }, @@ -26377,7 +26377,7 @@ ], "additionalProperties": false }, - "QuestionV2.Tool": { + "Question.Tool": { "type": "object", "properties": { "messageID": { @@ -26393,7 +26393,7 @@ ], "additionalProperties": false }, - "question.v2.asked": { + "question.asked": { "type": "object", "properties": { "id": { @@ -26413,7 +26413,7 @@ "type": { "type": "string", "enum": [ - "question.v2.asked" + "question.asked" ] }, "location": { @@ -26441,12 +26441,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -26465,13 +26465,13 @@ ], "additionalProperties": false }, - "QuestionV2.Answer": { + "Question.Answer": { "type": "array", "items": { "type": "string" } }, - "question.v2.replied": { + "question.replied": { "type": "object", "properties": { "id": { @@ -26491,7 +26491,7 @@ "type": { "type": "string", "enum": [ - "question.v2.replied" + "question.replied" ] }, "location": { @@ -26519,7 +26519,7 @@ "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" } } }, @@ -26539,7 +26539,7 @@ ], "additionalProperties": false }, - "question.v2.rejected": { + "question.rejected": { "type": "object", "properties": { "id": { @@ -26559,7 +26559,7 @@ "type": { "type": "string", "enum": [ - "question.v2.rejected" + "question.rejected" ] }, "location": { @@ -28068,483 +28068,6 @@ ], "additionalProperties": false }, - "permission.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object" - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "anyOf": [ - { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "permission.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "permission.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^per" - } - ] - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "Display text (1-5 words, concise)" - }, - "description": { - "type": "string", - "description": "Explanation of choice" - } - }, - "required": [ - "label", - "description" - ], - "additionalProperties": false - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Complete question" - }, - "header": { - "type": "string", - "description": "Very short label (max 30 chars)" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - }, - "description": "Available choices" - }, - "multiple": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow selecting multiple choices" - }, - "custom": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "description": "Allow typing a custom answer (default: true)" - } - }, - "required": [ - "question", - "header", - "options" - ], - "additionalProperties": false - }, - "QuestionTool": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "allOf": [ - { - "pattern": "^msg" - } - ] - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ], - "additionalProperties": false - }, - "question.asked": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.asked" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "questions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - }, - "description": "Questions to ask" - }, - "tool": { - "anyOf": [ - { - "$ref": "#/components/schemas/QuestionTool" - }, - { - "type": "null" - } - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "question.replied": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.replied" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, - "question.rejected": { - "type": "object", - "properties": { - "id": { - "type": "string", - "allOf": [ - { - "pattern": "^evt_" - } - ] - }, - "created": { - "type": "number" - }, - "metadata": { - "type": "object" - }, - "type": { - "type": "string", - "enum": [ - "question.rejected" - ] - }, - "location": { - "$ref": "#/components/schemas/Location.Ref" - }, - "data": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, - "requestID": { - "type": "string", - "allOf": [ - { - "pattern": "^que" - } - ] - } - }, - "required": [ - "sessionID", - "requestID" - ], - "additionalProperties": false - } - }, - "required": [ - "id", - "created", - "type", - "data" - ], - "additionalProperties": false - }, "session.error": { "type": "object", "properties": { @@ -28865,10 +28388,10 @@ "$ref": "#/components/schemas/reference.updated" }, { - "$ref": "#/components/schemas/permission.v2.asked" + "$ref": "#/components/schemas/permission.asked" }, { - "$ref": "#/components/schemas/permission.v2.replied" + "$ref": "#/components/schemas/permission.replied" }, { "$ref": "#/components/schemas/plugin.added" @@ -28910,13 +28433,13 @@ "$ref": "#/components/schemas/shell.deleted" }, { - "$ref": "#/components/schemas/question.v2.asked" + "$ref": "#/components/schemas/question.asked" }, { - "$ref": "#/components/schemas/question.v2.replied" + "$ref": "#/components/schemas/question.replied" }, { - "$ref": "#/components/schemas/question.v2.rejected" + "$ref": "#/components/schemas/question.rejected" }, { "$ref": "#/components/schemas/form.created" @@ -28963,21 +28486,6 @@ { "$ref": "#/components/schemas/mcp.resources.changed" }, - { - "$ref": "#/components/schemas/permission.asked" - }, - { - "$ref": "#/components/schemas/permission.replied" - }, - { - "$ref": "#/components/schemas/question.asked" - }, - { - "$ref": "#/components/schemas/question.replied" - }, - { - "$ref": "#/components/schemas/question.rejected" - }, { "$ref": "#/components/schemas/session.error" }, @@ -29153,7 +28661,7 @@ ], "additionalProperties": false }, - "QuestionV2.Request": { + "Question.Request": { "type": "object", "properties": { "id": { @@ -29175,12 +28683,12 @@ "questions": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Info" + "$ref": "#/components/schemas/Question.Info" }, "description": "Questions to ask" }, "tool": { - "$ref": "#/components/schemas/QuestionV2.Tool" + "$ref": "#/components/schemas/Question.Tool" } }, "required": [ @@ -29190,13 +28698,13 @@ ], "additionalProperties": false }, - "QuestionV2.Reply": { + "Question.Reply": { "type": "object", "properties": { "answers": { "type": "array", "items": { - "$ref": "#/components/schemas/QuestionV2.Answer" + "$ref": "#/components/schemas/Question.Answer" }, "description": "User answers in order of questions (each answer is an array of selected labels)" } diff --git a/patches/effect@4.0.0-beta.98.patch b/patches/effect@4.0.0-beta.101.patch similarity index 100% rename from patches/effect@4.0.0-beta.98.patch rename to patches/effect@4.0.0-beta.101.patch