From dc99c600b671f77d0e273c1610c29f26461fc240 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 6 Jul 2026 15:45:46 -0400 Subject: [PATCH] feat(v2): expose canonical schema contracts (#35595) --- bun.lock | 1 + .../client/test/contract-identity.test.ts | 8 ++++ packages/core/src/session/store.ts | 10 ++--- packages/core/src/tool/hooks.ts | 22 ++++++---- packages/plugin/package.json | 1 + packages/plugin/src/v2/effect/agent.ts | 2 +- packages/plugin/src/v2/effect/aisdk.ts | 6 +-- packages/plugin/src/v2/effect/index.ts | 10 +++++ packages/plugin/src/v2/promise/aisdk.ts | 6 +-- packages/plugin/src/v2/promise/index.ts | 10 +++++ .../plugin/test/contract-identity.test.ts | 41 +++++++++++++++++++ packages/schema/AGENTS.md | 7 ++++ packages/sdk-next/package.json | 1 + packages/sdk-next/src/index.ts | 35 ++++++++++------ .../sdk-next/test/contract-identity.test.ts | 40 ++++++++++++++++++ 15 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 packages/plugin/test/contract-identity.test.ts create mode 100644 packages/sdk-next/test/contract-identity.test.ts diff --git a/bun.lock b/bun.lock index c6862dc4bd..75f5ce7594 100644 --- a/bun.lock +++ b/bun.lock @@ -775,6 +775,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/server": "workspace:*", "effect": "catalog:", }, diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 90ac5d8cfc..a2afc9a002 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -21,6 +21,14 @@ import { Api } from "@opencode-ai/server/api" import { compile, emitPromise } from "@opencode-ai/httpapi-codegen" import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract" +const Client = await import("../src/effect") + +test("effect entrypoint exposes canonical Schema contracts", () => { + expect(Client.Agent).toBe(Agent) + expect(Client.Model).toBe(Model) + expect(Client.Session).toBe(Session) +}) + test("Core and Server reuse the authoritative Schema and Protocol values", () => { expect(AgentV2.ID).toBe(Agent.ID) expect(CoreLocation.Ref).toBe(Location.Ref) diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts index c3ffeedc3c..6458aa85fb 100644 --- a/packages/core/src/session/store.ts +++ b/packages/core/src/session/store.ts @@ -7,16 +7,16 @@ import { makeGlobalNode } from "../effect/app-node" import { SessionHistory } from "./history" import { MessageDecodeError } from "./error" import { SessionMessage } from "./message" -import { SessionSchema } from "./schema" +import { Session } from "@opencode-ai/schema/session" import { SessionMessageTable, SessionTable } from "./sql" import { fromRow } from "./info" export interface Interface { - readonly get: (sessionID: SessionSchema.ID) => Effect.Effect - readonly context: (sessionID: SessionSchema.ID) => Effect.Effect + readonly get: (sessionID: Session.ID) => Effect.Effect + readonly context: (sessionID: Session.ID) => Effect.Effect readonly message: ( messageID: SessionMessage.ID, - ) => Effect.Effect<{ readonly sessionID: SessionSchema.ID; readonly message: SessionMessage.Message } | undefined> + ) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Message } | undefined> } export class Service extends Context.Service()("@opencode/v2/SessionStore") {} @@ -44,7 +44,7 @@ const layer = Layer.effect( .pipe(Effect.orDie) return row ? { - sessionID: SessionSchema.ID.make(row.session_id), + sessionID: Session.ID.make(row.session_id), message: yield* decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie), } : undefined diff --git a/packages/core/src/tool/hooks.ts b/packages/core/src/tool/hooks.ts index d71be015dc..4b491c57cd 100644 --- a/packages/core/src/tool/hooks.ts +++ b/packages/core/src/tool/hooks.ts @@ -1,17 +1,17 @@ export * as ToolHooks from "./hooks" import { makeLocationNode } from "../effect/app-node" -import { AgentV2 } from "../agent" +import { Agent } from "@opencode-ai/schema/agent" +import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "../session/message" -import { SessionSchema } from "../session/schema" import { State } from "../state" import { Context, Effect, Layer, Scope } from "effect" import type { ToolOutput, ToolResultValue } from "@opencode-ai/llm" export interface BeforeEvent { readonly tool: string - readonly sessionID: SessionSchema.ID - readonly agent: AgentV2.ID + readonly sessionID: Session.ID + readonly agent: Agent.ID readonly assistantMessageID: SessionMessage.ID readonly toolCallID: string input: unknown @@ -19,8 +19,8 @@ export interface BeforeEvent { export interface AfterEvent { readonly tool: string - readonly sessionID: SessionSchema.ID - readonly agent: AgentV2.ID + readonly sessionID: Session.ID + readonly agent: Agent.ID readonly assistantMessageID: SessionMessage.ID readonly toolCallID: string readonly input: unknown @@ -80,8 +80,14 @@ const layer = Layer.effect( return Service.of({ hook: { - before: register(() => beforeHooks, (next) => (beforeHooks = next)), - after: register(() => afterHooks, (next) => (afterHooks = next)), + before: register( + () => beforeHooks, + (next) => (beforeHooks = next), + ), + after: register( + () => afterHooks, + (next) => (afterHooks = next), + ), }, runBefore: (event) => run(beforeHooks, event), runAfter: (event) => run(afterHooks, event), diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 93214e61bf..21f68ad682 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -5,6 +5,7 @@ "type": "module", "license": "MIT", "scripts": { + "test": "bun test --timeout 5000", "typecheck": "tsgo --noEmit", "build": "tsc" }, diff --git a/packages/plugin/src/v2/effect/agent.ts b/packages/plugin/src/v2/effect/agent.ts index ce56cbe777..aadacf99d5 100644 --- a/packages/plugin/src/v2/effect/agent.ts +++ b/packages/plugin/src/v2/effect/agent.ts @@ -1,5 +1,5 @@ -import type { AgentV2Info } from "@opencode-ai/sdk/v2/types" import type { AgentApi } from "@opencode-ai/client/effect/api" +import type { AgentV2Info } from "@opencode-ai/sdk/v2/types" import type { Effect } from "effect" import type { TransformHook } from "./registration.js" diff --git a/packages/plugin/src/v2/effect/aisdk.ts b/packages/plugin/src/v2/effect/aisdk.ts index ddc292e384..2934edc27d 100644 --- a/packages/plugin/src/v2/effect/aisdk.ts +++ b/packages/plugin/src/v2/effect/aisdk.ts @@ -1,16 +1,16 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" -import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import type { Model } from "@opencode-ai/schema/model" import type { Hooks } from "./registration.js" export type AISDKHooks = Hooks<{ sdk: { - readonly model: ModelV2Info + readonly model: Model.Info readonly package: string readonly options: Record sdk?: any } language: { - readonly model: ModelV2Info + readonly model: Model.Info readonly sdk: any readonly options: Record language?: LanguageModelV3 diff --git a/packages/plugin/src/v2/effect/index.ts b/packages/plugin/src/v2/effect/index.ts index 5468dcd82f..1332ce2752 100644 --- a/packages/plugin/src/v2/effect/index.ts +++ b/packages/plugin/src/v2/effect/index.ts @@ -12,3 +12,13 @@ export type { SkillDraft, SkillHooks } from "./skill.js" export * as Tool from "./tool.js" export type { ToolDomain, ToolDraft, ToolExecuteBeforeEvent, ToolExecuteAfterEvent } from "./tool.js" export type { SessionHooks } from "./runtime.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" diff --git a/packages/plugin/src/v2/promise/aisdk.ts b/packages/plugin/src/v2/promise/aisdk.ts index ddc292e384..2934edc27d 100644 --- a/packages/plugin/src/v2/promise/aisdk.ts +++ b/packages/plugin/src/v2/promise/aisdk.ts @@ -1,16 +1,16 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" -import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import type { Model } from "@opencode-ai/schema/model" import type { Hooks } from "./registration.js" export type AISDKHooks = Hooks<{ sdk: { - readonly model: ModelV2Info + readonly model: Model.Info readonly package: string readonly options: Record sdk?: any } language: { - readonly model: ModelV2Info + readonly model: Model.Info readonly sdk: any readonly options: Record language?: LanguageModelV3 diff --git a/packages/plugin/src/v2/promise/index.ts b/packages/plugin/src/v2/promise/index.ts index 594ff7da3c..4f5d5754ea 100644 --- a/packages/plugin/src/v2/promise/index.ts +++ b/packages/plugin/src/v2/promise/index.ts @@ -11,3 +11,13 @@ export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration export type { ReferenceDraft, ReferenceHooks } from "./reference.js" export type { SessionHooks } from "./runtime.js" export type { SkillDraft, SkillHooks } from "./skill.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" diff --git a/packages/plugin/test/contract-identity.test.ts b/packages/plugin/test/contract-identity.test.ts new file mode 100644 index 0000000000..00efbd2aff --- /dev/null +++ b/packages/plugin/test/contract-identity.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test" +import { Agent } from "@opencode-ai/schema/agent" +import { Command } from "@opencode-ai/schema/command" +import { Connection } from "@opencode-ai/schema/connection" +import { Credential } from "@opencode-ai/schema/credential" +import { Integration } from "@opencode-ai/schema/integration" +import { Model } from "@opencode-ai/schema/model" +import { Provider } from "@opencode-ai/schema/provider" +import { Reference } from "@opencode-ai/schema/reference" +import { Skill } from "@opencode-ai/schema/skill" + +const Plugin = await import("../src/v2/effect/index") +const PromisePlugin = await import("../src/v2/promise/index") + +test.each([ + ["effect", Plugin], + ["promise", PromisePlugin], +])("%s entrypoint exposes its canonical Schema contracts", (name, entrypoint) => { + expect(entrypoint.Agent).toBe(Agent) + expect(entrypoint.Command).toBe(Command) + expect(entrypoint.Connection).toBe(Connection) + expect(entrypoint.Credential).toBe(Credential) + expect(entrypoint.Integration).toBe(Integration) + expect(entrypoint.Model).toBe(Model) + expect(entrypoint.Provider).toBe(Provider) + expect(entrypoint.Reference).toBe(Reference) + expect(entrypoint.Skill).toBe(Skill) + expect(Object.keys(entrypoint).sort()).toEqual([ + "Agent", + "Command", + "Connection", + "Credential", + "Integration", + "Model", + "Provider", + "Reference", + "Skill", + ...(name === "effect" ? ["Tool"] : []), + "define", + ]) +}) diff --git a/packages/schema/AGENTS.md b/packages/schema/AGENTS.md index 1d5747d539..400b4b312b 100644 --- a/packages/schema/AGENTS.md +++ b/packages/schema/AGENTS.md @@ -34,6 +34,13 @@ - Use flat top-level exports plus the package's existing namespace projection pattern, for example `export * as SessionMessage from "./session-message"`. - Keep standalone ID modules only when they prevent real cycles or heavy dependency edges. Inline one-off IDs into their owning contract module when no cycle exists. +## Public Re-exports + +- Public leaf packages re-export the canonical domain namespaces used by their APIs so consumers install and version one package. Keep the list curated by public API reachability rather than mirroring the Schema root. +- Re-export directly from `@opencode-ai/schema/` and preserve the exact canonical values and types. Do not wrap schemas, duplicate definitions, or route exports through another facade package. +- Internal packages import contracts directly from their canonical Schema owner. Import a Core domain facade only when runtime behavior from that facade is required. +- Resolve name collisions by retaining the domain namespace and qualifying its members, such as `Session.Info` and `Agent.Info`. Do not introduce aliases such as `SessionSchema`, `AgentSchema`, or `ModelSchema`. + ## Naming - Exported schema values and namespace objects use `PascalCase`. diff --git a/packages/sdk-next/package.json b/packages/sdk-next/package.json index 2200251099..3b092e1745 100644 --- a/packages/sdk-next/package.json +++ b/packages/sdk-next/package.json @@ -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:" }, diff --git a/packages/sdk-next/src/index.ts b/packages/sdk-next/src/index.ts index 4101021152..d8ce5f656b 100644 --- a/packages/sdk-next/src/index.ts +++ b/packages/sdk-next/src/index.ts @@ -2,17 +2,26 @@ export * as OpenCode from "./opencode" export * as Tool from "./tool" export { ClientError } from "@opencode-ai/client/effect" -export { - AbsolutePath, - Agent, - Location, - Model, - Prompt, - PromptInput, - Provider, - RelativePath, - 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 { 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" diff --git a/packages/sdk-next/test/contract-identity.test.ts b/packages/sdk-next/test/contract-identity.test.ts new file mode 100644 index 0000000000..830fe5f43b --- /dev/null +++ b/packages/sdk-next/test/contract-identity.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test" +import { Agent } from "@opencode-ai/schema/agent" +import { Model } from "@opencode-ai/schema/model" +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.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", + "Session", + "SessionInput", + "SessionMessage", + "Skill", + "Tool", + ]) +})