refactor(core): move v1 schemas into core (#30473)

This commit is contained in:
Dax 2026-06-02 22:42:13 -04:00 committed by GitHub
commit 83452558f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
129 changed files with 1578 additions and 1227 deletions

View file

@ -1,6 +1,7 @@
#!/usr/bin/env bun
import { Config } from "@/config/config"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Schema } from "effect"
import { TuiInfo } from "../src/cli/cmd/tui/config/tui-schema"
@ -68,7 +69,7 @@ const configFile = process.argv[2]
const tuiFile = process.argv[3]
console.log(configFile)
await Bun.write(configFile, JSON.stringify(generateEffect(Config.Info), null, 2))
await Bun.write(configFile, JSON.stringify(generateEffect(ConfigV1.Info), null, 2))
if (tuiFile) {
console.log(tuiFile)

View file

@ -1,9 +1,9 @@
import type { ContentBlock, ContentChunk, ResourceLink, Role } from "@agentclientprotocol/sdk"
import path from "node:path"
import { pathToFileURL } from "node:url"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
export type PromptPart = SessionLegacy.TextPartInput | SessionLegacy.FilePartInput
export type PromptPart = SessionV1.TextPartInput | SessionV1.FilePartInput
export type ReplayPart =
| {
@ -141,7 +141,7 @@ function uriToFilePart(
uri: string,
mime: string,
filename?: string,
): SessionLegacy.FilePartInput | SessionLegacy.TextPartInput {
): SessionV1.FilePartInput | SessionV1.TextPartInput {
try {
if (uri.startsWith("file://")) {
return {

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Config } from "@/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Provider } from "@/provider/provider"
@ -35,7 +35,7 @@ export const Info = Schema.Struct({
topP: Schema.optional(Schema.Finite),
temperature: Schema.optional(Schema.Finite),
color: Schema.optional(Schema.String),
permission: PermissionLegacy.Ruleset,
permission: PermissionV1.Ruleset,
model: Schema.optional(
Schema.Struct({
modelID: ProviderV2.ModelID,

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import type { Permission } from "../permission"
import type { Agent } from "./agent"
@ -16,10 +16,10 @@ import type { Agent } from "./agent"
* doesn't already permit them.
*/
export function deriveSubagentSessionPermission(input: {
parentSessionPermission: PermissionLegacy.Ruleset
parentSessionPermission: PermissionV1.Ruleset
parentAgent: Agent.Info | undefined
subagent: Agent.Info
}): PermissionLegacy.Ruleset {
}): PermissionV1.Ruleset {
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
const parentAgentDenies =

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { EOL } from "os"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { basename } from "path"
import { Cause, Effect } from "effect"
import { Agent } from "../../../agent/agent"
@ -150,7 +150,7 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
)
})
const now = Date.now()
const message: SessionLegacy.Assistant = {
const message: SessionV1.Assistant = {
id: messageID,
sessionID: session.id,
role: "assistant",
@ -179,12 +179,12 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
abort: new AbortController().signal,
messages: [],
metadata: () => Effect.void,
ask(req: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">) {
ask(req: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">) {
return Effect.sync(() => {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, ruleset)
if (rule.action === "deny") {
throw new PermissionLegacy.DeniedError({ ruleset })
throw new PermissionV1.DeniedError({ ruleset })
}
}
})

View file

@ -1,5 +1,5 @@
import { Session } from "@/session/session"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema"
import { effectCmd, fail } from "../effect-cmd"
@ -32,7 +32,7 @@ function diff(kind: string, diffs: { file?: string; patch?: string }[] | undefin
}))
}
function source(part: SessionLegacy.FilePart) {
function source(part: SessionV1.FilePart) {
if (!part.source) return part.source
if (part.source.type === "symbol") {
return {
@ -57,7 +57,7 @@ function source(part: SessionLegacy.FilePart) {
}
}
function filepart(part: SessionLegacy.FilePart): SessionLegacy.FilePart {
function filepart(part: SessionV1.FilePart): SessionV1.FilePart {
return {
...part,
url: redact("file-url", part.id, part.url),
@ -66,7 +66,7 @@ function filepart(part: SessionLegacy.FilePart): SessionLegacy.FilePart {
}
}
function part(part: SessionLegacy.Part): SessionLegacy.Part {
function part(part: SessionV1.Part): SessionV1.Part {
switch (part.type) {
case "text":
return {
@ -160,7 +160,7 @@ function part(part: SessionLegacy.Part): SessionLegacy.Part {
const partFn = part
function sanitize(data: { info: Session.Info; messages: SessionLegacy.WithParts[] }) {
function sanitize(data: { info: Session.Info; messages: SessionV1.WithParts[] }) {
return {
info: {
...data.info,

View file

@ -1,4 +1,4 @@
import type { SessionLegacy } from "@opencode-ai/core/session/legacy"
import type { SessionV1 } from "@opencode-ai/core/v1/session"
export { parseGitHubRemote } from "@/util/repository"
@ -7,7 +7,7 @@ export { parseGitHubRemote } from "@/util/repository"
* Returns null for non-text responses (signals summary needed).
* Throws only for truly empty responses.
*/
export function extractResponseText(parts: SessionLegacy.Part[]): string | null {
export function extractResponseText(parts: SessionV1.Part[]): string | null {
const textPart = parts.findLast((p) => p.type === "text")
if (textPart) return textPart.text

View file

@ -1,5 +1,5 @@
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2"
import { CliError, effectCmd } from "../effect-cmd"
@ -13,8 +13,8 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Effect, Schema } from "effect"
import type { InstanceContext } from "@/project/instance-context"
const decodeMessageInfo = Schema.decodeUnknownSync(SessionLegacy.Info)
const decodePart = Schema.decodeUnknownSync(SessionLegacy.Part)
const decodeMessageInfo = Schema.decodeUnknownSync(SessionV1.Info)
const decodePart = Schema.decodeUnknownSync(SessionV1.Part)
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
export type ShareData =
@ -188,7 +188,7 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
.pipe(Effect.orDie)
for (const msg of exportData.messages) {
const msgInfo = decodeMessageInfo(msg.info) as SessionLegacy.Info
const msgInfo = decodeMessageInfo(msg.info) as SessionV1.Info
const { id, sessionID: _, ...msgData } = msgInfo
yield* db
.insert(MessageTable)
@ -203,7 +203,7 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
.pipe(Effect.orDie)
for (const part of msg.parts) {
const partInfo = decodePart(part) as SessionLegacy.Part
const partInfo = decodePart(part) as SessionV1.Part
const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
yield* db
.insert(PartTable)

View file

@ -1,4 +1,5 @@
import { cmd } from "./cmd"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { effectCmd } from "../effect-cmd"
import { Cause } from "effect"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
@ -10,7 +11,7 @@ import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
import { Config } from "@/config/config"
import { ConfigMCP } from "../../config/mcp"
import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
import { InstanceRef } from "@/effect/instance-ref"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import path from "path"
@ -43,9 +44,9 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
}
}
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
type McpEntry = NonNullable<ConfigV1.Info["mcp"]>[string]
type McpConfigured = ConfigMCP.Info
type McpConfigured = ConfigMCPV1.Info
function isMcpConfigured(config: McpEntry): config is McpConfigured {
return typeof config === "object" && config !== null && "type" in config
}
@ -55,11 +56,11 @@ function isMcpRemote(config: McpEntry): config is McpRemote {
return isMcpConfigured(config) && config.type === "remote"
}
function configuredServers(config: Config.Info) {
function configuredServers(config: ConfigV1.Info) {
return Object.entries(config.mcp ?? {}).filter((entry): entry is [string, McpConfigured] => isMcpConfigured(entry[1]))
}
function oauthServers(config: Config.Info) {
function oauthServers(config: ConfigV1.Info) {
return configuredServers(config).filter(
(entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
)
@ -418,7 +419,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
return candidates[0]
}
async function addMcpToConfig(name: string, mcpConfig: ConfigMCP.Info, configPath: string) {
async function addMcpToConfig(name: string, mcpConfig: ConfigMCPV1.Info, configPath: string) {
let text = "{}"
if (await Filesystem.exists(configPath)) {
text = await Filesystem.readText(configPath)
@ -507,7 +508,7 @@ export const McpAddCommand = effectCmd({
})
if (prompts.isCancel(command)) throw new UI.CancelledError()
const mcpConfig: ConfigMCP.Info = {
const mcpConfig: ConfigMCPV1.Info = {
type: "local",
command: command.split(" "),
}
@ -537,7 +538,7 @@ export const McpAddCommand = effectCmd({
})
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
let mcpConfig: ConfigMCP.Info
let mcpConfig: ConfigMCPV1.Info
if (useOAuth) {
const hasClientId = await prompts.confirm({

View file

@ -1,4 +1,4 @@
import type { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import type { PermissionV1 } from "@opencode-ai/core/v1/permission"
// CLI entry point for `opencode run`.
//
// Handles three modes:
@ -362,7 +362,7 @@ export const RunCommand = effectCmd({
process.exit(1)
}
const rules: PermissionLegacy.Ruleset = args.interactive
const rules: PermissionV1.Ruleset = args.interactive
? []
: [
{

View file

@ -1,4 +1,4 @@
import { ConfigPlugin } from "@/config/plugin"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { TuiKeybind } from "./keybind"
import { Schema } from "effect"
import { isRecord } from "@/util/record"
@ -74,7 +74,7 @@ export const TuiInfo = Schema.Struct({
$schema: Schema.optional(Schema.String),
theme: Schema.optional(Schema.String),
keybinds: Schema.optional(TuiKeybind.KeybindOverrides),
plugin: Schema.optional(Schema.Array(ConfigPlugin.Spec)),
plugin: Schema.optional(Schema.Array(ConfigPluginV1.Spec)),
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
leader_timeout: Schema.optional(KeymapLeaderTimeout),
attention: Schema.optional(Attention),

View file

@ -29,7 +29,7 @@ import { useExit } from "./exit"
import { useArgs } from "./args"
import { batch, onMount } from "solid-js"
import * as Log from "@opencode-ai/core/util/log"
import { emptyConsoleState, type ConsoleState } from "@/config/console-state"
import { emptyConsoleState, type ConsoleState } from "@opencode-ai/core/v1/config/console-state"
import path from "path"
import { useKV } from "./kv"
import { aggregateFailures } from "./aggregate-failures"

View file

@ -39,6 +39,7 @@ import { internalTuiPlugins, type InternalTuiPlugin } from "./internal"
import { setupSlots, Slot as View } from "./slots"
import type { HostPluginApi, HostSlots } from "./slots"
import { ConfigPlugin } from "@/config/plugin"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { createCommandShim } from "./command-shim"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Effect } from "effect"
@ -46,7 +47,7 @@ import { Effect } from "effect"
ensureRuntimePluginSupport({ additional: keymapRuntimeModules })
type PluginLoad = {
options: ConfigPlugin.Options | undefined
options: ConfigPluginV1.Options | undefined
spec: string
target: string
retry: boolean
@ -995,7 +996,7 @@ async function installPluginBySpec(
const tui = manifest.targets.find((item) => item.kind === "tui")
if (tui) {
const file = patch.items.find((item) => item.kind === "tui")?.file
const next = tui.opts ? ([spec, tui.opts] as ConfigPlugin.Spec) : spec
const next = tui.opts ? ([spec, tui.opts] as ConfigPluginV1.Spec) : spec
state.pending.set(spec, {
spec: next,
scope: global ? "global" : "local",

View file

@ -1,4 +1,5 @@
import type { Argv, InferredOptionTypes } from "yargs"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import type { Config } from "@/config/config"
import { Effect } from "effect"
@ -42,7 +43,7 @@ export const resolveNetworkOptions = Effect.fn("Cli.resolveNetworkOptions")(func
return resolveNetworkOptionsNoConfig(args, config)
})
export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Config.Info) {
export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: ConfigV1.Info) {
const portExplicitlySet = process.argv.includes("--port")
const hostnameExplicitlySet = process.argv.includes("--hostname")
const mdnsExplicitlySet = process.argv.includes("--mdns")

View file

@ -1,110 +1,18 @@
export * as ConfigAgent from "./agent"
import path from "path"
import { Exit, Schema, SchemaGetter } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
import { Exit, Schema } from "effect"
import * as Log from "@opencode-ai/core/util/log"
import { Glob } from "@opencode-ai/core/util/glob"
import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent"
import { configEntryNameFromPath } from "./entry-name"
import * as ConfigMarkdown from "./markdown"
import { ConfigModelID } from "./model-id"
import { ConfigParse } from "./parse"
import { ConfigPermission } from "./permission"
const log = Log.create({ service: "config" })
const Color = Schema.Union([
Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)),
Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]),
])
const AgentSchema = Schema.StructWithRest(
Schema.Struct({
model: Schema.optional(ConfigModelID),
variant: Schema.optional(Schema.String).annotate({
description: "Default model variant for this agent (applies only when using the agent's configured model).",
}),
temperature: Schema.optional(Schema.Finite),
top_p: Schema.optional(Schema.Finite),
prompt: Schema.optional(Schema.String),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({
description: "@deprecated Use 'permission' field instead",
}),
disable: Schema.optional(Schema.Boolean),
description: Schema.optional(Schema.String).annotate({ description: "Description of when to use the agent" }),
mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
hidden: Schema.optional(Schema.Boolean).annotate({
description: "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)",
}),
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
color: Schema.optional(Color).annotate({
description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)",
}),
steps: Schema.optional(PositiveInt).annotate({
description: "Maximum number of agentic iterations before forcing text-only response",
}),
maxSteps: Schema.optional(PositiveInt).annotate({ description: "@deprecated Use 'steps' field instead." }),
permission: Schema.optional(ConfigPermission.Info),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
const KNOWN_KEYS = new Set([
"name",
"model",
"variant",
"prompt",
"description",
"temperature",
"top_p",
"mode",
"hidden",
"color",
"steps",
"maxSteps",
"options",
"permission",
"disable",
"tools",
])
// Post-parse normalisation:
// - Promote any unknown-but-present keys into `options` so they survive the
// round-trip in a well-known field.
// - Translate the deprecated `tools: { name: boolean }` map into the new
// `permission` shape (write-adjacent tools collapse into `permission.edit`).
// - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias.
const normalize = (agent: Schema.Schema.Type<typeof AgentSchema>): Schema.Schema.Type<typeof AgentSchema> => {
const options: Record<string, unknown> = { ...agent.options }
for (const [key, value] of Object.entries(agent)) {
if (!KNOWN_KEYS.has(key)) options[key] = value
}
const permission: ConfigPermission.Info = {}
for (const [tool, enabled] of Object.entries(agent.tools ?? {})) {
const action = enabled ? "allow" : "deny"
if (tool === "write" || tool === "edit" || tool === "patch") {
permission.edit = action
continue
}
permission[tool] = action
}
globalThis.Object.assign(permission, agent.permission)
const steps = agent.steps ?? agent.maxSteps
return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) }
}
export const Info = AgentSchema.pipe(
Schema.decodeTo(AgentSchema, {
decode: SchemaGetter.transform(normalize),
encode: SchemaGetter.passthrough({ strict: false }),
}),
).annotate({ identifier: "AgentConfig" })
export type Info = Schema.Schema.Type<typeof Info>
export async function load(dir: string) {
const result: Record<string, Info> = {}
const result: Record<string, ConfigAgentV1.Info> = {}
for (const item of await Glob.scan("{agent,agents}/**/*.md", {
cwd: dir,
absolute: true,
@ -124,13 +32,13 @@ export async function load(dir: string) {
...md.data,
prompt: md.content.trim(),
}
result[config.name] = ConfigParse.schema(Info, config, item)
result[config.name] = ConfigParse.schema(ConfigAgentV1.Info, config, item)
}
return result
}
export async function loadMode(dir: string) {
const result: Record<string, Info> = {}
const result: Record<string, ConfigAgentV1.Info> = {}
for (const item of await Glob.scan("{mode,modes}/*.md", {
cwd: dir,
absolute: true,
@ -148,7 +56,7 @@ export async function loadMode(dir: string) {
...md.data,
prompt: md.content.trim(),
}
const parsed = Schema.decodeUnknownExit(Info)(config, { errors: "all", propertyOrder: "original" })
const parsed = Schema.decodeUnknownExit(ConfigAgentV1.Info)(config, { errors: "all", propertyOrder: "original" })
if (Exit.isSuccess(parsed)) {
result[config.name] = {
...parsed.value,

View file

@ -1,25 +0,0 @@
export * as ConfigAttachment from "./attachment"
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
export const Image = Schema.Struct({
auto_resize: Schema.optional(Schema.Boolean).annotate({
description: "Resize images before sending them to the model when they exceed configured limits (default: true)",
}),
max_width: Schema.optional(PositiveInt).annotate({
description: "Maximum image width before resizing or rejecting the attachment (default: 2000)",
}),
max_height: Schema.optional(PositiveInt).annotate({
description: "Maximum image height before resizing or rejecting the attachment (default: 2000)",
}),
max_base64_bytes: Schema.optional(PositiveInt).annotate({
description: "Maximum base64 payload bytes for an image attachment (default: 5242880)",
}),
}).annotate({ identifier: "ImageAttachmentConfig" })
export type Image = Schema.Schema.Type<typeof Image>
export const Info = Schema.Struct({
image: Schema.optional(Image).annotate({ description: "Image attachment configuration" }),
}).annotate({ identifier: "AttachmentConfig" })
export type Info = Schema.Schema.Type<typeof Info>

View file

@ -4,27 +4,17 @@ import path from "path"
import * as Log from "@opencode-ai/core/util/log"
import { Cause, Exit, Schema } from "effect"
import { Glob } from "@opencode-ai/core/util/glob"
import { ConfigCommandV1 } from "@opencode-ai/core/v1/config/command"
import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "./error"
import { InvalidError } from "@opencode-ai/core/v1/config/error"
import * as ConfigMarkdown from "./markdown"
import { ConfigModelID } from "./model-id"
const log = Log.create({ service: "config" })
export const Info = Schema.Struct({
template: Schema.String,
description: Schema.optional(Schema.String),
agent: Schema.optional(Schema.String),
model: Schema.optional(ConfigModelID),
subtask: Schema.optional(Schema.Boolean),
})
export type Info = Schema.Schema.Type<typeof Info>
const decodeInfo = Schema.decodeUnknownExit(Info)
const decodeInfo = Schema.decodeUnknownExit(ConfigCommandV1.Info)
export async function load(dir: string) {
const result: Record<string, Info> = {}
const result: Record<string, ConfigCommandV1.Info> = {}
for (const item of await Glob.scan("{command,commands}/**/*.md", {
cwd: dir,
absolute: true,

View file

@ -6,7 +6,6 @@ import os from "os"
import { mergeDeep } from "remeda"
import { Global } from "@opencode-ai/core/global"
import fsNode from "fs/promises"
import { NamedError } from "@opencode-ai/core/util/error"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
@ -15,35 +14,25 @@ import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/instal
import { existsSync } from "fs"
import { Account } from "@/account/account"
import { isRecord } from "@/util/record"
import type { ConsoleState } from "./console-state"
import type { ConsoleState } from "@opencode-ai/core/v1/config/console-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { containsPath, type InstanceContext } from "../project/instance-context"
import { NonNegativeInt, PositiveInt, type DeepMutable } from "@opencode-ai/core/schema"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { ConfigAgent } from "./agent"
import { ConfigAttachment } from "./attachment"
import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter"
import { ConfigLayout } from "./layout"
import { ConfigLSP } from "./lsp"
import { ConfigManaged } from "./managed"
import { ConfigMCP } from "./mcp"
import { ConfigModelID } from "./model-id"
import { ConfigParse } from "./parse"
import { ConfigPaths } from "./paths"
import { ConfigPermission } from "./permission"
import { ConfigPlugin } from "./plugin"
import { ConfigProvider } from "./provider"
import { ConfigReference } from "./reference"
import { ConfigServer } from "./server"
import { ConfigSkills } from "./skills"
import { ConfigVariable } from "./variable"
import { Npm } from "@opencode-ai/core/npm"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { ConfigExperimental } from "@opencode-ai/core/config/experimental"
const log = Log.create({ service: "config" })
@ -110,12 +99,7 @@ async function substituteWellKnownRemoteConfig(input: {
return { url, headers }
}
const WellKnownConfig = Schema.Struct({
config: Schema.optional(Schema.Json),
remote_config: Schema.optional(Schema.Json),
})
async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
async function resolveLoadedPlugins<T extends { plugin?: ConfigPluginV1.Spec[] }>(config: T, filepath: string) {
if (!config.plugin) return config
for (let i = 0; i < config.plugin.length; i++) {
// Normalize path-like plugin specs while we still know which config file declared them.
@ -125,193 +109,7 @@ async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(
return config
}
export type Layout = ConfigLayout.Layout
const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
identifier: "LogLevel",
description: "Log level",
})
export const Info = Schema.Struct({
$schema: Schema.optional(Schema.String).annotate({
description: "JSON schema reference for configuration validation",
}),
shell: Schema.optional(Schema.String).annotate({
description: "Default shell to use for terminal and bash tool",
}),
logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
server: Schema.optional(ConfigServer.Server).annotate({
description: "Server configuration for opencode serve and web commands",
}),
command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
description: "Command configuration, see https://opencode.ai/docs/commands",
}),
skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
reference: Schema.optional(ConfigReference.Info).annotate({
description: "Named git or local directory references that can be mentioned as @alias or @alias/path",
}),
watcher: Schema.optional(
Schema.Struct({
ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
}),
),
snapshot: Schema.optional(Schema.Boolean).annotate({
description:
"Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
}),
// User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))),
share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({
description:
"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
}),
autoshare: Schema.optional(Schema.Boolean).annotate({
description: "@deprecated Use 'share' field instead. Share newly created sessions automatically",
}),
autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
description:
"Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
}),
disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Disable providers that are loaded automatically",
}),
enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "When set, ONLY these providers will be enabled. All other providers will be ignored",
}),
model: Schema.optional(ConfigModelID).annotate({
description: "Model to use in the format of provider/model, eg anthropic/claude-2",
}),
small_model: Schema.optional(ConfigModelID).annotate({
description: "Small model to use for tasks like title generation in the format of provider/model",
}),
default_agent: Schema.optional(Schema.String).annotate({
description:
"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.",
}),
username: Schema.optional(Schema.String).annotate({
description: "Custom username to display in conversations instead of system username",
}),
mode: Schema.optional(
Schema.StructWithRest(
Schema.Struct({
build: Schema.optional(ConfigAgent.Info),
plan: Schema.optional(ConfigAgent.Info),
}),
[Schema.Record(Schema.String, ConfigAgent.Info)],
),
).annotate({ description: "@deprecated Use `agent` field instead." }),
agent: Schema.optional(
Schema.StructWithRest(
Schema.Struct({
// primary
plan: Schema.optional(ConfigAgent.Info),
build: Schema.optional(ConfigAgent.Info),
// subagent
general: Schema.optional(ConfigAgent.Info),
explore: Schema.optional(ConfigAgent.Info),
// specialized
title: Schema.optional(ConfigAgent.Info),
summary: Schema.optional(ConfigAgent.Info),
compaction: Schema.optional(ConfigAgent.Info),
}),
[Schema.Record(Schema.String, ConfigAgent.Info)],
),
).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }),
provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({
description: "Custom provider configurations and model overrides",
}),
mcp: Schema.optional(
Schema.Record(
Schema.String,
Schema.Union([
ConfigMCP.Info,
// Matches the legacy `{ enabled: false }` form used to disable a server.
Schema.Struct({ enabled: Schema.Boolean }),
]),
),
).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
formatter: Schema.optional(ConfigFormatter.Info).annotate({
description:
"Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides.",
}),
lsp: Schema.optional(ConfigLSP.Info).annotate({
description:
"Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides.",
}),
instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Additional instruction files or patterns to include",
}),
layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
permission: Schema.optional(ConfigPermission.Info),
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
attachment: Schema.optional(ConfigAttachment.Info).annotate({
description: "Attachment processing configuration, including image size limits and resizing behavior",
}),
enterprise: Schema.optional(
Schema.Struct({
url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
}),
),
tool_output: Schema.optional(
Schema.Struct({
max_lines: Schema.optional(PositiveInt).annotate({
description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
}),
max_bytes: Schema.optional(PositiveInt).annotate({
description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)",
}),
}),
).annotate({
description:
"Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
}),
compaction: Schema.optional(
Schema.Struct({
auto: Schema.optional(Schema.Boolean).annotate({
description: "Enable automatic compaction when context is full (default: true)",
}),
prune: Schema.optional(Schema.Boolean).annotate({
description: "Enable pruning of old tool outputs (default: true)",
}),
tail_turns: Schema.optional(NonNegativeInt).annotate({
description:
"Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
}),
preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
}),
reserved: Schema.optional(NonNegativeInt).annotate({
description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
}),
}),
),
experimental: Schema.optional(
Schema.Struct({
disable_paste_summary: Schema.optional(Schema.Boolean),
batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }),
openTelemetry: Schema.optional(Schema.Boolean).annotate({
description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)",
}),
primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Tools that should only be available to primary agents.",
}),
continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
description: "Continue the agent loop when a tool call is denied",
}),
mcp_timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in milliseconds for model context protocol (MCP) requests",
}),
policies: Schema.optional(Schema.mutable(Schema.Array(ConfigExperimental.Policy))).annotate({
description: "Policy statements applied to supported resources, such as provider access",
}),
}),
),
}).annotate({ identifier: "Config" })
// Uses the shared `DeepMutable` from `@opencode-ai/core/schema`. See the definition
// there for why the local variant is needed over `Types.DeepMutable` from
// effect-smol (the upstream version collapses `unknown` to `{}`).
export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
type Info = ConfigV1.Info & {
// plugin_origins is derived state, not a persisted config field. It keeps each winning plugin spec together
// with the file and scope it came from so later runtime code can make location-sensitive decisions.
plugin_origins?: ConfigPlugin.Origin[]
@ -375,12 +173,6 @@ function writableGlobal(info: Info) {
return next
}
export const ConfigDirectoryTypoError = NamedError.create("ConfigDirectoryTypoError", {
path: Schema.String,
dir: Schema.String,
suggestion: Schema.String,
})
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
@ -424,7 +216,7 @@ export const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
const data = ConfigParse.schema(Info, normalizeLoadedConfig(parsed, source), source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed, source), source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@ -530,7 +322,7 @@ export const layer = Layer.effect(
source: string,
// mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
// is attached.
list: ConfigPlugin.Spec[] | undefined,
list: ConfigPluginV1.Spec[] | undefined,
// Scope can be inferred from the source path, but some callers already know whether the config should
// behave as global or local and can pass that explicitly.
kind?: ConfigPlugin.Scope,
@ -558,7 +350,7 @@ export const layer = Layer.effect(
authEnv[value.key] = value.token
const wellknownURL = `${url}/.well-known/opencode`
log.debug("fetching remote config", { url: wellknownURL })
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, WellKnownConfig)
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown)
const remote = yield* Effect.promise(() =>
substituteWellKnownRemoteConfig({
value: wellknown.remote_config,
@ -753,9 +545,9 @@ export const layer = Layer.effect(
}
if (result.tools) {
const perms: Record<string, ConfigPermission.Action> = {}
const perms: Record<string, ConfigPermissionV1.Action> = {}
for (const [tool, enabled] of Object.entries(result.tools)) {
const action: ConfigPermission.Action = enabled ? "allow" : "deny"
const action: ConfigPermissionV1.Action = enabled ? "allow" : "deny"
if (tool === "write" || tool === "edit" || tool === "patch") {
perms.edit = action
continue
@ -844,7 +636,7 @@ export const layer = Layer.effect(
let next: Info
let changed: boolean
if (!file.endsWith(".jsonc")) {
const existing = ConfigParse.schema(Info, ConfigParse.jsonc(before, file), file)
const existing = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(before, file), file)
const merged = mergeDeep(writable(existing), patch)
const serialized = JSON.stringify(merged, null, 2)
changed = serialized !== before
@ -852,7 +644,7 @@ export const layer = Layer.effect(
next = merged
} else {
const updated = patchJsonc(before, patch)
next = ConfigParse.schema(Info, ConfigParse.jsonc(updated, file), file)
next = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(updated, file), file)
changed = updated !== before
if (changed) yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
}

View file

@ -1,14 +0,0 @@
import { Schema } from "effect"
import { NonNegativeInt } from "@opencode-ai/core/schema"
export class ConsoleState extends Schema.Class<ConsoleState>("ConsoleState")({
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
activeOrgName: Schema.optional(Schema.String),
switchableOrgCount: NonNegativeInt,
}) {}
export const emptyConsoleState: ConsoleState = ConsoleState.make({
consoleManagedProviders: [],
activeOrgName: undefined,
switchableOrgCount: 0,
})

View file

@ -1,23 +0,0 @@
export * as ConfigError from "./error"
import { NamedError } from "@opencode-ai/core/util/error"
import { Schema } from "effect"
const Issue = Schema.StructWithRest(
Schema.Struct({
message: Schema.String,
path: Schema.Array(Schema.String),
}),
[Schema.Record(Schema.String, Schema.Unknown)],
)
export const JsonError = NamedError.create("ConfigJsonError", {
path: Schema.String,
message: Schema.optional(Schema.String),
})
export const InvalidError = NamedError.create("ConfigInvalidError", {
path: Schema.String,
issues: Schema.optional(Schema.Array(Issue)),
message: Schema.optional(Schema.String),
})

View file

@ -1,13 +0,0 @@
export * as ConfigFormatter from "./formatter"
import { Schema } from "effect"
export const Entry = Schema.Struct({
disabled: Schema.optional(Schema.Boolean),
command: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)),
extensions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
})
export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)])
export type Info = Schema.Schema.Type<typeof Info>

View file

@ -1,6 +0,0 @@
import { Schema } from "effect"
export const Layout = Schema.Literals(["auto", "stretch"]).annotate({ identifier: "LayoutConfig" })
export type Layout = Schema.Schema.Type<typeof Layout>
export * as ConfigLayout from "./layout"

View file

@ -1,43 +0,0 @@
export * as ConfigLSP from "./lsp"
import { Schema } from "effect"
import * as LSPServer from "../lsp/server"
export const Disabled = Schema.Struct({
disabled: Schema.Literal(true),
}).pipe((schema) => schema)
export const Entry = Schema.Union([
Disabled,
Schema.Struct({
command: Schema.mutable(Schema.Array(Schema.String)),
extensions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
disabled: Schema.optional(Schema.Boolean),
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
initialization: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}),
]).pipe((schema) => schema)
/**
* For custom (non-builtin) LSP server entries, `extensions` is required so the
* client knows which files the server should attach to. Builtin server IDs and
* explicitly disabled entries are exempt.
*/
export const requiresExtensionsForCustomServers = Schema.makeFilter<
boolean | Record<string, Schema.Schema.Type<typeof Entry>>
>((data) => {
if (typeof data === "boolean") return undefined
const serverIds = new Set(Object.values(LSPServer).map((server) => server.id))
const ok = Object.entries(data).every(([id, config]) => {
if ("disabled" in config && config.disabled) return true
if (serverIds.has(id)) return true
return "extensions" in config && Boolean(config.extensions)
})
return ok ? undefined : "For custom LSP servers, 'extensions' array is required."
})
export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)])
.check(requiresExtensionsForCustomServers)
.pipe((schema) => schema)
export type Info = Schema.Schema.Type<typeof Info>

View file

@ -1,7 +1,6 @@
import { NamedError } from "@opencode-ai/core/util/error"
import matter from "gray-matter"
import { Schema } from "effect"
import { Filesystem } from "@/util/filesystem"
import { FrontmatterError } from "@opencode-ai/core/v1/config/error"
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
export const SHELL_REGEX = /!`([^`]+)`/g
@ -88,9 +87,4 @@ export async function parse(filePath: string) {
}
}
export const FrontmatterError = NamedError.create("ConfigFrontmatterError", {
path: Schema.String,
message: Schema.String,
})
export * as ConfigMarkdown from "./markdown"

View file

@ -1,60 +0,0 @@
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
export const Local = Schema.Struct({
type: Schema.Literal("local").annotate({ description: "Type of MCP server connection" }),
command: Schema.mutable(Schema.Array(Schema.String)).annotate({
description: "Command and arguments to run the MCP server",
}),
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
description: "Environment variables to set when running the MCP server",
}),
enabled: Schema.optional(Schema.Boolean).annotate({
description: "Enable or disable the MCP server on startup",
}),
timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
}),
}).annotate({ identifier: "McpLocalConfig" })
export type Local = Schema.Schema.Type<typeof Local>
export const OAuth = Schema.Struct({
clientId: Schema.optional(Schema.String).annotate({
description: "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.",
}),
clientSecret: Schema.optional(Schema.String).annotate({
description: "OAuth client secret (if required by the authorization server)",
}),
scope: Schema.optional(Schema.String).annotate({ description: "OAuth scopes to request during authorization" }),
callbackPort: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))).annotate({
description:
"Port for the local OAuth callback server (default: 19876). Shorthand for redirectUri when only the port needs changing. Ignored if redirectUri is set.",
}),
redirectUri: Schema.optional(Schema.String).annotate({
description: "OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback).",
}),
}).annotate({ identifier: "McpOAuthConfig" })
export type OAuth = Schema.Schema.Type<typeof OAuth>
export const Remote = Schema.Struct({
type: Schema.Literal("remote").annotate({ description: "Type of MCP server connection" }),
url: Schema.String.annotate({ description: "URL of the remote MCP server" }),
enabled: Schema.optional(Schema.Boolean).annotate({
description: "Enable or disable the MCP server on startup",
}),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)).annotate({
description: "Headers to send with the request",
}),
oauth: Schema.optional(Schema.Union([OAuth, Schema.Literal(false)])).annotate({
description: "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.",
}),
timeout: Schema.optional(PositiveInt).annotate({
description: "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
}),
}).annotate({ identifier: "McpRemoteConfig" })
export type Remote = Schema.Schema.Type<typeof Remote>
export const Info = Schema.Union([Local, Remote]).annotate({ discriminator: "type" })
export type Info = Schema.Schema.Type<typeof Info>
export * as ConfigMCP from "./mcp"

View file

@ -1,5 +0,0 @@
import { Schema } from "effect"
export const ConfigModelID = Schema.String
export type ConfigModelID = Schema.Schema.Type<typeof ConfigModelID>

View file

@ -3,7 +3,7 @@ export * as ConfigParse from "./parse"
import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser"
import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect"
import type { DeepMutable } from "@opencode-ai/core/schema"
import { InvalidError, JsonError } from "./error"
import { InvalidError, JsonError } from "@opencode-ai/core/v1/config/error"
export function jsonc(text: string, filepath: string): unknown {
const errors: JsoncParseError[] = []

View file

@ -1,56 +0,0 @@
export * as ConfigPermission from "./permission"
import { Schema, SchemaGetter } from "effect"
export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" })
export type Action = Schema.Schema.Type<typeof Action>
export const Object = Schema.Record(Schema.String, Action).annotate({ identifier: "PermissionObjectConfig" })
export type Object = Schema.Schema.Type<typeof Object>
export const Rule = Schema.Union([Action, Object]).annotate({ identifier: "PermissionRuleConfig" })
export type Rule = Schema.Schema.Type<typeof Rule>
// Known permission keys get explicit types in the Effect schema for generated
// docs/types. Runtime config parsing uses Effect's `propertyOrder: "original"`
// parse option so user key order is preserved for permission precedence.
const InputObject = Schema.StructWithRest(
Schema.Struct({
read: Schema.optional(Rule),
edit: Schema.optional(Rule),
glob: Schema.optional(Rule),
grep: Schema.optional(Rule),
list: Schema.optional(Rule),
bash: Schema.optional(Rule),
task: Schema.optional(Rule),
external_directory: Schema.optional(Rule),
todowrite: Schema.optional(Action),
question: Schema.optional(Action),
webfetch: Schema.optional(Action),
websearch: Schema.optional(Action),
lsp: Schema.optional(Rule),
doom_loop: Schema.optional(Action),
skill: Schema.optional(Rule),
}),
[Schema.Record(Schema.String, Rule)],
)
// Input the user writes in config: either a single Action (shorthand for "*")
// or an object of per-target rules.
const InputSchema = Schema.Union([Action, InputObject])
// Normalise the Action shorthand into `{ "*": action }`. Object inputs pass
// through untouched.
const normalizeInput = (input: Schema.Schema.Type<typeof InputSchema>): Schema.Schema.Type<typeof InputObject> =>
typeof input === "string" ? { "*": input } : input
export const Info = InputSchema.pipe(
Schema.decodeTo(InputObject, {
decode: SchemaGetter.transform(normalizeInput),
// Not perfectly invertible (we lose whether the user originally typed an
// Action shorthand), but the object form is always a valid representation
// of the same rules.
encode: SchemaGetter.passthrough({ strict: false }),
}),
).annotate({ identifier: "PermissionConfig" })
type _Info = Schema.Schema.Type<typeof InputObject>
export type Info = { -readonly [K in keyof _Info]: _Info[K] }

View file

@ -1,30 +1,22 @@
import { Glob } from "@opencode-ai/core/util/glob"
import { Schema } from "effect"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { pathToFileURL } from "url"
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
import path from "path"
export const Options = Schema.Record(Schema.String, Schema.Unknown)
export type Options = Schema.Schema.Type<typeof Options>
// Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
// It answers "what should we load?" but says nothing about where that value came from.
export const Spec = Schema.Union([Schema.String, Schema.mutable(Schema.Tuple([Schema.String, Options]))])
export type Spec = Schema.Schema.Type<typeof Spec>
export type Scope = "global" | "local"
// Origin keeps the original config provenance attached to a spec.
// After multiple config files are merged, callers still need to know which file declared the plugin
// and whether it should behave like a global or project-local plugin.
export type Origin = {
spec: Spec
spec: ConfigPluginV1.Spec
source: string
scope: Scope
}
export async function load(dir: string) {
const plugins: Spec[] = []
const plugins: ConfigPluginV1.Spec[] = []
for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
cwd: dir,
@ -37,17 +29,17 @@ export async function load(dir: string) {
return plugins
}
export function pluginSpecifier(plugin: Spec): string {
export function pluginSpecifier(plugin: ConfigPluginV1.Spec): string {
return Array.isArray(plugin) ? plugin[0] : plugin
}
export function pluginOptions(plugin: Spec): Options | undefined {
export function pluginOptions(plugin: ConfigPluginV1.Spec): ConfigPluginV1.Options | undefined {
return Array.isArray(plugin) ? plugin[1] : undefined
}
// Path-like specs are resolved relative to the config file that declared them so merges later on do not
// accidentally reinterpret `./plugin.ts` relative to some other directory.
export async function resolvePluginSpec(plugin: Spec, configFilepath: string): Promise<Spec> {
export async function resolvePluginSpec(plugin: ConfigPluginV1.Spec, configFilepath: string): Promise<ConfigPluginV1.Spec> {
const spec = pluginSpecifier(plugin)
if (!isPathPluginSpec(spec)) return plugin

View file

@ -1,120 +0,0 @@
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
import { ModelStatus } from "@/provider/model-status"
export const Model = Schema.Struct({
id: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
family: Schema.optional(Schema.String),
release_date: Schema.optional(Schema.String),
attachment: Schema.optional(Schema.Boolean),
reasoning: Schema.optional(Schema.Boolean),
temperature: Schema.optional(Schema.Boolean),
tool_call: Schema.optional(Schema.Boolean),
interleaved: Schema.optional(
Schema.Union([
Schema.Literal(true),
Schema.Struct({
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
}),
]),
),
cost: Schema.optional(
Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
context_over_200k: Schema.optional(
Schema.Struct({
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
}),
),
}),
),
limit: Schema.optional(
Schema.Struct({
context: Schema.Finite,
input: Schema.optional(Schema.Finite),
output: Schema.Finite,
}),
),
modalities: Schema.optional(
Schema.Struct({
input: Schema.optional(Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])))),
output: Schema.optional(
Schema.mutable(Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"]))),
),
}),
),
experimental: Schema.optional(Schema.Boolean),
status: Schema.optional(ModelStatus),
provider: Schema.optional(
Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }),
),
options: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
variants: Schema.optional(
Schema.Record(
Schema.String,
Schema.StructWithRest(
Schema.Struct({
disabled: Schema.optional(Schema.Boolean).annotate({ description: "Disable this variant for the model" }),
}),
[Schema.Record(Schema.String, Schema.Any)],
),
).annotate({ description: "Variant-specific configuration" }),
),
})
export const Info = Schema.Struct({
api: Schema.optional(Schema.String),
name: Schema.optional(Schema.String),
env: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
id: Schema.optional(Schema.String),
npm: Schema.optional(Schema.String),
whitelist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
blacklist: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
options: Schema.optional(
Schema.StructWithRest(
Schema.Struct({
apiKey: Schema.optional(Schema.String),
baseURL: Schema.optional(Schema.String),
enterpriseUrl: Schema.optional(Schema.String).annotate({
description: "GitHub Enterprise URL for copilot authentication",
}),
setCacheKey: Schema.optional(Schema.Boolean).annotate({
description: "Enable promptCacheKey for this provider (default false)",
}),
timeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description: "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout.",
}),
).annotate({
description: "Timeout in milliseconds for full requests to this provider. Set to false to disable timeout.",
}),
headerTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
).annotate({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
}),
chunkTimeout: Schema.optional(PositiveInt).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
}),
}),
[Schema.Record(Schema.String, Schema.Any)],
),
),
models: Schema.optional(Schema.Record(Schema.String, Model)),
}).annotate({ identifier: "ProviderConfig" })
export type Info = Schema.Schema.Type<typeof Info>
export * as ConfigProvider from "./provider"

View file

@ -1,27 +1,6 @@
export * as ConfigReference from "./reference"
import { Schema } from "effect"
const Git = Schema.Struct({
repository: Schema.String.annotate({
description: "Git repository URL, host/path reference, or GitHub owner/repo shorthand",
}),
branch: Schema.optional(Schema.String).annotate({
description: "Branch or ref to clone and inspect",
}),
})
const Local = Schema.Struct({
path: Schema.String.annotate({
description: "Absolute path, ~/ path, or workspace-relative path to a local reference directory",
}),
})
export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" })
export type Entry = Schema.Schema.Type<typeof Entry>
export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" })
export type Info = Schema.Schema.Type<typeof Info>
import { ConfigReferenceV1 } from "@opencode-ai/core/v1/config/reference"
export type NormalizedEntry =
| {
@ -47,7 +26,7 @@ export function validateAlias(name: string) {
}
}
export function normalizeEntry(entry: Entry): NormalizedEntry {
export function normalizeEntry(entry: ConfigReferenceV1.Entry): NormalizedEntry {
if (typeof entry === "string") {
if (entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~")) {
return { kind: "local", path: entry }
@ -59,7 +38,7 @@ export function normalizeEntry(entry: Entry): NormalizedEntry {
return { kind: "git", repository: entry.repository, branch: entry.branch }
}
export function normalize(info: Info): NormalizedInfo {
export function normalize(info: ConfigReferenceV1.Info): NormalizedInfo {
return Object.fromEntries(
Object.entries(info).map(([name, entry]) => {
const aliasError = validateAlias(name)

View file

@ -1,19 +0,0 @@
import { Schema } from "effect"
import { PositiveInt } from "@opencode-ai/core/schema"
export const Server = Schema.Struct({
port: Schema.optional(PositiveInt).annotate({
description: "Port to listen on",
}),
hostname: Schema.optional(Schema.String).annotate({ description: "Hostname to listen on" }),
mdns: Schema.optional(Schema.Boolean).annotate({ description: "Enable mDNS service discovery" }),
mdnsDomain: Schema.optional(Schema.String).annotate({
description: "Custom domain name for mDNS service (default: opencode.local)",
}),
cors: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Additional domains to allow for CORS",
}),
}).annotate({ identifier: "ServerConfig" })
export type Server = Schema.Schema.Type<typeof Server>
export * as ConfigServer from "./server"

View file

@ -1,14 +0,0 @@
import { Schema } from "effect"
export const Info = Schema.Struct({
paths: Schema.optional(Schema.Array(Schema.String)).annotate({
description: "Additional paths to skill folders",
}),
urls: Schema.optional(Schema.Array(Schema.String)).annotate({
description: "URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)",
}),
})
export type Info = Schema.Schema.Type<typeof Info>
export * as ConfigSkills from "./skills"

View file

@ -3,7 +3,7 @@ export * as ConfigVariable from "./variable"
import path from "path"
import os from "os"
import { Filesystem } from "@/util/filesystem"
import { InvalidError } from "./error"
import { InvalidError } from "@opencode-ai/core/v1/config/error"
type ParseSource =
| {

View file

@ -1,5 +1,5 @@
import { Config } from "@/config/config"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { MessageV2 } from "@/session/message-v2"
import * as Log from "@opencode-ai/core/util/log"
import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" }
@ -53,7 +53,7 @@ export class SizeError extends Schema.TaggedErrorClass<SizeError>()("ImageSizeEr
export type Error = ResizerUnavailableError | InvalidDataUrlError | DecodeError | SizeError
export interface Interface {
readonly normalize: (input: SessionLegacy.FilePart) => Effect.Effect<SessionLegacy.FilePart, Error>
readonly normalize: (input: SessionV1.FilePart) => Effect.Effect<SessionV1.FilePart, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Image") {}
@ -74,7 +74,7 @@ export const layer = Layer.effect(
),
)
const normalize = Effect.fn("Image.normalize")(function* (input: SessionLegacy.FilePart) {
const normalize = Effect.fn("Image.normalize")(function* (input: SessionV1.FilePart) {
const image = (yield* config.get()).attachment?.image
const info = {
autoResize: image?.auto_resize ?? AUTO_RESIZE,

View file

@ -1,4 +1,5 @@
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
@ -13,7 +14,7 @@ import {
ToolListChangedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"
import { Config } from "@/config/config"
import { ConfigMCP } from "../config/mcp"
import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
import * as Log from "@opencode-ai/core/util/log"
import { NamedError } from "@opencode-ai/core/util/error"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
@ -106,9 +107,9 @@ const pendingOAuthTransports = new Map<string, TransportWithAuth>()
// Prompt cache types
type PromptInfo = Awaited<ReturnType<MCPClient["listPrompts"]>>["prompts"][number]
type ResourceInfo = Awaited<ReturnType<MCPClient["listResources"]>>["resources"][number]
type McpEntry = NonNullable<Config.Info["mcp"]>[string]
type McpEntry = NonNullable<ConfigV1.Info["mcp"]>[string]
function isMcpConfigured(entry: McpEntry): entry is ConfigMCP.Info {
function isMcpConfigured(entry: McpEntry): entry is ConfigMCPV1.Info {
return typeof entry === "object" && entry !== null && "type" in entry
}
@ -234,7 +235,7 @@ interface AuthResult {
// --- Effect Service ---
interface State {
config: Record<string, ConfigMCP.Info>
config: Record<string, ConfigMCPV1.Info>
status: Record<string, Status>
clients: Record<string, MCPClient>
defs: Record<string, MCPToolDef[]>
@ -246,7 +247,7 @@ export interface Interface {
readonly tools: () => Effect.Effect<Record<string, Tool>>
readonly prompts: () => Effect.Effect<Record<string, PromptInfo & { client: string }>>
readonly resources: () => Effect.Effect<Record<string, ResourceInfo & { client: string }>>
readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
readonly add: (name: string, mcp: ConfigMCPV1.Info) => Effect.Effect<{ status: Record<string, Status> | Status }>
readonly connect: (name: string) => Effect.Effect<void, NotFoundError>
readonly disconnect: (name: string) => Effect.Effect<void, NotFoundError>
readonly getPrompt: (
@ -304,7 +305,7 @@ export const layer = Layer.effect(
const connectRemote = Effect.fn("MCP.connectRemote")(function* (
key: string,
mcp: ConfigMCP.Info & { type: "remote" },
mcp: ConfigMCPV1.Info & { type: "remote" },
) {
const oauthDisabled = mcp.oauth === false
const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
@ -421,7 +422,7 @@ export const layer = Layer.effect(
const connectLocal = Effect.fn("MCP.connectLocal")(function* (
key: string,
mcp: ConfigMCP.Info & { type: "local" },
mcp: ConfigMCPV1.Info & { type: "local" },
) {
const [cmd, ...args] = mcp.command
const cwd = yield* InstanceState.directory
@ -454,7 +455,7 @@ export const layer = Layer.effect(
)
})
const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCP.Info) {
const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCPV1.Info) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { key })
return DISABLED_RESULT
@ -464,8 +465,8 @@ export const layer = Layer.effect(
const { client: mcpClient, status } =
mcp.type === "remote"
? yield* connectRemote(key, mcp as ConfigMCP.Info & { type: "remote" })
: yield* connectLocal(key, mcp as ConfigMCP.Info & { type: "local" })
? yield* connectRemote(key, mcp as ConfigMCPV1.Info & { type: "remote" })
: yield* connectLocal(key, mcp as ConfigMCPV1.Info & { type: "local" })
if (!mcpClient) {
return { status } satisfies CreateResult
@ -633,7 +634,7 @@ export const layer = Layer.effect(
return s.clients
})
const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCP.Info) {
const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) {
const s = yield* InstanceState.get(state)
const result = yield* create(name, mcp)
@ -647,7 +648,7 @@ export const layer = Layer.effect(
return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout)
})
const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCP.Info) {
const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCPV1.Info) {
const s = yield* InstanceState.get(state)
s.config[name] = mcp
yield* createAndStore(name, mcp)

View file

@ -1,48 +1,48 @@
import { ConfigPermission } from "@/config/permission"
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
import { InstanceState } from "@/effect/instance-state"
import * as Log from "@opencode-ai/core/util/log"
import { Wildcard } from "@opencode-ai/core/util/wildcard"
import { Deferred, Effect, Layer, Context } from "effect"
import os from "os"
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
const log = Log.create({ service: "permission" })
export const Event = {
Asked: EventV2.define({ type: "permission.asked", schema: PermissionLegacy.Request.fields }),
Asked: EventV2.define({ type: "permission.asked", schema: PermissionV1.Request.fields }),
Replied: EventV2.define({
type: "permission.replied",
schema: {
sessionID: PermissionLegacy.Request.fields.sessionID,
requestID: PermissionLegacy.ID,
reply: PermissionLegacy.Reply,
sessionID: PermissionV1.Request.fields.sessionID,
requestID: PermissionV1.ID,
reply: PermissionV1.Reply,
},
}),
}
export interface Interface {
readonly ask: (input: PermissionLegacy.AskInput) => Effect.Effect<void, PermissionLegacy.Error>
readonly reply: (input: PermissionLegacy.ReplyInput) => Effect.Effect<void, PermissionLegacy.NotFoundError>
readonly list: () => Effect.Effect<ReadonlyArray<PermissionLegacy.Request>>
readonly ask: (input: PermissionV1.AskInput) => Effect.Effect<void, PermissionV1.Error>
readonly reply: (input: PermissionV1.ReplyInput) => Effect.Effect<void, PermissionV1.NotFoundError>
readonly list: () => Effect.Effect<ReadonlyArray<PermissionV1.Request>>
}
interface PendingEntry {
info: PermissionLegacy.Request
deferred: Deferred.Deferred<void, PermissionLegacy.RejectedError | PermissionLegacy.CorrectedError>
info: PermissionV1.Request
deferred: Deferred.Deferred<void, PermissionV1.RejectedError | PermissionV1.CorrectedError>
}
interface State {
pending: Map<PermissionLegacy.ID, PendingEntry>
approved: PermissionLegacy.Rule[]
pending: Map<PermissionV1.ID, PendingEntry>
approved: PermissionV1.Rule[]
}
export function evaluate(
permission: string,
pattern: string,
...rulesets: PermissionLegacy.Ruleset[]
): PermissionLegacy.Rule {
...rulesets: PermissionV1.Ruleset[]
): PermissionV1.Rule {
return (
rulesets
.flat()
@ -64,14 +64,14 @@ export const layer = Layer.effect(
Effect.fn("Permission.state")(function* (ctx) {
void ctx
const state = {
pending: new Map<PermissionLegacy.ID, PendingEntry>(),
pending: new Map<PermissionV1.ID, PendingEntry>(),
approved: [],
}
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
for (const item of state.pending.values()) {
yield* Deferred.fail(item.deferred, new PermissionLegacy.RejectedError())
yield* Deferred.fail(item.deferred, new PermissionV1.RejectedError())
}
state.pending.clear()
}),
@ -81,7 +81,7 @@ export const layer = Layer.effect(
}),
)
const ask = Effect.fn("Permission.ask")(function* (input: PermissionLegacy.AskInput) {
const ask = Effect.fn("Permission.ask")(function* (input: PermissionV1.AskInput) {
const { approved, pending } = yield* InstanceState.get(state)
const { ruleset, ...request } = input
let needsAsk = false
@ -90,7 +90,7 @@ export const layer = Layer.effect(
const rule = evaluate(request.permission, pattern, ruleset, approved)
log.info("evaluated", { permission: request.permission, pattern, action: rule })
if (rule.action === "deny") {
return yield* new PermissionLegacy.DeniedError({
return yield* new PermissionV1.DeniedError({
ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
})
}
@ -100,8 +100,8 @@ export const layer = Layer.effect(
if (!needsAsk) return
const id = request.id ?? PermissionLegacy.ID.ascending()
const info: PermissionLegacy.Request = {
const id = request.id ?? PermissionV1.ID.ascending()
const info: PermissionV1.Request = {
id,
sessionID: request.sessionID,
permission: request.permission,
@ -112,7 +112,7 @@ export const layer = Layer.effect(
}
log.info("asking", { id, permission: info.permission, patterns: info.patterns })
const deferred = yield* Deferred.make<void, PermissionLegacy.RejectedError | PermissionLegacy.CorrectedError>()
const deferred = yield* Deferred.make<void, PermissionV1.RejectedError | PermissionV1.CorrectedError>()
pending.set(id, { info, deferred })
yield* events.publish(Event.Asked, info)
return yield* Effect.ensuring(
@ -123,10 +123,10 @@ export const layer = Layer.effect(
)
})
const reply = Effect.fn("Permission.reply")(function* (input: PermissionLegacy.ReplyInput) {
const reply = Effect.fn("Permission.reply")(function* (input: PermissionV1.ReplyInput) {
const { approved, pending } = yield* InstanceState.get(state)
const existing = pending.get(input.requestID)
if (!existing) return yield* new PermissionLegacy.NotFoundError({ requestID: input.requestID })
if (!existing) return yield* new PermissionV1.NotFoundError({ requestID: input.requestID })
pending.delete(input.requestID)
yield* events.publish(Event.Replied, {
@ -139,8 +139,8 @@ export const layer = Layer.effect(
yield* Deferred.fail(
existing.deferred,
input.message
? new PermissionLegacy.CorrectedError({ feedback: input.message })
: new PermissionLegacy.RejectedError(),
? new PermissionV1.CorrectedError({ feedback: input.message })
: new PermissionV1.RejectedError(),
)
for (const [id, item] of pending.entries()) {
@ -151,7 +151,7 @@ export const layer = Layer.effect(
requestID: item.info.id,
reply: "reject",
})
yield* Deferred.fail(item.deferred, new PermissionLegacy.RejectedError())
yield* Deferred.fail(item.deferred, new PermissionV1.RejectedError())
}
return
}
@ -200,8 +200,8 @@ function expand(pattern: string): string {
return pattern
}
export function fromConfig(permission: ConfigPermission.Info) {
const ruleset: PermissionLegacy.Rule[] = []
export function fromConfig(permission: ConfigPermissionV1.Info) {
const ruleset: PermissionV1.Rule[] = []
for (const [key, value] of Object.entries(permission)) {
if (typeof value === "string") {
ruleset.push({ permission: key, action: value, pattern: "*" })
@ -214,11 +214,11 @@ export function fromConfig(permission: ConfigPermission.Info) {
return ruleset
}
export function merge(...rulesets: PermissionLegacy.Ruleset[]): PermissionLegacy.Rule[] {
export function merge(...rulesets: PermissionV1.Ruleset[]): PermissionV1.Rule[] {
return rulesets.flat()
}
export function disabled(tools: string[], ruleset: PermissionLegacy.Ruleset): Set<string> {
export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set<string> {
const edits = ["edit", "write", "apply_patch"]
return new Set(
tools.filter((tool) => {

View file

@ -9,13 +9,14 @@ import {
type PluginSource,
} from "./shared"
import { ConfigPlugin } from "@/config/plugin"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
export namespace PluginLoader {
// A normalized plugin declaration derived from config before any filesystem or npm work happens.
export type Plan = {
spec: string
options: ConfigPlugin.Options | undefined
options: ConfigPluginV1.Options | undefined
deprecated: boolean
}
@ -73,7 +74,7 @@ export namespace PluginLoader {
}
// Normalize a config item into the loader's internal representation.
function plan(item: ConfigPlugin.Spec): Plan {
function plan(item: ConfigPluginV1.Spec): Plan {
const spec = ConfigPlugin.pluginSpecifier(item)
return { spec, options: ConfigPlugin.pluginOptions(item), deprecated: isDeprecatedPlugin(spec) }
}

View file

@ -1,4 +1,5 @@
import os from "os"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import fuzzysort from "fuzzysort"
import { Config } from "@/config/config"
import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
@ -142,7 +143,7 @@ type CustomLoader = (provider: Info) => Effect.Effect<{
type CustomDep = {
auth: (id: string) => Effect.Effect<Auth.Info | undefined>
config: () => Effect.Effect<Config.Info>
config: () => Effect.Effect<ConfigV1.Info>
env: () => Effect.Effect<Record<string, string | undefined>>
get: (key: string) => Effect.Effect<string | undefined>
}

View file

@ -1,4 +1,5 @@
import { Config } from "@/config/config"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Provider } from "@/provider/provider"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
@ -14,7 +15,7 @@ export const ConfigApi = HttpApi.make("config")
.add(
HttpApiEndpoint.get("get", root, {
query: WorkspaceRoutingQuery,
success: described(Config.Info, "Get config info"),
success: described(ConfigV1.Info, "Get config info"),
}).annotateMerge(
OpenApi.annotations({
identifier: "config.get",
@ -24,8 +25,8 @@ export const ConfigApi = HttpApi.make("config")
),
HttpApiEndpoint.patch("update", root, {
query: WorkspaceRoutingQuery,
payload: Config.Info,
success: described(Config.Info, "Successfully updated config"),
payload: ConfigV1.Info,
success: described(ConfigV1.Info, "Successfully updated config"),
error: HttpApiError.BadRequest,
}).annotateMerge(
OpenApi.annotations({

View file

@ -1,4 +1,5 @@
import { Config } from "@/config/config"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { EventV2 } from "@opencode-ai/core/event"
import { InstanceDisposed } from "@/server/event"
import "@opencode-ai/core/account"
@ -90,7 +91,7 @@ export const GlobalApi = HttpApi.make("global").add(
}),
),
HttpApiEndpoint.get("configGet", GlobalPaths.config, {
success: described(Config.Info, "Get global config info"),
success: described(ConfigV1.Info, "Get global config info"),
}).annotateMerge(
OpenApi.annotations({
identifier: "global.config.get",
@ -99,8 +100,8 @@ export const GlobalApi = HttpApi.make("global").add(
}),
),
HttpApiEndpoint.patch("configUpdate", GlobalPaths.config, {
payload: Config.Info,
success: described(Config.Info, "Successfully updated global config"),
payload: ConfigV1.Info,
success: described(ConfigV1.Info, "Successfully updated global config"),
error: HttpApiError.BadRequest,
}).annotateMerge(
OpenApi.annotations({

View file

@ -1,5 +1,5 @@
import { MCP } from "@/mcp"
import { ConfigMCP } from "@/config/mcp"
import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { McpServerNotFoundError } from "../errors"
@ -10,7 +10,7 @@ import { described } from "./metadata"
export const AddPayload = Schema.Struct({
name: Schema.String,
config: ConfigMCP.Info,
config: ConfigMCPV1.Info,
})
export const StatusMap = Schema.Record(Schema.String, MCP.Status)

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Permission } from "@/permission"
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@ -10,7 +10,7 @@ import { described } from "./metadata"
const root = "/permission"
const ReplyPayload = Schema.Struct({
reply: PermissionLegacy.Reply,
reply: PermissionV1.Reply,
message: Schema.optional(Schema.String),
})
@ -20,7 +20,7 @@ export const PermissionApi = HttpApi.make("permission")
.add(
HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(PermissionLegacy.Request), "List of pending permissions"),
success: described(Schema.Array(PermissionV1.Request), "List of pending permissions"),
}).annotateMerge(
OpenApi.annotations({
identifier: "permission.list",
@ -29,7 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
}),
),
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
params: { requestID: PermissionLegacy.ID },
params: { requestID: PermissionV1.ID },
query: WorkspaceRoutingQuery,
payload: ReplyPayload,
success: described(Schema.Boolean, "Permission processed successfully"),

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Permission } from "@/permission"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Session } from "@/session/session"
import { MessageV2 } from "@/session/message-v2"
@ -48,7 +48,7 @@ export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
export const UpdatePayload = Schema.Struct({
title: Schema.optional(Schema.String),
metadata: Schema.optional(Session.Metadata),
permission: Schema.optional(PermissionLegacy.Ruleset),
permission: Schema.optional(PermissionV1.Ruleset),
time: Schema.optional(
Schema.Struct({
archived: Schema.optional(Session.ArchivedTimestamp),
@ -71,7 +71,7 @@ export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInp
export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"]))
export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"]))
export const PermissionResponsePayload = Schema.Struct({
response: PermissionLegacy.Reply,
response: PermissionV1.Reply,
})
export const SessionPaths = {
@ -178,7 +178,7 @@ export const SessionApi = HttpApi.make("session")
HttpApiEndpoint.get("messages", SessionPaths.messages, {
params: { sessionID: SessionID },
query: MessagesQuery,
success: described(Schema.Array(SessionLegacy.WithParts), "List of messages"),
success: described(Schema.Array(SessionV1.WithParts), "List of messages"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
OpenApi.annotations({
@ -190,7 +190,7 @@ export const SessionApi = HttpApi.make("session")
HttpApiEndpoint.get("message", SessionPaths.message, {
params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(SessionLegacy.WithParts, "Message"),
success: described(SessionV1.WithParts, "Message"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
OpenApi.annotations({
@ -316,7 +316,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: PromptPayload,
success: described(SessionLegacy.WithParts, "Created message"),
success: described(SessionV1.WithParts, "Created message"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
OpenApi.annotations({
@ -343,7 +343,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: CommandPayload,
success: described(SessionLegacy.WithParts, "Created message"),
success: described(SessionV1.WithParts, "Created message"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
OpenApi.annotations({
@ -356,7 +356,7 @@ export const SessionApi = HttpApi.make("session")
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: ShellPayload,
success: described(SessionLegacy.WithParts, "Created message"),
success: described(SessionV1.WithParts, "Created message"),
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
}).annotateMerge(
OpenApi.annotations({
@ -392,7 +392,7 @@ export const SessionApi = HttpApi.make("session")
}),
),
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
params: { sessionID: SessionID, permissionID: PermissionLegacy.ID },
params: { sessionID: SessionID, permissionID: PermissionV1.ID },
query: WorkspaceRoutingQuery,
payload: PermissionResponsePayload,
success: described(Schema.Boolean, "Permission processed successfully"),
@ -432,8 +432,8 @@ export const SessionApi = HttpApi.make("session")
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
query: WorkspaceRoutingQuery,
payload: SessionLegacy.Part,
success: described(SessionLegacy.Part, "Successfully updated part"),
payload: SessionV1.Part,
success: described(SessionV1.Part, "Successfully updated part"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
OpenApi.annotations({

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Permission } from "@/permission"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
@ -14,8 +14,8 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permiss
})
const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
params: { requestID: PermissionLegacy.ID }
payload: PermissionLegacy.ReplyBody
params: { requestID: PermissionV1.ID }
payload: PermissionV1.ReplyBody
}) {
yield* svc
.reply({

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Agent } from "@/agent/agent"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Command } from "@/command"
import { Permission } from "@/permission"
@ -360,7 +360,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
})
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
params: { sessionID: SessionID; permissionID: PermissionLegacy.ID }
params: { sessionID: SessionID; permissionID: PermissionV1.ID }
payload: typeof PermissionResponsePayload.Type
}) {
yield* requireSession(ctx.params.sessionID)
@ -396,10 +396,10 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
const updatePart = Effect.fn("SessionHttpApi.updatePart")(function* (ctx: {
params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
payload: typeof SessionLegacy.Part.Type
payload: typeof SessionV1.Part.Type
}) {
yield* requireSession(ctx.params.sessionID)
const payload = ctx.payload as SessionLegacy.Part
const payload = ctx.payload as SessionV1.Part
if (
payload.id !== ctx.params.partID ||
payload.messageID !== ctx.params.messageID ||

View file

@ -1,4 +1,5 @@
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Session } from "./session"
import { SessionID, MessageID, PartID } from "./schema"
import { Provider } from "@/provider/provider"
@ -93,9 +94,9 @@ type CompletedCompaction = {
summary: string | undefined
}
function summaryText(message: SessionLegacy.WithParts) {
function summaryText(message: SessionV1.WithParts) {
const text = message.parts
.filter((part): part is SessionLegacy.TextPart => part.type === "text")
.filter((part): part is SessionV1.TextPart => part.type === "text")
.map((part) => part.text.trim())
.filter(Boolean)
.join("\n\n")
@ -103,7 +104,7 @@ function summaryText(message: SessionLegacy.WithParts) {
return text || undefined
}
function completedCompactions(messages: SessionLegacy.WithParts[]) {
function completedCompactions(messages: SessionV1.WithParts[]) {
const users = new Map<MessageID, number>()
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
@ -134,14 +135,14 @@ function buildPrompt(input: { previousSummary?: string; context: string[] }) {
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
}
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
function preserveRecentBudget(input: { cfg: ConfigV1.Info; model: Provider.Model }) {
return (
input.cfg.compaction?.preserve_recent_tokens ??
Math.min(MAX_PRESERVE_RECENT_TOKENS, Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)))
)
}
function turns(messages: SessionLegacy.WithParts[]) {
function turns(messages: SessionV1.WithParts[]) {
const result: Turn[] = []
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
@ -160,11 +161,11 @@ function turns(messages: SessionLegacy.WithParts[]) {
}
function splitTurn(input: {
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
turn: Turn
model: Provider.Model
budget: number
estimate: (input: { messages: SessionLegacy.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
estimate: (input: { messages: SessionV1.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
}) {
return Effect.gen(function* () {
if (input.budget <= 0) return undefined
@ -186,13 +187,13 @@ function splitTurn(input: {
export interface Interface {
readonly isOverflow: (input: {
tokens: SessionLegacy.Assistant["tokens"]
tokens: SessionV1.Assistant["tokens"]
model: Provider.Model
}) => Effect.Effect<boolean>
readonly prune: (input: { sessionID: SessionID }) => Effect.Effect<void>
readonly process: (input: {
parentID: MessageID
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
sessionID: SessionID
auto: boolean
overflow?: boolean
@ -223,7 +224,7 @@ export const layer = Layer.effect(
const flags = yield* RuntimeFlags.Service
const isOverflow = Effect.fn("SessionCompaction.isOverflow")(function* (input: {
tokens: SessionLegacy.Assistant["tokens"]
tokens: SessionV1.Assistant["tokens"]
model: Provider.Model
}) {
return overflow({
@ -235,7 +236,7 @@ export const layer = Layer.effect(
})
const estimate = Effect.fn("SessionCompaction.estimate")(function* (input: {
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
model: Provider.Model
}) {
const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model)
@ -243,8 +244,8 @@ export const layer = Layer.effect(
})
const select = Effect.fn("SessionCompaction.select")(function* (input: {
messages: SessionLegacy.WithParts[]
cfg: Config.Info
messages: SessionV1.WithParts[]
cfg: ConfigV1.Info
model: Provider.Model
}) {
const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS
@ -307,7 +308,7 @@ export const layer = Layer.effect(
let total = 0
let pruned = 0
const toPrune: SessionLegacy.ToolPart[] = []
const toPrune: SessionV1.ToolPart[] = []
let turns = 0
loop: for (let msgIndex = msgs.length - 1; msgIndex >= 0; msgIndex--) {
@ -343,7 +344,7 @@ export const layer = Layer.effect(
const processCompaction = Effect.fn("SessionCompaction.process")(function* (input: {
parentID: MessageID
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
sessionID: SessionID
auto: boolean
overflow?: boolean
@ -354,14 +355,14 @@ export const layer = Layer.effect(
}
const userMessage = parent.info
const compactionPart = parent.parts.find(
(part): part is SessionLegacy.CompactionPart => part.type === "compaction",
(part): part is SessionV1.CompactionPart => part.type === "compaction",
)
let messages = input.messages
let replay:
| {
info: SessionLegacy.User
parts: SessionLegacy.Part[]
info: SessionV1.User
parts: SessionV1.Part[]
}
| undefined
if (input.overflow) {
@ -410,7 +411,7 @@ export const layer = Layer.effect(
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
})
const ctx = yield* InstanceState.context
const msg: SessionLegacy.Assistant = {
const msg: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
parentID: input.parentID,
@ -459,7 +460,7 @@ export const layer = Layer.effect(
})
if (result === "compact") {
processor.message.error = new SessionLegacy.ContextOverflowError({
processor.message.error = new SessionV1.ContextOverflowError({
message: replay
? "Conversation history too large to compact - exceeds model context limit"
: "Session too large to compact - context exceeds model limit even after stripping media",

View file

@ -1,5 +1,5 @@
import path from "path"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect, Layer, Context } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { Config } from "@/config/config"
@ -12,7 +12,7 @@ import { Global } from "@opencode-ai/core/global"
import type { MessageV2 } from "./message-v2"
import type { MessageID } from "./schema"
function extract(messages: SessionLegacy.WithParts[]) {
function extract(messages: SessionV1.WithParts[]) {
const paths = new Set<string>()
for (const msg of messages) {
for (const part of msg.parts) {
@ -35,7 +35,7 @@ export interface Interface {
readonly system: () => Effect.Effect<string[], FSUtil.Error>
readonly find: (dir: string) => Effect.Effect<string | undefined, FSUtil.Error>
readonly resolve: (
messages: SessionLegacy.WithParts[],
messages: SessionV1.WithParts[],
filepath: string,
messageID: MessageID,
) => Effect.Effect<{ filepath: string; content: string }[], FSUtil.Error>
@ -175,7 +175,7 @@ export const layer: Layer.Layer<
})
const resolve = Effect.fn("Instruction.resolve")(function* (
messages: SessionLegacy.WithParts[],
messages: SessionV1.WithParts[],
filepath: string,
messageID: MessageID,
) {
@ -230,7 +230,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(RuntimeFlags.defaultLayer),
)
export function loaded(messages: SessionLegacy.WithParts[]) {
export function loaded(messages: SessionV1.WithParts[]) {
return extract(messages)
}

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Provider } from "@/provider/provider"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Log } from "@opencode-ai/core/util/log"
import { Context, Effect, Layer } from "effect"
@ -33,12 +33,12 @@ const log = Log.create({ service: "llm" })
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
export type StreamInput = {
user: SessionLegacy.User
user: SessionV1.User
sessionID: string
parentSessionID?: string
model: Provider.Model
agent: Agent.Info
permission?: PermissionLegacy.Ruleset
permission?: PermissionV1.Ruleset
system: string[]
messages: ModelMessage[]
small?: boolean
@ -165,7 +165,7 @@ const live: Layer.Layer<
return { approved: true }
}
const id = PermissionLegacy.ID.ascending()
const id = PermissionV1.ID.ascending()
let unsub: EventV2.Unsubscribe | undefined
try {
unsub = await bridge.promise(

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import type { Auth } from "@/auth"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { RuntimeFlags } from "@/effect/runtime-flags"
import { InstanceState } from "@/effect/instance-state"
import { Permission } from "@/permission"
@ -18,12 +18,12 @@ import { mergeDeep } from "remeda"
const USER_AGENT = `opencode/${InstallationVersion}`
type PrepareInput = {
readonly user: SessionLegacy.User
readonly user: SessionV1.User
readonly sessionID: string
readonly parentSessionID?: string
readonly model: Provider.Model
readonly agent: Agent.Info
readonly permission?: PermissionLegacy.Ruleset
readonly permission?: PermissionV1.Ruleset
readonly system: string[]
readonly messages: ModelMessage[]
readonly small?: boolean

View file

@ -1,6 +1,6 @@
import { EventV2 } from "@opencode-ai/core/event"
import { SessionID, MessageID, PartID } from "./schema"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { ProviderV2 } from "@opencode-ai/core/provider"
import {
APIError,
@ -17,7 +17,7 @@ import {
User,
WithParts,
type ToolPart,
} from "@opencode-ai/core/session/legacy"
} from "@opencode-ai/core/v1/session"
import { NamedError } from "@opencode-ai/core/util/error"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
@ -56,9 +56,9 @@ function truncateToolOutput(text: string, maxChars?: number) {
}
export const Event = {
Updated: SessionLegacy.Event.MessageUpdated,
Removed: SessionLegacy.Event.MessageRemoved,
PartUpdated: SessionLegacy.Event.PartUpdated,
Updated: SessionV1.Event.MessageUpdated,
Removed: SessionV1.Event.MessageRemoved,
PartUpdated: SessionV1.Event.PartUpdated,
PartDelta: EventV2.define({
type: "message.part.delta",
schema: {
@ -69,7 +69,7 @@ export const Event = {
delta: Schema.String,
},
}),
PartRemoved: SessionLegacy.Event.PartRemoved,
PartRemoved: SessionV1.Event.PartRemoved,
}
const Cursor = Schema.Struct({

View file

@ -1,12 +1,13 @@
import type { Config } from "@/config/config"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import type { MessageV2 } from "./message-v2"
const COMPACTION_BUFFER = 20_000
export function usable(input: { cfg: Config.Info; model: Provider.Model; outputTokenMax?: number }) {
export function usable(input: { cfg: ConfigV1.Info; model: Provider.Model; outputTokenMax?: number }) {
const context = input.model.limit.context
if (context === 0) return 0
@ -19,8 +20,8 @@ export function usable(input: { cfg: Config.Info; model: Provider.Model; outputT
}
export function isOverflow(input: {
cfg: Config.Info
tokens: SessionLegacy.Assistant["tokens"]
cfg: ConfigV1.Info
tokens: SessionV1.Assistant["tokens"]
model: Provider.Model
outputTokenMax?: number
}) {

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Image } from "@/image/image"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Cause, Deferred, Effect, Exit, Layer, Context, Scope, Schema } from "effect"
import * as Stream from "effect/Stream"
import { Agent } from "@/agent/agent"
@ -37,25 +37,25 @@ const log = Log.create({ service: "session.processor" })
export type Result = "compact" | "stop" | "continue"
export interface Handle {
readonly message: SessionLegacy.Assistant
readonly message: SessionV1.Assistant
readonly updateToolCall: (
toolCallID: string,
update: (part: SessionLegacy.ToolPart) => SessionLegacy.ToolPart,
) => Effect.Effect<SessionLegacy.ToolPart | undefined>
update: (part: SessionV1.ToolPart) => SessionV1.ToolPart,
) => Effect.Effect<SessionV1.ToolPart | undefined>
readonly completeToolCall: (
toolCallID: string,
output: {
title: string
metadata: Record<string, any>
output: string
attachments?: SessionLegacy.FilePart[]
attachments?: SessionV1.FilePart[]
},
) => Effect.Effect<void>
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
}
type Input = {
assistantMessage: SessionLegacy.Assistant
assistantMessage: SessionV1.Assistant
sessionID: SessionID
model: Provider.Model
}
@ -65,9 +65,9 @@ export interface Interface {
}
type ToolCall = {
partID: SessionLegacy.ToolPart["id"]
messageID: SessionLegacy.ToolPart["messageID"]
sessionID: SessionLegacy.ToolPart["sessionID"]
partID: SessionV1.ToolPart["id"]
messageID: SessionV1.ToolPart["messageID"]
sessionID: SessionV1.ToolPart["sessionID"]
done: Deferred.Deferred<void>
inputEnded: boolean
}
@ -78,8 +78,8 @@ interface ProcessorContext extends Input {
snapshot: string | undefined
blocked: boolean
needsCompaction: boolean
currentText: SessionLegacy.TextPart | undefined
reasoningMap: Record<string, SessionLegacy.ReasoningPart>
currentText: SessionV1.TextPart | undefined
reasoningMap: Record<string, SessionV1.ReasoningPart>
}
type StreamEvent = LLMEvent
@ -153,7 +153,7 @@ export const layer = Layer.effect(
const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* (
toolCallID: string,
update: (part: SessionLegacy.ToolPart) => SessionLegacy.ToolPart,
update: (part: SessionV1.ToolPart) => SessionV1.ToolPart,
) {
const match = yield* readToolCall(toolCallID)
if (!match) return undefined
@ -173,7 +173,7 @@ export const layer = Layer.effect(
title: string
metadata: Record<string, any>
output: string
attachments?: SessionLegacy.FilePart[]
attachments?: SessionV1.FilePart[]
},
) {
const match = yield* readToolCall(toolCallID)
@ -205,7 +205,7 @@ export const layer = Layer.effect(
time: { start: match.part.state.time.start, end: Date.now() },
},
})
if (error instanceof PermissionLegacy.RejectedError || error instanceof Question.RejectedError) {
if (error instanceof PermissionV1.RejectedError || error instanceof Question.RejectedError) {
ctx.blocked = ctx.shouldBreak
}
yield* settleToolCall(toolCallID)
@ -268,7 +268,7 @@ export const layer = Layer.effect(
callID: input.id,
state: { status: "pending", input: {}, raw: "" },
metadata: input.providerExecuted ? { providerExecuted: true } : undefined,
} satisfies SessionLegacy.ToolPart)
} satisfies SessionV1.ToolPart)
ctx.toolcalls[input.id] = {
done: yield* Deferred.make<void>(),
partID: part.id,
@ -279,11 +279,11 @@ export const layer = Layer.effect(
return { call: ctx.toolcalls[input.id], part }
})
const isFilePart = (value: unknown): value is SessionLegacy.FilePart => Schema.is(SessionLegacy.FilePart)(value)
const isFilePart = (value: unknown): value is SessionV1.FilePart => Schema.is(SessionV1.FilePart)(value)
const toolResultOutput = (
value: Extract<StreamEvent, { type: "tool-result" }>,
): { title: string; metadata: Record<string, any>; output: string; attachments?: SessionLegacy.FilePart[] } => {
): { title: string; metadata: Record<string, any>; output: string; attachments?: SessionV1.FilePart[] } => {
if (isRecord(value.result.value) && typeof value.result.value.output === "string") {
return {
title: typeof value.result.value.title === "string" ? value.result.value.title : value.name,
@ -463,7 +463,7 @@ export const layer = Layer.effect(
),
Effect.exit,
)
: Effect.succeed(Exit.succeed<SessionLegacy.FilePart>(attachment)),
: Effect.succeed(Exit.succeed<SessionV1.FilePart>(attachment)),
)
const omitted = normalized.filter(Exit.isFailure).length
const attachments = normalized.filter(Exit.isSuccess).map((item) => item.value)
@ -486,7 +486,7 @@ export const layer = Layer.effect(
type: "text",
text: output.output,
},
...(output.attachments?.map((item: SessionLegacy.FilePart) => ({
...(output.attachments?.map((item: SessionV1.FilePart) => ({
type: "file" as const,
uri: item.url,
mime: item.mime,
@ -753,7 +753,7 @@ export const layer = Layer.effect(
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
const error = parse(e)
if (SessionLegacy.ContextOverflowError.isInstance(error)) {
if (SessionV1.ContextOverflowError.isInstance(error)) {
ctx.needsCompaction = true
yield* events.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
return

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import path from "path"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import os from "os"
import { SessionID, MessageID, PartID } from "./schema"
import { MessageV2 } from "./message-v2"
@ -66,8 +66,8 @@ import { LLMEvent } from "@opencode-ai/llm"
// @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false
const decodeMessageInfo = Schema.decodeUnknownExit(SessionLegacy.Info)
const decodeMessagePart = Schema.decodeUnknownExit(SessionLegacy.Part)
const decodeMessageInfo = Schema.decodeUnknownExit(SessionV1.Info)
const decodeMessagePart = Schema.decodeUnknownExit(SessionV1.Part)
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
@ -82,7 +82,7 @@ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested struc
const log = Log.create({ service: "session.prompt" })
const elog = EffectLogger.create({ service: "session.prompt" })
function isOrphanedInterruptedTool(part: SessionLegacy.ToolPart) {
function isOrphanedInterruptedTool(part: SessionV1.ToolPart) {
// cleanup() marks abandoned tool_use blocks this way after retries/aborts.
// They are not pending work and must not trigger an assistant-prefill request.
return part.state.status === "error" && part.state.metadata?.interrupted === true
@ -90,10 +90,10 @@ function isOrphanedInterruptedTool(part: SessionLegacy.ToolPart) {
export interface Interface {
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error>
readonly loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts>
readonly shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError>
readonly command: (input: CommandInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error>
readonly prompt: (input: PromptInput) => Effect.Effect<SessionV1.WithParts, Image.Error>
readonly loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts>
readonly shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError>
readonly command: (input: CommandInput) => Effect.Effect<SessionV1.WithParts, Image.Error>
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
}
@ -239,14 +239,14 @@ export const layer = Layer.effect(
const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
session: Session.Info
history: SessionLegacy.WithParts[]
history: SessionV1.WithParts[]
providerID: ProviderV2.ID
modelID: ProviderV2.ModelID
}) {
if (input.session.parentID) return
if (!Session.isDefaultTitle(input.session.title)) return
const real = (m: SessionLegacy.WithParts) =>
const real = (m: SessionV1.WithParts) =>
m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
const idx = input.history.findIndex(real)
if (idx === -1) return
@ -257,7 +257,7 @@ export const layer = Layer.effect(
if (!firstUser || firstUser.info.role !== "user") return
const firstInfo = firstUser.info
const subtasks = firstUser.parts.filter((p): p is SessionLegacy.SubtaskPart => p.type === "subtask")
const subtasks = firstUser.parts.filter((p): p is SessionV1.SubtaskPart => p.type === "subtask")
const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
const ag = yield* agents.get("title")
@ -300,19 +300,19 @@ export const layer = Layer.effect(
})
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
task: SessionLegacy.SubtaskPart
task: SessionV1.SubtaskPart
model: Provider.Model
lastUser: SessionLegacy.User
lastUser: SessionV1.User
sessionID: SessionID
session: Session.Info
msgs: SessionLegacy.WithParts[]
msgs: SessionV1.WithParts[]
}) {
const { task, model, lastUser, sessionID, session, msgs } = input
const ctx = yield* InstanceState.context
const promptOps = yield* ops()
const { task: taskTool } = yield* registry.named()
const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
const assistantMessage: SessionLegacy.Assistant = yield* sessions.updateMessage({
const assistantMessage: SessionV1.Assistant = yield* sessions.updateMessage({
id: MessageID.ascending(),
role: "assistant",
parentID: lastUser.id,
@ -327,7 +327,7 @@ export const layer = Layer.effect(
providerID: taskModel.providerID,
time: { created: Date.now() },
})
let part: SessionLegacy.ToolPart = yield* sessions.updatePart({
let part: SessionV1.ToolPart = yield* sessions.updatePart({
id: PartID.ascending(),
messageID: assistantMessage.id,
sessionID: assistantMessage.sessionID,
@ -383,7 +383,7 @@ export const layer = Layer.effect(
...part,
type: "tool",
state: { ...part.state, ...val },
} satisfies SessionLegacy.ToolPart)
} satisfies SessionV1.ToolPart)
}),
ask: (req: any) =>
permission
@ -417,7 +417,7 @@ export const layer = Layer.effect(
metadata: part.state.metadata,
input: part.state.input,
},
} satisfies SessionLegacy.ToolPart)
} satisfies SessionV1.ToolPart)
}
}),
),
@ -452,7 +452,7 @@ export const layer = Layer.effect(
attachments,
time: { ...part.state.time, end: Date.now() },
},
} satisfies SessionLegacy.ToolPart)
} satisfies SessionV1.ToolPart)
}
if (!result) {
@ -468,12 +468,12 @@ export const layer = Layer.effect(
metadata: part.state.status === "pending" ? undefined : part.state.metadata,
input: part.state.input,
},
} satisfies SessionLegacy.ToolPart)
} satisfies SessionV1.ToolPart)
}
if (!task.command) return
const summaryUserMsg: SessionLegacy.User = {
const summaryUserMsg: SessionV1.User = {
id: MessageID.ascending(),
sessionID,
role: "user",
@ -489,7 +489,7 @@ export const layer = Layer.effect(
type: "text",
text: "Summarize the task tool output above and continue with your task.",
synthetic: true,
} satisfies SessionLegacy.TextPart)
} satisfies SessionV1.TextPart)
})
const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput, ready?: Latch.Latch) {
@ -511,7 +511,7 @@ export const layer = Layer.effect(
throw error
}
const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID))
const userMsg: SessionLegacy.User = {
const userMsg: SessionV1.User = {
id: input.messageID ?? MessageID.ascending(),
sessionID: input.sessionID,
time: { created: Date.now() },
@ -520,7 +520,7 @@ export const layer = Layer.effect(
model: { providerID: model.providerID, modelID: model.modelID },
}
yield* sessions.updateMessage(userMsg)
const userPart: SessionLegacy.Part = {
const userPart: SessionV1.Part = {
type: "text",
id: PartID.ascending(),
messageID: userMsg.id,
@ -530,7 +530,7 @@ export const layer = Layer.effect(
}
yield* sessions.updatePart(userPart)
const msg: SessionLegacy.Assistant = {
const msg: SessionV1.Assistant = {
id: MessageID.ascending(),
sessionID: input.sessionID,
parentID: userMsg.id,
@ -546,7 +546,7 @@ export const layer = Layer.effect(
}
yield* sessions.updateMessage(msg)
const started = Date.now()
const part: SessionLegacy.ToolPart = {
const part: SessionV1.ToolPart = {
type: "tool",
id: PartID.ascending(),
messageID: msg.id,
@ -719,7 +719,7 @@ export const layer = Layer.effect(
: undefined
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
const info: SessionLegacy.User = {
const info: SessionV1.User = {
id: input.messageID ?? MessageID.ascending(),
role: "user",
sessionID: input.sessionID,
@ -760,8 +760,8 @@ export const layer = Layer.effect(
yield* Effect.addFinalizer(() => instruction.clear(info.id))
type Draft<T> = T extends SessionLegacy.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<SessionLegacy.Part>): SessionLegacy.Part => ({
type Draft<T> = T extends SessionV1.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<SessionV1.Part>): SessionV1.Part => ({
...part,
id: part.id ? PartID.make(part.id) : PartID.ascending(),
})
@ -790,14 +790,14 @@ export const layer = Layer.effect(
})
})
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionLegacy.Part>[]> = Effect.fn(
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionV1.Part>[]> = Effect.fn(
"SessionPrompt.resolveUserPart",
)(function* (part) {
if (part.type === "file") {
if (part.source?.type === "resource") {
const { clientName, uri } = part.source
log.info("mcp resource", { clientName, uri, mime: part.mime })
const pieces: Draft<SessionLegacy.Part>[] = [
const pieces: Draft<SessionV1.Part>[] = [
{
messageID: info.id,
sessionID: input.sessionID,
@ -917,7 +917,7 @@ export const layer = Layer.effect(
if (end) limit = end - (offset - 1)
}
const args = { filePath: filepath, offset, limit }
const pieces: Draft<SessionLegacy.Part>[] = [
const pieces: Draft<SessionV1.Part>[] = [
...(referenceContext
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
: []),
@ -1213,7 +1213,7 @@ export const layer = Layer.effect(
return { info, parts }
}, Effect.scoped)
const prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error> = Effect.fn(
const prompt: (input: PromptInput) => Effect.Effect<SessionV1.WithParts, Image.Error> = Effect.fn(
"SessionPrompt.prompt",
)(function* (input: PromptInput) {
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
@ -1221,7 +1221,7 @@ export const layer = Layer.effect(
const message = yield* createUserMessage(input)
yield* sessions.touch(input.sessionID)
const permissions: PermissionLegacy.Rule[] = []
const permissions: PermissionV1.Rule[] = []
for (const [t, enabled] of Object.entries(input.tools ?? {})) {
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
}
@ -1242,7 +1242,7 @@ export const layer = Layer.effect(
throw new Error("Impossible")
})
const runLoop: (sessionID: SessionID) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.run")(
const runLoop: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.run")(
function* (sessionID: SessionID) {
const ctx = yield* InstanceState.context
const slog = elog.with({ sessionID })
@ -1280,7 +1280,7 @@ export const layer = Layer.effect(
lastUser.id < lastAssistant.id
) {
const orphan = lastAssistantMsg?.parts.find(
(part): part is SessionLegacy.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
(part): part is SessionV1.ToolPart => part.type === "tool" && isOrphanedInterruptedTool(part),
)
if (orphan) {
yield* slog.warn("loop exit with orphaned interrupted tool", {
@ -1347,7 +1347,7 @@ export const layer = Layer.effect(
Effect.provideService(Session.Service, sessions),
)
const msg: SessionLegacy.Assistant = {
const msg: SessionV1.Assistant = {
id: MessageID.ascending(),
parentID: lastUser.id,
role: "assistant",
@ -1467,7 +1467,7 @@ export const layer = Layer.effect(
const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
if (finished && !handle.message.error) {
if (format.type === "json_schema") {
handle.message.error = new SessionLegacy.StructuredOutputError({
handle.message.error = new SessionV1.StructuredOutputError({
message: "Model did not produce structured output",
retries: 0,
}).toObject()
@ -1500,13 +1500,13 @@ export const layer = Layer.effect(
},
)
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.loop")(
const loop: (input: LoopInput) => Effect.Effect<SessionV1.WithParts> = Effect.fn("SessionPrompt.loop")(
function* (input: LoopInput) {
return yield* state.ensureRunning(input.sessionID, lastAssistant(input.sessionID), runLoop(input.sessionID))
},
)
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
const shell: (input: ShellInput) => Effect.Effect<SessionV1.WithParts, Session.BusyError> = Effect.fn(
"SessionPrompt.shell",
)(function* (input: ShellInput) {
const ready = yield* Latch.make()
@ -1691,15 +1691,15 @@ export const PromptInput = Schema.Struct({
description:
"@deprecated tools and permissions have been merged, you can set permissions on the session itself now",
}),
format: Schema.optional(SessionLegacy.Format),
format: Schema.optional(SessionV1.Format),
system: Schema.optional(Schema.String),
variant: Schema.optional(Schema.String),
parts: Schema.Array(
Schema.Union([
SessionLegacy.TextPartInput,
SessionLegacy.FilePartInput,
SessionLegacy.AgentPartInput,
SessionLegacy.SubtaskPartInput,
SessionV1.TextPartInput,
SessionV1.FilePartInput,
SessionV1.AgentPartInput,
SessionV1.SubtaskPartInput,
]).annotate({ discriminator: "type" }),
),
})
@ -1738,7 +1738,7 @@ export const CommandInput = Schema.Struct({
mime: Schema.String,
filename: Schema.optional(Schema.String),
url: Schema.String,
source: Schema.optional(SessionLegacy.FilePartSource),
source: Schema.optional(SessionV1.FilePartSource),
}),
]).annotate({ discriminator: "type" }),
),

View file

@ -1,5 +1,5 @@
import { Option, Schema } from "effect"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { MessageV2 } from "../message-v2"
import { Reference } from "@/reference/reference"
@ -34,7 +34,7 @@ export function referenceTextPart(input: {
target?: string
targetPath?: string
problem?: string
}): SessionLegacy.TextPartInput {
}): SessionV1.TextPartInput {
const metadata: ReferencePromptMetadata = {
name: input.reference.name,
kind: input.reference.kind,

View file

@ -1,5 +1,5 @@
import path from "path"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect } from "effect"
import { Agent } from "@/agent/agent"
import { FSUtil } from "@opencode-ai/core/fs-util"
@ -13,7 +13,7 @@ import BUILD_SWITCH from "./prompt/build-switch.txt"
import PLAN_MODE from "./prompt/plan-mode.txt"
export const apply = Effect.fn("SessionReminders.apply")(function* (input: {
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
agent: Agent.Info
session: Session.Info
}) {

View file

@ -1,5 +1,5 @@
import type { NamedError } from "@opencode-ai/core/util/error"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
import { MessageV2 } from "./message-v2"
import { iife } from "@/util/iife"
@ -32,7 +32,7 @@ function cap(ms: number) {
return Math.min(ms, RETRY_MAX_DELAY)
}
export function delay(attempt: number, error?: SessionLegacy.APIError) {
export function delay(attempt: number, error?: SessionV1.APIError) {
if (error) {
const headers = error.data.responseHeaders
if (headers) {
@ -67,8 +67,8 @@ export function delay(attempt: number, error?: SessionLegacy.APIError) {
export function retryable(error: Err, provider: string) {
// context overflow errors should not be retried
if (SessionLegacy.ContextOverflowError.isInstance(error)) return undefined
if (SessionLegacy.APIError.isInstance(error)) {
if (SessionV1.ContextOverflowError.isInstance(error)) return undefined
if (SessionV1.APIError.isInstance(error)) {
const status = error.data.statusCode
// 5xx errors are transient server failures and should always be retried,
// even when the provider SDK doesn't explicitly mark them as retryable.
@ -184,7 +184,7 @@ export function policy(opts: {
const retry = retryable(error, opts.provider)
if (!retry) return Cause.done(meta.attempt)
return Effect.gen(function* () {
const wait = delay(meta.attempt, SessionLegacy.APIError.isInstance(error) ? error : undefined)
const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined)
const now = yield* Clock.currentTimeMillis
yield* opts.set({
attempt: meta.attempt,

View file

@ -1,5 +1,5 @@
import { Effect, Layer, Context, Schema } from "effect"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Snapshot } from "../snapshot"
import { Storage } from "@/storage/storage"
@ -40,7 +40,7 @@ export const layer = Layer.effect(
const revert = Effect.fn("SessionRevert.revert")(function* (input: RevertInput) {
yield* state.assertNotBusy(input.sessionID)
const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
let lastUser: SessionLegacy.User | undefined
let lastUser: SessionV1.User | undefined
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
let rev: Session.Info["revert"]
@ -104,8 +104,8 @@ export const layer = Layer.effect(
const sessionID = session.id
const msgs = yield* sessions.messages({ sessionID }).pipe(Effect.orDie)
const messageID = session.revert.messageID
const remove = [] as SessionLegacy.WithParts[]
let target: SessionLegacy.WithParts | undefined
const remove = [] as SessionV1.WithParts[]
let target: SessionV1.WithParts | undefined
for (const msg of msgs) {
if (msg.info.id < messageID) continue
if (msg.info.id > messageID) {

View file

@ -1,5 +1,5 @@
import { InstanceState } from "@/effect/instance-state"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Runner } from "@/effect/runner"
import { BackgroundJob } from "@/background/job"
import { Effect, Latch, Layer, Scope, Context } from "effect"
@ -13,15 +13,15 @@ export interface Interface {
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
readonly ensureRunning: (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
work: Effect.Effect<SessionLegacy.WithParts>,
) => Effect.Effect<SessionLegacy.WithParts>
onInterrupt: Effect.Effect<SessionV1.WithParts>,
work: Effect.Effect<SessionV1.WithParts>,
) => Effect.Effect<SessionV1.WithParts>
readonly startShell: (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
work: Effect.Effect<SessionLegacy.WithParts>,
onInterrupt: Effect.Effect<SessionV1.WithParts>,
work: Effect.Effect<SessionV1.WithParts>,
ready?: Latch.Latch,
) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError>
) => Effect.Effect<SessionV1.WithParts, Session.BusyError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunState") {}
@ -35,7 +35,7 @@ export const layer = Layer.effect(
const state = yield* InstanceState.make(
Effect.fn("SessionRunState.state")(function* () {
const scope = yield* Scope.Scope
const runners = new Map<SessionID, Runner.Runner<SessionLegacy.WithParts>>()
const runners = new Map<SessionID, Runner.Runner<SessionV1.WithParts>>()
yield* Effect.addFinalizer(
Effect.fnUntraced(function* () {
yield* Effect.forEach(runners.values(), (runner) => runner.cancel, {
@ -51,12 +51,12 @@ export const layer = Layer.effect(
const runner = Effect.fn("SessionRunState.runner")(function* (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
onInterrupt: Effect.Effect<SessionV1.WithParts>,
) {
const data = yield* InstanceState.get(state)
const existing = data.runners.get(sessionID)
if (existing) return existing
const next = Runner.make<SessionLegacy.WithParts>(data.scope, {
const next = Runner.make<SessionV1.WithParts>(data.scope, {
onIdle: Effect.gen(function* () {
data.runners.delete(sessionID)
yield* status.set(sessionID, { type: "idle" })
@ -87,16 +87,16 @@ export const layer = Layer.effect(
const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
work: Effect.Effect<SessionLegacy.WithParts>,
onInterrupt: Effect.Effect<SessionV1.WithParts>,
work: Effect.Effect<SessionV1.WithParts>,
) {
return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work)
})
const startShell = Effect.fn("SessionRunState.startShell")(function* (
sessionID: SessionID,
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
work: Effect.Effect<SessionLegacy.WithParts>,
onInterrupt: Effect.Effect<SessionV1.WithParts>,
work: Effect.Effect<SessionV1.WithParts>,
ready?: Latch.Latch,
) {
return yield* (yield* runner(sessionID, onInterrupt))

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Slug } from "@opencode-ai/core/util/slug"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import path from "path"
import { BackgroundJob } from "@/background/job"
@ -234,7 +234,7 @@ export const Info = Schema.Struct({
version: Schema.String,
metadata: optionalOmitUndefined(Metadata),
time: Time,
permission: optionalOmitUndefined(PermissionLegacy.Ruleset),
permission: optionalOmitUndefined(PermissionV1.Ruleset),
revert: optionalOmitUndefined(Revert),
}).annotate({ identifier: "Session" })
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
@ -259,7 +259,7 @@ export const CreateInput = Schema.optional(
agent: Schema.optional(Schema.String),
model: Schema.optional(Model),
metadata: Schema.optional(Metadata),
permission: Schema.optional(PermissionLegacy.Ruleset),
permission: Schema.optional(PermissionV1.Ruleset),
workspaceID: Schema.optional(WorkspaceV2.ID),
}),
)
@ -283,7 +283,7 @@ export const SetMetadataInput = Schema.Struct({
})
export const SetPermissionInput = Schema.Struct({
sessionID: SessionID,
permission: PermissionLegacy.Ruleset,
permission: PermissionV1.Ruleset,
})
export const SetRevertInput = Schema.Struct({
sessionID: SessionID,
@ -349,7 +349,7 @@ const UpdatedInfo = Schema.Struct({
version: Schema.optional(Schema.NullOr(Schema.String)),
metadata: Schema.optional(Schema.NullOr(Metadata)),
time: Schema.optional(UpdatedTime),
permission: Schema.optional(Schema.NullOr(PermissionLegacy.Ruleset)),
permission: Schema.optional(Schema.NullOr(PermissionV1.Ruleset)),
revert: Schema.optional(Schema.NullOr(Revert)),
})
@ -359,9 +359,9 @@ const UpdatedEventSchema = Schema.Struct({
})
export const Event = {
Created: SessionLegacy.Event.Created,
Updated: SessionLegacy.Event.Updated,
Deleted: SessionLegacy.Event.Deleted,
Created: SessionV1.Event.Created,
Updated: SessionV1.Event.Updated,
Deleted: SessionV1.Event.Deleted,
Diff: EventV2.define({
type: "session.diff",
schema: {
@ -373,9 +373,9 @@ export const Event = {
type: "session.error",
schema: {
sessionID: Schema.optional(SessionID),
// Reuses SessionLegacy.Assistant.fields.error (already Schema.optional) so
// Reuses SessionV1.Assistant.fields.error (already Schema.optional) so
// the derived schema keeps the same discriminated-union shape on the event stream.
error: SessionLegacy.Assistant.fields.error,
error: SessionV1.Assistant.fields.error,
},
}),
}
@ -473,7 +473,7 @@ export interface Interface {
agent?: string
model?: Schema.Schema.Type<typeof Model>
metadata?: typeof Metadata.Type
permission?: PermissionLegacy.Ruleset
permission?: PermissionV1.Ruleset
workspaceID?: WorkspaceV2.ID
}) => Effect.Effect<Info>
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
@ -482,7 +482,7 @@ export interface Interface {
readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
readonly setMetadata: (input: typeof SetMetadataInput.Type) => Effect.Effect<void>
readonly setPermission: (input: { sessionID: SessionID; permission: PermissionLegacy.Ruleset }) => Effect.Effect<void>
readonly setPermission: (input: { sessionID: SessionID; permission: PermissionV1.Ruleset }) => Effect.Effect<void>
readonly setRevert: (input: {
sessionID: SessionID
revert: Info["revert"]
@ -496,18 +496,18 @@ export interface Interface {
readonly messages: (input: {
sessionID: SessionID
limit?: number
}) => Effect.Effect<SessionLegacy.WithParts[], NotFound>
}) => Effect.Effect<SessionV1.WithParts[], NotFound>
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
readonly remove: (sessionID: SessionID) => Effect.Effect<void, NotFound>
readonly updateMessage: <T extends SessionLegacy.Info>(msg: T) => Effect.Effect<T>
readonly updateMessage: <T extends SessionV1.Info>(msg: T) => Effect.Effect<T>
readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<MessageID>
readonly removePart: (input: { sessionID: SessionID; messageID: MessageID; partID: PartID }) => Effect.Effect<PartID>
readonly getPart: (input: {
sessionID: SessionID
messageID: MessageID
partID: PartID
}) => Effect.Effect<SessionLegacy.Part | undefined>
readonly updatePart: <T extends SessionLegacy.Part>(part: T) => Effect.Effect<T>
}) => Effect.Effect<SessionV1.Part | undefined>
readonly updatePart: <T extends SessionV1.Part>(part: T) => Effect.Effect<T>
readonly updatePartDelta: (input: {
sessionID: SessionID
messageID: MessageID
@ -518,8 +518,8 @@ export interface Interface {
/** Finds the first message matching the predicate, searching newest-first. */
readonly findMessage: (
sessionID: SessionID,
predicate: (msg: SessionLegacy.WithParts) => boolean,
) => Effect.Effect<Option.Option<SessionLegacy.WithParts>, NotFound>
predicate: (msg: SessionV1.WithParts) => boolean,
) => Effect.Effect<Option.Option<SessionV1.WithParts>, NotFound>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
@ -571,7 +571,7 @@ export const layer: Layer.Layer<
directory: string
path?: string
metadata?: typeof Metadata.Type
permission?: PermissionLegacy.Ruleset
permission?: PermissionV1.Ruleset
}) {
const ctx = yield* InstanceState.context
const result: Info = {
@ -598,7 +598,7 @@ export const layer: Layer.Layer<
log.info("created", result)
yield* events.publish(
SessionLegacy.Event.Created,
SessionV1.Event.Created,
{ sessionID: result.id, info: result },
{ location: eventLocation(result) },
)
@ -689,7 +689,7 @@ export const layer: Layer.Layer<
}
yield* events.publish(
SessionLegacy.Event.Deleted,
SessionV1.Event.Deleted,
{ sessionID, info: session },
{ location: eventLocation(session) },
)
@ -699,18 +699,18 @@ export const layer: Layer.Layer<
}
})
const updateMessage = <T extends SessionLegacy.Info>(msg: T): Effect.Effect<T> =>
const updateMessage = <T extends SessionV1.Info>(msg: T): Effect.Effect<T> =>
Effect.gen(function* () {
const location = yield* locationForSession(msg.sessionID)
yield* events.publish(SessionLegacy.Event.MessageUpdated, { sessionID: msg.sessionID, info: msg }, { location })
yield* events.publish(SessionV1.Event.MessageUpdated, { sessionID: msg.sessionID, info: msg }, { location })
return msg
}).pipe(Effect.withSpan("Session.updateMessage"))
const updatePart = <T extends SessionLegacy.Part>(part: T): Effect.Effect<T> =>
const updatePart = <T extends SessionV1.Part>(part: T): Effect.Effect<T> =>
Effect.gen(function* () {
const location = yield* locationForSession(part.sessionID)
yield* events.publish(
SessionLegacy.Event.PartUpdated,
SessionV1.Event.PartUpdated,
{
sessionID: part.sessionID,
part: structuredClone(part),
@ -740,7 +740,7 @@ export const layer: Layer.Layer<
id: row.id,
sessionID: row.session_id,
messageID: row.message_id,
} as SessionLegacy.Part
} as SessionV1.Part
})
const create = Effect.fn("Session.create")(function* (input?: {
@ -749,7 +749,7 @@ export const layer: Layer.Layer<
agent?: string
model?: Schema.Schema.Type<typeof Model>
metadata?: typeof Metadata.Type
permission?: PermissionLegacy.Ruleset
permission?: PermissionV1.Ruleset
workspaceID?: WorkspaceV2.ID
}) {
const ctx = yield* InstanceState.context
@ -795,7 +795,7 @@ export const layer: Layer.Layer<
})
for (const part of msg.parts) {
const p: SessionLegacy.Part = {
const p: SessionV1.Part = {
...part,
id: PartID.ascending(),
messageID: cloned.id,
@ -822,7 +822,7 @@ export const layer: Layer.Layer<
revert: info.revert === null ? undefined : (info.revert ?? current.revert),
permission: info.permission === null ? undefined : (info.permission ?? current.permission),
} as Info
yield* events.publish(SessionLegacy.Event.Updated, { sessionID, info: next }, { location: eventLocation(next) })
yield* events.publish(SessionV1.Event.Updated, { sessionID, info: next }, { location: eventLocation(next) })
})
const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) {
@ -843,7 +843,7 @@ export const layer: Layer.Layer<
const setPermission = Effect.fn("Session.setPermission")(function* (input: {
sessionID: SessionID
permission: PermissionLegacy.Ruleset
permission: PermissionV1.Ruleset
}) {
yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } }).pipe(
Effect.orDie,
@ -899,7 +899,7 @@ export const layer: Layer.Layer<
}
const size = 50
const result = [] as SessionLegacy.WithParts[]
const result = [] as SessionV1.WithParts[]
let before: string | undefined
while (true) {
const page = yield* MessageV2.page({ sessionID: input.sessionID, limit: size, before }).pipe(
@ -922,7 +922,7 @@ export const layer: Layer.Layer<
}) {
const location = yield* locationForSession(input.sessionID)
yield* events.publish(
SessionLegacy.Event.MessageRemoved,
SessionV1.Event.MessageRemoved,
{
sessionID: input.sessionID,
messageID: input.messageID,
@ -939,7 +939,7 @@ export const layer: Layer.Layer<
}) {
const location = yield* locationForSession(input.sessionID)
yield* events.publish(
SessionLegacy.Event.PartRemoved,
SessionV1.Event.PartRemoved,
{
sessionID: input.sessionID,
messageID: input.messageID,
@ -976,7 +976,7 @@ export const layer: Layer.Layer<
if (!page.more || !page.cursor) break
before = page.cursor
}
return Option.none<SessionLegacy.WithParts>()
return Option.none<SessionV1.WithParts>()
})
return Service.of({

View file

@ -1,5 +1,5 @@
import { Effect, Layer, Context, Schema } from "effect"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { EventV2Bridge } from "@/event-v2-bridge"
import { Snapshot } from "@/snapshot"
import { Session } from "./session"
@ -65,7 +65,7 @@ function unquoteGitPath(input: string) {
export interface Interface {
readonly summarize: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<void>
readonly diff: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Snapshot.FileDiff[]>
readonly computeDiff: (input: { messages: SessionLegacy.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
readonly computeDiff: (input: { messages: SessionV1.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionSummary") {}
@ -79,7 +79,7 @@ export const layer = Layer.effect(
const config = yield* Config.Service
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
}) {
let from: string | undefined
let to: string | undefined

View file

@ -1,5 +1,5 @@
import { Agent } from "@/agent/agent"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { MCP } from "@/mcp"
@ -29,7 +29,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
session: Session.Info
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
bypassAgentCheck: boolean
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
promptOps: TaskPromptOps
}) {
using _ = log.time("resolveTools")
@ -153,7 +153,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
)
const textParts: string[] = []
const attachments: Omit<SessionLegacy.FilePart, "id" | "sessionID" | "messageID">[] = []
const attachments: Omit<SessionV1.FilePart, "id" | "sessionID" | "messageID">[] = []
for (const contentItem of result.content) {
if (contentItem.type === "text") textParts.push(contentItem.text)
else if (contentItem.type === "image") {

View file

@ -9,6 +9,7 @@ import { Global } from "@opencode-ai/core/global"
import { Permission } from "@/permission"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Config } from "@/config/config"
import { FrontmatterError } from "@opencode-ai/core/v1/config/error"
import { ConfigMarkdown } from "@/config/markdown"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Glob } from "@opencode-ai/core/util/glob"
@ -108,7 +109,7 @@ const add = Effect.fnUntraced(function* (state: State, match: string, events: Ev
}).pipe(
Effect.catch(
Effect.fnUntraced(function* (err) {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
const message = FrontmatterError.isInstance(err)
? err.data.message
: `Failed to parse skill ${match}`
const { Session } = yield* Effect.promise(() => import("@/session/session"))

View file

@ -1,5 +1,5 @@
import path from "path"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect, Schema } from "effect"
import * as Tool from "./tool"
import { Question } from "../question"
@ -50,7 +50,7 @@ export const PlanExitTool = Tool.define(
const model =
lastUser?.info.role === "user" && lastUser.info.model ? lastUser.info.model : yield* provider.defaultModel()
const msg: SessionLegacy.User = {
const msg: SessionV1.User = {
id: MessageID.ascending(),
sessionID: ctx.sessionID,
role: "user",
@ -66,7 +66,7 @@ export const PlanExitTool = Tool.define(
type: "text",
text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`,
synthetic: true,
} satisfies SessionLegacy.TextPart)
} satisfies SessionV1.TextPart)
return {
title: "Switching to build agent",

View file

@ -1,7 +1,7 @@
import * as Tool from "./tool"
import DESCRIPTION from "./task.txt"
import { ToolJsonSchema } from "./json-schema"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { BackgroundJob } from "@/background/job"
import { Session } from "@/session/session"
import { SessionID, MessageID } from "../session/schema"
@ -18,7 +18,7 @@ import { Database } from "@opencode-ai/core/database/database"
export interface TaskPromptOps {
cancel(sessionID: SessionID): Effect.Effect<void>
resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
prompt(input: SessionPrompt.PromptInput): Effect.Effect<SessionLegacy.WithParts>
prompt(input: SessionPrompt.PromptInput): Effect.Effect<SessionV1.WithParts>
}
const id = "task"

View file

@ -1,6 +1,6 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Effect, Schema } from "effect"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { JSONSchema7 } from "@ai-sdk/provider"
import type { MessageV2 } from "../session/message-v2"
import type { Permission } from "../permission"
@ -40,16 +40,16 @@ export type Context<M extends Metadata = Metadata> = {
abort: AbortSignal
callID?: string
extra?: { [key: string]: unknown }
messages: SessionLegacy.WithParts[]
messages: SessionV1.WithParts[]
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
ask(input: Omit<PermissionLegacy.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
ask(input: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
}
export interface ExecuteResult<M extends Metadata = Metadata> {
title: string
metadata: M
output: string
attachments?: Omit<SessionLegacy.FilePart, "id" | "sessionID" | "messageID">[]
attachments?: Omit<SessionV1.FilePart, "id" | "sessionID" | "messageID">[]
}
export interface Def<

View file

@ -9,7 +9,7 @@ import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Global } from "@opencode-ai/core/global"
import { Permission } from "../../src/permission"
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider/provider"
import { Skill } from "../../src/skill"
@ -28,7 +28,7 @@ const agentLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
const it = testEffect(agentLayer())
// Helper to evaluate permission for a tool with wildcard pattern
function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionLegacy.Action | undefined {
function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionV1.Action | undefined {
if (!agent) return undefined
return Permission.evaluate(permission, "*", agent.permission).action
}

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
/**
* Reproducer for opencode issue #26514:
*
@ -61,7 +61,7 @@ it.instance("[#26514] subagent spawned from plan mode inherits read-only restric
// session's `permission` field is empty (Plan Mode lives on the agent
// ruleset, not the session). So we pass [] through as the parent
// session permission, exactly like the actual code path.
const parentSessionPermission: PermissionLegacy.Ruleset = []
const parentSessionPermission: PermissionV1.Ruleset = []
const subagentSessionPermission = deriveSubagentSessionPermission({
parentSessionPermission,
@ -89,7 +89,7 @@ it.instance("[#26514] explore subagent launched from plan mode also stays read-o
expect(planAgent).toBeDefined()
expect(explore).toBeDefined()
const parentSessionPermission: PermissionLegacy.Ruleset = []
const parentSessionPermission: PermissionV1.Ruleset = []
const subagentSessionPermission = deriveSubagentSessionPermission({
parentSessionPermission,
parentAgent: planAgent,
@ -114,7 +114,7 @@ it.instance(
expect(planAgent).toBeDefined()
expect(my).toBeDefined()
const parentSessionPermission: PermissionLegacy.Ruleset = []
const parentSessionPermission: PermissionV1.Ruleset = []
const subagentSessionPermission = deriveSubagentSessionPermission({
parentSessionPermission,
parentAgent: planAgent,

View file

@ -5,7 +5,7 @@
*/
import { describe, expect, test } from "bun:test"
import { aggregateFailures } from "@/cli/cmd/tui/context/aggregate-failures"
import { ConfigError } from "@/config/error"
import { ConfigErrorV1 } from "@opencode-ai/core/v1/config/error"
describe("aggregateFailures", () => {
test("returns null when every result is fulfilled", () => {
@ -43,7 +43,7 @@ describe("aggregateFailures", () => {
})
test("formats structured config errors hidden inside SDK error causes", () => {
const configError = new ConfigError.InvalidError({
const configError = new ConfigErrorV1.InvalidError({
path: "/tmp/opencode.json",
issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }],
})

View file

@ -1,11 +1,11 @@
import { test, expect, describe } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { extractResponseText, formatPromptTooLargeError } from "../../src/cli/cmd/github"
import type { MessageV2 } from "../../src/session/message-v2"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
// Helper to create minimal valid parts
function createTextPart(text: string): SessionLegacy.Part {
function createTextPart(text: string): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
@ -15,7 +15,7 @@ function createTextPart(text: string): SessionLegacy.Part {
}
}
function createReasoningPart(text: string): SessionLegacy.Part {
function createReasoningPart(text: string): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
@ -30,7 +30,7 @@ function createToolPart(
tool: string,
title: string,
status: "completed" | "running" = "completed",
): SessionLegacy.Part {
): SessionV1.Part {
if (status === "completed") {
return {
id: PartID.ascending(),
@ -64,7 +64,7 @@ function createToolPart(
}
}
function createStepStartPart(): SessionLegacy.Part {
function createStepStartPart(): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),
@ -73,7 +73,7 @@ function createStepStartPart(): SessionLegacy.Part {
}
}
function createStepFinishPart(): SessionLegacy.Part {
function createStepFinishPart(): SessionV1.Part {
return {
id: PartID.ascending(),
sessionID: SessionID.make("ses_test"),

View file

@ -1,4 +1,5 @@
import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Effect, Exit, Layer, Option } from "effect"
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
@ -34,6 +35,7 @@ import { Global } from "@opencode-ai/core/global"
import { ProjectV2 } from "@opencode-ai/core/project"
import { Filesystem } from "@/util/filesystem"
import { ConfigPlugin } from "@/config/plugin"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
@ -360,7 +362,7 @@ it.instance("updates config and preserves empty shell sentinel", () =>
"config.json",
)
yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(Config.Info, { shell: "" }, "test:config")))
yield* Config.Service.use((svc) => svc.update(ConfigParse.schema(ConfigV1.Info, { shell: "" }, "test:config")))
const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
expect(writtenConfig).toMatchObject({ shell: "" })
@ -385,7 +387,7 @@ it.effect("updates global config and omits empty shell key in jsonc", () =>
const file = path.join(dir, "opencode.jsonc")
const writtenConfig = yield* FSUtil.use.readFileString(file)
const parsed = ConfigParse.schema(Config.Info, ConfigParse.jsonc(writtenConfig, file), file)
const parsed = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(writtenConfig, file), file)
expect(writtenConfig).not.toContain('"shell"')
expect(parsed.shell).toBeUndefined()
expect(parsed.model).toBe("test/model")
@ -870,7 +872,7 @@ it.instance("updates config and writes to file", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* Config.Service.use((svc) =>
svc.update(ConfigParse.schema(Config.Info, { model: "updated/model" }, "test:config")),
svc.update(ConfigParse.schema(ConfigV1.Info, { model: "updated/model" }, "test:config")),
)
const writtenConfig = yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))
@ -1284,7 +1286,7 @@ it.instance("permission config preserves user key order", () =>
test("config parser preserves permission order while rejecting unknown top-level keys", () => {
const config = ConfigParse.schema(
Config.Info,
ConfigV1.Info,
{
permission: {
bash: "allow",
@ -1297,7 +1299,7 @@ test("config parser preserves permission order while rejecting unknown top-level
expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"])
try {
ConfigParse.schema(Config.Info, { invalid_field: true }, "test")
ConfigParse.schema(ConfigV1.Info, { invalid_field: true }, "test")
throw new Error("expected config parse to fail")
} catch (err) {
const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } }
@ -1684,7 +1686,7 @@ describe("resolvePluginSpec", () => {
})
describe("deduplicatePluginOrigins", () => {
const dedupe = (plugins: ConfigPlugin.Spec[]) =>
const dedupe = (plugins: ConfigPluginV1.Spec[]) =>
ConfigPlugin.deduplicatePluginOrigins(
plugins.map((spec) => ({
spec,
@ -1883,7 +1885,7 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
test("parseManagedPlist strips MDM metadata keys", async () => {
const config = ConfigParse.schema(
Config.Info,
ConfigV1.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@ -1911,7 +1913,7 @@ test("parseManagedPlist strips MDM metadata keys", async () => {
test("parseManagedPlist parses server settings", async () => {
const config = ConfigParse.schema(
Config.Info,
ConfigV1.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@ -1931,7 +1933,7 @@ test("parseManagedPlist parses server settings", async () => {
test("parseManagedPlist parses permission rules", async () => {
const config = ConfigParse.schema(
Config.Info,
ConfigV1.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@ -1961,7 +1963,7 @@ test("parseManagedPlist parses permission rules", async () => {
test("parseManagedPlist parses enabled_providers", async () => {
const config = ConfigParse.schema(
Config.Info,
ConfigV1.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(
JSON.stringify({
@ -1978,7 +1980,7 @@ test("parseManagedPlist parses enabled_providers", async () => {
test("parseManagedPlist handles empty config", async () => {
const config = ConfigParse.schema(
Config.Info,
ConfigV1.Info,
ConfigParse.jsonc(
await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://opencode.ai/config.json" })),
"test:mobileconfig",

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { ConfigLSP } from "../../src/config/lsp"
import { ConfigLSPV1 } from "@opencode-ai/core/v1/config/lsp"
// The LSP config refinement enforces: any custom (non-builtin) LSP server
// entry must declare an `extensions` array so the client knows which files
@ -8,8 +8,8 @@ import { ConfigLSP } from "../../src/config/lsp"
// entries are exempt.
//
// `typescript` is a builtin server id (see src/lsp/server.ts).
describe("ConfigLSP.Info refinement", () => {
const decodeEffect = Schema.decodeUnknownSync(ConfigLSP.Info)
describe("ConfigLSPV1.Info refinement", () => {
const decodeEffect = Schema.decodeUnknownSync(ConfigLSPV1.Info)
describe("accepted inputs", () => {
test("true and false pass (top-level toggle)", () => {

View file

@ -1,5 +1,5 @@
import { Config } from "@/config/config"
import { emptyConsoleState } from "@/config/console-state"
import { emptyConsoleState } from "@opencode-ai/core/v1/config/console-state"
import { Effect, Layer } from "effect"
export function make(overrides: Partial<Config.Interface> = {}) {

View file

@ -1,4 +1,5 @@
import { $ } from "bun"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import * as fs from "fs/promises"
import os from "os"
import path from "path"
@ -72,7 +73,7 @@ async function stop(dir: string) {
type TmpDirOptions<T> = {
git?: boolean
config?: Partial<Config.Info>
config?: Partial<ConfigV1.Info>
init?: (dir: string) => Promise<T>
dispose?: (dir: string) => Promise<T>
}
@ -116,7 +117,7 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
/** Effectful scoped tmpdir. Cleaned up when the scope closes. Make sure these stay in sync */
export function tmpdirScoped<E = never, R = never>(options?: {
git?: boolean
config?: Partial<Config.Info> | (() => Partial<Config.Info>)
config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>)
init?: (directory: string) => Effect.Effect<void, E, R>
}) {
return Effect.gen(function* () {
@ -177,7 +178,7 @@ export const disposeAllInstancesEffect = InstanceStore.Service.use((store) => st
export function provideTmpdirInstance<A, E, R>(
self: (path: string) => Effect.Effect<A, E, R>,
options?: { git?: boolean; config?: Partial<Config.Info> | (() => Partial<Config.Info>) },
options?: { git?: boolean; config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>) },
) {
return Effect.gen(function* () {
const path = yield* tmpdirScoped(options)
@ -196,7 +197,7 @@ export const requireInstance = Effect.gen(function* () {
export const withTmpdirInstance =
<E2 = never, R2 = never>(options?: {
git?: boolean
config?: Partial<Config.Info> | (() => Partial<Config.Info>)
config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>)
init?: (directory: string) => Effect.Effect<void, E2, R2>
}) =>
<A, E, R>(self: Effect.Effect<A, E, R>) =>
@ -207,7 +208,7 @@ export const withTmpdirInstance =
export function provideTmpdirServer<A, E, R>(
self: (input: { dir: string; llm: TestLLMServer["Service"] }) => Effect.Effect<A, E, R>,
options?: { git?: boolean; config?: (url: string) => Partial<Config.Info> },
options?: { git?: boolean; config?: (url: string) => Partial<ConfigV1.Info> },
): Effect.Effect<
A,
E | PlatformError.PlatformError,

View file

@ -1,4 +1,5 @@
import { test, type TestOptions } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { Cause, Duration, Effect, Exit, Layer } from "effect"
import * as Scope from "effect/Scope"
import * as TestClock from "effect/testing/TestClock"
@ -11,7 +12,7 @@ import { InstanceStore } from "@/project/instance-store"
type Body<A, E, R> = Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)
type InstanceOptions<E, R> = {
git?: boolean
config?: Partial<Config.Info> | (() => Partial<Config.Info>)
config?: Partial<ConfigV1.Info> | (() => Partial<ConfigV1.Info>)
init?: (directory: string) => Effect.Effect<void, E, R>
}

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { describe, test, expect } from "bun:test"
import { Effect } from "effect"
import { Permission } from "../src/permission"
@ -10,7 +10,7 @@ const it = testEffect(Config.defaultLayer)
const load = Config.use.get()
describe("Permission.evaluate for permission.task", () => {
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionLegacy.Ruleset =>
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionV1.Ruleset =>
Object.entries(rules).map(([pattern, action]) => ({
permission: "task",
pattern,
@ -76,7 +76,7 @@ describe("Permission.disabled for task tool", () => {
// Note: The `disabled` function checks if a TOOL should be completely removed from the tool list.
// It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`.
// It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`.
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionLegacy.Ruleset =>
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionV1.Ruleset =>
Object.entries(rules).map(([pattern, action]) => ({
permission: "task",
pattern,

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { test, expect } from "bun:test"
import os from "os"
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
@ -261,8 +261,8 @@ test("merge - preserves rule order", () => {
})
test("merge - config permission overrides default ask", () => {
const defaults: PermissionLegacy.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const defaults: PermissionV1.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const merged = Permission.merge(defaults, config)
expect(Permission.evaluate("bash", "ls", merged).action).toBe("allow")
@ -270,8 +270,8 @@ test("merge - config permission overrides default ask", () => {
})
test("merge - config ask overrides default allow", () => {
const defaults: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
const defaults: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
const merged = Permission.merge(defaults, config)
expect(Permission.evaluate("bash", "ls", merged).action).toBe("ask")
@ -443,8 +443,8 @@ test("evaluate - later wildcard permission can override earlier specific permiss
})
test("evaluate - merges multiple rulesets", () => {
const config: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const approved: PermissionLegacy.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
const config: PermissionV1.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
const approved: PermissionV1.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }]
const result = Permission.evaluate("bash", "rm", config, approved)
expect(result.action).toBe("deny")
})
@ -588,7 +588,7 @@ it.instance(
ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
}),
)
expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
expect(err).toBeInstanceOf(PermissionV1.DeniedError)
}),
{ git: true },
)
@ -655,10 +655,10 @@ it.instance(
() =>
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
const seen = yield* Deferred.make<PermissionLegacy.Request>()
const seen = yield* Deferred.make<PermissionV1.Request>()
const unsub = yield* events.listen((event) => {
if (event.type === Permission.Event.Asked.type)
Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionLegacy.Request))
Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionV1.Request))
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
@ -703,7 +703,7 @@ it.instance(
() =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_test1"),
id: PermissionV1.ID.make("per_test1"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
@ -713,7 +713,7 @@ it.instance(
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test1"), reply: "once" })
yield* reply({ requestID: PermissionV1.ID.make("per_test1"), reply: "once" })
yield* Fiber.join(fiber)
}),
{ git: true },
@ -724,7 +724,7 @@ it.instance(
() =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_test2"),
id: PermissionV1.ID.make("per_test2"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
@ -734,11 +734,11 @@ it.instance(
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test2"), reply: "reject" })
yield* reply({ requestID: PermissionV1.ID.make("per_test2"), reply: "reject" })
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
}),
{ git: true },
)
@ -748,7 +748,7 @@ it.instance(
() =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_test2b"),
id: PermissionV1.ID.make("per_test2b"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
@ -759,7 +759,7 @@ it.instance(
yield* waitForPending(1)
yield* reply({
requestID: PermissionLegacy.ID.make("per_test2b"),
requestID: PermissionV1.ID.make("per_test2b"),
reply: "reject",
message: "Use a safer command",
})
@ -768,7 +768,7 @@ it.instance(
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
const err = Cause.squash(exit.cause)
expect(err).toBeInstanceOf(PermissionLegacy.CorrectedError)
expect(err).toBeInstanceOf(PermissionV1.CorrectedError)
expect(String(err)).toContain("Use a safer command")
}
}),
@ -780,7 +780,7 @@ it.instance(
() =>
Effect.gen(function* () {
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_test3"),
id: PermissionV1.ID.make("per_test3"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
@ -790,7 +790,7 @@ it.instance(
}).pipe(Effect.forkScoped)
yield* waitForPending(1)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test3"), reply: "always" })
yield* reply({ requestID: PermissionV1.ID.make("per_test3"), reply: "always" })
yield* Fiber.join(fiber)
const result = yield* ask({
@ -811,7 +811,7 @@ it.instance(
() =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionLegacy.ID.make("per_test4a"),
id: PermissionV1.ID.make("per_test4a"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
@ -821,7 +821,7 @@ it.instance(
}).pipe(Effect.forkScoped)
const b = yield* ask({
id: PermissionLegacy.ID.make("per_test4b"),
id: PermissionV1.ID.make("per_test4b"),
sessionID: SessionID.make("session_same"),
permission: "edit",
patterns: ["foo.ts"],
@ -831,13 +831,13 @@ it.instance(
}).pipe(Effect.forkScoped)
yield* waitForPending(2)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test4a"), reply: "reject" })
yield* reply({ requestID: PermissionV1.ID.make("per_test4a"), reply: "reject" })
const [ea, eb] = yield* Effect.all([Fiber.await(a), Fiber.await(b)])
expect(Exit.isFailure(ea)).toBe(true)
expect(Exit.isFailure(eb)).toBe(true)
if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
if (Exit.isFailure(ea)) expect(Cause.squash(ea.cause)).toBeInstanceOf(PermissionV1.RejectedError)
if (Exit.isFailure(eb)) expect(Cause.squash(eb.cause)).toBeInstanceOf(PermissionV1.RejectedError)
}),
{ git: true },
)
@ -847,7 +847,7 @@ it.instance(
() =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionLegacy.ID.make("per_test5a"),
id: PermissionV1.ID.make("per_test5a"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
@ -857,7 +857,7 @@ it.instance(
}).pipe(Effect.forkScoped)
const b = yield* ask({
id: PermissionLegacy.ID.make("per_test5b"),
id: PermissionV1.ID.make("per_test5b"),
sessionID: SessionID.make("session_same"),
permission: "bash",
patterns: ["ls"],
@ -867,7 +867,7 @@ it.instance(
}).pipe(Effect.forkScoped)
yield* waitForPending(2)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test5a"), reply: "always" })
yield* reply({ requestID: PermissionV1.ID.make("per_test5a"), reply: "always" })
yield* Fiber.join(a)
yield* Fiber.join(b)
@ -881,7 +881,7 @@ it.instance(
() =>
Effect.gen(function* () {
const a = yield* ask({
id: PermissionLegacy.ID.make("per_test6a"),
id: PermissionV1.ID.make("per_test6a"),
sessionID: SessionID.make("session_a"),
permission: "bash",
patterns: ["ls"],
@ -891,7 +891,7 @@ it.instance(
}).pipe(Effect.forkScoped)
const b = yield* ask({
id: PermissionLegacy.ID.make("per_test6b"),
id: PermissionV1.ID.make("per_test6b"),
sessionID: SessionID.make("session_b"),
permission: "bash",
patterns: ["ls"],
@ -901,10 +901,10 @@ it.instance(
}).pipe(Effect.forkScoped)
yield* waitForPending(2)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test6a"), reply: "always" })
yield* reply({ requestID: PermissionV1.ID.make("per_test6a"), reply: "always" })
yield* Fiber.join(a)
expect((yield* list()).map((item) => item.id)).toEqual([PermissionLegacy.ID.make("per_test6b")])
expect((yield* list()).map((item) => item.id)).toEqual([PermissionV1.ID.make("per_test6b")])
yield* rejectAll()
yield* Fiber.await(b)
@ -919,12 +919,12 @@ it.instance(
const events = yield* EventV2Bridge.Service
const seen = yield* Deferred.make<{
sessionID: SessionID
requestID: PermissionLegacy.ID
reply: PermissionLegacy.Reply
requestID: PermissionV1.ID
reply: PermissionV1.Reply
}>()
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_test7"),
id: PermissionV1.ID.make("per_test7"),
sessionID: SessionID.make("session_test"),
permission: "bash",
patterns: ["ls"],
@ -940,14 +940,14 @@ it.instance(
Deferred.doneUnsafe(
seen,
Effect.succeed(
event.data as { sessionID: SessionID; requestID: PermissionLegacy.ID; reply: PermissionLegacy.Reply },
event.data as { sessionID: SessionID; requestID: PermissionV1.ID; reply: PermissionV1.Reply },
),
)
return Effect.void
})
yield* Effect.addFinalizer(() => unsub)
yield* reply({ requestID: PermissionLegacy.ID.make("per_test7"), reply: "once" })
yield* reply({ requestID: PermissionV1.ID.make("per_test7"), reply: "once" })
yield* Fiber.join(fiber)
expect(
yield* Deferred.await(seen).pipe(
@ -958,7 +958,7 @@ it.instance(
),
).toEqual({
sessionID: SessionID.make("session_test"),
requestID: PermissionLegacy.ID.make("per_test7"),
requestID: PermissionV1.ID.make("per_test7"),
reply: "once",
})
}),
@ -975,7 +975,7 @@ it.live("permission requests stay isolated by directory", () =>
.provide(
{ directory: one },
ask({
id: PermissionLegacy.ID.make("per_dir_a"),
id: PermissionV1.ID.make("per_dir_a"),
sessionID: SessionID.make("session_dir_a"),
permission: "bash",
patterns: ["ls"],
@ -990,7 +990,7 @@ it.live("permission requests stay isolated by directory", () =>
.provide(
{ directory: two },
ask({
id: PermissionLegacy.ID.make("per_dir_b"),
id: PermissionV1.ID.make("per_dir_b"),
sessionID: SessionID.make("session_dir_b"),
permission: "bash",
patterns: ["pwd"],
@ -1006,8 +1006,8 @@ it.live("permission requests stay isolated by directory", () =>
expect(onePending).toHaveLength(1)
expect(twoPending).toHaveLength(1)
expect(onePending[0].id).toBe(PermissionLegacy.ID.make("per_dir_a"))
expect(twoPending[0].id).toBe(PermissionLegacy.ID.make("per_dir_b"))
expect(onePending[0].id).toBe(PermissionV1.ID.make("per_dir_a"))
expect(twoPending[0].id).toBe(PermissionV1.ID.make("per_dir_b"))
yield* store.provide({ directory: one }, reply({ requestID: onePending[0].id, reply: "reject" }))
yield* store.provide({ directory: two }, reply({ requestID: twoPending[0].id, reply: "reject" }))
@ -1024,7 +1024,7 @@ it.instance(
const test = yield* TestInstance
const store = yield* InstanceStore.Service
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_dispose"),
id: PermissionV1.ID.make("per_dispose"),
sessionID: SessionID.make("session_dispose"),
permission: "bash",
patterns: ["ls"],
@ -1039,7 +1039,7 @@ it.instance(
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
}),
{ git: true },
)
@ -1051,7 +1051,7 @@ it.instance(
const test = yield* TestInstance
const store = yield* InstanceStore.Service
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_reload"),
id: PermissionV1.ID.make("per_reload"),
sessionID: SessionID.make("session_reload"),
permission: "bash",
patterns: ["ls"],
@ -1065,7 +1065,7 @@ it.instance(
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
}),
{ git: true },
)
@ -1074,7 +1074,7 @@ it.instance(
"reply - fails for unknown requestID",
() =>
Effect.gen(function* () {
const exit = yield* reply({ requestID: PermissionLegacy.ID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
const exit = yield* reply({ requestID: PermissionV1.ID.make("per_unknown"), reply: "once" }).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
expect(Cause.squash(exit.cause)).toMatchObject({ _tag: "Permission.NotFoundError", requestID: "per_unknown" })
@ -1101,7 +1101,7 @@ it.instance(
],
}),
)
expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
expect(err).toBeInstanceOf(PermissionV1.DeniedError)
}),
{ git: true },
)
@ -1141,7 +1141,7 @@ it.instance(
}),
)
expect(err).toBeInstanceOf(PermissionLegacy.DeniedError)
expect(err).toBeInstanceOf(PermissionV1.DeniedError)
expect(yield* list()).toHaveLength(0)
}),
{ git: true },
@ -1155,7 +1155,7 @@ it.instance(
const store = yield* InstanceStore.Service
const fiber = yield* ask({
id: PermissionLegacy.ID.make("per_reload"),
id: PermissionV1.ID.make("per_reload"),
sessionID: SessionID.make("session_reload"),
permission: "bash",
patterns: ["ls"],
@ -1170,7 +1170,7 @@ it.instance(
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionLegacy.RejectedError)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBeInstanceOf(PermissionV1.RejectedError)
}),
{ git: true },
)

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { ConfigProvider } from "@/config/provider"
import { ConfigProviderV1 } from "@opencode-ai/core/v1/config/provider"
import { CatalogModelStatus, ModelStatus } from "@/provider/model-status"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Provider } from "@/provider/provider"
@ -13,7 +13,7 @@ describe("provider model status schemas", () => {
})
test("accepts active status across public provider schemas", () => {
expect(Schema.decodeUnknownSync(ConfigProvider.Model)({ status: "active" }).status).toBe("active")
expect(Schema.decodeUnknownSync(ConfigProviderV1.Model)({ status: "active" }).status).toBe("active")
expect(
Schema.decodeUnknownSync(ModelsDev.Model)({
id: "test-model",

View file

@ -1,7 +1,7 @@
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { NamedError } from "@opencode-ai/core/util/error"
import { describe, expect } from "bun:test"
import { ConfigError } from "../../src/config/error"
import { ConfigErrorV1 } from "@opencode-ai/core/v1/config/error"
import { Effect, Layer } from "effect"
import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
import { errorLayer } from "../../src/server/routes/instance/httpapi/middleware/error"
@ -55,7 +55,7 @@ describe("HttpApi error middleware", () => {
it.live("does not expose config defects from generic middleware", () =>
Effect.gen(function* () {
const configError = new ConfigError.InvalidError({
const configError = new ConfigErrorV1.InvalidError({
path: "/tmp/opencode.json",
issues: [{ message: "Expected object", path: ["provider", "anthropic", "options"] }],
})

View file

@ -1,5 +1,6 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Cause, Duration, Effect, Layer, Scope } from "effect"
import { TestLLMServer } from "../../lib/llm-server"
import type { Config } from "../../../src/config/config"
@ -144,7 +145,7 @@ function withContext<A, E>(
}),
message: (sessionID, input) =>
Effect.gen(function* () {
const info: SessionLegacy.User = {
const info: SessionV1.User = {
id: MessageID.ascending(),
sessionID,
role: "user",
@ -155,7 +156,7 @@ function withContext<A, E>(
modelID: ProviderV2.ModelID.make("test"),
},
}
const part: SessionLegacy.TextPart = {
const part: SessionV1.TextPart = {
id: PartID.ascending(),
sessionID,
messageID: info.id,
@ -205,7 +206,7 @@ function trace(options: Options, scenario: ActiveScenario, phase: string) {
function projectOptions(
project: ProjectOptions,
llmUrl: string | undefined,
): { git?: boolean; config?: Partial<Config.Info> } {
): { git?: boolean; config?: Partial<ConfigV1.Info> } {
if (!project.llm || !llmUrl) return { git: project.git, config: project.config }
const fake = fakeLlmConfig(llmUrl)
return {
@ -221,7 +222,7 @@ function projectOptions(
}
}
function fakeLlmConfig(url: string): Partial<Config.Info> {
function fakeLlmConfig(url: string): Partial<ConfigV1.Info> {
return {
model: "test/test-model",
small_model: "test/test-model",

View file

@ -1,5 +1,6 @@
import type { Duration, Effect } from "effect"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { Config } from "../../../src/config/config"
import type { Project } from "../../../src/project/project"
import type { Worktree } from "../../../src/worktree"
@ -15,7 +16,7 @@ export type Mode = "effect" | "coverage" | "auth"
export type Comparison = "none" | "status" | "json"
export type CaptureMode = "full" | "stream"
export type AuthPolicy = "protected" | "public" | "public-bypass" | "ticket-bypass"
export type ProjectOptions = { git?: boolean; config?: Partial<Config.Info>; llm?: boolean }
export type ProjectOptions = { git?: boolean; config?: Partial<ConfigV1.Info>; llm?: boolean }
export type OpenApiSpec = { paths?: Record<string, Partial<Record<OpenApiMethod, unknown>>> }
export type JsonObject = Record<string, unknown>
@ -58,7 +59,7 @@ export type ScenarioContext = {
sessionGet: (sessionID: SessionID) => Effect.Effect<SessionInfo | undefined>
project: () => Effect.Effect<Project.Info>
message: (sessionID: SessionID, input?: { text?: string }) => Effect.Effect<MessageSeed>
messages: (sessionID: SessionID) => Effect.Effect<SessionLegacy.WithParts[]>
messages: (sessionID: SessionID) => Effect.Effect<SessionV1.WithParts[]>
todos: (sessionID: SessionID, todos: TodoInfo[]) => Effect.Effect<void>
worktree: (input?: { name?: string }) => Effect.Effect<Worktree.Info>
worktreeRemove: (directory: string) => Effect.Effect<void>
@ -119,4 +120,4 @@ export type Result =
export type SessionInfo = { id: SessionID; title: string; parentID?: SessionID }
export type TodoInfo = { content: string; status: string; priority: string }
export type MessageSeed = { info: SessionLegacy.User; part: SessionLegacy.TextPart }
export type MessageSeed = { info: SessionV1.User; part: SessionV1.TextPart }

View file

@ -1,4 +1,4 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { Flag } from "@opencode-ai/core/flag/flag"
import { describe, expect } from "bun:test"
@ -167,7 +167,7 @@ describe("instance HttpApi", () => {
handlerContext,
),
)
const permissionID = PermissionLegacy.ID.ascending()
const permissionID = PermissionV1.ID.ascending()
const questionReplyID = QuestionID.ascending()
const questionRejectID = QuestionID.ascending()
const [permission, questionReply, questionReject] = yield* Effect.all(

View file

@ -1,5 +1,6 @@
import { afterEach, describe, expect } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Deferred, Effect, Layer } from "effect"
import type * as Scope from "effect/Scope"
import { HttpServer } from "effect/unstable/http"
@ -190,7 +191,7 @@ function httpapiInstance<A, E>(
options: {
serverPath: ServerPath
git?: boolean
config?: Partial<Config.Info>
config?: Partial<ConfigV1.Info>
setup?: (dir: string) => Effect.Effect<void, E, TestServices>
},
run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
@ -214,7 +215,7 @@ function withProject<A, E, E2 = never>(
serverPath: ServerPath,
options: {
git?: boolean
config?: Partial<Config.Info>
config?: Partial<ConfigV1.Info>
setup?: (dir: string) => Effect.Effect<void, E2, TestServices>
},
run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
@ -300,7 +301,7 @@ function seedMessage(directory: string, sessionID: string) {
agent: "test",
model: { providerID: ProviderV2.ID.make("test"), modelID: ProviderV2.ModelID.make("test") },
tools: {},
} satisfies SessionLegacy.User)
} satisfies SessionV1.User)
const part = yield* svc.updatePart({
id: PartID.ascending(),
sessionID: id,

View file

@ -1,7 +1,7 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { afterEach, describe, expect } from "bun:test"
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { mkdir } from "node:fs/promises"
import path from "node:path"
import { Cause, Config, Effect, Exit, Layer } from "effect"
@ -362,7 +362,7 @@ describe("session HttpApi", () => {
const messages = yield* request(`${pathFor(SessionPaths.messages, { sessionID: parent.id })}?limit=1`, {
headers,
})
const messagePage = yield* json<SessionLegacy.WithParts[]>(messages)
const messagePage = yield* json<SessionV1.WithParts[]>(messages)
const nextCursor = messages.headers["x-next-cursor"]
expect(nextCursor).toBeTruthy()
expect(messagePage[0]?.parts[0]).toMatchObject({ type: "text" })
@ -379,7 +379,7 @@ describe("session HttpApi", () => {
).toBe(400)
expect(
yield* requestJson<SessionLegacy.WithParts>(
yield* requestJson<SessionV1.WithParts>(
pathFor(SessionPaths.message, { sessionID: parent.id, messageID: message.info.id }),
{ headers },
),
@ -829,7 +829,7 @@ describe("session HttpApi", () => {
const first = yield* createTextMessage(session.id, "first")
const second = yield* createTextMessage(session.id, "second")
const updated = yield* requestJson<SessionLegacy.Part>(
const updated = yield* requestJson<SessionV1.Part>(
pathFor(SessionPaths.updatePart, {
sessionID: session.id,
messageID: first.info.id,
@ -913,7 +913,7 @@ describe("session HttpApi", () => {
}),
).toMatchObject({ id: session.id })
const permissionID = String(PermissionLegacy.ID.ascending())
const permissionID = String(PermissionV1.ID.ascending())
const permission = yield* request(
pathFor(SessionPaths.permissions, {
sessionID: session.id,

View file

@ -15,7 +15,7 @@ import { Effect, Layer } from "effect"
import { SessionPaths } from "@/server/routes/instance/httpapi/groups/session"
import { Session } from "@/session/session"
import { Storage } from "@/storage/storage"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { MessageID } from "@/session/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { resetDatabase } from "../fixture/db"
@ -83,7 +83,7 @@ describe("session diff with missing patch (#26574)", () => {
summary: {
diffs: [{ file: "turn.ts", additions: 1, deletions: 0, status: "modified" }],
},
} satisfies SessionLegacy.User)
} satisfies SessionV1.User)
const response = yield* requestInDirectory(
`${pathFor(SessionPaths.diff, { sessionID: session.id })}?messageID=${messageID}`,

View file

@ -1,5 +1,5 @@
import { afterEach, describe, expect } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect, Layer } from "effect"
import { HttpClientResponse } from "effect/unstable/http"
import { Session as SessionNs } from "@/session/session"
@ -65,14 +65,14 @@ const fill = Effect.fn("SessionMessagesTest.fill")(function* (
agent: "test",
model,
tools: {},
} satisfies SessionLegacy.User)
} satisfies SessionV1.User)
yield* session.updatePart({
id: PartID.ascending(),
sessionID,
messageID: id,
type: "text",
text: `m${i}`,
} satisfies SessionLegacy.TextPart)
} satisfies SessionV1.TextPart)
return id
}),
)
@ -96,7 +96,7 @@ describe("session messages endpoint", () => {
const a = yield* request(`/session/${session.id}/message?limit=2`)
expect(a.status).toBe(200)
const aBody = yield* json<SessionLegacy.WithParts[]>(a)
const aBody = yield* json<SessionV1.WithParts[]>(a)
expect(aBody.map((item) => item.info.id)).toEqual(ids.slice(-2))
const cursor = a.headers["x-next-cursor"]
expect(cursor).toBeTruthy()
@ -104,7 +104,7 @@ describe("session messages endpoint", () => {
const b = yield* request(`/session/${session.id}/message?limit=2&before=${encodeURIComponent(cursor!)}`)
expect(b.status).toBe(200)
const bBody = yield* json<SessionLegacy.WithParts[]>(b)
const bBody = yield* json<SessionV1.WithParts[]>(b)
expect(bBody.map((item) => item.info.id)).toEqual(ids.slice(-4, -2))
}),
),
@ -120,7 +120,7 @@ describe("session messages endpoint", () => {
const res = yield* request(`/session/${session.id}/message`)
expect(res.status).toBe(200)
const body = yield* json<SessionLegacy.WithParts[]>(res)
const body = yield* json<SessionV1.WithParts[]>(res)
expect(body.map((item) => item.info.id)).toEqual(ids)
}),
),
@ -152,7 +152,7 @@ describe("session messages endpoint", () => {
const res = yield* request(`/session/${session.id}/message?limit=510`)
expect(res.status).toBe(200)
const body = yield* json<SessionLegacy.WithParts[]>(res)
const body = yield* json<SessionV1.WithParts[]>(res)
expect(body).toHaveLength(510)
}),
),

View file

@ -1,5 +1,6 @@
import { afterEach, describe, expect, mock, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2Bridge } from "@/event-v2-bridge"
import { APICallError } from "ai"
@ -218,8 +219,8 @@ function layer(result: "continue" | "compact") {
)
}
function cfg(compaction?: Config.Info["compaction"]) {
const base = Schema.decodeUnknownSync(Config.Info)({}) as Config.Info
function cfg(compaction?: ConfigV1.Info["compaction"]) {
const base = Schema.decodeUnknownSync(ConfigV1.Info)({}) as ConfigV1.Info
return TestConfig.layer({
get: () => Effect.succeed({ ...base, compaction }),
})
@ -303,7 +304,7 @@ function readCompactionPart(sessionID: SessionID) {
.messages({ sessionID })
.pipe(
Effect.map((messages) =>
messages.at(-2)?.parts.find((item): item is SessionLegacy.CompactionPart => item.type === "compaction"),
messages.at(-2)?.parts.find((item): item is SessionV1.CompactionPart => item.type === "compaction"),
),
)
}
@ -649,7 +650,7 @@ describe("session.compaction.prune", () => {
type: "text",
text: "first",
})
const b: SessionLegacy.Assistant = {
const b: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID: info.id,
@ -745,7 +746,7 @@ describe("session.compaction.prune", () => {
type: "text",
text: "first",
})
const b: SessionLegacy.Assistant = {
const b: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID: info.id,

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import path from "path"
import { Effect, FileSystem, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
@ -63,7 +63,7 @@ const tmpWithFiles = (files: Record<string, string>) =>
return dir
})
function loaded(filepath: string): SessionLegacy.WithParts[] {
function loaded(filepath: string): SessionV1.WithParts[] {
const sessionID = SessionID.make("session-loaded-1")
const messageID = MessageID.make("msg_message-loaded-1")

View file

@ -1,5 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
@ -52,7 +53,7 @@ type RecordedScenario = {
readonly recordAuth?: () => Auth.Info | undefined
readonly replayAuth?: Auth.Info
readonly stableID?: string
readonly config: (model: ModelsDev.Provider["models"][string]) => Partial<Config.Info>
readonly config: (model: ModelsDev.Provider["models"][string]) => Partial<ConfigV1.Info>
}
const cloneModel = (model: ModelsDev.Provider["models"][string]) => {
@ -60,9 +61,9 @@ const cloneModel = (model: ModelsDev.Provider["models"][string]) => {
const { experimental, ...rest } = cloned
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- The config schema accepts the same model shape except object-valued experimental metadata.
if (typeof experimental === "boolean")
return cloned as NonNullable<NonNullable<Config.Info["provider"]>[string]["models"]>[string]
return cloned as NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Dropping non-boolean experimental metadata makes the fixture model match config input.
return rest as NonNullable<NonNullable<Config.Info["provider"]>[string]["models"]>[string]
return rest as NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
}
const envValue = (...names: string[]) => names.map((name) => process.env[name]).find(Boolean)
@ -97,7 +98,7 @@ const providerConfig = (input: {
readonly api: string
readonly model: ModelsDev.Provider["models"][string]
readonly options: Record<string, unknown>
}): Partial<Config.Info> => ({
}): Partial<ConfigV1.Info> => ({
enabled_providers: [input.providerID],
provider: {
[input.providerID]: {
@ -396,7 +397,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
time: { created: 0 },
agent: agent.name,
model: { providerID: scenario.providerID, modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID,
model: resolved,
agent,

View file

@ -1,6 +1,7 @@
import { PermissionLegacy } from "@opencode-ai/core/permission/legacy"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import path from "path"
import { tool, type ModelMessage } from "ai"
import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
@ -26,9 +27,9 @@ import { LLMAISDK } from "@/session/llm/ai-sdk"
import { Session as SessionNs } from "@/session/session"
import { ProviderV2 } from "@opencode-ai/core/provider"
type ConfigModel = NonNullable<NonNullable<Config.Info["provider"]>[string]["models"]>[string]
type ConfigModel = NonNullable<NonNullable<ConfigV1.Info["provider"]>[string]["models"]>[string]
const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: string): Partial<Config.Info> => {
const openAIConfig = (model: ModelsDev.Provider["models"][string], baseURL: string): Partial<ConfigV1.Info> => {
const { experimental: _experimental, ...configModel } = model
return {
enabled_providers: ["openai"],
@ -333,7 +334,7 @@ describe("session.llm.ai-sdk adapter", () => {
})
test("preserves tool-error cause", async () => {
const error = new PermissionLegacy.RejectedError()
const error = new PermissionV1.RejectedError()
const events = await Effect.runPromise(
LLMAISDK.toLLMEvents(LLMAISDK.adapterState(), {
type: "tool-error",
@ -786,7 +787,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
yield* drain({
user,
@ -857,7 +858,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
const fiber = yield* drain({
user,
@ -927,7 +928,7 @@ describe("session.llm.stream", () => {
agent: agent.name,
model: { providerID: ProviderV2.ID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
tools: { question: true },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
yield* drain({
user,
@ -1029,7 +1030,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
yield* drain({
user,
@ -1143,7 +1144,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID,
model: resolved,
agent,
@ -1205,7 +1206,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id, variant: "high" },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID,
model: resolved,
agent,
@ -1288,7 +1289,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID,
model: resolved,
agent,
@ -1376,7 +1377,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID,
model: resolved,
agent,
@ -1501,7 +1502,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("openai"), modelID: resolved.id },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
yield* drain({
user,
@ -1593,7 +1594,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("minimax"), modelID: ProviderV2.ModelID.make("MiniMax-M2.5") },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
yield* drain({
user,
@ -1687,7 +1688,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make("anthropic"), modelID: resolved.id, variant: "max" },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
const input = [
{
@ -1892,7 +1893,7 @@ describe("session.llm.stream", () => {
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderV2.ID.make(geminiFixture.providerID), modelID: resolved.id },
} satisfies SessionLegacy.User
} satisfies SessionV1.User
yield* drain({
user,

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { APICallError } from "ai"
import { MessageV2 } from "../../src/session/message-v2"
import { ProviderTransform } from "@/provider/transform"
@ -60,7 +60,7 @@ const model: Provider.Model = {
release_date: "2026-01-01",
}
function userInfo(id: string): SessionLegacy.User {
function userInfo(id: string): SessionV1.User {
return {
id,
sessionID,
@ -70,15 +70,15 @@ function userInfo(id: string): SessionLegacy.User {
model: { providerID, modelID: ProviderV2.ModelID.make("test") },
tools: {},
mode: "",
} as unknown as SessionLegacy.User
} as unknown as SessionV1.User
}
function assistantInfo(
id: string,
parentID: string,
error?: SessionLegacy.Assistant["error"],
error?: SessionV1.Assistant["error"],
meta?: { providerID: string; modelID: string },
): SessionLegacy.Assistant {
): SessionV1.Assistant {
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
return {
id,
@ -99,7 +99,7 @@ function assistantInfo(
reasoning: 0,
cache: { read: 0, write: 0 },
},
} as unknown as SessionLegacy.Assistant
} as unknown as SessionV1.Assistant
}
function basePart(messageID: string, id: string) {
@ -112,7 +112,7 @@ function basePart(messageID: string, id: string) {
describe("session.message-v2.toModelMessage", () => {
test("filters out messages with no parts", async () => {
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo("m-empty"),
parts: [],
@ -125,7 +125,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "hello",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -140,7 +140,7 @@ describe("session.message-v2.toModelMessage", () => {
test("filters out messages with only ignored parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@ -150,7 +150,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "ignored",
ignored: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -160,7 +160,7 @@ describe("session.message-v2.toModelMessage", () => {
test("filters out user messages with only empty text parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@ -169,7 +169,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -179,7 +179,7 @@ describe("session.message-v2.toModelMessage", () => {
test("filters empty user text parts while keeping non-empty parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@ -193,7 +193,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "hello",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -208,7 +208,7 @@ describe("session.message-v2.toModelMessage", () => {
test("includes synthetic text parts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@ -218,7 +218,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "hello",
synthetic: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo("m-assistant", messageID),
@ -229,7 +229,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "assistant",
synthetic: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -248,7 +248,7 @@ describe("session.message-v2.toModelMessage", () => {
test("converts user text/file parts and injects compaction/subtask prompts", async () => {
const messageID = "m-user"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(messageID),
parts: [
@ -296,7 +296,7 @@ describe("session.message-v2.toModelMessage", () => {
description: "desc",
agent: "agent",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -322,7 +322,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -331,7 +331,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -366,7 +366,7 @@ describe("session.message-v2.toModelMessage", () => {
},
metadata: { openai: { tool: "meta" } },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -435,7 +435,7 @@ describe("session.message-v2.toModelMessage", () => {
)
const userID = "m-user-anthropic"
const assistantID = "m-assistant-anthropic"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -444,7 +444,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -472,7 +472,7 @@ describe("session.message-v2.toModelMessage", () => {
],
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -516,7 +516,7 @@ describe("session.message-v2.toModelMessage", () => {
const pdf = Buffer.from("%PDF-1.4\n").toString("base64")
const userID = "m-user-bedrock-pdf"
const assistantID = "m-assistant-bedrock-pdf"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -525,7 +525,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -553,7 +553,7 @@ describe("session.message-v2.toModelMessage", () => {
],
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -604,7 +604,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -613,7 +613,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
@ -646,7 +646,7 @@ describe("session.message-v2.toModelMessage", () => {
},
metadata: { openai: { tool: "meta" } },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -687,7 +687,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -696,7 +696,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -715,7 +715,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0, end: 1, compacted: 1 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -754,7 +754,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -763,7 +763,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -782,7 +782,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0, end: 1 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -824,7 +824,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -833,7 +833,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -852,7 +852,7 @@ describe("session.message-v2.toModelMessage", () => {
},
metadata: { openai: { tool: "meta" } },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -902,7 +902,7 @@ describe("session.message-v2.toModelMessage", () => {
"</shell_metadata>",
].join("\n")
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -911,7 +911,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -929,7 +929,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0, end: 1 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -967,12 +967,12 @@ describe("session.message-v2.toModelMessage", () => {
test("filters assistant messages with non-abort errors", async () => {
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(
assistantID,
"m-parent",
new SessionLegacy.APIError({ message: "boom", isRetryable: true }).toObject() as SessionLegacy.APIError,
new SessionV1.APIError({ message: "boom", isRetryable: true }).toObject() as SessionV1.APIError,
),
parts: [
{
@ -980,7 +980,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "should not render",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -991,11 +991,11 @@ describe("session.message-v2.toModelMessage", () => {
const assistantID1 = "m-assistant-1"
const assistantID2 = "m-assistant-2"
const aborted = new SessionLegacy.AbortedError({
const aborted = new SessionV1.AbortedError({
message: "aborted",
}).toObject() as SessionLegacy.Assistant["error"]
}).toObject() as SessionV1.Assistant["error"]
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID1, "m-parent", aborted),
parts: [
@ -1010,7 +1010,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "partial answer",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID2, "m-parent", aborted),
@ -1025,7 +1025,7 @@ describe("session.message-v2.toModelMessage", () => {
text: "thinking",
time: { start: 0 },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1065,7 +1065,7 @@ describe("session.message-v2.toModelMessage", () => {
index: 0,
},
]
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent", undefined, {
providerID: openrouterModel.providerID,
@ -1088,7 +1088,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "answer",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1116,7 +1116,7 @@ describe("session.message-v2.toModelMessage", () => {
test("splits assistant messages on step-start boundaries", async () => {
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@ -1134,7 +1134,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "second",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1153,7 +1153,7 @@ describe("session.message-v2.toModelMessage", () => {
test("drops messages that only contain step-start parts", async () => {
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@ -1161,7 +1161,7 @@ describe("session.message-v2.toModelMessage", () => {
...basePart(assistantID, "p1"),
type: "step-start",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1172,7 +1172,7 @@ describe("session.message-v2.toModelMessage", () => {
const userID = "m-user"
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: userInfo(userID),
parts: [
@ -1181,7 +1181,7 @@ describe("session.message-v2.toModelMessage", () => {
type: "text",
text: "run tool",
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
{
info: assistantInfo(assistantID, userID),
@ -1208,7 +1208,7 @@ describe("session.message-v2.toModelMessage", () => {
time: { start: 0 },
},
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1261,7 +1261,7 @@ describe("session.message-v2.toModelMessage", () => {
test("substitutes space for empty text between signed reasoning blocks", async () => {
// Reproduces the bug pattern: [reasoning(sig), text(""), reasoning(sig), text(full)]
const assistantID = "m-assistant"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@ -1281,7 +1281,7 @@ describe("session.message-v2.toModelMessage", () => {
metadata: { anthropic: { signature: "sig2" } },
},
{ ...basePart(assistantID, "p6"), type: "text", text: "the answer" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1297,7 +1297,7 @@ describe("session.message-v2.toModelMessage", () => {
// Bedrock signed reasoning is preserved as reasoning metadata, but unlike the
// direct Anthropic path we do not preserve empty text separators for Bedrock.
const assistantID = "m-assistant-bedrock"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
@ -1309,7 +1309,7 @@ describe("session.message-v2.toModelMessage", () => {
},
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1324,14 +1324,14 @@ describe("session.message-v2.toModelMessage", () => {
// Non-Anthropic providers' reasoning doesn't position-validate, so empty text
// should be filtered normally rather than substituted.
const assistantID = "m-assistant-unsigned"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
{ ...basePart(assistantID, "p1"), type: "reasoning", text: "thinking" },
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1344,13 +1344,13 @@ describe("session.message-v2.toModelMessage", () => {
test("leaves empty text alone in assistant messages without reasoning", async () => {
const assistantID = "m-assistant-no-reasoning"
const input: SessionLegacy.WithParts[] = [
const input: SessionV1.WithParts[] = [
{
info: assistantInfo(assistantID, "m-parent"),
parts: [
{ ...basePart(assistantID, "p1"), type: "text", text: "" },
{ ...basePart(assistantID, "p2"), type: "text", text: "hello" },
] as SessionLegacy.Part[],
] as SessionV1.Part[],
},
]
@ -1462,7 +1462,7 @@ describe("session.message-v2.fromError", () => {
isRetryable: false,
})
const result = MessageV2.fromError(error, { providerID })
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(true)
})
})
@ -1483,7 +1483,7 @@ describe("session.message-v2.fromError", () => {
isRetryable: false,
})
const result = MessageV2.fromError(error, { providerID })
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(true)
})
test("does not classify 429 no body as context overflow", () => {
@ -1498,8 +1498,8 @@ describe("session.message-v2.fromError", () => {
}),
{ providerID },
)
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(false)
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false)
expect(SessionV1.APIError.isInstance(result)).toBe(true)
})
test("serializes unknown inputs", () => {
@ -1534,9 +1534,9 @@ describe("session.message-v2.fromError", () => {
const result = MessageV2.fromError(zlibError, { providerID })
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
expect((result as SessionLegacy.APIError).data.isRetryable).toBe(true)
expect((result as SessionLegacy.APIError).data.message).toInclude("decompression")
expect(SessionV1.APIError.isInstance(result)).toBe(true)
expect((result as SessionV1.APIError).data.isRetryable).toBe(true)
expect((result as SessionV1.APIError).data.message).toInclude("decompression")
})
test("classifies ZlibError as AbortedError when abort context is provided", () => {
@ -1560,21 +1560,21 @@ describe("session.message-v2.latest", () => {
const CONTINUE_USER = MessageID.make("msg_005")
const NEW_COMPACTION_USER = MessageID.make("msg_006")
const tailUser: SessionLegacy.WithParts = {
const tailUser: SessionV1.WithParts = {
info: userInfo(TAIL_USER),
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as SessionLegacy.Part[],
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as SessionV1.Part[],
}
const overflowAssistant: SessionLegacy.WithParts = {
const overflowAssistant: SessionV1.WithParts = {
info: {
...assistantInfo(OVERFLOW_ASSISTANT, TAIL_USER),
finish: "tool-calls",
tokens: { input: 280_000, output: 200, reasoning: 0, cache: { read: 0, write: 0 }, total: 280_200 },
} as SessionLegacy.Assistant,
} as SessionV1.Assistant,
parts: [],
}
const compactionUser: SessionLegacy.WithParts = {
const compactionUser: SessionV1.WithParts = {
info: userInfo(COMPACTION_USER),
parts: [
{
@ -1583,20 +1583,20 @@ describe("session.message-v2.latest", () => {
auto: true,
tail_start_id: TAIL_USER,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
}
const summaryAssistant: SessionLegacy.WithParts = {
const summaryAssistant: SessionV1.WithParts = {
info: {
...assistantInfo(SUMMARY_ASSISTANT, COMPACTION_USER),
summary: true,
finish: "stop",
tokens: { input: 150_000, output: 1_500, reasoning: 0, cache: { read: 0, write: 0 }, total: 151_500 },
} as SessionLegacy.Assistant,
} as SessionV1.Assistant,
parts: [],
}
const continueUser: SessionLegacy.WithParts = {
const continueUser: SessionV1.WithParts = {
info: userInfo(CONTINUE_USER),
parts: [
{
@ -1606,7 +1606,7 @@ describe("session.message-v2.latest", () => {
synthetic: true,
metadata: { compaction_continue: true },
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
}
// Regression for double auto-compaction. The reorder in filterCompacted
@ -1632,7 +1632,7 @@ describe("session.message-v2.latest", () => {
})
test("a fresh compaction-user newer than the latest summary surfaces in tasks", () => {
const newCompactionUser: SessionLegacy.WithParts = {
const newCompactionUser: SessionV1.WithParts = {
info: userInfo(NEW_COMPACTION_USER),
parts: [
{
@ -1640,7 +1640,7 @@ describe("session.message-v2.latest", () => {
type: "compaction",
auto: true,
},
] as SessionLegacy.Part[],
] as SessionV1.Part[],
}
const state = MessageV2.latest([

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { Effect, Layer, Option } from "effect"
import { Session as SessionNs } from "@/session/session"
@ -48,7 +48,7 @@ const fill = Effect.fn("Test.fill")(function* (
model: { providerID: "test", modelID: "test" },
tools: {},
mode: "",
} as unknown as SessionLegacy.Info)
} as unknown as SessionV1.Info)
yield* session.updatePart({
id: PartID.ascending(),
sessionID,
@ -72,7 +72,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
model: { providerID: "test", modelID: "test" },
tools: {},
mode: "",
} as unknown as SessionLegacy.Info)
} as unknown as SessionV1.Info)
if (text) {
yield* session.updatePart({
id: PartID.ascending(),
@ -88,7 +88,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
const addAssistant = Effect.fn("Test.addAssistant")(function* (
sessionID: SessionID,
parentID: MessageID,
opts?: { summary?: boolean; finish?: string; error?: SessionLegacy.Assistant["error"] },
opts?: { summary?: boolean; finish?: string; error?: SessionV1.Assistant["error"] },
) {
const session = yield* SessionNs.Service
const id = MessageID.ascending()
@ -108,7 +108,7 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
summary: opts?.summary,
finish: opts?.finish,
error: opts?.error,
} as unknown as SessionLegacy.Info)
} as unknown as SessionV1.Info)
return id
})
@ -388,7 +388,7 @@ describe("MessageV2.parts", () => {
const result = yield* MessageV2.parts(id)
expect(result).toHaveLength(1)
expect(result[0].type).toBe("text")
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
expect((result[0] as SessionV1.TextPart).text).toBe("m0")
}),
),
)
@ -426,9 +426,9 @@ describe("MessageV2.parts", () => {
const result = yield* MessageV2.parts(id)
expect(result).toHaveLength(3)
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
expect((result[1] as SessionLegacy.TextPart).text).toBe("second")
expect((result[2] as SessionLegacy.TextPart).text).toBe("third")
expect((result[0] as SessionV1.TextPart).text).toBe("m0")
expect((result[1] as SessionV1.TextPart).text).toBe("second")
expect((result[2] as SessionV1.TextPart).text).toBe("third")
}),
),
)
@ -465,7 +465,7 @@ describe("MessageV2.get", () => {
expect(result.info.sessionID).toBe(sessionID)
expect(result.info.role).toBe("user")
expect(result.parts).toHaveLength(1)
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("m0")
expect((result.parts[0] as SessionV1.TextPart).text).toBe("m0")
}),
),
)
@ -535,7 +535,7 @@ describe("MessageV2.get", () => {
const result = yield* MessageV2.get({ sessionID, messageID: aid })
expect(result.info.role).toBe("assistant")
expect(result.parts).toHaveLength(1)
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("response")
expect((result.parts[0] as SessionV1.TextPart).text).toBe("response")
}),
),
)
@ -671,10 +671,10 @@ describe("MessageV2.filterCompacted", () => {
const u1 = yield* addUser(sessionID, "hello")
yield* addCompactionPart(sessionID, u1)
const error = new SessionLegacy.APIError({
const error = new SessionV1.APIError({
message: "boom",
isRetryable: true,
}).toObject() as SessionLegacy.Assistant["error"]
}).toObject() as SessionV1.Assistant["error"]
yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn", error })
yield* addUser(sessionID, "retry")
@ -950,7 +950,7 @@ describe("MessageV2.filterCompacted", () => {
test("works with array input", () => {
// filterCompacted accepts any Iterable, not just generators
const id = MessageID.ascending()
const items: SessionLegacy.WithParts[] = [
const items: SessionV1.WithParts[] = [
{
info: {
id,
@ -959,8 +959,8 @@ describe("MessageV2.filterCompacted", () => {
time: { created: 1 },
agent: "test",
model: { providerID: "test", modelID: "test" },
} as unknown as SessionLegacy.Info,
parts: [{ type: "text", text: "hello" }] as unknown as SessionLegacy.Part[],
} as unknown as SessionV1.Info,
parts: [{ type: "text", text: "hello" }] as unknown as SessionV1.Part[],
},
]
const result = MessageV2.filterCompacted(items)
@ -1026,7 +1026,7 @@ describe("MessageV2 consistency", () => {
const streamed = yield* MessageV2.stream(sessionID)
const paged = [] as SessionLegacy.WithParts[]
const paged = [] as SessionV1.WithParts[]
let cursor: string | undefined
while (true) {
const result = yield* MessageV2.page({ sessionID, limit: 3, before: cursor })

View file

@ -1,5 +1,5 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2Bridge } from "@/event-v2-bridge"
import { expect } from "bun:test"
@ -146,7 +146,7 @@ const assistant = Effect.fn("TestSession.assistant")(function* (
root: string,
) {
const session = yield* Session.Service
const msg: SessionLegacy.Assistant = {
const msg: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID,
@ -236,7 +236,7 @@ it.live("session.processor effect tests capture llm input cleanly", () =>
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -309,7 +309,7 @@ it.live("session.processor effect tests preserve text start time", () =>
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -321,7 +321,7 @@ it.live("session.processor effect tests preserve text start time", () =>
yield* waitFor(
MessageV2.parts(msg.id).pipe(
Effect.map((parts) => parts.find((part): part is SessionLegacy.TextPart => part.type === "text")),
Effect.map((parts) => parts.find((part): part is SessionV1.TextPart => part.type === "text")),
Effect.provideService(Database.Service, database),
),
"timed out waiting for text part",
@ -331,7 +331,7 @@ it.live("session.processor effect tests preserve text start time", () =>
const exit = yield* Fiber.await(run)
const text = (yield* MessageV2.parts(msg.id)).find(
(part): part is SessionLegacy.TextPart => part.type === "text",
(part): part is SessionV1.TextPart => part.type === "text",
)
expect(Exit.isSuccess(exit)).toBe(true)
@ -373,7 +373,7 @@ it.live("session.processor effect tests stop after token overflow requests compa
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -419,7 +419,7 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -429,8 +429,8 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
})
const parts = yield* MessageV2.parts(msg.id)
const reasoning = parts.find((part): part is SessionLegacy.ReasoningPart => part.type === "reasoning")
const text = parts.find((part): part is SessionLegacy.TextPart => part.type === "text")
const reasoning = parts.find((part): part is SessionV1.ReasoningPart => part.type === "reasoning")
const text = parts.find((part): part is SessionV1.TextPart => part.type === "text")
expect(value).toBe("continue")
expect(yield* llm.calls).toBe(1)
@ -467,7 +467,7 @@ it.live("session.processor effect tests reset reasoning state across retries", (
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -477,7 +477,7 @@ it.live("session.processor effect tests reset reasoning state across retries", (
})
const parts = yield* MessageV2.parts(msg.id)
const reasoning = parts.filter((part): part is SessionLegacy.ReasoningPart => part.type === "reasoning")
const reasoning = parts.filter((part): part is SessionV1.ReasoningPart => part.type === "reasoning")
expect(value).toBe("continue")
expect(yield* llm.calls).toBe(2)
@ -514,7 +514,7 @@ it.live("session.processor effect tests do not retry unknown json errors", () =>
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -558,7 +558,7 @@ it.live("session.processor effect tests retry recognized structured json errors"
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -613,7 +613,7 @@ it.live("session.processor effect tests publish retry status updates", () =>
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -658,7 +658,7 @@ it.live("session.processor effect tests compact on structured context overflow",
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -701,7 +701,7 @@ it.live("session.processor effect tests complete AI SDK tool calls when native f
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -721,7 +721,7 @@ it.live("session.processor effect tests complete AI SDK tool calls when native f
})
const parts = yield* MessageV2.parts(msg.id)
const call = parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
expect(value).toBe("continue")
expect(yield* llm.calls).toBe(1)
@ -768,7 +768,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -781,7 +781,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
yield* llm.wait(1)
yield* waitFor(
MessageV2.parts(msg.id).pipe(
Effect.map((parts) => parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")),
Effect.map((parts) => parts.find((part): part is SessionV1.ToolPart => part.type === "tool")),
Effect.provideService(Database.Service, database),
),
"timed out waiting for tool part",
@ -790,7 +790,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
const exit = yield* Fiber.await(run)
const parts = yield* MessageV2.parts(msg.id)
const call = parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
const call = parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) {
@ -847,7 +847,7 @@ it.live("session.processor effect tests record aborted errors and idle state", (
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),
@ -910,7 +910,7 @@ it.live("session.processor effect tests mark interruptions aborted without manua
time: parent.time,
agent: parent.agent,
model: { providerID: ref.providerID, modelID: ref.modelID },
} satisfies SessionLegacy.User,
} satisfies SessionV1.User,
sessionID: chat.id,
model: mdl,
agent: agent(),

View file

@ -1,5 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { eq } from "drizzle-orm"
import { EventV2Bridge } from "@/event-v2-bridge"
@ -91,20 +92,20 @@ function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
)
}
function toolPart(parts: SessionLegacy.Part[]) {
return parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
function toolPart(parts: SessionV1.Part[]) {
return parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
}
type CompletedToolPart = SessionLegacy.ToolPart & { state: SessionLegacy.ToolStateCompleted }
type ErrorToolPart = SessionLegacy.ToolPart & { state: SessionLegacy.ToolStateError }
type CompletedToolPart = SessionV1.ToolPart & { state: SessionV1.ToolStateCompleted }
type ErrorToolPart = SessionV1.ToolPart & { state: SessionV1.ToolStateError }
function completedTool(parts: SessionLegacy.Part[]) {
function completedTool(parts: SessionV1.Part[]) {
const part = toolPart(parts)
expect(part?.state.status).toBe("completed")
return part?.state.status === "completed" ? (part as CompletedToolPart) : undefined
}
function errorTool(parts: SessionLegacy.Part[]) {
function errorTool(parts: SessionV1.Part[]) {
const part = toolPart(parts)
expect(part?.state.status).toBe("error")
return part?.state.status === "error" ? (part as ErrorToolPart) : undefined
@ -305,14 +306,14 @@ const ensureDir = Effect.fn("test.ensureDir")(function* (dir: string) {
yield* fs.ensureDir(dir)
})
const writeConfig = Effect.fn("test.writeConfig")(function* (dir: string, config: Partial<Config.Info>) {
const writeConfig = Effect.fn("test.writeConfig")(function* (dir: string, config: Partial<ConfigV1.Info>) {
yield* writeText(
path.join(dir, "opencode.json"),
JSON.stringify({ $schema: "https://opencode.ai/config.json", ...config }),
)
})
const useServerConfig = Effect.fn("test.useServerConfig")(function* (config: (url: string) => Partial<Config.Info>) {
const useServerConfig = Effect.fn("test.useServerConfig")(function* (config: (url: string) => Partial<ConfigV1.Info>) {
const { directory: dir } = yield* TestInstance
const llm = yield* TestLLMServer
yield* writeConfig(dir, config(llm.url))
@ -389,7 +390,7 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, text: strin
const seed = Effect.fn("test.seed")(function* (sessionID: SessionID, opts?: { finish?: string }) {
const session = yield* Session.Service
const msg = yield* user(sessionID, "hello")
const assistant: SessionLegacy.Assistant = {
const assistant: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
parentID: msg.id,
@ -782,7 +783,7 @@ it.instance(
Effect.gen(function* () {
const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
const tool = taskMsg?.parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
const tool = taskMsg?.parts.find((part): part is SessionV1.ToolPart => part.type === "tool")
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
}),
"timed out waiting for running subtask metadata",
@ -825,7 +826,7 @@ it.instance(
const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
const assistant = msgs.findLast((item) => item.info.role === "assistant" && item.info.agent === "build")
const tool = assistant?.parts.find(
(part): part is SessionLegacy.ToolPart => part.type === "tool" && part.tool === "task",
(part): part is SessionV1.ToolPart => part.type === "tool" && part.tool === "task",
)
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
}),
@ -1946,11 +1947,11 @@ noLLMServer.instance(
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
)
const references = parts.filter(
(part): part is SessionLegacy.TextPartInput =>
(part): part is SessionV1.TextPartInput =>
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
)
const files = parts.filter((part): part is SessionLegacy.FilePartInput => part.type === "file")
const agents = parts.filter((part): part is SessionLegacy.AgentPartInput => part.type === "agent")
const files = parts.filter((part): part is SessionV1.FilePartInput => part.type === "file")
const agents = parts.filter((part): part is SessionV1.AgentPartInput => part.type === "agent")
const bare = references.find((part) => part.text.includes("@docs."))
const missing = references.find((part) => part.text.includes("@docs/missing.md"))
const guide = files.find((part) => part.filename === "docs/guide")
@ -2003,7 +2004,7 @@ noLLMServer.instance(
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts.filter(
(part): part is SessionLegacy.TextPart => part.type === "text" && part.synthetic === true,
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
)
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
@ -2058,7 +2059,7 @@ noLLMServer.instance(
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
const synthetic = stored.parts.filter(
(part): part is SessionLegacy.TextPart => part.type === "text" && part.synthetic === true,
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
)
const reference = synthetic.find((part) =>
part.text.startsWith("Referenced configured reference @docs/README.md."),

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import type { NamedError } from "@opencode-ai/core/util/error"
import { APICallError } from "ai"
import { setTimeout as sleep } from "node:timers/promises"
@ -17,9 +17,9 @@ const providerID = ProviderV2.ID.make("test")
const retryProvider = "test"
const it = testEffect(Layer.mergeAll(SessionStatus.defaultLayer, CrossSpawnSpawner.defaultLayer))
function apiError(headers?: Record<string, string>): SessionLegacy.APIError {
return Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
function apiError(headers?: Record<string, string>): SessionV1.APIError {
return Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "boom",
isRetryable: true,
responseHeaders: headers,
@ -94,7 +94,7 @@ describe("session.retry.delay", () => {
const step = yield* Schedule.toStepWithMetadata(
SessionRetry.policy({
provider: "test",
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
parse: Schema.decodeUnknownSync(SessionV1.APIError.Schema),
set: (info) =>
status.set(sessionID, {
type: "retry",
@ -164,7 +164,7 @@ describe("session.retry.retryable", () => {
test("retries transport timeout errors", () => {
const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID })
expect(SessionLegacy.APIError.isInstance(request)).toBe(true)
expect(SessionV1.APIError.isInstance(request)).toBe(true)
expect(SessionRetry.retryable(request, retryProvider)).toEqual({
message: "Provider response headers timed out after 10000ms",
})
@ -175,14 +175,14 @@ describe("session.retry.retryable", () => {
new ProviderError.ResponseStreamError("WebSocket closed before response.completed (code 1006: Connection ended)"),
{ providerID },
)
expect(SessionLegacy.APIError.isInstance(request)).toBe(true)
expect(SessionV1.APIError.isInstance(request)).toBe(true)
expect(SessionRetry.retryable(request, retryProvider)).toEqual({
message: "WebSocket closed before response.completed (code 1006: Connection ended)",
})
})
test("does not retry context overflow errors", () => {
const error = new SessionLegacy.ContextOverflowError({
const error = new SessionV1.ContextOverflowError({
message: "Input exceeds context window of this model",
responseBody: '{"error":{"code":"context_length_exceeded"}}',
}).toObject()
@ -191,8 +191,8 @@ describe("session.retry.retryable", () => {
})
test("retries 500 errors even when isRetryable is false", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Internal server error",
isRetryable: false,
statusCode: 500,
@ -204,8 +204,8 @@ describe("session.retry.retryable", () => {
})
test("retries 502 bad gateway errors", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Bad gateway",
isRetryable: false,
statusCode: 502,
@ -216,8 +216,8 @@ describe("session.retry.retryable", () => {
})
test("retries 503 service unavailable errors", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Service unavailable",
isRetryable: false,
statusCode: 503,
@ -228,8 +228,8 @@ describe("session.retry.retryable", () => {
})
test("does not retry 4xx errors when isRetryable is false", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Bad request",
isRetryable: false,
statusCode: 400,
@ -240,8 +240,8 @@ describe("session.retry.retryable", () => {
})
test("retries ZlibError decompression failures", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Response decompression failed",
isRetryable: true,
metadata: { code: "ZlibError" },
@ -254,8 +254,8 @@ describe("session.retry.retryable", () => {
})
test("maps free limits to Go upsell action", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Free usage exceeded",
isRetryable: true,
statusCode: 429,
@ -280,8 +280,8 @@ describe("session.retry.retryable", () => {
})
test("maps Go subscription limits to workspace PAYG upsell", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Subscription quota exceeded. You can continue using free models.",
isRetryable: true,
statusCode: 429,
@ -318,8 +318,8 @@ describe("session.retry.retryable", () => {
})
test("maps Go subscription limits without limit metadata", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Subscription quota exceeded. You can continue using free models.",
isRetryable: true,
statusCode: 429,
@ -373,8 +373,8 @@ describe("session.message-v2.fromError", () => {
const result = MessageV2.fromError(error, { providerID })
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
expect(SessionV1.APIError.isInstance(result)).toBe(true)
if (!SessionV1.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.isRetryable).toBe(true)
expect(result.data.message).toBe("Connection reset by server")
expect(result.data.metadata?.code).toBe("ECONNRESET")
@ -384,8 +384,8 @@ describe("session.message-v2.fromError", () => {
)
test("ECONNRESET socket error is retryable", () => {
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
new SessionLegacy.APIError({
const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)(
new SessionV1.APIError({
message: "Connection reset by server",
isRetryable: true,
metadata: { code: "ECONNRESET", message: "The socket connection was closed unexpectedly" },
@ -408,7 +408,7 @@ describe("session.message-v2.fromError", () => {
isRetryable: false,
})
const result = MessageV2.fromError(error, { providerID: ProviderV2.ID.make("openai") })
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
if (!SessionV1.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.isRetryable).toBe(true)
})
@ -429,8 +429,8 @@ describe("session.message-v2.fromError", () => {
{ providerID: ProviderV2.ID.make("openai") },
)
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
expect(SessionV1.APIError.isInstance(result)).toBe(true)
if (!SessionV1.APIError.isInstance(result)) throw new Error("expected APIError")
expect(result.data.isRetryable).toBe(true)
expect(SessionRetry.retryable(result, retryProvider)).toEqual({
message: "An error occurred while processing your request.",

View file

@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
@ -132,7 +132,7 @@ describe("revert + compact workflow", () => {
text: "Hello, please help me",
})
const assistantMsg1: SessionLegacy.Assistant = {
const assistantMsg1: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID,
@ -189,7 +189,7 @@ describe("revert + compact workflow", () => {
text: "What's the capital of France?",
})
const assistantMsg2: SessionLegacy.Assistant = {
const assistantMsg2: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID,
@ -294,7 +294,7 @@ describe("revert + compact workflow", () => {
text: "Hello",
})
const assistantMsg: SessionLegacy.Assistant = {
const assistantMsg: SessionV1.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID,

View file

@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Database } from "@opencode-ai/core/database/database"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { Deferred, Effect, Exit, Layer } from "effect"
@ -122,17 +122,17 @@ describe("step-finish token propagation via event", () => {
model: { providerID: "test", modelID: "test" },
tools: {},
mode: "",
} as unknown as SessionLegacy.Info)
} as unknown as SessionV1.Info)
// Event subscribers receive readonly Schema.Type payloads; `SessionLegacy.Part`
// Event subscribers receive readonly Schema.Type payloads; `SessionV1.Part`
// is the mutable domain type. Cast bridges the two — safe because the
// test only reads the value afterwards.
const received = yield* Deferred.make<SessionLegacy.Part>()
const received = yield* Deferred.make<SessionV1.Part>()
const unsub = yield* events.listen((event) => {
if (event.type === MessageV2.Event.PartUpdated.type)
Deferred.doneUnsafe(
received,
Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionLegacy.Part),
Effect.succeed((event.data as typeof MessageV2.Event.PartUpdated.data.Type).part as SessionV1.Part),
)
return Effect.void
})
@ -160,7 +160,7 @@ describe("step-finish token propagation via event", () => {
const receivedPart = yield* awaitDeferred(received, "timed out waiting for message.part.updated")
expect(receivedPart.type).toBe("step-finish")
const finish = receivedPart as SessionLegacy.StepFinishPart
const finish = receivedPart as SessionV1.StepFinishPart
expect(finish.tokens.input).toBe(500)
expect(finish.tokens.output).toBe(800)
expect(finish.tokens.reasoning).toBe(200)

View file

@ -22,7 +22,7 @@ import { SessionPrompt } from "../../src/session/prompt"
import { SessionRevert } from "../../src/session/revert"
import { SessionSummary } from "../../src/session/summary"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import * as Log from "@opencode-ai/core/util/log"
import { provideTmpdirServer } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@ -258,11 +258,11 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
// Verify the tool call completed (in the first assistant message)
const allMsgs = yield* MessageV2.filterCompactedEffect(session.id)
const user = allMsgs.find(
(msg): msg is SessionLegacy.WithParts & { info: SessionLegacy.User } => msg.info.role === "user",
(msg): msg is SessionV1.WithParts & { info: SessionV1.User } => msg.info.role === "user",
)
const tool = allMsgs
.flatMap((m) => m.parts)
.find((p): p is SessionLegacy.ToolPart => p.type === "tool" && p.tool === "bash")
.find((p): p is SessionV1.ToolPart => p.type === "tool" && p.tool === "bash")
expect(tool?.state.status).toBe("completed")
if (!user) throw new Error("Expected user message")

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Effect, Layer } from "effect"
import { Session } from "@/session/session"
import { SessionPrompt } from "../../src/session/prompt"
@ -219,7 +219,7 @@ describe("StructuredOutput Integration", () => {
)
test("unit test: StructuredOutputError is properly structured", () => {
const error = new SessionLegacy.StructuredOutputError({
const error = new SessionV1.StructuredOutputError({
message: "Failed to produce valid structured output after 3 attempts",
retries: 3,
})

View file

@ -1,13 +1,13 @@
import { describe, expect, test } from "bun:test"
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Exit, Schema } from "effect"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionPrompt } from "../../src/session/prompt"
import { SessionID, MessageID } from "../../src/session/schema"
const decodeFormat = Schema.decodeUnknownExit(SessionLegacy.Format)
const decodeUser = Schema.decodeUnknownExit(SessionLegacy.User)
const decodeAssistant = Schema.decodeUnknownExit(SessionLegacy.Assistant)
const decodeFormat = Schema.decodeUnknownExit(SessionV1.Format)
const decodeUser = Schema.decodeUnknownExit(SessionV1.User)
const decodeAssistant = Schema.decodeUnknownExit(SessionV1.Assistant)
describe("structured-output.OutputFormat", () => {
test("parses text format", () => {
@ -66,7 +66,7 @@ describe("structured-output.OutputFormat", () => {
describe("structured-output.StructuredOutputError", () => {
test("creates error with message and retries", () => {
const error = new SessionLegacy.StructuredOutputError({
const error = new SessionV1.StructuredOutputError({
message: "Failed to validate",
retries: 3,
})
@ -77,7 +77,7 @@ describe("structured-output.StructuredOutputError", () => {
})
test("converts to object correctly", () => {
const error = new SessionLegacy.StructuredOutputError({
const error = new SessionV1.StructuredOutputError({
message: "Test error",
retries: 2,
})
@ -89,13 +89,13 @@ describe("structured-output.StructuredOutputError", () => {
})
test("isInstance correctly identifies error", () => {
const error = new SessionLegacy.StructuredOutputError({
const error = new SessionV1.StructuredOutputError({
message: "Test",
retries: 1,
})
expect(SessionLegacy.StructuredOutputError.isInstance(error)).toBe(true)
expect(SessionLegacy.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
expect(SessionV1.StructuredOutputError.isInstance(error)).toBe(true)
expect(SessionV1.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
})
})

Some files were not shown because too many files have changed in this diff Show more