Merge remote-tracking branch 'origin/v2' into search-integration

# Conflicts:
#	packages/client/src/promise/generated/client.ts
#	packages/client/src/promise/generated/types.ts
#	packages/client/test/promise.test.ts
#	packages/core/src/plugin/host.ts
#	packages/core/src/plugin/internal.ts
#	packages/core/src/plugin/promise.ts
#	packages/core/test/plugin/host.ts
#	packages/plugin/src/v2/effect/index.ts
#	packages/plugin/src/v2/effect/integration.ts
#	packages/plugin/src/v2/promise/index.ts
#	packages/plugin/src/v2/promise/integration.ts
#	packages/protocol/src/client.ts
#	packages/schema/src/index.ts
#	packages/sdk/js/src/v2/gen/sdk.gen.ts
This commit is contained in:
Shoubhit Dash 2026-07-08 15:09:39 +05:30
commit a6acc2397d
374 changed files with 15254 additions and 10009 deletions

View file

@ -13,6 +13,7 @@ import { Integration } from "@opencode-ai/core/integration"
import { Location } from "@opencode-ai/core/location"
import { Npm } from "@opencode-ai/core/npm"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
import { Reference } from "@opencode-ai/core/reference"
import { SkillV2 } from "@opencode-ai/core/skill"
@ -46,6 +47,7 @@ export const PluginTestLayer = AppNodeBuilder.build(
CommandV2.node,
Integration.node,
PluginRuntime.node,
PluginHooks.node,
Reference.node,
SkillV2.node,
ToolHooks.node,

View file

@ -1,7 +1,7 @@
import { define } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect"
export default define({
export default Plugin.define({
id: "config-effect-plugin",
effect: (ctx) =>
ctx.agent

View file

@ -1,6 +1,6 @@
import { define } from "@opencode-ai/plugin/v2/promise"
import { Plugin } from "@opencode-ai/plugin/v2"
export default define({
export default Plugin.define({
id: "config-promise-plugin",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {

View file

@ -1,7 +1,7 @@
import { define } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect"
export default define({
export default Plugin.define({
id: "failing-plugin",
effect: () => Effect.die("plugin failed"),
})

View file

@ -1,8 +1,8 @@
import { define } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Effect } from "effect"
export default define({
export default Plugin.define({
id: "variant-source",
effect: (ctx) =>
ctx.catalog

View file

@ -1,4 +1,5 @@
import type { IntegrationDefinition, IntegrationMethodRegistration, PluginContext } from "@opencode-ai/plugin/v2/effect"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { IntegrationDefinition, IntegrationMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
@ -13,9 +14,9 @@ import type {
} from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
type Overrides = Partial<Omit<PluginContext, "options">>
type Overrides = Partial<Omit<Plugin.Context, "options">>
export function host(overrides: Overrides = {}): PluginContext {
export function host(overrides: Overrides = {}): Plugin.Context {
return {
options: {},
agent: overrides.agent ?? {
@ -24,8 +25,7 @@ export function host(overrides: Overrides = {}): PluginContext {
reload: () => Effect.die("unused agent.reload"),
},
aisdk: overrides.aisdk ?? {
sdk: () => Effect.die("unused aisdk.sdk"),
language: () => Effect.die("unused aisdk.language"),
hook: () => Effect.die("unused aisdk.hook"),
},
catalog: overrides.catalog ?? {
provider: {
@ -50,11 +50,15 @@ export function host(overrides: Overrides = {}): PluginContext {
integration: overrides.integration ?? {
list: () => Effect.die("unused integration.list"),
get: () => Effect.die("unused integration.get"),
connectKey: () => Effect.die("unused integration.connectKey"),
connectOauth: () => Effect.die("unused integration.connectOauth"),
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
attemptComplete: () => Effect.die("unused integration.attemptComplete"),
attemptCancel: () => Effect.die("unused integration.attemptCancel"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
},
register: () => Effect.die("unused integration.register"),
transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"),
@ -78,10 +82,7 @@ export function host(overrides: Overrides = {}): PluginContext {
},
tool: overrides.tool ?? {
transform: () => Effect.die("unused tool.transform"),
execute: {
before: () => Effect.die("unused tool.execute.before"),
after: () => Effect.die("unused tool.execute.after"),
},
hook: () => Effect.die("unused tool.hook"),
},
session: overrides.session ?? {
create: () => Effect.die("unused session.create"),
@ -89,11 +90,14 @@ export function host(overrides: Overrides = {}): PluginContext {
prompt: () => Effect.die("unused session.prompt"),
command: () => Effect.die("unused session.command"),
interrupt: () => Effect.die("unused session.interrupt"),
// Plugins register session hooks during setup, so a bare host accepts the
// registration; the callback only runs when a test triggers the request pipeline.
hook: () => Effect.succeed({ dispose: Effect.void }),
},
}
}
export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
export function agentHost(agent: AgentV2.Interface): Plugin.Context["agent"] {
return {
list: () => Effect.die("unused agent.list"),
reload: agent.reload,
@ -118,7 +122,7 @@ export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
}
}
export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] {
export function catalogHost(catalog: Catalog.Interface): Plugin.Context["catalog"] {
return {
provider: {
list: () => Effect.die("unused catalog.provider.list"),
@ -190,15 +194,19 @@ export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"
}
}
export function integrationHost(integration: Integration.Interface): PluginContext["integration"] {
export function integrationHost(integration: Integration.Interface): Plugin.Context["integration"] {
return {
list: () => Effect.die("unused integration.list"),
get: () => Effect.die("unused integration.get"),
connectKey: () => Effect.die("unused integration.connectKey"),
connectOauth: () => Effect.die("unused integration.connectOauth"),
attemptStatus: () => Effect.die("unused integration.attemptStatus"),
attemptComplete: () => Effect.die("unused integration.attemptComplete"),
attemptCancel: () => Effect.die("unused integration.attemptCancel"),
connect: {
key: () => Effect.die("unused integration.connect.key"),
oauth: () => Effect.die("unused integration.connect.oauth"),
},
attempt: {
status: () => Effect.die("unused integration.attempt.status"),
complete: () => Effect.die("unused integration.attempt.complete"),
cancel: () => Effect.die("unused integration.attempt.cancel"),
},
reload: integration.reload,
connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)),

View file

@ -1,5 +1,6 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Layer } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
@ -51,23 +52,31 @@ describe("ModelsDevPlugin", () => {
temperature: true,
tool_call: true,
cost: {
input: 2.5,
output: 15,
input: Money.USDPerMillionTokens.make(2.5),
output: Money.USDPerMillionTokens.make(15),
tiers: [
{
tier: { type: "context", size: 272_000 },
input: 3,
output: 18,
cache_read: 0.25,
input: Money.USDPerMillionTokens.make(3),
output: Money.USDPerMillionTokens.make(18),
cache_read: Money.USDPerMillionTokens.make(0.25),
},
],
context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 },
context_over_200k: {
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(22.5),
cache_read: Money.USDPerMillionTokens.make(0.5),
},
},
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
experimental: {
modes: {
fast: {
cost: { input: 5, output: 30, cache_read: 0.5 },
cost: {
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(30),
cache_read: Money.USDPerMillionTokens.make(0.5),
},
provider: {
headers: { "x-mode": "fast" },
body: { service_tier: "priority" },
@ -107,18 +116,31 @@ describe("ModelsDevPlugin", () => {
variants: [],
})
expect(fast?.cost).toEqual([
{ input: 5, output: 30, cache: { read: 0.5, write: 0 } },
{
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(30),
cache: {
read: Money.USDPerMillionTokens.make(0.5),
write: Money.USDPerMillionTokens.zero,
},
},
{
tier: { type: "context", size: 272_000 },
input: 3,
output: 18,
cache: { read: 0.25, write: 0 },
input: Money.USDPerMillionTokens.make(3),
output: Money.USDPerMillionTokens.make(18),
cache: {
read: Money.USDPerMillionTokens.make(0.25),
write: Money.USDPerMillionTokens.zero,
},
},
{
tier: { type: "context", size: 200_000 },
input: 5,
output: 22.5,
cache: { read: 0.5, write: 0 },
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(22.5),
cache: {
read: Money.USDPerMillionTokens.make(0.5),
write: Money.USDPerMillionTokens.zero,
},
},
])
}),

View file

@ -5,7 +5,7 @@ import { Integration } from "@opencode-ai/core/integration"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
import { define } from "@opencode-ai/plugin/v2/promise"
import { Plugin } from "@opencode-ai/plugin/v2"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
@ -17,7 +17,7 @@ describe("fromPromise", () => {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const seen: string[] = []
const promisePlugin = define({
const promisePlugin = Plugin.define({
id: "promise-client-reads",
setup: async (ctx) => {
const results = await Promise.all([
@ -47,7 +47,7 @@ describe("fromPromise", () => {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const promisePlugin = define({
const promisePlugin = Plugin.define({
id: "promise-example",
setup: async (ctx) => {
expect(ctx.options.mode).toBe("strict")
@ -76,7 +76,7 @@ describe("fromPromise", () => {
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const promisePlugin = define({
const promisePlugin = Plugin.define({
id: "promise-dispose",
setup: async (ctx) => {
const registration = await ctx.agent.transform((draft) => {
@ -100,7 +100,7 @@ describe("fromPromise", () => {
const integrations = yield* Integration.Service
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const promisePlugin = define({
const promisePlugin = Plugin.define({
id: "promise-search",
setup: async (ctx) => {
await ctx.integration.register({

View file

@ -1,4 +1,5 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { Money } from "@opencode-ai/schema/money"
import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
@ -185,7 +186,16 @@ describe("OpenAIPlugin", () => {
draft.package = item.package
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => {
model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }]
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: {
read: Money.USDPerMillionTokens.make(0.1),
write: Money.USDPerMillionTokens.zero,
},
},
]
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5-pro"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})

View file

@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
@ -65,7 +66,16 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
)
}
const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
const cost = (input: number, output = 0) => [
{
input: Money.USDPerMillionTokens.make(input),
output: Money.USDPerMillionTokens.make(output),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
]
describe("OpencodePlugin", () => {
it.effect("registers account and service account methods", () =>

View file

@ -37,17 +37,19 @@ describe("SkillPlugin.Plugin", () => {
Effect.provide(NodeFileSystem.layer),
)
const skills = yield* skill.list()
const report = skills.find((item) => item.name === "report")
const report = skills.find((item) => item.id === "report")
expect(skills).toContainEqual(
expect.objectContaining({
name: "opencode",
id: "opencode",
name: "OpenCode",
description: expect.stringContaining("any question about OpenCode itself"),
}),
)
expect(skills).toContainEqual(
expect.objectContaining({
name: "report",
id: "report",
name: "Report",
description: expect.stringContaining("opencode issue"),
}),
)