refactor(core): consolidate tool architecture
This commit is contained in:
parent
0fd73a2976
commit
8db7487c89
466 changed files with 9405 additions and 11071 deletions
515
packages/plugin/src/effect/PLAN.md
Normal file
515
packages/plugin/src/effect/PLAN.md
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
# V2 Plugin System Implementation Plan
|
||||
|
||||
## Status
|
||||
|
||||
This document describes the agreed target design for the V2 plugin system. It is an implementation plan, not documentation for the current API.
|
||||
|
||||
## Goals
|
||||
|
||||
- Internal and external plugins use the same public plugin API.
|
||||
- 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.
|
||||
- Registrations are scoped, independently disposable, ordered, and removable.
|
||||
- Dynamic sources such as models.dev, config files, and skill directories can rebuild one domain without reloading the entire Location.
|
||||
- The initial implementation covers the Effect API. A Promise API will be designed afterward as a wrapper over the same capabilities.
|
||||
|
||||
## Authoring Model
|
||||
|
||||
A plugin setup effect receives `PluginHost` and imperatively registers transforms and hooks.
|
||||
|
||||
```ts
|
||||
export const Plugin = define({
|
||||
id: "example",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (agent) {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code for regressions"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.tool.hook(
|
||||
"execute.before",
|
||||
Effect.fn(function* (event) {
|
||||
event.args.update(sanitizeArgs)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Plugin setup does not return hooks.
|
||||
|
||||
## Public Naming
|
||||
|
||||
Settled names:
|
||||
|
||||
- Replayable domain registration: `transform`
|
||||
- Explicit domain replay: `rebuild`
|
||||
- Runtime callback registration: `hook`
|
||||
- Registration cleanup: `dispose`
|
||||
- Event domain: singular `event`
|
||||
- Other domains are singular: `agent`, `command`, `integration`, `reference`, `session`, `skill`, and `tool`; `catalog` remains `catalog`
|
||||
- Hook names use dotted lifecycle names such as `"execute.before"` and `"execute.after"`
|
||||
|
||||
## Transform API
|
||||
|
||||
Each transformable domain exposes:
|
||||
|
||||
```ts
|
||||
interface TransformDomain<Editor> {
|
||||
transform(callback: (editor: Editor) => Effect.Effect<void>): Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
rebuild(): Effect.Effect<void>
|
||||
}
|
||||
```
|
||||
|
||||
The actual callback may be represented with the project's normal `Effect.fn` style.
|
||||
|
||||
```ts
|
||||
const registration =
|
||||
yield *
|
||||
ctx.catalog.transform(
|
||||
Effect.fn(function* (catalog) {
|
||||
const integration = yield* ctx.integration.get("anthropic")
|
||||
if (!integration) return
|
||||
|
||||
catalog.provider.update("anthropic", (provider) => {
|
||||
provider.name = "Anthropic"
|
||||
})
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Transforms may perform arbitrary Effects, including reads from other PluginHost services, filesystem I/O, and network I/O. Reads from another domain observe that domain's latest committed state.
|
||||
|
||||
Transforms have no typed error channel. Unexpected failures are defects.
|
||||
|
||||
## Transform Semantics
|
||||
|
||||
- Every call to `transform()` creates an independent registration.
|
||||
- Multiple transforms from one plugin and domain are allowed.
|
||||
- Transform order is plugin registration order, then transform registration order within the plugin.
|
||||
- A transform is automatically removed when its registration scope closes.
|
||||
- `Registration.dispose` removes it early and is idempotent.
|
||||
- Registering or disposing a transform automatically rebuilds its domain.
|
||||
- During bulk plugin boot, automatic rebuilds are deferred and each affected domain is rebuilt once after the batch.
|
||||
- `rebuild()` waits until replay and finalization complete.
|
||||
- `rebuild()` always replays every active transform for the domain.
|
||||
- Rebuilds are serialized and coalesced. Calls arriving during an active rebuild schedule at most one additional rebuild.
|
||||
- A rebuild captures its registration list at the start. Concurrent registration changes affect the next rebuild.
|
||||
- Transforms may not register or dispose transforms while replaying. Such changes are rejected or deferred by the runtime.
|
||||
- Calling `rebuild()` for the currently rebuilding domain from one of its transforms is rejected.
|
||||
- Rebuilding another domain from a transform is deferred until the current transform finishes.
|
||||
|
||||
## Registration API
|
||||
|
||||
Transforms and runtime hooks return the same Effect registration type.
|
||||
|
||||
```ts
|
||||
interface Registration {
|
||||
readonly dispose: Effect.Effect<void>
|
||||
}
|
||||
```
|
||||
|
||||
Registration behavior:
|
||||
|
||||
- Automatically attached to the current `Scope.Scope`
|
||||
- Explicitly disposable before scope closure
|
||||
- Disposal affects future replays or invocations
|
||||
- An in-flight rebuild or hook invocation uses the registration snapshot captured when it started and is allowed to finish
|
||||
|
||||
## Runtime Hook API
|
||||
|
||||
Domains expose runtime interception through `hook()`.
|
||||
|
||||
```ts
|
||||
const registration =
|
||||
yield *
|
||||
ctx.tool.hook(
|
||||
"execute.before",
|
||||
Effect.fn(function* (event) {
|
||||
event.args.update(sanitizeArgs)
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
Runtime hook behavior:
|
||||
|
||||
- Multiple registrations for the same hook are allowed.
|
||||
- Hooks run sequentially in plugin and registration order.
|
||||
- Later hooks observe mutations made by earlier hooks.
|
||||
- Hook registration is scope-owned and independently disposable.
|
||||
- Disposal affects future invocations; an in-flight invocation finishes using its captured registration snapshot.
|
||||
- Runtime hooks are not replayed during domain rebuilds.
|
||||
- Runtime hook callbacks have no typed error channel.
|
||||
|
||||
## Hook Contexts
|
||||
|
||||
Each hook receives one purpose-built context object rather than separate input/output parameters.
|
||||
|
||||
```ts
|
||||
ctx.tool.hook("execute.before", (event) => {
|
||||
event.args.update((args) => ({
|
||||
...args,
|
||||
timeout: 30,
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
Hook context objects may contain:
|
||||
|
||||
- Readonly SDK-typed operation data
|
||||
- Purpose-built methods for allowed mutations
|
||||
- Capability methods where the operation requires more than field assignment
|
||||
|
||||
They must not expose core drafts or unrestricted internal objects.
|
||||
|
||||
## Domain Transforms Versus Runtime Hooks
|
||||
|
||||
Both use the same low-level scoped registration registry, but consumers invoke them differently.
|
||||
|
||||
```ts
|
||||
ctx.tool.transform(...) // replayed to build effective tool registry state
|
||||
ctx.tool.hook(...) // invoked at a live tool operation boundary
|
||||
```
|
||||
|
||||
The shared low-level machinery owns registration order, scope cleanup, disposal, and snapshots. Each domain owns when its transforms or runtime hooks execute.
|
||||
|
||||
## Event API
|
||||
|
||||
The Effect API exposes the existing event system as typed streams using generated SDK event discriminants.
|
||||
|
||||
```ts
|
||||
ctx.event.subscribe("catalog.updated")
|
||||
// Stream.Stream<EventCatalogUpdated>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.event.subscribe("catalog.updated").pipe(
|
||||
Stream.runForEach(() => ctx.agent.rebuild()),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
```
|
||||
|
||||
The plugin package derives event payload types from the generated SDK `Event` union:
|
||||
|
||||
```ts
|
||||
type EventMap = {
|
||||
[Item in Event as Item["type"]]: Item
|
||||
}
|
||||
```
|
||||
|
||||
Core resolves the public event type string to its internal event definition and delegates to `Event.Service.subscribe`.
|
||||
|
||||
## Domain State Model
|
||||
|
||||
Each transformable core service continues to own:
|
||||
|
||||
- Base state
|
||||
- Effective committed state
|
||||
- Editor creation
|
||||
- Ordered transform registrations for that domain
|
||||
- Rebuild serialization and coalescing
|
||||
- Core finalization
|
||||
- Commit and post-commit events
|
||||
|
||||
The initial implementation should evolve the existing generic `State` helper rather than create a central cross-domain state manager.
|
||||
|
||||
```text
|
||||
base state
|
||||
→ replay active transforms in order
|
||||
→ core domain finalization
|
||||
→ commit effective state
|
||||
→ publish updated event
|
||||
```
|
||||
|
||||
No cross-domain transform or transaction API is included.
|
||||
|
||||
## Finalization
|
||||
|
||||
Each domain has one plugin transform phase followed by core finalization.
|
||||
|
||||
Core finalization is for invariants and materialization, not plugin extension behavior.
|
||||
|
||||
Examples:
|
||||
|
||||
- Catalog policy filtering and validation
|
||||
- Reference repository materialization
|
||||
- Integration connection projection
|
||||
- Index construction
|
||||
- Post-commit update events
|
||||
|
||||
Finalizers should distinguish pre-commit work from post-commit notification. Update events should publish after the new state is visible.
|
||||
|
||||
## Plugin Order
|
||||
|
||||
The default distribution uses an opinionated internal order:
|
||||
|
||||
```text
|
||||
1. Built-in agents, commands, and skills
|
||||
2. Base data sources such as models.dev
|
||||
3. Configuration projections
|
||||
4. Provider-specific normalization and authentication
|
||||
5. External user plugins
|
||||
6. Core domain finalization
|
||||
```
|
||||
|
||||
For catalog transforms:
|
||||
|
||||
```text
|
||||
models.dev
|
||||
→ config provider overrides
|
||||
→ built-in provider normalization
|
||||
→ user catalog transforms
|
||||
→ catalog finalization
|
||||
```
|
||||
|
||||
This replaces the current distinction between setup-installed State transforms and catalog hooks invoked from the catalog finalizer.
|
||||
|
||||
Replacing a plugin with the same ID retains its existing order position. The old plugin is disabled before the replacement setup starts.
|
||||
|
||||
## Boot Batching
|
||||
|
||||
Plugin boot runs in an internal registration batch.
|
||||
|
||||
```text
|
||||
begin batch
|
||||
→ initialize plugins sequentially
|
||||
→ register transforms and hooks
|
||||
→ collect affected domains
|
||||
→ rebuild each affected domain once
|
||||
→ end batch
|
||||
```
|
||||
|
||||
Registration itself is not staged per plugin. If setup fails, closing the plugin's child scope removes every registration made before the failure. A replacement then retries the previous definition; if that setup also fails, the plugin remains inactive.
|
||||
|
||||
Outside a batch, transform registration and disposal rebuild immediately.
|
||||
|
||||
## Models.dev Example
|
||||
|
||||
Models.dev performs effectful reads directly from its transforms and rebuilds affected domains after refresh.
|
||||
|
||||
```ts
|
||||
export const ModelsDevPlugin = define({
|
||||
id: "models-dev",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const event = yield* Event.Service
|
||||
|
||||
yield* ctx.integration.transform(
|
||||
Effect.fn(function* (integration) {
|
||||
const data = yield* modelsDev.get()
|
||||
applyIntegrations(data, integration)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.catalog.transform(
|
||||
Effect.fn(function* (catalog) {
|
||||
const data = yield* modelsDev.get()
|
||||
applyCatalog(data, catalog)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* event.subscribe(ModelsDev.Event.Refreshed).pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
yield* ctx.integration.rebuild()
|
||||
yield* ctx.catalog.rebuild()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The two domains rebuild sequentially. This plan does not add a cross-domain atomic transaction.
|
||||
|
||||
## Config Watcher Example
|
||||
|
||||
```ts
|
||||
export const ConfigPlugin = define({
|
||||
id: "config",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
const config = yield* ConfigSource.Service
|
||||
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (agent) {
|
||||
applyAgentConfig(yield* config.get(), agent)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.command.transform(
|
||||
Effect.fn(function* (command) {
|
||||
applyCommandConfig(yield* config.get(), command)
|
||||
}),
|
||||
)
|
||||
|
||||
yield* config.changes.pipe(
|
||||
Stream.runForEach(
|
||||
Effect.fn(function* () {
|
||||
yield* ctx.agent.rebuild()
|
||||
yield* ctx.command.rebuild()
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## Cross-Domain Read Example
|
||||
|
||||
A transform may read another committed service. It must still arrange for its own domain to rebuild when that dependency changes.
|
||||
|
||||
```ts
|
||||
export const AnthropicAgentPlugin = define({
|
||||
id: "anthropic-agent",
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.agent.transform(
|
||||
Effect.fn(function* (agent) {
|
||||
const providers = yield* ctx.catalog.provider.list()
|
||||
if (!providers.some((provider) => provider.id === "anthropic")) return
|
||||
|
||||
agent.update("anthropic-reviewer", (item) => {
|
||||
item.description = "Reviews code using Anthropic"
|
||||
item.mode = "subagent"
|
||||
item.model = {
|
||||
providerID: "anthropic",
|
||||
id: "claude-sonnet",
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
yield* ctx.event.subscribe("catalog.updated").pipe(
|
||||
Stream.runForEach(() => ctx.agent.rebuild()),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
The runtime does not infer cross-domain dependencies.
|
||||
|
||||
## Embedding API Compatibility
|
||||
|
||||
The imperative registration model maps naturally to a future application embedding API:
|
||||
|
||||
```ts
|
||||
const registration = oc.agent.transform((agent) => {
|
||||
agent.update("reviewer", configureReviewer)
|
||||
})
|
||||
|
||||
registration.dispose()
|
||||
```
|
||||
|
||||
An application registration is stored as an application-level plugin registration. It attaches to every current Location and is installed during future Location boot. Disposal removes all current attachments and prevents future attachment.
|
||||
|
||||
The Effect implementation remains the canonical runtime. Promise and embedding wrappers are deferred until after the Effect API is stable.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 1. Define Public Contracts
|
||||
|
||||
- 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`.
|
||||
- Define typed `event.subscribe(type)`.
|
||||
|
||||
### 2. Generalize Registration Machinery
|
||||
|
||||
- Add one low-level scoped registration registry used by transforms and runtime hooks.
|
||||
- Preserve plugin order and registration order.
|
||||
- Support idempotent disposal and registration snapshots.
|
||||
- Retain plugin position during same-ID replacement.
|
||||
|
||||
### 3. Evolve State
|
||||
|
||||
- Replace the current returned transform-slot updater with direct `transform(callback)` registration.
|
||||
- Support Effectful callbacks.
|
||||
- Add public `rebuild()`.
|
||||
- Add rebuild serialization and coalescing.
|
||||
- Add boot batching that defers automatic rebuilds.
|
||||
- Move update event publication after commit.
|
||||
|
||||
### 4. Expand Domain Transform Hooks
|
||||
|
||||
- Agent
|
||||
- Catalog
|
||||
- Command
|
||||
- Integration
|
||||
- Reference
|
||||
- Skill
|
||||
- Tool
|
||||
|
||||
### 5. Migrate Existing Plugins
|
||||
|
||||
- Built-in agent transform
|
||||
- Built-in command transform
|
||||
- Built-in skill transform
|
||||
- Models.dev catalog and integration transforms
|
||||
- Config transforms
|
||||
- OpenAI integration transform
|
||||
- Provider catalog transforms
|
||||
|
||||
### 6. Migrate Runtime Hooks
|
||||
|
||||
- AI SDK resolution
|
||||
- Language model resolution
|
||||
- Tool execution hooks
|
||||
- Session prompt/context hooks as required
|
||||
|
||||
### 7. Remove Returned Hooks
|
||||
|
||||
- Remove `HookFunctions` as the plugin setup return value.
|
||||
- Remove catalog's special finalizer-triggered plugin hook path.
|
||||
- Remove `plugin.added` catalog mutation handling.
|
||||
- Make add/remove/replacement rely on scoped registration and domain rebuilds.
|
||||
|
||||
### 8. Add Event Adapter
|
||||
|
||||
- Build the SDK event discriminant map.
|
||||
- Resolve public type strings to internal Event definitions.
|
||||
- Return typed Effect streams.
|
||||
|
||||
### 9. Verification
|
||||
|
||||
- Transform order is deterministic.
|
||||
- Multiple transforms per plugin/domain compose.
|
||||
- Registration and disposal rebuild automatically outside boot batches.
|
||||
- Boot performs one rebuild per affected domain.
|
||||
- Plugin setup failure removes prior registrations.
|
||||
- Same-ID replacement retains order and disables the old plugin first.
|
||||
- Rebuilds serialize and coalesce.
|
||||
- Registration changes during replay affect the next rebuild.
|
||||
- Same-domain recursive rebuild is rejected.
|
||||
- Cross-domain rebuild requests from transforms are deferred.
|
||||
- Hook execution is sequential and snapshot-based.
|
||||
- Models.dev refresh replays config and provider transforms.
|
||||
- Config and skill watcher refreshes remove stale entries.
|
||||
- Plugin removal restores prior effective state.
|
||||
- Events observe newly committed state.
|
||||
|
||||
## Deferred Decisions
|
||||
|
||||
- Promise API shape
|
||||
- Typed error model
|
||||
- Transform timeouts
|
||||
- Cross-domain atomic rebuilds
|
||||
- Automatic dependency tracking
|
||||
- Whole-Location generation reload
|
||||
- Exact editors and runtime hooks not required by current plugins
|
||||
122
packages/plugin/src/effect/README.md
Normal file
122
packages/plugin/src/effect/README.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# OpenCode V2 Effect Plugin API
|
||||
|
||||
The Effect plugin API grants plugins two in-process capabilities:
|
||||
|
||||
- `hook` installs behavior at an OpenCode extension point.
|
||||
- `reload` reruns every transform hook for a stateful domain.
|
||||
|
||||
## Defining A Plugin
|
||||
|
||||
```ts
|
||||
import { Plugin } from "@opencode-ai/plugin/effect"
|
||||
import { Effect } from "effect"
|
||||
|
||||
export default Plugin.define({
|
||||
id: "example",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
yield* ctx.catalog.transform((catalog) => {
|
||||
catalog.provider.update("example", (provider) => {
|
||||
provider.name = "Example"
|
||||
})
|
||||
})
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
Plugin setup registers hooks imperatively through each domain's `hook` method.
|
||||
|
||||
Configuration supplied for the plugin is available as `ctx.options`.
|
||||
|
||||
Registrations are owned by the plugin scope. Closing the scope removes them automatically; a registration may also be removed early through `dispose`.
|
||||
|
||||
## Transform Hooks
|
||||
|
||||
Transform hooks contribute to stateful domains:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.agent.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = "Reviews code for regressions"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
OpenCode rebuilds the domain when a transform is registered or disposed. A rebuild starts from fresh domain state and runs every active transform in registration order.
|
||||
|
||||
Available transform hooks are namespaced by domain:
|
||||
|
||||
```ts
|
||||
ctx.agent.transform
|
||||
ctx.catalog.transform
|
||||
ctx.command.transform
|
||||
ctx.integration.transform
|
||||
ctx.reference.transform
|
||||
ctx.skill.transform
|
||||
```
|
||||
|
||||
## Runtime Hooks
|
||||
|
||||
Runtime hooks intercept live operations rather than rebuilding domain state:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.aisdk.hook(
|
||||
"sdk",
|
||||
Effect.fn(function* (event) {
|
||||
if (event.package !== "@ai-sdk/xai") return
|
||||
const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
|
||||
event.sdk = mod.createXai(event.options)
|
||||
}),
|
||||
)
|
||||
|
||||
yield *
|
||||
ctx.aisdk.hook("language", (event) => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.api.id)
|
||||
})
|
||||
```
|
||||
|
||||
Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
|
||||
|
||||
Session context is mutable immediately before provider dispatch:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
ctx.session.hook("context", (event) =>
|
||||
Effect.sync(() => {
|
||||
event.tools.read.description = "Read a file using narrow line ranges."
|
||||
delete event.tools.write
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Reloading A Domain
|
||||
|
||||
When data captured by a transform changes, reload the affected domain:
|
||||
|
||||
```ts
|
||||
let data = yield * loadCatalog()
|
||||
|
||||
yield *
|
||||
ctx.catalog.transform((catalog) => {
|
||||
applyCatalog(data, catalog)
|
||||
})
|
||||
|
||||
data = yield * loadCatalog()
|
||||
yield * ctx.catalog.reload()
|
||||
```
|
||||
|
||||
Reload belongs to the domain, not an individual registration. `ctx.catalog.reload()` reruns every active catalog transform and publishes the rebuilt catalog.
|
||||
|
||||
Available reload operations are:
|
||||
|
||||
```ts
|
||||
ctx.agent.reload()
|
||||
ctx.catalog.reload()
|
||||
ctx.command.reload()
|
||||
ctx.integration.reload()
|
||||
ctx.reference.reload()
|
||||
ctx.skill.reload()
|
||||
```
|
||||
17
packages/plugin/src/effect/agent.ts
Normal file
17
packages/plugin/src/effect/agent.ts
Normal file
|
|
@ -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<Agent.Info>[]
|
||||
get(id: string): Types.DeepMutable<Agent.Info> | undefined
|
||||
default(id: string | undefined): void
|
||||
update(id: string, update: (agent: Types.DeepMutable<Agent.Info>) => void): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
export interface AgentDomain extends AgentApi<unknown> {
|
||||
readonly transform: Transform<AgentDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
22
packages/plugin/src/effect/aisdk.ts
Normal file
22
packages/plugin/src/effect/aisdk.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface AISDKHooks {
|
||||
sdk: {
|
||||
readonly model: Model.Info
|
||||
readonly package: string
|
||||
readonly options: Record<string, any>
|
||||
sdk?: any
|
||||
}
|
||||
language: {
|
||||
readonly model: Model.Info
|
||||
readonly sdk: any
|
||||
readonly options: Record<string, any>
|
||||
language?: LanguageModelV3
|
||||
}
|
||||
}
|
||||
|
||||
export interface AISDKDomain {
|
||||
readonly hook: Hooks<AISDKHooks>
|
||||
}
|
||||
33
packages/plugin/src/effect/catalog.ts
Normal file
33
packages/plugin/src/effect/catalog.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { CatalogApi } from "@opencode-ai/client/effect/api"
|
||||
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"
|
||||
|
||||
export interface CatalogProviderRecord {
|
||||
readonly provider: Types.DeepMutable<Provider.Info>
|
||||
readonly models: ReadonlyMap<string, Types.DeepMutable<Model.Info>>
|
||||
}
|
||||
|
||||
export interface CatalogDraft {
|
||||
readonly provider: {
|
||||
list(): readonly CatalogProviderRecord[]
|
||||
get(providerID: string): CatalogProviderRecord | undefined
|
||||
update(providerID: string, update: (provider: Types.DeepMutable<Provider.Info>) => void): void
|
||||
remove(providerID: string): void
|
||||
}
|
||||
readonly model: {
|
||||
get(providerID: string, modelID: string): Types.DeepMutable<Model.Info> | undefined
|
||||
update(providerID: string, modelID: string, update: (model: Types.DeepMutable<Model.Info>) => 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<unknown> {
|
||||
readonly transform: Transform<CatalogDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
16
packages/plugin/src/effect/command.ts
Normal file
16
packages/plugin/src/effect/command.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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"
|
||||
|
||||
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<unknown> {
|
||||
readonly transform: Transform<CommandDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
3
packages/plugin/src/effect/event.ts
Normal file
3
packages/plugin/src/effect/event.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import type { EventApi } from "@opencode-ai/client/effect/api"
|
||||
|
||||
export interface EventDomain extends Pick<EventApi<unknown>, "subscribe"> {}
|
||||
12
packages/plugin/src/effect/index.ts
Normal file
12
packages/plugin/src/effect/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export * as Plugin from "./plugin.js"
|
||||
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Connection } from "@opencode-ai/schema/connection"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Provider } from "@opencode-ai/schema/provider"
|
||||
export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
export { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
72
packages/plugin/src/effect/integration.ts
Normal file
72
packages/plugin/src/effect/integration.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import type {
|
||||
ConnectionInfo,
|
||||
IntegrationCommandMethod,
|
||||
IntegrationEnvMethod,
|
||||
IntegrationKeyMethod,
|
||||
IntegrationMethod,
|
||||
IntegrationOAuthMethod,
|
||||
} 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<string, string>
|
||||
type IntegrationRef = { id: string; name: string }
|
||||
|
||||
export type IntegrationOAuthAuthorization = {
|
||||
readonly url: string
|
||||
readonly instructions: string
|
||||
readonly expiresAt?: number
|
||||
} & (
|
||||
| {
|
||||
readonly mode: "auto"
|
||||
readonly callback: Effect.Effect<Credential.OAuth, unknown>
|
||||
}
|
||||
| {
|
||||
readonly mode: "code"
|
||||
readonly callback: (code: string) => Effect.Effect<Credential.OAuth, unknown>
|
||||
}
|
||||
)
|
||||
export type IntegrationOAuthMethodRegistration = {
|
||||
readonly integrationID: string
|
||||
readonly method: IntegrationOAuthMethod
|
||||
readonly authorize: (inputs: IntegrationInputs) => Effect.Effect<IntegrationOAuthAuthorization, unknown, Scope.Scope>
|
||||
readonly refresh?: (credential: Credential.OAuth) => Effect.Effect<Credential.OAuth, unknown>
|
||||
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<IntegrationApi<unknown>, "wellknown"> {
|
||||
readonly transform: Transform<IntegrationDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
readonly connection: {
|
||||
readonly active: (integrationID: string) => Effect.Effect<ConnectionInfo | undefined>
|
||||
readonly resolve: (connection: ConnectionInfo) => Effect.Effect<Credential.Value | undefined, unknown>
|
||||
}
|
||||
}
|
||||
41
packages/plugin/src/effect/plugin.ts
Normal file
41
packages/plugin/src/effect/plugin.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { PluginApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect, Scope } from "effect"
|
||||
import type { PluginOptions } from "../options.js"
|
||||
import type { App } from "../app.js"
|
||||
import type { AgentDomain } from "./agent.js"
|
||||
import type { AISDKDomain } from "./aisdk.js"
|
||||
import type { CatalogDomain } from "./catalog.js"
|
||||
import type { CommandDomain } from "./command.js"
|
||||
import type { EventDomain } from "./event.js"
|
||||
import type { IntegrationDomain } from "./integration.js"
|
||||
import type { ReferenceDomain } from "./reference.js"
|
||||
import type { SessionDomain } from "./session.js"
|
||||
import type { SkillDomain } from "./skill.js"
|
||||
import type { ToolDomain } from "./tool.js"
|
||||
import type { WebSearchDomain } from "./websearch.js"
|
||||
|
||||
export interface Context {
|
||||
readonly app: App
|
||||
readonly options: PluginOptions
|
||||
readonly agent: AgentDomain
|
||||
readonly aisdk: AISDKDomain
|
||||
readonly catalog: CatalogDomain
|
||||
readonly command: CommandDomain
|
||||
readonly event: EventDomain
|
||||
readonly integration: IntegrationDomain
|
||||
readonly plugin: PluginApi<unknown>
|
||||
readonly reference: ReferenceDomain
|
||||
readonly session: SessionDomain
|
||||
readonly skill: SkillDomain
|
||||
readonly tool: ToolDomain
|
||||
readonly websearch: WebSearchDomain
|
||||
}
|
||||
|
||||
export interface Plugin<R = Scope.Scope> {
|
||||
readonly id: string
|
||||
readonly effect: (context: Context) => Effect.Effect<void, never, R>
|
||||
}
|
||||
|
||||
export function define<R = Scope.Scope>(plugin: Plugin<R>) {
|
||||
return plugin
|
||||
}
|
||||
15
packages/plugin/src/effect/reference.ts
Normal file
15
packages/plugin/src/effect/reference.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
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"
|
||||
|
||||
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<unknown> {
|
||||
readonly transform: Transform<ReferenceDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
12
packages/plugin/src/effect/registration.ts
Normal file
12
packages/plugin/src/effect/registration.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { Effect, Scope } from "effect"
|
||||
|
||||
export interface Registration {
|
||||
readonly dispose: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export type Hooks<Spec> = <Name extends keyof Spec>(
|
||||
name: Name,
|
||||
callback: (input: Spec[Name]) => Effect.Effect<void>,
|
||||
) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
|
||||
export type Transform<Input> = (callback: (input: Input) => void) => Effect.Effect<Registration, never, Scope.Scope>
|
||||
27
packages/plugin/src/effect/session.ts
Normal file
27
packages/plugin/src/effect/session.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { SessionApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Message, SystemPart } from "@opencode-ai/ai"
|
||||
import type { Agent } from "@opencode-ai/schema/agent"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { JsonSchema } from "effect"
|
||||
import type { Hooks } from "./registration.js"
|
||||
|
||||
export interface SessionContext {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
system: Array<SystemPart>
|
||||
messages: Array<Message>
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
}
|
||||
|
||||
export interface SessionHooks {
|
||||
readonly context: SessionContext
|
||||
}
|
||||
|
||||
export type SessionDomain = Pick<
|
||||
SessionApi<unknown>,
|
||||
"create" | "get" | "prompt" | "generate" | "command" | "synthetic" | "interrupt"
|
||||
> & {
|
||||
readonly hook: Hooks<SessionHooks>
|
||||
}
|
||||
14
packages/plugin/src/effect/skill.ts
Normal file
14
packages/plugin/src/effect/skill.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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: Skill.Source): void
|
||||
list(): readonly Skill.Source[]
|
||||
}
|
||||
|
||||
export interface SkillDomain extends SkillApi<unknown> {
|
||||
readonly transform: Transform<SkillDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
45
packages/plugin/src/effect/tool.ts
Normal file
45
packages/plugin/src/effect/tool.ts
Normal file
|
|
@ -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<any>,
|
||||
Output extends Tool.ValueSchema<any> | undefined,
|
||||
>(tool: Tool.Info<Input, Output>): 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<ToolDraft>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
}
|
||||
23
packages/plugin/src/effect/websearch.ts
Normal file
23
packages/plugin/src/effect/websearch.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import type { WebsearchApi } from "@opencode-ai/client/effect/api"
|
||||
import type { Effect } from "effect"
|
||||
import type { Transform } from "./registration.js"
|
||||
|
||||
export interface WebSearchDefinition {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly execute: (input: WebSearch.ProviderInput) => Effect.Effect<readonly WebSearch.Result[], unknown>
|
||||
}
|
||||
|
||||
export interface WebSearchDomain extends WebsearchApi<unknown> {
|
||||
readonly transform: Transform<WebSearchDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export interface WebSearchDraft {
|
||||
add(definition: WebSearchDefinition): void
|
||||
readonly default: {
|
||||
get(): string | undefined
|
||||
set(providerID: string): void
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue