Merge remote-tracking branch 'origin/v2' into search-integration
# Conflicts: # packages/client/test/promise.test.ts # packages/core/schema.json # packages/core/src/database/migration.gen.ts # packages/core/src/tool/websearch.ts # packages/sdk-next/src/index.ts # packages/sdk/js/src/v2/gen/types.gen.ts
This commit is contained in:
commit
7b8d8b8861
666 changed files with 46671 additions and 20220 deletions
|
|
@ -11,7 +11,7 @@ const opencode = yield * OpenCode.create()
|
|||
const session = yield * opencode.sessions.get({ sessionID })
|
||||
```
|
||||
|
||||
It also exports `Tool` for plugins that register tools with `ctx.tool.register(...)`. Embedded plugins run through the ordinary discovery flow and register tools into each Location's `ToolRegistry` through the normal `Tools.Service.register(...)` path. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
||||
It also exports `Tool` for plugins that add tools with `ctx.tool.transform(...)`. Embedded plugins run through the ordinary discovery flow and register tools into each Location's `ToolRegistry` through the normal `Tools.Service.register(...)` path. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
||||
|
||||
`sessions.events({ sessionID, after })` replays durable events after the optional aggregate sequence, then emits newly committed durable events. `sessions.interrupt(...)` targets execution owned by this host, and `sessions.message(...)` retrieves one projected Session message.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"effect": "catalog:"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,18 +2,27 @@ export * as OpenCode from "./opencode"
|
|||
export * as Tool from "./tool"
|
||||
|
||||
export { ClientError } from "@opencode-ai/client/effect"
|
||||
export {
|
||||
AbsolutePath,
|
||||
Agent,
|
||||
Integration,
|
||||
Location,
|
||||
Model,
|
||||
Prompt,
|
||||
Provider,
|
||||
RelativePath,
|
||||
Search,
|
||||
Session,
|
||||
SessionInput,
|
||||
SessionMessage,
|
||||
} from "@opencode-ai/client/effect"
|
||||
export type { OpenCodeEvent } from "@opencode-ai/client/effect"
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Location } from "@opencode-ai/schema/location"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
export { Permission } from "@opencode-ai/schema/permission"
|
||||
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||
export { Project } from "@opencode-ai/schema/project"
|
||||
export { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
||||
export { Prompt } from "@opencode-ai/schema/prompt"
|
||||
export { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
export { Provider } from "@opencode-ai/schema/provider"
|
||||
export { Pty } from "@opencode-ai/schema/pty"
|
||||
export { Question } from "@opencode-ai/schema/question"
|
||||
export { Reference } from "@opencode-ai/schema/reference"
|
||||
export { Search } from "@opencode-ai/schema/search"
|
||||
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
|
||||
export { Session } from "@opencode-ai/schema/session"
|
||||
export { SessionInput } from "@opencode-ai/schema/session-input"
|
||||
export { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
export { Skill } from "@opencode-ai/schema/skill"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { OpenCode } from "@opencode-ai/client/effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
|
|
@ -13,7 +14,7 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
|||
const memoMap = yield* Layer.makeMemoMap
|
||||
const sdkPlugins = SdkPlugins.makeStore()
|
||||
const context = yield* Layer.buildWithMemoMap(
|
||||
AppNodeBuilder.build(LayerNode.group([PermissionSaved.node, Project.node, SdkPlugins.node]), [
|
||||
AppNodeBuilder.build(LayerNode.group([EventV2.node, PermissionSaved.node, Project.node, SdkPlugins.node]), [
|
||||
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
|
||||
]),
|
||||
memoMap,
|
||||
|
|
|
|||
43
packages/sdk-next/test/contract-identity.test.ts
Normal file
43
packages/sdk-next/test/contract-identity.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Search } from "@opencode-ai/schema/search"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
|
||||
const SDK = await import("../src/index")
|
||||
|
||||
test("re-exports canonical contracts directly from Schema", () => {
|
||||
expect(SDK.Agent).toBe(Agent)
|
||||
expect(SDK.Model).toBe(Model)
|
||||
expect(SDK.Search).toBe(Search)
|
||||
expect(SDK.Session).toBe(Session)
|
||||
expect(Object.keys(SDK).sort()).toEqual([
|
||||
"AbsolutePath",
|
||||
"Agent",
|
||||
"ClientError",
|
||||
"Command",
|
||||
"Credential",
|
||||
"FileSystem",
|
||||
"Integration",
|
||||
"Location",
|
||||
"Model",
|
||||
"OpenCode",
|
||||
"Permission",
|
||||
"PermissionSaved",
|
||||
"Project",
|
||||
"ProjectCopy",
|
||||
"Prompt",
|
||||
"PromptInput",
|
||||
"Provider",
|
||||
"Pty",
|
||||
"Question",
|
||||
"Reference",
|
||||
"RelativePath",
|
||||
"Search",
|
||||
"Session",
|
||||
"SessionInput",
|
||||
"SessionMessage",
|
||||
"Skill",
|
||||
"Tool",
|
||||
])
|
||||
})
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { expect } from "bun:test"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Deferred, Effect, Latch, Layer, Option, Schema, Stream } from "effect"
|
||||
import { Deferred, Effect, Latch, Layer, Option, Ref, Schema, Stream } from "effect"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||
import type { OpenCodeEvent } from "../src"
|
||||
|
|
@ -26,6 +28,118 @@ const sessionID = (fixture: Fixture) => fixture.sdk.Session.ID.create()
|
|||
const location = (fixture: Fixture) =>
|
||||
fixture.sdk.Location.Ref.make({ directory: fixture.sdk.AbsolutePath.make(fixture.directory) })
|
||||
|
||||
it.live(
|
||||
"reloads every booted Location after SDK plugin registration",
|
||||
() =>
|
||||
withEmbedded("opencode-embedded-plugin-reload-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||
const booted = yield* Deferred.make<void>()
|
||||
const activated = yield* Deferred.make<boolean>()
|
||||
const bootCount = yield* Ref.make(0)
|
||||
const activationCount = yield* Ref.make(0)
|
||||
const secondDirectory = path.join(fixture.directory, "second")
|
||||
yield* Effect.promise(() => fs.mkdir(secondDirectory))
|
||||
const refs = [
|
||||
location(fixture),
|
||||
fixture.sdk.Location.Ref.make({ directory: fixture.sdk.AbsolutePath.make(secondDirectory) }),
|
||||
]
|
||||
const bootstrapID = `bootstrap-sdk-${crypto.randomUUID()}`
|
||||
const id = `late-sdk-${crypto.randomUUID()}`
|
||||
|
||||
yield* opencode.plugin({
|
||||
id: bootstrapID,
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.tool
|
||||
.transform((draft) =>
|
||||
draft.add(
|
||||
"bootstrap_sdk_tool",
|
||||
fixture.sdk.Tool.make({
|
||||
description: "Marks the initial Location plugin generation",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Void,
|
||||
execute: () => Effect.void,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
if (yield* Ref.updateAndGet(bootCount, (count) => count + 1).pipe(Effect.map((count) => count === 2))) {
|
||||
yield* Deferred.succeed(booted, undefined)
|
||||
}
|
||||
}),
|
||||
})
|
||||
yield* Effect.all(
|
||||
refs.map((ref) => opencode.plugin.list({ location: ref })),
|
||||
{ discard: true },
|
||||
)
|
||||
yield* Deferred.await(booted).pipe(Effect.timeout("4 seconds"))
|
||||
yield* opencode.plugin({
|
||||
id,
|
||||
effect: (ctx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* ctx.tool
|
||||
.transform((draft) =>
|
||||
draft.add(
|
||||
"late_sdk_tool",
|
||||
fixture.sdk.Tool.make({
|
||||
description: "Tool registered after Location boot",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Void,
|
||||
execute: () => Effect.void,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
if (
|
||||
yield* Ref.updateAndGet(activationCount, (count) => count + 1).pipe(Effect.map((count) => count === 2))
|
||||
) {
|
||||
yield* Deferred.succeed(activated, true)
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
expect(yield* Deferred.await(activated).pipe(Effect.timeout("10 seconds"))).toBe(true)
|
||||
}),
|
||||
),
|
||||
25_000,
|
||||
)
|
||||
|
||||
it.live(
|
||||
"keeps SDK plugin registration isolated between embedded hosts",
|
||||
() =>
|
||||
withEmbedded("opencode-embedded-plugin-isolation-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const first = yield* fixture.sdk.OpenCode.create()
|
||||
const second = yield* fixture.sdk.OpenCode.create()
|
||||
const firstReady = yield* Deferred.make<void>()
|
||||
const secondReady = yield* Deferred.make<void>()
|
||||
const activated = yield* Deferred.make<void>()
|
||||
const ref = location(fixture)
|
||||
const id = `isolated-sdk-${crypto.randomUUID()}`
|
||||
|
||||
yield* first.plugin({
|
||||
id: `first-ready-${crypto.randomUUID()}`,
|
||||
effect: () => Deferred.succeed(firstReady, undefined),
|
||||
})
|
||||
yield* second.plugin({
|
||||
id: `second-ready-${crypto.randomUUID()}`,
|
||||
effect: () => Deferred.succeed(secondReady, undefined),
|
||||
})
|
||||
yield* Effect.all([first.plugin.list({ location: ref }), second.plugin.list({ location: ref })], {
|
||||
discard: true,
|
||||
})
|
||||
yield* Effect.all([Deferred.await(firstReady), Deferred.await(secondReady)], { discard: true })
|
||||
|
||||
yield* first.plugin({ id, effect: () => Deferred.succeed(activated, undefined) })
|
||||
yield* Deferred.await(activated).pipe(Effect.timeout("5 seconds"))
|
||||
|
||||
expect((yield* second.plugin.list({ location: ref })).data.map((plugin) => String(plugin.id))).not.toContain(id)
|
||||
}),
|
||||
),
|
||||
15_000,
|
||||
)
|
||||
|
||||
it.live(
|
||||
"embedded client uses the real router and handlers",
|
||||
() =>
|
||||
|
|
@ -42,14 +156,17 @@ it.live(
|
|||
id: `embedded-tools-${crypto.randomUUID()}`,
|
||||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
.register({
|
||||
embedded_tool: fixture.sdk.Tool.make({
|
||||
description: "Embedded test tool",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
execute: () => Effect.succeed({ ok: true }),
|
||||
}),
|
||||
})
|
||||
.transform((draft) =>
|
||||
draft.add(
|
||||
"embedded_tool",
|
||||
fixture.sdk.Tool.make({
|
||||
description: "Embedded test tool",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
execute: () => Effect.succeed({ ok: true }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie),
|
||||
})
|
||||
|
||||
|
|
@ -64,18 +181,18 @@ it.live(
|
|||
const active = yield* opencode.sessions.active()
|
||||
const admitted = yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Do not run" }),
|
||||
prompt: fixture.sdk.PromptInput.Prompt.make({ text: "Do not run" }),
|
||||
resume: false,
|
||||
})
|
||||
const context = yield* opencode.sessions.context({ sessionID: id })
|
||||
yield* opencode.sessions.putContextEntry({ sessionID: id, key: "deploy-target", value: "production" })
|
||||
yield* opencode.sessions.putContextEntry({ sessionID: id, key: "flags", value: { beta: true } })
|
||||
const contextEntries = yield* opencode.sessions.listContextEntries({ sessionID: id })
|
||||
yield* opencode.sessions.removeContextEntry({ sessionID: id, key: "flags" })
|
||||
const remainingContextEntries = yield* opencode.sessions.listContextEntries({ sessionID: id })
|
||||
yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "deploy-target", value: "production" })
|
||||
yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "flags", value: { beta: true } })
|
||||
const contextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
||||
yield* opencode.sessions.instructions.entry.remove({ sessionID: id, key: "flags" })
|
||||
const remainingContextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id })
|
||||
const wake = yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
|
||||
prompt: fixture.sdk.PromptInput.Prompt.make({ text: "Promote this input" }),
|
||||
})
|
||||
const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe(
|
||||
Stream.filter((event) => event.type === "session.prompt.promoted" && event.data.inputID === wake.id),
|
||||
|
|
@ -102,7 +219,7 @@ it.live(
|
|||
opencode.sessions.log({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
||||
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||
opencode.sessions.listContextEntries({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.instructions.entry.list({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
|
@ -117,7 +234,7 @@ it.live(
|
|||
expect(selected.model?.id).toBe(model.id)
|
||||
expect(selected.model?.providerID).toBe(model.providerID)
|
||||
expect(page.data.some((session) => session.id === id)).toBe(true)
|
||||
expect(active).toEqual({ data: {}, watermarks: {} })
|
||||
expect(active).toEqual({})
|
||||
expect(admitted.sessionID).toBe(id)
|
||||
expect(prompted.type).toBe("session.prompt.promoted")
|
||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||
|
|
@ -199,7 +316,7 @@ it.live(
|
|||
yield* opencode.sessions.create({ id, location: location(fixture) })
|
||||
yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Observe this input" }),
|
||||
prompt: fixture.sdk.PromptInput.Prompt.make({ text: "Observe this input" }),
|
||||
})
|
||||
|
||||
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue