refactor(core): centralize legacy session schemas
This commit is contained in:
@@ -8,6 +8,7 @@ import { Flag } from "../flag/flag"
|
||||
import { isAbsolute, join } from "path"
|
||||
import { DatabaseMigration } from "./migration"
|
||||
import { InstallationChannel } from "../installation/version"
|
||||
import { makeRuntime } from "../effect/runtime"
|
||||
|
||||
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
|
||||
type DatabaseShape = Effect.Success<typeof makeDatabase>
|
||||
@@ -58,7 +59,6 @@ export const defaultLayer = Layer.unwrap(
|
||||
}),
|
||||
).pipe(Layer.provide(Global.defaultLayer))
|
||||
|
||||
export function init(options: { path?: string } = {}) {
|
||||
const filename = options.path ?? path()
|
||||
return Effect.runSync(Service.use(() => Effect.void).pipe(Effect.provide(layerFromPath(filename))))
|
||||
}
|
||||
const { runSync } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export const init = () => runSync(() => Effect.void)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
export * as SessionLegacy from "./legacy"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Effect, Schema, Types } from "effect"
|
||||
import { withStatics } from "../schema"
|
||||
import { Identifier } from "../util/identifier"
|
||||
import { NonNegativeInt } from "../schema"
|
||||
import { NamedError } from "../util/error"
|
||||
import { SessionSchema } from "./schema"
|
||||
|
||||
export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe(
|
||||
Schema.brand("MessageID"),
|
||||
@@ -15,3 +18,494 @@ export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe(
|
||||
withStatics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + Identifier.ascending()) })),
|
||||
)
|
||||
export type PartID = typeof PartID.Type
|
||||
|
||||
export const ProviderID = Schema.String.pipe(
|
||||
Schema.brand("ProviderID"),
|
||||
withStatics((schema) => ({
|
||||
opencode: schema.make("opencode"),
|
||||
anthropic: schema.make("anthropic"),
|
||||
openai: schema.make("openai"),
|
||||
google: schema.make("google"),
|
||||
googleVertex: schema.make("google-vertex"),
|
||||
githubCopilot: schema.make("github-copilot"),
|
||||
amazonBedrock: schema.make("amazon-bedrock"),
|
||||
azure: schema.make("azure"),
|
||||
openrouter: schema.make("openrouter"),
|
||||
mistral: schema.make("mistral"),
|
||||
gitlab: schema.make("gitlab"),
|
||||
})),
|
||||
)
|
||||
export type ProviderID = typeof ProviderID.Type
|
||||
|
||||
export const ModelID = Schema.String.pipe(Schema.brand("ModelID"))
|
||||
export type ModelID = typeof ModelID.Type
|
||||
|
||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", {})
|
||||
|
||||
export const AuthError = NamedError.create("ProviderAuthError", {
|
||||
providerID: Schema.String,
|
||||
message: Schema.String,
|
||||
})
|
||||
|
||||
export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
|
||||
export const StructuredOutputError = NamedError.create("StructuredOutputError", {
|
||||
message: Schema.String,
|
||||
retries: NonNegativeInt,
|
||||
})
|
||||
export const APIError = NamedError.create("APIError", {
|
||||
message: Schema.String,
|
||||
statusCode: Schema.optional(NonNegativeInt),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
})
|
||||
export type APIError = Schema.Schema.Type<typeof APIError.Schema>
|
||||
export const ContextOverflowError = NamedError.create("ContextOverflowError", {
|
||||
message: Schema.String,
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
|
||||
type: Schema.Literal("text"),
|
||||
}) {}
|
||||
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {}
|
||||
|
||||
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "OutputFormat",
|
||||
})
|
||||
export type OutputFormat = Schema.Schema.Type<typeof Format>
|
||||
|
||||
const partBase = {
|
||||
id: PartID,
|
||||
sessionID: SessionSchema.ID,
|
||||
messageID: MessageID,
|
||||
}
|
||||
|
||||
export const SnapshotPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("snapshot"),
|
||||
snapshot: Schema.String,
|
||||
}).annotate({ identifier: "SnapshotPart" })
|
||||
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
|
||||
|
||||
export const PatchPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("patch"),
|
||||
hash: Schema.String,
|
||||
files: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PatchPart" })
|
||||
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
|
||||
|
||||
export const TextPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPart" })
|
||||
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
|
||||
|
||||
export const ReasoningPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
}).annotate({ identifier: "ReasoningPart" })
|
||||
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
|
||||
|
||||
const filePartSourceBase = {
|
||||
text: Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Finite,
|
||||
end: Schema.Finite,
|
||||
}).annotate({ identifier: "FilePartSourceText" }),
|
||||
}
|
||||
|
||||
export const Range = Schema.Struct({
|
||||
start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
|
||||
end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }),
|
||||
}).annotate({ identifier: "Range" })
|
||||
export type Range = typeof Range.Type
|
||||
|
||||
export const FileSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}).annotate({ identifier: "FileSource" })
|
||||
|
||||
export const SymbolSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("symbol"),
|
||||
path: Schema.String,
|
||||
range: Range,
|
||||
name: Schema.String,
|
||||
kind: NonNegativeInt,
|
||||
}).annotate({ identifier: "SymbolSource" })
|
||||
|
||||
export const ResourceSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("resource"),
|
||||
clientName: Schema.String,
|
||||
uri: Schema.String,
|
||||
}).annotate({ identifier: "ResourceSource" })
|
||||
|
||||
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "FilePartSource",
|
||||
})
|
||||
|
||||
export const FilePart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePart" })
|
||||
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
|
||||
|
||||
export const AgentPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPart" })
|
||||
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
|
||||
|
||||
export const CompactionPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("compaction"),
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.optional(Schema.Boolean),
|
||||
tail_start_id: Schema.optional(MessageID),
|
||||
}).annotate({ identifier: "CompactionPart" })
|
||||
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
|
||||
|
||||
export const SubtaskPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPart" })
|
||||
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
|
||||
|
||||
export const RetryPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("retry"),
|
||||
attempt: NonNegativeInt,
|
||||
error: APIError.EffectSchema,
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "RetryPart" })
|
||||
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
|
||||
error: APIError
|
||||
}
|
||||
|
||||
export const StepStartPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-start"),
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "StepStartPart" })
|
||||
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
|
||||
|
||||
export const StepFinishPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-finish"),
|
||||
reason: Schema.String,
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
}).annotate({ identifier: "StepFinishPart" })
|
||||
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
|
||||
|
||||
export const ToolStatePending = Schema.Struct({
|
||||
status: Schema.Literal("pending"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
raw: Schema.String,
|
||||
}).annotate({ identifier: "ToolStatePending" })
|
||||
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
|
||||
|
||||
export const ToolStateRunning = Schema.Struct({
|
||||
status: Schema.Literal("running"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
title: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateRunning" })
|
||||
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
|
||||
|
||||
export const ToolStateCompleted = Schema.Struct({
|
||||
status: Schema.Literal("completed"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
output: Schema.String,
|
||||
title: Schema.String,
|
||||
metadata: Schema.Record(Schema.String, Schema.Any),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
compacted: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
attachments: Schema.optional(Schema.Array(FilePart)),
|
||||
}).annotate({ identifier: "ToolStateCompleted" })
|
||||
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
|
||||
|
||||
export const ToolStateError = Schema.Struct({
|
||||
status: Schema.Literal("error"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
error: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateError" })
|
||||
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
|
||||
|
||||
export const ToolState = Schema.Union([
|
||||
ToolStatePending,
|
||||
ToolStateRunning,
|
||||
ToolStateCompleted,
|
||||
ToolStateError,
|
||||
]).annotate({
|
||||
discriminator: "status",
|
||||
identifier: "ToolState",
|
||||
})
|
||||
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
|
||||
|
||||
export const ToolPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("tool"),
|
||||
callID: Schema.String,
|
||||
tool: Schema.String,
|
||||
state: ToolState,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "ToolPart" })
|
||||
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
|
||||
state: ToolState
|
||||
}
|
||||
|
||||
const messageBase = {
|
||||
id: MessageID,
|
||||
sessionID: partBase.sessionID,
|
||||
}
|
||||
|
||||
const FileDiff = Schema.Struct({
|
||||
file: Schema.optional(Schema.String),
|
||||
patch: Schema.optional(Schema.String),
|
||||
additions: Schema.Finite,
|
||||
deletions: Schema.Finite,
|
||||
status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])),
|
||||
}).annotate({ identifier: "SnapshotFileDiff" })
|
||||
|
||||
export const User = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("user"),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
}),
|
||||
format: Schema.optional(Format),
|
||||
summary: Schema.optional(
|
||||
Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
body: Schema.optional(Schema.String),
|
||||
diffs: Schema.Array(FileDiff),
|
||||
}),
|
||||
),
|
||||
agent: Schema.String,
|
||||
model: Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
variant: Schema.optional(Schema.String),
|
||||
}),
|
||||
system: Schema.optional(Schema.String),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
}).annotate({ identifier: "UserMessage" })
|
||||
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
|
||||
|
||||
export const Part = Schema.Union([
|
||||
TextPart,
|
||||
SubtaskPart,
|
||||
ReasoningPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
SnapshotPart,
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
RetryPart,
|
||||
CompactionPart,
|
||||
]).annotate({ discriminator: "type", identifier: "Part" })
|
||||
export type Part =
|
||||
| TextPart
|
||||
| SubtaskPart
|
||||
| ReasoningPart
|
||||
| FilePart
|
||||
| ToolPart
|
||||
| StepStartPart
|
||||
| StepFinishPart
|
||||
| SnapshotPart
|
||||
| PatchPart
|
||||
| AgentPart
|
||||
| RetryPart
|
||||
| CompactionPart
|
||||
|
||||
const AssistantErrorSchema = Schema.Union([
|
||||
AuthError.EffectSchema,
|
||||
NamedError.Unknown.EffectSchema,
|
||||
OutputLengthError.EffectSchema,
|
||||
AbortedError.EffectSchema,
|
||||
StructuredOutputError.EffectSchema,
|
||||
ContextOverflowError.EffectSchema,
|
||||
APIError.EffectSchema,
|
||||
]).annotate({ discriminator: "name" })
|
||||
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
|
||||
|
||||
export const TextPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPartInput" })
|
||||
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
|
||||
|
||||
export const FilePartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePartInput" })
|
||||
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
|
||||
|
||||
export const AgentPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPartInput" })
|
||||
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
|
||||
|
||||
export const SubtaskPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPartInput" })
|
||||
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
||||
|
||||
export const Assistant = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("assistant"),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
completed: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
error: Schema.optional(AssistantErrorSchema),
|
||||
parentID: MessageID,
|
||||
modelID: ModelID,
|
||||
providerID: ProviderID,
|
||||
mode: Schema.String,
|
||||
agent: Schema.String,
|
||||
path: Schema.Struct({
|
||||
cwd: Schema.String,
|
||||
root: Schema.String,
|
||||
}),
|
||||
summary: Schema.optional(Schema.Boolean),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
structured: Schema.optional(Schema.Any),
|
||||
variant: Schema.optional(Schema.String),
|
||||
finish: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "AssistantMessage" })
|
||||
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
|
||||
error?: AssistantError
|
||||
}
|
||||
|
||||
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
|
||||
export type Info = User | Assistant
|
||||
|
||||
export const WithParts = Schema.Struct({
|
||||
info: Info,
|
||||
parts: Schema.Array(Part),
|
||||
})
|
||||
export type WithParts = {
|
||||
info: Info
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import type { Snapshot } from "../snapshot"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { ProjectV2 } from "../project"
|
||||
import type { SessionSchema } from "./schema"
|
||||
import type { MessageID, PartID } from "./legacy"
|
||||
import type { MessageID, PartID, Info as LegacyMessageInfo, Part as LegacyMessagePart } from "./legacy"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
|
||||
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
||||
type LegacyMessageData = Omit<LegacyMessageInfo, "id" | "sessionID">
|
||||
type LegacyPartData = Omit<LegacyMessagePart, "id" | "sessionID" | "messageID">
|
||||
|
||||
export const SessionTable = sqliteTable(
|
||||
"session",
|
||||
@@ -65,7 +67,7 @@ export const MessageTable = sqliteTable(
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
...Timestamps,
|
||||
data: text({ mode: "json" }).notNull(),
|
||||
data: text({ mode: "json" }).notNull().$type<LegacyMessageData>(),
|
||||
},
|
||||
(table) => [index("message_session_time_created_id_idx").on(table.session_id, table.time_created, table.id)],
|
||||
)
|
||||
@@ -80,7 +82,7 @@ export const PartTable = sqliteTable(
|
||||
.references(() => MessageTable.id, { onDelete: "cascade" }),
|
||||
session_id: text().$type<SessionSchema.ID>().notNull(),
|
||||
...Timestamps,
|
||||
data: text({ mode: "json" }).notNull(),
|
||||
data: text({ mode: "json" }).notNull().$type<LegacyPartData>(),
|
||||
},
|
||||
(table) => [
|
||||
index("part_message_id_id_idx").on(table.message_id, table.id),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EOL } from "os"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { basename } from "path"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { Agent } from "../../../agent/agent"
|
||||
@@ -163,7 +164,7 @@ const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(functio
|
||||
)
|
||||
})
|
||||
const now = Date.now()
|
||||
const message: MessageV2.Assistant = {
|
||||
const message: SessionLegacy.Assistant = {
|
||||
id: messageID,
|
||||
sessionID: session.id,
|
||||
role: "assistant",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { SessionID } from "../../session/schema"
|
||||
import { effectCmd, fail } from "../effect-cmd"
|
||||
@@ -31,7 +32,7 @@ function diff(kind: string, diffs: { file?: string; patch?: string }[] | undefin
|
||||
}))
|
||||
}
|
||||
|
||||
function source(part: MessageV2.FilePart) {
|
||||
function source(part: SessionLegacy.FilePart) {
|
||||
if (!part.source) return part.source
|
||||
if (part.source.type === "symbol") {
|
||||
return {
|
||||
@@ -56,7 +57,7 @@ function source(part: MessageV2.FilePart) {
|
||||
}
|
||||
}
|
||||
|
||||
function filepart(part: MessageV2.FilePart): MessageV2.FilePart {
|
||||
function filepart(part: SessionLegacy.FilePart): SessionLegacy.FilePart {
|
||||
return {
|
||||
...part,
|
||||
url: redact("file-url", part.id, part.url),
|
||||
@@ -65,7 +66,7 @@ function filepart(part: MessageV2.FilePart): MessageV2.FilePart {
|
||||
}
|
||||
}
|
||||
|
||||
function part(part: MessageV2.Part): MessageV2.Part {
|
||||
function part(part: SessionLegacy.Part): SessionLegacy.Part {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return {
|
||||
@@ -159,7 +160,7 @@ function part(part: MessageV2.Part): MessageV2.Part {
|
||||
|
||||
const partFn = part
|
||||
|
||||
function sanitize(data: { info: Session.Info; messages: MessageV2.WithParts[] }) {
|
||||
function sanitize(data: { info: Session.Info; messages: SessionLegacy.WithParts[] }) {
|
||||
return {
|
||||
info: {
|
||||
...data.info,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "path"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { exec } from "child_process"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import * as prompts from "@clack/prompts"
|
||||
@@ -159,7 +160,7 @@ export { parseGitHubRemote }
|
||||
* Returns null for non-text responses (signals summary needed).
|
||||
* Throws only for truly empty responses.
|
||||
*/
|
||||
export function extractResponseText(parts: MessageV2.Part[]): string | null {
|
||||
export function extractResponseText(parts: SessionLegacy.Part[]): string | null {
|
||||
const textPart = parts.findLast((p) => p.type === "text")
|
||||
if (textPart) return textPart.text
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "../../session/message-v2"
|
||||
import { CliError, effectCmd } from "../effect-cmd"
|
||||
@@ -12,8 +13,8 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
|
||||
const decodeMessageInfo = Schema.decodeUnknownSync(MessageV2.Info)
|
||||
const decodePart = Schema.decodeUnknownSync(MessageV2.Part)
|
||||
const decodeMessageInfo = Schema.decodeUnknownSync(SessionLegacy.Info)
|
||||
const decodePart = Schema.decodeUnknownSync(SessionLegacy.Part)
|
||||
|
||||
/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
|
||||
export type ShareData =
|
||||
@@ -187,7 +188,7 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
|
||||
)
|
||||
|
||||
for (const msg of exportData.messages) {
|
||||
const msgInfo = decodeMessageInfo(msg.info) as MessageV2.Info
|
||||
const msgInfo = decodeMessageInfo(msg.info) as SessionLegacy.Info
|
||||
const { id, sessionID: _, ...msgData } = msgInfo
|
||||
Database.use((db) =>
|
||||
db
|
||||
@@ -203,7 +204,7 @@ const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: Ins
|
||||
)
|
||||
|
||||
for (const part of msg.parts) {
|
||||
const partInfo = decodePart(part) as MessageV2.Part
|
||||
const partInfo = decodePart(part) as SessionLegacy.Part
|
||||
const { id: partId, sessionID: _s, messageID, ...partData } = partInfo
|
||||
Database.use((db) =>
|
||||
db
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
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" }
|
||||
@@ -52,7 +53,7 @@ export class SizeError extends Schema.TaggedErrorClass<SizeError>()("ImageSizeEr
|
||||
export type Error = ResizerUnavailableError | InvalidDataUrlError | DecodeError | SizeError
|
||||
|
||||
export interface Interface {
|
||||
readonly normalize: (input: MessageV2.FilePart) => Effect.Effect<MessageV2.FilePart, Error>
|
||||
readonly normalize: (input: SessionLegacy.FilePart) => Effect.Effect<SessionLegacy.FilePart, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Image") {}
|
||||
@@ -73,7 +74,7 @@ export const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
|
||||
const normalize = Effect.fn("Image.normalize")(function* (input: MessageV2.FilePart) {
|
||||
const normalize = Effect.fn("Image.normalize")(function* (input: SessionLegacy.FilePart) {
|
||||
const image = (yield* config.get()).attachment?.image
|
||||
const info = {
|
||||
autoResize: image?.auto_resize ?? AUTO_RESIZE,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Session } from "@/session/session"
|
||||
@@ -175,7 +176,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("messages", SessionPaths.messages, {
|
||||
params: { sessionID: SessionID },
|
||||
query: MessagesQuery,
|
||||
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
||||
success: described(Schema.Array(SessionLegacy.WithParts), "List of messages"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -187,7 +188,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
success: described(MessageV2.WithParts, "Message"),
|
||||
success: described(SessionLegacy.WithParts, "Message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -313,7 +314,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: PromptPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
success: described(SessionLegacy.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -340,7 +341,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: CommandPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
success: described(SessionLegacy.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -353,7 +354,7 @@ export const SessionApi = HttpApi.make("session")
|
||||
params: { sessionID: SessionID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: ShellPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
success: described(SessionLegacy.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
@@ -429,8 +430,8 @@ export const SessionApi = HttpApi.make("session")
|
||||
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
query: WorkspaceRoutingQuery,
|
||||
payload: MessageV2.Part,
|
||||
success: described(MessageV2.Part, "Successfully updated part"),
|
||||
payload: SessionLegacy.Part,
|
||||
success: described(SessionLegacy.Part, "Successfully updated part"),
|
||||
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Bus } from "@/bus"
|
||||
import { Command } from "@/command"
|
||||
import { Permission } from "@/permission"
|
||||
@@ -389,10 +390,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 MessageV2.Part.Type
|
||||
payload: typeof SessionLegacy.Part.Type
|
||||
}) {
|
||||
yield* requireSession(ctx.params.sessionID)
|
||||
const payload = ctx.payload as MessageV2.Part
|
||||
const payload = ctx.payload as SessionLegacy.Part
|
||||
if (
|
||||
payload.id !== ctx.params.partID ||
|
||||
payload.messageID !== ctx.params.messageID ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Bus } from "@/bus"
|
||||
import * as Session from "./session"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
@@ -92,9 +93,9 @@ type CompletedCompaction = {
|
||||
summary: string | undefined
|
||||
}
|
||||
|
||||
function summaryText(message: MessageV2.WithParts) {
|
||||
function summaryText(message: SessionLegacy.WithParts) {
|
||||
const text = message.parts
|
||||
.filter((part): part is MessageV2.TextPart => part.type === "text")
|
||||
.filter((part): part is SessionLegacy.TextPart => part.type === "text")
|
||||
.map((part) => part.text.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
@@ -102,7 +103,7 @@ function summaryText(message: MessageV2.WithParts) {
|
||||
return text || undefined
|
||||
}
|
||||
|
||||
function completedCompactions(messages: MessageV2.WithParts[]) {
|
||||
function completedCompactions(messages: SessionLegacy.WithParts[]) {
|
||||
const users = new Map<MessageID, number>()
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
@@ -140,7 +141,7 @@ function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }
|
||||
)
|
||||
}
|
||||
|
||||
function turns(messages: MessageV2.WithParts[]) {
|
||||
function turns(messages: SessionLegacy.WithParts[]) {
|
||||
const result: Turn[] = []
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
@@ -159,11 +160,11 @@ function turns(messages: MessageV2.WithParts[]) {
|
||||
}
|
||||
|
||||
function splitTurn(input: {
|
||||
messages: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
turn: Turn
|
||||
model: Provider.Model
|
||||
budget: number
|
||||
estimate: (input: { messages: MessageV2.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
|
||||
estimate: (input: { messages: SessionLegacy.WithParts[]; model: Provider.Model }) => Effect.Effect<number>
|
||||
}) {
|
||||
return Effect.gen(function* () {
|
||||
if (input.budget <= 0) return undefined
|
||||
@@ -185,13 +186,13 @@ function splitTurn(input: {
|
||||
|
||||
export interface Interface {
|
||||
readonly isOverflow: (input: {
|
||||
tokens: MessageV2.Assistant["tokens"]
|
||||
tokens: SessionLegacy.Assistant["tokens"]
|
||||
model: Provider.Model
|
||||
}) => Effect.Effect<boolean>
|
||||
readonly prune: (input: { sessionID: SessionID }) => Effect.Effect<void>
|
||||
readonly process: (input: {
|
||||
parentID: MessageID
|
||||
messages: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.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: MessageV2.Assistant["tokens"]
|
||||
tokens: SessionLegacy.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: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
model: Provider.Model
|
||||
}) {
|
||||
const msgs = yield* MessageV2.toModelMessagesEffect(input.messages, input.model)
|
||||
@@ -243,7 +244,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const select = Effect.fn("SessionCompaction.select")(function* (input: {
|
||||
messages: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
cfg: Config.Info
|
||||
model: Provider.Model
|
||||
}) {
|
||||
@@ -307,7 +308,7 @@ export const layer = Layer.effect(
|
||||
|
||||
let total = 0
|
||||
let pruned = 0
|
||||
const toPrune: MessageV2.ToolPart[] = []
|
||||
const toPrune: SessionLegacy.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: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
sessionID: SessionID
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
@@ -353,13 +354,13 @@ export const layer = Layer.effect(
|
||||
throw new Error(`Compaction parent must be a user message: ${input.parentID}`)
|
||||
}
|
||||
const userMessage = parent.info
|
||||
const compactionPart = parent.parts.find((part): part is MessageV2.CompactionPart => part.type === "compaction")
|
||||
const compactionPart = parent.parts.find((part): part is SessionLegacy.CompactionPart => part.type === "compaction")
|
||||
|
||||
let messages = input.messages
|
||||
let replay:
|
||||
| {
|
||||
info: MessageV2.User
|
||||
parts: MessageV2.Part[]
|
||||
info: SessionLegacy.User
|
||||
parts: SessionLegacy.Part[]
|
||||
}
|
||||
| undefined
|
||||
if (input.overflow) {
|
||||
@@ -408,7 +409,7 @@ export const layer = Layer.effect(
|
||||
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
|
||||
})
|
||||
const ctx = yield* InstanceState.context
|
||||
const msg: MessageV2.Assistant = {
|
||||
const msg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: input.parentID,
|
||||
@@ -457,7 +458,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
if (result === "compact") {
|
||||
processor.message.error = new MessageV2.ContextOverflowError({
|
||||
processor.message.error = new SessionLegacy.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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "path"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect, Layer, Context } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Config } from "@/config/config"
|
||||
@@ -17,7 +18,7 @@ const files = (disableClaudeCodePrompt: boolean) => [
|
||||
"CONTEXT.md", // deprecated
|
||||
]
|
||||
|
||||
function extract(messages: MessageV2.WithParts[]) {
|
||||
function extract(messages: SessionLegacy.WithParts[]) {
|
||||
const paths = new Set<string>()
|
||||
for (const msg of messages) {
|
||||
for (const part of msg.parts) {
|
||||
@@ -40,7 +41,7 @@ export interface Interface {
|
||||
readonly system: () => Effect.Effect<string[], AppFileSystem.Error>
|
||||
readonly find: (dir: string) => Effect.Effect<string | undefined, AppFileSystem.Error>
|
||||
readonly resolve: (
|
||||
messages: MessageV2.WithParts[],
|
||||
messages: SessionLegacy.WithParts[],
|
||||
filepath: string,
|
||||
messageID: MessageID,
|
||||
) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error>
|
||||
@@ -176,7 +177,7 @@ export const layer: Layer.Layer<
|
||||
})
|
||||
|
||||
const resolve = Effect.fn("Instruction.resolve")(function* (
|
||||
messages: MessageV2.WithParts[],
|
||||
messages: SessionLegacy.WithParts[],
|
||||
filepath: string,
|
||||
messageID: MessageID,
|
||||
) {
|
||||
@@ -231,7 +232,7 @@ export const defaultLayer = layer.pipe(
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
)
|
||||
|
||||
export function loaded(messages: MessageV2.WithParts[]) {
|
||||
export function loaded(messages: SessionLegacy.WithParts[]) {
|
||||
return extract(messages)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
@@ -31,7 +32,7 @@ const log = Log.create({ service: "llm" })
|
||||
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
|
||||
|
||||
export type StreamInput = {
|
||||
user: MessageV2.User
|
||||
user: SessionLegacy.User
|
||||
sessionID: string
|
||||
parentSessionID?: string
|
||||
model: Provider.Model
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Auth } from "@/auth"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Permission } from "@/permission"
|
||||
@@ -16,7 +17,7 @@ import { mergeDeep } from "remeda"
|
||||
const USER_AGENT = `opencode/${InstallationVersion}`
|
||||
|
||||
type PrepareInput = {
|
||||
readonly user: MessageV2.User
|
||||
readonly user: SessionLegacy.User
|
||||
readonly sessionID: string
|
||||
readonly parentSessionID?: string
|
||||
readonly model: Provider.Model
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import {
|
||||
APIError,
|
||||
AbortedError,
|
||||
Assistant,
|
||||
AuthError,
|
||||
CompactionPart,
|
||||
ContextOverflowError,
|
||||
Info,
|
||||
OutputLengthError,
|
||||
Part,
|
||||
StructuredOutputError,
|
||||
SubtaskPart,
|
||||
User,
|
||||
WithParts,
|
||||
type ModelID,
|
||||
type ProviderID,
|
||||
type ToolPart,
|
||||
} from "@opencode-ai/core/session/legacy"
|
||||
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { SyncEvent } from "../sync"
|
||||
import { Database } from "@/storage/db"
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
@@ -20,13 +37,8 @@ import { errorMessage } from "@/util/error"
|
||||
import { isMedia } from "@/util/media"
|
||||
import type { SystemError } from "bun"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Schema, Types } from "effect"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as EffectLogger from "@opencode-ai/core/effect/logger"
|
||||
import { MessageError } from "./message-error"
|
||||
import { AuthError, OutputLengthError } from "./message-error"
|
||||
export { AuthError, OutputLengthError } from "./message-error"
|
||||
|
||||
/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
|
||||
interface FetchDecompressionError extends Error {
|
||||
@@ -38,460 +50,12 @@ interface FetchDecompressionError extends Error {
|
||||
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached media from tool result:"
|
||||
export { isMedia }
|
||||
|
||||
export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
|
||||
export const StructuredOutputError = NamedError.create("StructuredOutputError", {
|
||||
message: Schema.String,
|
||||
retries: NonNegativeInt,
|
||||
})
|
||||
export const APIError = NamedError.create("APIError", {
|
||||
message: Schema.String,
|
||||
statusCode: Schema.optional(NonNegativeInt),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
})
|
||||
export type APIError = Schema.Schema.Type<typeof APIError.Schema>
|
||||
export const ContextOverflowError = NamedError.create("ContextOverflowError", {
|
||||
message: Schema.String,
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({
|
||||
type: Schema.Literal("text"),
|
||||
}) {}
|
||||
|
||||
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
||||
type: Schema.Literal("json_schema"),
|
||||
schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
||||
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
||||
}) {}
|
||||
|
||||
export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "OutputFormat",
|
||||
})
|
||||
export type OutputFormat = Schema.Schema.Type<typeof Format>
|
||||
|
||||
const partBase = {
|
||||
id: PartID,
|
||||
sessionID: SessionID,
|
||||
messageID: MessageID,
|
||||
}
|
||||
|
||||
export const SnapshotPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("snapshot"),
|
||||
snapshot: Schema.String,
|
||||
}).annotate({ identifier: "SnapshotPart" })
|
||||
export type SnapshotPart = Types.DeepMutable<Schema.Schema.Type<typeof SnapshotPart>>
|
||||
|
||||
export const PatchPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("patch"),
|
||||
hash: Schema.String,
|
||||
files: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "PatchPart" })
|
||||
export type PatchPart = Types.DeepMutable<Schema.Schema.Type<typeof PatchPart>>
|
||||
|
||||
export const TextPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPart" })
|
||||
export type TextPart = Types.DeepMutable<Schema.Schema.Type<typeof TextPart>>
|
||||
|
||||
export const ReasoningPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
}).annotate({ identifier: "ReasoningPart" })
|
||||
export type ReasoningPart = Types.DeepMutable<Schema.Schema.Type<typeof ReasoningPart>>
|
||||
|
||||
const filePartSourceBase = {
|
||||
text: Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: Schema.Finite,
|
||||
end: Schema.Finite,
|
||||
}).annotate({ identifier: "FilePartSourceText" }),
|
||||
}
|
||||
|
||||
export const FileSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("file"),
|
||||
path: Schema.String,
|
||||
}).annotate({ identifier: "FileSource" })
|
||||
|
||||
export const SymbolSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("symbol"),
|
||||
path: Schema.String,
|
||||
range: LSP.Range,
|
||||
name: Schema.String,
|
||||
kind: NonNegativeInt,
|
||||
}).annotate({ identifier: "SymbolSource" })
|
||||
|
||||
export const ResourceSource = Schema.Struct({
|
||||
...filePartSourceBase,
|
||||
type: Schema.Literal("resource"),
|
||||
clientName: Schema.String,
|
||||
uri: Schema.String,
|
||||
}).annotate({ identifier: "ResourceSource" })
|
||||
|
||||
export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({
|
||||
discriminator: "type",
|
||||
identifier: "FilePartSource",
|
||||
})
|
||||
|
||||
export const FilePart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePart" })
|
||||
export type FilePart = Types.DeepMutable<Schema.Schema.Type<typeof FilePart>>
|
||||
|
||||
export const AgentPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPart" })
|
||||
export type AgentPart = Types.DeepMutable<Schema.Schema.Type<typeof AgentPart>>
|
||||
|
||||
export const CompactionPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("compaction"),
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.optional(Schema.Boolean),
|
||||
tail_start_id: Schema.optional(MessageID),
|
||||
}).annotate({ identifier: "CompactionPart" })
|
||||
export type CompactionPart = Types.DeepMutable<Schema.Schema.Type<typeof CompactionPart>>
|
||||
|
||||
export const SubtaskPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPart" })
|
||||
export type SubtaskPart = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPart>>
|
||||
|
||||
export const RetryPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("retry"),
|
||||
attempt: NonNegativeInt,
|
||||
error: APIError.EffectSchema,
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "RetryPart" })
|
||||
export type RetryPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof RetryPart>>, "error"> & {
|
||||
error: APIError
|
||||
}
|
||||
|
||||
export const StepStartPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-start"),
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "StepStartPart" })
|
||||
export type StepStartPart = Types.DeepMutable<Schema.Schema.Type<typeof StepStartPart>>
|
||||
|
||||
export const StepFinishPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("step-finish"),
|
||||
reason: Schema.String,
|
||||
snapshot: Schema.optional(Schema.String),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
}).annotate({ identifier: "StepFinishPart" })
|
||||
export type StepFinishPart = Types.DeepMutable<Schema.Schema.Type<typeof StepFinishPart>>
|
||||
|
||||
export const ToolStatePending = Schema.Struct({
|
||||
status: Schema.Literal("pending"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
raw: Schema.String,
|
||||
}).annotate({ identifier: "ToolStatePending" })
|
||||
export type ToolStatePending = Types.DeepMutable<Schema.Schema.Type<typeof ToolStatePending>>
|
||||
|
||||
export const ToolStateRunning = Schema.Struct({
|
||||
status: Schema.Literal("running"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
title: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateRunning" })
|
||||
export type ToolStateRunning = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateRunning>>
|
||||
|
||||
export const ToolStateCompleted = Schema.Struct({
|
||||
status: Schema.Literal("completed"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
output: Schema.String,
|
||||
title: Schema.String,
|
||||
metadata: Schema.Record(Schema.String, Schema.Any),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
compacted: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
attachments: Schema.optional(Schema.Array(FilePart)),
|
||||
}).annotate({ identifier: "ToolStateCompleted" })
|
||||
export type ToolStateCompleted = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateCompleted>>
|
||||
|
||||
function truncateToolOutput(text: string, maxChars?: number) {
|
||||
if (!maxChars || text.length <= maxChars) return text
|
||||
const omitted = text.length - maxChars
|
||||
return `${text.slice(0, maxChars)}\n[Tool output truncated for compaction: omitted ${omitted} chars]`
|
||||
}
|
||||
|
||||
export const ToolStateError = Schema.Struct({
|
||||
status: Schema.Literal("error"),
|
||||
input: Schema.Record(Schema.String, Schema.Any),
|
||||
error: Schema.String,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
time: Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
}).annotate({ identifier: "ToolStateError" })
|
||||
export type ToolStateError = Types.DeepMutable<Schema.Schema.Type<typeof ToolStateError>>
|
||||
|
||||
export const ToolState = Schema.Union([
|
||||
ToolStatePending,
|
||||
ToolStateRunning,
|
||||
ToolStateCompleted,
|
||||
ToolStateError,
|
||||
]).annotate({
|
||||
discriminator: "status",
|
||||
identifier: "ToolState",
|
||||
})
|
||||
export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError
|
||||
|
||||
export const ToolPart = Schema.Struct({
|
||||
...partBase,
|
||||
type: Schema.Literal("tool"),
|
||||
callID: Schema.String,
|
||||
tool: Schema.String,
|
||||
state: ToolState,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "ToolPart" })
|
||||
export type ToolPart = Omit<Types.DeepMutable<Schema.Schema.Type<typeof ToolPart>>, "state"> & {
|
||||
state: ToolState
|
||||
}
|
||||
|
||||
const messageBase = {
|
||||
id: MessageID,
|
||||
sessionID: SessionID,
|
||||
}
|
||||
|
||||
export const User = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("user"),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
}),
|
||||
format: Schema.optional(Format),
|
||||
summary: Schema.optional(
|
||||
Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
body: Schema.optional(Schema.String),
|
||||
diffs: Schema.Array(Snapshot.FileDiff),
|
||||
}),
|
||||
),
|
||||
agent: Schema.String,
|
||||
model: Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
variant: Schema.optional(Schema.String),
|
||||
}),
|
||||
system: Schema.optional(Schema.String),
|
||||
tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
|
||||
}).annotate({ identifier: "UserMessage" })
|
||||
export type User = Types.DeepMutable<Schema.Schema.Type<typeof User>>
|
||||
|
||||
export const Part = Schema.Union([
|
||||
TextPart,
|
||||
SubtaskPart,
|
||||
ReasoningPart,
|
||||
FilePart,
|
||||
ToolPart,
|
||||
StepStartPart,
|
||||
StepFinishPart,
|
||||
SnapshotPart,
|
||||
PatchPart,
|
||||
AgentPart,
|
||||
RetryPart,
|
||||
CompactionPart,
|
||||
]).annotate({ discriminator: "type", identifier: "Part" })
|
||||
export type Part =
|
||||
| TextPart
|
||||
| SubtaskPart
|
||||
| ReasoningPart
|
||||
| FilePart
|
||||
| ToolPart
|
||||
| StepStartPart
|
||||
| StepFinishPart
|
||||
| SnapshotPart
|
||||
| PatchPart
|
||||
| AgentPart
|
||||
| RetryPart
|
||||
| CompactionPart
|
||||
|
||||
const AssistantErrorSchema = Schema.Union([
|
||||
...MessageError.Shared,
|
||||
AbortedError.EffectSchema,
|
||||
StructuredOutputError.EffectSchema,
|
||||
ContextOverflowError.EffectSchema,
|
||||
APIError.EffectSchema,
|
||||
]).annotate({ discriminator: "name" })
|
||||
type AssistantError = Schema.Schema.Type<typeof AssistantErrorSchema>
|
||||
|
||||
// ── Prompt input schemas ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Consumers of `SessionPrompt.PromptInput.parts` send part drafts without the
|
||||
// ambient IDs (`messageID`, `sessionID`) that live on stored parts, and may
|
||||
// omit `id` to let the server allocate one. These Schema-Struct variants
|
||||
// carry that shape so prompt decoding can accept drafts without stored IDs.
|
||||
|
||||
export const TextPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
synthetic: Schema.optional(Schema.Boolean),
|
||||
ignored: Schema.optional(Schema.Boolean),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
|
||||
}).annotate({ identifier: "TextPartInput" })
|
||||
export type TextPartInput = Types.DeepMutable<Schema.Schema.Type<typeof TextPartInput>>
|
||||
|
||||
export const FilePartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("file"),
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(FilePartSource),
|
||||
}).annotate({ identifier: "FilePartInput" })
|
||||
export type FilePartInput = Types.DeepMutable<Schema.Schema.Type<typeof FilePartInput>>
|
||||
|
||||
export const AgentPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("agent"),
|
||||
name: Schema.String,
|
||||
source: Schema.optional(
|
||||
Schema.Struct({
|
||||
value: Schema.String,
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "AgentPartInput" })
|
||||
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
|
||||
|
||||
export const SubtaskPartInput = Schema.Struct({
|
||||
id: Schema.optional(PartID),
|
||||
type: Schema.Literal("subtask"),
|
||||
prompt: Schema.String,
|
||||
description: Schema.String,
|
||||
agent: Schema.String,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
}),
|
||||
),
|
||||
command: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "SubtaskPartInput" })
|
||||
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
||||
|
||||
export const Assistant = Schema.Struct({
|
||||
...messageBase,
|
||||
role: Schema.Literal("assistant"),
|
||||
time: Schema.Struct({
|
||||
created: NonNegativeInt,
|
||||
completed: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
error: Schema.optional(AssistantErrorSchema),
|
||||
parentID: MessageID,
|
||||
modelID: ModelID,
|
||||
providerID: ProviderID,
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
mode: Schema.String,
|
||||
agent: Schema.String,
|
||||
path: Schema.Struct({
|
||||
cwd: Schema.String,
|
||||
root: Schema.String,
|
||||
}),
|
||||
summary: Schema.optional(Schema.Boolean),
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.optional(Schema.Finite),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
reasoning: Schema.Finite,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Finite,
|
||||
write: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
structured: Schema.optional(Schema.Any),
|
||||
variant: Schema.optional(Schema.String),
|
||||
finish: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "AssistantMessage" })
|
||||
export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assistant>>, "error"> & {
|
||||
error?: AssistantError
|
||||
}
|
||||
|
||||
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" })
|
||||
export type Info = User | Assistant
|
||||
|
||||
const UpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
info: Info,
|
||||
@@ -505,7 +69,7 @@ const RemovedEventSchema = Schema.Struct({
|
||||
const PartUpdatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
part: Part,
|
||||
time: NonNegativeInt,
|
||||
time: Schema.Number,
|
||||
})
|
||||
|
||||
const PartRemovedEventSchema = Schema.Struct({
|
||||
@@ -551,15 +115,6 @@ export const Event = {
|
||||
}),
|
||||
}
|
||||
|
||||
export const WithParts = Schema.Struct({
|
||||
info: Info,
|
||||
parts: Schema.Array(Part),
|
||||
})
|
||||
export type WithParts = {
|
||||
info: Info
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
const Cursor = Schema.Struct({
|
||||
id: MessageID,
|
||||
time: Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)),
|
||||
@@ -579,14 +134,14 @@ export const cursor = {
|
||||
|
||||
const info = (row: typeof MessageTable.$inferSelect) =>
|
||||
({
|
||||
...(row.data as object),
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
}) as Info
|
||||
|
||||
const part = (row: typeof PartTable.$inferSelect) =>
|
||||
({
|
||||
...(row.data as object),
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
@@ -988,7 +543,7 @@ export function parts(message_id: MessageID) {
|
||||
return rows.map(
|
||||
(row) =>
|
||||
({
|
||||
...(row.data as object),
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Config } from "@/config/config"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
@@ -19,7 +20,7 @@ export function usable(input: { cfg: Config.Info; model: Provider.Model; outputT
|
||||
|
||||
export function isOverflow(input: {
|
||||
cfg: Config.Info
|
||||
tokens: MessageV2.Assistant["tokens"]
|
||||
tokens: SessionLegacy.Assistant["tokens"]
|
||||
model: Provider.Model
|
||||
outputTokenMax?: number
|
||||
}) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Image } from "@/image/image"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Cause, Deferred, Effect, Exit, Layer, Context, Scope, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { Agent } from "@/agent/agent"
|
||||
@@ -35,25 +36,25 @@ const log = Log.create({ service: "session.processor" })
|
||||
export type Result = "compact" | "stop" | "continue"
|
||||
|
||||
export interface Handle {
|
||||
readonly message: MessageV2.Assistant
|
||||
readonly message: SessionLegacy.Assistant
|
||||
readonly updateToolCall: (
|
||||
toolCallID: string,
|
||||
update: (part: MessageV2.ToolPart) => MessageV2.ToolPart,
|
||||
) => Effect.Effect<MessageV2.ToolPart | undefined>
|
||||
update: (part: SessionLegacy.ToolPart) => SessionLegacy.ToolPart,
|
||||
) => Effect.Effect<SessionLegacy.ToolPart | undefined>
|
||||
readonly completeToolCall: (
|
||||
toolCallID: string,
|
||||
output: {
|
||||
title: string
|
||||
metadata: Record<string, any>
|
||||
output: string
|
||||
attachments?: MessageV2.FilePart[]
|
||||
attachments?: SessionLegacy.FilePart[]
|
||||
},
|
||||
) => Effect.Effect<void>
|
||||
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
|
||||
}
|
||||
|
||||
type Input = {
|
||||
assistantMessage: MessageV2.Assistant
|
||||
assistantMessage: SessionLegacy.Assistant
|
||||
sessionID: SessionID
|
||||
model: Provider.Model
|
||||
}
|
||||
@@ -63,9 +64,9 @@ export interface Interface {
|
||||
}
|
||||
|
||||
type ToolCall = {
|
||||
partID: MessageV2.ToolPart["id"]
|
||||
messageID: MessageV2.ToolPart["messageID"]
|
||||
sessionID: MessageV2.ToolPart["sessionID"]
|
||||
partID: SessionLegacy.ToolPart["id"]
|
||||
messageID: SessionLegacy.ToolPart["messageID"]
|
||||
sessionID: SessionLegacy.ToolPart["sessionID"]
|
||||
done: Deferred.Deferred<void>
|
||||
inputEnded: boolean
|
||||
}
|
||||
@@ -76,8 +77,8 @@ interface ProcessorContext extends Input {
|
||||
snapshot: string | undefined
|
||||
blocked: boolean
|
||||
needsCompaction: boolean
|
||||
currentText: MessageV2.TextPart | undefined
|
||||
reasoningMap: Record<string, MessageV2.ReasoningPart>
|
||||
currentText: SessionLegacy.TextPart | undefined
|
||||
reasoningMap: Record<string, SessionLegacy.ReasoningPart>
|
||||
}
|
||||
|
||||
type StreamEvent = LLMEvent
|
||||
@@ -151,7 +152,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* (
|
||||
toolCallID: string,
|
||||
update: (part: MessageV2.ToolPart) => MessageV2.ToolPart,
|
||||
update: (part: SessionLegacy.ToolPart) => SessionLegacy.ToolPart,
|
||||
) {
|
||||
const match = yield* readToolCall(toolCallID)
|
||||
if (!match) return undefined
|
||||
@@ -171,7 +172,7 @@ export const layer = Layer.effect(
|
||||
title: string
|
||||
metadata: Record<string, any>
|
||||
output: string
|
||||
attachments?: MessageV2.FilePart[]
|
||||
attachments?: SessionLegacy.FilePart[]
|
||||
},
|
||||
) {
|
||||
const match = yield* readToolCall(toolCallID)
|
||||
@@ -266,7 +267,7 @@ export const layer = Layer.effect(
|
||||
callID: input.id,
|
||||
state: { status: "pending", input: {}, raw: "" },
|
||||
metadata: input.providerExecuted ? { providerExecuted: true } : undefined,
|
||||
} satisfies MessageV2.ToolPart)
|
||||
} satisfies SessionLegacy.ToolPart)
|
||||
ctx.toolcalls[input.id] = {
|
||||
done: yield* Deferred.make<void>(),
|
||||
partID: part.id,
|
||||
@@ -277,11 +278,11 @@ export const layer = Layer.effect(
|
||||
return { call: ctx.toolcalls[input.id], part }
|
||||
})
|
||||
|
||||
const isFilePart = (value: unknown): value is MessageV2.FilePart => Schema.is(MessageV2.FilePart)(value)
|
||||
const isFilePart = (value: unknown): value is SessionLegacy.FilePart => Schema.is(SessionLegacy.FilePart)(value)
|
||||
|
||||
const toolResultOutput = (
|
||||
value: Extract<StreamEvent, { type: "tool-result" }>,
|
||||
): { title: string; metadata: Record<string, any>; output: string; attachments?: MessageV2.FilePart[] } => {
|
||||
): { title: string; metadata: Record<string, any>; output: string; attachments?: SessionLegacy.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,
|
||||
@@ -461,7 +462,7 @@ export const layer = Layer.effect(
|
||||
),
|
||||
Effect.exit,
|
||||
)
|
||||
: Effect.succeed(Exit.succeed<MessageV2.FilePart>(attachment)),
|
||||
: Effect.succeed(Exit.succeed<SessionLegacy.FilePart>(attachment)),
|
||||
)
|
||||
const omitted = normalized.filter(Exit.isFailure).length
|
||||
const attachments = normalized.filter(Exit.isSuccess).map((item) => item.value)
|
||||
@@ -484,7 +485,7 @@ export const layer = Layer.effect(
|
||||
type: "text",
|
||||
text: output.output,
|
||||
},
|
||||
...(output.attachments?.map((item: MessageV2.FilePart) => ({
|
||||
...(output.attachments?.map((item: SessionLegacy.FilePart) => ({
|
||||
type: "file" as const,
|
||||
uri: item.url,
|
||||
mime: item.mime,
|
||||
@@ -751,7 +752,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 (MessageV2.ContextOverflowError.isInstance(error)) {
|
||||
if (SessionLegacy.ContextOverflowError.isInstance(error)) {
|
||||
ctx.needsCompaction = true
|
||||
yield* bus.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
|
||||
return
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and } from "drizzle-orm"
|
||||
import { sql } from "drizzle-orm"
|
||||
@@ -21,9 +22,9 @@ function foreign(err: unknown) {
|
||||
|
||||
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> | null } : T
|
||||
|
||||
type Usage = Pick<MessageV2.StepFinishPart, "cost" | "tokens">
|
||||
type Usage = Pick<SessionLegacy.StepFinishPart, "cost" | "tokens">
|
||||
|
||||
function usage(part: MessageV2.Part | unknown): Usage | undefined {
|
||||
function usage(part: SessionLegacy.Part | unknown): Usage | undefined {
|
||||
if (typeof part !== "object" || part === null) return undefined
|
||||
const value = part as Record<string, unknown>
|
||||
if (value.type !== "step-finish") return undefined
|
||||
@@ -134,9 +135,9 @@ export default [
|
||||
id,
|
||||
session_id: sessionID,
|
||||
time_created,
|
||||
data: rest as never,
|
||||
data: rest,
|
||||
})
|
||||
.onConflictDoUpdate({ target: MessageTable.id, set: { data: rest as never } })
|
||||
.onConflictDoUpdate({ target: MessageTable.id, set: { data: rest } })
|
||||
.run()
|
||||
} catch (err) {
|
||||
if (!foreign(err)) throw err
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "path"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import os from "os"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
@@ -65,8 +66,8 @@ import { LLMEvent } from "@opencode-ai/llm"
|
||||
// @ts-ignore
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
||||
const decodeMessageInfo = Schema.decodeUnknownExit(MessageV2.Info)
|
||||
const decodeMessagePart = Schema.decodeUnknownExit(MessageV2.Part)
|
||||
const decodeMessageInfo = Schema.decodeUnknownExit(SessionLegacy.Info)
|
||||
const decodeMessagePart = Schema.decodeUnknownExit(SessionLegacy.Part)
|
||||
|
||||
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
|
||||
|
||||
@@ -83,10 +84,10 @@ const elog = EffectLogger.create({ service: "session.prompt" })
|
||||
|
||||
export interface Interface {
|
||||
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
|
||||
readonly loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts>
|
||||
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts, Session.BusyError>
|
||||
readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts, Image.Error>
|
||||
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 resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
|
||||
}
|
||||
|
||||
@@ -235,14 +236,14 @@ export const layer = Layer.effect(
|
||||
|
||||
const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
|
||||
session: Session.Info
|
||||
history: MessageV2.WithParts[]
|
||||
history: SessionLegacy.WithParts[]
|
||||
providerID: ProviderID
|
||||
modelID: ModelID
|
||||
}) {
|
||||
if (input.session.parentID) return
|
||||
if (!Session.isDefaultTitle(input.session.title)) return
|
||||
|
||||
const real = (m: MessageV2.WithParts) =>
|
||||
const real = (m: SessionLegacy.WithParts) =>
|
||||
m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
|
||||
const idx = input.history.findIndex(real)
|
||||
if (idx === -1) return
|
||||
@@ -253,7 +254,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 MessageV2.SubtaskPart => p.type === "subtask")
|
||||
const subtasks = firstUser.parts.filter((p): p is SessionLegacy.SubtaskPart => p.type === "subtask")
|
||||
const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
|
||||
|
||||
const ag = yield* agents.get("title")
|
||||
@@ -296,19 +297,19 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
|
||||
task: MessageV2.SubtaskPart
|
||||
task: SessionLegacy.SubtaskPart
|
||||
model: Provider.Model
|
||||
lastUser: MessageV2.User
|
||||
lastUser: SessionLegacy.User
|
||||
sessionID: SessionID
|
||||
session: Session.Info
|
||||
msgs: MessageV2.WithParts[]
|
||||
msgs: SessionLegacy.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: MessageV2.Assistant = yield* sessions.updateMessage({
|
||||
const assistantMessage: SessionLegacy.Assistant = yield* sessions.updateMessage({
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: lastUser.id,
|
||||
@@ -323,7 +324,7 @@ export const layer = Layer.effect(
|
||||
providerID: taskModel.providerID,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
let part: MessageV2.ToolPart = yield* sessions.updatePart({
|
||||
let part: SessionLegacy.ToolPart = yield* sessions.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: assistantMessage.id,
|
||||
sessionID: assistantMessage.sessionID,
|
||||
@@ -379,7 +380,7 @@ export const layer = Layer.effect(
|
||||
...part,
|
||||
type: "tool",
|
||||
state: { ...part.state, ...val },
|
||||
} satisfies MessageV2.ToolPart)
|
||||
} satisfies SessionLegacy.ToolPart)
|
||||
}),
|
||||
ask: (req: any) =>
|
||||
permission
|
||||
@@ -413,7 +414,7 @@ export const layer = Layer.effect(
|
||||
metadata: part.state.metadata,
|
||||
input: part.state.input,
|
||||
},
|
||||
} satisfies MessageV2.ToolPart)
|
||||
} satisfies SessionLegacy.ToolPart)
|
||||
}
|
||||
}),
|
||||
),
|
||||
@@ -448,7 +449,7 @@ export const layer = Layer.effect(
|
||||
attachments,
|
||||
time: { ...part.state.time, end: Date.now() },
|
||||
},
|
||||
} satisfies MessageV2.ToolPart)
|
||||
} satisfies SessionLegacy.ToolPart)
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
@@ -464,12 +465,12 @@ export const layer = Layer.effect(
|
||||
metadata: part.state.status === "pending" ? undefined : part.state.metadata,
|
||||
input: part.state.input,
|
||||
},
|
||||
} satisfies MessageV2.ToolPart)
|
||||
} satisfies SessionLegacy.ToolPart)
|
||||
}
|
||||
|
||||
if (!task.command) return
|
||||
|
||||
const summaryUserMsg: MessageV2.User = {
|
||||
const summaryUserMsg: SessionLegacy.User = {
|
||||
id: MessageID.ascending(),
|
||||
sessionID,
|
||||
role: "user",
|
||||
@@ -485,7 +486,7 @@ export const layer = Layer.effect(
|
||||
type: "text",
|
||||
text: "Summarize the task tool output above and continue with your task.",
|
||||
synthetic: true,
|
||||
} satisfies MessageV2.TextPart)
|
||||
} satisfies SessionLegacy.TextPart)
|
||||
})
|
||||
|
||||
const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput, ready?: Latch.Latch) {
|
||||
@@ -507,7 +508,7 @@ export const layer = Layer.effect(
|
||||
throw error
|
||||
}
|
||||
const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID))
|
||||
const userMsg: MessageV2.User = {
|
||||
const userMsg: SessionLegacy.User = {
|
||||
id: input.messageID ?? MessageID.ascending(),
|
||||
sessionID: input.sessionID,
|
||||
time: { created: Date.now() },
|
||||
@@ -516,7 +517,7 @@ export const layer = Layer.effect(
|
||||
model: { providerID: model.providerID, modelID: model.modelID },
|
||||
}
|
||||
yield* sessions.updateMessage(userMsg)
|
||||
const userPart: MessageV2.Part = {
|
||||
const userPart: SessionLegacy.Part = {
|
||||
type: "text",
|
||||
id: PartID.ascending(),
|
||||
messageID: userMsg.id,
|
||||
@@ -526,7 +527,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
yield* sessions.updatePart(userPart)
|
||||
|
||||
const msg: MessageV2.Assistant = {
|
||||
const msg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
sessionID: input.sessionID,
|
||||
parentID: userMsg.id,
|
||||
@@ -542,7 +543,7 @@ export const layer = Layer.effect(
|
||||
}
|
||||
yield* sessions.updateMessage(msg)
|
||||
const started = Date.now()
|
||||
const part: MessageV2.ToolPart = {
|
||||
const part: SessionLegacy.ToolPart = {
|
||||
type: "tool",
|
||||
id: PartID.ascending(),
|
||||
messageID: msg.id,
|
||||
@@ -713,7 +714,7 @@ export const layer = Layer.effect(
|
||||
: undefined
|
||||
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
|
||||
|
||||
const info: MessageV2.User = {
|
||||
const info: SessionLegacy.User = {
|
||||
id: input.messageID ?? MessageID.ascending(),
|
||||
role: "user",
|
||||
sessionID: input.sessionID,
|
||||
@@ -754,8 +755,8 @@ export const layer = Layer.effect(
|
||||
|
||||
yield* Effect.addFinalizer(() => instruction.clear(info.id))
|
||||
|
||||
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
|
||||
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
|
||||
type Draft<T> = T extends SessionLegacy.Part ? Omit<T, "id"> & { id?: string } : never
|
||||
const assign = (part: Draft<SessionLegacy.Part>): SessionLegacy.Part => ({
|
||||
...part,
|
||||
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
||||
})
|
||||
@@ -784,14 +785,14 @@ export const layer = Layer.effect(
|
||||
})
|
||||
})
|
||||
|
||||
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<MessageV2.Part>[]> = Effect.fn(
|
||||
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionLegacy.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<MessageV2.Part>[] = [
|
||||
const pieces: Draft<SessionLegacy.Part>[] = [
|
||||
{
|
||||
messageID: info.id,
|
||||
sessionID: input.sessionID,
|
||||
@@ -911,7 +912,7 @@ export const layer = Layer.effect(
|
||||
if (end) limit = end - (offset - 1)
|
||||
}
|
||||
const args = { filePath: filepath, offset, limit }
|
||||
const pieces: Draft<MessageV2.Part>[] = [
|
||||
const pieces: Draft<SessionLegacy.Part>[] = [
|
||||
...(referenceContext
|
||||
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
||||
: []),
|
||||
@@ -1207,7 +1208,7 @@ export const layer = Layer.effect(
|
||||
return { info, parts }
|
||||
}, Effect.scoped)
|
||||
|
||||
const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts, Image.Error> = Effect.fn(
|
||||
const prompt: (input: PromptInput) => Effect.Effect<SessionLegacy.WithParts, Image.Error> = Effect.fn(
|
||||
"SessionPrompt.prompt",
|
||||
)(function* (input: PromptInput) {
|
||||
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
||||
@@ -1236,7 +1237,7 @@ export const layer = Layer.effect(
|
||||
throw new Error("Impossible")
|
||||
})
|
||||
|
||||
const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")(
|
||||
const runLoop: (sessionID: SessionID) => Effect.Effect<SessionLegacy.WithParts> = Effect.fn("SessionPrompt.run")(
|
||||
function* (sessionID: SessionID) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const slog = elog.with({ sessionID })
|
||||
@@ -1328,7 +1329,7 @@ export const layer = Layer.effect(
|
||||
Effect.provideService(Session.Service, sessions),
|
||||
)
|
||||
|
||||
const msg: MessageV2.Assistant = {
|
||||
const msg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
parentID: lastUser.id,
|
||||
role: "assistant",
|
||||
@@ -1448,7 +1449,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 MessageV2.StructuredOutputError({
|
||||
handle.message.error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Model did not produce structured output",
|
||||
retries: 0,
|
||||
}).toObject()
|
||||
@@ -1481,13 +1482,13 @@ export const layer = Layer.effect(
|
||||
},
|
||||
)
|
||||
|
||||
const loop: (input: LoopInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.loop")(function* (
|
||||
const loop: (input: LoopInput) => Effect.Effect<SessionLegacy.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<MessageV2.WithParts, Session.BusyError> = Effect.fn(
|
||||
const shell: (input: ShellInput) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError> = Effect.fn(
|
||||
"SessionPrompt.shell",
|
||||
)(function* (input: ShellInput) {
|
||||
const ready = yield* Latch.make()
|
||||
@@ -1672,15 +1673,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(MessageV2.Format),
|
||||
format: Schema.optional(SessionLegacy.Format),
|
||||
system: Schema.optional(Schema.String),
|
||||
variant: Schema.optional(Schema.String),
|
||||
parts: Schema.Array(
|
||||
Schema.Union([
|
||||
MessageV2.TextPartInput,
|
||||
MessageV2.FilePartInput,
|
||||
MessageV2.AgentPartInput,
|
||||
MessageV2.SubtaskPartInput,
|
||||
SessionLegacy.TextPartInput,
|
||||
SessionLegacy.FilePartInput,
|
||||
SessionLegacy.AgentPartInput,
|
||||
SessionLegacy.SubtaskPartInput,
|
||||
]).annotate({ discriminator: "type" }),
|
||||
),
|
||||
})
|
||||
@@ -1719,7 +1720,7 @@ export const CommandInput = Schema.Struct({
|
||||
mime: Schema.String,
|
||||
filename: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
source: Schema.optional(MessageV2.FilePartSource),
|
||||
source: Schema.optional(SessionLegacy.FilePartSource),
|
||||
}),
|
||||
]).annotate({ discriminator: "type" }),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Option, Schema } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { MessageV2 } from "../message-v2"
|
||||
import { Reference } from "@/reference/reference"
|
||||
|
||||
@@ -33,7 +34,7 @@ export function referenceTextPart(input: {
|
||||
target?: string
|
||||
targetPath?: string
|
||||
problem?: string
|
||||
}): MessageV2.TextPartInput {
|
||||
}): SessionLegacy.TextPartInput {
|
||||
const metadata: ReferencePromptMetadata = {
|
||||
name: input.reference.name,
|
||||
kind: input.reference.kind,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "path"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect } from "effect"
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
@@ -12,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: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
agent: Agent.Info
|
||||
session: Session.Info
|
||||
}) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { iife } from "@/util/iife"
|
||||
@@ -31,7 +32,7 @@ function cap(ms: number) {
|
||||
return Math.min(ms, RETRY_MAX_DELAY)
|
||||
}
|
||||
|
||||
export function delay(attempt: number, error?: MessageV2.APIError) {
|
||||
export function delay(attempt: number, error?: SessionLegacy.APIError) {
|
||||
if (error) {
|
||||
const headers = error.data.responseHeaders
|
||||
if (headers) {
|
||||
@@ -66,8 +67,8 @@ export function delay(attempt: number, error?: MessageV2.APIError) {
|
||||
|
||||
export function retryable(error: Err, provider: string) {
|
||||
// context overflow errors should not be retried
|
||||
if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
|
||||
if (MessageV2.APIError.isInstance(error)) {
|
||||
if (SessionLegacy.ContextOverflowError.isInstance(error)) return undefined
|
||||
if (SessionLegacy.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.
|
||||
@@ -183,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, MessageV2.APIError.isInstance(error) ? error : undefined)
|
||||
const wait = delay(meta.attempt, SessionLegacy.APIError.isInstance(error) ? error : undefined)
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
yield* opts.set({
|
||||
attempt: meta.attempt,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Bus } from "../bus"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { Storage } from "@/storage/storage"
|
||||
@@ -41,7 +42,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: MessageV2.User | undefined
|
||||
let lastUser: SessionLegacy.User | undefined
|
||||
const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie)
|
||||
|
||||
let rev: Session.Info["revert"]
|
||||
@@ -105,8 +106,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 MessageV2.WithParts[]
|
||||
let target: MessageV2.WithParts | undefined
|
||||
const remove = [] as SessionLegacy.WithParts[]
|
||||
let target: SessionLegacy.WithParts | undefined
|
||||
for (const msg of msgs) {
|
||||
if (msg.info.id < messageID) continue
|
||||
if (msg.info.id > messageID) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Runner } from "@/effect/runner"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Effect, Latch, Layer, Scope, Context } from "effect"
|
||||
@@ -12,15 +13,15 @@ export interface Interface {
|
||||
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly ensureRunning: (
|
||||
sessionID: SessionID,
|
||||
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
||||
work: Effect.Effect<MessageV2.WithParts>,
|
||||
) => Effect.Effect<MessageV2.WithParts>
|
||||
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
|
||||
work: Effect.Effect<SessionLegacy.WithParts>,
|
||||
) => Effect.Effect<SessionLegacy.WithParts>
|
||||
readonly startShell: (
|
||||
sessionID: SessionID,
|
||||
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
||||
work: Effect.Effect<MessageV2.WithParts>,
|
||||
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
|
||||
work: Effect.Effect<SessionLegacy.WithParts>,
|
||||
ready?: Latch.Latch,
|
||||
) => Effect.Effect<MessageV2.WithParts, Session.BusyError>
|
||||
) => Effect.Effect<SessionLegacy.WithParts, Session.BusyError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunState") {}
|
||||
@@ -34,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<MessageV2.WithParts>>()
|
||||
const runners = new Map<SessionID, Runner.Runner<SessionLegacy.WithParts>>()
|
||||
yield* Effect.addFinalizer(
|
||||
Effect.fnUntraced(function* () {
|
||||
yield* Effect.forEach(runners.values(), (runner) => runner.cancel, {
|
||||
@@ -50,12 +51,12 @@ export const layer = Layer.effect(
|
||||
|
||||
const runner = Effect.fn("SessionRunState.runner")(function* (
|
||||
sessionID: SessionID,
|
||||
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
||||
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
|
||||
) {
|
||||
const data = yield* InstanceState.get(state)
|
||||
const existing = data.runners.get(sessionID)
|
||||
if (existing) return existing
|
||||
const next = Runner.make<MessageV2.WithParts>(data.scope, {
|
||||
const next = Runner.make<SessionLegacy.WithParts>(data.scope, {
|
||||
onIdle: Effect.gen(function* () {
|
||||
data.runners.delete(sessionID)
|
||||
yield* status.set(sessionID, { type: "idle" })
|
||||
@@ -86,16 +87,16 @@ export const layer = Layer.effect(
|
||||
|
||||
const ensureRunning = Effect.fn("SessionRunState.ensureRunning")(function* (
|
||||
sessionID: SessionID,
|
||||
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
||||
work: Effect.Effect<MessageV2.WithParts>,
|
||||
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
|
||||
work: Effect.Effect<SessionLegacy.WithParts>,
|
||||
) {
|
||||
return yield* (yield* runner(sessionID, onInterrupt)).ensureRunning(work)
|
||||
})
|
||||
|
||||
const startShell = Effect.fn("SessionRunState.startShell")(function* (
|
||||
sessionID: SessionID,
|
||||
onInterrupt: Effect.Effect<MessageV2.WithParts>,
|
||||
work: Effect.Effect<MessageV2.WithParts>,
|
||||
onInterrupt: Effect.Effect<SessionLegacy.WithParts>,
|
||||
work: Effect.Effect<SessionLegacy.WithParts>,
|
||||
ready?: Latch.Latch,
|
||||
) {
|
||||
return yield* (yield* runner(sessionID, onInterrupt))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import path from "path"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
@@ -361,9 +362,9 @@ export const Event = {
|
||||
"session.error",
|
||||
Schema.Struct({
|
||||
sessionID: Schema.optional(SessionID),
|
||||
// Reuses MessageV2.Assistant.fields.error (already Schema.optional) so
|
||||
// Reuses SessionLegacy.Assistant.fields.error (already Schema.optional) so
|
||||
// the derived zod keeps the same discriminated-union shape on the bus.
|
||||
error: MessageV2.Assistant.fields.error,
|
||||
error: SessionLegacy.Assistant.fields.error,
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -472,18 +473,18 @@ export interface Interface {
|
||||
readonly clearRevert: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect<void>
|
||||
readonly diff: (sessionID: SessionID) => Effect.Effect<Snapshot.FileDiff[]>
|
||||
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<MessageV2.WithParts[], NotFound>
|
||||
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<SessionLegacy.WithParts[], NotFound>
|
||||
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
|
||||
readonly remove: (sessionID: SessionID) => Effect.Effect<void, NotFound>
|
||||
readonly updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.Effect<T>
|
||||
readonly updateMessage: <T extends SessionLegacy.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<MessageV2.Part | undefined>
|
||||
readonly updatePart: <T extends MessageV2.Part>(part: T) => Effect.Effect<T>
|
||||
}) => Effect.Effect<SessionLegacy.Part | undefined>
|
||||
readonly updatePart: <T extends SessionLegacy.Part>(part: T) => Effect.Effect<T>
|
||||
readonly updatePartDelta: (input: {
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
@@ -494,8 +495,8 @@ export interface Interface {
|
||||
/** Finds the first message matching the predicate, searching newest-first. */
|
||||
readonly findMessage: (
|
||||
sessionID: SessionID,
|
||||
predicate: (msg: MessageV2.WithParts) => boolean,
|
||||
) => Effect.Effect<Option.Option<MessageV2.WithParts>, NotFound>
|
||||
predicate: (msg: SessionLegacy.WithParts) => boolean,
|
||||
) => Effect.Effect<Option.Option<SessionLegacy.WithParts>, NotFound>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
|
||||
@@ -615,13 +616,13 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
})
|
||||
|
||||
const updateMessage = <T extends MessageV2.Info>(msg: T): Effect.Effect<T> =>
|
||||
const updateMessage = <T extends SessionLegacy.Info>(msg: T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
yield* sync.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg })
|
||||
return msg
|
||||
}).pipe(Effect.withSpan("Session.updateMessage"))
|
||||
|
||||
const updatePart = <T extends MessageV2.Part>(part: T): Effect.Effect<T> =>
|
||||
const updatePart = <T extends SessionLegacy.Part>(part: T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
yield* sync.run(MessageV2.Event.PartUpdated, {
|
||||
sessionID: part.sessionID,
|
||||
@@ -647,11 +648,11 @@ export const layer: Layer.Layer<
|
||||
)
|
||||
if (!row) return
|
||||
return {
|
||||
...(row.data as object),
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
} as MessageV2.Part
|
||||
} as SessionLegacy.Part
|
||||
})
|
||||
|
||||
const create = Effect.fn("Session.create")(function* (input?: {
|
||||
@@ -703,7 +704,7 @@ export const layer: Layer.Layer<
|
||||
})
|
||||
|
||||
for (const part of msg.parts) {
|
||||
const p: MessageV2.Part = {
|
||||
const p: SessionLegacy.Part = {
|
||||
...part,
|
||||
id: PartID.ascending(),
|
||||
messageID: cloned.id,
|
||||
@@ -770,7 +771,7 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
|
||||
const size = 50
|
||||
const result = [] as MessageV2.WithParts[]
|
||||
const result = [] as SessionLegacy.WithParts[]
|
||||
let before: string | undefined
|
||||
while (true) {
|
||||
const page = yield* MessageV2.page({ sessionID: input.sessionID, limit: size, before })
|
||||
@@ -833,7 +834,7 @@ export const layer: Layer.Layer<
|
||||
if (!page.more || !page.cursor) break
|
||||
before = page.cursor
|
||||
}
|
||||
return Option.none<MessageV2.WithParts>()
|
||||
return Option.none<SessionLegacy.WithParts>()
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Bus } from "@/bus"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { Storage } from "@/storage/storage"
|
||||
@@ -65,7 +66,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: MessageV2.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
|
||||
readonly computeDiff: (input: { messages: SessionLegacy.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionSummary") {}
|
||||
@@ -78,7 +79,7 @@ export const layer = Layer.effect(
|
||||
const storage = yield* Storage.Service
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: MessageV2.WithParts[] }) {
|
||||
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: SessionLegacy.WithParts[] }) {
|
||||
let from: string | undefined
|
||||
let to: string | undefined
|
||||
for (const item of input.messages) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { MCP } from "@/mcp"
|
||||
@@ -27,7 +28,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||
session: Session.Info
|
||||
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
|
||||
bypassAgentCheck: boolean
|
||||
messages: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
promptOps: TaskPromptOps
|
||||
}) {
|
||||
using _ = log.time("resolveTools")
|
||||
@@ -151,7 +152,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||
)
|
||||
|
||||
const textParts: string[] = []
|
||||
const attachments: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[] = []
|
||||
const attachments: Omit<SessionLegacy.FilePart, "id" | "sessionID" | "messageID">[] = []
|
||||
for (const contentItem of result.content) {
|
||||
if (contentItem.type === "text") textParts.push(contentItem.text)
|
||||
else if (contentItem.type === "image") {
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { init } from "#db"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Schema } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
|
||||
export const NotFoundError = NamedError.create("NotFoundError", {
|
||||
@@ -30,7 +30,7 @@ export const Client = Object.assign(
|
||||
const dbPath = getPath()
|
||||
log.info("opening database", { path: dbPath })
|
||||
|
||||
Database.init({ path: dbPath })
|
||||
Database.init()
|
||||
|
||||
const db = init(dbPath)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "path"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Tool from "./tool"
|
||||
import { Question } from "../question"
|
||||
@@ -49,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: MessageV2.User = {
|
||||
const msg: SessionLegacy.User = {
|
||||
id: MessageID.ascending(),
|
||||
sessionID: ctx.sessionID,
|
||||
role: "user",
|
||||
@@ -65,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 MessageV2.TextPart)
|
||||
} satisfies SessionLegacy.TextPart)
|
||||
|
||||
return {
|
||||
title: "Switching to build agent",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Tool from "./tool"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import DESCRIPTION from "./task.txt"
|
||||
import { ToolJsonSchema } from "./json-schema"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
@@ -19,8 +20,8 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
export interface TaskPromptOps {
|
||||
cancel(sessionID: SessionID): Effect.Effect<void>
|
||||
resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
|
||||
prompt(input: SessionPrompt.PromptInput): Effect.Effect<MessageV2.WithParts>
|
||||
loop(input: SessionPrompt.LoopInput): Effect.Effect<MessageV2.WithParts>
|
||||
prompt(input: SessionPrompt.PromptInput): Effect.Effect<SessionLegacy.WithParts>
|
||||
loop(input: SessionPrompt.LoopInput): Effect.Effect<SessionLegacy.WithParts>
|
||||
}
|
||||
|
||||
const id = "task"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Tool from "./tool"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import DESCRIPTION from "./task_status.txt"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { Session } from "@/session/session"
|
||||
@@ -30,14 +31,14 @@ function format(input: { taskID: SessionID; state: State; text: string }) {
|
||||
return [`task_id: ${input.taskID}`, `state: ${input.state}`, "", `<${tag}>`, input.text, `</${tag}>`].join("\n")
|
||||
}
|
||||
|
||||
function errorText(error: NonNullable<MessageV2.Assistant["error"]>) {
|
||||
function errorText(error: NonNullable<SessionLegacy.Assistant["error"]>) {
|
||||
const data = Reflect.get(error, "data")
|
||||
const message = data && typeof data === "object" ? Reflect.get(data, "message") : undefined
|
||||
if (typeof message === "string" && message) return message
|
||||
return error.name
|
||||
}
|
||||
|
||||
function inspectMessage(message: MessageV2.WithParts): InspectResult | undefined {
|
||||
function inspectMessage(message: SessionLegacy.WithParts): InspectResult | undefined {
|
||||
if (message.info.role !== "assistant") return
|
||||
const text = message.parts.findLast((part) => part.type === "text")?.text ?? ""
|
||||
if (message.info.error) return { state: "error", text: text || errorText(message.info.error) }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
import type { MessageV2 } from "../session/message-v2"
|
||||
import type { Permission } from "../permission"
|
||||
@@ -38,7 +39,7 @@ export type Context<M extends Metadata = Metadata> = {
|
||||
abort: AbortSignal
|
||||
callID?: string
|
||||
extra?: { [key: string]: unknown }
|
||||
messages: MessageV2.WithParts[]
|
||||
messages: SessionLegacy.WithParts[]
|
||||
metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
|
||||
ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
|
||||
}
|
||||
@@ -47,7 +48,7 @@ export interface ExecuteResult<M extends Metadata = Metadata> {
|
||||
title: string
|
||||
metadata: M
|
||||
output: string
|
||||
attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[]
|
||||
attachments?: Omit<SessionLegacy.FilePart, "id" | "sessionID" | "messageID">[]
|
||||
}
|
||||
|
||||
export interface Def<
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
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): MessageV2.Part {
|
||||
function createTextPart(text: string): SessionLegacy.Part {
|
||||
return {
|
||||
id: PartID.ascending(),
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
@@ -14,7 +15,7 @@ function createTextPart(text: string): MessageV2.Part {
|
||||
}
|
||||
}
|
||||
|
||||
function createReasoningPart(text: string): MessageV2.Part {
|
||||
function createReasoningPart(text: string): SessionLegacy.Part {
|
||||
return {
|
||||
id: PartID.ascending(),
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
@@ -25,7 +26,7 @@ function createReasoningPart(text: string): MessageV2.Part {
|
||||
}
|
||||
}
|
||||
|
||||
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
|
||||
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): SessionLegacy.Part {
|
||||
if (status === "completed") {
|
||||
return {
|
||||
id: PartID.ascending(),
|
||||
@@ -59,7 +60,7 @@ function createToolPart(tool: string, title: string, status: "completed" | "runn
|
||||
}
|
||||
}
|
||||
|
||||
function createStepStartPart(): MessageV2.Part {
|
||||
function createStepStartPart(): SessionLegacy.Part {
|
||||
return {
|
||||
id: PartID.ascending(),
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
@@ -68,7 +69,7 @@ function createStepStartPart(): MessageV2.Part {
|
||||
}
|
||||
}
|
||||
|
||||
function createStepFinishPart(): MessageV2.Part {
|
||||
function createStepFinishPart(): SessionLegacy.Part {
|
||||
return {
|
||||
id: PartID.ascending(),
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Cause, Duration, Effect } from "effect"
|
||||
import { TestLLMServer } from "../../lib/llm-server"
|
||||
import type { Config } from "../../../src/config/config"
|
||||
@@ -140,7 +141,7 @@ function withContext<A, E>(
|
||||
}),
|
||||
message: (sessionID, input) =>
|
||||
Effect.gen(function* () {
|
||||
const info: MessageV2.User = {
|
||||
const info: SessionLegacy.User = {
|
||||
id: MessageID.ascending(),
|
||||
sessionID,
|
||||
role: "user",
|
||||
@@ -151,7 +152,7 @@ function withContext<A, E>(
|
||||
modelID: ModelID.make("test"),
|
||||
},
|
||||
}
|
||||
const part: MessageV2.TextPart = {
|
||||
const part: SessionLegacy.TextPart = {
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
messageID: info.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Duration, Effect } from "effect"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { Config } from "../../../src/config/config"
|
||||
import type { Project } from "../../../src/project/project"
|
||||
import type { Worktree } from "../../../src/worktree"
|
||||
@@ -57,7 +58,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<MessageV2.WithParts[]>
|
||||
messages: (sessionID: SessionID) => Effect.Effect<SessionLegacy.WithParts[]>
|
||||
todos: (sessionID: SessionID, todos: TodoInfo[]) => Effect.Effect<void>
|
||||
worktree: (input?: { name?: string }) => Effect.Effect<Worktree.Info>
|
||||
worktreeRemove: (directory: string) => Effect.Effect<void>
|
||||
@@ -118,4 +119,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: MessageV2.User; part: MessageV2.TextPart }
|
||||
export type MessageSeed = { info: SessionLegacy.User; part: SessionLegacy.TextPart }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { ConfigProvider, Deferred, Effect, Layer } from "effect"
|
||||
import type * as Scope from "effect/Scope"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
@@ -312,7 +313,7 @@ function seedMessage(directory: string, sessionID: string) {
|
||||
agent: "test",
|
||||
model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
||||
tools: {},
|
||||
} satisfies MessageV2.User)
|
||||
} satisfies SessionLegacy.User)
|
||||
const part = yield* svc.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID: id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Cause, Effect, Exit, Layer } from "effect"
|
||||
@@ -335,7 +336,7 @@ describe("session HttpApi", () => {
|
||||
const messages = yield* request(`${pathFor(SessionPaths.messages, { sessionID: parent.id })}?limit=1`, {
|
||||
headers,
|
||||
})
|
||||
const messagePage = yield* json<MessageV2.WithParts[]>(messages)
|
||||
const messagePage = yield* json<SessionLegacy.WithParts[]>(messages)
|
||||
const nextCursor = messages.headers.get("x-next-cursor")
|
||||
expect(nextCursor).toBeTruthy()
|
||||
expect(messagePage[0]?.parts[0]).toMatchObject({ type: "text" })
|
||||
@@ -352,7 +353,7 @@ describe("session HttpApi", () => {
|
||||
).toBe(400)
|
||||
|
||||
expect(
|
||||
yield* requestJson<MessageV2.WithParts>(
|
||||
yield* requestJson<SessionLegacy.WithParts>(
|
||||
pathFor(SessionPaths.message, { sessionID: parent.id, messageID: message.info.id }),
|
||||
{ headers },
|
||||
),
|
||||
@@ -737,7 +738,7 @@ describe("session HttpApi", () => {
|
||||
const first = yield* createTextMessage(session.id, "first")
|
||||
const second = yield* createTextMessage(session.id, "second")
|
||||
|
||||
const updated = yield* requestJson<MessageV2.Part>(
|
||||
const updated = yield* requestJson<SessionLegacy.Part>(
|
||||
pathFor(SessionPaths.updatePart, {
|
||||
sessionID: session.id,
|
||||
messageID: first.info.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect } from "effect"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
@@ -62,14 +63,14 @@ const fill = Effect.fn("SessionMessagesTest.fill")(function* (
|
||||
agent: "test",
|
||||
model,
|
||||
tools: {},
|
||||
} satisfies MessageV2.User)
|
||||
} satisfies SessionLegacy.User)
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
messageID: id,
|
||||
type: "text",
|
||||
text: `m${i}`,
|
||||
} satisfies MessageV2.TextPart)
|
||||
} satisfies SessionLegacy.TextPart)
|
||||
return id
|
||||
}),
|
||||
)
|
||||
@@ -93,7 +94,7 @@ describe("session messages endpoint", () => {
|
||||
|
||||
const a = yield* request(`/session/${session.id}/message?limit=2`)
|
||||
expect(a.status).toBe(200)
|
||||
const aBody = yield* json<MessageV2.WithParts[]>(a)
|
||||
const aBody = yield* json<SessionLegacy.WithParts[]>(a)
|
||||
expect(aBody.map((item) => item.info.id)).toEqual(ids.slice(-2))
|
||||
const cursor = a.headers.get("x-next-cursor")
|
||||
expect(cursor).toBeTruthy()
|
||||
@@ -101,7 +102,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<MessageV2.WithParts[]>(b)
|
||||
const bBody = yield* json<SessionLegacy.WithParts[]>(b)
|
||||
expect(bBody.map((item) => item.info.id)).toEqual(ids.slice(-4, -2))
|
||||
}),
|
||||
),
|
||||
@@ -117,7 +118,7 @@ describe("session messages endpoint", () => {
|
||||
|
||||
const res = yield* request(`/session/${session.id}/message`)
|
||||
expect(res.status).toBe(200)
|
||||
const body = yield* json<MessageV2.WithParts[]>(res)
|
||||
const body = yield* json<SessionLegacy.WithParts[]>(res)
|
||||
expect(body.map((item) => item.info.id)).toEqual(ids)
|
||||
}),
|
||||
),
|
||||
@@ -149,7 +150,7 @@ describe("session messages endpoint", () => {
|
||||
|
||||
const res = yield* request(`/session/${session.id}/message?limit=510`)
|
||||
expect(res.status).toBe(200)
|
||||
const body = yield* json<MessageV2.WithParts[]>(res)
|
||||
const body = yield* json<SessionLegacy.WithParts[]>(res)
|
||||
expect(body).toHaveLength(510)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { APICallError } from "ai"
|
||||
import { Cause, Deferred, Effect, Exit, Fiber, Layer, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
@@ -296,7 +297,7 @@ function readCompactionPart(sessionID: SessionID) {
|
||||
.messages({ sessionID })
|
||||
.pipe(
|
||||
Effect.map((messages) =>
|
||||
messages.at(-2)?.parts.find((item): item is MessageV2.CompactionPart => item.type === "compaction"),
|
||||
messages.at(-2)?.parts.find((item): item is SessionLegacy.CompactionPart => item.type === "compaction"),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -623,7 +624,7 @@ describe("session.compaction.prune", () => {
|
||||
type: "text",
|
||||
text: "first",
|
||||
})
|
||||
const b: MessageV2.Assistant = {
|
||||
const b: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID: info.id,
|
||||
@@ -719,7 +720,7 @@ describe("session.compaction.prune", () => {
|
||||
type: "text",
|
||||
text: "first",
|
||||
})
|
||||
const b: MessageV2.Assistant = {
|
||||
const b: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID: info.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import path from "path"
|
||||
import { Effect, FileSystem, Layer } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
@@ -61,7 +62,7 @@ const tmpWithFiles = (files: Record<string, string>) =>
|
||||
return dir
|
||||
})
|
||||
|
||||
function loaded(filepath: string): MessageV2.WithParts[] {
|
||||
function loaded(filepath: string): SessionLegacy.WithParts[] {
|
||||
const sessionID = SessionID.make("session-loaded-1")
|
||||
const messageID = MessageID.make("msg_message-loaded-1")
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { ModelsDev } from "@opencode-ai/core/models-dev"
|
||||
import { HttpRecorder, Redactor } from "@opencode-ai/http-recorder"
|
||||
@@ -392,7 +393,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
|
||||
time: { created: 0 },
|
||||
agent: agent.name,
|
||||
model: { providerID: scenario.providerID, modelID },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import path from "path"
|
||||
import { tool, type ModelMessage } from "ai"
|
||||
import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect"
|
||||
@@ -732,7 +733,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(vivgridFixture.providerID), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -803,7 +804,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
const fiber = yield* drain({
|
||||
user,
|
||||
@@ -873,7 +874,7 @@ describe("session.llm.stream", () => {
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(alibabaQwenFixture.providerID), modelID: resolved.id },
|
||||
tools: { question: true },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -975,7 +976,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -1089,7 +1090,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1151,7 +1152,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1234,7 +1235,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1322,7 +1323,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID,
|
||||
model: resolved,
|
||||
agent,
|
||||
@@ -1447,7 +1448,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -1539,7 +1540,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
@@ -1630,7 +1631,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make("anthropic"), modelID: resolved.id, variant: "max" },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
const input = [
|
||||
{
|
||||
@@ -1832,7 +1833,7 @@ describe("session.llm.stream", () => {
|
||||
time: { created: Date.now() },
|
||||
agent: agent.name,
|
||||
model: { providerID: ProviderID.make(geminiFixture.providerID), modelID: resolved.id },
|
||||
} satisfies MessageV2.User
|
||||
} satisfies SessionLegacy.User
|
||||
|
||||
yield* drain({
|
||||
user,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { APICallError } from "ai"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
@@ -58,7 +59,7 @@ const model: Provider.Model = {
|
||||
release_date: "2026-01-01",
|
||||
}
|
||||
|
||||
function userInfo(id: string): MessageV2.User {
|
||||
function userInfo(id: string): SessionLegacy.User {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
@@ -68,15 +69,15 @@ function userInfo(id: string): MessageV2.User {
|
||||
model: { providerID, modelID: ModelID.make("test") },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.User
|
||||
} as unknown as SessionLegacy.User
|
||||
}
|
||||
|
||||
function assistantInfo(
|
||||
id: string,
|
||||
parentID: string,
|
||||
error?: MessageV2.Assistant["error"],
|
||||
error?: SessionLegacy.Assistant["error"],
|
||||
meta?: { providerID: string; modelID: string },
|
||||
): MessageV2.Assistant {
|
||||
): SessionLegacy.Assistant {
|
||||
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
|
||||
return {
|
||||
id,
|
||||
@@ -97,7 +98,7 @@ function assistantInfo(
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
} as unknown as MessageV2.Assistant
|
||||
} as unknown as SessionLegacy.Assistant
|
||||
}
|
||||
|
||||
function basePart(messageID: string, id: string) {
|
||||
@@ -110,7 +111,7 @@ function basePart(messageID: string, id: string) {
|
||||
|
||||
describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out messages with no parts", async () => {
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo("m-empty"),
|
||||
parts: [],
|
||||
@@ -123,7 +124,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "hello",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -138,7 +139,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out messages with only ignored parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -148,7 +149,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "ignored",
|
||||
ignored: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -158,7 +159,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters out user messages with only empty text parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -167,7 +168,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -177,7 +178,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters empty user text parts while keeping non-empty parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -191,7 +192,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "hello",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -206,7 +207,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("includes synthetic text parts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -216,7 +217,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "hello",
|
||||
synthetic: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo("m-assistant", messageID),
|
||||
@@ -227,7 +228,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "assistant",
|
||||
synthetic: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -246,7 +247,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("converts user text/file parts and injects compaction/subtask prompts", async () => {
|
||||
const messageID = "m-user"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(messageID),
|
||||
parts: [
|
||||
@@ -294,7 +295,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
description: "desc",
|
||||
agent: "agent",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -320,7 +321,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -329,7 +330,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -364,7 +365,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -433,7 +434,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
)
|
||||
const userID = "m-user-anthropic"
|
||||
const assistantID = "m-assistant-anthropic"
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -442,7 +443,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -470,7 +471,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -514,7 +515,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: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -523,7 +524,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -551,7 +552,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -602,7 +603,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -611,7 +612,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
|
||||
@@ -644,7 +645,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -685,7 +686,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -694,7 +695,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -713,7 +714,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0, end: 1, compacted: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -752,7 +753,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -761,7 +762,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -780,7 +781,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -822,7 +823,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -831,7 +832,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -850,7 +851,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
metadata: { openai: { tool: "meta" } },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -900,7 +901,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
"</shell_metadata>",
|
||||
].join("\n")
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -909,7 +910,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -927,7 +928,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0, end: 1 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -965,12 +966,12 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("filters assistant messages with non-abort errors", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(
|
||||
assistantID,
|
||||
"m-parent",
|
||||
new MessageV2.APIError({ message: "boom", isRetryable: true }).toObject() as MessageV2.APIError,
|
||||
new SessionLegacy.APIError({ message: "boom", isRetryable: true }).toObject() as SessionLegacy.APIError,
|
||||
),
|
||||
parts: [
|
||||
{
|
||||
@@ -978,7 +979,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "should not render",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -989,9 +990,9 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const assistantID1 = "m-assistant-1"
|
||||
const assistantID2 = "m-assistant-2"
|
||||
|
||||
const aborted = new MessageV2.AbortedError({ message: "aborted" }).toObject() as MessageV2.Assistant["error"]
|
||||
const aborted = new SessionLegacy.AbortedError({ message: "aborted" }).toObject() as SessionLegacy.Assistant["error"]
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID1, "m-parent", aborted),
|
||||
parts: [
|
||||
@@ -1006,7 +1007,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "partial answer",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID2, "m-parent", aborted),
|
||||
@@ -1021,7 +1022,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
text: "thinking",
|
||||
time: { start: 0 },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1061,7 +1062,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
index: 0,
|
||||
},
|
||||
]
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent", undefined, {
|
||||
providerID: openrouterModel.providerID,
|
||||
@@ -1084,7 +1085,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "answer",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1112,7 +1113,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("splits assistant messages on step-start boundaries", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1130,7 +1131,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "second",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1149,7 +1150,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
test("drops messages that only contain step-start parts", async () => {
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1157,7 +1158,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
...basePart(assistantID, "p1"),
|
||||
type: "step-start",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1168,7 +1169,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
const userID = "m-user"
|
||||
const assistantID = "m-assistant"
|
||||
|
||||
const input: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: userInfo(userID),
|
||||
parts: [
|
||||
@@ -1177,7 +1178,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
type: "text",
|
||||
text: "run tool",
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
{
|
||||
info: assistantInfo(assistantID, userID),
|
||||
@@ -1204,7 +1205,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
time: { start: 0 },
|
||||
},
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1257,7 +1258,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: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1277,7 +1278,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
metadata: { anthropic: { signature: "sig2" } },
|
||||
},
|
||||
{ ...basePart(assistantID, "p6"), type: "text", text: "the answer" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1293,7 +1294,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: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
@@ -1305,7 +1306,7 @@ describe("session.message-v2.toModelMessage", () => {
|
||||
},
|
||||
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
|
||||
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1320,14 +1321,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: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.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 MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1340,13 +1341,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: MessageV2.WithParts[] = [
|
||||
const input: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: assistantInfo(assistantID, "m-parent"),
|
||||
parts: [
|
||||
{ ...basePart(assistantID, "p1"), type: "text", text: "" },
|
||||
{ ...basePart(assistantID, "p2"), type: "text", text: "hello" },
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1458,7 +1459,7 @@ describe("session.message-v2.fromError", () => {
|
||||
isRetryable: false,
|
||||
})
|
||||
const result = MessageV2.fromError(error, { providerID })
|
||||
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1479,7 +1480,7 @@ describe("session.message-v2.fromError", () => {
|
||||
isRetryable: false,
|
||||
})
|
||||
const result = MessageV2.fromError(error, { providerID })
|
||||
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(true)
|
||||
})
|
||||
|
||||
test("does not classify 429 no body as context overflow", () => {
|
||||
@@ -1494,8 +1495,8 @@ describe("session.message-v2.fromError", () => {
|
||||
}),
|
||||
{ providerID },
|
||||
)
|
||||
expect(MessageV2.ContextOverflowError.isInstance(result)).toBe(false)
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
expect(SessionLegacy.ContextOverflowError.isInstance(result)).toBe(false)
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
})
|
||||
|
||||
test("serializes unknown inputs", () => {
|
||||
@@ -1530,9 +1531,9 @@ describe("session.message-v2.fromError", () => {
|
||||
|
||||
const result = MessageV2.fromError(zlibError, { providerID })
|
||||
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
expect((result as MessageV2.APIError).data.isRetryable).toBe(true)
|
||||
expect((result as MessageV2.APIError).data.message).toInclude("decompression")
|
||||
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")
|
||||
})
|
||||
|
||||
test("classifies ZlibError as AbortedError when abort context is provided", () => {
|
||||
@@ -1556,21 +1557,21 @@ describe("session.message-v2.latest", () => {
|
||||
const CONTINUE_USER = MessageID.make("msg_005")
|
||||
const NEW_COMPACTION_USER = MessageID.make("msg_006")
|
||||
|
||||
const tailUser: MessageV2.WithParts = {
|
||||
const tailUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(TAIL_USER),
|
||||
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as MessageV2.Part[],
|
||||
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
const overflowAssistant: MessageV2.WithParts = {
|
||||
const overflowAssistant: SessionLegacy.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 MessageV2.Assistant,
|
||||
} as SessionLegacy.Assistant,
|
||||
parts: [],
|
||||
}
|
||||
|
||||
const compactionUser: MessageV2.WithParts = {
|
||||
const compactionUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(COMPACTION_USER),
|
||||
parts: [
|
||||
{
|
||||
@@ -1579,20 +1580,20 @@ describe("session.message-v2.latest", () => {
|
||||
auto: true,
|
||||
tail_start_id: TAIL_USER,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
const summaryAssistant: MessageV2.WithParts = {
|
||||
const summaryAssistant: SessionLegacy.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 MessageV2.Assistant,
|
||||
} as SessionLegacy.Assistant,
|
||||
parts: [],
|
||||
}
|
||||
|
||||
const continueUser: MessageV2.WithParts = {
|
||||
const continueUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(CONTINUE_USER),
|
||||
parts: [
|
||||
{
|
||||
@@ -1602,7 +1603,7 @@ describe("session.message-v2.latest", () => {
|
||||
synthetic: true,
|
||||
metadata: { compaction_continue: true },
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
// Regression for double auto-compaction. The reorder in filterCompacted
|
||||
@@ -1628,7 +1629,7 @@ describe("session.message-v2.latest", () => {
|
||||
})
|
||||
|
||||
test("a fresh compaction-user newer than the latest summary surfaces in tasks", () => {
|
||||
const newCompactionUser: MessageV2.WithParts = {
|
||||
const newCompactionUser: SessionLegacy.WithParts = {
|
||||
info: userInfo(NEW_COMPACTION_USER),
|
||||
parts: [
|
||||
{
|
||||
@@ -1636,7 +1637,7 @@ describe("session.message-v2.latest", () => {
|
||||
type: "compaction",
|
||||
auto: true,
|
||||
},
|
||||
] as MessageV2.Part[],
|
||||
] as SessionLegacy.Part[],
|
||||
}
|
||||
|
||||
const state = MessageV2.latest([
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
@@ -45,7 +46,7 @@ const fill = Effect.fn("Test.fill")(function* (
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
sessionID,
|
||||
@@ -69,7 +70,7 @@ const addUser = Effect.fn("Test.addUser")(function* (sessionID: SessionID, text?
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
if (text) {
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
@@ -85,7 +86,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?: MessageV2.Assistant["error"] },
|
||||
opts?: { summary?: boolean; finish?: string; error?: SessionLegacy.Assistant["error"] },
|
||||
) {
|
||||
const session = yield* SessionNs.Service
|
||||
const id = MessageID.ascending()
|
||||
@@ -105,7 +106,7 @@ const addAssistant = Effect.fn("Test.addAssistant")(function* (
|
||||
summary: opts?.summary,
|
||||
finish: opts?.finish,
|
||||
error: opts?.error,
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
return id
|
||||
})
|
||||
|
||||
@@ -389,7 +390,7 @@ describe("MessageV2.parts", () => {
|
||||
const result = MessageV2.parts(id)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].type).toBe("text")
|
||||
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
expect((result[0] as SessionLegacy.TextPart).text).toBe("m0")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -427,9 +428,9 @@ describe("MessageV2.parts", () => {
|
||||
|
||||
const result = MessageV2.parts(id)
|
||||
expect(result).toHaveLength(3)
|
||||
expect((result[0] as MessageV2.TextPart).text).toBe("m0")
|
||||
expect((result[1] as MessageV2.TextPart).text).toBe("second")
|
||||
expect((result[2] as MessageV2.TextPart).text).toBe("third")
|
||||
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")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -466,7 +467,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 MessageV2.TextPart).text).toBe("m0")
|
||||
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("m0")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -536,7 +537,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 MessageV2.TextPart).text).toBe("response")
|
||||
expect((result.parts[0] as SessionLegacy.TextPart).text).toBe("response")
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -672,10 +673,10 @@ describe("MessageV2.filterCompacted", () => {
|
||||
const u1 = yield* addUser(sessionID, "hello")
|
||||
yield* addCompactionPart(sessionID, u1)
|
||||
|
||||
const error = new MessageV2.APIError({
|
||||
const error = new SessionLegacy.APIError({
|
||||
message: "boom",
|
||||
isRetryable: true,
|
||||
}).toObject() as MessageV2.Assistant["error"]
|
||||
}).toObject() as SessionLegacy.Assistant["error"]
|
||||
yield* addAssistant(sessionID, u1, { summary: true, finish: "end_turn", error })
|
||||
yield* addUser(sessionID, "retry")
|
||||
|
||||
@@ -951,7 +952,7 @@ describe("MessageV2.filterCompacted", () => {
|
||||
test("works with array input", () => {
|
||||
// filterCompacted accepts any Iterable, not just generators
|
||||
const id = MessageID.ascending()
|
||||
const items: MessageV2.WithParts[] = [
|
||||
const items: SessionLegacy.WithParts[] = [
|
||||
{
|
||||
info: {
|
||||
id,
|
||||
@@ -960,8 +961,8 @@ describe("MessageV2.filterCompacted", () => {
|
||||
time: { created: 1 },
|
||||
agent: "test",
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
} as unknown as MessageV2.Info,
|
||||
parts: [{ type: "text", text: "hello" }] as unknown as MessageV2.Part[],
|
||||
} as unknown as SessionLegacy.Info,
|
||||
parts: [{ type: "text", text: "hello" }] as unknown as SessionLegacy.Part[],
|
||||
},
|
||||
]
|
||||
const result = MessageV2.filterCompacted(items)
|
||||
@@ -1027,7 +1028,7 @@ describe("MessageV2 consistency", () => {
|
||||
|
||||
const streamed = Array.from(MessageV2.stream(sessionID))
|
||||
|
||||
const paged = [] as MessageV2.WithParts[]
|
||||
const paged = [] as SessionLegacy.WithParts[]
|
||||
let cursor: string | undefined
|
||||
while (true) {
|
||||
const result = yield* MessageV2.page({ sessionID, limit: 3, before: cursor })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { expect } from "bun:test"
|
||||
import { tool } from "ai"
|
||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||
@@ -145,7 +146,7 @@ const assistant = Effect.fn("TestSession.assistant")(function* (
|
||||
root: string,
|
||||
) {
|
||||
const session = yield* Session.Service
|
||||
const msg: MessageV2.Assistant = {
|
||||
const msg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -234,7 +235,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -306,7 +307,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -317,14 +318,14 @@ it.live("session.processor effect tests preserve text start time", () =>
|
||||
.pipe(Effect.forkChild)
|
||||
|
||||
yield* waitFor(
|
||||
Effect.sync(() => MessageV2.parts(msg.id).find((part): part is MessageV2.TextPart => part.type === "text")),
|
||||
Effect.sync(() => MessageV2.parts(msg.id).find((part): part is SessionLegacy.TextPart => part.type === "text")),
|
||||
"timed out waiting for text part",
|
||||
)
|
||||
yield* Effect.sleep("20 millis")
|
||||
gate.resolve()
|
||||
|
||||
const exit = yield* Fiber.await(run)
|
||||
const text = MessageV2.parts(msg.id).find((part): part is MessageV2.TextPart => part.type === "text")
|
||||
const text = MessageV2.parts(msg.id).find((part): part is SessionLegacy.TextPart => part.type === "text")
|
||||
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
expect(text?.text).toBe("hello")
|
||||
@@ -364,7 +365,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -409,7 +410,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -419,8 +420,8 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const reasoning = parts.find((part): part is MessageV2.ReasoningPart => part.type === "reasoning")
|
||||
const text = parts.find((part): part is MessageV2.TextPart => part.type === "text")
|
||||
const reasoning = parts.find((part): part is SessionLegacy.ReasoningPart => part.type === "reasoning")
|
||||
const text = parts.find((part): part is SessionLegacy.TextPart => part.type === "text")
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(1)
|
||||
@@ -457,7 +458,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -467,7 +468,7 @@ it.live("session.processor effect tests reset reasoning state across retries", (
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const reasoning = parts.filter((part): part is MessageV2.ReasoningPart => part.type === "reasoning")
|
||||
const reasoning = parts.filter((part): part is SessionLegacy.ReasoningPart => part.type === "reasoning")
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(2)
|
||||
@@ -504,7 +505,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -548,7 +549,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -601,7 +602,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -646,7 +647,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -689,7 +690,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -709,7 +710,7 @@ it.live("session.processor effect tests complete AI SDK tool calls when native f
|
||||
})
|
||||
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
const call = parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
|
||||
expect(value).toBe("continue")
|
||||
expect(yield* llm.calls).toBe(1)
|
||||
@@ -755,7 +756,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -767,14 +768,14 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
|
||||
|
||||
yield* llm.wait(1)
|
||||
yield* waitFor(
|
||||
Effect.sync(() => MessageV2.parts(msg.id).find((part): part is MessageV2.ToolPart => part.type === "tool")),
|
||||
Effect.sync(() => MessageV2.parts(msg.id).find((part): part is SessionLegacy.ToolPart => part.type === "tool")),
|
||||
"timed out waiting for tool part",
|
||||
)
|
||||
yield* Fiber.interrupt(run)
|
||||
|
||||
const exit = yield* Fiber.await(run)
|
||||
const parts = MessageV2.parts(msg.id)
|
||||
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
const call = parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
if (Exit.isFailure(exit)) {
|
||||
@@ -829,7 +830,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
@@ -892,7 +893,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 MessageV2.User,
|
||||
} satisfies SessionLegacy.User,
|
||||
sessionID: chat.id,
|
||||
model: mdl,
|
||||
agent: agent(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { expect } from "bun:test"
|
||||
import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
|
||||
@@ -90,20 +91,20 @@ function withSh<A, E, R>(fx: () => Effect.Effect<A, E, R>) {
|
||||
)
|
||||
}
|
||||
|
||||
function toolPart(parts: MessageV2.Part[]) {
|
||||
return parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||
function toolPart(parts: SessionLegacy.Part[]) {
|
||||
return parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
}
|
||||
|
||||
type CompletedToolPart = MessageV2.ToolPart & { state: MessageV2.ToolStateCompleted }
|
||||
type ErrorToolPart = MessageV2.ToolPart & { state: MessageV2.ToolStateError }
|
||||
type CompletedToolPart = SessionLegacy.ToolPart & { state: SessionLegacy.ToolStateCompleted }
|
||||
type ErrorToolPart = SessionLegacy.ToolPart & { state: SessionLegacy.ToolStateError }
|
||||
|
||||
function completedTool(parts: MessageV2.Part[]) {
|
||||
function completedTool(parts: SessionLegacy.Part[]) {
|
||||
const part = toolPart(parts)
|
||||
expect(part?.state.status).toBe("completed")
|
||||
return part?.state.status === "completed" ? (part as CompletedToolPart) : undefined
|
||||
}
|
||||
|
||||
function errorTool(parts: MessageV2.Part[]) {
|
||||
function errorTool(parts: SessionLegacy.Part[]) {
|
||||
const part = toolPart(parts)
|
||||
expect(part?.state.status).toBe("error")
|
||||
return part?.state.status === "error" ? (part as ErrorToolPart) : undefined
|
||||
@@ -388,7 +389,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: MessageV2.Assistant = {
|
||||
const assistant: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: msg.id,
|
||||
@@ -748,7 +749,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 MessageV2.ToolPart => part.type === "tool")
|
||||
const tool = taskMsg?.parts.find((part): part is SessionLegacy.ToolPart => part.type === "tool")
|
||||
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
|
||||
}),
|
||||
"timed out waiting for running subtask metadata",
|
||||
@@ -791,7 +792,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 MessageV2.ToolPart => part.type === "tool" && part.tool === "task",
|
||||
(part): part is SessionLegacy.ToolPart => part.type === "tool" && part.tool === "task",
|
||||
)
|
||||
if (tool?.state.status === "running" && tool.state.metadata?.sessionId) return tool
|
||||
}),
|
||||
@@ -1910,11 +1911,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 MessageV2.TextPartInput =>
|
||||
(part): part is SessionLegacy.TextPartInput =>
|
||||
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
|
||||
)
|
||||
const files = parts.filter((part): part is MessageV2.FilePartInput => part.type === "file")
|
||||
const agents = parts.filter((part): part is MessageV2.AgentPartInput => part.type === "agent")
|
||||
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 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")
|
||||
@@ -1967,7 +1968,7 @@ noLLMServer.instance(
|
||||
|
||||
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
const synthetic = stored.parts.filter(
|
||||
(part): part is MessageV2.TextPart => part.type === "text" && part.synthetic === true,
|
||||
(part): part is SessionLegacy.TextPart => part.type === "text" && part.synthetic === true,
|
||||
)
|
||||
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
|
||||
|
||||
@@ -2022,7 +2023,7 @@ noLLMServer.instance(
|
||||
|
||||
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||
const synthetic = stored.parts.filter(
|
||||
(part): part is MessageV2.TextPart => part.type === "text" && part.synthetic === true,
|
||||
(part): part is SessionLegacy.TextPart => part.type === "text" && part.synthetic === true,
|
||||
)
|
||||
const reference = synthetic.find((part) =>
|
||||
part.text.startsWith("Referenced configured reference @docs/README.md."),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import type { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { APICallError } from "ai"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
@@ -16,9 +17,9 @@ const providerID = ProviderID.make("test")
|
||||
const retryProvider = "test"
|
||||
const it = testEffect(Layer.mergeAll(SessionStatus.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
|
||||
function apiError(headers?: Record<string, string>): MessageV2.APIError {
|
||||
return Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
function apiError(headers?: Record<string, string>): SessionLegacy.APIError {
|
||||
return Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "boom",
|
||||
isRetryable: true,
|
||||
responseHeaders: headers,
|
||||
@@ -94,7 +95,7 @@ describe("session.retry.delay", () => {
|
||||
const step = yield* Schedule.toStepWithMetadata(
|
||||
SessionRetry.policy({
|
||||
provider: "test",
|
||||
parse: Schema.decodeUnknownSync(MessageV2.APIError.Schema),
|
||||
parse: Schema.decodeUnknownSync(SessionLegacy.APIError.Schema),
|
||||
set: (info) =>
|
||||
status.set(sessionID, {
|
||||
type: "retry",
|
||||
@@ -164,7 +165,7 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("does not retry context overflow errors", () => {
|
||||
const error = new MessageV2.ContextOverflowError({
|
||||
const error = new SessionLegacy.ContextOverflowError({
|
||||
message: "Input exceeds context window of this model",
|
||||
responseBody: '{"error":{"code":"context_length_exceeded"}}',
|
||||
}).toObject()
|
||||
@@ -173,8 +174,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries 500 errors even when isRetryable is false", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Internal server error",
|
||||
isRetryable: false,
|
||||
statusCode: 500,
|
||||
@@ -186,8 +187,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries 502 bad gateway errors", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Bad gateway",
|
||||
isRetryable: false,
|
||||
statusCode: 502,
|
||||
@@ -198,8 +199,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries 503 service unavailable errors", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Service unavailable",
|
||||
isRetryable: false,
|
||||
statusCode: 503,
|
||||
@@ -210,8 +211,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("does not retry 4xx errors when isRetryable is false", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Bad request",
|
||||
isRetryable: false,
|
||||
statusCode: 400,
|
||||
@@ -222,8 +223,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("retries ZlibError decompression failures", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Response decompression failed",
|
||||
isRetryable: true,
|
||||
metadata: { code: "ZlibError" },
|
||||
@@ -236,8 +237,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("maps free limits to Go upsell action", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Free usage exceeded",
|
||||
isRetryable: true,
|
||||
statusCode: 429,
|
||||
@@ -262,8 +263,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("maps Go subscription limits to workspace PAYG upsell", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Subscription quota exceeded. You can continue using free models.",
|
||||
isRetryable: true,
|
||||
statusCode: 429,
|
||||
@@ -300,8 +301,8 @@ describe("session.retry.retryable", () => {
|
||||
})
|
||||
|
||||
test("maps Go subscription limits without limit metadata", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Subscription quota exceeded. You can continue using free models.",
|
||||
isRetryable: true,
|
||||
statusCode: 429,
|
||||
@@ -355,8 +356,8 @@ describe("session.message-v2.fromError", () => {
|
||||
|
||||
const result = MessageV2.fromError(error, { providerID })
|
||||
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
if (!SessionLegacy.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")
|
||||
@@ -366,8 +367,8 @@ describe("session.message-v2.fromError", () => {
|
||||
)
|
||||
|
||||
test("ECONNRESET socket error is retryable", () => {
|
||||
const error = Schema.decodeUnknownSync(MessageV2.APIError.Schema)(
|
||||
new MessageV2.APIError({
|
||||
const error = Schema.decodeUnknownSync(SessionLegacy.APIError.Schema)(
|
||||
new SessionLegacy.APIError({
|
||||
message: "Connection reset by server",
|
||||
isRetryable: true,
|
||||
metadata: { code: "ECONNRESET", message: "The socket connection was closed unexpectedly" },
|
||||
@@ -390,7 +391,7 @@ describe("session.message-v2.fromError", () => {
|
||||
isRetryable: false,
|
||||
})
|
||||
const result = MessageV2.fromError(error, { providerID: ProviderID.make("openai") })
|
||||
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
if (!SessionLegacy.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(result.data.isRetryable).toBe(true)
|
||||
})
|
||||
|
||||
@@ -411,8 +412,8 @@ describe("session.message-v2.fromError", () => {
|
||||
{ providerID: ProviderID.make("openai") },
|
||||
)
|
||||
|
||||
expect(MessageV2.APIError.isInstance(result)).toBe(true)
|
||||
if (!MessageV2.APIError.isInstance(result)) throw new Error("expected APIError")
|
||||
expect(SessionLegacy.APIError.isInstance(result)).toBe(true)
|
||||
if (!SessionLegacy.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.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect, Layer } from "effect"
|
||||
@@ -130,7 +131,7 @@ describe("revert + compact workflow", () => {
|
||||
text: "Hello, please help me",
|
||||
})
|
||||
|
||||
const assistantMsg1: MessageV2.Assistant = {
|
||||
const assistantMsg1: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -187,7 +188,7 @@ describe("revert + compact workflow", () => {
|
||||
text: "What's the capital of France?",
|
||||
})
|
||||
|
||||
const assistantMsg2: MessageV2.Assistant = {
|
||||
const assistantMsg2: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
@@ -292,7 +293,7 @@ describe("revert + compact workflow", () => {
|
||||
text: "Hello",
|
||||
})
|
||||
|
||||
const assistantMsg: MessageV2.Assistant = {
|
||||
const assistantMsg: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
sessionID,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Deferred, Effect, Exit, Layer } from "effect"
|
||||
import { Session as SessionNs } from "@/session/session"
|
||||
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
|
||||
@@ -121,14 +122,14 @@ describe("step-finish token propagation via Bus event", () => {
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
tools: {},
|
||||
mode: "",
|
||||
} as unknown as MessageV2.Info)
|
||||
} as unknown as SessionLegacy.Info)
|
||||
|
||||
// Bus subscribers receive readonly Schema.Type payloads; `MessageV2.Part`
|
||||
// Bus subscribers receive readonly Schema.Type payloads; `SessionLegacy.Part`
|
||||
// is the mutable domain type. Cast bridges the two — safe because the
|
||||
// test only reads the value afterwards.
|
||||
const received = yield* Deferred.make<MessageV2.Part>()
|
||||
const received = yield* Deferred.make<SessionLegacy.Part>()
|
||||
const unsub = subscribeGlobal(MessageV2.Event.PartUpdated.type, (event) => {
|
||||
Deferred.doneUnsafe(received, Effect.succeed(event.properties.part as MessageV2.Part))
|
||||
Deferred.doneUnsafe(received, Effect.succeed(event.properties.part as SessionLegacy.Part))
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsub))
|
||||
|
||||
@@ -154,7 +155,7 @@ describe("step-finish token propagation via Bus event", () => {
|
||||
const receivedPart = yield* awaitDeferred(received, "timed out waiting for message.part.updated")
|
||||
|
||||
expect(receivedPart.type).toBe("step-finish")
|
||||
const finish = receivedPart as MessageV2.StepFinishPart
|
||||
const finish = receivedPart as SessionLegacy.StepFinishPart
|
||||
expect(finish.tokens.input).toBe(500)
|
||||
expect(finish.tokens.output).toBe(800)
|
||||
expect(finish.tokens.reasoning).toBe(200)
|
||||
|
||||
@@ -22,6 +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 * as Log from "@opencode-ai/core/util/log"
|
||||
import { provideTmpdirServer } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -259,7 +260,7 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
|
||||
const allMsgs = yield* MessageV2.filterCompactedEffect(session.id)
|
||||
const tool = allMsgs
|
||||
.flatMap((m) => m.parts)
|
||||
.find((p): p is MessageV2.ToolPart => p.type === "tool" && p.tool === "bash")
|
||||
.find((p): p is SessionLegacy.ToolPart => p.type === "tool" && p.tool === "bash")
|
||||
expect(tool?.state.status).toBe("completed")
|
||||
|
||||
// Poll for diff — summarize() is fire-and-forget
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
@@ -218,7 +219,7 @@ describe("StructuredOutput Integration", () => {
|
||||
)
|
||||
|
||||
test("unit test: StructuredOutputError is properly structured", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Failed to produce valid structured output after 3 attempts",
|
||||
retries: 3,
|
||||
})
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
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(MessageV2.Format)
|
||||
const decodeUser = Schema.decodeUnknownExit(MessageV2.User)
|
||||
const decodeAssistant = Schema.decodeUnknownExit(MessageV2.Assistant)
|
||||
const decodeFormat = Schema.decodeUnknownExit(SessionLegacy.Format)
|
||||
const decodeUser = Schema.decodeUnknownExit(SessionLegacy.User)
|
||||
const decodeAssistant = Schema.decodeUnknownExit(SessionLegacy.Assistant)
|
||||
|
||||
describe("structured-output.OutputFormat", () => {
|
||||
test("parses text format", () => {
|
||||
@@ -65,7 +66,7 @@ describe("structured-output.OutputFormat", () => {
|
||||
|
||||
describe("structured-output.StructuredOutputError", () => {
|
||||
test("creates error with message and retries", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Failed to validate",
|
||||
retries: 3,
|
||||
})
|
||||
@@ -76,7 +77,7 @@ describe("structured-output.StructuredOutputError", () => {
|
||||
})
|
||||
|
||||
test("converts to object correctly", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Test error",
|
||||
retries: 2,
|
||||
})
|
||||
@@ -88,13 +89,13 @@ describe("structured-output.StructuredOutputError", () => {
|
||||
})
|
||||
|
||||
test("isInstance correctly identifies error", () => {
|
||||
const error = new MessageV2.StructuredOutputError({
|
||||
const error = new SessionLegacy.StructuredOutputError({
|
||||
message: "Test",
|
||||
retries: 1,
|
||||
})
|
||||
|
||||
expect(MessageV2.StructuredOutputError.isInstance(error)).toBe(true)
|
||||
expect(MessageV2.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
|
||||
expect(SessionLegacy.StructuredOutputError.isInstance(error)).toBe(true)
|
||||
expect(SessionLegacy.StructuredOutputError.isInstance({ name: "other" })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { Effect, Exit, Fiber, Layer } from "effect"
|
||||
import { Agent } from "../../src/agent/agent"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
@@ -65,7 +66,7 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
|
||||
model: ref,
|
||||
time: { created: Date.now() },
|
||||
})
|
||||
const assistant: MessageV2.Assistant = {
|
||||
const assistant: SessionLegacy.Assistant = {
|
||||
id: MessageID.ascending(),
|
||||
role: "assistant",
|
||||
parentID: user.id,
|
||||
@@ -96,7 +97,7 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void;
|
||||
}
|
||||
}
|
||||
|
||||
function reply(input: SessionPrompt.PromptInput, text: string): MessageV2.WithParts {
|
||||
function reply(input: SessionPrompt.PromptInput, text: string): SessionLegacy.WithParts {
|
||||
const id = MessageID.ascending()
|
||||
return {
|
||||
info: {
|
||||
|
||||
Reference in New Issue
Block a user