diff --git a/bun.lock b/bun.lock index 657424dfef..c77c1cca54 100644 --- a/bun.lock +++ b/bun.lock @@ -277,6 +277,7 @@ "@opencode-ai/effect-sqlite-node": "workspace:*", "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@openrouter/ai-sdk-provider": "2.9.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", @@ -480,6 +481,7 @@ "name": "@opencode-ai/llm", "version": "1.17.9", "dependencies": { + "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", @@ -651,6 +653,17 @@ "@opentui/solid", ], }, + "packages/schema": { + "name": "@opencode-ai/schema", + "dependencies": { + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/script": { "name": "@opencode-ai/script", "dependencies": { @@ -1804,6 +1817,8 @@ "@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"], + "@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"], + "@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"], "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], diff --git a/packages/core/package.json b/packages/core/package.json index 2aec5ecc7e..633da4b92e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -91,6 +91,7 @@ "@opencode-ai/effect-drizzle-sqlite": "workspace:*", "@opencode-ai/effect-sqlite-node": "workspace:*", "@opencode-ai/llm": "workspace:*", + "@opencode-ai/schema": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index f27b5c3ef8..91f411684a 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,46 +1,17 @@ export * as AgentV2 from "./agent" -import { Array, Context, Effect, Layer, Schema, Scope, Types } from "effect" -import { ModelV2 } from "./model" -import { PermissionSchema } from "./permission/schema" -import { ProviderV2 } from "./provider" -import { PositiveInt } from "./schema" +import { Array, Context, Effect, Layer, Types } from "effect" +import { Agent } from "@opencode-ai/schema/agent" import { State } from "./state" -export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID")) +export const ID = Agent.ID export type ID = typeof ID.Type export const defaultID = ID.make("build") -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +export const Color = Agent.Color -export class Info extends Schema.Class("AgentV2.Info")({ - id: ID, - model: ModelV2.Ref.pipe(Schema.optional), - request: ProviderV2.Request, - system: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - mode: Schema.Literals(["subagent", "primary", "all"]), - hidden: Schema.Boolean, - color: Color.pipe(Schema.optional), - steps: PositiveInt.pipe(Schema.optional), - permissions: PermissionSchema.Ruleset, -}) { - static empty(id: ID) { - return new Info({ - id, - request: { - headers: {}, - body: {}, - }, - mode: "all", - hidden: false, - permissions: [], - }) - } -} +export const Info = Agent.Info +export type Info = Agent.Info export interface Selection { readonly id: ID diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index d19a414b34..0b274299e5 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -89,7 +89,7 @@ export const layer = Layer.effect( ...ModelRequest.merge({ ...provider.request, generation: {}, options: {} }, model.request), variant: model.request.variant, } - return new ModelV2.Info({ + return ModelV2.Info.make({ ...model, api, request, diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index 947ad311e7..c65693ebfd 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -1,17 +1,11 @@ export * as CommandV2 from "./command" -import { Context, Effect, Layer, Schema, Types } from "effect" -import { ModelV2 } from "./model" +import { Context, Effect, Layer, Types } from "effect" +import { Command } from "@opencode-ai/schema/command" import { State } from "./state" -export class Info extends Schema.Class("CommandV2.Info")({ - name: Schema.String, - template: Schema.String, - description: Schema.String.pipe(Schema.optional), - agent: Schema.String.pipe(Schema.optional), - model: ModelV2.Ref.pipe(Schema.optional), - subtask: Schema.Boolean.pipe(Schema.optional), -}) {} +export const Info = Command.Info +export type Info = Command.Info export type Data = { commands: Map> @@ -40,7 +34,7 @@ export const layer = Layer.effect( list: () => Array.from(draft.commands.values()) as Info[], get: (name) => draft.commands.get(name), update: (name, update) => { - const current = draft.commands.get(name) ?? (new Info({ name, template: "" }) as Types.DeepMutable) + const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable) if (!draft.commands.has(name)) draft.commands.set(name, current) update(current) current.name = name diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index 82487e4a89..0e7b95ac08 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -28,7 +28,7 @@ export const Plugin = define({ entries.set( name, local(entry) - ? new Reference.LocalSource({ + ? Reference.LocalSource.make({ type: "local", path: AbsolutePath.make( localPath(directory, global.home, typeof entry === "string" ? entry : entry.path), @@ -36,7 +36,7 @@ export const Plugin = define({ description: typeof entry === "string" ? undefined : entry.description, hidden: typeof entry === "string" ? undefined : entry.hidden, }) - : new Reference.GitSource({ + : Reference.GitSource.make({ type: "git", repository: typeof entry === "string" ? entry : entry.repository, branch: typeof entry === "string" ? undefined : entry.branch, diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts index eca8b5ccae..765992a765 100644 --- a/packages/core/src/config/plugin/skill.ts +++ b/packages/core/src/config/plugin/skill.ts @@ -22,20 +22,23 @@ export const Plugin = define({ const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) for (const directory of directories) { draft.source( - new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), ) draft.source( - new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), + SkillV2.DirectorySource.make({ + type: "directory", + path: AbsolutePath.make(path.join(directory, "skills")), + }), ) } for (const item of items) { if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { - draft.source(new SkillV2.UrlSource({ type: "url", url: item })) + draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) continue } const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item draft.source( - new SkillV2.DirectorySource({ + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), }), diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index da8fcb5c63..42cb3b64b5 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -2,37 +2,22 @@ export * as Credential from "./credential" import { asc, eq } from "drizzle-orm" import { Context, Effect, Layer, Schema } from "effect" +import { Credential } from "@opencode-ai/schema/credential" import { Database } from "./database/database" import { IntegrationSchema } from "./integration/schema" -import { NonNegativeInt, withStatics } from "./schema" -import { Identifier } from "./util/identifier" import { CredentialTable } from "./credential/sql" -export const ID = Schema.String.pipe( - Schema.brand("Credential.ID"), - withStatics((schema) => ({ create: () => schema.make("cred_" + Identifier.ascending()) })), -) -export type ID = typeof ID.Type +export const ID = Credential.ID +export type ID = Credential.ID -export class OAuth extends Schema.Class("Credential.OAuth")({ - type: Schema.Literal("oauth"), - methodID: IntegrationSchema.MethodID, - refresh: Schema.String, - access: Schema.String, - expires: NonNegativeInt, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}) {} +export const OAuth = Credential.OAuth +export type OAuth = Credential.OAuth -export class Key extends Schema.Class("Credential.Key")({ - type: Schema.Literal("key"), - key: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}) {} +export const Key = Credential.Key +export type Key = Credential.Key -export const Value = Schema.Union([OAuth, Key]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Credential.Value" }) -export type Value = Schema.Schema.Type +export const Value = Credential.Value +export type Value = Credential.Value export class Info extends Schema.Class("Credential.Info")({ id: ID, diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 3439a0aefc..93d5afce53 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -5,7 +5,7 @@ import { and, asc, eq, gt } from "drizzle-orm" import { Database } from "./database/database" import { EventSequenceTable, EventTable } from "./event/sql" import { Location } from "./location" -import { externalID, type ExternalID, withStatics } from "./schema" +import { withStatics } from "./schema" import { Identifier } from "./util/identifier" import { LayerNode } from "./effect/layer-node" import { isDeepStrictEqual } from "node:util" @@ -14,7 +14,6 @@ export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe( Schema.brand("Event.ID"), withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()), - fromExternal: (input: ExternalID) => schema.make(externalID("evt", input)), })), ) export type ID = typeof ID.Type diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index 976ec020b8..9a95f4a480 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -108,7 +108,7 @@ const baseLayer = Layer.effect( const absolute = path.join(target.absolute, item.name) const relative = path.relative(target.directory, absolute) return [ - new Entry({ + Entry.make({ path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), type: item.type, }), diff --git a/packages/core/src/filesystem/schema.ts b/packages/core/src/filesystem/schema.ts index fdb2ed1817..8f4f27cc59 100644 --- a/packages/core/src/filesystem/schema.ts +++ b/packages/core/src/filesystem/schema.ts @@ -1,22 +1,10 @@ -import { Schema } from "effect" -import { NonNegativeInt, PositiveInt, RelativePath } from "../schema" +import { FileSystem } from "@opencode-ai/schema/filesystem" -export class Entry extends Schema.Class("FileSystem.Entry")({ - path: RelativePath, - type: Schema.Literals(["file", "directory"]), -}) {} +export const Entry = FileSystem.Entry +export type Entry = FileSystem.Entry -export const Submatch = Schema.Struct({ - text: Schema.String, - start: NonNegativeInt, - end: NonNegativeInt, -}) -export type Submatch = typeof Submatch.Type +export const Submatch = FileSystem.Submatch +export type Submatch = FileSystem.Submatch -export class Match extends Schema.Class("FileSystem.Match")({ - entry: Entry, - line: PositiveInt, - offset: NonNegativeInt, - text: Schema.String, - submatches: Schema.Array(Submatch), -}) {} +export const Match = FileSystem.Match +export type Match = FileSystem.Match diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 600dbe8039..5ef1a3c2d1 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -59,12 +59,11 @@ export const ripgrepLayer = Layer.effect( }) .pipe( Effect.map((result) => - result.map( - (entry) => - new FileSystem.Entry({ - ...entry, - path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), - }), + result.map((entry) => + FileSystem.Entry.make({ + ...entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), + }), ), ), Effect.orDie, @@ -85,15 +84,14 @@ export const ripgrepLayer = Layer.effect( }) .pipe( Effect.map((result) => - result.map( - (match) => - new FileSystem.Match({ - ...match, - entry: new FileSystem.Entry({ - ...match.entry, - path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))), - }), + result.map((match) => + FileSystem.Match.make({ + ...match, + entry: FileSystem.Entry.make({ + ...match.entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))), }), + }), ), ), Effect.orDie, @@ -110,7 +108,7 @@ export const ripgrepLayer = Layer.effect( return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => { const relative = item.target const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const) - return new FileSystem.Entry({ + return FileSystem.Entry.make({ path: RelativePath.make(relative), type, }) @@ -145,12 +143,11 @@ export const fffLayer = Layer.effect( pageSize: input.limit, }) if (!found.ok) throw found.error - return found.value.items.map( - (item) => - new FileSystem.Entry({ - path: RelativePath.make(item.relativePath.replaceAll("\\", "/")), - type: "file", - }), + return found.value.items.map((item) => + FileSystem.Entry.make({ + path: RelativePath.make(item.relativePath.replaceAll("\\", "/")), + type: "file", + }), ) }), grep: (input) => @@ -165,8 +162,8 @@ export const fffLayer = Layer.effect( if (!found.ok) throw found.error return found.value.items.map((match) => { const bytes = Buffer.from(match.lineContent) - return new FileSystem.Match({ - entry: new FileSystem.Entry({ + return FileSystem.Match.make({ + entry: FileSystem.Entry.make({ path: RelativePath.make(match.relativePath.replaceAll("\\", "/")), type: "file", }), @@ -215,7 +212,7 @@ export const fffLayer = Layer.effect( .sort((a, b) => b.score - a.score || a.path.length - b.path.length) .map((item) => { const relative = item.path.replaceAll("\\", "/").replace(/\/$/, "") - return new FileSystem.Entry({ + return FileSystem.Entry.make({ path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), type: item.type, }) diff --git a/packages/core/src/id/id.ts b/packages/core/src/id/id.ts index 847a5c0329..be1efc446a 100644 --- a/packages/core/src/id/id.ts +++ b/packages/core/src/id/id.ts @@ -1,4 +1,4 @@ -import { randomBytes } from "crypto" +import { create as createIdentifier } from "@opencode-ai/schema/identifier" const prefixes = { job: "job", @@ -13,12 +13,6 @@ const prefixes = { workspace: "wrk", } as const -const LENGTH = 26 - -// State for monotonic ID generation -let lastTimestamp = 0 -let counter = 0 - export function ascending(prefix: keyof typeof prefixes, given?: string) { return generateID(prefix, "ascending", given) } @@ -38,35 +32,8 @@ function generateID(prefix: keyof typeof prefixes, direction: "descending" | "as return given } -function randomBase62(length: number): string { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - let result = "" - const bytes = randomBytes(length) - for (let i = 0; i < length; i++) { - result += chars[bytes[i] % 62] - } - return result -} - export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { - const currentTimestamp = timestamp ?? Date.now() - - if (currentTimestamp !== lastTimestamp) { - lastTimestamp = currentTimestamp - counter = 0 - } - counter++ - - let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter) - - now = direction === "descending" ? ~now : now - - const timeBytes = Buffer.alloc(6) - for (let i = 0; i < 6; i++) { - timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) - } - - return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12) + return prefix + "_" + createIdentifier(direction === "descending", timestamp) } /** Extract timestamp from an ascending ID. Does not work with descending IDs. */ diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 9769d78e59..ed0e3935e8 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -14,6 +14,7 @@ import { SynchronizedRef, Types, } from "effect" +import { Integration } from "@opencode-ai/schema/integration" import { Credential } from "./credential" import { IntegrationSchema } from "./integration/schema" import { withStatics } from "./schema" @@ -34,66 +35,29 @@ export const AttemptID = Schema.String.pipe( ) export type AttemptID = typeof AttemptID.Type -export const When = Schema.Struct({ - key: Schema.String, - op: Schema.Literals(["eq", "neq"]), - value: Schema.String, -}).annotate({ identifier: "Integration.When" }) -export type When = typeof When.Type +export const When = Integration.When +export type When = Integration.When -export const TextPrompt = Schema.Struct({ - type: Schema.Literal("text"), - key: Schema.String, - message: Schema.String, - placeholder: Schema.optional(Schema.String), - when: Schema.optional(When), -}).annotate({ identifier: "Integration.TextPrompt" }) -export type TextPrompt = typeof TextPrompt.Type +export const TextPrompt = Integration.TextPrompt +export type TextPrompt = Integration.TextPrompt -export const SelectPrompt = Schema.Struct({ - type: Schema.Literal("select"), - key: Schema.String, - message: Schema.String, - options: Schema.mutable( - Schema.Array( - Schema.Struct({ - label: Schema.String, - value: Schema.String, - hint: Schema.optional(Schema.String), - }), - ), - ), - when: Schema.optional(When), -}).annotate({ identifier: "Integration.SelectPrompt" }) -export type SelectPrompt = typeof SelectPrompt.Type +export const SelectPrompt = Integration.SelectPrompt +export type SelectPrompt = Integration.SelectPrompt -export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type")) -export type Prompt = typeof Prompt.Type +export const Prompt = Integration.Prompt +export type Prompt = Integration.Prompt -export const OAuthMethod = Schema.Struct({ - id: MethodID, - type: Schema.Literal("oauth"), - label: Schema.String, - prompts: Schema.optional(Schema.mutable(Schema.Array(Prompt))), -}).annotate({ identifier: "Integration.OAuthMethod" }) -export type OAuthMethod = typeof OAuthMethod.Type +export const OAuthMethod = Integration.OAuthMethod +export type OAuthMethod = Integration.OAuthMethod -export const KeyMethod = Schema.Struct({ - type: Schema.Literal("key"), - label: Schema.optional(Schema.String), -}).annotate({ identifier: "Integration.KeyMethod" }) -export type KeyMethod = typeof KeyMethod.Type +export const KeyMethod = Integration.KeyMethod +export type KeyMethod = Integration.KeyMethod -export const EnvMethod = Schema.Struct({ - type: Schema.Literal("env"), - names: Schema.mutable(Schema.Array(Schema.String)), -}).annotate({ identifier: "Integration.EnvMethod" }) -export type EnvMethod = typeof EnvMethod.Type +export const EnvMethod = Integration.EnvMethod +export type EnvMethod = Integration.EnvMethod -export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Integration.Method" }) -export type Method = typeof Method.Type +export const Method = Integration.Method +export type Method = Integration.Method export class Info extends Schema.Class("Integration.Info")({ id: ID, @@ -102,8 +66,8 @@ export class Info extends Schema.Class("Integration.Info")({ connections: Schema.mutable(Schema.Array(IntegrationConnection.Info)), }) {} -export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" }) -export type Inputs = typeof Inputs.Type +export const Inputs = Integration.Inputs +export type Inputs = Integration.Inputs export type OAuthAuthorization = { readonly url: string @@ -188,11 +152,8 @@ export const Event = { }), } -export const Ref = Schema.Struct({ - id: ID, - name: Schema.String, -}).annotate({ identifier: "Integration.Ref" }) -export type Ref = typeof Ref.Type +export const Ref = Integration.Ref +export type Ref = Integration.Ref type Entry = { ref: Types.DeepMutable @@ -464,7 +425,7 @@ export const locationLayer = Layer.effect( resolve: Effect.fn("Integration.connection.resolve")(function* (connection) { if (connection.type === "env") { const key = process.env[connection.name] - return key ? new Credential.Key({ type: "key", key }) : undefined + return key ? Credential.Key.make({ type: "key", key }) : undefined } const credential = yield* credentials.get(connection.id) if (!credential) return undefined @@ -489,7 +450,7 @@ export const locationLayer = Layer.effect( yield* credentials.create({ integrationID: input.integrationID, label: input.label, - value: new Credential.Key({ type: "key", key: input.key }), + value: Credential.Key.make({ type: "key", key: input.key }), }) yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) yield* events.publish(Event.Updated, {}) diff --git a/packages/core/src/integration/connection.ts b/packages/core/src/integration/connection.ts index 200cf26580..91ab7e9e40 100644 --- a/packages/core/src/integration/connection.ts +++ b/packages/core/src/integration/connection.ts @@ -1,22 +1,12 @@ export * as IntegrationConnection from "./connection" -import { Schema } from "effect" -import { Credential } from "../credential" +import { Connection } from "@opencode-ai/schema/connection" -export const CredentialInfo = Schema.Struct({ - type: Schema.Literal("credential"), - id: Credential.ID, - label: Schema.String, -}).annotate({ identifier: "Connection.CredentialInfo" }) -export type CredentialInfo = typeof CredentialInfo.Type +export const CredentialInfo = Connection.CredentialInfo +export type CredentialInfo = Connection.CredentialInfo -export const EnvInfo = Schema.Struct({ - type: Schema.Literal("env"), - name: Schema.String, -}).annotate({ identifier: "Connection.EnvInfo" }) -export type EnvInfo = typeof EnvInfo.Type +export const EnvInfo = Connection.EnvInfo +export type EnvInfo = Connection.EnvInfo -export const Info = Schema.Union([CredentialInfo, EnvInfo]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Connection.Info" }) -export type Info = typeof Info.Type +export const Info = Connection.Info +export type Info = Connection.Info diff --git a/packages/core/src/integration/schema.ts b/packages/core/src/integration/schema.ts index 472f4e0609..bb73d571ec 100644 --- a/packages/core/src/integration/schema.ts +++ b/packages/core/src/integration/schema.ts @@ -1,9 +1,9 @@ export * as IntegrationSchema from "./schema" -import { Schema } from "effect" +import { Integration } from "@opencode-ai/schema/integration" -export const ID = Schema.String.pipe(Schema.brand("Integration.ID")) -export type ID = typeof ID.Type +export const ID = Integration.ID +export type ID = Integration.ID -export const MethodID = Schema.String.pipe(Schema.brand("Integration.MethodID")) -export type MethodID = typeof MethodID.Type +export const MethodID = Integration.MethodID +export type MethodID = Integration.MethodID diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts index 9225e4a01a..d9f0ac1c2d 100644 --- a/packages/core/src/location.ts +++ b/packages/core/src/location.ts @@ -1,14 +1,12 @@ import { Context, Effect, Layer, Schema } from "effect" +import { Ref } from "@opencode-ai/schema/location" import { Project } from "./project" import { AbsolutePath, optionalOmitUndefined } from "./schema" import { WorkspaceV2 } from "./workspace" export * as Location from "./location" -export class Ref extends Schema.Class("Location.Ref")({ - directory: AbsolutePath, - workspaceID: Schema.optional(WorkspaceV2.ID).pipe(Schema.withConstructorDefault(Effect.succeed(undefined))), -}) {} +export { Ref } export class Info extends Schema.Class("Location.Info")({ directory: AbsolutePath, diff --git a/packages/core/src/model-request.ts b/packages/core/src/model-request.ts index 5de1f9803d..06059a9d73 100644 --- a/packages/core/src/model-request.ts +++ b/packages/core/src/model-request.ts @@ -1,34 +1,12 @@ export * as ModelRequest from "./model-request" -import { Effect, Schema } from "effect" +import { ModelRequest } from "@opencode-ai/schema/model-request" -export const Generation = Schema.Struct({ - maxTokens: Schema.Number.pipe(Schema.optional), - temperature: Schema.Number.pipe(Schema.optional), - topP: Schema.Number.pipe(Schema.optional), - topK: Schema.Number.pipe(Schema.optional), - frequencyPenalty: Schema.Number.pipe(Schema.optional), - presencePenalty: Schema.Number.pipe(Schema.optional), - seed: Schema.Number.pipe(Schema.optional), - stop: Schema.String.pipe(Schema.Array, Schema.mutable, Schema.optional), -}) -export type Generation = typeof Generation.Type +export const Generation = ModelRequest.Generation +export type Generation = ModelRequest.Generation -export const Request = Schema.Struct({ - headers: Schema.Record(Schema.String, Schema.String), - body: Schema.Record(Schema.String, Schema.Any), - generation: Generation.pipe( - Schema.optionalKey, - Schema.withConstructorDefault(Effect.succeed({})), - Schema.withDecodingDefaultKey(Effect.succeed({})), - ), - options: Schema.Record(Schema.String, Schema.Any).pipe( - Schema.optionalKey, - Schema.withConstructorDefault(Effect.succeed({})), - Schema.withDecodingDefaultKey(Effect.succeed({})), - ), -}) -export type Request = typeof Request.Type +export const Request = ModelRequest.Request +export type Request = ModelRequest.Request interface MutableRequest { headers: Record diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 8a06229e04..52fff98733 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,119 +1,30 @@ -import { Schema, Types } from "effect" +import { Types } from "effect" +import { Model } from "@opencode-ai/schema/model" import { ProviderV2 } from "./provider" -import { ModelRequest } from "./model-request" -export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID")) +export const ID = Model.ID export type ID = typeof ID.Type -export const VariantID = Schema.String.pipe(Schema.brand("VariantID")) +export const VariantID = Model.VariantID export type VariantID = typeof VariantID.Type // Grouping of models, eg claude opus, claude sonnet -export const Family = Schema.String.pipe(Schema.brand("Family")) -export type Family = typeof Family.Type +export const Family = Model.Family +export type Family = Model.Family -export const Capabilities = Schema.Struct({ - tools: Schema.Boolean, - // mime patterns, image, audio, video/*, text/* - input: Schema.String.pipe(Schema.Array, Schema.mutable), - output: Schema.String.pipe(Schema.Array, Schema.mutable), -}) -export type Capabilities = typeof Capabilities.Type +export const Capabilities = Model.Capabilities +export type Capabilities = Model.Capabilities -export const Cost = Schema.Struct({ - tier: Schema.Struct({ - type: Schema.Literal("context"), - size: Schema.Int, - }).pipe(Schema.optional), - input: Schema.Finite, - output: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), -}) +export const Cost = Model.Cost -export const Ref = Schema.Struct({ - id: ID, - providerID: ProviderV2.ID, - variant: VariantID.pipe(Schema.optional), -}) +export const Ref = Model.Ref export type Ref = typeof Ref.Type -export const Api = Schema.Union([ - Schema.Struct({ - id: ID, - ...ProviderV2.AISDK.fields, - }), - Schema.Struct({ - id: ID, - ...ProviderV2.Native.fields, - }), -]).pipe(Schema.toTaggedUnion("type")) -export type Api = typeof Api.Type +export const Api = Model.Api +export type Api = Model.Api -export class Info extends Schema.Class("ModelV2.Info")({ - id: ID, - providerID: ProviderV2.ID, - family: Family.pipe(Schema.optional), - name: Schema.String, - api: Api, - capabilities: Capabilities, - request: Schema.Struct({ - ...ModelRequest.Request.fields, - variant: Schema.String.pipe(Schema.optional), - }), - variants: Schema.Struct({ - id: VariantID, - ...ModelRequest.Request.fields, - }).pipe(Schema.Array, Schema.mutable), - time: Schema.Struct({ - released: Schema.Finite, - }), - cost: Cost.pipe(Schema.Array, Schema.mutable), - status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), - enabled: Schema.Boolean, - limit: Schema.Struct({ - context: Schema.Int, - input: Schema.Int.pipe(Schema.optional), - output: Schema.Int, - }), -}) { - static empty(providerID: ProviderV2.ID, modelID: ID): Info { - return new Info({ - id: modelID, - providerID, - name: modelID, - api: { - id: modelID, - type: "native", - settings: {}, - }, - capabilities: { - tools: false, - input: [], - output: [], - }, - request: { - headers: {}, - body: {}, - generation: {}, - options: {}, - }, - variants: [], - time: { - released: 0, - }, - cost: [], - status: "active", - enabled: true, - limit: { - context: 0, - output: 0, - }, - }) - } -} +export const Info = Model.Info +export type Info = Model.Info export type MutableInfo = Omit, "api"> & { api: ProviderV2.MutableApi diff --git a/packages/core/src/permission/schema.ts b/packages/core/src/permission/schema.ts index 9fde8ef8ec..c48511654a 100644 --- a/packages/core/src/permission/schema.ts +++ b/packages/core/src/permission/schema.ts @@ -1,16 +1,12 @@ export * as PermissionSchema from "./schema" -import { Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" -export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) -export type Effect = typeof Effect.Type +export const Effect = Permission.Effect +export type Effect = Permission.Effect -export const Rule = Schema.Struct({ - action: Schema.String, - resource: Schema.String, - effect: Effect, -}).annotate({ identifier: "PermissionV2.Rule" }) -export type Rule = typeof Rule.Type +export const Rule = Permission.Rule +export type Rule = Permission.Rule -export const Ruleset = Schema.mutable(Schema.Array(Rule)).annotate({ identifier: "PermissionV2.Ruleset" }) -export type Ruleset = typeof Ruleset.Type +export const Ruleset = Permission.Ruleset +export type Ruleset = Permission.Ruleset diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index cd1cf129ba..8c35d0f19a 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -128,12 +128,11 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int return { ...authorization, callback: authorization.callback.pipe( - Effect.map( - (credential) => - new Credential.OAuth({ - ...credential, - methodID: Integration.MethodID.make(credential.methodID), - }), + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), ), ), } @@ -142,12 +141,11 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int ...authorization, callback: (code: string) => authorization.callback(code).pipe( - Effect.map( - (credential) => - new Credential.OAuth({ - ...credential, - methodID: Integration.MethodID.make(credential.methodID), - }), + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), ), ), } @@ -157,12 +155,11 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int ? { refresh: (value: Credential.OAuth) => refresh(value).pipe( - Effect.map( - (next) => - new Credential.OAuth({ - ...next, - methodID: Integration.MethodID.make(next.methodID), - }), + Effect.map((next) => + Credential.OAuth.make({ + ...next, + methodID: Integration.MethodID.make(next.methodID), + }), ), ), } diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 8d226e7f02..e89382440e 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -213,7 +213,7 @@ function refresh(methodID: Integration.MethodID, value: Pick { const next = credential(methodID, tokens) - return new Credential.OAuth({ ...next, metadata: next.metadata ?? value.metadata }) + return Credential.OAuth.make({ ...next, metadata: next.metadata ?? value.metadata }) }), ) } @@ -231,7 +231,7 @@ function request(url: string, init: RequestInit) { function credential(methodID: Integration.MethodID, tokens: TokenResponse) { const accountID = extractAccountID(tokens) - return new Credential.OAuth({ + return Credential.OAuth.make({ type: "oauth", methodID, refresh: tokens.refresh_token, diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 7ed4e1e38a..691a2b2a28 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -274,7 +274,7 @@ function credential(http: HttpClient.HttpClient, server: string, token: typeof T { concurrency: 2 }, ) const org = orgs.toSorted((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))[0] - return new Credential.OAuth({ + return Credential.OAuth.make({ type: "oauth" as const, methodID, access: token.access_token, diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts index 0e9c85abe9..ea723dd89d 100644 --- a/packages/core/src/plugin/skill.ts +++ b/packages/core/src/plugin/skill.ts @@ -15,9 +15,9 @@ export const Plugin = define({ effect: Effect.fn(function* (ctx) { yield* ctx.skill.transform((draft) => { draft.source( - new SkillV2.EmbeddedSource({ + SkillV2.EmbeddedSource.make({ type: "embedded", - skill: new SkillV2.Info({ + skill: SkillV2.Info.make({ name: "customize-opencode", description: "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.", diff --git a/packages/core/src/project/schema.ts b/packages/core/src/project/schema.ts index 51d9581cc6..eed359abad 100644 --- a/packages/core/src/project/schema.ts +++ b/packages/core/src/project/schema.ts @@ -1,14 +1,10 @@ export * as ProjectSchema from "./schema" import { Schema } from "effect" -import { AbsolutePath, withStatics } from "../schema" +import { Project } from "@opencode-ai/schema/project" +import { AbsolutePath } from "../schema" -export const ID = Schema.String.pipe( - Schema.brand("Project.ID"), - withStatics((schema) => ({ - global: schema.make("global"), - })), -) +export const ID = Project.ID export type ID = typeof ID.Type export const Vcs = Schema.Union([ diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index a8e22f15a2..03f7d7eef3 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -1,75 +1,25 @@ export * as ProviderV2 from "./provider" -import { withStatics } from "./schema" -import { IntegrationSchema } from "./integration/schema" -import { Schema, Types } from "effect" +import { Types } from "effect" +import { Provider } from "@opencode-ai/schema/provider" -export const ID = Schema.String.pipe( - Schema.brand("ProviderV2.ID"), - withStatics((schema) => ({ - // Well-known providers - opencode: schema.make("opencode"), - anthropic: schema.make("anthropic"), - openai: schema.make("openai"), - google: schema.make("google"), - googleVertex: schema.make("google-vertex"), - githubCopilot: schema.make("github-copilot"), - amazonBedrock: schema.make("amazon-bedrock"), - azure: schema.make("azure"), - openrouter: schema.make("openrouter"), - mistral: schema.make("mistral"), - gitlab: schema.make("gitlab"), - })), -) +export const ID = Provider.ID export type ID = typeof ID.Type -export const AISDK = Schema.Struct({ - type: Schema.Literal("aisdk"), - package: Schema.String, - url: Schema.String.pipe(Schema.optional), - settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), -}) +export const AISDK = Provider.AISDK -export const Native = Schema.Struct({ - type: Schema.Literal("native"), - url: Schema.String.pipe(Schema.optional), - settings: Schema.Record(Schema.String, Schema.Unknown), -}) +export const Native = Provider.Native -export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type")) -export type Api = typeof Api.Type +export const Api = Provider.Api +export type Api = Provider.Api export type MutableApi = T extends Api ? Omit, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any }) : never -export const Request = Schema.Struct({ - headers: Schema.Record(Schema.String, Schema.String), - body: Schema.Record(Schema.String, Schema.Any), -}) -export type Request = typeof Request.Type +export const Request = Provider.Request +export type Request = Provider.Request -export class Info extends Schema.Class("ProviderV2.Info")({ - id: ID, - integrationID: IntegrationSchema.ID.pipe(Schema.optional), - name: Schema.String, - disabled: Schema.Boolean.pipe(Schema.optional), - api: Api, - request: Request, -}) { - static empty(providerID: ID): Info { - return new Info({ - id: providerID, - name: providerID, - api: { - type: "native", - settings: {}, - }, - request: { - headers: {}, - body: {}, - }, - }) - } -} +export const Info = Provider.Info +export type Info = Provider.Info export type MutableInfo = Omit, "api"> & { api: MutableApi } diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 9572f55a22..3edc84e98a 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -1,6 +1,7 @@ export * as Reference from "./reference" import { Context, Effect, Layer, Schema, Scope, Types } from "effect" +import { Reference } from "@opencode-ai/schema/reference" import { Global } from "./global" import { EventV2 } from "./event" import { Repository } from "./repository" @@ -8,23 +9,14 @@ import { RepositoryCache } from "./repository-cache" import { AbsolutePath } from "./schema" import { State } from "./state" -export class LocalSource extends Schema.Class("Reference.LocalSource")({ - type: Schema.Literal("local"), - path: AbsolutePath, - description: Schema.String.pipe(Schema.optional), - hidden: Schema.Boolean.pipe(Schema.optional), -}) {} +export const LocalSource = Reference.LocalSource +export type LocalSource = Reference.LocalSource -export class GitSource extends Schema.Class("Reference.GitSource")({ - type: Schema.Literal("git"), - repository: Schema.String, - branch: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - hidden: Schema.Boolean.pipe(Schema.optional), -}) {} +export const GitSource = Reference.GitSource +export type GitSource = Reference.GitSource -export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type")) -export type Source = typeof Source.Type +export const Source = Reference.Source +export type Source = Reference.Source export const Event = { Updated: EventV2.define({ type: "reference.updated", schema: {} }), diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 2fadb8e5a2..e878c30fc1 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -176,12 +176,11 @@ export const layer = Layer.effect( ), }).pipe( Effect.map((result) => - result.items.map( - (relative) => - new Entry({ - path: RelativePath.make(relative), - type: "file", - }), + result.items.map((relative) => + Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), ), ), Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), @@ -206,7 +205,7 @@ export const layer = Layer.effect( .replace(/^[\\/]+/u, "") .replaceAll("\\", "/") return Effect.succeed( - new Entry({ + Entry.make({ path: RelativePath.make(relative), type: "file", }), @@ -259,8 +258,8 @@ export const layer = Layer.effect( .replace(/^(?:\.[\\/])+/u, "") .replace(/^[\\/]+/u, "") .replaceAll("\\", "/") - return new Match({ - entry: new Entry({ + return Match.make({ + entry: Entry.make({ path: RelativePath.make(relative), type: "file", }), diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts index 97b24dbda8..0412c7df46 100644 --- a/packages/core/src/schema.ts +++ b/packages/core/src/schema.ts @@ -1,48 +1,24 @@ -import { Option, Schema, SchemaGetter } from "effect" -import { Hash } from "./util/hash" +import { Schema } from "effect" +import { + AbsolutePath, + DateTimeUtcFromMillis, + NonNegativeInt, + optionalOmitUndefined, + PositiveInt, + RelativePath, + withStatics, +} from "@opencode-ai/schema/schema" -export type ExternalID = { - readonly namespace: string - readonly key: string +export { + AbsolutePath, + DateTimeUtcFromMillis, + NonNegativeInt, + optionalOmitUndefined, + PositiveInt, + RelativePath, + withStatics, } -export const externalID = (prefix: string, input: ExternalID) => - `${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}` - -/** - * Integer greater than zero. - */ -export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) - -/** - * Integer greater than or equal to zero. - */ -export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) - -/** - * Relative file path (e.g., `src/components/Button.tsx`). - */ -export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath")) -export type RelativePath = Schema.Schema.Type - -/** - * Absolute file path (e.g., `/home/user/projects/myapp/src/main.ts`). - */ -export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath")) -export type AbsolutePath = Schema.Schema.Type - -/** - * Optional public JSON field that can hold explicit `undefined` on the type - * side but encodes it as an omitted key, matching legacy `JSON.stringify`. - */ -export const optionalOmitUndefined = (schema: S) => - Schema.optionalKey(schema).pipe( - Schema.decodeTo(Schema.optional(schema), { - decode: SchemaGetter.passthrough({ strict: false }), - encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)), - }), - ) - /** * Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable` * until `effect:core/x228my` ("Types.DeepMutable widens unknown to `{}`") lands. @@ -71,22 +47,6 @@ export type DeepMutable = T extends string | number | boolean | bigint | symb ? { -readonly [K in keyof T]: DeepMutable } : T -/** - * Attach static methods to a schema object. Designed to be used with `.pipe()`: - * - * @example - * export const Foo = fooSchema.pipe( - * withStatics((schema) => ({ - * zero: schema.make(0), - * from: Schema.decodeUnknownOption(schema), - * })) - * ) - */ -export const withStatics = - >(methods: (schema: S) => M) => - (schema: S): S & M => - Object.assign(schema, methods(schema)) - /** * Nominal wrapper for scalar types. The class itself is a valid schema — * pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc. diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 5dae699733..888052411a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -2,6 +2,7 @@ export * as SessionV2 from "./session" export * from "./session/schema" import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect" +import { ListAnchor } from "@opencode-ai/schema/session" import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" import { ProjectV2 } from "./project" import { WorkspaceV2 } from "./workspace" @@ -38,12 +39,7 @@ import { SessionInput } from "./session/input" // - by subpath // - by workspace (home is special) -export const ListAnchor = Schema.Struct({ - id: SessionSchema.ID, - time: Schema.Finite, - direction: Schema.Literals(["previous", "next"]), -}) -export type ListAnchor = typeof ListAnchor.Type +export { ListAnchor } const ListInputBase = { workspaceID: WorkspaceV2.ID.pipe(Schema.optional), diff --git a/packages/core/src/session/event.ts b/packages/core/src/session/event.ts index 88ac4aa267..25cf147f80 100644 --- a/packages/core/src/session/event.ts +++ b/packages/core/src/session/event.ts @@ -1,14 +1,14 @@ import { Schema } from "effect" -import { ProviderMetadata, ToolContent } from "@opencode-ai/llm" +import { ProviderMetadata, ToolContent } from "@opencode-ai/schema/llm" +import { Delivery } from "@opencode-ai/schema/session-delivery" import { EventV2 } from "../event" import { ModelV2 } from "../model" -import { NonNegativeInt } from "../schema" -import { V2Schema } from "../v2-schema" +import { DateTimeUtcFromMillis, NonNegativeInt, RelativePath } from "../schema" import { FileAttachment, Prompt } from "./prompt" import { SessionSchema } from "./schema" import { Location } from "../location" -import { RelativePath } from "../schema" import { SessionMessageID } from "./message-id" +import { SessionMessage } from "./message" export { FileAttachment } @@ -22,14 +22,14 @@ export const Source = Schema.Struct({ export type Source = typeof Source.Type const Base = { - timestamp: V2Schema.DateTimeUtcFromMillis, + timestamp: DateTimeUtcFromMillis, sessionID: SessionSchema.ID, } const PromptFields = { ...Base, messageID: SessionMessageID.ID, prompt: Prompt, - delivery: Schema.Literals(["steer", "queue"]), + delivery: Delivery, } const options = { @@ -45,13 +45,8 @@ const stepSettlementOptions = { }, } as const -export const UnknownError = Schema.Struct({ - type: Schema.Literal("unknown"), - message: Schema.String, -}).annotate({ - identifier: "Session.Error.Unknown", -}) -export type UnknownError = typeof UnknownError.Type +export const UnknownError = SessionMessage.UnknownError +export type UnknownError = SessionMessage.UnknownError export const AgentSwitched = EventV2.define({ type: "session.next.agent.switched", diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts index a45a371003..14b613678d 100644 --- a/packages/core/src/session/input.ts +++ b/packages/core/src/session/input.ts @@ -2,10 +2,9 @@ export * as SessionInput from "./input" import { and, asc, eq, isNull, lte } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" +import { Admitted, Delivery } from "@opencode-ai/schema/session-input" import type { Database } from "../database/database" import type { EventV2 } from "../event" -import { NonNegativeInt } from "../schema" -import { V2Schema } from "../v2-schema" import { SessionEvent } from "./event" import { SessionMessage } from "./message" import { Prompt } from "./prompt" @@ -14,24 +13,13 @@ import { SessionInputTable, SessionMessageTable } from "./sql" type DatabaseService = Database.Interface["db"] -export const Delivery = Schema.Literals(["steer", "queue"]) -export type Delivery = typeof Delivery.Type - -export class Admitted extends Schema.Class("SessionInput.Admitted")({ - admittedSeq: NonNegativeInt, - id: SessionMessage.ID, - sessionID: SessionSchema.ID, - prompt: Prompt, - delivery: Delivery, - timeCreated: V2Schema.DateTimeUtcFromMillis, - promotedSeq: NonNegativeInt.pipe(Schema.optional), -}) {} +export { Admitted, Delivery } const decodePrompt = Schema.decodeUnknownSync(Prompt) const encodePrompt = Schema.encodeSync(Prompt) const fromRow = (row: typeof SessionInputTable.$inferSelect): Admitted => - new Admitted({ + Admitted.make({ admittedSeq: row.admitted_seq, id: SessionMessage.ID.make(row.id), sessionID: SessionSchema.ID.make(row.session_id), @@ -76,7 +64,7 @@ export const admit = Effect.fn("SessionInput.admit")(function* ( event.durable === undefined ? Effect.die("Prompt admission event is missing aggregate sequence") : Effect.succeed( - new Admitted({ + Admitted.make({ admittedSeq: event.durable.seq, id: input.id, sessionID: input.sessionID, diff --git a/packages/core/src/session/message-id.ts b/packages/core/src/session/message-id.ts index f06fc0fcd5..75c871c0b3 100644 --- a/packages/core/src/session/message-id.ts +++ b/packages/core/src/session/message-id.ts @@ -1,13 +1,2 @@ export * as SessionMessageID from "./message-id" - -import { Schema } from "effect" -import { withStatics } from "../schema" -import { Identifier } from "../util/identifier" - -export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe( - Schema.brand("Session.Message.ID"), - withStatics((schema) => ({ - create: () => schema.make("msg_" + Identifier.ascending()), - })), -) -export type ID = typeof ID.Type +export { ID } from "@opencode-ai/schema/session-message-id" diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 4ece3c2d19..430172ca83 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -102,7 +102,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { yield* SessionEvent.All.match(event, { "session.next.agent.switched": (event) => { return adapter.appendMessage( - new SessionMessage.AgentSwitched({ + SessionMessage.AgentSwitched.make({ id: event.data.messageID, type: "agent-switched", metadata: event.metadata, @@ -113,7 +113,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { }, "session.next.model.switched": (event) => { return adapter.appendMessage( - new SessionMessage.ModelSwitched({ + SessionMessage.ModelSwitched.make({ id: event.data.messageID, type: "model-switched", metadata: event.metadata, @@ -125,7 +125,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.next.moved": () => Effect.void, "session.next.prompted": (event) => { return adapter.appendMessage( - new SessionMessage.User({ + SessionMessage.User.make({ id: event.data.messageID, type: "user", metadata: event.metadata, @@ -139,7 +139,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.next.prompt.admitted": () => Effect.void, "session.next.context.updated": (event) => adapter.appendMessage( - new SessionMessage.System({ + SessionMessage.System.make({ id: event.data.messageID, type: "system", text: event.data.text, @@ -148,7 +148,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { ), "session.next.synthetic": (event) => { return adapter.appendMessage( - new SessionMessage.Synthetic({ + SessionMessage.Synthetic.make({ sessionID: event.data.sessionID, text: event.data.text, id: event.data.messageID, @@ -159,7 +159,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { }, "session.next.shell.started": (event) => { return adapter.appendMessage( - new SessionMessage.Shell({ + SessionMessage.Shell.make({ id: event.data.messageID, type: "shell", metadata: event.metadata, @@ -194,7 +194,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { ) } yield* adapter.appendMessage( - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: event.data.assistantMessageID, type: "assistant", agent: event.data.agent, @@ -225,7 +225,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.next.text.started": (event) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { draft.content.push( - castDraft(new SessionMessage.AssistantText({ type: "text", id: event.data.textID, text: "" })), + castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })), ) }) }, @@ -245,12 +245,12 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { draft.content.push( castDraft( - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: event.data.callID, name: event.data.name, time: { created: event.data.timestamp }, - state: new SessionMessage.ToolStatePending({ status: "pending", input: "" }), + state: SessionMessage.ToolStatePending.make({ status: "pending", input: "" }), }), ), ) @@ -270,7 +270,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { match.provider = event.data.provider match.time.ran = event.data.timestamp match.state = castDraft( - new SessionMessage.ToolStateRunning({ + SessionMessage.ToolStateRunning.make({ status: "running", input: event.data.input, structured: {}, @@ -300,7 +300,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { } match.time.completed = event.data.timestamp match.state = castDraft( - new SessionMessage.ToolStateCompleted({ + SessionMessage.ToolStateCompleted.make({ status: "completed", input: match.state.input, structured: event.data.structured, @@ -323,7 +323,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { } match.time.completed = event.data.timestamp match.state = castDraft( - new SessionMessage.ToolStateError({ + SessionMessage.ToolStateError.make({ status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, @@ -339,7 +339,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { draft.content.push( castDraft( - new SessionMessage.AssistantReasoning({ + SessionMessage.AssistantReasoning.make({ type: "reasoning", id: event.data.reasoningID, text: "", @@ -369,7 +369,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.next.compaction.delta": () => Effect.void, "session.next.compaction.ended": (event) => { return adapter.appendMessage( - new SessionMessage.Compaction({ + SessionMessage.Compaction.make({ id: event.data.messageID, type: "compaction", metadata: event.metadata, diff --git a/packages/core/src/session/message.ts b/packages/core/src/session/message.ts index c5b621a08d..47f1baac05 100644 --- a/packages/core/src/session/message.ts +++ b/packages/core/src/session/message.ts @@ -1,193 +1,2 @@ export * as SessionMessage from "./message" - -import { Schema } from "effect" -import { ProviderMetadata, ToolContent } from "@opencode-ai/llm" -import { ModelV2 } from "../model" -import { V2Schema } from "../v2-schema" -import { SessionEvent } from "./event" -import { Prompt } from "./prompt" -import { SessionMessageID } from "./message-id" - -export const ID = SessionMessageID.ID -export type ID = typeof ID.Type - -const Base = { - id: ID, - metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - }), -} - -export class AgentSwitched extends Schema.Class("Session.Message.AgentSwitched")({ - ...Base, - type: Schema.Literal("agent-switched"), - agent: SessionEvent.AgentSwitched.data.fields.agent, -}) {} - -export class ModelSwitched extends Schema.Class("Session.Message.ModelSwitched")({ - ...Base, - type: Schema.Literal("model-switched"), - model: ModelV2.Ref, -}) {} - -export class User extends Schema.Class("Session.Message.User")({ - ...Base, - text: Prompt.fields.text, - files: Prompt.fields.files, - agents: Prompt.fields.agents, - type: Schema.Literal("user"), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - }), -}) {} - -export class Synthetic extends Schema.Class("Session.Message.Synthetic")({ - ...Base, - sessionID: SessionEvent.Synthetic.data.fields.sessionID, - text: SessionEvent.Synthetic.data.fields.text, - type: Schema.Literal("synthetic"), -}) {} - -export class System extends Schema.Class("Session.Message.System")({ - ...Base, - type: Schema.Literal("system"), - text: SessionEvent.ContextUpdated.data.fields.text, -}) {} - -export class Shell extends Schema.Class("Session.Message.Shell")({ - ...Base, - type: Schema.Literal("shell"), - callID: SessionEvent.Shell.Started.data.fields.callID, - command: SessionEvent.Shell.Started.data.fields.command, - output: Schema.String, - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), -}) {} - -export class ToolStatePending extends Schema.Class("Session.Message.ToolState.Pending")({ - status: Schema.Literal("pending"), - input: Schema.String, -}) {} - -export class ToolStateRunning extends Schema.Class("Session.Message.ToolState.Running")({ - status: Schema.Literal("running"), - input: Schema.Record(Schema.String, Schema.Unknown), - structured: Schema.Record(Schema.String, Schema.Any), - content: ToolContent.pipe(Schema.Array), -}) {} - -export class ToolStateCompleted extends Schema.Class("Session.Message.ToolState.Completed")({ - status: Schema.Literal("completed"), - input: Schema.Record(Schema.String, Schema.Unknown), - attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional), - content: ToolContent.pipe(Schema.Array), - outputPaths: SessionEvent.Tool.Success.data.fields.outputPaths, - structured: Schema.Record(Schema.String, Schema.Any), - result: SessionEvent.Tool.Success.data.fields.result, -}) {} - -export class ToolStateError extends Schema.Class("Session.Message.ToolState.Error")({ - status: Schema.Literal("error"), - input: Schema.Record(Schema.String, Schema.Unknown), - content: ToolContent.pipe(Schema.Array), - structured: Schema.Record(Schema.String, Schema.Any), - error: SessionEvent.UnknownError, - result: SessionEvent.Tool.Failed.data.fields.result, -}) {} - -export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( - Schema.toTaggedUnion("status"), -) -export type ToolState = Schema.Schema.Type - -export class AssistantTool extends Schema.Class("Session.Message.Assistant.Tool")({ - type: Schema.Literal("tool"), - id: Schema.String, - name: Schema.String, - provider: Schema.Struct({ - executed: Schema.Boolean, - metadata: ProviderMetadata.pipe(Schema.optional), - resultMetadata: ProviderMetadata.pipe(Schema.optional), - }).pipe(Schema.optional), - state: ToolState, - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - ran: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - pruned: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), -}) {} - -export class AssistantText extends Schema.Class("Session.Message.Assistant.Text")({ - type: Schema.Literal("text"), - id: Schema.String, - text: Schema.String, -}) {} - -export class AssistantReasoning extends Schema.Class("Session.Message.Assistant.Reasoning")({ - type: Schema.Literal("reasoning"), - id: Schema.String, - text: Schema.String, - providerMetadata: ProviderMetadata.pipe(Schema.optional), -}) {} - -export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( - Schema.toTaggedUnion("type"), -) -export type AssistantContent = Schema.Schema.Type - -export class Assistant extends Schema.Class("Session.Message.Assistant")({ - ...Base, - type: Schema.Literal("assistant"), - agent: Schema.String, - model: SessionEvent.Step.Started.data.fields.model, - content: AssistantContent.pipe(Schema.Array), - snapshot: Schema.Struct({ - start: Schema.String.pipe(Schema.optional), - end: Schema.String.pipe(Schema.optional), - }).pipe(Schema.optional), - finish: Schema.String.pipe(Schema.optional), - cost: Schema.Finite.pipe(Schema.optional), - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), - }).pipe(Schema.optional), - error: SessionEvent.Step.Failed.data.fields.error.pipe(Schema.optional), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), -}) {} - -export class Compaction extends Schema.Class("Session.Message.Compaction")({ - type: Schema.Literal("compaction"), - reason: SessionEvent.Compaction.Started.data.fields.reason, - summary: Schema.String, - recent: Schema.String, - ...Base, -}) {} - -export const Message = Schema.Union([ - AgentSwitched, - ModelSwitched, - User, - Synthetic, - System, - Shell, - Assistant, - Compaction, -]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Session.Message" }) - -export type Message = Schema.Schema.Type - -export type Type = Message["type"] +export * from "@opencode-ai/schema/session-message" diff --git a/packages/core/src/session/prompt.ts b/packages/core/src/session/prompt.ts index a0653c51e6..b079349c56 100644 --- a/packages/core/src/session/prompt.ts +++ b/packages/core/src/session/prompt.ts @@ -1,46 +1 @@ -import * as Schema from "effect/Schema" - -export class Source extends Schema.Class("Prompt.Source")({ - start: Schema.Finite, - end: Schema.Finite, - text: Schema.String, -}) {} - -export class FileAttachment extends Schema.Class("Prompt.FileAttachment")({ - uri: Schema.String, - mime: Schema.String, - name: Schema.String.pipe(Schema.optional), - description: Schema.String.pipe(Schema.optional), - source: Source.pipe(Schema.optional), -}) { - static create(input: FileAttachment) { - return new FileAttachment({ - uri: input.uri, - mime: input.mime, - name: input.name, - description: input.description, - source: input.source, - }) - } -} - -export class AgentAttachment extends Schema.Class("Prompt.AgentAttachment")({ - name: Schema.String, - source: Source.pipe(Schema.optional), -}) {} - -export class Prompt extends Schema.Class("Prompt")({ - text: Schema.String, - files: Schema.Array(FileAttachment).pipe(Schema.optional), - agents: Schema.Array(AgentAttachment).pipe(Schema.optional), -}) { - static readonly equivalence = Schema.toEquivalence(Prompt) - - static fromUserMessage(input: Pick) { - return new Prompt({ - text: input.text, - ...(input.files === undefined ? {} : { files: input.files }), - ...(input.agents === undefined ? {} : { agents: input.agents }), - }) - } -} +export { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt" diff --git a/packages/core/src/session/schema.ts b/packages/core/src/session/schema.ts index 8509cabee4..05808a2799 100644 --- a/packages/core/src/session/schema.ts +++ b/packages/core/src/session/schema.ts @@ -1,49 +1,9 @@ export * as SessionSchema from "./schema" -import { Schema } from "effect" -import { Location } from "../location" -import { ModelV2 } from "../model" -import { ProjectV2 } from "../project" -import { externalID, type ExternalID, RelativePath, optionalOmitUndefined, withStatics } from "../schema" -import { Identifier } from "../util/identifier" -import { V2Schema } from "../v2-schema" -import { AgentV2 } from "../agent" +import { Session } from "@opencode-ai/schema/session" -export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( - Schema.brand("SessionID"), - withStatics((schema) => { - const create = () => schema.make("ses_" + Identifier.descending()) - return { - create, - descending: (id?: string) => (id === undefined ? create() : schema.make(id)), - fromExternal: (input: ExternalID) => schema.make(externalID("ses", input)), - } - }), -) +export const ID = Session.ID export type ID = typeof ID.Type -export class Info extends Schema.Class("SessionV2.Info")({ - id: ID, - parentID: ID.pipe(optionalOmitUndefined), - projectID: ProjectV2.ID, - agent: AgentV2.ID.pipe(Schema.optional), - model: ModelV2.Ref.pipe(Schema.optional), - cost: Schema.Finite, - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), - }), - time: Schema.Struct({ - created: V2Schema.DateTimeUtcFromMillis, - updated: V2Schema.DateTimeUtcFromMillis, - archived: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), - }), - title: Schema.String, - location: Location.Ref, - subpath: RelativePath.pipe(Schema.optional), -}) {} +export const Info = Session.Info +export type Info = Session.Info diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index b964cfde83..6c1d37f305 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -2,57 +2,29 @@ export * as SkillV2 from "./skill" import path from "path" import { Context, Effect, Layer, Schema, Types } from "effect" +import { Skill } from "@opencode-ai/schema/skill" import { AgentV2 } from "./agent" import { ConfigMarkdown } from "./config/markdown" import { FSUtil } from "./fs-util" import { PermissionV2 } from "./permission" -import { AbsolutePath, withStatics } from "./schema" +import { AbsolutePath } from "./schema" import { SkillDiscovery } from "./skill/discovery" import { State } from "./state" -export class DirectorySource extends Schema.Class("SkillV2.DirectorySource")({ - type: Schema.Literal("directory"), - path: AbsolutePath, -}) {} +export const DirectorySource = Skill.DirectorySource +export type DirectorySource = Skill.DirectorySource -export class UrlSource extends Schema.Class("SkillV2.UrlSource")({ - type: Schema.Literal("url"), - url: Schema.String, -}) {} +export const UrlSource = Skill.UrlSource +export type UrlSource = Skill.UrlSource -export class EmbeddedSource extends Schema.Class("SkillV2.EmbeddedSource")({ - type: Schema.Literal("embedded"), - skill: Schema.suspend(() => Info), -}) {} +export const EmbeddedSource = Skill.EmbeddedSource +export type EmbeddedSource = Skill.EmbeddedSource -export const Source = Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe( - Schema.toTaggedUnion("type"), - Schema.annotate({ identifier: "SkillV2.Source" }), - withStatics(() => ({ - equals: (a: DirectorySource | UrlSource | EmbeddedSource, b: DirectorySource | UrlSource | EmbeddedSource) => { - if (a.type !== b.type) return false - if (a.type === "directory" && b.type === "directory") return a.path === b.path - if (a.type === "url" && b.type === "url") return a.url === b.url - if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name - return false - }, - key: (source: DirectorySource | UrlSource | EmbeddedSource) => - source.type === "directory" - ? `directory:${source.path}` - : source.type === "url" - ? `url:${source.url}` - : `embedded:${source.skill.name}`, - })), -) +export const Source = Skill.Source export type Source = typeof Source.Type -export class Info extends Schema.Class("SkillV2.Info")({ - name: Schema.String, - description: Schema.String.pipe(Schema.optional), - slash: Schema.Boolean.pipe(Schema.optional), - location: AbsolutePath, - content: Schema.String, -}) {} +export const Info = Skill.Info +export type Info = Skill.Info export const available = (skills: ReadonlyArray, agent: AgentV2.Info) => skills.filter((skill) => PermissionV2.evaluate("skill", skill.name, agent.permissions).effect !== "deny") @@ -119,15 +91,13 @@ export const layer = Layer.effect( ? path.basename(filepath, ".md") : undefined if (!name) continue - skills.push( - new Info({ - name, - description: frontmatter.description, - slash: frontmatter.slash, - location: AbsolutePath.make(filepath), - content: markdown.content, - }), - ) + skills.push({ + name, + description: frontmatter.description, + slash: frontmatter.slash, + location: AbsolutePath.make(filepath), + content: markdown.content, + }) } } return skills diff --git a/packages/core/src/tool/glob.ts b/packages/core/src/tool/glob.ts index af0838b9af..ea27dfe889 100644 --- a/packages/core/src/tool/glob.ts +++ b/packages/core/src/tool/glob.ts @@ -79,12 +79,11 @@ export const layer = Layer.effectDiscard( }) .pipe( Effect.map((result) => - result.map( - (entry) => - new FileSystem.Entry({ - ...entry, - path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), - }), + result.map((entry) => + FileSystem.Entry.make({ + ...entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), + }), ), ), ) diff --git a/packages/core/src/tool/grep.ts b/packages/core/src/tool/grep.ts index dd8df87251..ffd59bb489 100644 --- a/packages/core/src/tool/grep.ts +++ b/packages/core/src/tool/grep.ts @@ -102,23 +102,22 @@ export const layer = Layer.effectDiscard( }) .pipe( Effect.map((result) => - result.map( - (match) => - new FileSystem.Match({ - ...match, - entry: new FileSystem.Entry({ - ...match.entry, - path: RelativePath.make( - path.relative( - location.directory, - path.resolve( - info?.type === "Directory" ? target : path.dirname(target), - match.entry.path, - ), + result.map((match) => + FileSystem.Match.make({ + ...match, + entry: FileSystem.Entry.make({ + ...match.entry, + path: RelativePath.make( + path.relative( + location.directory, + path.resolve( + info?.type === "Directory" ? target : path.dirname(target), + match.entry.path, ), ), - }), + ), }), + }), ), ), ) diff --git a/packages/core/src/tool/read-filesystem.ts b/packages/core/src/tool/read-filesystem.ts index 19892efac1..6f54f796f3 100644 --- a/packages/core/src/tool/read-filesystem.ts +++ b/packages/core/src/tool/read-filesystem.ts @@ -335,7 +335,7 @@ export const list = Effect.fn("ReadTool.list")(function* (fs: FSUtil.Interface, const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.void)) const type = info?.type === "Directory" ? "directory" : info?.type === "File" ? "file" : undefined if (!type) return - return new FileSystem.Entry({ + return FileSystem.Entry.make({ path: RelativePath.make(item.name + (type === "directory" ? path.sep : "")), type, }) diff --git a/packages/core/src/util/identifier.ts b/packages/core/src/util/identifier.ts index ba28a351ba..442fbb3983 100644 --- a/packages/core/src/util/identifier.ts +++ b/packages/core/src/util/identifier.ts @@ -1,48 +1 @@ -import { randomBytes } from "crypto" - -export namespace Identifier { - const LENGTH = 26 - - // State for monotonic ID generation - let lastTimestamp = 0 - let counter = 0 - - export function ascending() { - return create(false) - } - - export function descending() { - return create(true) - } - - function randomBase62(length: number): string { - const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - let result = "" - const bytes = randomBytes(length) - for (let i = 0; i < length; i++) { - result += chars[bytes[i] % 62] - } - return result - } - - export function create(descending: boolean, timestamp?: number): string { - const currentTimestamp = timestamp ?? Date.now() - - if (currentTimestamp !== lastTimestamp) { - lastTimestamp = currentTimestamp - counter = 0 - } - counter++ - - let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter) - - now = descending ? ~now : now - - const timeBytes = Buffer.alloc(6) - for (let i = 0; i < 6; i++) { - timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) - } - - return timeBytes.toString("hex") + randomBase62(LENGTH - 12) - } -} +export * as Identifier from "@opencode-ai/schema/identifier" diff --git a/packages/core/src/v2-schema.ts b/packages/core/src/v2-schema.ts index a34b0b1516..4dfd51d92b 100644 --- a/packages/core/src/v2-schema.ts +++ b/packages/core/src/v2-schema.ts @@ -1,10 +1,3 @@ -import { DateTime, Schema, SchemaGetter } from "effect" - -export const DateTimeUtcFromMillis = Schema.Finite.pipe( - Schema.decodeTo(Schema.DateTimeUtc, { - decode: SchemaGetter.transform((value) => DateTime.makeUnsafe(value)), - encode: SchemaGetter.transform((value) => DateTime.toEpochMillis(value)), - }), -) - export * as V2Schema from "./v2-schema" + +export { DateTimeUtcFromMillis } from "@opencode-ai/schema/schema" diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index 30d33abbee..d85bbe4ba4 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -1,18 +1,6 @@ export * as WorkspaceV2 from "./workspace" -import { Schema } from "effect" -import { withStatics } from "./schema" -import { Identifier } from "./util/identifier" +import { Workspace } from "@opencode-ai/schema/workspace" -export const ID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( - Schema.brand("WorkspaceV2.ID"), - withStatics((schema) => ({ - ascending: (id?: string) => { - if (!id) return schema.make("wrk_" + Identifier.ascending()) - if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`) - return schema.make(id) - }, - create: () => schema.make("wrk_" + Identifier.ascending()), - })), -) +export const ID = Workspace.ID export type ID = typeof ID.Type diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 1fe1787672..23a3a0f4aa 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -61,7 +61,7 @@ describe("CatalogV2", () => { yield* credentials.create({ integrationID, label: "First", - value: new Credential.Key({ type: "key", key: "first", metadata: { tenant: "one" } }), + value: Credential.Key.make({ type: "key", key: "first", metadata: { tenant: "one" } }), }) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) @@ -69,7 +69,7 @@ describe("CatalogV2", () => { yield* credentials.create({ integrationID, label: "Second", - value: new Credential.Key({ type: "key", key: "second", metadata: { tenant: "two" } }), + value: Credential.Key.make({ type: "key", key: "second", metadata: { tenant: "two" } }), }) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")]) expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({}) @@ -98,7 +98,7 @@ describe("CatalogV2", () => { yield* (yield* Credential.Service).create({ integrationID, - value: new Credential.Key({ type: "key", key: "secret" }), + value: Credential.Key.make({ type: "key", key: "secret" }), }) expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([providerID]) diff --git a/packages/core/test/command.test.ts b/packages/core/test/command.test.ts index 8c2c5843fe..814da3a787 100644 --- a/packages/core/test/command.test.ts +++ b/packages/core/test/command.test.ts @@ -27,7 +27,7 @@ describe("CommandV2", () => { }) expect(yield* command.get("review")).toEqual( - new CommandV2.Info({ + CommandV2.Info.make({ name: "review", template: "Second", description: "Review code", @@ -39,7 +39,7 @@ describe("CommandV2", () => { }), ) expect(yield* command.list()).toEqual([ - new CommandV2.Info({ + CommandV2.Info.make({ name: "review", template: "Second", description: "Review code", diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index 11707c2026..49df4eeec1 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -59,7 +59,7 @@ Review files`, ) expect(yield* command.list()).toEqual([ - new CommandV2.Info({ + CommandV2.Info.make({ name: "review", template: "Review files", description: "File review", @@ -71,8 +71,8 @@ Review files`, }, subtask: true, }), - new CommandV2.Info({ name: "empty", template: "" }), - new CommandV2.Info({ name: "nested/docs", template: "Write docs" }), + CommandV2.Info.make({ name: "empty", template: "" }), + CommandV2.Info.make({ name: "nested/docs", template: "Write docs" }), ]) }), ), diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index e3cbfd557c..0dc5e3b4d7 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -59,21 +59,21 @@ describe("ConfigSkillPlugin.Plugin", () => { ) expect(sources).toEqual([ - new SkillV2.DirectorySource({ + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skill")), }), - new SkillV2.DirectorySource({ + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/repo/.opencode", "skills")), }), - new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), - new SkillV2.DirectorySource({ + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }), + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join("/home/test", "shared-skills")), }), - new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make("/opt/skills") }), - new SkillV2.UrlSource({ type: "url", url: "https://example.test/skills/" }), + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/opt/skills") }), + SkillV2.UrlSource.make({ type: "url", url: "https://example.test/skills/" }), ]) }), ) diff --git a/packages/core/test/credential.test.ts b/packages/core/test/credential.test.ts index 8c1901acd2..ce3efdb2c8 100644 --- a/packages/core/test/credential.test.ts +++ b/packages/core/test/credential.test.ts @@ -14,7 +14,7 @@ describe("Credential", () => { const created = yield* credentials.create({ integrationID, label: "Work", - value: new Credential.Key({ type: "key", key: "secret" }), + value: Credential.Key.make({ type: "key", key: "secret" }), }) expect(yield* credentials.list(integrationID)).toEqual([created]) @@ -24,7 +24,7 @@ describe("Credential", () => { const replacement = yield* credentials.create({ integrationID, label: "Replacement", - value: new Credential.Key({ type: "key", key: "replacement" }), + value: Credential.Key.make({ type: "key", key: "replacement" }), }) expect(yield* credentials.list(integrationID)).toEqual([replacement]) diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts index 2001de4efe..bd2f2eee98 100644 --- a/packages/core/test/event.test.ts +++ b/packages/core/test/event.test.ts @@ -4,9 +4,8 @@ import { EventV2 } from "@opencode-ai/core/event" import { Database } from "@opencode-ai/core/database/database" import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql" import { Location } from "@opencode-ai/core/location" -import { AbsolutePath } from "@opencode-ai/core/schema" +import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/core/schema" import { WorkspaceV2 } from "@opencode-ai/core/workspace" -import { V2Schema } from "@opencode-ai/core/v2-schema" import { eq } from "drizzle-orm" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" @@ -79,24 +78,11 @@ const SyncTimestamp = EventV2.define({ }, schema: { id: Schema.String, - timestamp: V2Schema.DateTimeUtcFromMillis, + timestamp: DateTimeUtcFromMillis, }, }) describe("EventV2", () => { - it.effect("derives stable namespaced external IDs", () => - Effect.sync(() => { - const input = { namespace: "opencord.agent-input", key: "input-1" } - - expect(EventV2.ID.fromExternal(input)).toBe(EventV2.ID.fromExternal(input)) - expect(EventV2.ID.fromExternal(input)).toMatch(/^evt_[a-f0-9]{64}$/) - expect(EventV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe(EventV2.ID.fromExternal(input)) - expect(EventV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe( - EventV2.ID.fromExternal({ namespace: "a", key: "b:c" }), - ) - }), - ) - it.effect("publishes events with the current location", () => Effect.gen(function* () { const events = yield* EventV2.Service diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts index 1e032d3827..9dd6959f72 100644 --- a/packages/core/test/integration.test.ts +++ b/packages/core/test/integration.test.ts @@ -125,7 +125,7 @@ describe("Integration", () => { expect.objectContaining({ integrationID, label: "Work", - value: new Credential.Key({ type: "key", key: "secret" }), + value: Credential.Key.make({ type: "key", key: "secret" }), }), ]) expect((yield* Fiber.join(updated)).length).toBe(1) @@ -149,7 +149,7 @@ describe("Integration", () => { instructions: "Paste the code", callback: (code: string) => Effect.succeed( - new Credential.OAuth({ + Credential.OAuth.make({ type: "oauth", methodID, access: "access", @@ -175,7 +175,7 @@ describe("Integration", () => { expect.objectContaining({ integrationID, label: "Personal", - value: new Credential.OAuth({ + value: Credential.OAuth.make({ type: "oauth", methodID, access: "access", @@ -238,7 +238,7 @@ describe("Integration", () => { url: "https://example.com/authorize", instructions: "Sign in", callback: Effect.succeed( - new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + Credential.OAuth.make({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), ), }), }), @@ -315,12 +315,12 @@ describe("Integration", () => { const work = yield* credentials.create({ integrationID, label: "Work", - value: new Credential.Key({ type: "key", key: "a" }), + value: Credential.Key.make({ type: "key", key: "a" }), }) const personal = yield* credentials.create({ integrationID, label: "Personal", - value: new Credential.Key({ type: "key", key: "b" }), + value: Credential.Key.make({ type: "key", key: "b" }), }) // Stored credentials and detected env vars appear as connections. diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 5bb064b5f8..14b2ed7378 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -52,14 +52,28 @@ const it = testEffect( ) describe("LocationServiceMap", () => { - it.effect("compares equivalent location refs by value", () => - Effect.sync(() => { - const directory = AbsolutePath.make("/project") - expect(Equal.equals(Location.Ref.make({ directory }), Location.Ref.make({ directory }))).toBe(true) - expect(Hash.hash(Location.Ref.make({ directory }))).toBe( - Hash.hash(Location.Ref.make({ directory, workspaceID: undefined })), - ) - }), + it.live("reuses cached services for constructed and decoded location refs", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((dir) => + Effect.scoped( + Effect.gen(function* () { + const locations = yield* LocationServiceMap + const directory = AbsolutePath.make(dir.path) + const constructed = Location.Ref.make({ directory }) + const decoded = Schema.decodeUnknownSync(Location.Ref)({ directory }) + + expect(constructed).toEqual({ directory, workspaceID: undefined }) + expect(decoded).toEqual(constructed) + expect(Equal.equals(constructed, decoded)).toBe(true) + expect(Hash.hash(constructed)).toBe(Hash.hash(decoded)) + expect(yield* locations.contextEffect(constructed)).toBe(yield* locations.contextEffect(decoded)) + }), + ), + ), + ), ) it.live("isolates location state while sharing location policy with catalog", () => diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index ba99ebfe5f..5ec8c16bd0 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -176,12 +176,11 @@ export function integrationHost(integration: Integration.Interface): PluginConte return { ...authorization, callback: authorization.callback.pipe( - Effect.map( - (credential) => - new Credential.OAuth({ - ...credential, - methodID: Integration.MethodID.make(credential.methodID), - }), + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), ), ), } @@ -190,12 +189,11 @@ export function integrationHost(integration: Integration.Interface): PluginConte ...authorization, callback: (code: string) => authorization.callback(code).pipe( - Effect.map( - (credential) => - new Credential.OAuth({ - ...credential, - methodID: Integration.MethodID.make(credential.methodID), - }), + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), ), ), } @@ -205,12 +203,11 @@ export function integrationHost(integration: Integration.Interface): PluginConte ? { refresh: (value: Credential.OAuth) => refresh(value).pipe( - Effect.map( - (next) => - new Credential.OAuth({ - ...next, - methodID: Integration.MethodID.make(next.methodID), - }), + Effect.map((next) => + Credential.OAuth.make({ + ...next, + methodID: Integration.MethodID.make(next.methodID), + }), ), ), } diff --git a/packages/core/test/plugin/provider-alibaba.test.ts b/packages/core/test/plugin/provider-alibaba.test.ts index cda7b23e2d..25d46bf83c 100644 --- a/packages/core/test/plugin/provider-alibaba.test.ts +++ b/packages/core/test/plugin/provider-alibaba.test.ts @@ -26,7 +26,7 @@ describe("AlibabaPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, }), @@ -43,7 +43,7 @@ describe("AlibabaPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, }), @@ -60,7 +60,7 @@ describe("AlibabaPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")), api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, }), @@ -79,7 +79,7 @@ describe("AlibabaPlugin", () => { const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service yield* addPlugin() - const item = new ModelV2.Info({ + const item = ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" }, }) diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index b6ef65f1ae..5d24879e49 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -83,7 +83,7 @@ describe("AmazonBedrockPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const bedrock = new ProviderV2.Info({ + const bedrock = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock), api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" }, request: { @@ -114,7 +114,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -139,7 +139,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -173,7 +173,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), @@ -197,7 +197,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -216,7 +216,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -235,7 +235,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -255,7 +255,7 @@ describe("AmazonBedrockPlugin", () => { const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -284,7 +284,7 @@ describe("AmazonBedrockPlugin", () => { const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -312,7 +312,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), api: { id: ModelV2.ID.make("openai.gpt-5.5"), @@ -343,7 +343,7 @@ describe("AmazonBedrockPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), api: { id: ModelV2.ID.make("openai.gpt-5.5"), @@ -355,7 +355,7 @@ describe("AmazonBedrockPlugin", () => { options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" }, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), api: { id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), @@ -376,7 +376,7 @@ describe("AmazonBedrockPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), @@ -407,7 +407,7 @@ describe("AmazonBedrockPlugin", () => { const headers: Array = [] yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), @@ -442,7 +442,7 @@ describe("AmazonBedrockPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -450,7 +450,7 @@ describe("AmazonBedrockPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -458,7 +458,7 @@ describe("AmazonBedrockPlugin", () => { options: { region: "eu-west-1" }, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), @@ -470,7 +470,7 @@ describe("AmazonBedrockPlugin", () => { options: { region: "eu-west-1" }, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -478,7 +478,7 @@ describe("AmazonBedrockPlugin", () => { options: { region: "ap-northeast-1" }, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -503,7 +503,7 @@ describe("AmazonBedrockPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -589,7 +589,7 @@ describe("AmazonBedrockPlugin", () => { yield* addPlugin() for (const item of cases) { yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" }, }), @@ -608,7 +608,7 @@ describe("AmazonBedrockPlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index 8cf8d15555..a4ad4d7950 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -29,7 +29,7 @@ describe("AnthropicPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = new ProviderV2.Info({ + const item = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.anthropic), api: { type: "aisdk", package: "@ai-sdk/anthropic" }, request: { headers: { Existing: "1" }, body: {} }, @@ -64,7 +64,7 @@ describe("AnthropicPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" }, }), @@ -81,7 +81,7 @@ describe("AnthropicPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" }, }), diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index 08e878801a..6b3f6c6a38 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -87,11 +87,11 @@ describe("AzureCognitiveServicesPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = new ProviderV2.Info({ + const azure = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")), api: { type: "aisdk", package: "@ai-sdk/openai-compatible" }, }) - const openai = new ProviderV2.Info({ + const openai = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.openai), api: { type: "aisdk", package: "test-provider" }, }) @@ -120,7 +120,7 @@ describe("AzureCognitiveServicesPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -138,7 +138,7 @@ describe("AzureCognitiveServicesPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -146,7 +146,7 @@ describe("AzureCognitiveServicesPlugin", () => { options: {}, }) const ignored = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -166,7 +166,7 @@ describe("AzureCognitiveServicesPlugin", () => { const sdk = fakeSelectorSdk(calls) yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, }), @@ -174,7 +174,7 @@ describe("AzureCognitiveServicesPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" }, }), @@ -182,7 +182,7 @@ describe("AzureCognitiveServicesPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")), api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index b479435e5e..1d8172854c 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -80,7 +80,7 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = new ProviderV2.Info({ + const azure = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.azure), api: { type: "aisdk", package: "@ai-sdk/azure" }, request: { headers: {}, body: { resourceName: "from-config" } }, @@ -103,7 +103,7 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = new ProviderV2.Info({ + const azure = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.azure), api: { type: "aisdk", package: "@ai-sdk/azure" }, request: { headers: {}, body: { resourceName: "" } }, @@ -124,7 +124,7 @@ describe("AzurePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const azure = new ProviderV2.Info({ + const azure = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.azure), api: { type: "aisdk", package: "@ai-sdk/azure" }, request: { headers: {}, body: { resourceName: " " } }, @@ -147,7 +147,7 @@ describe("AzurePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -166,7 +166,7 @@ describe("AzurePlugin", () => { yield* addPlugin() const exit = yield* aisdk .runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -186,7 +186,7 @@ describe("AzurePlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -204,7 +204,7 @@ describe("AzurePlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -222,7 +222,7 @@ describe("AzurePlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, request: { headers: {}, body: { useCompletionUrls: true } }, @@ -241,7 +241,7 @@ describe("AzurePlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -249,7 +249,7 @@ describe("AzurePlugin", () => { options: {}, }) const ignored = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")), api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }), @@ -272,7 +272,7 @@ describe("AzurePlugin", () => { } yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, }), @@ -280,7 +280,7 @@ describe("AzurePlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")), api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index f741041e54..adcc1779b0 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -64,7 +64,7 @@ describe("CerebrasPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), @@ -90,7 +90,7 @@ describe("CerebrasPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), @@ -115,7 +115,7 @@ describe("CerebrasPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("custom-cerebras"), ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index 5bc8a3d5ff..008b9a46a3 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -116,7 +116,7 @@ describe("CloudflareAIGatewayPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -137,7 +137,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -181,7 +181,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -210,7 +210,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -247,7 +247,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -278,7 +278,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -300,7 +300,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -323,7 +323,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -352,7 +352,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -375,7 +375,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("anthropic/claude-sonnet-4-5"), @@ -410,7 +410,7 @@ describe("CloudflareAIGatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index c911f740f5..d1eeaa581e 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -93,7 +93,7 @@ describe("CloudflareWorkersAIPlugin", () => { yield* addPlugin() const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) const sdk = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), api: { id: ModelV2.ID.make("@cf/model"), ...provider.api }, }), @@ -136,7 +136,7 @@ describe("CloudflareWorkersAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), api: { id: ModelV2.ID.make("@cf/model"), @@ -180,7 +180,7 @@ describe("CloudflareWorkersAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), api: { id: ModelV2.ID.make("@cf/model"), @@ -212,7 +212,7 @@ describe("CloudflareWorkersAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), api: { id: ModelV2.ID.make("@cf/model"), @@ -241,7 +241,7 @@ describe("CloudflareWorkersAIPlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" }, }), @@ -260,7 +260,7 @@ describe("CloudflareWorkersAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), api: { id: ModelV2.ID.make("@cf/model"), diff --git a/packages/core/test/plugin/provider-cohere.test.ts b/packages/core/test/plugin/provider-cohere.test.ts index 0ee465d6f9..c001ad935f 100644 --- a/packages/core/test/plugin/provider-cohere.test.ts +++ b/packages/core/test/plugin/provider-cohere.test.ts @@ -54,7 +54,7 @@ describe("CoherePlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" }, }), @@ -64,7 +64,7 @@ describe("CoherePlugin", () => { expect(ignored.sdk).toBeUndefined() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" }, }), @@ -81,7 +81,7 @@ describe("CoherePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")), api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" }, }), @@ -106,7 +106,7 @@ describe("CoherePlugin", () => { const sdk = fakeSelectorSdk(calls) yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-deepinfra.test.ts b/packages/core/test/plugin/provider-deepinfra.test.ts index d428b9b7e0..9cb9a4866c 100644 --- a/packages/core/test/plugin/provider-deepinfra.test.ts +++ b/packages/core/test/plugin/provider-deepinfra.test.ts @@ -46,7 +46,7 @@ describe("DeepInfraPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, }), @@ -64,7 +64,7 @@ describe("DeepInfraPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, }), @@ -83,7 +83,7 @@ describe("DeepInfraPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, }), @@ -109,7 +109,7 @@ describe("DeepInfraPlugin", () => { yield* Effect.forEach(packages, (item) => Effect.gen(function* () { const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, }), @@ -120,7 +120,7 @@ describe("DeepInfraPlugin", () => { }), ) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, }), @@ -139,7 +139,7 @@ describe("DeepInfraPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const sdkEvent = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")), api: { id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index 548b1a92e9..3ba6e5aae4 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -51,7 +51,7 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, }), @@ -69,7 +69,7 @@ describe("DynamicProviderPlugin", () => { const sdk = { marker: "existing" } yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, }), @@ -86,7 +86,7 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, }), @@ -102,7 +102,7 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(npmEntrypoint(fixtureProviderPath)) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" }, }), @@ -119,7 +119,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin(npmEntrypoint()) const exit = yield* aisdk .language( - new ModelV2.Info({ + ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" }, }), @@ -136,7 +136,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin() const exit = yield* aisdk .language( - new ModelV2.Info({ + ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" }, }), @@ -155,7 +155,7 @@ describe("DynamicProviderPlugin", () => { yield* addPlugin(npmEntrypoint(tmp.entrypoint)) const exit = yield* aisdk .language( - new ModelV2.Info({ + ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" }, }), @@ -172,7 +172,7 @@ describe("DynamicProviderPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const language = yield* aisdk.language( - new ModelV2.Info({ + ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider }, }), diff --git a/packages/core/test/plugin/provider-gateway.test.ts b/packages/core/test/plugin/provider-gateway.test.ts index 9179a0b042..c6a0427cdb 100644 --- a/packages/core/test/plugin/provider-gateway.test.ts +++ b/packages/core/test/plugin/provider-gateway.test.ts @@ -43,7 +43,7 @@ describe("GatewayPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -63,7 +63,7 @@ describe("GatewayPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), api: { id: ModelV2.ID.make("anthropic/claude-sonnet-4"), @@ -89,7 +89,7 @@ describe("GatewayPlugin", () => { for (const modelID of vercelGatewayModels) { const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" }, }), @@ -99,7 +99,7 @@ describe("GatewayPlugin", () => { expect(ignored.sdk).toBeUndefined() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index 03d644132d..beef89ae8c 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -45,7 +45,7 @@ describe("GithubCopilotPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -53,7 +53,7 @@ describe("GithubCopilotPlugin", () => { options: { name: "github-copilot" }, }) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -72,7 +72,7 @@ describe("GithubCopilotPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, }), @@ -90,7 +90,7 @@ describe("GithubCopilotPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, }), @@ -108,7 +108,7 @@ describe("GithubCopilotPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -116,7 +116,7 @@ describe("GithubCopilotPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" }, }), @@ -124,7 +124,7 @@ describe("GithubCopilotPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" }, }), @@ -132,7 +132,7 @@ describe("GithubCopilotPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")), api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" }, }), @@ -140,7 +140,7 @@ describe("GithubCopilotPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")), api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" }, }), @@ -164,7 +164,7 @@ describe("GithubCopilotPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -172,7 +172,7 @@ describe("GithubCopilotPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" }, }), @@ -180,7 +180,7 @@ describe("GithubCopilotPlugin", () => { options: {}, }) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")), api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, }), @@ -228,7 +228,7 @@ describe("GithubCopilotPlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index f7c75a7d1d..f9880844d3 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -68,7 +68,7 @@ describe("GitLabPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, }), @@ -105,7 +105,7 @@ describe("GitLabPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, }), @@ -130,7 +130,7 @@ describe("GitLabPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, }), @@ -171,7 +171,7 @@ describe("GitLabPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, }), @@ -190,7 +190,7 @@ describe("GitLabPlugin", () => { const calls: [string, unknown][] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, request: { @@ -225,7 +225,7 @@ describe("GitLabPlugin", () => { const calls: [string, unknown][] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" }, }), @@ -252,7 +252,7 @@ describe("GitLabPlugin", () => { const calls: [string, unknown][] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, request: { @@ -280,7 +280,7 @@ describe("GitLabPlugin", () => { const calls: [string, unknown][] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, request: { headers: { h: "v" }, body: {} }, diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 511cb7ac05..d94321f3bd 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -116,7 +116,7 @@ describe("GoogleVertexAnthropicPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make("claude-sonnet-4-5"), @@ -142,7 +142,7 @@ describe("GoogleVertexAnthropicPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make("claude-sonnet-4-5"), @@ -165,7 +165,7 @@ describe("GoogleVertexAnthropicPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -184,7 +184,7 @@ describe("GoogleVertexAnthropicPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), @@ -202,7 +202,7 @@ describe("GoogleVertexAnthropicPlugin", () => { yield* addPlugin(GoogleVertexPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin) const sdkResult = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, }), @@ -210,7 +210,7 @@ describe("GoogleVertexAnthropicPlugin", () => { options: { name: "google-vertex", project: "project", location: "us" }, }) const languageResult = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, }), @@ -232,7 +232,7 @@ describe("GoogleVertexAnthropicPlugin", () => { const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")), api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, }), @@ -250,7 +250,7 @@ describe("GoogleVertexAnthropicPlugin", () => { const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index b66ce14356..388528bd65 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -170,7 +170,7 @@ describe("GoogleVertexPlugin", () => { yield* addPlugin() const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), api: { id: ModelV2.ID.make("gemini"), @@ -295,7 +295,7 @@ describe("GoogleVertexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), api: { id: ModelV2.ID.make("gemini"), @@ -343,7 +343,7 @@ describe("GoogleVertexPlugin", () => { Effect.void, () => aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), api: { id: ModelV2.ID.make("gemini"), @@ -374,7 +374,7 @@ describe("GoogleVertexPlugin", () => { const calls: string[] = [] yield* addPlugin() yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index 620f01e9ce..8c10689ca1 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -25,7 +25,7 @@ describe("GooglePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" }, }), @@ -43,7 +43,7 @@ describe("GooglePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" }, }), @@ -60,7 +60,7 @@ describe("GooglePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const sdkEvent = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" }, }), diff --git a/packages/core/test/plugin/provider-groq.test.ts b/packages/core/test/plugin/provider-groq.test.ts index 4f469e95c0..ac96ea98a3 100644 --- a/packages/core/test/plugin/provider-groq.test.ts +++ b/packages/core/test/plugin/provider-groq.test.ts @@ -26,7 +26,7 @@ describe("GroqPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, }), @@ -43,7 +43,7 @@ describe("GroqPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, }), @@ -60,7 +60,7 @@ describe("GroqPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, }), @@ -77,7 +77,7 @@ describe("GroqPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")), api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, }), @@ -102,7 +102,7 @@ describe("GroqPlugin", () => { name: string }) const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("llama-api"), diff --git a/packages/core/test/plugin/provider-mistral.test.ts b/packages/core/test/plugin/provider-mistral.test.ts index 0544d5f50c..b1b1bc0638 100644 --- a/packages/core/test/plugin/provider-mistral.test.ts +++ b/packages/core/test/plugin/provider-mistral.test.ts @@ -26,7 +26,7 @@ describe("MistralPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, }), @@ -43,7 +43,7 @@ describe("MistralPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, }), @@ -66,7 +66,7 @@ describe("MistralPlugin", () => { }), ) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, }), @@ -90,7 +90,7 @@ describe("MistralPlugin", () => { }), ) yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")), api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, }), @@ -114,7 +114,7 @@ describe("MistralPlugin", () => { } yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-openai-compatible.test.ts b/packages/core/test/plugin/provider-openai-compatible.test.ts index 74adb0ef43..4e74a37125 100644 --- a/packages/core/test/plugin/provider-openai-compatible.test.ts +++ b/packages/core/test/plugin/provider-openai-compatible.test.ts @@ -25,7 +25,7 @@ describe("OpenAICompatiblePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const defaulted = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -33,7 +33,7 @@ describe("OpenAICompatiblePlugin", () => { options: { name: "custom" }, }) const disabled = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -51,7 +51,7 @@ describe("OpenAICompatiblePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -74,7 +74,7 @@ describe("OpenAICompatiblePlugin", () => { }), ) yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -94,7 +94,7 @@ describe("OpenAICompatiblePlugin", () => { }) yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index 867de3a481..31a80f9319 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -65,7 +65,7 @@ describe("OpenAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -82,7 +82,7 @@ describe("OpenAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -100,7 +100,7 @@ describe("OpenAIPlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -119,7 +119,7 @@ describe("OpenAIPlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -135,7 +135,7 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = new ProviderV2.Info({ + const item = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.openai), api: { type: "aisdk", package: "@ai-sdk/openai" }, }) @@ -157,7 +157,7 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const item = new ProviderV2.Info({ + const item = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")), api: { type: "aisdk", package: "test-provider" }, }) diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index f7deac17e1..a2d450d31b 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -120,7 +120,7 @@ describe("OpencodePlugin", () => { }) yield* credentials.create({ integrationID: Integration.ID.make("opencode"), - value: new Credential.Key({ + value: Credential.Key.make({ type: "key", key: "secret", metadata: { server: server.url.origin }, @@ -176,11 +176,11 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.opencode), api: { type: "aisdk", package: "test-provider" }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, cost: cost(1), @@ -202,11 +202,11 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.opencode), api: { type: "aisdk", package: "test-provider" }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")), api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" }, cost: cost(0), @@ -228,11 +228,11 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.opencode), api: { type: "aisdk", package: "test-provider" }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")), api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" }, cost: cost(0, 1), @@ -256,11 +256,11 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.opencode), api: { type: "aisdk", package: "test-provider" }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, cost: cost(1), @@ -289,11 +289,11 @@ describe("OpencodePlugin", () => { }) }) yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.opencode), api: { type: "aisdk", package: "test-provider" }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, cost: cost(1), @@ -315,7 +315,7 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.opencode), api: { type: "aisdk", package: "test-provider" }, request: { @@ -323,7 +323,7 @@ describe("OpencodePlugin", () => { body: { apiKey: "configured" }, }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, cost: cost(1), @@ -347,11 +347,11 @@ describe("OpencodePlugin", () => { Effect.gen(function* () { const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => { - const provider = new ProviderV2.Info({ + const provider = ProviderV2.Info.make({ ...ProviderV2.Info.empty(ProviderV2.ID.openai), api: { type: "aisdk", package: "test-provider" }, }) - const model = new ModelV2.Info({ + const model = ModelV2.Info.make({ ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")), api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" }, cost: cost(1), diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index d05f5721c5..953943673f 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -53,7 +53,7 @@ describe("OpenRouterPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), @@ -63,7 +63,7 @@ describe("OpenRouterPlugin", () => { expect(ignored.sdk).toBeUndefined() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-perplexity.test.ts b/packages/core/test/plugin/provider-perplexity.test.ts index 7044b55e5a..a2a3969233 100644 --- a/packages/core/test/plugin/provider-perplexity.test.ts +++ b/packages/core/test/plugin/provider-perplexity.test.ts @@ -39,7 +39,7 @@ describe("PerplexityPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, }), @@ -56,7 +56,7 @@ describe("PerplexityPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, }), @@ -73,7 +73,7 @@ describe("PerplexityPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, }), @@ -90,7 +90,7 @@ describe("PerplexityPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")), api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, }), @@ -108,7 +108,7 @@ describe("PerplexityPlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-sap-ai-core.test.ts b/packages/core/test/plugin/provider-sap-ai-core.test.ts index ab48409902..5056c5dbb7 100644 --- a/packages/core/test/plugin/provider-sap-ai-core.test.ts +++ b/packages/core/test/plugin/provider-sap-ai-core.test.ts @@ -47,7 +47,7 @@ function withEnv(vars: Record, effect: () = } function model(providerID: string) { - return new ModelV2.Info({ + return ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")), api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider }, }) diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index a2b6371a3d..cd67feb40e 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -55,7 +55,7 @@ describe("SnowflakeCortexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" }, }), @@ -73,7 +73,7 @@ describe("SnowflakeCortexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, }), @@ -92,7 +92,7 @@ describe("SnowflakeCortexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, }), @@ -115,7 +115,7 @@ describe("SnowflakeCortexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, }), @@ -134,7 +134,7 @@ describe("SnowflakeCortexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, }), @@ -157,7 +157,7 @@ describe("SnowflakeCortexPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-togetherai.test.ts b/packages/core/test/plugin/provider-togetherai.test.ts index 439a26e66a..56bf7c9642 100644 --- a/packages/core/test/plugin/provider-togetherai.test.ts +++ b/packages/core/test/plugin/provider-togetherai.test.ts @@ -39,7 +39,7 @@ describe("TogetherAIPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -57,7 +57,7 @@ describe("TogetherAIPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -67,7 +67,7 @@ describe("TogetherAIPlugin", () => { expect(ignored.sdk).toBeUndefined() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -85,7 +85,7 @@ describe("TogetherAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -105,7 +105,7 @@ describe("TogetherAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty( ProviderV2.ID.make("togetherai"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), diff --git a/packages/core/test/plugin/provider-venice.test.ts b/packages/core/test/plugin/provider-venice.test.ts index a8a7c1f0bd..3ed6c711c4 100644 --- a/packages/core/test/plugin/provider-venice.test.ts +++ b/packages/core/test/plugin/provider-venice.test.ts @@ -39,7 +39,7 @@ describe("VenicePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -56,7 +56,7 @@ describe("VenicePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -74,7 +74,7 @@ describe("VenicePlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const similar = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -82,7 +82,7 @@ describe("VenicePlugin", () => { options: { name: "venice" }, }) const other = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, }), @@ -101,7 +101,7 @@ describe("VenicePlugin", () => { const calls: string[] = [] yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" }, }), diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 3611172e9c..eb87b15132 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -60,7 +60,7 @@ describe("VercelPlugin", () => { const aisdk = yield* AISDK.Service yield* addPlugin() const event = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" }, }), diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index b5f79e4c09..c4d1a5ece3 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -40,7 +40,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const ignored = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, }), @@ -49,7 +49,7 @@ describe("XAIPlugin", () => { }) const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, }), @@ -69,7 +69,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runSDK({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, }), @@ -89,7 +89,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, }), @@ -110,7 +110,7 @@ describe("XAIPlugin", () => { yield* addPlugin() const result = yield* aisdk.runLanguage({ - model: new ModelV2.Info({ + model: ModelV2.Info.make({ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, }), diff --git a/packages/core/test/reference-guidance.test.ts b/packages/core/test/reference-guidance.test.ts index 621f9a6766..6f11cbc431 100644 --- a/packages/core/test/reference-guidance.test.ts +++ b/packages/core/test/reference-guidance.test.ts @@ -26,7 +26,7 @@ describe("ReferenceGuidance", () => { name: "docs", path: AbsolutePath.make("/docs"), description: "Use for product documentation", - source: new Reference.LocalSource({ + source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs"), description: "Use for product documentation", @@ -63,7 +63,7 @@ describe("ReferenceGuidance", () => { new Reference.Info({ name: "docs", path: AbsolutePath.make("/docs"), - source: new Reference.LocalSource({ type: "local", path: AbsolutePath.make("/docs") }), + source: Reference.LocalSource.make({ type: "local", path: AbsolutePath.make("/docs") }), }), ]), }), diff --git a/packages/core/test/reference.test.ts b/packages/core/test/reference.test.ts index a5a94b9d2b..b245e90fe0 100644 --- a/packages/core/test/reference.test.ts +++ b/packages/core/test/reference.test.ts @@ -18,7 +18,7 @@ describe("Reference", () => { const references = yield* Reference.Service const scope = yield* Scope.make() const path = AbsolutePath.make("/docs") - const source = new Reference.LocalSource({ + const source = Reference.LocalSource.make({ type: "local", path, description: "Use for API documentation", @@ -44,7 +44,7 @@ describe("Reference", () => { Effect.gen(function* () { const references = yield* Reference.Service const repository = Repository.parseRemote("owner/repo") - const source = new Reference.GitSource({ type: "git", repository: "owner/repo", branch: "main" }) + const source = Reference.GitSource.make({ type: "git", repository: "owner/repo", branch: "main" }) yield* references.transform((editor) => editor.add("sdk", source)) expect(yield* references.list()).toEqual([ @@ -67,7 +67,7 @@ describe("Reference", () => { Effect.gen(function* () { const references = yield* Reference.Service const repository = Repository.parseRemote("owner/repo") - const source = new Reference.GitSource({ + const source = Reference.GitSource.make({ type: "git", repository: "owner/repo", description: "Use for SDK implementation details", diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 1ae6c011fc..85c7054213 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -55,21 +55,6 @@ const location = Location.Ref.make({ directory: AbsolutePath.make("/project") }) const id = SessionV2.ID.create() describe("SessionV2.create", () => { - it.effect("derives stable namespaced external IDs", () => - Effect.sync(() => { - const input = { namespace: "opencord.agent-thread", key: "thread-1" } - - expect(SessionV2.ID.fromExternal(input)).toBe(SessionV2.ID.fromExternal(input)) - expect(SessionV2.ID.fromExternal(input)).toMatch(/^ses_[a-f0-9]{64}$/) - expect(SessionV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe( - SessionV2.ID.fromExternal(input), - ) - expect(SessionV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe( - SessionV2.ID.fromExternal({ namespace: "a", key: "b:c" }), - ) - }), - ) - it.effect("creates a fresh projected session when the ID is omitted", () => Effect.gen(function* () { const session = yield* SessionV2.Service @@ -218,7 +203,7 @@ describe("SessionV2.create", () => { const events = yield* EventV2.Service const { db } = yield* Database.Service const created = yield* session.create({ location }) - yield* session.prompt({ sessionID: created.id, prompt: new Prompt({ text: "Hello" }), resume: false }) + yield* session.prompt({ sessionID: created.id, prompt: Prompt.make({ text: "Hello" }), resume: false }) yield* SessionInput.promoteSteers(db, events, created.id, Number.MAX_SAFE_INTEGER) expect( @@ -238,7 +223,7 @@ describe("SessionV2.create", () => { const created = yield* session.create({ id: SessionV2.ID.make("ses_fresh_target_replay"), location }) const admitted = yield* session.prompt({ sessionID: created.id, - prompt: new Prompt({ text: "Replay lifecycle" }), + prompt: Prompt.make({ text: "Replay lifecycle" }), resume: false, }) yield* SessionInput.promoteSteers(sourceDb, sourceEvents, created.id, Number.MAX_SAFE_INTEGER) diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 76c3290a70..277e1d52f0 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -36,7 +36,7 @@ const assistantRow = ( id: _, type, ...data - } = encodeMessage(new SessionMessage.Assistant({ id, type: "assistant", agent: "build", model, content: [], time })) + } = encodeMessage(SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time })) return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data } } @@ -69,7 +69,7 @@ describe("SessionProjector", () => { sessionID, messageID: SessionMessage.ID.make("msg_first"), timestamp: created, - prompt: new Prompt({ text: "first" }), + prompt: Prompt.make({ text: "first" }), delivery: "steer", }, { id: EventV2.ID.make("evt_z") }, @@ -80,7 +80,7 @@ describe("SessionProjector", () => { sessionID, messageID: SessionMessage.ID.make("msg_second"), timestamp: created, - prompt: new Prompt({ text: "second" }), + prompt: Prompt.make({ text: "second" }), delivery: "steer", }, { id: EventV2.ID.make("evt_a") }, @@ -145,7 +145,7 @@ describe("SessionProjector", () => { const admitted = yield* SessionInput.admit(db, events, { id, sessionID, - prompt: new Prompt({ text: "promote me" }), + prompt: Prompt.make({ text: "promote me" }), delivery: "steer", }) if (!admitted) return yield* Effect.die("Prompt admission failed") @@ -154,7 +154,7 @@ describe("SessionProjector", () => { sessionID, timestamp: admitted.timeCreated, messageID: id, - prompt: new Prompt({ text: "promote me" }), + prompt: Prompt.make({ text: "promote me" }), delivery: "steer", }) @@ -334,7 +334,7 @@ describe("SessionProjector", () => { it.effect("does not revive a stale incomplete in-memory assistant projection", () => Effect.gen(function* () { - const stale = new SessionMessage.Assistant({ + const stale = SessionMessage.Assistant.make({ id: SessionMessage.ID.make("msg_assistant_stale"), type: "assistant", agent: "build", @@ -342,7 +342,7 @@ describe("SessionProjector", () => { content: [], time: { created }, }) - const completed = new SessionMessage.Assistant({ + const completed = SessionMessage.Assistant.make({ id: SessionMessage.ID.make("msg_assistant_completed"), type: "assistant", agent: "build", @@ -466,15 +466,15 @@ describe("SessionProjector", () => { Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), ) expect(messages).toEqual([ - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: SessionMessage.ID.make("msg_assistant_completed"), type: "assistant", agent: "build", model, - content: [new SessionMessage.AssistantText({ type: "text", id: "text-stale", text: "" })], + content: [SessionMessage.AssistantText.make({ type: "text", id: "text-stale", text: "" })], time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) }, }), - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: SessionMessage.ID.make("msg_assistant_stale"), type: "assistant", agent: "build", diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 842474396e..1ab6c64c61 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -147,7 +147,7 @@ describe("SessionV2.prompt", () => { const message = yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false, }) @@ -171,8 +171,8 @@ describe("SessionV2.prompt", () => { const fiber = yield* session.events({ sessionID }).pipe(Stream.take(4), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* SessionInput.promoteSteers(db, events, sessionID, Number.MAX_SAFE_INTEGER) const streamed = Array.from(yield* Fiber.join(fiber)) @@ -198,7 +198,7 @@ describe("SessionV2.prompt", () => { const session = yield* SessionV2.Service const message = yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false, }) @@ -217,7 +217,7 @@ describe("SessionV2.prompt", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - const input = { sessionID, prompt: new Prompt({ text: "Fix the failing tests" }), resume: false } + const input = { sessionID, prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false } const first = yield* session.prompt(input) const second = yield* session.prompt(input) @@ -235,7 +235,7 @@ describe("SessionV2.prompt", () => { const input = { sessionID, id: messageID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false, } @@ -255,7 +255,7 @@ describe("SessionV2.prompt", () => { const input = { sessionID, id: messageID, - prompt: new Prompt({ text: "Recover committed prompt" }), + prompt: Prompt.make({ text: "Recover committed prompt" }), resume: false, } const first = yield* session.prompt(input) @@ -276,13 +276,13 @@ describe("SessionV2.prompt", () => { yield* session.prompt({ sessionID, id: messageID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), }) const failure = yield* session .prompt({ sessionID, id: messageID, - prompt: new Prompt({ text: "Delete the failing tests" }), + prompt: Prompt.make({ text: "Delete the failing tests" }), resume: false, }) .pipe(Effect.flip) @@ -301,14 +301,14 @@ describe("SessionV2.prompt", () => { yield* session.prompt({ id: messageID, sessionID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false, }) const failure = yield* session .prompt({ id: messageID, sessionID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), delivery: "queue", resume: false, }) @@ -325,7 +325,7 @@ describe("SessionV2.prompt", () => { const input = { sessionID, id: messageID, - prompt: new Prompt({ text: "Fix the failing tests" }), + prompt: Prompt.make({ text: "Fix the failing tests" }), resume: false, } @@ -344,7 +344,7 @@ describe("SessionV2.prompt", () => { const { db } = yield* Database.Service const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Promote once" }), resume: false }) + yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Promote once" }), resume: false }) yield* Effect.all( [ @@ -368,9 +368,9 @@ describe("SessionV2.prompt", () => { const { db } = yield* Database.Service const session = yield* SessionV2.Service const events = yield* EventV2.Service - const first = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Before cutoff" }), resume: false }) + const first = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Before cutoff" }), resume: false }) const cutoff = first.admittedSeq - const second = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "After cutoff" }), resume: false }) + const second = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "After cutoff" }), resume: false }) yield* SessionInput.promoteSteers(db, events, sessionID, cutoff) @@ -386,7 +386,12 @@ describe("SessionV2.prompt", () => { const session = yield* SessionV2.Service const events = yield* EventV2.Service wakeCalls.length = 0 - yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Replay pending" }), resume: false }) + yield* session.prompt({ + id: messageID, + sessionID, + prompt: Prompt.make({ text: "Replay pending" }), + resume: false, + }) const recorded = yield* db .select() .from(EventTable) @@ -422,7 +427,7 @@ describe("SessionV2.prompt", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - const prompt = new Prompt({ text: "Historical prompt" }) + const prompt = Prompt.make({ text: "Historical prompt" }) yield* events.publish(SessionEvent.Prompted, { sessionID, messageID, @@ -443,7 +448,7 @@ describe("SessionV2.prompt", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - const prompt = new Prompt({ text: "Historical queued prompt" }) + const prompt = Prompt.make({ text: "Historical queued prompt" }) yield* events.publish(SessionEvent.Prompted, { sessionID, messageID, @@ -478,7 +483,7 @@ describe("SessionV2.prompt", () => { .onConflictDoNothing() .run() .pipe(Effect.orDie) - const prompt = new Prompt({ text: "Fix the failing tests" }) + const prompt = Prompt.make({ text: "Fix the failing tests" }) yield* session.prompt({ id: messageID, sessionID, prompt, resume: false }) const failure = yield* session @@ -502,7 +507,7 @@ describe("SessionV2.prompt", () => { }) const failure = yield* session - .prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "Conflicting prompt" }), resume: false }) + .prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "Conflicting prompt" }), resume: false }) .pipe(Effect.flip) expect(failure).toMatchObject({ _tag: "Session.PromptConflictError", sessionID, messageID }) @@ -517,7 +522,7 @@ describe("SessionV2.prompt", () => { executionCalls.length = 0 wakeCalls.length = 0 - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run by default" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run by default" }) }) expect(executionCalls).toEqual([]) expect(wakeCalls).toEqual([sessionID]) @@ -533,7 +538,7 @@ describe("SessionV2.prompt", () => { yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Run explicitly" }), + prompt: Prompt.make({ text: "Run explicitly" }), resume: true, }) @@ -549,7 +554,7 @@ describe("SessionV2.prompt", () => { executionCalls.length = 0 wakeCalls.length = 0 - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Do not run" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Do not run" }), resume: false }) expect(executionCalls).toEqual([]) expect(wakeCalls).toEqual([]) diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index cc515742dd..653a192a62 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -16,7 +16,7 @@ const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat. describe("toLLMMessages", () => { test("omits empty assistant turns", () => { const assistant = (value: string, content: SessionMessage.Assistant["content"]) => - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: id(value), type: "assistant", agent: "build", @@ -27,13 +27,13 @@ describe("toLLMMessages", () => { const messages = toLLMMessages( [ assistant("empty", []), - assistant("empty-text", [new SessionMessage.AssistantText({ type: "text", id: "empty", text: "" })]), + assistant("empty-text", [SessionMessage.AssistantText.make({ type: "text", id: "empty", text: "" })]), assistant("empty-reasoning", [ - new SessionMessage.AssistantReasoning({ type: "reasoning", id: "empty-reasoning", text: "" }), + SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "empty-reasoning", text: "" }), ]), - assistant("text", [new SessionMessage.AssistantText({ type: "text", id: "text", text: "Partial" })]), + assistant("text", [SessionMessage.AssistantText.make({ type: "text", id: "text", text: "Partial" })]), assistant("reasoning", [ - new SessionMessage.AssistantReasoning({ + SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "reasoning", text: "", @@ -48,43 +48,43 @@ describe("toLLMMessages", () => { }) test("maps every top-level V2 Session message type", () => { - const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" }) + const file = FileAttachment.make({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" }) const messages = toLLMMessages( [ - new SessionMessage.AgentSwitched({ + SessionMessage.AgentSwitched.make({ id: id("agent"), type: "agent-switched", agent: "build", time: { created }, }), - new SessionMessage.ModelSwitched({ + SessionMessage.ModelSwitched.make({ id: id("model"), type: "model-switched", model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, time: { created }, }), - new SessionMessage.System({ + SessionMessage.System.make({ id: id("system"), type: "system", text: "Updated context\n\nOther context", time: { created }, }), - new SessionMessage.User({ + SessionMessage.User.make({ id: id("user"), type: "user", text: "Inspect this image", files: [file], - agents: [new AgentAttachment({ name: "build" })], + agents: [AgentAttachment.make({ name: "build" })], time: { created }, }), - new SessionMessage.Synthetic({ + SessionMessage.Synthetic.make({ id: id("synthetic"), type: "synthetic", sessionID: SessionV2.ID.make("ses_translate"), text: "Synthetic context", time: { created }, }), - new SessionMessage.Shell({ + SessionMessage.Shell.make({ id: id("shell"), type: "shell", callID: "shell-1", @@ -92,7 +92,7 @@ describe("toLLMMessages", () => { output: "/project", time: { created, completed: created }, }), - new SessionMessage.Compaction({ + SessionMessage.Compaction.make({ id: id("compaction"), type: "compaction", reason: "auto", @@ -142,31 +142,31 @@ Recent work test("replays durable tool media into canonical tool messages without structured base64", () => { const messages = toLLMMessages( [ - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: id("assistant"), type: "assistant", agent: "build", model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, content: [ - new SessionMessage.AssistantText({ type: "text", id: "text-1", text: "Checking" }), - new SessionMessage.AssistantReasoning({ + SessionMessage.AssistantText.make({ type: "text", id: "text-1", text: "Checking" }), + SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "reasoning-1", text: "Think", providerMetadata: { anthropic: { signature: "sig_1" } }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "pending", name: "read", - state: new SessionMessage.ToolStatePending({ status: "pending", input: '{"path":"README.md"}' }), + state: SessionMessage.ToolStatePending.make({ status: "pending", input: '{"path":"README.md"}' }), time: { created }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "running", name: "read", - state: new SessionMessage.ToolStateRunning({ + state: SessionMessage.ToolStateRunning.make({ status: "running", input: { path: "README.md" }, content: [], @@ -174,11 +174,11 @@ Recent work }), time: { created }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "completed", name: "read", - state: new SessionMessage.ToolStateCompleted({ + state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { path: "README.md" }, content: [ @@ -194,7 +194,7 @@ Recent work }), time: { created, completed: created }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "hosted", name: "web_search", @@ -203,7 +203,7 @@ Recent work metadata: { fake: { continuation: "hosted-call" } }, resultMetadata: { fake: { continuation: "hosted-result" } }, }, - state: new SessionMessage.ToolStateCompleted({ + state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { query: "Effect" }, content: [{ type: "text", text: "Found it" }], @@ -211,12 +211,12 @@ Recent work }), time: { created, completed: created }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "hosted-failed", name: "write", provider: { executed: true, metadata: { fake: { continuation: "failed" } } }, - state: new SessionMessage.ToolStateError({ + state: SessionMessage.ToolStateError.make({ status: "error", input: { path: "README.md" }, content: [], @@ -299,13 +299,13 @@ Recent work test("restores OpenAI encrypted reasoning metadata", () => { const messages = toLLMMessages( [ - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: id("assistant-openai-reasoning"), type: "assistant", agent: "build", model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, content: [ - new SessionMessage.AssistantReasoning({ + SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "reasoning-openai", text: "Think", @@ -330,19 +330,19 @@ Recent work test("drops provider-native continuation metadata after a model switch", () => { const messages = toLLMMessages( [ - new SessionMessage.Assistant({ + SessionMessage.Assistant.make({ id: id("assistant-old-model"), type: "assistant", agent: "build", model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") }, content: [ - new SessionMessage.AssistantReasoning({ + SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "reasoning-old-model", text: "Visible thought", providerMetadata: { anthropic: { signature: "sig_old" } }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "hosted-old-model", name: "web_search", @@ -351,7 +351,7 @@ Recent work metadata: { openai: { itemId: "hosted-old-model" } }, resultMetadata: { openai: { itemId: "hosted-old-model" } }, }, - state: new SessionMessage.ToolStateCompleted({ + state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { query: "Effect" }, content: [], @@ -360,7 +360,7 @@ Recent work }), time: { created, completed: created }, }), - new SessionMessage.AssistantTool({ + SessionMessage.AssistantTool.make({ type: "tool", id: "local-old-model", name: "read", @@ -369,7 +369,7 @@ Recent work metadata: { fake: { call: "old" } }, resultMetadata: { fake: { result: "old" } }, }, - state: new SessionMessage.ToolStateCompleted({ + state: SessionMessage.ToolStateCompleted.make({ status: "completed", input: { path: "README.md" }, content: [], diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts index 8b18b53984..efebe60d04 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/session-runner-model.test.ts @@ -22,7 +22,7 @@ type Api = | { readonly type: "native"; readonly url?: string; readonly settings: Record } const model = (api: Api, variants: ModelV2.Info["variants"] = []) => - new ModelV2.Info({ + ModelV2.Info.make({ id: ModelV2.ID.make("test-model"), providerID: ProviderV2.ID.make("test-provider"), name: "Test model", @@ -79,7 +79,7 @@ describe("SessionRunnerModel", () => { it.effect("uses merged API settings for OpenAI-compatible auth and request defaults", () => Effect.gen(function* () { const resolved = yield* SessionRunnerModel.fromCatalogModel( - new ModelV2.Info({ + ModelV2.Info.make({ ...model({ type: "aisdk", package: "@ai-sdk/openai-compatible", @@ -114,7 +114,7 @@ describe("SessionRunnerModel", () => { options: { reasoningEffort: "high" }, }, ]) - const catalog = new ModelV2.Info({ + const catalog = ModelV2.Info.make({ ...base, request: { ...base.request, options: { ...base.request.options, reasoningEffort: "medium" } }, }) @@ -264,11 +264,11 @@ describe("SessionRunnerModel", () => { it.effect("uses resolved credentials for bearer auth", () => Effect.gen(function* () { const resolved = yield* SessionRunnerModel.fromCatalogModel( - new ModelV2.Info({ + ModelV2.Info.make({ ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), request: { headers: {}, body: {}, generation: {}, options: {} }, }), - new Credential.Key({ type: "key", key: "secret" }), + Credential.Key.make({ type: "key", key: "secret" }), ) const request = LLM.request({ model: resolved, prompt: "Hello" }) const headers = yield* resolved.route.auth.apply({ @@ -285,9 +285,9 @@ describe("SessionRunnerModel", () => { it.effect("prefers stored credentials over configured auth", () => Effect.gen(function* () { - const credential = new Credential.Key({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) + const credential = Credential.Key.make({ type: "key", key: "stored-secret", metadata: { tenant: "work" } }) const resolved = yield* SessionRunnerModel.fromCatalogModel( - new ModelV2.Info({ + ModelV2.Info.make({ ...model({ type: "aisdk", package: "@ai-sdk/openai", url: "https://openai.example/v1" }), request: { headers: {}, body: { apiKey: "configured-secret" }, generation: {}, options: {} }, }), diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index 331c5bb48c..926248c0b0 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -154,7 +154,7 @@ describe("SessionRunnerLLM recorded", () => { const session = yield* SessionV2.Service const prompt = yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Say hello in one short sentence." }), + prompt: Prompt.make({ text: "Say hello in one short sentence." }), resume: false, }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 572c599be7..afb4f9ad1f 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -26,7 +26,6 @@ import { SessionMessage } from "@opencode-ai/core/session/message" import { Prompt } from "@opencode-ai/core/session/prompt" import { SessionProjector } from "@opencode-ai/core/session/projector" import { SessionExecution } from "@opencode-ai/core/session/execution" -import { SessionContextEpoch } from "@opencode-ai/core/session/context-epoch" import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator" import { SessionRunner } from "@opencode-ai/core/session/runner" import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm" @@ -351,7 +350,7 @@ const setupOverflowRecovery = Effect.gen(function* () { response = fragmentFixture("text", "text-earlier", ["Earlier answer"]).completeEvents yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Earlier question ".repeat(700) }), + prompt: Prompt.make({ text: "Earlier question ".repeat(700) }), resume: false, }) yield* session.resume(sessionID) @@ -476,7 +475,7 @@ const verifyEphemeralDeltas = (kind: FragmentKind) => const chunks = Array.from({ length: 32 }, (_, index) => `${index},`) const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks) const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant] - yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false }) const events = yield* EventV2.Service const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow @@ -507,7 +506,7 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) => const prompt = `Fail after ${kind}` const fixture = fragmentFixture(kind, fragmentID(kind, "partial"), ["Partial"]) const failure = providerUnavailable() - yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false }) responseStream = Stream.concat(Stream.fromIterable(fixture.partialEvents), Stream.fail(failure)) expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure) @@ -529,7 +528,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) => const prompt = `Interrupt after ${kind}` const fixture = fragmentFixture(kind, fragmentID(kind, "interrupted"), ["Partial"]) const streamed = yield* Deferred.make() - yield* session.prompt({ sessionID, prompt: new Prompt({ text: prompt }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: prompt }), resume: false }) responseStream = Stream.concat( Stream.fromIterable(fixture.partialEvents), Stream.fromEffect(Deferred.succeed(streamed, undefined)).pipe(Stream.flatMap(() => Stream.never)), @@ -573,7 +572,7 @@ describe("SessionRunnerLLM", () => { }), }), }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Use application context" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Use application context" }), resume: false }) responses = [ [ LLMEvent.stepStart({ index: 0 }), @@ -621,7 +620,7 @@ describe("SessionRunnerLLM", () => { streamStarted = undefined response = [] - const message = yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run automatically" }) }) + const message = yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run automatically" }) }) expect(requests).toHaveLength(1) expect(yield* session.messages({ sessionID })).toMatchObject([ @@ -634,8 +633,8 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) requests.length = 0 responses = undefined @@ -662,7 +661,7 @@ describe("SessionRunnerLLM", () => { const { db } = yield* Database.Service const messageID = SessionMessage.ID.create() systemUnavailable = true - yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 const exit = yield* session.resume(sessionID).pipe(Effect.exit) @@ -680,7 +679,7 @@ describe("SessionRunnerLLM", () => { ).toBeUndefined() systemUnavailable = false - yield* session.prompt({ id: messageID, sessionID, prompt: new Prompt({ text: "First" }) }) + yield* session.prompt({ id: messageID, sessionID, prompt: Prompt.make({ text: "First" }) }) expect(requests).toHaveLength(1) expect(requests[0]?.messages.map((message) => message.role)).toEqual(["user"]) @@ -693,7 +692,7 @@ describe("SessionRunnerLLM", () => { const session = yield* SessionV2.Service const events = yield* EventV2.Service const { db } = yield* Database.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] yield* session.resume(sessionID) @@ -711,7 +710,7 @@ describe("SessionRunnerLLM", () => { .get(), ).toBeUndefined() - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) const exit = yield* session.resume(sessionID).pipe(Effect.exit) expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true) @@ -725,7 +724,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const { db } = yield* Database.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) response = [] yield* session.resume(sessionID) yield* db @@ -734,7 +733,7 @@ describe("SessionRunnerLLM", () => { .where(eq(SessionContextEpochTable.session_id, sessionID)) .run() .pipe(Effect.orDie) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) requests.length = 0 const exit = yield* session.resume(sessionID).pipe(Effect.exit) @@ -749,13 +748,13 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] yield* session.resume(sessionID) systemBaseline = "Changed context" - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ @@ -790,7 +789,7 @@ describe("SessionRunnerLLM", () => { }), ) const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = fragmentFixture("text", "text-build", ["Done"]).completeEvents @@ -816,7 +815,7 @@ describe("SessionRunnerLLM", () => { editor.default(AgentV2.ID.make("reviewer")) }) const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = fragmentFixture("text", "text-reviewer", ["Done"]).completeEvents @@ -845,7 +844,7 @@ describe("SessionRunnerLLM", () => { .run() .pipe(Effect.orDie) const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = fragmentFixture("text", "text-selected", ["Done"]).completeEvents @@ -862,7 +861,7 @@ describe("SessionRunnerLLM", () => { const session = yield* SessionV2.Service const events = yield* EventV2.Service skillBaselines.set(AgentV2.ID.make("build"), "Build skills") - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] @@ -874,7 +873,7 @@ describe("SessionRunnerLLM", () => { timestamp: DateTime.makeUnsafe(1), agent: "reviewer", }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ @@ -905,7 +904,7 @@ describe("SessionRunnerLLM", () => { }) .pipe(Effect.asVoid) }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] @@ -935,7 +934,7 @@ describe("SessionRunnerLLM", () => { }) .pipe(Effect.asVoid) }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] @@ -949,13 +948,13 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] yield* session.resume(sessionID) systemRemoved = true - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "user", "system"]) @@ -971,13 +970,13 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] yield* session.resume(sessionID) systemBaseline = "Changed context" - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) yield* events.publish(SessionEvent.ModelSwitched, { sessionID, @@ -986,7 +985,7 @@ describe("SessionRunnerLLM", () => { model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, }) systemBaseline = "Replacement context" - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false }) yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ @@ -1006,7 +1005,7 @@ describe("SessionRunnerLLM", () => { ]) yield* replaySessionProjection(sessionID) expect(yield* session.messages({ sessionID })).toHaveLength(6) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fourth" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fourth" }), resume: false }) yield* session.resume(sessionID) }), ) @@ -1016,7 +1015,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] @@ -1028,11 +1027,11 @@ describe("SessionRunnerLLM", () => { model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") }, }) systemUnavailable = true - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) systemUnavailable = false systemBaseline = "Replacement context" - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false }) yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ @@ -1048,7 +1047,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] @@ -1069,7 +1068,7 @@ describe("SessionRunnerLLM", () => { recent: "", }) systemBaseline = "Replacement context" - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([ @@ -1077,7 +1076,7 @@ describe("SessionRunnerLLM", () => { ["Replacement context"], ]) yield* replaySessionProjection(sessionID) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false }) yield* session.resume(sessionID) }), ) @@ -1089,7 +1088,7 @@ describe("SessionRunnerLLM", () => { response = fragmentFixture("text", "text-first", ["Earlier answer"]).completeEvents yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Earlier question ".repeat(180) }), + prompt: Prompt.make({ text: "Earlier question ".repeat(180) }), resume: false, }) yield* session.resume(sessionID) @@ -1102,7 +1101,7 @@ describe("SessionRunnerLLM", () => { ] yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Recent exact request ".repeat(180) }), + prompt: Prompt.make({ text: "Recent exact request ".repeat(180) }), resume: false, }) yield* session.resume(sessionID) @@ -1128,7 +1127,7 @@ describe("SessionRunnerLLM", () => { ] yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Newest exact request ".repeat(180) }), + prompt: Prompt.make({ text: "Newest exact request ".repeat(180) }), resume: false, }) yield* session.resume(sessionID) @@ -1156,7 +1155,7 @@ describe("SessionRunnerLLM", () => { fragmentFixture("text", "text-summary", ["## Goal\n- Recover overflow"]).completeEvents, fragmentFixture("text", "text-final", ["Recovered"]).completeEvents, ] - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) yield* session.resume(sessionID) expect(requests).toHaveLength(3) @@ -1186,7 +1185,7 @@ describe("SessionRunnerLLM", () => { fragmentFixture("text", "text-summary", ["## Goal\n- Recover once"]).completeEvents, overflow(), ] - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) yield* session.resume(sessionID) expect(requests).toHaveLength(3) @@ -1214,7 +1213,7 @@ describe("SessionRunnerLLM", () => { fragmentFixture("text", "text-summary", ["## Goal\n- Recover raw overflow"]).completeEvents, fragmentFixture("text", "text-final", ["Recovered"]).completeEvents, ] - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) yield* session.resume(sessionID) expect(requests).toHaveLength(3) @@ -1232,7 +1231,7 @@ describe("SessionRunnerLLM", () => { [LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })], [LLMEvent.providerError({ message: "summary unavailable" })], ] - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) yield* session.resume(sessionID) expect(requests).toHaveLength(2) @@ -1255,7 +1254,7 @@ describe("SessionRunnerLLM", () => { const firstGate = yield* Deferred.make() const summaryGate = yield* Deferred.make() streamGate = firstGate - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) const run = yield* session.resume(sessionID).pipe(Effect.forkChild) while (requests.length < 1) yield* Effect.yieldNow streamGate = summaryGate @@ -1275,13 +1274,13 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First" }), resume: false }) requests.length = 0 response = [] yield* session.resume(sessionID) systemBaseline = "Changed context" - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second" }), resume: false }) yield* session.resume(sessionID) const compactionID = SessionMessage.ID.create() yield* events.publish(SessionEvent.Compaction.Started, { @@ -1299,7 +1298,7 @@ describe("SessionRunnerLLM", () => { recent: "", }) systemUnavailable = true - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Third" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Third" }), resume: false }) yield* session.resume(sessionID) expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Initial context"]) @@ -1311,7 +1310,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Use tools" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Use tools" }), resume: false }) requests.length = 0 responses = undefined @@ -1409,7 +1408,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo this" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo this" }), resume: false }) requests.length = 0 authorizations.length = 0 @@ -1468,7 +1467,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo this" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo this" }), resume: false }) requests.length = 0 responses = [ @@ -1512,7 +1511,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Think first" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Think first" }), resume: false }) requests.length = 0 response = [ @@ -1550,7 +1549,7 @@ describe("SessionRunnerLLM", () => { }, ]) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) response = [] yield* session.resume(sessionID) @@ -1569,7 +1568,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Search first" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Search first" }), resume: false }) requests.length = 0 response = [ @@ -1594,7 +1593,7 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) yield* replaySessionProjection(sessionID) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Continue" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false }) response = [] yield* session.resume(sessionID) @@ -1624,7 +1623,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo five times" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo five times" }), resume: false }) requests.length = 0 executions.length = 0 @@ -1685,7 +1684,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Echo twice" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Echo twice" }), resume: false }) requests.length = 0 executions.length = 0 @@ -1773,7 +1772,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run once" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run once" }), resume: false }) requests.length = 0 responses = undefined @@ -1812,7 +1811,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false }) requests.length = 0 responses = [ @@ -1832,7 +1831,7 @@ describe("SessionRunnerLLM", () => { const first = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Change direction" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Change direction" }) }) yield* Deferred.succeed(streamGate, undefined) yield* Fiber.join(first) streamGate = undefined @@ -1855,7 +1854,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false }) requests.length = 0 responses = [ @@ -1883,7 +1882,7 @@ describe("SessionRunnerLLM", () => { yield* Deferred.await(streamStarted) yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Wait until continuation ends" }), + prompt: Prompt.make({ text: "Wait until continuation ends" }), delivery: "queue", }) yield* Deferred.succeed(streamGate, undefined) @@ -1903,7 +1902,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const { db } = yield* Database.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt current work" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt current work" }), resume: false }) requests.length = 0 responses = [ @@ -1921,7 +1920,7 @@ describe("SessionRunnerLLM", () => { yield* Deferred.await(streamStarted) yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Run after interrupt" }), + prompt: Prompt.make({ text: "Run after interrupt" }), delivery: "queue", }) yield* session.interrupt(sessionID) @@ -1946,7 +1945,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const { db } = yield* Database.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt current work" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt current work" }), resume: false }) requests.length = 0 responses = [ @@ -1964,7 +1963,7 @@ describe("SessionRunnerLLM", () => { yield* Deferred.await(streamStarted) yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Steer after interrupt" }), + prompt: Prompt.make({ text: "Steer after interrupt" }), }) yield* session.interrupt(sessionID) expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" }) @@ -1988,7 +1987,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false }) requests.length = 0 responses = [ @@ -2013,8 +2012,8 @@ describe("SessionRunnerLLM", () => { const first = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue first" }), delivery: "queue" }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue second" }), delivery: "queue" }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue first" }), delivery: "queue" }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue second" }), delivery: "queue" }) yield* Deferred.succeed(streamGate, undefined) yield* Fiber.join(first) streamGate = undefined @@ -2031,10 +2030,10 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start steering" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start steering" }), resume: false }) yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Queue for later" }), + prompt: Prompt.make({ text: "Queue for later" }), delivery: "queue", resume: false, }) @@ -2065,7 +2064,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false }) requests.length = 0 responses = [ @@ -2096,13 +2095,13 @@ describe("SessionRunnerLLM", () => { const first = yield* session.resume(sessionID).pipe(Effect.forkChild) while (requests.length < 1) yield* Effect.yieldNow - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue first" }), delivery: "queue" }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Queue second" }), delivery: "queue" }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue first" }), delivery: "queue" }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Queue second" }), delivery: "queue" }) streamGate = secondGate yield* Deferred.succeed(firstGate, undefined) while (requests.length < 2) yield* Effect.yieldNow - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Steer before next queued input" }) }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Also steer before next queued input" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Steer before next queued input" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Also steer before next queued input" }) }) yield* Deferred.succeed(secondGate, undefined) yield* Fiber.join(first) streamGate = undefined @@ -2130,7 +2129,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false }) requests.length = 0 responses = [ @@ -2150,8 +2149,8 @@ describe("SessionRunnerLLM", () => { const first = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "First steer" }) }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Second steer" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "First steer" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Second steer" }) }) yield* Deferred.succeed(streamGate, undefined) yield* Fiber.join(first) streamGate = undefined @@ -2170,7 +2169,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start working" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start working" }), resume: false }) requests.length = 0 responses = undefined @@ -2181,7 +2180,7 @@ describe("SessionRunnerLLM", () => { const first = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover with this" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover with this" }) }) yield* Deferred.succeed(streamGate, undefined) expect(yield* Fiber.join(first).pipe(Effect.flip)).toBe(streamFailure) @@ -2200,7 +2199,7 @@ describe("SessionRunnerLLM", () => { yield* setup const session = yield* SessionV2.Service const events = yield* EventV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover interrupted tool" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover interrupted tool" }), resume: false }) yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER) const assistantMessageID = SessionMessage.ID.create() yield* events.publish(SessionEvent.Step.Started, { @@ -2262,7 +2261,7 @@ describe("SessionRunnerLLM", () => { const events = yield* EventV2.Service yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Recover interrupted hosted tool" }), + prompt: Prompt.make({ text: "Recover interrupted hosted tool" }), resume: false, }) yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER) @@ -2322,7 +2321,7 @@ describe("SessionRunnerLLM", () => { const events = yield* EventV2.Service yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Recover interrupted tool input" }), + prompt: Prompt.make({ text: "Recover interrupted tool input" }), resume: false, }) yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID, Number.MAX_SAFE_INTEGER) @@ -2360,7 +2359,7 @@ describe("SessionRunnerLLM", () => { const session = yield* SessionV2.Service yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Wait in queue" }), + prompt: Prompt.make({ text: "Wait in queue" }), delivery: "queue", resume: false, }) @@ -2382,7 +2381,7 @@ describe("SessionRunnerLLM", () => { const defect = new Error("fail after prompt promotion") let fail = true yield* events.project(SessionEvent.Prompted, () => (fail ? Effect.die(defect) : Effect.void)) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Recover promoted input" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Recover promoted input" }), resume: false }) expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect) fail = false @@ -2410,7 +2409,7 @@ describe("SessionRunnerLLM", () => { ) yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Run committed promotion" }), + prompt: Prompt.make({ text: "Run committed promotion" }), resume: false, }) @@ -2427,8 +2426,8 @@ describe("SessionRunnerLLM", () => { yield* setup yield* insertSession(otherSessionID) const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Run first" }), resume: false }) - yield* session.prompt({ sessionID: otherSessionID, prompt: new Prompt({ text: "Run second" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Run first" }), resume: false }) + yield* session.prompt({ sessionID: otherSessionID, prompt: Prompt.make({ text: "Run second" }), resume: false }) requests.length = 0 responses = undefined @@ -2454,37 +2453,31 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("bounds external session prompt cache keys", () => + it.effect("bounds 64-character session prompt cache keys", () => Effect.gen(function* () { yield* setup - const externalSessionID = SessionV2.ID.fromExternal({ - namespace: "discord", - key: "thread-one", - }) - const otherExternalSessionID = SessionV2.ID.fromExternal({ - namespace: "discord", - key: "thread-two", - }) - yield* insertSession(externalSessionID) - yield* insertSession(otherExternalSessionID) + const longSessionID = SessionV2.ID.make(`ses_${"a".repeat(64)}`) + const otherLongSessionID = SessionV2.ID.make(`ses_${"b".repeat(64)}`) + yield* insertSession(longSessionID) + yield* insertSession(otherLongSessionID) const session = yield* SessionV2.Service yield* session.prompt({ - sessionID: externalSessionID, - prompt: new Prompt({ text: "Run external session" }), + sessionID: longSessionID, + prompt: Prompt.make({ text: "Run long session" }), resume: false, }) yield* session.prompt({ - sessionID: otherExternalSessionID, - prompt: new Prompt({ text: "Run other external session" }), + sessionID: otherLongSessionID, + prompt: Prompt.make({ text: "Run other long session" }), resume: false, }) requests.length = 0 - yield* session.resume(externalSessionID) - yield* session.resume(otherExternalSessionID) + yield* session.resume(longSessionID) + yield* session.resume(otherLongSessionID) const keys = requests.map((request) => request.providerOptions?.openai?.promptCacheKey) - expect(keys).toEqual([externalSessionID.slice(4), otherExternalSessionID.slice(4)]) + expect(keys).toEqual([longSessionID.slice(4), otherLongSessionID.slice(4)]) expect(keys.every((key) => typeof key === "string" && key.length === 64)).toBe(true) expect(keys[0]).not.toBe(keys[1]) }), @@ -2494,7 +2487,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Retry after failure" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Retry after failure" }), resume: false }) requests.length = 0 responses = undefined @@ -2525,7 +2518,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call missing" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call missing" }), resume: false }) requests.length = 0 responses = [ @@ -2571,7 +2564,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call defect" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call defect" }), resume: false }) requests.length = 0 responses = [ @@ -2620,7 +2613,7 @@ describe("SessionRunnerLLM", () => { questions.ask({ sessionID: context.sessionID, questions: [] }).pipe(Effect.as({}), Effect.orDie), }), }) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Ask then stop" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Ask then stop" }), resume: false }) requests.length = 0 responses = [ @@ -2665,7 +2658,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Settle before failing" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Settle before failing" }), resume: false }) const failure = providerUnavailable() toolExecutionGate = yield* Deferred.make() responseStream = Stream.concat( @@ -2699,7 +2692,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt blocked tool" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt blocked tool" }), resume: false }) executions.length = 0 toolExecutionGate = yield* Deferred.make() responseStream = Stream.concat( @@ -2749,7 +2742,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt provider" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt provider" }), resume: false }) requests.length = 0 response = [] streamGate = yield* Deferred.make() @@ -2772,7 +2765,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Interrupt tool settlement" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Interrupt tool settlement" }), resume: false }) executions.length = 0 toolExecutionGate = yield* Deferred.make() response = [ @@ -2815,7 +2808,7 @@ describe("SessionRunnerLLM", () => { }), ) const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Finish at the limit" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Finish at the limit" }), resume: false }) requests.length = 0 executions.length = 0 @@ -2863,7 +2856,7 @@ describe("SessionRunnerLLM", () => { }), ) const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Start work" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Start work" }), resume: false }) requests.length = 0 executions.length = 0 @@ -2891,7 +2884,7 @@ describe("SessionRunnerLLM", () => { const run = yield* session.resume(sessionID).pipe(Effect.forkChild) yield* Deferred.await(streamStarted) - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Change direction" }) }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Change direction" }) }) yield* Deferred.succeed(streamGate, undefined) yield* Fiber.join(run) streamGate = undefined @@ -2909,7 +2902,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail durably" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail durably" }), resume: false }) requests.length = 0 responses = undefined @@ -2931,7 +2924,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail before step" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail before step" }), resume: false }) requests.length = 0 response = [LLMEvent.providerError({ message: "Provider unavailable" })] @@ -2950,7 +2943,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail after output" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail after output" }), resume: false }) requests.length = 0 response = [ @@ -2979,7 +2972,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail raw stream durably" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail raw stream durably" }), resume: false }) const failure = providerUnavailable() responseStream = Stream.fail(failure) @@ -2998,7 +2991,7 @@ describe("SessionRunnerLLM", () => { const session = yield* SessionV2.Service yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Do not continue failed provider" }), + prompt: Prompt.make({ text: "Do not continue failed provider" }), resume: false, }) @@ -3021,7 +3014,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool durably" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail hosted tool durably" }), resume: false }) requests.length = 0 response = [ @@ -3052,7 +3045,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool at EOF" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail hosted tool at EOF" }), resume: false }) response = [ LLMEvent.stepStart({ index: 0 }), LLMEvent.toolCall({ @@ -3079,7 +3072,7 @@ describe("SessionRunnerLLM", () => { const session = yield* SessionV2.Service yield* session.prompt({ sessionID, - prompt: new Prompt({ text: "Fail hosted tool on raw failure" }), + prompt: Prompt.make({ text: "Fail hosted tool on raw failure" }), resume: false, }) const failure = providerUnavailable() @@ -3114,7 +3107,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Two blocks" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Two blocks" }), resume: false }) responses = undefined streamGate = undefined @@ -3177,7 +3170,7 @@ describe("SessionRunnerLLM", () => { Effect.gen(function* () { yield* setup const session = yield* SessionV2.Service - yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Call provider tool" }), resume: false }) + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Call provider tool" }), resume: false }) responses = undefined streamGate = undefined diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts new file mode 100644 index 0000000000..063202ec7c --- /dev/null +++ b/packages/core/test/shared-schema.test.ts @@ -0,0 +1,183 @@ +import { expect, test } from "bun:test" +import { Schema } from "effect" +import { AgentV2 } from "@opencode-ai/core/agent" +import { ModelV2 } from "@opencode-ai/core/model" +import { SessionV2 } from "@opencode-ai/core/session" +import { Agent } from "@opencode-ai/schema/agent" +import { Location } from "@opencode-ai/schema/location" +import { Model } from "@opencode-ai/schema/model" +import { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt" +import { Provider } from "@opencode-ai/schema/provider" +import { Project } from "@opencode-ai/schema/project" +import { Session } from "@opencode-ai/schema/session" +import { SessionInput } from "@opencode-ai/schema/session-input" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import { Workspace } from "@opencode-ai/schema/workspace" +import { Command } from "@opencode-ai/schema/command" +import { Connection } from "@opencode-ai/schema/connection" +import { Credential } from "@opencode-ai/schema/credential" +import { FileSystem } from "@opencode-ai/schema/filesystem" +import { Integration } from "@opencode-ai/schema/integration" +import { LLM } from "@opencode-ai/schema/llm" +import { ModelRequest } from "@opencode-ai/schema/model-request" +import { Permission } from "@opencode-ai/schema/permission" +import { Reference } from "@opencode-ai/schema/reference" +import { Skill } from "@opencode-ai/schema/skill" +import { AbsolutePath, DateTimeUtcFromMillis } from "@opencode-ai/schema/schema" +import { ProviderV2 } from "@opencode-ai/core/provider" + +test("Core reuses the canonical shared schemas", async () => { + const [ + coreCommand, + coreConnection, + coreCredential, + coreFileSystem, + coreIntegration, + coreLocation, + coreLLM, + coreModelRequest, + corePermission, + coreProject, + coreReference, + coreSessionInput, + coreSessionMessage, + corePrompt, + coreSkill, + coreV2Schema, + coreWorkspace, + ] = await Promise.all([ + import("@opencode-ai/core/command"), + import("@opencode-ai/core/integration/connection"), + import("@opencode-ai/core/credential"), + import("@opencode-ai/core/filesystem/schema"), + import("@opencode-ai/core/integration"), + import("@opencode-ai/core/location"), + import("@opencode-ai/llm"), + import("@opencode-ai/core/model-request"), + import("@opencode-ai/core/permission/schema"), + import("@opencode-ai/core/project/schema"), + import("@opencode-ai/core/reference"), + import("@opencode-ai/core/session/input"), + import("@opencode-ai/core/session/message"), + import("@opencode-ai/core/session/prompt"), + import("@opencode-ai/core/skill"), + import("@opencode-ai/core/v2-schema"), + import("@opencode-ai/core/workspace"), + ]) + + const schemas = [ + [AgentV2.ID, Agent.ID], + [AgentV2.Color, Agent.Color], + [AgentV2.Info, Agent.Info], + [coreCommand.Info, Command.Info], + [coreConnection.CredentialInfo, Connection.CredentialInfo], + [coreConnection.EnvInfo, Connection.EnvInfo], + [coreConnection.Info, Connection.Info], + [coreCredential.ID, Credential.ID], + [coreCredential.OAuth, Credential.OAuth], + [coreCredential.Key, Credential.Key], + [coreCredential.Value, Credential.Value], + [coreFileSystem.Entry, FileSystem.Entry], + [coreFileSystem.Submatch, FileSystem.Submatch], + [coreFileSystem.Match, FileSystem.Match], + [coreIntegration.When, Integration.When], + [coreIntegration.TextPrompt, Integration.TextPrompt], + [coreIntegration.SelectPrompt, Integration.SelectPrompt], + [coreIntegration.Prompt, Integration.Prompt], + [coreIntegration.OAuthMethod, Integration.OAuthMethod], + [coreIntegration.KeyMethod, Integration.KeyMethod], + [coreIntegration.EnvMethod, Integration.EnvMethod], + [coreIntegration.Method, Integration.Method], + [coreIntegration.Inputs, Integration.Inputs], + [coreIntegration.Ref, Integration.Ref], + [coreLocation.Ref, Location.Ref], + [coreLLM.ProviderMetadata, LLM.ProviderMetadata], + [coreLLM.ToolTextContent, LLM.ToolTextContent], + [coreLLM.ToolFileContent, LLM.ToolFileContent], + [coreLLM.ToolContent, LLM.ToolContent], + [ModelV2.ID, Model.ID], + [ModelV2.VariantID, Model.VariantID], + [ModelV2.Ref, Model.Ref], + [ModelV2.Family, Model.Family], + [ModelV2.Capabilities, Model.Capabilities], + [ModelV2.Cost, Model.Cost], + [ModelV2.Api, Model.Api], + [ModelV2.Info, Model.Info], + [ProviderV2.ID, Provider.ID], + [ProviderV2.AISDK, Provider.AISDK], + [ProviderV2.Native, Provider.Native], + [ProviderV2.Api, Provider.Api], + [ProviderV2.Request, Provider.Request], + [ProviderV2.Info, Provider.Info], + [coreModelRequest.Generation, ModelRequest.Generation], + [coreModelRequest.Request, ModelRequest.Request], + [corePermission.Effect, Permission.Effect], + [corePermission.Rule, Permission.Rule], + [corePermission.Ruleset, Permission.Ruleset], + [coreProject.ID, Project.ID], + [coreReference.LocalSource, Reference.LocalSource], + [coreReference.GitSource, Reference.GitSource], + [coreReference.Source, Reference.Source], + [SessionV2.ID, Session.ID], + [SessionV2.Info, Session.Info], + [SessionV2.ListAnchor, Session.ListAnchor], + [coreSessionInput.Delivery, SessionInput.Delivery], + [coreSessionInput.Admitted, SessionInput.Admitted], + [coreSessionMessage.ID, SessionMessage.ID], + [coreSessionMessage.UnknownError, SessionMessage.UnknownError], + [coreSessionMessage.AgentSwitched, SessionMessage.AgentSwitched], + [coreSessionMessage.ModelSwitched, SessionMessage.ModelSwitched], + [coreSessionMessage.User, SessionMessage.User], + [coreSessionMessage.Synthetic, SessionMessage.Synthetic], + [coreSessionMessage.System, SessionMessage.System], + [coreSessionMessage.Shell, SessionMessage.Shell], + [coreSessionMessage.ToolStatePending, SessionMessage.ToolStatePending], + [coreSessionMessage.ToolStateRunning, SessionMessage.ToolStateRunning], + [coreSessionMessage.ToolStateCompleted, SessionMessage.ToolStateCompleted], + [coreSessionMessage.ToolStateError, SessionMessage.ToolStateError], + [coreSessionMessage.ToolState, SessionMessage.ToolState], + [coreSessionMessage.AssistantTool, SessionMessage.AssistantTool], + [coreSessionMessage.AssistantText, SessionMessage.AssistantText], + [coreSessionMessage.AssistantReasoning, SessionMessage.AssistantReasoning], + [coreSessionMessage.AssistantContent, SessionMessage.AssistantContent], + [coreSessionMessage.Assistant, SessionMessage.Assistant], + [coreSessionMessage.Compaction, SessionMessage.Compaction], + [coreSessionMessage.Message, SessionMessage.Message], + [corePrompt.Source, Source], + [corePrompt.FileAttachment, FileAttachment], + [corePrompt.AgentAttachment, AgentAttachment], + [corePrompt.Prompt, Prompt], + [coreSkill.DirectorySource, Skill.DirectorySource], + [coreSkill.UrlSource, Skill.UrlSource], + [coreSkill.EmbeddedSource, Skill.EmbeddedSource], + [coreSkill.Source, Skill.Source], + [coreSkill.Info, Skill.Info], + [coreV2Schema.DateTimeUtcFromMillis, DateTimeUtcFromMillis], + [coreWorkspace.ID, Workspace.ID], + ] + for (const [core, shared] of schemas) expect(core).toBe(shared) + + expect(Agent.Info.empty(Agent.ID.make("test"))).toEqual(AgentV2.Info.empty(AgentV2.ID.make("test"))) + expect(Model.Info.empty(Provider.ID.make("test"), Model.ID.make("model"))).toEqual( + ModelV2.Info.empty(ProviderV2.ID.make("test"), ModelV2.ID.make("model")), + ) + expect(Provider.Info.empty(Provider.ID.make("test"))).toEqual(ProviderV2.Info.empty(ProviderV2.ID.make("test"))) + expect(Skill.Source.key(Skill.DirectorySource.make({ type: "directory", path: AbsolutePath.make("/tmp") }))).toBe( + "directory:/tmp", + ) +}) + +test("shared record schemas construct and decode plain objects", () => { + const made = Prompt.make({ text: "hello" }) + const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" }) + const content = Schema.decodeUnknownSync(SessionMessage.AssistantText)({ type: "text", id: "part_1", text: "hi" }) + + expect(Object.getPrototypeOf(made)).toBe(Object.prototype) + expect(Object.getPrototypeOf(decoded)).toBe(Object.prototype) + expect(Object.getPrototypeOf(content)).toBe(Object.prototype) + expect(Prompt.ast.annotations?.identifier).toBe("Prompt") + expect(SessionMessage.AssistantText.ast.annotations?.identifier).toBe("Session.Message.Assistant.Text") + expect(Prompt.equivalence(Prompt.make({ text: "hello" }), decoded)).toBe(true) + expect(Prompt.fromUserMessage({ text: "hello" })).toEqual(made) + expect(Workspace.ID.ascending("")).toStartWith("wrk_") +}) diff --git a/packages/core/test/skill.test.ts b/packages/core/test/skill.test.ts index 46ee13d120..e42363ec3f 100644 --- a/packages/core/test/skill.test.ts +++ b/packages/core/test/skill.test.ts @@ -74,7 +74,7 @@ describe("SkillV2", () => { { type: "directory", path: AbsolutePath.make(second) }, ]) expect(yield* skill.list()).toEqual([ - new SkillV2.Info({ + SkillV2.Info.make({ name: "foo", slash: true, location: AbsolutePath.make(path.join(first, "foo.md")), diff --git a/packages/core/test/skill/guidance.test.ts b/packages/core/test/skill/guidance.test.ts index 9b35d9083c..4ebd0e4574 100644 --- a/packages/core/test/skill/guidance.test.ts +++ b/packages/core/test/skill/guidance.test.ts @@ -9,18 +9,18 @@ import { SkillGuidance } from "@opencode-ai/core/skill/guidance" import { it } from "../lib/effect" const build = AgentV2.ID.make("build") -const effect = new SkillV2.Info({ +const effect = SkillV2.Info.make({ name: "effect", description: "Build applications with Effect", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Effect guidance", }) -const hidden = new SkillV2.Info({ +const hidden = SkillV2.Info.make({ name: "hidden", location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")), content: "Undescribed guidance", }) -const denied = new SkillV2.Info({ +const denied = SkillV2.Info.make({ name: "denied", description: "Must not be advertised", location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")), @@ -32,7 +32,7 @@ const layer = (list: () => SkillV2.Info[]) => describe("SkillGuidance", () => { it.effect("renders described agent skills and reconciles the complete available list", () => { - const agent = new AgentV2.Info({ + const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [{ action: "skill", resource: "denied", effect: "deny" }], }) @@ -69,7 +69,7 @@ describe("SkillGuidance", () => { }) it.effect("omits guidance when the selected agent denies all skills", () => { - const agent = new AgentV2.Info({ + const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [{ action: "skill", resource: "*", effect: "deny" }], }) @@ -85,7 +85,7 @@ describe("SkillGuidance", () => { }) it.effect("omits guidance when a resource-specific denial follows the global denial", () => { - const agent = new AgentV2.Info({ + const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, @@ -104,7 +104,7 @@ describe("SkillGuidance", () => { }) it.effect("retains specifically allowed skills after a global denial", () => { - const agent = new AgentV2.Info({ + const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, @@ -120,7 +120,7 @@ describe("SkillGuidance", () => { }) it.effect("omits guidance when a specifically allowed skill is denied again", () => { - const agent = new AgentV2.Info({ + const agent = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions: [ { action: "skill", resource: "*", effect: "deny" }, diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index 2848fc37aa..abeb35bf9b 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -122,7 +122,7 @@ describe("SkillTool", () => { }), ).toEqual({ type: "error", value: "Unable to load skill effect" }) deny = false - const flat = new SkillV2.Info({ + const flat = SkillV2.Info.make({ name: "public", description: "Public guidance", location: AbsolutePath.make(path.join(tmp.path, "public.md")), diff --git a/packages/llm/package.json b/packages/llm/package.json index e1a691c0ee..faccd3dd47 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -45,6 +45,7 @@ "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", + "@opencode-ai/schema": "workspace:*", "aws4fetch": "1.0.20", "effect": "catalog:" } diff --git a/packages/llm/src/schema/ids.ts b/packages/llm/src/schema/ids.ts index 61289aa9d0..7eb7409802 100644 --- a/packages/llm/src/schema/ids.ts +++ b/packages/llm/src/schema/ids.ts @@ -1,4 +1,7 @@ import { Schema } from "effect" +import { ProviderMetadata } from "@opencode-ai/schema/llm" + +export { ProviderMetadata } /** Stable string identifier for a protocol implementation. */ export const ProtocolID = Schema.String @@ -38,6 +41,3 @@ export type FinishReason = Schema.Schema.Type export const JsonSchema = Schema.Record(Schema.String, Schema.Unknown) export type JsonSchema = Schema.Schema.Type - -export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)) -export type ProviderMetadata = Schema.Schema.Type diff --git a/packages/llm/src/schema/messages.ts b/packages/llm/src/schema/messages.ts index b160f2d4a4..4a9de3a735 100644 --- a/packages/llm/src/schema/messages.ts +++ b/packages/llm/src/schema/messages.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { ToolContent, ToolFileContent, ToolTextContent } from "@opencode-ai/schema/llm" import { JsonSchema, MessageRole, ProviderMetadata } from "./ids" import { CacheHint, CachePolicy, GenerationOptions, HttpOptions, ModelSchema, ProviderOptions } from "./options" import { isRecord } from "../utils/record" @@ -39,23 +40,7 @@ export const MediaPart = Schema.Struct({ }).annotate({ identifier: "LLM.Content.Media" }) export type MediaPart = Schema.Schema.Type -export const ToolTextContent = Schema.Struct({ - type: Schema.Literal("text"), - text: Schema.String, -}).annotate({ identifier: "Tool.TextContent" }) -export type ToolTextContent = typeof ToolTextContent.Type - -export const ToolFileContent = Schema.Struct({ - type: Schema.Literal("file"), - uri: Schema.String, - mime: Schema.String, - name: Schema.optional(Schema.String), -}).annotate({ identifier: "Tool.FileContent" }) -export type ToolFileContent = typeof ToolFileContent.Type - -/** Ordered, provider-independent content shown to models and UIs after a tool succeeds. */ -export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type")) -export type ToolContent = Schema.Schema.Type +export { ToolContent, ToolFileContent, ToolTextContent } const isToolResultValue = (value: unknown): value is ToolResultValue => isRecord(value) && diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 92d371077f..af2ded7488 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1086,12 +1086,12 @@ export const layer = Layer.effect( } if (part.type === "file") { result.files.push( - new FileAttachment({ + FileAttachment.make({ uri: part.url, mime: part.mime, name: part.filename, source: part.source - ? new Source({ + ? Source.make({ start: part.source.text.start, end: part.source.text.end, text: part.source.text.value, @@ -1102,10 +1102,10 @@ export const layer = Layer.effect( } if (part.type === "agent") { result.agents.push( - new AgentAttachment({ + AgentAttachment.make({ name: part.name, source: part.source - ? new Source({ + ? Source.make({ start: part.source.start, end: part.source.end, text: part.source.value, @@ -1130,7 +1130,7 @@ export const layer = Layer.effect( messageID: SessionMessage.ID.create(), timestamp: DateTime.makeUnsafe(info.time.created), delivery: "steer", - prompt: new Prompt({ + prompt: Prompt.make({ text: nextPrompt.text.join("\n"), files: nextPrompt.files, agents: nextPrompt.agents, diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 9a0679f4fc..ae72bc4d0a 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -23,7 +23,6 @@ import * as HttpSessionError from "../../src/server/routes/instance/httpapi/hand import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" import { Session } from "@/session/session" import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema" -import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@opencode-ai/core/database/database" import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" import { SessionMessage } from "@opencode-ai/core/session/message" @@ -129,7 +128,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri const insertLegacyAssistantMessage = (sessionID: SessionIDType, seq = 1, time = seq) => Effect.gen(function* () { - const message = new SessionMessage.Assistant({ + const message = SessionMessage.Assistant.make({ id: SessionMessage.ID.create(), type: "assistant", agent: "build", diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 0000000000..675f09969c --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/schema", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "effect": "catalog:" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:" + } +} diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts new file mode 100644 index 0000000000..b2e0102efe --- /dev/null +++ b/packages/schema/src/agent.ts @@ -0,0 +1,37 @@ +export * as Agent from "./agent" + +import { Schema } from "effect" +import { Model } from "./model" +import { Permission } from "./permission" +import { Provider } from "./provider" +import { PositiveInt, withStatics } from "./schema" + +export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID")) +export type ID = typeof ID.Type + +export const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) +export type Color = typeof Color.Type + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + id: ID, + model: Model.Ref.pipe(Schema.optional), + request: Provider.Request, + system: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + mode: Schema.Literals(["subagent", "primary", "all"]), + hidden: Schema.Boolean, + color: Color.pipe(Schema.optional), + steps: PositiveInt.pipe(Schema.optional), + permissions: Permission.Ruleset, +}) + .annotate({ identifier: "AgentV2.Info" }) + .pipe( + withStatics((schema) => ({ + empty: (id: ID) => + schema.make({ id, request: { headers: {}, body: {} }, mode: "all", hidden: false, permissions: [] }), + })), + ) diff --git a/packages/schema/src/command.ts b/packages/schema/src/command.ts new file mode 100644 index 0000000000..a038f1e166 --- /dev/null +++ b/packages/schema/src/command.ts @@ -0,0 +1,14 @@ +export * as Command from "./command" + +import { Schema } from "effect" +import { Model } from "./model" + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + name: Schema.String, + template: Schema.String, + description: Schema.String.pipe(Schema.optional), + agent: Schema.String.pipe(Schema.optional), + model: Model.Ref.pipe(Schema.optional), + subtask: Schema.Boolean.pipe(Schema.optional), +}).annotate({ identifier: "CommandV2.Info" }) diff --git a/packages/schema/src/connection.ts b/packages/schema/src/connection.ts new file mode 100644 index 0000000000..2bacb977a8 --- /dev/null +++ b/packages/schema/src/connection.ts @@ -0,0 +1,22 @@ +export * as Connection from "./connection" + +import { Schema } from "effect" +import { Credential } from "./credential" + +export interface CredentialInfo extends Schema.Schema.Type {} +export const CredentialInfo = Schema.Struct({ + type: Schema.Literal("credential"), + id: Credential.ID, + label: Schema.String, +}).annotate({ identifier: "Connection.CredentialInfo" }) + +export interface EnvInfo extends Schema.Schema.Type {} +export const EnvInfo = Schema.Struct({ + type: Schema.Literal("env"), + name: Schema.String, +}).annotate({ identifier: "Connection.EnvInfo" }) + +export const Info = Schema.Union([CredentialInfo, EnvInfo]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Connection.Info" }) +export type Info = typeof Info.Type diff --git a/packages/schema/src/credential.ts b/packages/schema/src/credential.ts new file mode 100644 index 0000000000..a4303a28f9 --- /dev/null +++ b/packages/schema/src/credential.ts @@ -0,0 +1,34 @@ +export * as Credential from "./credential" + +import { Schema } from "effect" +import { Integration } from "./integration" +import { ascending } from "./identifier" +import { NonNegativeInt, withStatics } from "./schema" + +export const ID = Schema.String.pipe( + Schema.brand("Credential.ID"), + withStatics((schema) => ({ create: () => schema.make("cred_" + ascending()) })), +) +export type ID = typeof ID.Type + +export interface OAuth extends Schema.Schema.Type {} +export const OAuth = Schema.Struct({ + type: Schema.Literal("oauth"), + methodID: Integration.MethodID, + refresh: Schema.String, + access: Schema.String, + expires: NonNegativeInt, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), +}).annotate({ identifier: "Credential.OAuth" }) + +export interface Key extends Schema.Schema.Type {} +export const Key = Schema.Struct({ + type: Schema.Literal("key"), + key: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), +}).annotate({ identifier: "Credential.Key" }) + +export const Value = Schema.Union([OAuth, Key]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Credential.Value" }) +export type Value = Schema.Schema.Type diff --git a/packages/schema/src/filesystem.ts b/packages/schema/src/filesystem.ts new file mode 100644 index 0000000000..f638696e61 --- /dev/null +++ b/packages/schema/src/filesystem.ts @@ -0,0 +1,26 @@ +export * as FileSystem from "./filesystem" + +import { Schema } from "effect" +import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" + +export interface Entry extends Schema.Schema.Type {} +export const Entry = Schema.Struct({ + path: RelativePath, + type: Schema.Literals(["file", "directory"]), +}).annotate({ identifier: "FileSystem.Entry" }) + +export interface Submatch extends Schema.Schema.Type {} +export const Submatch = Schema.Struct({ + text: Schema.String, + start: NonNegativeInt, + end: NonNegativeInt, +}) + +export interface Match extends Schema.Schema.Type {} +export const Match = Schema.Struct({ + entry: Entry, + line: PositiveInt, + offset: NonNegativeInt, + text: Schema.String, + submatches: Schema.Array(Submatch), +}).annotate({ identifier: "FileSystem.Match" }) diff --git a/packages/schema/src/identifier.ts b/packages/schema/src/identifier.ts new file mode 100644 index 0000000000..9812a673fb --- /dev/null +++ b/packages/schema/src/identifier.ts @@ -0,0 +1,30 @@ +const length = 26 +const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +let lastTimestamp = 0 +let counter = 0 + +export function ascending() { + return create(false) +} + +export function descending() { + return create(true) +} + +export function create(descending: boolean, timestamp = Date.now()) { + if (timestamp !== lastTimestamp) { + lastTimestamp = timestamp + counter = 0 + } + counter++ + + const current = BigInt(timestamp) * 0x1000n + BigInt(counter) + const value = descending ? ~current : current + const time = Array.from({ length: 6 }, (_, index) => + Number((value >> BigInt(40 - 8 * index)) & 0xffn) + .toString(16) + .padStart(2, "0"), + ).join("") + const bytes = crypto.getRandomValues(new Uint8Array(length - 12)) + return time + Array.from(bytes, (byte) => chars[byte % 62]).join("") +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts new file mode 100644 index 0000000000..9b30f76f72 --- /dev/null +++ b/packages/schema/src/index.ts @@ -0,0 +1,21 @@ +export { Agent } from "./agent" +export { Command } from "./command" +export { Connection } from "./connection" +export { Credential } from "./credential" +export { FileSystem } from "./filesystem" +export { Integration } from "./integration" +export { LLM } from "./llm" +export { Location } from "./location" +export { Model } from "./model" +export { ModelRequest } from "./model-request" +export { Permission } from "./permission" +export { Project } from "./project" +export { Provider } from "./provider" +export { Reference } from "./reference" +export { Session } from "./session" +export { SessionInput } from "./session-input" +export { SessionMessage } from "./session-message" +export { Skill } from "./skill" +export { Workspace } from "./workspace" +export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt" +export * from "./schema" diff --git a/packages/schema/src/integration.ts b/packages/schema/src/integration.ts new file mode 100644 index 0000000000..b30b333a41 --- /dev/null +++ b/packages/schema/src/integration.ts @@ -0,0 +1,79 @@ +export * as Integration from "./integration" + +import { Schema } from "effect" + +export const ID = Schema.String.pipe(Schema.brand("Integration.ID")) +export type ID = typeof ID.Type + +export const MethodID = Schema.String.pipe(Schema.brand("Integration.MethodID")) +export type MethodID = typeof MethodID.Type + +export interface When extends Schema.Schema.Type {} +export const When = Schema.Struct({ + key: Schema.String, + op: Schema.Literals(["eq", "neq"]), + value: Schema.String, +}).annotate({ identifier: "Integration.When" }) + +export interface TextPrompt extends Schema.Schema.Type {} +export const TextPrompt = Schema.Struct({ + type: Schema.Literal("text"), + key: Schema.String, + message: Schema.String, + placeholder: Schema.optional(Schema.String), + when: Schema.optional(When), +}).annotate({ identifier: "Integration.TextPrompt" }) + +export interface SelectPrompt extends Schema.Schema.Type {} +export const SelectPrompt = Schema.Struct({ + type: Schema.Literal("select"), + key: Schema.String, + message: Schema.String, + options: Schema.mutable( + Schema.Array( + Schema.Struct({ + label: Schema.String, + value: Schema.String, + hint: Schema.optional(Schema.String), + }), + ), + ), + when: Schema.optional(When), +}).annotate({ identifier: "Integration.SelectPrompt" }) + +export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type")) +export type Prompt = typeof Prompt.Type + +export interface OAuthMethod extends Schema.Schema.Type {} +export const OAuthMethod = Schema.Struct({ + id: MethodID, + type: Schema.Literal("oauth"), + label: Schema.String, + prompts: Schema.optional(Schema.mutable(Schema.Array(Prompt))), +}).annotate({ identifier: "Integration.OAuthMethod" }) + +export interface KeyMethod extends Schema.Schema.Type {} +export const KeyMethod = Schema.Struct({ + type: Schema.Literal("key"), + label: Schema.optional(Schema.String), +}).annotate({ identifier: "Integration.KeyMethod" }) + +export interface EnvMethod extends Schema.Schema.Type {} +export const EnvMethod = Schema.Struct({ + type: Schema.Literal("env"), + names: Schema.mutable(Schema.Array(Schema.String)), +}).annotate({ identifier: "Integration.EnvMethod" }) + +export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Integration.Method" }) +export type Method = typeof Method.Type + +export const Inputs = Schema.Record(Schema.String, Schema.String).annotate({ identifier: "Integration.Inputs" }) +export type Inputs = typeof Inputs.Type + +export interface Ref extends Schema.Schema.Type {} +export const Ref = Schema.Struct({ + id: ID, + name: Schema.String, +}).annotate({ identifier: "Integration.Ref" }) diff --git a/packages/schema/src/llm.ts b/packages/schema/src/llm.ts new file mode 100644 index 0000000000..545d7dbb51 --- /dev/null +++ b/packages/schema/src/llm.ts @@ -0,0 +1,23 @@ +export * as LLM from "./llm" + +import { Schema } from "effect" + +export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)) +export type ProviderMetadata = Schema.Schema.Type + +export interface ToolTextContent extends Schema.Schema.Type {} +export const ToolTextContent = Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, +}).annotate({ identifier: "Tool.TextContent" }) + +export interface ToolFileContent extends Schema.Schema.Type {} +export const ToolFileContent = Schema.Struct({ + type: Schema.Literal("file"), + uri: Schema.String, + mime: Schema.String, + name: Schema.optional(Schema.String), +}).annotate({ identifier: "Tool.FileContent" }) + +export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type")) +export type ToolContent = Schema.Schema.Type diff --git a/packages/schema/src/location.ts b/packages/schema/src/location.ts new file mode 100644 index 0000000000..970ec7f0a3 --- /dev/null +++ b/packages/schema/src/location.ts @@ -0,0 +1,14 @@ +export * as Location from "./location" + +import { Effect, Schema } from "effect" +import { AbsolutePath } from "./schema" +import { Workspace } from "./workspace" + +export interface Ref extends Schema.Schema.Type {} +export const Ref = Schema.Struct({ + directory: AbsolutePath, + workspaceID: Schema.optional(Workspace.ID).pipe( + Schema.withDecodingDefault(Effect.succeed(undefined)), + Schema.withConstructorDefault(Effect.succeed(undefined)), + ), +}).annotate({ identifier: "Location.Ref" }) diff --git a/packages/schema/src/model-request.ts b/packages/schema/src/model-request.ts new file mode 100644 index 0000000000..d829acca66 --- /dev/null +++ b/packages/schema/src/model-request.ts @@ -0,0 +1,31 @@ +export * as ModelRequest from "./model-request" + +import { Effect, Schema } from "effect" +import { Provider } from "./provider" + +export interface Generation extends Schema.Schema.Type {} +export const Generation = Schema.Struct({ + maxTokens: Schema.Number.pipe(Schema.optional), + temperature: Schema.Number.pipe(Schema.optional), + topP: Schema.Number.pipe(Schema.optional), + topK: Schema.Number.pipe(Schema.optional), + frequencyPenalty: Schema.Number.pipe(Schema.optional), + presencePenalty: Schema.Number.pipe(Schema.optional), + seed: Schema.Number.pipe(Schema.optional), + stop: Schema.String.pipe(Schema.Array, Schema.mutable, Schema.optional), +}) + +export interface Request extends Schema.Schema.Type {} +export const Request = Schema.Struct({ + ...Provider.Request.fields, + generation: Generation.pipe( + Schema.optionalKey, + Schema.withConstructorDefault(Effect.succeed({})), + Schema.withDecodingDefaultKey(Effect.succeed({})), + ), + options: Schema.Record(Schema.String, Schema.Any).pipe( + Schema.optionalKey, + Schema.withConstructorDefault(Effect.succeed({})), + Schema.withDecodingDefaultKey(Effect.succeed({})), + ), +}) diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts new file mode 100644 index 0000000000..62b76df0fc --- /dev/null +++ b/packages/schema/src/model.ts @@ -0,0 +1,104 @@ +export * as Model from "./model" + +import { Schema } from "effect" +import { ModelRequest } from "./model-request" +import { Provider } from "./provider" +import { withStatics } from "./schema" + +export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID")) +export type ID = typeof ID.Type + +export const VariantID = Schema.String.pipe(Schema.brand("VariantID")) +export type VariantID = typeof VariantID.Type + +export const Ref = Schema.Struct({ + id: ID, + providerID: Provider.ID, + variant: VariantID.pipe(Schema.optional), +}) +export type Ref = typeof Ref.Type + +export const Family = Schema.String.pipe(Schema.brand("Family")) +export type Family = typeof Family.Type + +export interface Capabilities extends Schema.Schema.Type {} +export const Capabilities = Schema.Struct({ + tools: Schema.Boolean, + input: Schema.String.pipe(Schema.Array, Schema.mutable), + output: Schema.String.pipe(Schema.Array, Schema.mutable), +}) + +export interface Cost extends Schema.Schema.Type {} +export const Cost = Schema.Struct({ + tier: Schema.Struct({ + type: Schema.Literal("context"), + size: Schema.Int, + }).pipe(Schema.optional), + input: Schema.Finite, + output: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), +}) + +export const Api = Schema.Union([ + Schema.Struct({ + id: ID, + ...Provider.AISDK.fields, + }), + Schema.Struct({ + id: ID, + ...Provider.Native.fields, + }), +]).pipe(Schema.toTaggedUnion("type")) +export type Api = typeof Api.Type + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + id: ID, + providerID: Provider.ID, + family: Family.pipe(Schema.optional), + name: Schema.String, + api: Api, + capabilities: Capabilities, + request: Schema.Struct({ + ...ModelRequest.Request.fields, + variant: Schema.String.pipe(Schema.optional), + }), + variants: Schema.Struct({ + id: VariantID, + ...ModelRequest.Request.fields, + }).pipe(Schema.Array, Schema.mutable), + time: Schema.Struct({ + released: Schema.Finite, + }), + cost: Cost.pipe(Schema.Array, Schema.mutable), + status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), + enabled: Schema.Boolean, + limit: Schema.Struct({ + context: Schema.Int, + input: Schema.Int.pipe(Schema.optional), + output: Schema.Int, + }), +}) + .annotate({ identifier: "ModelV2.Info" }) + .pipe( + withStatics((schema) => ({ + empty: (providerID: Provider.ID, modelID: ID) => + schema.make({ + id: modelID, + providerID, + name: modelID, + api: { id: modelID, type: "native", settings: {} }, + capabilities: { tools: false, input: [], output: [] }, + request: { headers: {}, body: {}, generation: {}, options: {} }, + variants: [], + time: { released: 0 }, + cost: [], + status: "active", + enabled: true, + limit: { context: 0, output: 0 }, + }), + })), + ) diff --git a/packages/schema/src/permission.ts b/packages/schema/src/permission.ts new file mode 100644 index 0000000000..0c3227335a --- /dev/null +++ b/packages/schema/src/permission.ts @@ -0,0 +1,16 @@ +export * as Permission from "./permission" + +import { Schema } from "effect" + +export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "PermissionV2.Effect" }) +export type Effect = typeof Effect.Type + +export interface Rule extends Schema.Schema.Type {} +export const Rule = Schema.Struct({ + action: Schema.String, + resource: Schema.String, + effect: Effect, +}).annotate({ identifier: "PermissionV2.Rule" }) + +export const Ruleset = Schema.mutable(Schema.Array(Rule)).annotate({ identifier: "PermissionV2.Ruleset" }) +export type Ruleset = typeof Ruleset.Type diff --git a/packages/schema/src/project.ts b/packages/schema/src/project.ts new file mode 100644 index 0000000000..3a957a0347 --- /dev/null +++ b/packages/schema/src/project.ts @@ -0,0 +1,10 @@ +export * as Project from "./project" + +import { Schema } from "effect" +import { withStatics } from "./schema" + +export const ID = Schema.String.pipe( + Schema.brand("Project.ID"), + withStatics((schema) => ({ global: schema.make("global") })), +) +export type ID = typeof ID.Type diff --git a/packages/schema/src/prompt.ts b/packages/schema/src/prompt.ts new file mode 100644 index 0000000000..ac7f4275ff --- /dev/null +++ b/packages/schema/src/prompt.ts @@ -0,0 +1,56 @@ +import { Schema } from "effect" +import { withStatics } from "./schema" + +export interface Source extends Schema.Schema.Type {} +export const Source = Schema.Struct({ + start: Schema.Finite, + end: Schema.Finite, + text: Schema.String, +}).annotate({ identifier: "Prompt.Source" }) + +export interface FileAttachment extends Schema.Schema.Type {} +export const FileAttachment = Schema.Struct({ + uri: Schema.String, + mime: Schema.String, + name: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + source: Source.pipe(Schema.optional), +}) + .annotate({ identifier: "Prompt.FileAttachment" }) + .pipe( + withStatics((schema) => ({ + create: (input: FileAttachment) => + schema.make({ + uri: input.uri, + mime: input.mime, + name: input.name, + description: input.description, + source: input.source, + }), + })), + ) + +export interface AgentAttachment extends Schema.Schema.Type {} +export const AgentAttachment = Schema.Struct({ + name: Schema.String, + source: Source.pipe(Schema.optional), +}).annotate({ identifier: "Prompt.AgentAttachment" }) + +export interface Prompt extends Schema.Schema.Type {} +export const Prompt = Schema.Struct({ + text: Schema.String, + files: Schema.Array(FileAttachment).pipe(Schema.optional), + agents: Schema.Array(AgentAttachment).pipe(Schema.optional), +}) + .annotate({ identifier: "Prompt" }) + .pipe( + withStatics((schema) => ({ + equivalence: Schema.toEquivalence(schema), + fromUserMessage: (input: Pick) => + schema.make({ + text: input.text, + ...(input.files === undefined ? {} : { files: input.files }), + ...(input.agents === undefined ? {} : { agents: input.agents }), + }), + })), + ) diff --git a/packages/schema/src/provider.ts b/packages/schema/src/provider.ts new file mode 100644 index 0000000000..d3d5109608 --- /dev/null +++ b/packages/schema/src/provider.ts @@ -0,0 +1,69 @@ +export * as Provider from "./provider" + +import { Schema } from "effect" +import { Integration } from "./integration" +import { withStatics } from "./schema" + +export const ID = Schema.String.pipe( + Schema.brand("ProviderV2.ID"), + withStatics((schema) => ({ + opencode: schema.make("opencode"), + anthropic: schema.make("anthropic"), + openai: schema.make("openai"), + google: schema.make("google"), + googleVertex: schema.make("google-vertex"), + githubCopilot: schema.make("github-copilot"), + amazonBedrock: schema.make("amazon-bedrock"), + azure: schema.make("azure"), + openrouter: schema.make("openrouter"), + mistral: schema.make("mistral"), + gitlab: schema.make("gitlab"), + })), +) +export type ID = typeof ID.Type + +export interface AISDK extends Schema.Schema.Type {} +export const AISDK = Schema.Struct({ + type: Schema.Literal("aisdk"), + package: Schema.String, + url: Schema.String.pipe(Schema.optional), + settings: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) + +export interface Native extends Schema.Schema.Type {} +export const Native = Schema.Struct({ + type: Schema.Literal("native"), + url: Schema.String.pipe(Schema.optional), + settings: Schema.Record(Schema.String, Schema.Unknown), +}) + +export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type")) +export type Api = typeof Api.Type + +export interface Request extends Schema.Schema.Type {} +export const Request = Schema.Struct({ + headers: Schema.Record(Schema.String, Schema.String), + body: Schema.Record(Schema.String, Schema.Any), +}) + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + id: ID, + integrationID: Integration.ID.pipe(Schema.optional), + name: Schema.String, + disabled: Schema.Boolean.pipe(Schema.optional), + api: Api, + request: Request, +}) + .annotate({ identifier: "ProviderV2.Info" }) + .pipe( + withStatics((schema) => ({ + empty: (id: ID) => + schema.make({ + id, + name: id, + api: { type: "native", settings: {} }, + request: { headers: {}, body: {} }, + }), + })), + ) diff --git a/packages/schema/src/reference.ts b/packages/schema/src/reference.ts new file mode 100644 index 0000000000..b0a61cee42 --- /dev/null +++ b/packages/schema/src/reference.ts @@ -0,0 +1,24 @@ +export * as Reference from "./reference" + +import { Schema } from "effect" +import { AbsolutePath } from "./schema" + +export interface LocalSource extends Schema.Schema.Type {} +export const LocalSource = Schema.Struct({ + type: Schema.Literal("local"), + path: AbsolutePath, + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), +}).annotate({ identifier: "Reference.LocalSource" }) + +export interface GitSource extends Schema.Schema.Type {} +export const GitSource = Schema.Struct({ + type: Schema.Literal("git"), + repository: Schema.String, + branch: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), +}).annotate({ identifier: "Reference.GitSource" }) + +export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type")) +export type Source = typeof Source.Type diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts new file mode 100644 index 0000000000..405fd9b606 --- /dev/null +++ b/packages/schema/src/schema.ts @@ -0,0 +1,30 @@ +import { DateTime, Option, Schema, SchemaGetter } from "effect" + +export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) +export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) + +export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath")) +export type RelativePath = typeof RelativePath.Type + +export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath")) +export type AbsolutePath = typeof AbsolutePath.Type + +export const optionalOmitUndefined = (schema: S) => + Schema.optionalKey(schema).pipe( + Schema.decodeTo(Schema.optional(schema), { + decode: SchemaGetter.passthrough({ strict: false }), + encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)), + }), + ) + +export const withStatics = + >(methods: (schema: S) => M) => + (schema: S): S & M => + Object.assign(schema, methods(schema)) + +export const DateTimeUtcFromMillis = Schema.Finite.pipe( + Schema.decodeTo(Schema.DateTimeUtc, { + decode: SchemaGetter.transform((value) => DateTime.makeUnsafe(value)), + encode: SchemaGetter.transform((value) => DateTime.toEpochMillis(value)), + }), +) diff --git a/packages/schema/src/session-delivery.ts b/packages/schema/src/session-delivery.ts new file mode 100644 index 0000000000..9b678dabf9 --- /dev/null +++ b/packages/schema/src/session-delivery.ts @@ -0,0 +1,6 @@ +export * as SessionDelivery from "./session-delivery" + +import { Schema } from "effect" + +export const Delivery = Schema.Literals(["steer", "queue"]) +export type Delivery = typeof Delivery.Type diff --git a/packages/schema/src/session-id.ts b/packages/schema/src/session-id.ts new file mode 100644 index 0000000000..8193f5728c --- /dev/null +++ b/packages/schema/src/session-id.ts @@ -0,0 +1,17 @@ +export * as SessionID from "./session-id" + +import { Schema } from "effect" +import { descending } from "./identifier" +import { withStatics } from "./schema" + +export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( + Schema.brand("SessionID"), + withStatics((schema) => { + const create = () => schema.make("ses_" + descending()) + return { + create, + descending: (id?: string) => (id === undefined ? create() : schema.make(id)), + } + }), +) +export type ID = typeof ID.Type diff --git a/packages/schema/src/session-input.ts b/packages/schema/src/session-input.ts new file mode 100644 index 0000000000..13d7959249 --- /dev/null +++ b/packages/schema/src/session-input.ts @@ -0,0 +1,22 @@ +export * as SessionInput from "./session-input" + +import { Schema } from "effect" +import { Prompt } from "./prompt" +import { DateTimeUtcFromMillis, NonNegativeInt } from "./schema" +import { SessionDelivery } from "./session-delivery" +import { SessionID } from "./session-id" +import { SessionMessageID } from "./session-message-id" + +export const Delivery = SessionDelivery.Delivery +export type Delivery = SessionDelivery.Delivery + +export interface Admitted extends Schema.Schema.Type {} +export const Admitted = Schema.Struct({ + admittedSeq: NonNegativeInt, + id: SessionMessageID.ID, + sessionID: SessionID.ID, + prompt: Prompt, + delivery: Delivery, + timeCreated: DateTimeUtcFromMillis, + promotedSeq: NonNegativeInt.pipe(Schema.optional), +}).annotate({ identifier: "SessionInput.Admitted" }) diff --git a/packages/schema/src/session-message-id.ts b/packages/schema/src/session-message-id.ts new file mode 100644 index 0000000000..ea0d808b76 --- /dev/null +++ b/packages/schema/src/session-message-id.ts @@ -0,0 +1,11 @@ +export * as SessionMessageID from "./session-message-id" + +import { Schema } from "effect" +import { ascending } from "./identifier" +import { withStatics } from "./schema" + +export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe( + Schema.brand("Session.Message.ID"), + withStatics((schema) => ({ create: () => schema.make("msg_" + ascending()) })), +) +export type ID = typeof ID.Type diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts new file mode 100644 index 0000000000..806922332d --- /dev/null +++ b/packages/schema/src/session-message.ts @@ -0,0 +1,204 @@ +export * as SessionMessage from "./session-message" + +import { Schema } from "effect" +import { ProviderMetadata, ToolContent } from "./llm" +import { Model } from "./model" +import { FileAttachment, Prompt } from "./prompt" +import { DateTimeUtcFromMillis } from "./schema" +import { SessionID } from "./session-id" +import { SessionMessageID } from "./session-message-id" + +export const ID = SessionMessageID.ID +export type ID = SessionMessageID.ID + +export interface UnknownError extends Schema.Schema.Type {} +export const UnknownError = Schema.Struct({ + type: Schema.Literal("unknown"), + message: Schema.String, +}).annotate({ identifier: "Session.Error.Unknown" }) + +const Base = { + id: ID, + metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), + time: Schema.Struct({ created: DateTimeUtcFromMillis }), +} + +export interface AgentSwitched extends Schema.Schema.Type {} +export const AgentSwitched = Schema.Struct({ + ...Base, + type: Schema.Literal("agent-switched"), + agent: Schema.String, +}).annotate({ identifier: "Session.Message.AgentSwitched" }) + +export interface ModelSwitched extends Schema.Schema.Type {} +export const ModelSwitched = Schema.Struct({ + ...Base, + type: Schema.Literal("model-switched"), + model: Model.Ref, +}).annotate({ identifier: "Session.Message.ModelSwitched" }) + +export interface User extends Schema.Schema.Type {} +export const User = Schema.Struct({ + ...Base, + text: Prompt.fields.text, + files: Prompt.fields.files, + agents: Prompt.fields.agents, + type: Schema.Literal("user"), +}).annotate({ identifier: "Session.Message.User" }) + +export interface Synthetic extends Schema.Schema.Type {} +export const Synthetic = Schema.Struct({ + ...Base, + sessionID: SessionID.ID, + text: Schema.String, + type: Schema.Literal("synthetic"), +}).annotate({ identifier: "Session.Message.Synthetic" }) + +export interface System extends Schema.Schema.Type {} +export const System = Schema.Struct({ + ...Base, + type: Schema.Literal("system"), + text: Schema.String, +}).annotate({ identifier: "Session.Message.System" }) + +export interface Shell extends Schema.Schema.Type {} +export const Shell = Schema.Struct({ + ...Base, + type: Schema.Literal("shell"), + callID: Schema.String, + command: Schema.String, + output: Schema.String, + time: Schema.Struct({ + created: DateTimeUtcFromMillis, + completed: DateTimeUtcFromMillis.pipe(Schema.optional), + }), +}).annotate({ identifier: "Session.Message.Shell" }) + +export interface ToolStatePending extends Schema.Schema.Type {} +export const ToolStatePending = Schema.Struct({ + status: Schema.Literal("pending"), + input: Schema.String, +}).annotate({ identifier: "Session.Message.ToolState.Pending" }) + +export interface ToolStateRunning extends Schema.Schema.Type {} +export const ToolStateRunning = Schema.Struct({ + status: Schema.Literal("running"), + input: Schema.Record(Schema.String, Schema.Unknown), + structured: Schema.Record(Schema.String, Schema.Any), + content: ToolContent.pipe(Schema.Array), +}).annotate({ identifier: "Session.Message.ToolState.Running" }) + +export interface ToolStateCompleted extends Schema.Schema.Type {} +export const ToolStateCompleted = Schema.Struct({ + status: Schema.Literal("completed"), + input: Schema.Record(Schema.String, Schema.Unknown), + attachments: FileAttachment.pipe(Schema.Array, Schema.optional), + content: ToolContent.pipe(Schema.Array), + outputPaths: Schema.Array(Schema.String).pipe(Schema.optional), + structured: Schema.Record(Schema.String, Schema.Any), + result: Schema.Unknown.pipe(Schema.optional), +}).annotate({ identifier: "Session.Message.ToolState.Completed" }) + +export interface ToolStateError extends Schema.Schema.Type {} +export const ToolStateError = Schema.Struct({ + status: Schema.Literal("error"), + input: Schema.Record(Schema.String, Schema.Unknown), + content: ToolContent.pipe(Schema.Array), + structured: Schema.Record(Schema.String, Schema.Any), + error: UnknownError, + result: Schema.Unknown.pipe(Schema.optional), +}).annotate({ identifier: "Session.Message.ToolState.Error" }) + +export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( + Schema.toTaggedUnion("status"), +) +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export interface AssistantTool extends Schema.Schema.Type {} +export const AssistantTool = Schema.Struct({ + type: Schema.Literal("tool"), + id: Schema.String, + name: Schema.String, + provider: Schema.Struct({ + executed: Schema.Boolean, + metadata: ProviderMetadata.pipe(Schema.optional), + resultMetadata: ProviderMetadata.pipe(Schema.optional), + }).pipe(Schema.optional), + state: ToolState, + time: Schema.Struct({ + created: DateTimeUtcFromMillis, + ran: DateTimeUtcFromMillis.pipe(Schema.optional), + completed: DateTimeUtcFromMillis.pipe(Schema.optional), + pruned: DateTimeUtcFromMillis.pipe(Schema.optional), + }), +}).annotate({ identifier: "Session.Message.Assistant.Tool" }) + +export interface AssistantText extends Schema.Schema.Type {} +export const AssistantText = Schema.Struct({ + type: Schema.Literal("text"), + id: Schema.String, + text: Schema.String, +}).annotate({ identifier: "Session.Message.Assistant.Text" }) + +export interface AssistantReasoning extends Schema.Schema.Type {} +export const AssistantReasoning = Schema.Struct({ + type: Schema.Literal("reasoning"), + id: Schema.String, + text: Schema.String, + providerMetadata: ProviderMetadata.pipe(Schema.optional), +}).annotate({ identifier: "Session.Message.Assistant.Reasoning" }) + +export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe( + Schema.toTaggedUnion("type"), +) +export type AssistantContent = AssistantText | AssistantReasoning | AssistantTool + +export interface Assistant extends Schema.Schema.Type {} +export const Assistant = Schema.Struct({ + ...Base, + type: Schema.Literal("assistant"), + agent: Schema.String, + model: Model.Ref, + content: AssistantContent.pipe(Schema.Array), + snapshot: Schema.Struct({ + start: Schema.String.pipe(Schema.optional), + end: Schema.String.pipe(Schema.optional), + }).pipe(Schema.optional), + finish: Schema.String.pipe(Schema.optional), + cost: Schema.Finite.pipe(Schema.optional), + tokens: Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }), + }).pipe(Schema.optional), + error: UnknownError.pipe(Schema.optional), + time: Schema.Struct({ + created: DateTimeUtcFromMillis, + completed: DateTimeUtcFromMillis.pipe(Schema.optional), + }), +}).annotate({ identifier: "Session.Message.Assistant" }) + +export interface Compaction extends Schema.Schema.Type {} +export const Compaction = Schema.Struct({ + type: Schema.Literal("compaction"), + reason: Schema.Literals(["auto", "manual"]), + summary: Schema.String, + recent: Schema.String, + ...Base, +}).annotate({ identifier: "Session.Message.Compaction" }) + +export const Message = Schema.Union([ + AgentSwitched, + ModelSwitched, + User, + Synthetic, + System, + Shell, + Assistant, + Compaction, +]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Session.Message" }) +export type Message = AgentSwitched | ModelSwitched | User | Synthetic | System | Shell | Assistant | Compaction +export type Type = Message["type"] diff --git a/packages/schema/src/session.ts b/packages/schema/src/session.ts new file mode 100644 index 0000000000..7e0e16c755 --- /dev/null +++ b/packages/schema/src/session.ts @@ -0,0 +1,46 @@ +export * as Session from "./session" + +import { Schema } from "effect" +import { Agent } from "./agent" +import { Location } from "./location" +import { Model } from "./model" +import { Project } from "./project" +import { DateTimeUtcFromMillis, optionalOmitUndefined, RelativePath } from "./schema" +import { SessionID } from "./session-id" + +export const ID = SessionID.ID +export type ID = SessionID.ID + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + id: ID, + parentID: ID.pipe(optionalOmitUndefined), + projectID: Project.ID, + agent: Agent.ID.pipe(Schema.optional), + model: Model.Ref.pipe(Schema.optional), + cost: Schema.Finite, + tokens: Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), + }), + time: Schema.Struct({ + created: DateTimeUtcFromMillis, + updated: DateTimeUtcFromMillis, + archived: DateTimeUtcFromMillis.pipe(Schema.optional), + }), + title: Schema.String, + location: Location.Ref, + subpath: RelativePath.pipe(Schema.optional), +}).annotate({ identifier: "SessionV2.Info" }) + +export const ListAnchor = Schema.Struct({ + id: ID, + time: Schema.Finite, + direction: Schema.Literals(["previous", "next"]), +}) +export type ListAnchor = typeof ListAnchor.Type diff --git a/packages/schema/src/skill.ts b/packages/schema/src/skill.ts new file mode 100644 index 0000000000..3a39e3be6e --- /dev/null +++ b/packages/schema/src/skill.ts @@ -0,0 +1,54 @@ +export * as Skill from "./skill" + +import { Schema } from "effect" +import { AbsolutePath } from "./schema" + +export interface DirectorySource extends Schema.Schema.Type {} +export const DirectorySource = Schema.Struct({ + type: Schema.Literal("directory"), + path: AbsolutePath, +}).annotate({ identifier: "SkillV2.DirectorySource" }) + +export interface UrlSource extends Schema.Schema.Type {} +export const UrlSource = Schema.Struct({ + type: Schema.Literal("url"), + url: Schema.String, +}).annotate({ identifier: "SkillV2.UrlSource" }) + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.String.pipe(Schema.optional), + slash: Schema.Boolean.pipe(Schema.optional), + location: AbsolutePath, + content: Schema.String, +}).annotate({ identifier: "SkillV2.Info" }) + +export interface EmbeddedSource extends Schema.Schema.Type {} +export const EmbeddedSource = Schema.Struct({ + type: Schema.Literal("embedded"), + skill: Schema.suspend(() => Info), +}).annotate({ identifier: "SkillV2.EmbeddedSource" }) + +export type Source = DirectorySource | UrlSource | EmbeddedSource +export const Source = Object.assign( + Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe( + Schema.toTaggedUnion("type"), + Schema.annotate({ identifier: "SkillV2.Source" }), + ), + { + equals: (a: Source, b: Source) => { + if (a.type !== b.type) return false + if (a.type === "directory" && b.type === "directory") return a.path === b.path + if (a.type === "url" && b.type === "url") return a.url === b.url + if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name + return false + }, + key: (source: Source) => + source.type === "directory" + ? `directory:${source.path}` + : source.type === "url" + ? `url:${source.url}` + : `embedded:${source.skill.name}`, + }, +) diff --git a/packages/schema/src/workspace.ts b/packages/schema/src/workspace.ts new file mode 100644 index 0000000000..c2a7d1a6f9 --- /dev/null +++ b/packages/schema/src/workspace.ts @@ -0,0 +1,21 @@ +export * as Workspace from "./workspace" + +import { Schema } from "effect" +import { ascending } from "./identifier" +import { withStatics } from "./schema" + +export const ID = Schema.String.check(Schema.isStartsWith("wrk")).pipe( + Schema.brand("WorkspaceV2.ID"), + withStatics((schema) => { + const create = () => schema.make("wrk_" + ascending()) + return { + ascending: (id?: string) => { + if (!id) return create() + if (!id.startsWith("wrk")) throw new Error(`ID ${id} does not start with wrk`) + return schema.make(id) + }, + create, + } + }), +) +export type ID = typeof ID.Type diff --git a/packages/schema/tsconfig.json b/packages/schema/tsconfig.json new file mode 100644 index 0000000000..00ef125468 --- /dev/null +++ b/packages/schema/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "noUncheckedIndexedAccess": false + } +} diff --git a/specs/v2/schema-changelog.md b/specs/v2/schema-changelog.md index bfe7efd89c..9f0cc83856 100644 --- a/specs/v2/schema-changelog.md +++ b/specs/v2/schema-changelog.md @@ -83,26 +83,6 @@ Compatibility: - The `session.next.*` lifecycle event family predates this branch; this branch refines its experimental V2 durability and replay contracts. - Durable replay cursors are per-aggregate event sequences; ephemeral deltas are intentionally absent after reconnect. -### Deterministic IDs From External Keys - -Affected schema: - -- Session and Event ID construction helpers. - -Change: - -- Add deterministic `SessionSchema.ID.fromExternal(...)` and `EventV2.ID.fromExternal(...)` constructors for trusted external keys. - -Reason: - -- Embedded adapters need stable local identities when the same external conversation or stimulus is delivered more than once. -- Deterministic IDs let durable admission and event publication retain their idempotency boundaries across retries. - -Compatibility: - -- Existing generated Session and Event IDs retain their current prefixes and generation behavior. -- Deterministic constructors are additive internal helpers; public ID schemas remain strings with their existing prefixes. - ### Durable Step Settlement Ownership Affected schema: