feat(v2): expose canonical schema contracts (#35595)
This commit is contained in:
parent
6ffe61b9fe
commit
dc99c600b6
15 changed files with 167 additions and 33 deletions
1
bun.lock
1
bun.lock
|
|
@ -775,6 +775,7 @@
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,14 @@ import { Api } from "@opencode-ai/server/api"
|
||||||
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
|
||||||
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
|
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", () => {
|
test("Core and Server reuse the authoritative Schema and Protocol values", () => {
|
||||||
expect(AgentV2.ID).toBe(Agent.ID)
|
expect(AgentV2.ID).toBe(Agent.ID)
|
||||||
expect(CoreLocation.Ref).toBe(Location.Ref)
|
expect(CoreLocation.Ref).toBe(Location.Ref)
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,16 @@ import { makeGlobalNode } from "../effect/app-node"
|
||||||
import { SessionHistory } from "./history"
|
import { SessionHistory } from "./history"
|
||||||
import { MessageDecodeError } from "./error"
|
import { MessageDecodeError } from "./error"
|
||||||
import { SessionMessage } from "./message"
|
import { SessionMessage } from "./message"
|
||||||
import { SessionSchema } from "./schema"
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
import { SessionMessageTable, SessionTable } from "./sql"
|
import { SessionMessageTable, SessionTable } from "./sql"
|
||||||
import { fromRow } from "./info"
|
import { fromRow } from "./info"
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info | undefined>
|
readonly get: (sessionID: Session.ID) => Effect.Effect<Session.Info | undefined>
|
||||||
readonly context: (sessionID: SessionSchema.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
readonly context: (sessionID: Session.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||||
readonly message: (
|
readonly message: (
|
||||||
messageID: SessionMessage.ID,
|
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<Service, Interface>()("@opencode/v2/SessionStore") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionStore") {}
|
||||||
|
|
@ -44,7 +44,7 @@ const layer = Layer.effect(
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
return row
|
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),
|
message: yield* decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
export * as ToolHooks from "./hooks"
|
export * as ToolHooks from "./hooks"
|
||||||
|
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
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 { SessionMessage } from "../session/message"
|
||||||
import { SessionSchema } from "../session/schema"
|
|
||||||
import { State } from "../state"
|
import { State } from "../state"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
import type { ToolOutput, ToolResultValue } from "@opencode-ai/llm"
|
import type { ToolOutput, ToolResultValue } from "@opencode-ai/llm"
|
||||||
|
|
||||||
export interface BeforeEvent {
|
export interface BeforeEvent {
|
||||||
readonly tool: string
|
readonly tool: string
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: Session.ID
|
||||||
readonly agent: AgentV2.ID
|
readonly agent: Agent.ID
|
||||||
readonly assistantMessageID: SessionMessage.ID
|
readonly assistantMessageID: SessionMessage.ID
|
||||||
readonly toolCallID: string
|
readonly toolCallID: string
|
||||||
input: unknown
|
input: unknown
|
||||||
|
|
@ -19,8 +19,8 @@ export interface BeforeEvent {
|
||||||
|
|
||||||
export interface AfterEvent {
|
export interface AfterEvent {
|
||||||
readonly tool: string
|
readonly tool: string
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: Session.ID
|
||||||
readonly agent: AgentV2.ID
|
readonly agent: Agent.ID
|
||||||
readonly assistantMessageID: SessionMessage.ID
|
readonly assistantMessageID: SessionMessage.ID
|
||||||
readonly toolCallID: string
|
readonly toolCallID: string
|
||||||
readonly input: unknown
|
readonly input: unknown
|
||||||
|
|
@ -80,8 +80,14 @@ const layer = Layer.effect(
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
hook: {
|
hook: {
|
||||||
before: register(() => beforeHooks, (next) => (beforeHooks = next)),
|
before: register(
|
||||||
after: register(() => afterHooks, (next) => (afterHooks = next)),
|
() => beforeHooks,
|
||||||
|
(next) => (beforeHooks = next),
|
||||||
|
),
|
||||||
|
after: register(
|
||||||
|
() => afterHooks,
|
||||||
|
(next) => (afterHooks = next),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
runBefore: (event) => run(beforeHooks, event),
|
runBefore: (event) => run(beforeHooks, event),
|
||||||
runAfter: (event) => run(afterHooks, event),
|
runAfter: (event) => run(afterHooks, event),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "bun test --timeout 5000",
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { AgentV2Info } from "@opencode-ai/sdk/v2/types"
|
|
||||||
import type { AgentApi } from "@opencode-ai/client/effect/api"
|
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 { Effect } from "effect"
|
||||||
import type { TransformHook } from "./registration.js"
|
import type { TransformHook } from "./registration.js"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
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"
|
import type { Hooks } from "./registration.js"
|
||||||
|
|
||||||
export type AISDKHooks = Hooks<{
|
export type AISDKHooks = Hooks<{
|
||||||
sdk: {
|
sdk: {
|
||||||
readonly model: ModelV2Info
|
readonly model: Model.Info
|
||||||
readonly package: string
|
readonly package: string
|
||||||
readonly options: Record<string, any>
|
readonly options: Record<string, any>
|
||||||
sdk?: any
|
sdk?: any
|
||||||
}
|
}
|
||||||
language: {
|
language: {
|
||||||
readonly model: ModelV2Info
|
readonly model: Model.Info
|
||||||
readonly sdk: any
|
readonly sdk: any
|
||||||
readonly options: Record<string, any>
|
readonly options: Record<string, any>
|
||||||
language?: LanguageModelV3
|
language?: LanguageModelV3
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,13 @@ export type { SkillDraft, SkillHooks } from "./skill.js"
|
||||||
export * as Tool from "./tool.js"
|
export * as Tool from "./tool.js"
|
||||||
export type { ToolDomain, ToolDraft, ToolExecuteBeforeEvent, ToolExecuteAfterEvent } from "./tool.js"
|
export type { ToolDomain, ToolDraft, ToolExecuteBeforeEvent, ToolExecuteAfterEvent } from "./tool.js"
|
||||||
export type { SessionHooks } from "./runtime.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"
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import type { LanguageModelV3 } from "@ai-sdk/provider"
|
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"
|
import type { Hooks } from "./registration.js"
|
||||||
|
|
||||||
export type AISDKHooks = Hooks<{
|
export type AISDKHooks = Hooks<{
|
||||||
sdk: {
|
sdk: {
|
||||||
readonly model: ModelV2Info
|
readonly model: Model.Info
|
||||||
readonly package: string
|
readonly package: string
|
||||||
readonly options: Record<string, any>
|
readonly options: Record<string, any>
|
||||||
sdk?: any
|
sdk?: any
|
||||||
}
|
}
|
||||||
language: {
|
language: {
|
||||||
readonly model: ModelV2Info
|
readonly model: Model.Info
|
||||||
readonly sdk: any
|
readonly sdk: any
|
||||||
readonly options: Record<string, any>
|
readonly options: Record<string, any>
|
||||||
language?: LanguageModelV3
|
language?: LanguageModelV3
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,13 @@ export type { IntegrationDraft, IntegrationHooks, IntegrationMethodRegistration
|
||||||
export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
|
export type { ReferenceDraft, ReferenceHooks } from "./reference.js"
|
||||||
export type { SessionHooks } from "./runtime.js"
|
export type { SessionHooks } from "./runtime.js"
|
||||||
export type { SkillDraft, SkillHooks } from "./skill.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"
|
||||||
|
|
|
||||||
41
packages/plugin/test/contract-identity.test.ts
Normal file
41
packages/plugin/test/contract-identity.test.ts
Normal file
|
|
@ -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",
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
@ -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"`.
|
- 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.
|
- 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/<domain>` 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
|
## Naming
|
||||||
|
|
||||||
- Exported schema values and namespace objects use `PascalCase`.
|
- Exported schema values and namespace objects use `PascalCase`.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
"effect": "catalog:"
|
"effect": "catalog:"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,26 @@ export * as OpenCode from "./opencode"
|
||||||
export * as Tool from "./tool"
|
export * as Tool from "./tool"
|
||||||
|
|
||||||
export { ClientError } from "@opencode-ai/client/effect"
|
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 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"
|
||||||
|
|
|
||||||
40
packages/sdk-next/test/contract-identity.test.ts
Normal file
40
packages/sdk-next/test/contract-identity.test.ts
Normal file
|
|
@ -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",
|
||||||
|
])
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue