mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-23 21:16:06 +00:00
121 lines
5.5 KiB
Markdown
121 lines
5.5 KiB
Markdown
# V2 Core Instructions
|
|
|
|
These notes describe how to work on `packages/core` during the v2 port.
|
|
|
|
## Direction
|
|
|
|
Move behavior out of large application services and into plugins. Core services should become small, typed containers that own state, expose simple operations, and trigger hooks where policy or integration-specific logic belongs.
|
|
|
|
The target shape is:
|
|
|
|
- `packages/core` contains domain schemas, typed errors, state containers, events, and plugin hook contracts.
|
|
- Plugins implement provider-specific, config-specific, auth-specific, model-discovery, and generation behavior.
|
|
- Services are hot-reloadable by design: updates are granular, observable, and do not require tearing down the whole process.
|
|
- `packages/opencode` becomes thinner over time: UI, server routes, CLI, storage glue, and legacy compatibility should call the core services instead of owning domain logic directly.
|
|
|
|
## Service Shape
|
|
|
|
Core services should look like `Catalog`, `AccountV2`, and `AgentV2`:
|
|
|
|
- define schemas and branded ids at the top of the module
|
|
- define typed `Schema.TaggedErrorClass` errors for expected failures
|
|
- define an `Interface` with small operations
|
|
- expose a `Context.Service`
|
|
- implement `layer` with private in-memory state
|
|
- expose `defaultLayer` with explicit dependencies
|
|
- self-export with `export * as Name from "./file"`
|
|
|
|
Prefer a dumb container API:
|
|
|
|
- `get`, `all`, `available`, `default`, `update`, `remove`, `activate`, or other small domain verbs
|
|
- `update(id, draft => ...)` for registration and mutation
|
|
- hook calls before committing mutations when plugins need to enrich, cancel, or validate changes
|
|
- events after committing mutations when other services or frontends need to react
|
|
|
|
Avoid putting application policy directly in core services unless it is a domain invariant. For example, resolving model endpoint inheritance is catalog-owned; deciding which providers to register is plugin-owned.
|
|
|
|
## Plugin Hooks
|
|
|
|
Plugins are the extension boundary for v2. Add hooks to `PluginV2.HookSpec` when logic should be provided by integrations instead of the container itself.
|
|
|
|
Hook conventions:
|
|
|
|
- hooks receive immutable input plus mutable output
|
|
- mutable object outputs are exposed as Immer drafts
|
|
- include `cancel: boolean` when plugins can prevent a mutation
|
|
- trigger hooks sequentially so ordering remains deterministic
|
|
- keep hook names domain-oriented, like `provider.update`, `model.update`, `account.activate`, `agent.generate`
|
|
- keep hook payloads small and typed with core schemas
|
|
|
|
Use hooks for:
|
|
|
|
- registering providers and models
|
|
- applying env/account/config-derived enablement
|
|
- transforming SDK/provider options
|
|
- implementing generated behavior such as agent generation
|
|
- choosing defaults when the choice is policy rather than state
|
|
|
|
Do not use hooks as a dumping ground for transport concerns, UI behavior, or compatibility shims.
|
|
|
|
## Plugin Boot
|
|
|
|
Built-in core plugins are registered by `packages/core/src/plugin/boot.ts`.
|
|
|
|
When a new core service is intended to be available to plugins:
|
|
|
|
- add the service to the boot layer dependency type
|
|
- yield the service inside the layer
|
|
- provide it to each plugin effect in `add`
|
|
- add its default layer to `PluginBoot.defaultLayer` only when that does not create a cycle
|
|
|
|
Keep boot as composition only. It should not contain provider, account, agent, or model policy itself.
|
|
|
|
## Boundaries
|
|
|
|
Core should not import from `packages/opencode`. If a type or concept is needed by core, move or remodel the domain shape in core first.
|
|
|
|
Avoid moving legacy services over wholesale. Port the domain shape and the container API, then leave specific behavior behind hooks for plugins to implement.
|
|
|
|
When porting an opencode service:
|
|
|
|
- identify the state it owns
|
|
- identify the operations callers actually need
|
|
- identify which branches are policy or integration behavior
|
|
- model state and operations in `packages/core`
|
|
- add hooks for the policy/integration branches
|
|
- keep old package code working until callers can migrate incrementally
|
|
|
|
## Schemas And Types
|
|
|
|
Use Effect schemas as the public contract:
|
|
|
|
- branded schemas for ids
|
|
- `Schema.Class` or `Schema.Struct` for domain data
|
|
- `Schema.TaggedErrorClass` for expected errors
|
|
- existing core helpers like `DeepMutable`, `withStatics`, and integer schemas where appropriate
|
|
|
|
Prefer `Info` objects as the stored domain records. Add static `empty(...)` constructors when update APIs need to create records on first mutation.
|
|
|
|
Keep schemas stable and explicit. Do not rely on opencode config shapes as core domain shapes unless the config shape is actually the domain model.
|
|
|
|
## State And Events
|
|
|
|
Keep state private to the service layer. Use immutable replacement or Effect refs when persistence/concurrency requires it.
|
|
|
|
Publish events for committed domain changes, not for attempted mutations. Event names should describe domain facts, for example `catalog.model.updated`.
|
|
|
|
The v2 goal is granular reconfiguration. A model update should let dependents react to that model update; it should not require global reloads.
|
|
|
|
## Style
|
|
|
|
Follow the local core style:
|
|
|
|
- `Effect.gen(function* () { ... })` for composition
|
|
- `Effect.fn("Domain.method")` for public service methods
|
|
- `Effect.fnUntraced` for small internal mutation helpers
|
|
- `yield* new ErrorClass(...)` for typed failures
|
|
- minimal helpers unless they name a real concept
|
|
- no `any` unless an existing plugin boundary requires it
|
|
- no compatibility code without a concrete persisted or external-consumer need
|
|
|
|
Prefer the smallest correct port. The goal is to make services easier to replace and reason about, not to recreate the old architecture in a new package.
|