refactor(core): consolidate tool architecture
This commit is contained in:
parent
0fd73a2976
commit
8db7487c89
466 changed files with 9405 additions and 11071 deletions
|
|
@ -4,7 +4,7 @@ import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest, type Mode
|
|||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { Config } from "../config"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { llmClient } from "../effect/app-node-platform"
|
||||
import { SessionEvent } from "./event"
|
||||
|
|
@ -15,7 +15,7 @@ import { SessionRunnerModel } from "./runner/model"
|
|||
import { SessionSchema } from "./schema"
|
||||
import { toSessionError } from "./to-session-error"
|
||||
import { Token } from "../util/token"
|
||||
import type { ModelV2 } from "../model"
|
||||
import type { Info } from "../model"
|
||||
import { SessionUsage } from "./usage"
|
||||
|
||||
const DEFAULT_BUFFER = 20_000
|
||||
|
|
@ -62,7 +62,7 @@ type Settings = {
|
|||
|
||||
type Dependencies = {
|
||||
readonly app: App.Info
|
||||
readonly events: EventV2.Interface
|
||||
readonly bus: Bus.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ export type AutoInput = {
|
|||
readonly session: SessionSchema.Info
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly model: Model
|
||||
readonly cost: ModelV2.Info["cost"]
|
||||
readonly cost: Info["cost"]
|
||||
}
|
||||
|
||||
export type ManualInput = {
|
||||
|
|
@ -86,7 +86,7 @@ export type ManualInput = {
|
|||
type Plan = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly model: Model
|
||||
readonly cost: ModelV2.Info["cost"]
|
||||
readonly cost: Info["cost"]
|
||||
readonly reason: SessionMessage.Compaction["reason"]
|
||||
readonly prompt: string
|
||||
readonly recent: string
|
||||
|
|
@ -103,7 +103,7 @@ export interface Interface {
|
|||
readonly compactManual: (input: ManualInput) => Effect.Effect<Outcome>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionCompaction") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionCompaction") {}
|
||||
|
||||
const truncate = (value: string) =>
|
||||
value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]`
|
||||
|
|
@ -233,11 +233,11 @@ const make = (dependencies: Dependencies) => {
|
|||
readonly error: SessionError.Error
|
||||
readonly inputID?: SessionMessage.ID
|
||||
}) {
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Failed, input)
|
||||
yield* dependencies.bus.publish(SessionEvent.Compaction.Failed, input)
|
||||
return { status: "failed" as const, error: input.error }
|
||||
})
|
||||
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
|
||||
yield* dependencies.bus.publish(SessionEvent.Compaction.Started, {
|
||||
sessionID: plan.session.id,
|
||||
reason: plan.reason,
|
||||
recent: plan.recent,
|
||||
|
|
@ -249,7 +249,7 @@ const make = (dependencies: Dependencies) => {
|
|||
let usage: SessionUsage.Recorded | undefined
|
||||
const recordUsage = Effect.suspend(() =>
|
||||
usage
|
||||
? dependencies.events.publish(SessionEvent.UsageRecorded, {
|
||||
? dependencies.bus.publish(SessionEvent.UsageRecorded, {
|
||||
sessionID: plan.session.id,
|
||||
source: "compaction",
|
||||
...usage,
|
||||
|
|
@ -274,7 +274,7 @@ const make = (dependencies: Dependencies) => {
|
|||
}
|
||||
if (LLMEvent.is.textDelta(event)) {
|
||||
chunks.push(event.text)
|
||||
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
|
||||
return dependencies.bus.publish(SessionEvent.Compaction.Delta, {
|
||||
sessionID: plan.session.id,
|
||||
text: event.text,
|
||||
})
|
||||
|
|
@ -316,7 +316,7 @@ const make = (dependencies: Dependencies) => {
|
|||
inputID: plan.inputID,
|
||||
})
|
||||
}
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||
yield* dependencies.bus.publish(SessionEvent.Compaction.Ended, {
|
||||
sessionID: plan.session.id,
|
||||
reason: plan.reason,
|
||||
text: summary,
|
||||
|
|
@ -395,17 +395,17 @@ const make = (dependencies: Dependencies) => {
|
|||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const llm = yield* LLMClient.Service
|
||||
const config = yield* Config.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const app = yield* App.Metadata
|
||||
return make({ events, llm, models, config: settings(yield* config.entries()), app })
|
||||
return make({ bus, llm, models, config: settings(yield* config.entries()), app })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [EventV2.node, llmClient, Config.node, SessionRunnerModel.node, App.node],
|
||||
deps: [Bus.node, llmClient, Config.node, SessionRunnerModel.node, App.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as SessionContext from "./context"
|
||||
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Agent } from "../agent"
|
||||
import { CodeModeInstructions } from "../codemode/instructions"
|
||||
import { Database } from "../database/database"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
|
|
@ -13,7 +13,7 @@ import { McpInstructions } from "../mcp/instructions"
|
|||
import { PluginSupervisor } from "../plugin/supervisor"
|
||||
import { ReferenceInstructions } from "../reference/instructions"
|
||||
import { SkillInstructions } from "../skill/instructions"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { Tool } from "../tool"
|
||||
import { AgentNotFoundError } from "./error"
|
||||
import { SessionHistory } from "./history"
|
||||
import { InstructionEntry } from "./instruction-entry"
|
||||
|
|
@ -24,18 +24,18 @@ import { SessionStore } from "./store"
|
|||
|
||||
export interface Selection {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info }
|
||||
readonly agent: Agent.Selection & { readonly info: Agent.Info }
|
||||
readonly instructions: Instructions.Instructions
|
||||
readonly toolSet: ToolRegistry.ToolSet
|
||||
readonly tools: Tool.Snapshot
|
||||
}
|
||||
|
||||
export interface Loaded {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly agent: AgentV2.Selection & { readonly info: AgentV2.Info }
|
||||
readonly agent: Agent.Selection & { readonly info: Agent.Info }
|
||||
readonly model: SessionRunnerModel.Resolved
|
||||
readonly initial: string
|
||||
readonly messages: ReadonlyArray<SessionMessage.Info>
|
||||
readonly toolSet: ToolRegistry.ToolSet
|
||||
readonly tools: Tool.Snapshot
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -52,12 +52,12 @@ export interface Interface {
|
|||
}
|
||||
|
||||
/** Location-scoped model-context loader for durable Session Steps. */
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContext") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionContext") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
const agents = yield* Agent.Service
|
||||
const builtins = yield* InstructionBuiltIns.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const discovery = yield* InstructionDiscovery.Service
|
||||
|
|
@ -69,7 +69,7 @@ const layer = Layer.effect(
|
|||
const referenceInstructions = yield* ReferenceInstructions.Service
|
||||
const skillInstructions = yield* SkillInstructions.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
const select = Effect.fn("SessionContext.select")(function* (sessionID: SessionSchema.ID) {
|
||||
const session = yield* store.get(sessionID)
|
||||
|
|
@ -82,7 +82,7 @@ const layer = Layer.effect(
|
|||
if (!agent.info) return yield* new AgentNotFoundError({ sessionID: session.id, agent: session.agent ?? agent.id })
|
||||
const loaded = yield* Effect.all(
|
||||
{
|
||||
toolSet: registry.snapshot(agent.info.permissions),
|
||||
tools: registry.snapshot(agent.info.permissions),
|
||||
builtins: builtins.load(sessionID),
|
||||
discovery: discovery.load(),
|
||||
skills: skillInstructions.load(agent),
|
||||
|
|
@ -97,14 +97,14 @@ const layer = Layer.effect(
|
|||
agent: { ...agent, info: agent.info },
|
||||
instructions: Instructions.combine([
|
||||
loaded.builtins,
|
||||
CodeModeInstructions.make(loaded.toolSet.codeModeCatalog),
|
||||
CodeModeInstructions.make(loaded.tools.codeModeCatalog),
|
||||
loaded.discovery,
|
||||
loaded.skills,
|
||||
loaded.references,
|
||||
loaded.mcp,
|
||||
loaded.entries,
|
||||
]),
|
||||
toolSet: loaded.toolSet,
|
||||
tools: loaded.tools,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ const layer = Layer.effect(
|
|||
model,
|
||||
initial: history.initial,
|
||||
messages: history.entries.map((entry) => entry.message),
|
||||
toolSet: selection.toolSet,
|
||||
tools: selection.tools,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ export const node = makeLocationNode({
|
|||
service: Service,
|
||||
layer,
|
||||
deps: [
|
||||
AgentV2.node,
|
||||
Agent.node,
|
||||
Database.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
|
|
@ -141,6 +141,6 @@ export const node = makeLocationNode({
|
|||
SessionRunnerModel.node,
|
||||
SessionStore.node,
|
||||
SkillInstructions.node,
|
||||
ToolRegistry.node,
|
||||
Tool.node,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as SessionExecution from "./execution"
|
||||
|
||||
import { Cause, Context, Effect, Exit, Layer } from "effect"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { LocationServiceMap } from "../location-service-map"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { SessionEvent } from "./event"
|
||||
|
|
@ -26,7 +26,7 @@ export interface Interface {
|
|||
}
|
||||
|
||||
/** Routes execution from a Session ID to the runner owned by that Session's Location. */
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionExecution") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionExecution") {}
|
||||
|
||||
type InterruptReason = "user" | "shutdown" | "superseded"
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ export const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const store = yield* SessionStore.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const reportLifecycle = <A>(sessionID: SessionSchema.ID, effect: Effect.Effect<A>) =>
|
||||
effect.pipe(
|
||||
Effect.tapCause((cause) =>
|
||||
|
|
@ -65,7 +65,7 @@ export const layer = Layer.effect(
|
|||
started: (sessionID) =>
|
||||
reportLifecycle(
|
||||
sessionID,
|
||||
events.publish(SessionEvent.Execution.Started, { sessionID }, clearSuspensionOnCommit(sessionID)),
|
||||
bus.publish(SessionEvent.Execution.Started, { sessionID }, clearSuspensionOnCommit(sessionID)),
|
||||
),
|
||||
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
|
||||
const session = yield* store.get(sessionID)
|
||||
|
|
@ -86,14 +86,14 @@ export const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const outcome = terminal(exit, reason)
|
||||
if (outcome.type === "succeeded") {
|
||||
yield* events.publish(SessionEvent.Execution.Succeeded, { sessionID }, clearSuspensionOnCommit(sessionID))
|
||||
yield* bus.publish(SessionEvent.Execution.Succeeded, { sessionID }, clearSuspensionOnCommit(sessionID))
|
||||
return
|
||||
}
|
||||
if (outcome.type === "interrupted") {
|
||||
yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason })
|
||||
yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason })
|
||||
return
|
||||
}
|
||||
yield* events.publish(
|
||||
yield* bus.publish(
|
||||
SessionEvent.Execution.Failed,
|
||||
{
|
||||
sessionID,
|
||||
|
|
@ -118,7 +118,7 @@ export const layer = Layer.effect(
|
|||
export const node = makeGlobalNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [SessionStore.node, LocationServiceMap.node, EventV2.node],
|
||||
deps: [SessionStore.node, LocationServiceMap.node, Bus.node],
|
||||
})
|
||||
|
||||
/** Low-level compatibility layer for callers that only need durable Session recording. */
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface Interface {
|
|||
* Restart continuity actions for the managed server. The service is inert until called: only the
|
||||
* managed server invokes it, so default, embedded, and stdio servers never suspend or auto-resume.
|
||||
*/
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRestart") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRestart") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ export const layer = Layer.effect(
|
|||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(selection.session.id)
|
||||
? selection.session.id.slice(4)
|
||||
: selection.session.id
|
||||
const toolSet = selection.toolSet
|
||||
const toolDefinitions = toolSet.definitions
|
||||
const tools = selection.tools
|
||||
const toolDefinitions = tools.definitions
|
||||
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
||||
const contextEvent = yield* hooks.trigger("session", "context", {
|
||||
sessionID: selection.session.id,
|
||||
|
|
|
|||
|
|
@ -18,4 +18,4 @@ export interface Interface {
|
|||
}
|
||||
|
||||
/** Location-scoped transient generation from Session context. */
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionGenerate") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionGenerate") {}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { DateTime, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Agent } from "../agent"
|
||||
import { Location } from "../location"
|
||||
import { ModelV2 } from "../model"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { ProviderV2 } from "../provider"
|
||||
import { Model } from "../model"
|
||||
import { Project } from "../project"
|
||||
import { Provider } from "../provider"
|
||||
import { AbsolutePath, RelativePath } from "../schema"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { Workspace } from "../workspace"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionTable } from "./sql"
|
||||
import { SessionMessage } from "./message"
|
||||
|
|
@ -17,7 +17,7 @@ const decodeRevert = Schema.decodeUnknownSync(PersistedRevert)
|
|||
export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info {
|
||||
return SessionSchema.Info.make({
|
||||
id: SessionSchema.ID.make(row.id),
|
||||
projectID: ProjectV2.ID.make(row.project_id),
|
||||
projectID: Project.ID.make(row.project_id),
|
||||
title: row.title,
|
||||
parentID: row.parent_id ? SessionSchema.ID.make(row.parent_id) : undefined,
|
||||
fork: row.fork_session_id
|
||||
|
|
@ -26,12 +26,12 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
|||
messageID: row.fork_message_id ? SessionMessage.ID.make(row.fork_message_id) : undefined,
|
||||
}
|
||||
: undefined,
|
||||
agent: row.agent ? AgentV2.ID.make(row.agent) : undefined,
|
||||
agent: row.agent ? Agent.ID.make(row.agent) : undefined,
|
||||
model: row.model
|
||||
? {
|
||||
id: ModelV2.ID.make(row.model.id),
|
||||
providerID: ProviderV2.ID.make(row.model.providerID),
|
||||
variant: ModelV2.VariantID.make(row.model.variant ?? "default"),
|
||||
id: Model.ID.make(row.model.id),
|
||||
providerID: Provider.ID.make(row.model.providerID),
|
||||
variant: Model.VariantID.make(row.model.variant ?? "default"),
|
||||
}
|
||||
: undefined,
|
||||
cost: Money.USD.make(row.cost),
|
||||
|
|
@ -46,7 +46,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
|||
},
|
||||
location: Location.Ref.make({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined,
|
||||
workspaceID: row.workspace_id ? Workspace.ID.make(row.workspace_id) : undefined,
|
||||
}),
|
||||
subpath: row.path ? RelativePath.make(row.path) : undefined,
|
||||
revert: row.revert ? decodeRevert(row.revert) : undefined,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export interface Interface {
|
|||
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<Instructions.Instructions>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionEntry") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/InstructionEntry") {}
|
||||
|
||||
const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2))
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ export * as InstructionState from "./instruction-state"
|
|||
import { and, asc, desc, eq, gt, inArray, lte, sql } from "drizzle-orm"
|
||||
import { DateTime, Effect, Option, Schema } from "effect"
|
||||
import type { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { EventTable } from "../event/sql"
|
||||
import { Instructions } from "../instructions/index"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionMessage } from "./message"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { InstructionBlobTable, InstructionStateTable, SessionTable } from "./sql"
|
||||
|
||||
|
|
@ -39,11 +40,11 @@ export const observe = Effect.fn("InstructionState.observe")(function* (
|
|||
|
||||
export const commit = Effect.fn("InstructionState.commit")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
observation: Observation,
|
||||
) {
|
||||
if (!observation.initial && Object.keys(observation.delta).length === 0) return
|
||||
yield* events.publish(
|
||||
yield* bus.publish(
|
||||
SessionEvent.InstructionsUpdated,
|
||||
{ sessionID: observation.sessionID, delta: observation.delta },
|
||||
{
|
||||
|
|
@ -56,11 +57,11 @@ export const commit = Effect.fn("InstructionState.commit")(function* (
|
|||
|
||||
export const prepare = Effect.fn("InstructionState.prepare")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
instructions: Instructions.Instructions,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
yield* commit(db, events, yield* observe(db, instructions, sessionID))
|
||||
yield* commit(db, bus, yield* observe(db, instructions, sessionID))
|
||||
})
|
||||
|
||||
export const apply = Effect.fn("InstructionState.apply")(function* (
|
||||
|
|
@ -171,7 +172,7 @@ const assembleState = Effect.fnUntraced(function* (
|
|||
result.push({
|
||||
seq: update.row.seq,
|
||||
message: SessionMessage.System.make({
|
||||
id: SessionMessage.ID.fromEvent(EventV2.ID.make(update.row.id)),
|
||||
id: SessionMessage.ID.fromEvent(Event.ID.make(update.row.id)),
|
||||
type: "system",
|
||||
text,
|
||||
time: { created: DateTime.makeUnsafe(update.row.created) },
|
||||
|
|
@ -310,16 +311,16 @@ function requireBlob(blobs: ReadonlyMap<Instructions.Hash, Schema.Json>, hash: I
|
|||
return value
|
||||
}
|
||||
|
||||
const instructionEventType = EventV2.versionedType(
|
||||
const instructionEventType = Bus.versionedType(
|
||||
SessionEvent.InstructionsUpdated.type,
|
||||
SessionEvent.InstructionsUpdated.durable.version,
|
||||
)
|
||||
const compactionEventType = EventV2.versionedType(
|
||||
const compactionEventType = Bus.versionedType(
|
||||
SessionEvent.Compaction.Ended.type,
|
||||
SessionEvent.Compaction.Ended.durable.version,
|
||||
)
|
||||
const movedEventType = EventV2.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version)
|
||||
const revertedEventType = EventV2.versionedType(
|
||||
const movedEventType = Bus.versionedType(SessionEvent.Moved.type, SessionEvent.Moved.durable.version)
|
||||
const revertedEventType = Bus.versionedType(
|
||||
SessionEvent.RevertEvent.Committed.type,
|
||||
SessionEvent.RevertEvent.Committed.durable.version,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export * as SessionInstructions from "./instructions"
|
|||
import { relative } from "path"
|
||||
import { Context, DateTime, Effect, Layer, Option, Ref, Schema } from "effect"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Location } from "../location"
|
||||
import { SessionEvent } from "./event"
|
||||
|
|
@ -23,12 +23,12 @@ export interface Interface {
|
|||
}) => Effect.Effect<void, MessageDecodeError | FSUtil.Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionInstructions") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionInstructions") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const location = yield* Location.Service
|
||||
|
|
@ -67,12 +67,12 @@ const layer = Layer.effect(
|
|||
)
|
||||
const readable = files.filter((file): file is { path: string; content: string } => file !== undefined)
|
||||
if (readable.length === 0) return
|
||||
// Publish directly rather than through SessionV2.synthetic: a Location-scoped layer
|
||||
// cannot depend on SessionV2 (it routes through LocationServiceMap, forming a type
|
||||
// Publish directly rather than through Session.synthetic: a Location-scoped layer
|
||||
// cannot depend on Session (it routes through LocationServiceMap, forming a type
|
||||
// cycle with this node). The durable publish is what makes the synthetic visible on
|
||||
// the next projected history reload. The dedup ledger lives on the synthetic message
|
||||
// metadata so it survives across Location layer restarts.
|
||||
yield* events.publish(SessionEvent.Synthetic, {
|
||||
yield* bus.publish(SessionEvent.Synthetic, {
|
||||
sessionID: input.sessionID,
|
||||
text: readable.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n"),
|
||||
description: `Loaded ${readable.map((file) => describePath(root, file.path)).join(", ")}`,
|
||||
|
|
@ -109,5 +109,5 @@ function describePath(root: string, path: string) {
|
|||
export const node = makeLocationNode({
|
||||
name: "session-instructions",
|
||||
layer,
|
||||
deps: [EventV2.node, FSUtil.node, Location.node, SessionStore.node],
|
||||
deps: [Bus.node, FSUtil.node, Location.node, SessionStore.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
export * as SessionModelRequest from "./model-request"
|
||||
|
||||
import { LLM, Message, SystemPart, type LLMRequest, type ToolContent } from "@opencode-ai/ai"
|
||||
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||
import type { Content } from "@opencode-ai/schema/tool"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Cause, Context, Effect, Layer, Result } from "effect"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { App } from "../app"
|
||||
import { ModelV2 } from "../model"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { Model } from "../model"
|
||||
import { Permission } from "../permission"
|
||||
import { PluginHooks } from "../plugin/hooks"
|
||||
import { QuestionTool } from "../tool/question"
|
||||
import { ToolOutputStore } from "../tool-output-store"
|
||||
import { ToolRegistry } from "../tool/registry"
|
||||
import { QuestionTool } from "../tool/plugin/question"
|
||||
import { Tool } from "../tool"
|
||||
import { SessionContext } from "./context"
|
||||
import { SessionModelHeaders } from "./model-headers"
|
||||
import { MAX_STEPS_PROMPT } from "./runner/max-steps"
|
||||
|
|
@ -18,16 +18,16 @@ import PROMPT_DEFAULT from "./runner/prompt/base.txt"
|
|||
import { toLLMMessages } from "./runner/to-llm-message"
|
||||
|
||||
/** Failures a prepared execution can surface: infrastructure errors plus user declines resurfaced from the defect tunnel. */
|
||||
export type ExecuteError = ToolOutputStore.Error | PermissionV2.DeclinedError | QuestionTool.CancelledError
|
||||
export type ExecuteError = Tool.Error | Permission.DeclinedError | QuestionTool.CancelledError
|
||||
|
||||
// User declines dive under the leaves' blanket `mapError` as defects (the deliberate
|
||||
// tunnel entered in PermissionV2.assert and the question tool), so a user's "no" can
|
||||
// tunnel entered in Permission.assert and the question tool), so a user's "no" can
|
||||
// never become model-facing tool output. They resurface as typed failures exactly once,
|
||||
// here at the seam the runner executes through.
|
||||
const declineDefect = (cause: Cause.Cause<ToolOutputStore.Error>) => {
|
||||
const declineDefect = (cause: Cause.Cause<Tool.Error>) => {
|
||||
const decline = cause.reasons.flatMap((reason) =>
|
||||
Cause.isDieReason(reason) &&
|
||||
(reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError)
|
||||
(reason.defect instanceof Permission.DeclinedError || reason.defect instanceof QuestionTool.CancelledError)
|
||||
? [reason.defect]
|
||||
: [],
|
||||
)[0]
|
||||
|
|
@ -41,8 +41,8 @@ interface Prepared {
|
|||
* step-limit-violating calls fail individually through the same seam.
|
||||
*/
|
||||
readonly executeTool: (
|
||||
input: ToolRegistry.ExecuteInput,
|
||||
) => Effect.Effect<ToolRegistry.ToolOutcome, ExecuteError>
|
||||
input: Parameters<Tool.Snapshot["execute"]>[0],
|
||||
) => Effect.Effect<Tool.Result, ExecuteError>
|
||||
/** True when this request is the final Step; violating calls are rejected and no continuation follows. */
|
||||
readonly stepLimitReached: boolean
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ const mimeToModality = (mime: string) => {
|
|||
if (mime === "application/pdf") return "pdf"
|
||||
}
|
||||
|
||||
const unsupportedMedia = (mime: string, name: string | undefined, capabilities: ModelV2.Capabilities) => {
|
||||
const unsupportedMedia = (mime: string, name: string | undefined, capabilities: Model.Capabilities) => {
|
||||
const modality = mimeToModality(mime)
|
||||
if (!modality || capabilities.input.some((item) => item.startsWith(modality))) return
|
||||
return {
|
||||
|
|
@ -68,7 +68,7 @@ const unsupportedMedia = (mime: string, name: string | undefined, capabilities:
|
|||
}
|
||||
}
|
||||
|
||||
export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: ModelV2.Capabilities) =>
|
||||
export const unsupportedParts = (messages: LLMRequest["messages"], capabilities: Model.Capabilities) =>
|
||||
messages.map((message) =>
|
||||
Message.make({
|
||||
...message,
|
||||
|
|
@ -81,7 +81,7 @@ export const unsupportedParts = (messages: LLMRequest["messages"], capabilities:
|
|||
...part,
|
||||
result: {
|
||||
...part.result,
|
||||
value: part.result.value.map((item: ToolContent) => {
|
||||
value: part.result.value.map((item: Content) => {
|
||||
if (item.type !== "file") return item
|
||||
return unsupportedMedia(item.mime, item.name, capabilities) ?? item
|
||||
}),
|
||||
|
|
@ -102,7 +102,7 @@ export interface Interface {
|
|||
}
|
||||
|
||||
/** Location-scoped outbound model-request preparation. */
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionModelRequest") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionModelRequest") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
|
|
@ -119,14 +119,14 @@ export const layer = Layer.effect(
|
|||
const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps
|
||||
// The final Step keeps definitions available to protocols with native "none",
|
||||
// preserving their prompt cache prefix. Calls are still rejected at execution.
|
||||
const toolSet = input.context.toolSet
|
||||
const tools = input.context.tools
|
||||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
||||
const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial]
|
||||
.filter((part) => part.length > 0)
|
||||
.map(SystemPart.make)
|
||||
const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey)
|
||||
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
|
||||
const toolDefinitions = toolSet.definitions
|
||||
const toolDefinitions = tools.definitions
|
||||
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
||||
// Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit.
|
||||
const contextEvent = yield* hooks.trigger("session", "context", {
|
||||
|
|
@ -142,7 +142,7 @@ export const layer = Layer.effect(
|
|||
const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
|
||||
const registered = toolsByName.get(name)
|
||||
return registered
|
||||
? [Object.assign({}, registered, { description: tool.description, inputSchema: tool.input })]
|
||||
? [{ ...registered, description: tool.description, inputSchema: tool.input }]
|
||||
: []
|
||||
})
|
||||
const request = LLM.request({
|
||||
|
|
@ -158,16 +158,10 @@ export const layer = Layer.effect(
|
|||
})
|
||||
const executeTool: Prepared["executeTool"] = (executeInput) => {
|
||||
if (stepLimitReached)
|
||||
return Effect.succeed({
|
||||
status: "error",
|
||||
error: { type: "tool.execution", message: "Tools are disabled after the maximum agent steps" },
|
||||
})
|
||||
return new Tool.Error({ message: "Tools are disabled after the maximum agent steps" })
|
||||
if (toolsByName.has(executeInput.call.name) && !Object.hasOwn(contextEvent.tools, executeInput.call.name))
|
||||
return Effect.succeed({
|
||||
status: "error",
|
||||
error: { type: "tool.unknown", message: `Tool is not available for this request: ${executeInput.call.name}` },
|
||||
})
|
||||
return toolSet
|
||||
return new Tool.Error({ message: `Tool is not available for this request: ${executeInput.call.name}` })
|
||||
return tools
|
||||
.execute(executeInput)
|
||||
.pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "@opencode-ai/schema/session-pending"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import type { Database } from "../database/database"
|
||||
import type { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { EventTable } from "../event/sql"
|
||||
import { KeyedMutex } from "../effect/keyed-mutex"
|
||||
import { SessionEvent } from "./event"
|
||||
|
|
@ -38,7 +38,7 @@ const encodeUser = Schema.encodeSync(UserData)
|
|||
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
|
||||
const encodeSynthetic = Schema.encodeSync(SyntheticData)
|
||||
const decodeAdmittedEvent = Schema.decodeUnknownOption(SessionEvent.InputAdmitted.data)
|
||||
const admittedEventType = Event.versionedType(
|
||||
const admittedEventType = Bus.versionedType(
|
||||
SessionEvent.InputAdmitted.type,
|
||||
SessionEvent.InputAdmitted.durable.version,
|
||||
)
|
||||
|
|
@ -150,7 +150,7 @@ const promotedFromHistory = Effect.fn("SessionPending.promotedFromHistory")(func
|
|||
|
||||
export const admit = Effect.fn("SessionPending.admit")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
request: {
|
||||
readonly id: SessionMessage.ID
|
||||
readonly sessionID: SessionSchema.ID
|
||||
|
|
@ -164,7 +164,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* (
|
|||
}
|
||||
const promoted = yield* promotedFromHistory(db, request.sessionID, request.id)
|
||||
if (promoted !== undefined) return promoted
|
||||
return yield* events
|
||||
return yield* bus
|
||||
.publish(SessionEvent.InputAdmitted, {
|
||||
inputID: request.id,
|
||||
sessionID: request.sessionID,
|
||||
|
|
@ -198,7 +198,7 @@ export const admit = Effect.fn("SessionPending.admit")(function* (
|
|||
|
||||
export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID },
|
||||
) {
|
||||
return yield* inboxLocks.withLock(input.sessionID)(
|
||||
|
|
@ -210,7 +210,7 @@ export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(funct
|
|||
}
|
||||
const pending = yield* compaction(db, input.sessionID)
|
||||
if (pending) return pending
|
||||
return yield* events
|
||||
return yield* bus
|
||||
.publish(SessionEvent.Compaction.Admitted, {
|
||||
inputID: input.id,
|
||||
sessionID: input.sessionID,
|
||||
|
|
@ -413,7 +413,7 @@ export const equivalent = (
|
|||
|
||||
const publish = Effect.fn("SessionPending.publish")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
rows: ReadonlyArray<typeof SessionPendingTable.$inferSelect>,
|
||||
) {
|
||||
|
|
@ -423,7 +423,7 @@ const publish = Effect.fn("SessionPending.publish")(function* (
|
|||
(row) => {
|
||||
const entry = fromRow(row)
|
||||
if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id }))
|
||||
return events
|
||||
return bus
|
||||
.publish(SessionEvent.InputPromoted, {
|
||||
sessionID,
|
||||
inputID: entry.id,
|
||||
|
|
@ -450,7 +450,7 @@ const publish = Effect.fn("SessionPending.publish")(function* (
|
|||
*/
|
||||
export const promote = Effect.fn("SessionPending.promote")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
scope: Promotable,
|
||||
) {
|
||||
|
|
@ -464,7 +464,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* (
|
|||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
if (steers.length > 0 || scope === "steer") return yield* publish(db, events, sessionID, steers)
|
||||
if (steers.length > 0 || scope === "steer") return yield* publish(db, bus, sessionID, steers)
|
||||
|
||||
const queued = yield* db
|
||||
.select()
|
||||
|
|
@ -475,7 +475,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* (
|
|||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!queued) return 0
|
||||
const promoted = yield* publish(db, events, sessionID, [queued])
|
||||
const promoted = yield* publish(db, bus, sessionID, [queued])
|
||||
const arrivedSteers = yield* db
|
||||
.select()
|
||||
.from(SessionPendingTable)
|
||||
|
|
@ -483,7 +483,7 @@ export const promote = Effect.fn("SessionPending.promote")(function* (
|
|||
.orderBy(asc(SessionPendingTable.admitted_seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return promoted + (yield* publish(db, events, sessionID, arrivedSteers))
|
||||
return promoted + (yield* publish(db, bus, sessionID, arrivedSteers))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ export * as SessionProjector from "./projector"
|
|||
import { and, asc, desc, eq, gt, gte, inArray, lt, sql } from "drizzle-orm"
|
||||
import { DateTime, Effect, Layer, Schema, Stream } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { ModelV2 } from "../model"
|
||||
import { Model } from "../model"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionV1 } from "../v1/session"
|
||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionMessageUpdater } from "./message-updater"
|
||||
import { SessionPending } from "./pending"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { Workspace } from "../workspace"
|
||||
import { InstructionState } from "./instruction-state"
|
||||
import { MessageTable, PartTable, SessionPendingTable, SessionMessageTable, SessionTable } from "./sql"
|
||||
import type { DeepMutable } from "../schema"
|
||||
|
|
@ -134,7 +134,7 @@ function applyUsage(
|
|||
|
||||
const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
bus: Bus.Interface,
|
||||
sessionID: (typeof SessionEvent.Step.Ended.Type)["data"]["sessionID"],
|
||||
) {
|
||||
const row = yield* db
|
||||
|
|
@ -151,7 +151,7 @@ const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function*
|
|||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return
|
||||
yield* events.publish(SessionEvent.UsageUpdated, {
|
||||
yield* bus.publish(SessionEvent.UsageUpdated, {
|
||||
sessionID,
|
||||
cost: Money.USD.make(row.cost),
|
||||
tokens: {
|
||||
|
|
@ -314,7 +314,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
|
||||
cursor = rows.at(-1)!.seq
|
||||
}
|
||||
yield* EventV2.reserveSequence(db, event.data.sessionID, event.data.parentSeq)
|
||||
yield* Bus.reserveSequence(db, event.data.sessionID, event.data.parentSeq)
|
||||
yield* InstructionState.rebuild(db, event.data.sessionID)
|
||||
})
|
||||
|
||||
|
|
@ -349,7 +349,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
.get()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(ModelV2.Ref)(row.model) : undefined)),
|
||||
Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(Model.Ref)(row.model) : undefined)),
|
||||
)
|
||||
},
|
||||
getCurrentAssistant() {
|
||||
|
|
@ -460,9 +460,9 @@ function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, me
|
|||
|
||||
const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const db = (yield* Database.Service).db
|
||||
yield* events.project(SessionV1.Event.Created, (event) =>
|
||||
yield* bus.project(SessionV1.Event.Created, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const stored = yield* db
|
||||
.insert(SessionTable)
|
||||
|
|
@ -482,7 +482,7 @@ const layer = Layer.effectDiscard(
|
|||
}
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.Updated, (event) =>
|
||||
yield* bus.project(SessionV1.Event.Updated, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set(sessionRow(event.data.info))
|
||||
|
|
@ -490,7 +490,7 @@ const layer = Layer.effectDiscard(
|
|||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* events.project(SessionEvent.Moved, (event) =>
|
||||
yield* bus.project(SessionEvent.Moved, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* db
|
||||
.update(SessionTable)
|
||||
|
|
@ -498,7 +498,7 @@ const layer = Layer.effectDiscard(
|
|||
directory: event.data.location.directory,
|
||||
path: event.data.subpath,
|
||||
...(event.data.projectID ? { project_id: event.data.projectID } : {}),
|
||||
workspace_id: event.data.location.workspaceID ? WorkspaceV2.ID.make(event.data.location.workspaceID) : null,
|
||||
workspace_id: event.data.location.workspaceID ? Workspace.ID.make(event.data.location.workspaceID) : null,
|
||||
time_updated: DateTime.toEpochMillis(event.created),
|
||||
})
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
|
|
@ -507,13 +507,13 @@ const layer = Layer.effectDiscard(
|
|||
yield* InstructionState.reset(db, event.data.sessionID)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.Deleted, (event) =>
|
||||
yield* bus.project(SessionV1.Event.Deleted, (event) =>
|
||||
db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie),
|
||||
)
|
||||
yield* events.project(SessionEvent.Deleted, (event) =>
|
||||
yield* bus.project(SessionEvent.Deleted, (event) =>
|
||||
db.delete(SessionTable).where(eq(SessionTable.id, event.data.sessionID)).run().pipe(Effect.orDie),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.MessageUpdated, (event) =>
|
||||
yield* bus.project(SessionV1.Event.MessageUpdated, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const time_created = event.data.info.time.created
|
||||
const id = event.data.info.id
|
||||
|
|
@ -527,7 +527,7 @@ const layer = Layer.effectDiscard(
|
|||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.MessageRemoved, (event) =>
|
||||
yield* bus.project(SessionV1.Event.MessageRemoved, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
|
|
@ -546,7 +546,7 @@ const layer = Layer.effectDiscard(
|
|||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.PartRemoved, (event) =>
|
||||
yield* bus.project(SessionV1.Event.PartRemoved, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const row = yield* db
|
||||
.select()
|
||||
|
|
@ -563,7 +563,7 @@ const layer = Layer.effectDiscard(
|
|||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.PartUpdated, (event) =>
|
||||
yield* bus.project(SessionV1.Event.PartUpdated, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const id = event.data.part.id
|
||||
const messageID = event.data.part.messageID
|
||||
|
|
@ -582,7 +582,7 @@ const layer = Layer.effectDiscard(
|
|||
if (next) yield* applyUsage(db, sessionID, next)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.AgentSelected, (event) =>
|
||||
yield* bus.project(SessionEvent.AgentSelected, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
|
|
@ -590,7 +590,7 @@ const layer = Layer.effectDiscard(
|
|||
.run()
|
||||
.pipe(Effect.orDie, Effect.andThen(run(db, event))),
|
||||
)
|
||||
yield* events.project(SessionEvent.ModelSelected, (event) =>
|
||||
yield* bus.project(SessionEvent.ModelSelected, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* run(db, event)
|
||||
yield* db
|
||||
|
|
@ -601,7 +601,7 @@ const layer = Layer.effectDiscard(
|
|||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Renamed, (event) =>
|
||||
yield* bus.project(SessionEvent.Renamed, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
|
|
@ -609,9 +609,9 @@ const layer = Layer.effectDiscard(
|
|||
.run()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
yield* events.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
|
||||
yield* events.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
||||
yield* events.project(SessionEvent.InputPromoted, (event) =>
|
||||
yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
|
||||
yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event))
|
||||
yield* bus.project(SessionEvent.InputPromoted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
|
|
@ -643,7 +643,7 @@ const layer = Layer.effectDiscard(
|
|||
)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.InputAdmitted, (event) =>
|
||||
yield* bus.project(SessionEvent.InputAdmitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
|
|
@ -656,7 +656,7 @@ const layer = Layer.effectDiscard(
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Compaction.Admitted, (event) =>
|
||||
yield* bus.project(SessionEvent.Compaction.Admitted, (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
|
|
@ -668,42 +668,42 @@ const layer = Layer.effectDiscard(
|
|||
})
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Execution.Failed, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Execution.Interrupted, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.InstructionsUpdated, (event) =>
|
||||
yield* bus.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Execution.Failed, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Execution.Interrupted, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.InstructionsUpdated, (event) =>
|
||||
InstructionState.apply(db, event.data.sessionID, event.durable.seq, event.data.delta),
|
||||
)
|
||||
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Skill.Activated, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Step.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Step.Ended, (event) =>
|
||||
yield* bus.project(SessionEvent.Synthetic, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Skill.Activated, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Shell.Started, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Shell.Ended, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Step.Started, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Step.Ended, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* run(db, event)
|
||||
yield* applyUsage(db, event.data.sessionID, event.data)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Step.Failed, (event) =>
|
||||
yield* bus.project(SessionEvent.Step.Failed, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* run(db, event)
|
||||
if (event.data.cost !== undefined && event.data.tokens !== undefined)
|
||||
yield* applyUsage(db, event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens })
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Text.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Text.Ended, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Tool.Input.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Tool.Called, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Tool.Success, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.RetryScheduled, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Compaction.Started, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Compaction.Ended, (event) =>
|
||||
yield* bus.project(SessionEvent.Text.Started, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Text.Ended, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Tool.Input.Started, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Tool.Input.Ended, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Tool.Called, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Tool.Success, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Tool.Failed, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Reasoning.Started, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Reasoning.Ended, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.RetryScheduled, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Compaction.Started, (event) => run(db, event))
|
||||
yield* bus.project(SessionEvent.Compaction.Ended, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* run(db, event)
|
||||
yield* InstructionState.advanceEpoch(db, event.data.sessionID, event.durable.seq)
|
||||
|
|
@ -713,7 +713,7 @@ const layer = Layer.effectDiscard(
|
|||
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Compaction.Failed, (event) =>
|
||||
yield* bus.project(SessionEvent.Compaction.Failed, (event) =>
|
||||
Effect.gen(function* () {
|
||||
yield* run(db, event)
|
||||
if (event.durable === undefined)
|
||||
|
|
@ -722,7 +722,7 @@ const layer = Layer.effectDiscard(
|
|||
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.RevertEvent.Staged, (event) =>
|
||||
yield* bus.project(SessionEvent.RevertEvent.Staged, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const revert = event.data.revert
|
||||
yield* db
|
||||
|
|
@ -736,7 +736,7 @@ const layer = Layer.effectDiscard(
|
|||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.RevertEvent.Cleared, (event) =>
|
||||
yield* bus.project(SessionEvent.RevertEvent.Cleared, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({ revert: null, time_updated: DateTime.toEpochMillis(event.created) })
|
||||
|
|
@ -744,7 +744,7 @@ const layer = Layer.effectDiscard(
|
|||
.run()
|
||||
.pipe(Effect.orDie, Effect.asVoid),
|
||||
)
|
||||
yield* events.project(SessionEvent.RevertEvent.Committed, (event) =>
|
||||
yield* bus.project(SessionEvent.RevertEvent.Committed, (event) =>
|
||||
Effect.gen(function* () {
|
||||
const boundary = yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
|
|
@ -781,18 +781,18 @@ const layer = Layer.effectDiscard(
|
|||
yield* InstructionState.reset(db, event.data.sessionID)
|
||||
}),
|
||||
)
|
||||
yield* events.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe(
|
||||
yield* bus.subscribe([SessionEvent.Step.Ended, SessionEvent.Step.Failed, SessionEvent.UsageRecorded]).pipe(
|
||||
Stream.runForEach((event) => {
|
||||
if (
|
||||
event.type === SessionEvent.Step.Failed.type &&
|
||||
(event.data.cost === undefined || event.data.tokens === undefined)
|
||||
)
|
||||
return Effect.void
|
||||
return publishSessionUsage(db, events, event.data.sessionID)
|
||||
return publishSessionUsage(db, bus, event.data.sessionID)
|
||||
}),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeGlobalNode({ name: "session-projector", layer, deps: [EventV2.node, Database.node] })
|
||||
export const node = makeGlobalNode({ name: "session-projector", layer, deps: [Bus.node, Database.node] })
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export * as SessionRevert from "./revert"
|
|||
import { and, asc, eq, gt } from "drizzle-orm"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { RelativePath } from "../schema"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { SessionEvent } from "./event"
|
||||
|
|
@ -63,7 +63,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
|
|||
readonly files?: boolean
|
||||
}) {
|
||||
const snapshot = yield* Snapshot.Service
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const original = input.session.revert?.snapshot
|
||||
? Snapshot.ID.make(input.session.revert.snapshot)
|
||||
: yield* snapshot.capture()
|
||||
|
|
@ -83,7 +83,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
|
|||
snapshot: original,
|
||||
files,
|
||||
} satisfies SessionSchema.Info["revert"]
|
||||
yield* events.publish(SessionEvent.RevertEvent.Staged, {
|
||||
yield* bus.publish(SessionEvent.RevertEvent.Staged, {
|
||||
sessionID: input.session.id,
|
||||
revert,
|
||||
})
|
||||
|
|
@ -98,16 +98,16 @@ export const clear = Effect.fn("SessionRevert.clear")(function* (session: Sessio
|
|||
yield* snapshot.restore({
|
||||
files: new Map((session.revert.files ?? []).map((file) => [RelativePath.make(file.file), original])),
|
||||
})
|
||||
const events = yield* EventV2.Service
|
||||
yield* events.publish(SessionEvent.RevertEvent.Cleared, {
|
||||
const bus = yield* Bus.Service
|
||||
yield* bus.publish(SessionEvent.RevertEvent.Cleared, {
|
||||
sessionID: session.id,
|
||||
})
|
||||
})
|
||||
|
||||
export const commit = Effect.fn("SessionRevert.commit")(function* (session: SessionSchema.Info) {
|
||||
if (!session.revert) return
|
||||
const events = yield* EventV2.Service
|
||||
yield* events.publish(SessionEvent.RevertEvent.Committed, {
|
||||
const bus = yield* Bus.Service
|
||||
yield* bus.publish(SessionEvent.RevertEvent.Committed, {
|
||||
sessionID: session.id,
|
||||
to: session.revert.messageID,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { SessionSchema } from "../schema"
|
|||
import type { AgentNotFoundError, MessageDecodeError, StepFailedError, UserInterruptedError } from "../error"
|
||||
import { SessionRunnerModel } from "./model"
|
||||
import type { Instructions } from "../../instructions/index"
|
||||
import type { ToolOutputStore } from "../../tool-output-store"
|
||||
|
||||
export type RunError =
|
||||
| LLMError
|
||||
|
|
@ -16,7 +15,6 @@ export type RunError =
|
|||
| StepFailedError
|
||||
| UserInterruptedError
|
||||
| Instructions.InitializationBlocked
|
||||
| ToolOutputStore.Error
|
||||
|
||||
/** Runs one local continuation from already-recorded Session history. */
|
||||
export interface Interface {
|
||||
|
|
@ -27,4 +25,4 @@ export interface Interface {
|
|||
}) => Effect.Effect<void, RunError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunner") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunner") {}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ export * as SessionRunnerLLM from "./llm"
|
|||
import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent, type ToolCall } from "@opencode-ai/ai"
|
||||
import { Cause, Data, Effect, Exit, Fiber, FiberSet, Layer, Option, Pull, Schedule, Stream } from "effect"
|
||||
import { Database } from "../../database/database"
|
||||
import { EventV2 } from "../../event"
|
||||
import { PermissionV2 } from "../../permission"
|
||||
import { QuestionTool } from "../../tool/question"
|
||||
import { ToolOutputStore } from "../../tool-output-store"
|
||||
import { Bus } from "../../bus"
|
||||
import { Permission } from "../../permission"
|
||||
import { QuestionTool } from "../../tool/plugin/question"
|
||||
import { InstructionState } from "../instruction-state"
|
||||
import { SessionCompaction } from "../compaction"
|
||||
import { SessionContext } from "../context"
|
||||
|
|
@ -38,8 +37,8 @@ const CallOutcome = Data.taggedEnum<CallOutcome>()
|
|||
// Declining an interactive prompt halts the drain instead of becoming model-facing tool output.
|
||||
const isDecline = (
|
||||
error: SessionModelRequest.ExecuteError,
|
||||
): error is PermissionV2.DeclinedError | QuestionTool.CancelledError =>
|
||||
error._tag === "PermissionV2.DeclinedError" || error._tag === "QuestionTool.CancelledError"
|
||||
): error is Permission.DeclinedError | QuestionTool.CancelledError =>
|
||||
error._tag === "Permission.DeclinedError" || error._tag === "QuestionTool.CancelledError"
|
||||
|
||||
/**
|
||||
* Classifies how the owned tool fibers ended. Interrupts abort the step; a user decline
|
||||
|
|
@ -66,8 +65,8 @@ const classifyToolExits = (
|
|||
// drain's error channel never carries a decline.
|
||||
const failure = causes.flatMap((cause) => {
|
||||
if (Cause.hasInterrupts(cause)) return []
|
||||
const reasons = cause.reasons.flatMap((reason): Array<Cause.Reason<ToolOutputStore.Error>> =>
|
||||
Cause.isFailReason(reason) ? (isDecline(reason.error) ? [] : [Cause.makeFailReason(reason.error)]) : [reason],
|
||||
const reasons = cause.reasons.flatMap((reason): Array<Cause.Reason<never>> =>
|
||||
Cause.isFailReason(reason) ? [] : [reason],
|
||||
)
|
||||
return reasons.length > 0 ? [Cause.fromReasons(reasons)] : []
|
||||
}).at(0)
|
||||
|
|
@ -75,7 +74,6 @@ const classifyToolExits = (
|
|||
interrupted: causes.some(Cause.hasInterrupts),
|
||||
declines,
|
||||
failure,
|
||||
infraError: failure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(failure)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +84,7 @@ const RESULT_MISSING = { type: "tool.result-missing", message: "Provider did not
|
|||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const llm = yield* LLMClient.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const context = yield* SessionContext.Service
|
||||
|
|
@ -146,7 +144,7 @@ const layer = Layer.effect(
|
|||
// compaction boundary, so the rebuilt step needs identity inside the new epoch.
|
||||
let assistantMessageID = SessionMessage.ID.create()
|
||||
const retry = yield* Schedule.toStepWithSleep(
|
||||
SessionRunnerRetry.schedule(events, sessionID, () => assistantMessageID),
|
||||
SessionRunnerRetry.schedule(bus, sessionID, () => assistantMessageID),
|
||||
)
|
||||
/**
|
||||
* Consumes one retry allowance: sleeps the scheduled backoff, or publishes
|
||||
|
|
@ -157,7 +155,7 @@ const layer = Layer.effect(
|
|||
retry(failure).pipe(
|
||||
Effect.as(CallOutcome.Retry({ step: failure.step })),
|
||||
Pull.catchDone(() =>
|
||||
events
|
||||
bus
|
||||
.publish(SessionEvent.Step.Failed, {
|
||||
sessionID,
|
||||
assistantMessageID,
|
||||
|
|
@ -203,8 +201,8 @@ const layer = Layer.effect(
|
|||
const selected = yield* context.select(sessionID)
|
||||
// Establish what the model knows before admitting what the user said, so
|
||||
// a blocked first step leaves pending inputs untouched.
|
||||
yield* InstructionState.prepare(db, events, selected.instructions, selected.session.id)
|
||||
const promoted = promotable ? yield* SessionPending.promote(db, events, selected.session.id, promotable) : 0
|
||||
yield* InstructionState.prepare(db, bus, selected.instructions, selected.session.id)
|
||||
const promoted = promotable ? yield* SessionPending.promote(db, bus, selected.session.id, promotable) : 0
|
||||
// Promoted input opens a fresh step allowance.
|
||||
const currentStep = promoted > 0 ? 1 : step
|
||||
const loaded = yield* context.load(selected)
|
||||
|
|
@ -231,7 +229,7 @@ const layer = Layer.effect(
|
|||
}> = []
|
||||
const interruptTools = Effect.suspend(() => Fiber.interruptAll(toolRuns.map((run) => run.fiber)))
|
||||
const startSnapshot = yield* snapshots.capture()
|
||||
const publisher = createLLMEventPublisher(events, {
|
||||
const publisher = createLLMEventPublisher(bus, {
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
// The selected catalog identity, not model.id: route-level ids are provider API
|
||||
|
|
@ -260,7 +258,7 @@ const layer = Layer.effect(
|
|||
const publishStepEnd = (finish: NonNullable<StepRecord["finish"]>) =>
|
||||
Effect.gen(function* () {
|
||||
const end = yield* captureStepEnd()
|
||||
yield* events.publish(SessionEvent.Step.Ended, {
|
||||
yield* bus.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: session.id,
|
||||
assistantMessageID: yield* publisher.startAssistant(),
|
||||
finish: finish.finish,
|
||||
|
|
@ -310,6 +308,9 @@ const layer = Layer.effect(
|
|||
// The fiber owns its call: it publishes its own completion, masked so a
|
||||
// finished execution always reaches its durable settlement.
|
||||
Effect.flatMap((outcome) => publisher.toolExecution(event.id, event.name, outcome)),
|
||||
Effect.catchTag("Tool.Error", (error) =>
|
||||
publisher.failTool(event.id, toSessionError(error)).pipe(Effect.asVoid),
|
||||
),
|
||||
),
|
||||
).pipe(Effect.forkScoped),
|
||||
})
|
||||
|
|
@ -386,9 +387,8 @@ const layer = Layer.effect(
|
|||
yield* publisher.failAssistant(STEP_INTERRUPTED)
|
||||
}
|
||||
if (tools.failure !== undefined) {
|
||||
const error = toSessionError(tools.infraError ?? Cause.squash(tools.failure))
|
||||
const error = toSessionError(Cause.squash(tools.failure))
|
||||
yield* publisher.failUnsettledTools(error)
|
||||
if (tools.infraError !== undefined) yield* publisher.failAssistant(error)
|
||||
}
|
||||
// Local calls have joined, so the remaining sweeps only close hosted calls the
|
||||
// provider promised but never resolved.
|
||||
|
|
@ -413,8 +413,7 @@ const layer = Layer.effect(
|
|||
|
||||
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
|
||||
if (tools.declines.length > 0) return yield* Effect.interrupt
|
||||
if ((tools.interrupted || tools.infraError !== undefined) && tools.failure)
|
||||
return yield* Effect.failCause(tools.failure)
|
||||
if (tools.interrupted && tools.failure) return yield* Effect.failCause(tools.failure)
|
||||
if (tools.interrupted && joined._tag === "Failure") return yield* Effect.failCause(joined.cause)
|
||||
if (record.failure) return yield* new StepFailedError({ error: record.failure })
|
||||
return CallOutcome.Completed({
|
||||
|
|
@ -450,7 +449,7 @@ const layer = Layer.effect(
|
|||
if (Exit.isSuccess(compacted)) return
|
||||
const unsettled = yield* SessionPending.compaction(db, sessionID)
|
||||
if (unsettled)
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
yield* bus.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
reason: "manual",
|
||||
error: Cause.hasInterruptsOnly(compacted.cause)
|
||||
|
|
@ -471,7 +470,7 @@ const layer = Layer.effect(
|
|||
if (message.type !== "assistant") continue
|
||||
for (const tool of message.content) {
|
||||
if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID,
|
||||
assistantMessageID: message.id,
|
||||
callID: tool.id,
|
||||
|
|
@ -503,7 +502,7 @@ export const node = makeLocationNode({
|
|||
service: Service,
|
||||
layer,
|
||||
deps: [
|
||||
EventV2.node,
|
||||
Bus.node,
|
||||
llmClient,
|
||||
SessionContext.node,
|
||||
SessionModelRequest.node,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import { Model } from "@opencode-ai/ai"
|
|||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { Catalog } from "../../catalog"
|
||||
import { ModelResolver } from "../../model-resolver"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { Capabilities, ID, Info, Ref, VariantID } from "../../model"
|
||||
import { Provider } from "../../provider"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
||||
export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelectedError>()(
|
||||
|
|
@ -20,7 +20,7 @@ export class ModelNotSelectedError extends Schema.TaggedErrorClass<ModelNotSelec
|
|||
|
||||
export class ModelUnavailableError extends Schema.TaggedErrorClass<ModelUnavailableError>()(
|
||||
"SessionRunnerModel.ModelUnavailableError",
|
||||
{ providerID: ProviderV2.ID, modelID: ModelV2.ID },
|
||||
{ providerID: Provider.ID, modelID: ID },
|
||||
) {
|
||||
override get message() {
|
||||
return `Model unavailable: ${this.providerID}/${this.modelID}`
|
||||
|
|
@ -38,21 +38,21 @@ export interface Interface {
|
|||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunnerModel") {}
|
||||
|
||||
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
||||
export const resolved = (
|
||||
model: Model,
|
||||
options: {
|
||||
readonly capabilities: ModelV2.Capabilities
|
||||
readonly variant?: ModelV2.VariantID
|
||||
readonly cost: ModelV2.Info["cost"]
|
||||
readonly capabilities: Capabilities
|
||||
readonly variant?: VariantID
|
||||
readonly cost: Info["cost"]
|
||||
},
|
||||
): Resolved => ({
|
||||
model,
|
||||
ref: ModelV2.Ref.make({
|
||||
id: ModelV2.ID.make(model.id),
|
||||
providerID: ProviderV2.ID.make(model.provider),
|
||||
ref: Ref.make({
|
||||
id: ID.make(model.id),
|
||||
providerID: Provider.ID.make(model.provider),
|
||||
...(options.variant === undefined ? {} : { variant: options.variant }),
|
||||
}),
|
||||
capabilities: options.capabilities,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import { type LLMEvent, type ProviderMetadata, type ToolResultValue } from "@opencode-ai/ai"
|
||||
import { Effect } from "effect"
|
||||
import { EventV2 } from "../../event"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { Bus } from "../../bus"
|
||||
import { Model } from "../../model"
|
||||
import { SessionEvent } from "../event"
|
||||
import { SessionMessage } from "../message"
|
||||
import { SessionSchema } from "../schema"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { AgentV2 } from "../../agent"
|
||||
import { Agent } from "../../agent"
|
||||
import { Snapshot } from "../../snapshot"
|
||||
import { RelativePath } from "../../schema"
|
||||
import { SessionUsage } from "../usage"
|
||||
import { Tool } from "../../tool/tool"
|
||||
import type { ToolRegistry } from "../../tool/registry"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
|
||||
type Input = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly agent: AgentV2.ID
|
||||
readonly model: ModelV2.Ref
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly providerMetadataKey: string
|
||||
readonly snapshot?: Snapshot.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
|
|
@ -48,12 +47,26 @@ export interface StepRecord {
|
|||
}
|
||||
|
||||
/** Derives canonical model content from a provider-hosted tool result. */
|
||||
const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => {
|
||||
type NonEmptyContent = readonly [Tool.Content, ...Tool.Content[]]
|
||||
|
||||
const nonEmpty = (content: ReadonlyArray<Tool.Content>): NonEmptyContent | undefined =>
|
||||
content.length > 0 ? (content as NonEmptyContent) : undefined
|
||||
|
||||
const stringify = (value: unknown) => {
|
||||
if (typeof value === "string") return value
|
||||
try {
|
||||
return JSON.stringify(value) ?? String(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
const hostedContent = (result: ToolResultValue): NonEmptyContent => {
|
||||
if (result.type === "content") {
|
||||
const content = Tool.nonEmpty(result.value)
|
||||
const content = nonEmpty(result.value)
|
||||
if (content !== undefined) return content
|
||||
}
|
||||
return [{ type: "text", text: Tool.stringify(result.value) }]
|
||||
return [{ type: "text", text: stringify(result.value) }]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,7 +80,7 @@ const hostedContent = (result: ToolResultValue): Tool.NonEmptyContent => {
|
|||
* order: each publishing fiber is sequential, so per-source order holds by construction,
|
||||
* and consumers fold by callID/ordinal rather than global position.
|
||||
*/
|
||||
export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish">, input: Input) => {
|
||||
export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, input: Input) => {
|
||||
const tools = new Map<
|
||||
string,
|
||||
{
|
||||
|
|
@ -76,10 +89,10 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
called: boolean
|
||||
settled: boolean
|
||||
providerExecuted: boolean
|
||||
progress?: ToolRegistry.Progress
|
||||
progress?: Tool.Metadata
|
||||
}
|
||||
>()
|
||||
const failureSnapshot = (tool: { readonly progress?: ToolRegistry.Progress }) =>
|
||||
const failureSnapshot = (tool: { readonly progress?: Tool.Metadata }) =>
|
||||
tool.progress === undefined ? {} : { metadata: tool.progress }
|
||||
const assistantMessageID = input.assistantMessageID
|
||||
let stepStarted = false
|
||||
|
|
@ -92,7 +105,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
const startAssistant = Effect.fnUntraced(function* () {
|
||||
if (stepStarted) return assistantMessageID
|
||||
stepStarted = true
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
yield* bus.publish(SessionEvent.Step.Started, {
|
||||
sessionID: input.sessionID,
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
|
|
@ -151,7 +164,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
"text",
|
||||
(_textID, value, ordinal, state) =>
|
||||
Effect.gen(function* () {
|
||||
yield* events.publish(SessionEvent.Text.Ended, {
|
||||
yield* bus.publish(SessionEvent.Text.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
ordinal,
|
||||
|
|
@ -165,7 +178,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
"reasoning",
|
||||
(_reasoningID, value, ordinal, state) =>
|
||||
Effect.gen(function* () {
|
||||
yield* events.publish(SessionEvent.Reasoning.Ended, {
|
||||
yield* bus.publish(SessionEvent.Reasoning.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
ordinal,
|
||||
|
|
@ -179,7 +192,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
Effect.gen(function* () {
|
||||
const tool = tools.get(callID)
|
||||
if (!tool) return yield* Effect.die(new Error(`Tool input end before start: ${callID}`))
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
|
|
@ -209,7 +222,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
providerExecuted: event.providerExecuted === true,
|
||||
})
|
||||
yield* toolInput.start(event.id)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID,
|
||||
callID: event.id,
|
||||
|
|
@ -242,7 +255,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (toolInput.has(event.id)) yield* endToolInput(event, event.raw)
|
||||
tool.settled = true
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
|
|
@ -263,7 +276,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
const tool = tools.get(callID)
|
||||
if (!tool || tool.settled) return false
|
||||
tool.settled = true
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
|
|
@ -300,7 +313,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
if (stepFailed || stepFailure === undefined) return
|
||||
const assistantMessageID = yield* startAssistant()
|
||||
stepFailed = true
|
||||
yield* events.publish(SessionEvent.Step.Failed, {
|
||||
yield* bus.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID,
|
||||
error: stepFailure,
|
||||
|
|
@ -328,7 +341,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
case "text-start":
|
||||
outputStarted = true
|
||||
const startedTextOrdinal = yield* text.start(event.id, providerState(event.providerMetadata))
|
||||
yield* events.publish(SessionEvent.Text.Started, {
|
||||
yield* bus.publish(SessionEvent.Text.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
ordinal: startedTextOrdinal,
|
||||
|
|
@ -336,7 +349,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
return
|
||||
case "text-delta":
|
||||
const deltaTextOrdinal = yield* text.append(event.id, event.text, providerState(event.providerMetadata))
|
||||
yield* events.publish(SessionEvent.Text.Delta, {
|
||||
yield* bus.publish(SessionEvent.Text.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
ordinal: deltaTextOrdinal,
|
||||
|
|
@ -349,7 +362,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
case "reasoning-start":
|
||||
outputStarted = true
|
||||
const startedReasoningOrdinal = yield* reasoning.start(event.id, providerState(event.providerMetadata))
|
||||
yield* events.publish(SessionEvent.Reasoning.Started, {
|
||||
yield* bus.publish(SessionEvent.Reasoning.Started, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* startAssistant(),
|
||||
ordinal: startedReasoningOrdinal,
|
||||
|
|
@ -362,7 +375,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
event.text,
|
||||
providerState(event.providerMetadata),
|
||||
)
|
||||
yield* events.publish(SessionEvent.Reasoning.Delta, {
|
||||
yield* bus.publish(SessionEvent.Reasoning.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: yield* currentAssistantMessageID(),
|
||||
ordinal: deltaReasoningOrdinal,
|
||||
|
|
@ -383,7 +396,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
return yield* Effect.die(new Error(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (!toolInput.has(event.id)) return yield* Effect.die(new Error(`Tool input delta after end: ${event.id}`))
|
||||
yield* toolInput.append(event.id, event.text)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
||||
yield* bus.publish(SessionEvent.Tool.Input.Delta, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
|
|
@ -408,7 +421,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
|
||||
tool.called = true
|
||||
tool.providerExecuted = event.providerExecuted === true
|
||||
yield* events.publish(SessionEvent.Tool.Called, {
|
||||
yield* bus.publish(SessionEvent.Tool.Called, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
|
|
@ -434,18 +447,18 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
const executed = event.providerExecuted === true || tool.providerExecuted
|
||||
const resultState = providerState(event.providerMetadata)
|
||||
if (event.result.type === "error") {
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
error: { type: "tool.execution", message: Tool.stringify(event.result.value) },
|
||||
error: { type: "tool.execution", message: stringify(event.result.value) },
|
||||
...failureSnapshot(tool),
|
||||
executed,
|
||||
resultState,
|
||||
})
|
||||
return
|
||||
}
|
||||
yield* events.publish(SessionEvent.Tool.Success, {
|
||||
yield* bus.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
|
|
@ -462,7 +475,7 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
return yield* Effect.die(new Error(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`))
|
||||
if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool error: ${event.id}`))
|
||||
tool.settled = true
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
yield* bus.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
|
|
@ -495,12 +508,12 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
}
|
||||
})
|
||||
|
||||
const progress = Effect.fnUntraced(function* (callID: string, update: ToolRegistry.Progress) {
|
||||
const progress = Effect.fnUntraced(function* (callID: string, update: Tool.Metadata) {
|
||||
const tool = tools.get(callID)
|
||||
if (!tool?.called || tool.settled)
|
||||
return yield* Effect.die(new Error(`Tool progress outside running call: ${callID}`))
|
||||
tool.progress = update
|
||||
yield* events.publish(SessionEvent.Tool.Progress, {
|
||||
yield* bus.publish(SessionEvent.Tool.Progress, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
|
|
@ -512,42 +525,27 @@ export const createLLMEventPublisher = (events: Pick<EventV2.Interface, "publish
|
|||
const toolExecution = Effect.fnUntraced(function* (
|
||||
callID: string,
|
||||
name: string,
|
||||
execution: ToolRegistry.ToolOutcome,
|
||||
result: Tool.Result,
|
||||
) {
|
||||
const tool = tools.get(callID)
|
||||
if (!tool?.called) return yield* Effect.die(new Error(`Tool execution before call: ${callID}`))
|
||||
if (tool.name !== name)
|
||||
return yield* Effect.die(new Error(`Tool execution name changed for ${callID}: ${tool.name} -> ${name}`))
|
||||
if (tool.settled) {
|
||||
if (execution.status === "error") return
|
||||
return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`))
|
||||
}
|
||||
if (tool.settled) return yield* Effect.die(new Error(`Duplicate tool execution: ${callID}`))
|
||||
tool.settled = true
|
||||
if (execution.status === "completed") {
|
||||
yield* events.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
content: execution.content,
|
||||
...(execution.metadata === undefined ? {} : { metadata: execution.metadata }),
|
||||
executed: tool.providerExecuted,
|
||||
})
|
||||
return
|
||||
}
|
||||
// An execution-provided snapshot wins; otherwise fall back to retained progress.
|
||||
const snapshot =
|
||||
execution.content !== undefined || execution.metadata !== undefined
|
||||
? {
|
||||
...(execution.content === undefined ? {} : { content: execution.content }),
|
||||
...(execution.metadata === undefined ? {} : { metadata: execution.metadata }),
|
||||
}
|
||||
: failureSnapshot(tool)
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
const content =
|
||||
typeof result.content === "string"
|
||||
? [{ type: "text" as const, text: result.content }]
|
||||
: result.content === undefined
|
||||
? []
|
||||
: [...result.content]
|
||||
if (content.length === 0) return yield* Effect.die(new Error(`Tool execution has no content: ${callID}`))
|
||||
yield* bus.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: input.sessionID,
|
||||
assistantMessageID: tool.assistantMessageID,
|
||||
callID,
|
||||
error: execution.error,
|
||||
...snapshot,
|
||||
content: [content[0], ...content.slice(1)],
|
||||
...(result.metadata === undefined ? {} : { metadata: result.metadata }),
|
||||
executed: tool.providerExecuted,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export * as SessionRunnerRetry from "./retry"
|
|||
import { LLMError } from "@opencode-ai/ai"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Data, Duration, Effect, Schedule } from "effect"
|
||||
import { EventV2 } from "../../event"
|
||||
import { Bus } from "../../bus"
|
||||
import { SessionEvent } from "../event"
|
||||
import { SessionMessage } from "../message"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
|
@ -41,7 +41,7 @@ const retryAfter = (failure: RetryableFailure) => {
|
|||
return undefined
|
||||
}
|
||||
|
||||
export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) =>
|
||||
export const schedule = (bus: Bus.Interface, sessionID: SessionSchema.ID, assistantMessageID: () => SessionMessage.ID) =>
|
||||
Schedule.max([Schedule.exponential("2 seconds"), Schedule.recurs(4)]).pipe(
|
||||
Schedule.setInputType<RetryableFailure>(),
|
||||
Schedule.modifyDelay(({ input: failure, duration: delay }) => {
|
||||
|
|
@ -49,7 +49,7 @@ export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID,
|
|||
return Effect.succeed(minimum === undefined ? delay : Duration.max(delay, Duration.millis(minimum)))
|
||||
}),
|
||||
Schedule.tap((metadata) =>
|
||||
events.publish(SessionEvent.RetryScheduled, {
|
||||
bus.publish(SessionEvent.RetryScheduled, {
|
||||
sessionID,
|
||||
assistantMessageID: assistantMessageID(),
|
||||
attempt: metadata.attempt + 1,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Message, ToolCallPart, ToolResultPart, type ContentPart, type ProviderMetadata } from "@opencode-ai/ai"
|
||||
import { Option, Schema } from "effect"
|
||||
import type { ModelV2 } from "../../model"
|
||||
import type { Model } from "../../model"
|
||||
import { SessionMessage } from "../message"
|
||||
import type { FileAttachment } from "@opencode-ai/schema/prompt"
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
|
|||
}
|
||||
}
|
||||
|
||||
const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, providerMetadataKey: string) => {
|
||||
const assistant = (message: SessionMessage.Assistant, model: Model.Ref, providerMetadataKey: string) => {
|
||||
const sameProvider = String(message.model.providerID) === String(model.providerID)
|
||||
const sameModel = sameProvider && String(message.model.id) === String(model.id)
|
||||
const reuseProviderMetadata = sameModel && message.error === undefined
|
||||
|
|
@ -177,7 +177,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid
|
|||
]
|
||||
}
|
||||
|
||||
function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref, providerMetadataKey: string): Message[] {
|
||||
function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMetadataKey: string): Message[] {
|
||||
switch (message.type) {
|
||||
case "agent-switched":
|
||||
case "model-switched":
|
||||
|
|
@ -239,9 +239,9 @@ ${message.recent}
|
|||
}
|
||||
}
|
||||
|
||||
/** Translate projected V2 Session history into canonical @opencode-ai/ai context. */
|
||||
/** Translate projected Session history into canonical @opencode-ai/ai context. */
|
||||
export const toLLMMessages = (
|
||||
messages: readonly SessionMessage.Info[],
|
||||
model: ModelV2.Ref,
|
||||
model: Model.Ref,
|
||||
providerMetadataKey: string = model.providerID,
|
||||
) => messages.flatMap((message) => toLLMMessage(message, model, providerMetadataKey))
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import type { SessionMessage } from "./message"
|
|||
import type { SessionPending } from "./pending"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { PermissionV1 } from "../v1/permission"
|
||||
import { ProjectV2 } from "../project"
|
||||
import { Project } from "../project"
|
||||
import type { SessionSchema } from "./schema"
|
||||
import type { MessageID, PartID, SessionV1 } from "../v1/session"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { Workspace } from "../workspace"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { Instruction } from "@opencode-ai/schema/instruction"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
|
|
@ -26,10 +26,10 @@ export const SessionTable = sqliteTable(
|
|||
{
|
||||
id: text().$type<SessionSchema.ID>().primaryKey(),
|
||||
project_id: text()
|
||||
.$type<ProjectV2.ID>()
|
||||
.$type<Project.ID>()
|
||||
.notNull()
|
||||
.references(() => ProjectTable.id, { onDelete: "cascade" }),
|
||||
workspace_id: text().$type<WorkspaceV2.ID>(),
|
||||
workspace_id: text().$type<Workspace.ID>(),
|
||||
parent_id: text().$type<SessionSchema.ID>(),
|
||||
fork_session_id: text().$type<SessionSchema.ID>(),
|
||||
fork_message_id: text().$type<SessionMessage.ID>(),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export interface Interface {
|
|||
readonly suspend: (sessionIDs: Iterable<Session.ID>) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionStore") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionStore") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ export * as SessionTitle from "./title"
|
|||
|
||||
import { LLM, LLMClient, LLMError, LLMEvent, Message, type LLMRequest } from "@opencode-ai/ai"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Agent } from "../agent"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { Bus } from "../bus"
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { App } from "../app"
|
||||
import { llmClient } from "../effect/app-node-platform"
|
||||
|
|
@ -19,11 +19,11 @@ const MAX_LENGTH = 100
|
|||
|
||||
type Dependencies = {
|
||||
readonly app: App.Info
|
||||
readonly events: EventV2.Interface
|
||||
readonly bus: Bus.Interface
|
||||
readonly llm: {
|
||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||
}
|
||||
readonly agents: AgentV2.Interface
|
||||
readonly agents: Agent.Interface
|
||||
readonly models: SessionRunnerModel.Interface
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export interface Interface {
|
|||
readonly generateForFirstPrompt: (session: SessionSchema.Info) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionTitle") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionTitle") {}
|
||||
|
||||
const truncate = (value: string) => (value.length <= MAX_LENGTH ? value : `${value.slice(0, MAX_LENGTH - 3)}...`)
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ const make = (dependencies: Dependencies) => {
|
|||
if (session.parentID) return
|
||||
const firstUser = yield* SessionHistory.firstUserMessageIfOnly(db, session.id)
|
||||
if (!firstUser) return
|
||||
const agent = yield* dependencies.agents.get(AgentV2.ID.make("title"))
|
||||
const agent = yield* dependencies.agents.get(Agent.ID.make("title"))
|
||||
if (!agent) return
|
||||
const resolved = yield* (
|
||||
agent.model
|
||||
|
|
@ -57,7 +57,7 @@ const make = (dependencies: Dependencies) => {
|
|||
let usage: SessionUsage.Recorded | undefined
|
||||
const recordUsage = Effect.suspend(() =>
|
||||
usage
|
||||
? dependencies.events.publish(SessionEvent.UsageRecorded, {
|
||||
? dependencies.bus.publish(SessionEvent.UsageRecorded, {
|
||||
sessionID: session.id,
|
||||
source: "title",
|
||||
...usage,
|
||||
|
|
@ -96,7 +96,7 @@ const make = (dependencies: Dependencies) => {
|
|||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0)
|
||||
if (!title) return
|
||||
yield* dependencies.events.publish(SessionEvent.Renamed, {
|
||||
yield* dependencies.bus.publish(SessionEvent.Renamed, {
|
||||
sessionID: session.id,
|
||||
title: truncate(title),
|
||||
})
|
||||
|
|
@ -107,13 +107,13 @@ const make = (dependencies: Dependencies) => {
|
|||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const bus = yield* Bus.Service
|
||||
const llm = yield* LLMClient.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const agents = yield* Agent.Service
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const database = yield* Database.Service
|
||||
const app = yield* App.Metadata
|
||||
const title = make({ events, llm, agents, models, app })
|
||||
const title = make({ bus, llm, agents, models, app })
|
||||
return Service.of({
|
||||
generateForFirstPrompt: (session) => title.generateForFirstPrompt(database.db, session),
|
||||
})
|
||||
|
|
@ -123,5 +123,5 @@ export const layer = Layer.effect(
|
|||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [EventV2.node, llmClient, AgentV2.node, SessionRunnerModel.node, Database.node, App.node],
|
||||
deps: [Bus.node, llmClient, Agent.node, SessionRunnerModel.node, Database.node, App.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { LLMError, ToolFailure } from "@opencode-ai/ai"
|
||||
import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { QuestionV2 } from "../question"
|
||||
import { Permission } from "../permission"
|
||||
import { Question } from "../question"
|
||||
import { Integration } from "../integration"
|
||||
import { ToolOutputStore } from "../tool-output-store"
|
||||
import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "./error"
|
||||
import { SessionRunnerModel } from "./runner/model"
|
||||
|
||||
|
|
@ -37,9 +36,9 @@ export function toSessionError(cause: unknown): SessionError.Error {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (cause instanceof PermissionV2.BlockedError) return { type: "permission.rejected", message: cause.message }
|
||||
if (cause instanceof QuestionV2.RejectedError) return { type: "aborted", message: cause.message }
|
||||
if (cause instanceof ToolFailure || cause instanceof Tool.Failure) {
|
||||
if (cause instanceof Permission.BlockedError) return { type: "permission.rejected", message: cause.message }
|
||||
if (cause instanceof Question.RejectedError) return { type: "aborted", message: cause.message }
|
||||
if (cause instanceof ToolFailure || cause instanceof Tool.Error) {
|
||||
if (cause.error === undefined) return { type: "tool.execution", message: cause.message }
|
||||
// The canonical error is the sole model-visible representation, so a cause
|
||||
// with no message must not erase the tool's curated failure message.
|
||||
|
|
@ -57,6 +56,5 @@ export function toSessionError(cause: unknown): SessionError.Error {
|
|||
)
|
||||
return { type: "provider.no-route", message: cause.message }
|
||||
if (cause instanceof Integration.AuthorizationError) return { type: "provider.auth", message: cause.message }
|
||||
if (cause instanceof ToolOutputStore.StorageError) return { type: "unknown", message: cause.message }
|
||||
return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ export * as SessionUsage from "./usage"
|
|||
import type { Usage } from "@opencode-ai/ai"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import type { TokenUsage } from "@opencode-ai/schema/token-usage"
|
||||
import type { ModelV2 } from "../model"
|
||||
import type { Model } from "../model"
|
||||
|
||||
const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0)
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ export const tokens = (usage: Usage | undefined): TokenUsage.Info => ({
|
|||
})
|
||||
|
||||
// TODO(#35765): Use Copilot's reported billed amount once billing has a dedicated typed runtime contract.
|
||||
export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Info) {
|
||||
export function calculateCost(costs: Model.Info["cost"], usage: TokenUsage.Info) {
|
||||
const context = usage.input + usage.cache.read + usage.cache.write
|
||||
const tier = costs
|
||||
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
|
||||
|
|
@ -36,7 +36,7 @@ export function calculateCost(costs: ModelV2.Info["cost"], usage: TokenUsage.Inf
|
|||
|
||||
export type Recorded = { readonly tokens: TokenUsage.Info; readonly cost: Money.USD }
|
||||
|
||||
export const record = (usage: Usage | undefined, costs: ModelV2.Info["cost"]): Recorded => {
|
||||
export const record = (usage: Usage | undefined, costs: Model.Info["cost"]): Recorded => {
|
||||
const normalized = tokens(usage)
|
||||
return { tokens: normalized, cost: calculateCost(costs, normalized) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue