refactor(core): consolidate tool architecture
This commit is contained in:
parent
0fd73a2976
commit
8db7487c89
466 changed files with 9405 additions and 11071 deletions
6
packages/plugin/AGENTS.md
Normal file
6
packages/plugin/AGENTS.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
}),
|
||||
},
|
||||
)
|
||||
})
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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({
|
||||
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>
|
||||
}
|
||||
|
|
@ -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<string, CatalogModel>
|
||||
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: ProviderV2Info) => void): void
|
||||
update(providerID: string, update: (provider: Types.DeepMutable<Provider.Info>) => 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<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
|
||||
|
|
@ -30,11 +28,6 @@ export interface CatalogDraft {
|
|||
}
|
||||
|
||||
export interface CatalogDomain extends CatalogApi<unknown> {
|
||||
readonly model: CatalogApi<unknown>["model"] & {
|
||||
readonly get: (providerID: string, modelID: string) => Effect.Effect<ModelGetOutput | undefined>
|
||||
}
|
||||
readonly transform: Transform<CatalogDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
type ModelGetOutput = Effect.Success<ReturnType<CatalogApi<unknown>["model"]["list"]>>["data"][number]
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
@ -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<string, string>
|
||||
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<CredentialOAuth, unknown>
|
||||
readonly callback: Effect.Effect<Credential.OAuth, unknown>
|
||||
}
|
||||
| {
|
||||
readonly mode: "code"
|
||||
readonly callback: (code: string) => Effect.Effect<CredentialOAuth, unknown>
|
||||
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: CredentialOAuth) => Effect.Effect<CredentialOAuth, unknown>
|
||||
readonly label?: (credential: CredentialOAuth) => string | undefined
|
||||
readonly refresh?: (credential: Credential.OAuth) => Effect.Effect<Credential.OAuth, unknown>
|
||||
readonly label?: (credential: Credential.OAuth) => string | undefined
|
||||
}
|
||||
export type IntegrationMethodRegistration =
|
||||
| IntegrationOAuthMethodRegistration
|
||||
|
|
@ -67,6 +67,6 @@ export interface IntegrationDomain extends Omit<IntegrationApi<unknown>, "wellkn
|
|||
readonly reload: () => Effect.Effect<void>
|
||||
readonly connection: {
|
||||
readonly active: (integrationID: string) => Effect.Effect<ConnectionInfo | undefined>
|
||||
readonly resolve: (connection: ConnectionInfo) => Effect.Effect<CredentialValue | undefined, unknown>
|
||||
readonly resolve: (connection: ConnectionInfo) => Effect.Effect<Credential.Value | undefined, unknown>
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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<unknown> {
|
||||
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>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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}!`
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
17
packages/plugin/src/promise/agent.ts
Normal file
17
packages/plugin/src/promise/agent.ts
Normal file
|
|
@ -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<Agent.Info>[]
|
||||
get(id: string): DeepMutable<Agent.Info> | undefined
|
||||
default(id: string | undefined): void
|
||||
update(id: string, update: (agent: DeepMutable<Agent.Info>) => void): void
|
||||
remove(id: string): void
|
||||
}
|
||||
|
||||
export interface AgentDomain extends AgentApi {
|
||||
readonly transform: Transform<AgentDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
33
packages/plugin/src/promise/catalog.ts
Normal file
33
packages/plugin/src/promise/catalog.ts
Normal file
|
|
@ -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<Provider.Info>
|
||||
readonly models: ReadonlyMap<string, DeepMutable<Model.Info>>
|
||||
}
|
||||
|
||||
export interface CatalogDraft {
|
||||
readonly provider: {
|
||||
list(): readonly CatalogProviderRecord[]
|
||||
get(providerID: string): CatalogProviderRecord | undefined
|
||||
update(providerID: string, update: (provider: DeepMutable<Provider.Info>) => void): void
|
||||
remove(providerID: string): void
|
||||
}
|
||||
readonly model: {
|
||||
get(providerID: string, modelID: string): DeepMutable<Model.Info> | undefined
|
||||
update(providerID: string, modelID: string, update: (model: 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 {
|
||||
readonly transform: Transform<CatalogDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
15
packages/plugin/src/promise/command.ts
Normal file
15
packages/plugin/src/promise/command.ts
Normal file
|
|
@ -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<CommandDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
64
packages/plugin/src/promise/integration.ts
Normal file
64
packages/plugin/src/promise/integration.ts
Normal file
|
|
@ -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<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: Promise<Credential.OAuth>
|
||||
}
|
||||
| {
|
||||
readonly mode: "code"
|
||||
readonly callback: (code: string) => Promise<Credential.OAuth>
|
||||
}
|
||||
)
|
||||
|
||||
export type IntegrationOAuthMethodRegistration = {
|
||||
readonly integrationID: string
|
||||
readonly method: IntegrationOAuthMethod
|
||||
readonly authorize: (inputs: IntegrationInputs) => Promise<IntegrationOAuthAuthorization>
|
||||
readonly refresh?: (credential: Credential.OAuth) => Promise<Credential.OAuth>
|
||||
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, "wellknown"> {
|
||||
readonly transform: Transform<IntegrationDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
readonly connection: {
|
||||
readonly active: (integrationID: string) => Promise<ConnectionInfo | undefined>
|
||||
readonly resolve: (connection: ConnectionInfo) => Promise<Credential.Value | undefined>
|
||||
}
|
||||
}
|
||||
14
packages/plugin/src/promise/reference.ts
Normal file
14
packages/plugin/src/promise/reference.ts
Normal file
|
|
@ -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<ReferenceDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
|
|
@ -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<SkillDraft>
|
||||
62
packages/plugin/src/promise/tool.ts
Normal file
62
packages/plugin/src/promise/tool.ts
Normal file
|
|
@ -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<Tool.Context, "progress"> {
|
||||
readonly progress: (update: Tool.Metadata) => Promise<void>
|
||||
}
|
||||
|
||||
export type Info<
|
||||
Input extends Tool.ValueSchema<any> = Tool.ValueSchema<any>,
|
||||
Output extends Tool.ValueSchema<any> | undefined = Tool.ValueSchema<any> | undefined,
|
||||
> = Omit<Tool.Info<Input, Output>, "execute"> & {
|
||||
readonly execute: (
|
||||
input: Parameters<Tool.Info<Input, Output>["execute"]>[0],
|
||||
context: ToolContext,
|
||||
) => Promise<Tool.Result<Output>>
|
||||
}
|
||||
|
||||
interface ToolDraft {
|
||||
add<
|
||||
Input extends Tool.ValueSchema<any>,
|
||||
Output extends Tool.ValueSchema<any> | undefined,
|
||||
>(tool: Info<Input, Output>): 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<ToolDraft>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
}
|
||||
9
packages/plugin/src/promise/types.ts
Normal file
9
packages/plugin/src/promise/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type DeepMutable<A> = A extends (...args: never[]) => unknown
|
||||
? A
|
||||
: A extends ReadonlyMap<infer K, infer V>
|
||||
? Map<DeepMutable<K>, DeepMutable<V>>
|
||||
: A extends ReadonlyArray<infer I>
|
||||
? DeepMutable<I>[]
|
||||
: A extends object
|
||||
? { -readonly [K in keyof A]: DeepMutable<A[K]> }
|
||||
: A
|
||||
|
|
@ -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<void>
|
||||
invalidate(sessionID: string): void
|
||||
}
|
||||
|
|
@ -90,7 +90,7 @@ export interface Data {
|
|||
readonly resource: LocationCollection<McpResource>
|
||||
}
|
||||
readonly model: LocationCollection<ModelInfo>
|
||||
readonly provider: LocationCollection<ProviderV2Info>
|
||||
readonly provider: LocationCollection<ProviderInfo>
|
||||
readonly reference: LocationCollection<ReferenceInfo>
|
||||
readonly skill: LocationCollection<SkillInfo>
|
||||
}
|
||||
|
|
@ -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<unknown> {
|
||||
readonly get: (id: string) => Effect.Effect<AgentGetOutput | undefined>
|
||||
readonly transform: Transform<AgentDraft>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
type AgentGetOutput = Effect.Success<ReturnType<AgentApi<unknown>["list"]>>["data"][number]
|
||||
|
|
@ -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<FileSystemEntry[]>
|
||||
find(input: {
|
||||
readonly query: string
|
||||
readonly type?: "file" | "directory"
|
||||
readonly limit?: number
|
||||
}): Effect.Effect<FileSystemEntry[]>
|
||||
glob(input: {
|
||||
readonly pattern: string
|
||||
readonly path?: string
|
||||
readonly limit?: number
|
||||
}): Effect.Effect<readonly FileSystemEntry[]>
|
||||
}
|
||||
|
|
@ -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<Record<string, JsonValue>>
|
||||
|
||||
export interface Context {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly messageID: SessionMessage.ID
|
||||
readonly callID: string
|
||||
readonly progress: (update: Progress) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
/** Live replacement metadata for a running tool. */
|
||||
export type Progress = Metadata
|
||||
|
||||
export type StandardSchemaType<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> &
|
||||
StandardJSONSchemaV1<Input, Output>
|
||||
export type SchemaType<A = unknown> = Schema.Codec<A, any> | StandardSchemaType<any, A> | JsonSchema.JsonSchema
|
||||
type IsAny<A> = 0 extends 1 & A ? true : false
|
||||
export type InputValue<S> =
|
||||
IsAny<S> extends true
|
||||
? any
|
||||
: S extends Schema.Codec<infer A, any>
|
||||
? A
|
||||
: S extends StandardSchemaV1<any, infer A>
|
||||
? A
|
||||
: unknown
|
||||
export type OutputValue<S> =
|
||||
IsAny<S> extends true
|
||||
? any
|
||||
: S extends Schema.Codec<infer A, any>
|
||||
? A
|
||||
: S extends StandardSchemaV1<infer A, any>
|
||||
? A
|
||||
: unknown
|
||||
export type EncodedValue<S> =
|
||||
IsAny<S> extends true
|
||||
? any
|
||||
: S extends Schema.Codec<any, infer A>
|
||||
? A
|
||||
: S extends StandardSchemaV1<any, infer A>
|
||||
? A
|
||||
: unknown
|
||||
|
||||
type ToolDefinition = {
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly inputSchema: JsonSchema.JsonSchema
|
||||
readonly outputSchema?: JsonSchema.JsonSchema
|
||||
}
|
||||
|
||||
export class Failure extends Schema.TaggedErrorClass<Failure>()("LLM.ToolFailure", {
|
||||
message: Schema.String,
|
||||
error: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
|
||||
export class RegistrationError extends Schema.TaggedErrorClass<RegistrationError>()("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<Input extends SchemaType<any>> = {
|
||||
readonly description: string
|
||||
readonly input: Input
|
||||
}
|
||||
|
||||
export type Response<Output extends SchemaType<any>> = {
|
||||
readonly output: OutputValue<Output>
|
||||
readonly content?: ModelOutput
|
||||
readonly metadata?: Metadata
|
||||
}
|
||||
|
||||
export type ContentResponse = {
|
||||
readonly content: ModelOutput
|
||||
readonly metadata?: Metadata
|
||||
}
|
||||
|
||||
export type Tool<
|
||||
Input extends SchemaType<any>,
|
||||
Output extends SchemaType<any> | undefined = undefined,
|
||||
> = BaseTool<Input> &
|
||||
(Output extends SchemaType<any>
|
||||
? {
|
||||
readonly output: Output
|
||||
readonly execute: (input: InputValue<Input>, context: Context) => Effect.Effect<Response<Output>, Failure>
|
||||
}
|
||||
: {
|
||||
readonly output?: undefined
|
||||
readonly execute: (input: InputValue<Input>, context: Context) => Effect.Effect<ContentResponse, Failure>
|
||||
})
|
||||
|
||||
export type Any = BaseTool<any> & {
|
||||
readonly output?: SchemaType<any>
|
||||
readonly execute: (input: any, context: Context) => Effect.Effect<Response<any> | ContentResponse, Failure>
|
||||
}
|
||||
|
||||
export function make<Input extends SchemaType<any>, Output extends SchemaType<any>>(
|
||||
config: Tool<Input, Output>,
|
||||
): Tool<Input, Output>
|
||||
export function make<Input extends SchemaType<any>>(config: Tool<Input>): Tool<Input>
|
||||
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<Record<string, Any>>,
|
||||
options?: RegisterOptions,
|
||||
): Array<Registration & { readonly key: string }> =>
|
||||
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<any>, value: unknown): Effect.Effect<any, Failure> {
|
||||
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<any>, value: unknown): Effect.Effect<any, Failure> {
|
||||
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<any>): schema is StandardSchemaType {
|
||||
return "~standard" in schema
|
||||
}
|
||||
|
||||
function validateStandard(schema: StandardSchemaType, value: unknown, prefix: string): Effect.Effect<unknown, Failure> {
|
||||
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<any>): 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<any>): 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<A> = { -readonly [K in keyof A]: A[K] }
|
||||
type HookOutcome<A extends { readonly status: string }> = Omit<Mutable<A>, "status"> & Pick<A, "status">
|
||||
|
||||
/** The bounded terminal outcome exposed to tool hooks. */
|
||||
export type Outcome = typeof ExecuteAfterOutcome.Type extends infer A
|
||||
? A extends { readonly status: string }
|
||||
? HookOutcome<A>
|
||||
: 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<ToolDraft>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export interface Location {
|
||||
readonly directory: string
|
||||
readonly project: {
|
||||
readonly directory: string
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import type { Effect } from "effect"
|
||||
|
||||
export interface Npm {
|
||||
add(pkg: string): Effect.Effect<
|
||||
{
|
||||
readonly directory: string
|
||||
readonly entrypoint?: string
|
||||
},
|
||||
unknown
|
||||
>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * as Tool from "./internal/tool.js"
|
||||
export * from "./internal/tool.js"
|
||||
|
|
@ -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<AgentGetOutput | undefined>
|
||||
readonly transform: Transform<AgentDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
|
||||
type AgentGetOutput = Awaited<ReturnType<AgentApi["list"]>>["data"][number]
|
||||
|
|
@ -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<ModelGetOutput | undefined>
|
||||
}
|
||||
readonly transform: Transform<CatalogDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
|
||||
type ModelGetOutput = Awaited<ReturnType<CatalogApi["model"]["list"]>>["data"][number]
|
||||
|
|
@ -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<CommandDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
|
|
@ -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<CredentialOAuth>
|
||||
}
|
||||
| {
|
||||
readonly mode: "code"
|
||||
readonly callback: (code: string) => Promise<CredentialOAuth>
|
||||
}
|
||||
)
|
||||
|
||||
export interface IntegrationDomain extends Omit<IntegrationApi, "wellknown"> {
|
||||
readonly transform: Transform<IntegrationDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
readonly connection: {
|
||||
readonly active: (integrationID: string) => Promise<import("@opencode-ai/sdk/v2/types").ConnectionInfo | undefined>
|
||||
readonly resolve: (
|
||||
connection: import("@opencode-ai/sdk/v2/types").ConnectionInfo,
|
||||
) => Promise<CredentialValue | undefined>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import type { Hooks, Transform } from "../registration.js"
|
||||
|
||||
export type Context = Omit<import("../../effect/internal/tool.js").Context, "progress"> & {
|
||||
readonly progress: (update: import("../../effect/internal/tool.js").Progress) => Promise<void>
|
||||
}
|
||||
export type SchemaType<A> = import("../../effect/internal/tool.js").SchemaType<A>
|
||||
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<Input extends SchemaType<any>, Output extends SchemaType<any> | undefined = undefined> = Omit<
|
||||
import("../../effect/internal/tool.js").Tool<Input, Output>,
|
||||
"execute"
|
||||
> & {
|
||||
readonly execute: (
|
||||
input: import("../../effect/internal/tool.js").InputValue<Input>,
|
||||
context: Context,
|
||||
) => Promise<
|
||||
Output extends SchemaType<any>
|
||||
? import("../../effect/internal/tool.js").Response<Output>
|
||||
: import("../../effect/internal/tool.js").ContentResponse
|
||||
>
|
||||
}
|
||||
|
||||
export type Any = Omit<import("../../effect/internal/tool.js").Any, "execute"> & {
|
||||
readonly execute: (
|
||||
input: any,
|
||||
context: Context,
|
||||
) => Promise<
|
||||
import("../../effect/internal/tool.js").Response<any> | import("../../effect/internal/tool.js").ContentResponse
|
||||
>
|
||||
}
|
||||
|
||||
export function make<Input extends SchemaType<any>, Output extends SchemaType<any>>(
|
||||
tool: Tool<Input, Output>,
|
||||
): Tool<Input, Output>
|
||||
export function make<Input extends SchemaType<any>>(tool: Tool<Input>): Tool<Input>
|
||||
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<Input extends SchemaType<any>, Output extends SchemaType<any>>(
|
||||
name: string,
|
||||
tool: Tool<Input, Output>,
|
||||
options?: RegisterOptions,
|
||||
): void
|
||||
add<Input extends SchemaType<any>>(name: string, tool: Tool<Input>, options?: RegisterOptions): void
|
||||
}
|
||||
|
||||
export interface ToolHooks {
|
||||
readonly "execute.before": ToolExecuteBeforeEvent
|
||||
readonly "execute.after": ToolExecuteAfterEvent
|
||||
}
|
||||
|
||||
export interface ToolDomain {
|
||||
readonly transform: Transform<ToolDraft>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
}
|
||||
|
|
@ -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<ReferenceDraft>
|
||||
readonly reload: () => Promise<void>
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * as Tool from "./internal/tool.js"
|
||||
export * from "./internal/tool.js"
|
||||
|
|
@ -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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "../src/v2/effect/tool"
|
||||
|
||||
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`)
|
||||
const config = {
|
||||
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)
|
||||
|
||||
expect(Tool.toLLMDefinition("foreign", tool)).toEqual({
|
||||
name: "foreign",
|
||||
description: "Foreign tool",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
required: ["value"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { ok: { type: "boolean" } },
|
||||
required: ["ok"],
|
||||
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 }> = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "test",
|
||||
validate: (value) => {
|
||||
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)
|
||||
return Number.isFinite(count) ? { value: { count } } : { issues: [{ message: "count must be numeric" }] }
|
||||
},
|
||||
jsonSchema: {
|
||||
input: () => ({ type: "object", properties: { count: { type: "string" } } }),
|
||||
output: () => ({ type: "object", properties: { count: { type: "number" } } }),
|
||||
},
|
||||
},
|
||||
}
|
||||
const output: Tool.StandardSchemaType<number, string> = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "test",
|
||||
validate: (value) => ({ value: String(value) }),
|
||||
jsonSchema: {
|
||||
input: () => ({ type: "number" }),
|
||||
output: () => ({ type: "string" }),
|
||||
},
|
||||
},
|
||||
}
|
||||
const tool = Tool.make({
|
||||
description: "Portable tool",
|
||||
input,
|
||||
output,
|
||||
execute: ({ count }) => Effect.succeed({ output: count + 1 }),
|
||||
})
|
||||
|
||||
expect(Tool.toLLMDefinition("portable", 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")
|
||||
})
|
||||
|
||||
test("portable schema failures become tool failures", async () => {
|
||||
const input: Tool.StandardSchemaType<string> = {
|
||||
"~standard": {
|
||||
version: 1,
|
||||
vendor: "test",
|
||||
validate: () => ({ issues: [{ message: "expected a string" }] }),
|
||||
jsonSchema: {
|
||||
input: () => ({ type: "string" }),
|
||||
output: () => ({ type: "string" }),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const error = await Effect.runPromiseExit(Tool.decodeInput(input, 1))
|
||||
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({
|
||||
description: "Annotated tool",
|
||||
input,
|
||||
output,
|
||||
execute: ({ value }) => Effect.succeed({ output: { value, internal: true }, metadata: { value }, content: value }),
|
||||
})
|
||||
|
||||
expect(await Effect.runPromise(tool.execute({ value: "out" }, {} as Tool.Context))).toEqual({
|
||||
output: { value: "out", internal: true },
|
||||
metadata: { value: "out" },
|
||||
content: "out",
|
||||
})
|
||||
})
|
||||
|
||||
test("raw JSON schemas are render-only and omitted output means model-only", async () => {
|
||||
const tool = Tool.make({
|
||||
description: "Raw tool",
|
||||
input: { type: "object", properties: { value: { type: "string" } } },
|
||||
execute: (input) => Effect.succeed({ content: JSON.stringify(input) }),
|
||||
})
|
||||
|
||||
expect(Tool.toLLMDefinition("raw", 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 })
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue