chore: merge dev
This commit is contained in:
@@ -3,6 +3,7 @@ import { Command } from "@/command"
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
||||
import type * as ACPError from "./error"
|
||||
@@ -10,7 +11,7 @@ import type * as ACPError from "./error"
|
||||
export type ModelOption = {
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly providerName: string
|
||||
readonly modelID: ProviderV2.ModelID
|
||||
readonly modelID: ModelV2.ID
|
||||
readonly modelName: string
|
||||
}
|
||||
|
||||
@@ -24,7 +25,7 @@ export type ModelVariants = NonNullable<Provider.Model["variants"]>
|
||||
|
||||
export type DefaultModel = {
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly modelID: ProviderV2.ModelID
|
||||
readonly modelID: ModelV2.ID
|
||||
}
|
||||
|
||||
export type Snapshot = {
|
||||
|
||||
@@ -42,6 +42,7 @@ import { ACPSession } from "./session"
|
||||
import { UsageService } from "./usage"
|
||||
import { ACPProfile } from "./profile"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import type { Command } from "@/command"
|
||||
|
||||
@@ -650,7 +651,7 @@ function makeUsageService(sdk: OpencodeClient) {
|
||||
const size = yield* contextLimit({
|
||||
directory: params.directory,
|
||||
providerID: ProviderV2.ID.make(message.providerID),
|
||||
modelID: ProviderV2.ModelID.make(message.modelID),
|
||||
modelID: ModelV2.ID.make(message.modelID),
|
||||
})
|
||||
if (!size) return
|
||||
|
||||
@@ -812,7 +813,7 @@ function selectDefaultModel(snapshot: Directory.Snapshot) {
|
||||
if (snapshot.defaultModel) return snapshot.defaultModel
|
||||
const model = snapshot.modelOptions[0]
|
||||
if (model) return { providerID: model.providerID, modelID: model.modelID }
|
||||
return { providerID: "unknown" as ProviderV2.ID, modelID: "unknown" as ProviderV2.ModelID }
|
||||
return { providerID: "unknown" as ProviderV2.ID, modelID: "unknown" as ModelV2.ID }
|
||||
}
|
||||
|
||||
function detectSlashCommand(parts: ReturnType<typeof promptContentToParts>) {
|
||||
@@ -872,7 +873,7 @@ function configOptions(snapshot: Directory.Snapshot, session: ConfigState) {
|
||||
function parseSelectedModel(snapshot: Directory.Snapshot, modelId: string) {
|
||||
const selected = parseModelSelection(modelId, Object.values(snapshot.providers))
|
||||
const provider = snapshot.providers[ProviderV2.ID.make(selected.model.providerID)]
|
||||
const model = provider?.models[ProviderV2.ModelID.make(selected.model.modelID)]
|
||||
const model = provider?.models[ModelV2.ID.make(selected.model.modelID)]
|
||||
if (!model) {
|
||||
return Effect.fail(
|
||||
new ACPError.InvalidModelError({
|
||||
@@ -1000,7 +1001,7 @@ function restoreFromMessages(messages: readonly MessageInfo[]) {
|
||||
)
|
||||
if (user?.model?.providerID && user.model.modelID) {
|
||||
return {
|
||||
model: { providerID: user.model.providerID as ProviderV2.ID, modelID: user.model.modelID as ProviderV2.ModelID },
|
||||
model: { providerID: user.model.providerID as ProviderV2.ID, modelID: user.model.modelID as ModelV2.ID },
|
||||
variant: user.model.variant,
|
||||
modeId: user.agent,
|
||||
}
|
||||
@@ -1009,7 +1010,7 @@ function restoreFromMessages(messages: readonly MessageInfo[]) {
|
||||
const assistant = messages.findLast((message) => message.providerID && message.modelID)
|
||||
if (assistant?.providerID && assistant.modelID) {
|
||||
return {
|
||||
model: { providerID: assistant.providerID as ProviderV2.ID, modelID: assistant.modelID as ProviderV2.ModelID },
|
||||
model: { providerID: assistant.providerID as ProviderV2.ID, modelID: assistant.modelID as ModelV2.ID },
|
||||
variant: assistant.variant,
|
||||
modeId: assistant.mode ?? assistant.agent,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { McpServer } from "@agentclientprotocol/sdk"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Context, Effect, Layer, Ref } from "effect"
|
||||
import * as ACPError from "./error"
|
||||
|
||||
export type SelectedModel = {
|
||||
providerID: ProviderV2.ID
|
||||
modelID: ProviderV2.ModelID
|
||||
modelID: ModelV2.ID
|
||||
}
|
||||
|
||||
export type KnownMessagePartMetadata = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AssistantMessage as OpenCodeAssistantMessage, Message } from "@ope
|
||||
import { InstanceRef } from "@/effect/instance-ref"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { Context, Effect, Layer, SynchronizedRef } from "effect"
|
||||
|
||||
@@ -50,7 +51,7 @@ export interface Interface {
|
||||
readonly contextLimit: (input: {
|
||||
readonly directory: string
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly modelID: ProviderV2.ModelID
|
||||
readonly modelID: ModelV2.ID
|
||||
}) => Effect.Effect<number | undefined>
|
||||
readonly sendUpdate: (input: {
|
||||
readonly connection: UsageConnection
|
||||
@@ -112,7 +113,7 @@ export function totalSessionCost(messages: readonly SessionMessage[]): number {
|
||||
export function findContextLimit(
|
||||
providers: Record<ProviderV2.ID, Provider.Info>,
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
): number | undefined {
|
||||
return providers[providerID]?.models[modelID]?.limit.context
|
||||
}
|
||||
@@ -144,7 +145,7 @@ export const layer = Layer.effect(
|
||||
const cachedLimit = Effect.fnUntraced(function* (input: {
|
||||
readonly directory: string
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly modelID: ProviderV2.ModelID
|
||||
readonly modelID: ModelV2.ID
|
||||
}) {
|
||||
return yield* SynchronizedRef.modifyEffect(
|
||||
limits,
|
||||
@@ -171,7 +172,7 @@ export const layer = Layer.effect(
|
||||
const contextLimit = Effect.fn("ACPUsage.contextLimit")(function* (input: {
|
||||
readonly directory: string
|
||||
readonly providerID: ProviderV2.ID
|
||||
readonly modelID: ProviderV2.ModelID
|
||||
readonly modelID: ModelV2.ID
|
||||
}) {
|
||||
return yield* yield* cachedLimit(input)
|
||||
})
|
||||
@@ -198,7 +199,7 @@ export const layer = Layer.effect(
|
||||
const size = yield* contextLimit({
|
||||
directory: input.directory,
|
||||
providerID: ProviderV2.ID.make(message.providerID),
|
||||
modelID: ProviderV2.ModelID.make(message.modelID),
|
||||
modelID: ModelV2.ID.make(message.modelID),
|
||||
})
|
||||
if (!size) return
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
import { type DeepMutable } from "@opencode-ai/core/schema"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
name: Schema.String,
|
||||
@@ -38,7 +39,7 @@ export const Info = Schema.Struct({
|
||||
permission: PermissionV1.Ruleset,
|
||||
model: Schema.optional(
|
||||
Schema.Struct({
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
}),
|
||||
),
|
||||
@@ -62,7 +63,7 @@ export interface Interface {
|
||||
readonly defaultAgent: () => Effect.Effect<string>
|
||||
readonly generate: (input: {
|
||||
description: string
|
||||
model?: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }
|
||||
model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
}) => Effect.Effect<
|
||||
{
|
||||
identifier: string
|
||||
@@ -350,7 +351,7 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
generate: Effect.fn("Agent.generate")(function* (input: {
|
||||
description: string
|
||||
model?: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }
|
||||
model?: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
}) {
|
||||
const cfg = yield* config.get()
|
||||
const model = input.model ?? (yield* provider.defaultModel())
|
||||
|
||||
@@ -1,274 +1,31 @@
|
||||
import { BackgroundJob as CoreBackgroundJob } from "@opencode-ai/core/background-job"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
||||
export type Status = "running" | "completed" | "error" | "cancelled"
|
||||
|
||||
export type Info = {
|
||||
id: string
|
||||
type: string
|
||||
title?: string
|
||||
status: Status
|
||||
started_at: number
|
||||
completed_at?: number
|
||||
output?: string
|
||||
error?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type Active = {
|
||||
info: Info
|
||||
done: Deferred.Deferred<Info>
|
||||
scope: Scope.Closeable
|
||||
token: object
|
||||
pending: number
|
||||
next: number
|
||||
output?: { sequence: number; text: string }
|
||||
}
|
||||
|
||||
type State = {
|
||||
jobs: SynchronizedRef.SynchronizedRef<Map<string, Active>>
|
||||
scope: Scope.Scope
|
||||
}
|
||||
|
||||
type FinishResult = {
|
||||
info?: Info
|
||||
done?: Deferred.Deferred<Info>
|
||||
scope?: Scope.Closeable
|
||||
}
|
||||
|
||||
export type StartInput = {
|
||||
id?: string
|
||||
type: string
|
||||
title?: string
|
||||
metadata?: Record<string, unknown>
|
||||
run: Effect.Effect<string, unknown>
|
||||
}
|
||||
|
||||
export type ExtendInput = {
|
||||
id: string
|
||||
run: Effect.Effect<string, unknown>
|
||||
}
|
||||
|
||||
export type WaitInput = {
|
||||
id: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export type WaitResult = {
|
||||
info?: Info
|
||||
timedOut: boolean
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly get: (id: string) => Effect.Effect<Info | undefined>
|
||||
readonly start: (input: StartInput) => Effect.Effect<Info>
|
||||
readonly extend: (input: ExtendInput) => Effect.Effect<boolean>
|
||||
readonly wait: (input: WaitInput) => Effect.Effect<WaitResult>
|
||||
readonly cancel: (id: string) => Effect.Effect<Info | undefined>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/BackgroundJob") {}
|
||||
|
||||
function snapshot(job: Active): Info {
|
||||
return {
|
||||
...job.info,
|
||||
...(job.info.metadata ? { metadata: { ...job.info.metadata } } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
export {
|
||||
Service,
|
||||
type ExtendInput,
|
||||
type Info,
|
||||
type Interface,
|
||||
type StartInput,
|
||||
type Status,
|
||||
type WaitInput,
|
||||
type WaitResult,
|
||||
} from "@opencode-ai/core/background-job"
|
||||
|
||||
/** Keeps the legacy service instance-scoped while sharing the core registry engine. */
|
||||
export const layer = Layer.effect(
|
||||
CoreBackgroundJob.Service,
|
||||
Effect.gen(function* () {
|
||||
const state = yield* InstanceState.make<State>(
|
||||
Effect.fn("BackgroundJob.state")(function* () {
|
||||
return {
|
||||
jobs: yield* SynchronizedRef.make(new Map()),
|
||||
scope: yield* Scope.Scope,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const settle = Effect.fn("BackgroundJob.settle")(function* (
|
||||
id: string,
|
||||
token: object,
|
||||
sequence: number,
|
||||
exit: Exit.Exit<string, unknown>,
|
||||
) {
|
||||
const completed_at = yield* Clock.currentTimeMillis
|
||||
const s = yield* InstanceState.get(state)
|
||||
const result = yield* SynchronizedRef.modify(s.jobs, (jobs): readonly [FinishResult, Map<string, Active>] => {
|
||||
const job = jobs.get(id)
|
||||
if (!job) return [{}, jobs]
|
||||
if (job.token !== token) return [{}, jobs]
|
||||
if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs]
|
||||
const pending = job.pending - 1
|
||||
const output =
|
||||
Exit.isSuccess(exit) && (!job.output || sequence > job.output.sequence)
|
||||
? { sequence, text: exit.value }
|
||||
: job.output
|
||||
if (Exit.isSuccess(exit) && pending > 0) {
|
||||
return [{}, new Map(jobs).set(id, { ...job, pending, output })]
|
||||
}
|
||||
const status: Exclude<Status, "running"> = Exit.isSuccess(exit)
|
||||
? "completed"
|
||||
: Cause.hasInterruptsOnly(exit.cause)
|
||||
? "cancelled"
|
||||
: "error"
|
||||
const next = {
|
||||
...job,
|
||||
pending: 0,
|
||||
output,
|
||||
info: {
|
||||
...job.info,
|
||||
status,
|
||||
completed_at,
|
||||
...(output ? { output: output.text } : {}),
|
||||
...(Exit.isFailure(exit) ? { error: errorText(Cause.squash(exit.cause)) } : {}),
|
||||
},
|
||||
}
|
||||
return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)]
|
||||
})
|
||||
if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore)
|
||||
if (result.scope) {
|
||||
yield* Scope.close(result.scope, Exit.void).pipe(Effect.forkIn(s.scope, { startImmediately: true }))
|
||||
}
|
||||
return result.info
|
||||
const state = yield* InstanceState.make(() => CoreBackgroundJob.make)
|
||||
return CoreBackgroundJob.Service.of({
|
||||
list: () => InstanceState.useEffect(state, (jobs) => jobs.list()),
|
||||
get: (id) => InstanceState.useEffect(state, (jobs) => jobs.get(id)),
|
||||
start: (input) => InstanceState.useEffect(state, (jobs) => jobs.start(input)),
|
||||
extend: (input) => InstanceState.useEffect(state, (jobs) => jobs.extend(input)),
|
||||
wait: (input) => InstanceState.useEffect(state, (jobs) => jobs.wait(input)),
|
||||
cancel: (id) => InstanceState.useEffect(state, (jobs) => jobs.cancel(id)),
|
||||
})
|
||||
|
||||
const fork = Effect.fn("BackgroundJob.fork")(function* (
|
||||
scope: Scope.Scope,
|
||||
id: string,
|
||||
token: object,
|
||||
sequence: number,
|
||||
run: Effect.Effect<string, unknown>,
|
||||
) {
|
||||
return yield* run.pipe(
|
||||
Effect.matchCauseEffect({
|
||||
onSuccess: (output) => settle(id, token, sequence, Exit.succeed(output)),
|
||||
onFailure: (cause) => settle(id, token, sequence, Exit.failCause(cause)),
|
||||
}),
|
||||
Effect.asVoid,
|
||||
Effect.forkIn(scope, { startImmediately: true }),
|
||||
)
|
||||
})
|
||||
|
||||
const list: Interface["list"] = Effect.fn("BackgroundJob.list")(function* () {
|
||||
return Array.from((yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).values())
|
||||
.map(snapshot)
|
||||
.toSorted((a, b) => a.started_at - b.started_at)
|
||||
})
|
||||
|
||||
const get: Interface["get"] = Effect.fn("BackgroundJob.get")(function* (id) {
|
||||
const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(id)
|
||||
if (!job) return
|
||||
return snapshot(job)
|
||||
})
|
||||
|
||||
const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) {
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const id = input.id ?? Identifier.ascending("job")
|
||||
const started_at = yield* Clock.currentTimeMillis
|
||||
const done = yield* Deferred.make<Info>()
|
||||
return yield* SynchronizedRef.modifyEffect(
|
||||
s.jobs,
|
||||
Effect.fnUntraced(function* (jobs) {
|
||||
const existing = jobs.get(id)
|
||||
if (existing?.info.status === "running") return [snapshot(existing), jobs] as const
|
||||
const scope = yield* Scope.fork(s.scope, "parallel")
|
||||
const token = {}
|
||||
yield* fork(scope, id, token, 0, restore(input.run))
|
||||
const job = {
|
||||
info: {
|
||||
id,
|
||||
type: input.type,
|
||||
title: input.title,
|
||||
status: "running" as const,
|
||||
started_at,
|
||||
metadata: input.metadata,
|
||||
},
|
||||
done,
|
||||
scope,
|
||||
token,
|
||||
pending: 1,
|
||||
next: 1,
|
||||
}
|
||||
return [snapshot(job), new Map(jobs).set(id, job)] as const
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) {
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
return yield* SynchronizedRef.modifyEffect(
|
||||
s.jobs,
|
||||
Effect.fnUntraced(function* (jobs) {
|
||||
const job = jobs.get(input.id)
|
||||
if (!job || job.info.status !== "running") return [false, jobs] as const
|
||||
yield* fork(job.scope, input.id, job.token, job.next, restore(input.run))
|
||||
return [
|
||||
true,
|
||||
new Map(jobs).set(input.id, {
|
||||
...job,
|
||||
pending: job.pending + 1,
|
||||
next: job.next + 1,
|
||||
}),
|
||||
] as const
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const wait: Interface["wait"] = Effect.fn("BackgroundJob.wait")(function* (input) {
|
||||
const job = (yield* SynchronizedRef.get((yield* InstanceState.get(state)).jobs)).get(input.id)
|
||||
if (!job) return { timedOut: false }
|
||||
if (job.info.status !== "running") return { info: snapshot(job), timedOut: false }
|
||||
if (input.timeout === undefined) return { info: yield* Deferred.await(job.done), timedOut: false }
|
||||
if (input.timeout <= 0) return { info: snapshot(job), timedOut: true }
|
||||
const info = yield* Deferred.await(job.done).pipe(Effect.timeoutOption(input.timeout))
|
||||
if (info._tag === "Some") return { info: info.value, timedOut: false }
|
||||
return { info: snapshot(job), timedOut: true }
|
||||
})
|
||||
|
||||
const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) {
|
||||
const completed_at = yield* Clock.currentTimeMillis
|
||||
const result = yield* SynchronizedRef.modify(
|
||||
(yield* InstanceState.get(state)).jobs,
|
||||
(jobs): readonly [FinishResult, Map<string, Active>] => {
|
||||
const job = jobs.get(id)
|
||||
if (!job) return [{}, jobs]
|
||||
if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs]
|
||||
const next = {
|
||||
...job,
|
||||
pending: 0,
|
||||
info: {
|
||||
...job.info,
|
||||
status: "cancelled" as const,
|
||||
completed_at,
|
||||
},
|
||||
}
|
||||
return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)]
|
||||
},
|
||||
)
|
||||
if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore)
|
||||
if (result.scope) yield* Scope.close(result.scope, Exit.void)
|
||||
return result.info
|
||||
})
|
||||
|
||||
return Service.of({ list, get, start, extend, wait, cancel })
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ function activeAssistant(messages: SessionMessage[]) {
|
||||
return assistant?.type === "assistant" ? assistant : undefined
|
||||
}
|
||||
|
||||
function ownedAssistant(messages: SessionMessage[], messageID: string) {
|
||||
const message = messages.find((message) => message.type === "assistant" && message.id === messageID)
|
||||
return message?.type === "assistant" ? message : undefined
|
||||
}
|
||||
|
||||
function activeCompaction(messages: SessionMessage[]) {
|
||||
const index = messages.findIndex((message) => message.type === "compaction")
|
||||
if (index < 0) return
|
||||
@@ -37,8 +42,10 @@ function latestTool(assistant: SessionMessageAssistant | undefined, callID?: str
|
||||
)
|
||||
}
|
||||
|
||||
function latestText(assistant: SessionMessageAssistant | undefined) {
|
||||
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text")
|
||||
function latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
|
||||
return assistant?.content.findLast(
|
||||
(item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
|
||||
)
|
||||
}
|
||||
|
||||
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
||||
@@ -72,6 +79,26 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
|
||||
event.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "session.next.agent.switched":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
draft.unshift({
|
||||
id: event.id,
|
||||
type: "agent-switched",
|
||||
agent: event.properties.agent,
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.model.switched":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
draft.unshift({
|
||||
id: event.id,
|
||||
type: "model-switched",
|
||||
model: event.properties.model,
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.prompted": {
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
draft.unshift({
|
||||
@@ -80,6 +107,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
text: event.properties.prompt.text,
|
||||
files: event.properties.prompt.files,
|
||||
agents: event.properties.prompt.agents,
|
||||
references: event.properties.prompt.references,
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
@@ -133,7 +161,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.step.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const currentAssistant = activeAssistant(draft)
|
||||
const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID)
|
||||
if (!currentAssistant) return
|
||||
currentAssistant.time.completed = event.properties.timestamp
|
||||
currentAssistant.finish = event.properties.finish
|
||||
@@ -145,7 +173,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.step.failed":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const currentAssistant = activeAssistant(draft)
|
||||
const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID)
|
||||
if (!currentAssistant) return
|
||||
currentAssistant.time.completed = event.properties.timestamp
|
||||
currentAssistant.finish = "error"
|
||||
@@ -154,24 +182,24 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.text.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
activeAssistant(draft)?.content.push({ type: "text", text: "" })
|
||||
activeAssistant(draft)?.content.push({ type: "text", id: event.properties.textID, text: "" })
|
||||
})
|
||||
break
|
||||
case "session.next.text.delta":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestText(activeAssistant(draft))
|
||||
const match = latestText(activeAssistant(draft), event.properties.textID)
|
||||
if (match) match.text += event.properties.delta
|
||||
})
|
||||
break
|
||||
case "session.next.text.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestText(activeAssistant(draft))
|
||||
const match = latestText(activeAssistant(draft), event.properties.textID)
|
||||
if (match) match.text = event.properties.text
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
activeAssistant(draft)?.content.push({
|
||||
ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({
|
||||
type: "tool",
|
||||
id: event.properties.callID,
|
||||
name: event.properties.name,
|
||||
@@ -182,15 +210,28 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.tool.input.delta":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
const match = latestTool(
|
||||
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||
event.properties.callID,
|
||||
)
|
||||
if (match?.state.status === "pending") match.state.input += event.properties.delta
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(
|
||||
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||
event.properties.callID,
|
||||
)
|
||||
if (match?.state.status === "pending") match.state.input = event.properties.text
|
||||
})
|
||||
break
|
||||
case "session.next.tool.called":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
const match = latestTool(
|
||||
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||
event.properties.callID,
|
||||
)
|
||||
if (!match) return
|
||||
match.time.ran = event.properties.timestamp
|
||||
match.provider = event.properties.provider
|
||||
@@ -199,7 +240,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.tool.progress":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
const match = latestTool(
|
||||
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||
event.properties.callID,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state.structured = event.properties.structured
|
||||
match.state.content = [...event.properties.content]
|
||||
@@ -207,13 +251,17 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.tool.success":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
const match = latestTool(
|
||||
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||
event.properties.callID,
|
||||
)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state = {
|
||||
status: "completed",
|
||||
input: match.state.input,
|
||||
structured: event.properties.structured,
|
||||
content: [...event.properties.content],
|
||||
result: event.properties.result,
|
||||
}
|
||||
match.provider = event.properties.provider
|
||||
match.time.completed = event.properties.timestamp
|
||||
@@ -221,14 +269,18 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
break
|
||||
case "session.next.tool.failed":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
if (match?.state.status !== "running") return
|
||||
const match = latestTool(
|
||||
ownedAssistant(draft, event.properties.assistantMessageID),
|
||||
event.properties.callID,
|
||||
)
|
||||
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
||||
match.state = {
|
||||
status: "error",
|
||||
error: event.properties.error,
|
||||
input: match.state.input,
|
||||
structured: match.state.structured,
|
||||
content: match.state.content,
|
||||
input: typeof match.state.input === "string" ? {} : match.state.input,
|
||||
structured: match.state.status === "running" ? match.state.structured : {},
|
||||
content: match.state.status === "running" ? match.state.content : [],
|
||||
result: event.properties.result,
|
||||
}
|
||||
match.provider = event.properties.provider
|
||||
match.time.completed = event.properties.timestamp
|
||||
@@ -240,6 +292,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
type: "reasoning",
|
||||
id: event.properties.reasoningID,
|
||||
text: "",
|
||||
providerMetadata: event.properties.providerMetadata,
|
||||
})
|
||||
})
|
||||
break
|
||||
@@ -252,7 +305,11 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
case "session.next.reasoning.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
||||
if (match) match.text = event.properties.text
|
||||
if (match) {
|
||||
match.text = event.properties.text
|
||||
if (event.properties.providerMetadata !== undefined)
|
||||
match.providerMetadata = event.properties.providerMetadata
|
||||
}
|
||||
})
|
||||
break
|
||||
case "session.next.retried":
|
||||
@@ -291,7 +348,7 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
|
||||
message: {
|
||||
async sync(sessionID: string) {
|
||||
const response = await sdk.client.v2.session.messages({ sessionID })
|
||||
setStore("messages", sessionID, reconcile(response.data?.data.items ?? []))
|
||||
setStore("messages", sessionID, reconcile(response.data?.data ?? []))
|
||||
},
|
||||
fromSession(sessionID: string) {
|
||||
const messages = store.messages[sessionID]
|
||||
|
||||
@@ -1087,7 +1087,9 @@ function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
|
||||
return (content ?? [])
|
||||
.map((item) => {
|
||||
if (item.type === "text") return item.text.trim()
|
||||
return `[file ${item.name ?? item.uri}]`
|
||||
const source =
|
||||
item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
|
||||
return `[file ${item.name ?? source}]`
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
|
||||
@@ -389,7 +389,7 @@ export const layer = Layer.effect(
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
},
|
||||
{ publish: true },
|
||||
{ publish: true, ownerID: space.id },
|
||||
)
|
||||
.pipe(Effect.provideService(WorkspaceRef, space.id)),
|
||||
{ discard: true },
|
||||
@@ -434,7 +434,7 @@ export const layer = Layer.effect(
|
||||
if (payload.type === "server.heartbeat") return
|
||||
|
||||
if (payload.type === "sync" && payload.syncEvent) {
|
||||
const failed = yield* events.replay(payload.syncEvent, { publish: true }).pipe(
|
||||
const failed = yield* events.replay(payload.syncEvent, { publish: true, ownerID: space.id }).pipe(
|
||||
Effect.as(false),
|
||||
Effect.catchCause((error) =>
|
||||
Effect.sync(() => {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import "@opencode-ai/core/account"
|
||||
import "@opencode-ai/core/catalog"
|
||||
@@ -26,11 +24,10 @@ export const layer = Layer.effect(
|
||||
const workspaceID = yield* WorkspaceRef
|
||||
return yield* events.publish(definition, data, {
|
||||
...options,
|
||||
location: new Location.Info({
|
||||
location: {
|
||||
directory: AbsolutePath.make(ctx.directory),
|
||||
...(workspaceID ? { workspaceID } : {}),
|
||||
project: { id: Project.ID.make(ctx.project.id), directory: AbsolutePath.make(ctx.worktree) },
|
||||
}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,21 +41,6 @@ export const layer = Layer.effect(
|
||||
workspace: workspaceID,
|
||||
payload: { id: event.id, type: event.type, properties: event.data },
|
||||
})
|
||||
if (!event.sync || event.version === undefined) return
|
||||
GlobalBus.emit("event", {
|
||||
directory: event.location?.directory ?? ctx?.directory,
|
||||
project: ctx?.project.id,
|
||||
workspace: workspaceID,
|
||||
payload: {
|
||||
type: "sync",
|
||||
syncEvent: {
|
||||
id: event.id,
|
||||
type: EventV2.versionedType(event.type, event.version),
|
||||
...event.sync,
|
||||
data: event.data,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
|
||||
@@ -27,6 +27,7 @@ import { isRecord } from "@/util/record"
|
||||
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { ProviderTransform } from "./transform"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ModelStatus } from "./model-status"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderError } from "./error"
|
||||
@@ -653,7 +654,7 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
|
||||
for (const m of result.models) {
|
||||
if (!input.models[m.id]) {
|
||||
models[m.id] = {
|
||||
id: ProviderV2.ModelID.make(m.id),
|
||||
id: ModelV2.ID.make(m.id),
|
||||
providerID: ProviderV2.ID.make("gitlab"),
|
||||
name: `Agent Platform (${m.name})`,
|
||||
family: "",
|
||||
@@ -909,7 +910,7 @@ const ProviderLimit = Schema.Struct({
|
||||
})
|
||||
|
||||
export const Model = Schema.Struct({
|
||||
id: ProviderV2.ModelID,
|
||||
id: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
api: ProviderApiInfo,
|
||||
name: Schema.String,
|
||||
@@ -967,7 +968,7 @@ export function defaultModelIDs<T extends { models: Record<string, { id: string
|
||||
|
||||
export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("ProviderModelNotFoundError", {
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
suggestions: Schema.optional(Schema.Array(Schema.String)),
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {
|
||||
@@ -1007,7 +1008,7 @@ export interface Interface {
|
||||
readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect<Info>
|
||||
readonly getModel: (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
) => Effect.Effect<Model, ModelNotFoundError>
|
||||
readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3, ModelNotFoundError>
|
||||
readonly closest: (
|
||||
@@ -1016,7 +1017,7 @@ export interface Interface {
|
||||
) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined>
|
||||
readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect<Model | undefined>
|
||||
readonly defaultModel: () => Effect.Effect<
|
||||
{ providerID: ProviderV2.ID; modelID: ProviderV2.ModelID },
|
||||
{ providerID: ProviderV2.ID; modelID: ModelV2.ID },
|
||||
DefaultModelError
|
||||
>
|
||||
}
|
||||
@@ -1069,7 +1070,7 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
||||
|
||||
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
|
||||
const base: Model = {
|
||||
id: ProviderV2.ModelID.make(model.id),
|
||||
id: ModelV2.ID.make(model.id),
|
||||
providerID: ProviderV2.ID.make(provider.id),
|
||||
name: model.name,
|
||||
family: model.family,
|
||||
@@ -1127,7 +1128,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
const base = fromModelsDevModel(provider, model)
|
||||
models[id] = {
|
||||
...base,
|
||||
id: ProviderV2.ModelID.make(id),
|
||||
id: ModelV2.ID.make(id),
|
||||
name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`,
|
||||
cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost,
|
||||
options: opts.provider?.body
|
||||
@@ -1152,7 +1153,7 @@ export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
|
||||
}
|
||||
}
|
||||
|
||||
function modelSuggestions(provider: Info | undefined, modelID: ProviderV2.ModelID, enableExperimentalModels: boolean) {
|
||||
function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enableExperimentalModels: boolean) {
|
||||
const available = provider
|
||||
? Object.keys(provider.models).filter((id) => {
|
||||
const model = provider.models[id]
|
||||
@@ -1268,7 +1269,7 @@ export const layer = Layer.effect(
|
||||
id,
|
||||
{
|
||||
...model,
|
||||
id: ProviderV2.ModelID.make(id),
|
||||
id: ModelV2.ID.make(id),
|
||||
providerID,
|
||||
},
|
||||
]),
|
||||
@@ -1303,7 +1304,7 @@ export const layer = Layer.effect(
|
||||
return existingModel?.name ?? modelID
|
||||
})
|
||||
const parsedModel: Model = {
|
||||
id: ProviderV2.ModelID.make(modelID),
|
||||
id: ModelV2.ID.make(modelID),
|
||||
api: {
|
||||
id: apiID,
|
||||
npm: apiNpm,
|
||||
@@ -1690,7 +1691,7 @@ export const layer = Layer.effect(
|
||||
InstanceState.use(state, (s) => s.providers[providerID]),
|
||||
)
|
||||
|
||||
const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderV2.ID, modelID: ProviderV2.ModelID) {
|
||||
const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderV2.ID, modelID: ModelV2.ID) {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const provider = s.providers[providerID]
|
||||
if (!provider) {
|
||||
@@ -1774,7 +1775,7 @@ export const layer = Layer.effect(
|
||||
if (experimental.model) {
|
||||
return {
|
||||
...experimental.model,
|
||||
id: ProviderV2.ModelID.make(experimental.model.id),
|
||||
id: ModelV2.ID.make(experimental.model.id),
|
||||
providerID: ProviderV2.ID.make(experimental.model.providerID),
|
||||
}
|
||||
}
|
||||
@@ -1828,16 +1829,16 @@ export const layer = Layer.effect(
|
||||
|
||||
const s = yield* InstanceState.get(state)
|
||||
const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
|
||||
Effect.map((x): { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[] => {
|
||||
Effect.map((x): { providerID: ProviderV2.ID; modelID: ModelV2.ID }[] => {
|
||||
if (!isRecord(x) || !Array.isArray(x.recent)) return []
|
||||
return x.recent.flatMap((item) => {
|
||||
if (!isRecord(item)) return []
|
||||
if (typeof item.providerID !== "string") return []
|
||||
if (typeof item.modelID !== "string") return []
|
||||
return [{ providerID: ProviderV2.ID.make(item.providerID), modelID: ProviderV2.ModelID.make(item.modelID) }]
|
||||
return [{ providerID: ProviderV2.ID.make(item.providerID), modelID: ModelV2.ID.make(item.modelID) }]
|
||||
})
|
||||
}),
|
||||
Effect.catch(() => Effect.succeed([] as { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }[])),
|
||||
Effect.catch(() => Effect.succeed([] as { providerID: ProviderV2.ID; modelID: ModelV2.ID }[])),
|
||||
)
|
||||
for (const entry of recent) {
|
||||
const provider = s.providers[entry.providerID]
|
||||
@@ -1886,7 +1887,7 @@ export function parseModel(model: string) {
|
||||
const [providerID, ...rest] = model.split("/")
|
||||
return {
|
||||
providerID: ProviderV2.ID.make(providerID),
|
||||
modelID: ProviderV2.ModelID.make(rest.join("/")),
|
||||
modelID: ModelV2.ID.make(rest.join("/")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -611,6 +611,30 @@ function googleThinkingBudgetMax(apiId: string) {
|
||||
return 24_576
|
||||
}
|
||||
|
||||
// SAP's Zod schema drops unknown top-level keys; reasoning controls survive
|
||||
// only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs.
|
||||
function wrapInSapModelParams(variants: Record<string, Record<string, any>>): Record<string, Record<string, any>> {
|
||||
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
|
||||
}
|
||||
|
||||
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||
const id = model.api.id.toLowerCase()
|
||||
if (id.includes("2.5")) {
|
||||
return {
|
||||
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||
max: {
|
||||
thinkingConfig: { includeThoughts: true, thinkingBudget: googleThinkingBudgetMax(id) },
|
||||
},
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
googleThinkingLevelEfforts(id).map((effort) => [
|
||||
effort,
|
||||
{ thinkingConfig: { includeThoughts: true, thinkingLevel: effort } },
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||
if (!model.capabilities.reasoning) return {}
|
||||
|
||||
@@ -716,7 +740,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
max: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 24576,
|
||||
thinkingBudget: googleThinkingBudgetMax(id),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -903,34 +927,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
||||
case "@ai-sdk/google":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
||||
if (id.includes("2.5")) {
|
||||
return {
|
||||
high: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: googleThinkingBudgetMax(id),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
googleThinkingLevelEfforts(id).map((effort) => [
|
||||
effort,
|
||||
{
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingLevel: effort,
|
||||
},
|
||||
},
|
||||
]),
|
||||
)
|
||||
return googleThinkingVariants(model)
|
||||
|
||||
case "@ai-sdk/mistral":
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
||||
@@ -969,57 +966,39 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
|
||||
return {}
|
||||
|
||||
case "@jerome-benoit/sap-ai-provider-v2":
|
||||
if (model.api.id.includes("anthropic")) {
|
||||
case "@jerome-benoit/sap-ai-provider-v2": {
|
||||
if (id.includes("anthropic")) {
|
||||
if (adaptiveEfforts) {
|
||||
return Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
effort,
|
||||
{
|
||||
thinking: {
|
||||
type: "adaptive",
|
||||
...(adaptiveOpus ? { display: "summarized" } : {}),
|
||||
},
|
||||
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
|
||||
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(
|
||||
adaptiveEfforts.map((effort) => [
|
||||
effort,
|
||||
},
|
||||
]),
|
||||
{
|
||||
thinking: { type: "adaptive", ...(adaptiveOpus ? { display: "summarized" } : {}) },
|
||||
output_config: { effort },
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
}
|
||||
return {
|
||||
high: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 31999,
|
||||
},
|
||||
},
|
||||
}
|
||||
return wrapInSapModelParams({
|
||||
high: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||
max: { thinking: { type: "enabled", budget_tokens: 31999 } },
|
||||
})
|
||||
}
|
||||
if (model.api.id.includes("gemini") && id.includes("2.5")) {
|
||||
return {
|
||||
high: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 16000,
|
||||
},
|
||||
},
|
||||
max: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 24576,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (id.includes("gemini") && id.includes("2.5")) {
|
||||
return wrapInSapModelParams(googleThinkingVariants(model))
|
||||
}
|
||||
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
|
||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
||||
if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
|
||||
const efforts = openaiReasoningEfforts(id, model.release_date)
|
||||
return wrapInSapModelParams(Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])))
|
||||
}
|
||||
return {}
|
||||
return wrapInSapModelParams(
|
||||
Object.fromEntries(["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }])),
|
||||
)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { described } from "./metadata"
|
||||
import { QueryBoolean } from "./query"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const ConsoleStateResponse = Schema.Struct({
|
||||
consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)),
|
||||
@@ -51,7 +52,7 @@ const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
|
||||
export const ToolListQuery = Schema.Struct({
|
||||
...WorkspaceRoutingQueryFields,
|
||||
provider: ProviderV2.ID,
|
||||
model: ProviderV2.ModelID,
|
||||
model: ModelV2.ID,
|
||||
})
|
||||
|
||||
const WorktreeList = Schema.Array(Schema.String)
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../
|
||||
import { described } from "./metadata"
|
||||
import { QueryBoolean } from "./query"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const root = "/session"
|
||||
export const ListQuery = Schema.Struct({
|
||||
@@ -57,13 +58,13 @@ export const UpdatePayload = Schema.Struct({
|
||||
})
|
||||
export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
|
||||
export const InitPayload = Schema.Struct({
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
messageID: MessageID,
|
||||
})
|
||||
export const SummarizePayload = Schema.Struct({
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
auto: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
|
||||
|
||||
@@ -50,7 +50,8 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
|
||||
last: payload.at(-1)?.seq,
|
||||
directory: ctx.payload.directory,
|
||||
})
|
||||
yield* events.replayAll(payload)
|
||||
const ownerID = yield* InstanceState.workspaceID
|
||||
yield* events.replayAll(payload, { ownerID, strictOwner: true })
|
||||
log.info("sync replay complete", {
|
||||
sessionID: source,
|
||||
events: payload.length,
|
||||
|
||||
@@ -110,7 +110,7 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
if (operation.requestBody) {
|
||||
// The legacy OpenAPI surface never marked request bodies as required.
|
||||
// Keep that SDK surface stable while the HttpApi spec is tightened.
|
||||
delete operation.requestBody.required
|
||||
if (!isV2Api) delete operation.requestBody.required
|
||||
const body = operation.requestBody.content?.["application/json"]
|
||||
if (body?.schema) body.schema = stripOptionalNull(structuredClone(body.schema))
|
||||
if (path === "/experimental/workspace" && method === "post") {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const log = Log.create({ service: "session.compaction" })
|
||||
@@ -201,7 +202,7 @@ export interface Interface {
|
||||
readonly create: (input: {
|
||||
sessionID: SessionID
|
||||
agent: string
|
||||
model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }
|
||||
model: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
}) => Effect.Effect<void>
|
||||
@@ -585,7 +586,7 @@ export const layer = Layer.effect(
|
||||
const create = Effect.fn("SessionCompaction.create")(function* (input: {
|
||||
sessionID: SessionID
|
||||
agent: string
|
||||
model: { providerID: ProviderV2.ID; modelID: ProviderV2.ModelID }
|
||||
model: { providerID: ProviderV2.ID; modelID: ModelV2.ID }
|
||||
auto: boolean
|
||||
overflow?: boolean
|
||||
}) {
|
||||
|
||||
@@ -4,10 +4,18 @@ import { ProviderTransform } from "@/provider/transform"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { asSchema, type ModelMessage, type Tool } from "ai"
|
||||
import { Effect } from "effect"
|
||||
import { Cause, Effect, FiberSet, Queue } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { tool as nativeTool, ToolFailure, type JsonSchema, type LLMEvent } from "@opencode-ai/llm"
|
||||
import {
|
||||
LLMRequest,
|
||||
Tool as NativeTool,
|
||||
ToolFailure,
|
||||
ToolRuntime,
|
||||
toDefinitions,
|
||||
type JsonSchema,
|
||||
type LLMEvent,
|
||||
} from "@opencode-ai/llm"
|
||||
import type { LLMClientShape } from "@opencode-ai/llm/route"
|
||||
import { LLMNative } from "./native-request"
|
||||
|
||||
@@ -78,22 +86,58 @@ export function stream(input: StreamInput): StreamResult {
|
||||
// OpenAI's official wire field names, so this is identity, not translation
|
||||
// — if a field ever needs to differ between the two surfaces, the
|
||||
// translation belongs here, not split across both packages.
|
||||
const stream = input.llmClient.stream({
|
||||
request: LLMNative.request({
|
||||
model: input.model,
|
||||
apiKey: current.apiKey,
|
||||
baseURL: current.baseURL,
|
||||
messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
|
||||
toolChoice: input.toolChoice,
|
||||
temperature: input.temperature,
|
||||
topP: input.topP,
|
||||
topK: input.topK,
|
||||
maxOutputTokens: input.maxOutputTokens,
|
||||
providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
|
||||
headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
|
||||
}),
|
||||
tools: nativeTools(input.tools, input),
|
||||
const tools = nativeTools(input.tools, input)
|
||||
const request = LLMNative.request({
|
||||
model: input.model,
|
||||
apiKey: current.apiKey,
|
||||
baseURL: current.baseURL,
|
||||
messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
|
||||
toolChoice: input.toolChoice,
|
||||
temperature: input.temperature,
|
||||
topP: input.topP,
|
||||
topK: input.topK,
|
||||
maxOutputTokens: input.maxOutputTokens,
|
||||
providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
|
||||
headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
|
||||
})
|
||||
const stream = Stream.scoped(
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const settlements = yield* FiberSet.make<void>()
|
||||
const results = yield* Queue.unbounded<LLMEvent, Cause.Done>()
|
||||
const provider = input.llmClient
|
||||
.stream(
|
||||
LLMRequest.update(request, {
|
||||
tools: [...request.tools, ...toDefinitions(tools)],
|
||||
}),
|
||||
)
|
||||
.pipe(
|
||||
Stream.flatMap((event) =>
|
||||
event.type !== "tool-call" || event.providerExecuted
|
||||
? Stream.make(event)
|
||||
: Stream.make(event).pipe(
|
||||
Stream.concat(
|
||||
Stream.fromEffectDrain(
|
||||
ToolRuntime.dispatch(tools, event).pipe(
|
||||
Effect.flatMap((dispatched) => Queue.offerAll(results, dispatched.events)),
|
||||
Effect.catchCause((cause) => Queue.failCause(results, cause)),
|
||||
Effect.asVoid,
|
||||
FiberSet.run(settlements, { startImmediately: true }),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Stream.concat(
|
||||
Stream.fromEffectDrain(
|
||||
FiberSet.awaitEmpty(settlements).pipe(Effect.andThen(Queue.end(results)), Effect.asVoid),
|
||||
),
|
||||
),
|
||||
)
|
||||
return provider.pipe(Stream.concat(Stream.fromQueue(results)))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
...current,
|
||||
@@ -128,7 +172,7 @@ export function nativeTools(tools: Record<string, Tool>, input: Pick<StreamInput
|
||||
name,
|
||||
// Tool execution remains opencode-owned. The native runtime only adapts
|
||||
// the @opencode-ai/llm tool call back into the AI SDK Tool.execute shape.
|
||||
nativeTool({
|
||||
NativeTool.make({
|
||||
description: item.description ?? "",
|
||||
jsonSchema: nativeSchema(item.inputSchema),
|
||||
execute: (args: unknown, ctx) =>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { MessageError } from "./message-error"
|
||||
import { AuthError, OutputLengthError } from "./message-error"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
export { AuthError, OutputLengthError } from "./message-error"
|
||||
|
||||
export const ToolCall = Schema.Struct({
|
||||
@@ -120,7 +121,7 @@ export const Info = Schema.Struct({
|
||||
assistant: Schema.optional(
|
||||
Schema.Struct({
|
||||
system: Schema.Array(Schema.String),
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
path: Schema.Struct({
|
||||
cwd: Schema.String,
|
||||
|
||||
@@ -29,7 +29,9 @@ import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { Usage, type LLMEvent } from "@opencode-ai/llm"
|
||||
import { toolFileSourceFromUri, Usage, type LLMEvent } from "@opencode-ai/llm"
|
||||
import { ToolOutput } from "@opencode-ai/core/tool-output"
|
||||
import type { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
const log = Log.create({ service: "session.processor" })
|
||||
@@ -65,11 +67,13 @@ export interface Interface {
|
||||
}
|
||||
|
||||
type ToolCall = {
|
||||
assistantMessageID?: EventV2.ID
|
||||
partID: SessionV1.ToolPart["id"]
|
||||
messageID: SessionV1.ToolPart["messageID"]
|
||||
sessionID: SessionV1.ToolPart["sessionID"]
|
||||
done: Deferred.Deferred<void>
|
||||
inputEnded: boolean
|
||||
raw: string
|
||||
}
|
||||
|
||||
interface ProcessorContext extends Input {
|
||||
@@ -79,7 +83,9 @@ interface ProcessorContext extends Input {
|
||||
blocked: boolean
|
||||
needsCompaction: boolean
|
||||
currentText: SessionV1.TextPart | undefined
|
||||
currentTextID: string | undefined
|
||||
reasoningMap: Record<string, SessionV1.ReasoningPart>
|
||||
v2AssistantMessageID: EventV2.ID | undefined
|
||||
}
|
||||
|
||||
type StreamEvent = LLMEvent
|
||||
@@ -119,7 +125,9 @@ export const layer = Layer.effect(
|
||||
blocked: false,
|
||||
needsCompaction: false,
|
||||
currentText: undefined,
|
||||
currentTextID: undefined,
|
||||
reasoningMap: {},
|
||||
v2AssistantMessageID: undefined,
|
||||
}
|
||||
let aborted = false
|
||||
const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id)
|
||||
@@ -136,6 +144,32 @@ export const layer = Layer.effect(
|
||||
if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
const ensureV2AssistantMessage = Effect.fn("SessionProcessor.ensureV2AssistantMessage")(function* () {
|
||||
if (ctx.v2AssistantMessageID) return ctx.v2AssistantMessageID
|
||||
ctx.v2AssistantMessageID = (yield* events.publish(SessionEvent.Step.Started, {
|
||||
sessionID: ctx.sessionID,
|
||||
agent: input.assistantMessage.agent,
|
||||
model: {
|
||||
id: ModelV2.ID.make(ctx.model.id),
|
||||
providerID: ProviderV2.ID.make(ctx.model.providerID),
|
||||
variant: ModelV2.VariantID.make(input.assistantMessage.variant ?? "default"),
|
||||
},
|
||||
snapshot: ctx.snapshot,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})).id
|
||||
return ctx.v2AssistantMessageID
|
||||
})
|
||||
|
||||
const requireV2AssistantMessage = (toolCall?: ToolCall) =>
|
||||
toolCall?.assistantMessageID === undefined
|
||||
? Effect.die("V2 tool settlement has no owning assistant message")
|
||||
: Effect.succeed(toolCall.assistantMessageID)
|
||||
|
||||
const currentV2AssistantMessage = () =>
|
||||
ctx.v2AssistantMessageID === undefined
|
||||
? Effect.die("V2 step settlement has no owning assistant message")
|
||||
: Effect.succeed(ctx.v2AssistantMessageID)
|
||||
|
||||
const readToolCall = Effect.fn("SessionProcessor.readToolCall")(function* (toolCallID: string) {
|
||||
const call = ctx.toolcalls[toolCallID]
|
||||
if (!call) return undefined
|
||||
@@ -220,6 +254,7 @@ export const layer = Layer.effect(
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID,
|
||||
text: ctx.reasoningMap[reasoningID].text,
|
||||
providerMetadata: ctx.reasoningMap[reasoningID].metadata,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
@@ -230,6 +265,27 @@ export const layer = Layer.effect(
|
||||
delete ctx.reasoningMap[reasoningID]
|
||||
})
|
||||
|
||||
const flushV2Fragments = Effect.fn("SessionProcessor.flushV2Fragments")(function* () {
|
||||
if (!flags.experimentalEventSystem) return
|
||||
if (!ctx.assistantMessage.summary && ctx.currentText && ctx.currentTextID) {
|
||||
yield* events.publish(SessionEvent.Text.Ended, {
|
||||
sessionID: ctx.sessionID,
|
||||
textID: ctx.currentTextID,
|
||||
text: ctx.currentText.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
yield* Effect.forEach(Object.entries(ctx.reasoningMap), ([reasoningID, part]) =>
|
||||
events.publish(SessionEvent.Reasoning.Ended, {
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID,
|
||||
text: part.text,
|
||||
providerMetadata: part.metadata,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const ensureToolCall = Effect.fn("SessionProcessor.ensureToolCall")(function* (input: {
|
||||
id: string
|
||||
name: string
|
||||
@@ -251,9 +307,11 @@ export const layer = Layer.effect(
|
||||
return { call: ctx.toolcalls[input.id], part }
|
||||
}
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
const assistantMessageID = flags.experimentalEventSystem ? yield* ensureV2AssistantMessage() : undefined
|
||||
if (assistantMessageID) {
|
||||
yield* events.publish(SessionEvent.Tool.Input.Started, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: input.id,
|
||||
name: input.name,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
@@ -270,11 +328,13 @@ export const layer = Layer.effect(
|
||||
metadata: input.providerExecuted ? { providerExecuted: true } : undefined,
|
||||
} satisfies SessionV1.ToolPart)
|
||||
ctx.toolcalls[input.id] = {
|
||||
assistantMessageID,
|
||||
done: yield* Deferred.make<void>(),
|
||||
partID: part.id,
|
||||
messageID: part.messageID,
|
||||
sessionID: part.sessionID,
|
||||
inputEnded: false,
|
||||
raw: "",
|
||||
}
|
||||
return { call: ctx.toolcalls[input.id], part }
|
||||
})
|
||||
@@ -311,6 +371,7 @@ export const layer = Layer.effect(
|
||||
yield* events.publish(SessionEvent.Reasoning.Started, {
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID: value.id,
|
||||
providerMetadata: value.providerMetadata,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
@@ -331,6 +392,14 @@ export const layer = Layer.effect(
|
||||
if (!(value.id in ctx.reasoningMap)) return
|
||||
ctx.reasoningMap[value.id].text += value.text
|
||||
if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Reasoning.Delta, {
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID: value.id,
|
||||
delta: value.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
yield* session.updatePartDelta({
|
||||
sessionID: ctx.reasoningMap[value.id].sessionID,
|
||||
messageID: ctx.reasoningMap[value.id].messageID,
|
||||
@@ -355,18 +424,34 @@ export const layer = Layer.effect(
|
||||
return
|
||||
|
||||
case "tool-input-delta":
|
||||
// AI SDK emits a final `tool-call` with the parsed `input`; accumulating
|
||||
// delta fragments into `state.raw` is redundant work for no current consumer.
|
||||
{
|
||||
const toolCall = yield* ensureToolCall(value)
|
||||
const assistantMessageID = flags.experimentalEventSystem
|
||||
? yield* requireV2AssistantMessage(toolCall.call)
|
||||
: undefined
|
||||
if (assistantMessageID) {
|
||||
yield* events.publish(SessionEvent.Tool.Input.Delta, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
delta: value.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
ctx.toolcalls[value.id] = { ...toolCall.call, raw: toolCall.call.raw + value.text }
|
||||
}
|
||||
return
|
||||
|
||||
case "tool-input-end": {
|
||||
const toolCall = yield* ensureToolCall(value)
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
text: "",
|
||||
text: toolCall.call.raw,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
@@ -383,18 +468,22 @@ export const layer = Layer.effect(
|
||||
if (!toolCall.call.inputEnded) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call)
|
||||
yield* events.publish(SessionEvent.Tool.Input.Ended, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
text: "",
|
||||
text: toolCall.call.raw,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
}
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
const assistantMessageID = yield* requireV2AssistantMessage(toolCall.call)
|
||||
yield* events.publish(SessionEvent.Tool.Called, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
tool: value.name,
|
||||
input,
|
||||
@@ -453,6 +542,27 @@ export const layer = Layer.effect(
|
||||
|
||||
case "tool-result": {
|
||||
const toolCall = yield* readToolCall(value.id)
|
||||
if (!toolCall && value.result.type === "error") return
|
||||
if (value.result.type === "error") {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
error: { type: "unknown", message: errorMessage(value.result.value) },
|
||||
result: value.result,
|
||||
provider: {
|
||||
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
|
||||
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
yield* failToolCall(value.id, value.result.value)
|
||||
return
|
||||
}
|
||||
const rawOutput = toolResultOutput(value)
|
||||
const normalized = yield* Effect.forEach(rawOutput.attachments ?? [], (attachment) =>
|
||||
attachment.mime.startsWith("image/")
|
||||
@@ -477,27 +587,53 @@ export const layer = Layer.effect(
|
||||
}
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: ctx.sessionID,
|
||||
callID: value.id,
|
||||
structured: output.metadata,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: output.output,
|
||||
},
|
||||
...(output.attachments?.map((item: SessionV1.FilePart) => ({
|
||||
type: "file" as const,
|
||||
uri: item.url,
|
||||
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
|
||||
const content = [
|
||||
ToolOutput.text({ type: "text", text: output.output }),
|
||||
...(output.attachments?.map((item: SessionV1.FilePart) =>
|
||||
ToolOutput.file({
|
||||
type: "file",
|
||||
source: toolFileSourceFromUri(item.url),
|
||||
mime: item.mime,
|
||||
name: item.filename,
|
||||
})) ?? []),
|
||||
],
|
||||
provider: {
|
||||
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}),
|
||||
) ?? []),
|
||||
]
|
||||
const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data")
|
||||
if (unsupported?.type === "file") {
|
||||
const error = new Error(
|
||||
`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`,
|
||||
)
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
error: {
|
||||
type: "unknown",
|
||||
message: error.message,
|
||||
},
|
||||
provider: {
|
||||
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
|
||||
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
yield* failToolCall(value.id, error)
|
||||
return
|
||||
} else
|
||||
yield* events.publish(SessionEvent.Tool.Success, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
structured: output.metadata,
|
||||
content,
|
||||
result: value.result,
|
||||
provider: {
|
||||
executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
|
||||
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
yield* completeToolCall(value.id, output)
|
||||
return
|
||||
@@ -507,8 +643,10 @@ export const layer = Layer.effect(
|
||||
const toolCall = yield* readToolCall(value.id)
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID,
|
||||
callID: value.id,
|
||||
error: {
|
||||
type: "unknown",
|
||||
@@ -516,6 +654,7 @@ export const layer = Layer.effect(
|
||||
},
|
||||
provider: {
|
||||
executed: toolCall?.part.metadata?.providerExecuted === true,
|
||||
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
@@ -532,17 +671,7 @@ export const layer = Layer.effect(
|
||||
if (!ctx.assistantMessage.summary) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Step.Started, {
|
||||
sessionID: ctx.sessionID,
|
||||
agent: input.assistantMessage.agent,
|
||||
model: {
|
||||
id: ModelV2.ID.make(ctx.model.id),
|
||||
providerID: ProviderV2.ID.make(ctx.model.providerID),
|
||||
variant: ModelV2.VariantID.make(input.assistantMessage.variant ?? "default"),
|
||||
},
|
||||
snapshot: ctx.snapshot,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
yield* ensureV2AssistantMessage()
|
||||
}
|
||||
}
|
||||
yield* session.updatePart({
|
||||
@@ -567,12 +696,14 @@ export const layer = Layer.effect(
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID: yield* currentV2AssistantMessage(),
|
||||
finish: value.reason,
|
||||
cost: usage.cost,
|
||||
tokens: usage.tokens,
|
||||
snapshot: completedSnapshot,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
ctx.v2AssistantMessageID = undefined
|
||||
}
|
||||
}
|
||||
ctx.assistantMessage.finish = value.reason
|
||||
@@ -625,6 +756,7 @@ export const layer = Layer.effect(
|
||||
yield* events.publish(SessionEvent.Text.Started, {
|
||||
sessionID: ctx.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
textID: value.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -637,6 +769,7 @@ export const layer = Layer.effect(
|
||||
time: { start: Date.now() },
|
||||
metadata: value.providerMetadata,
|
||||
}
|
||||
ctx.currentTextID = value.id
|
||||
yield* session.updatePart(ctx.currentText)
|
||||
return
|
||||
|
||||
@@ -644,6 +777,14 @@ export const layer = Layer.effect(
|
||||
if (!ctx.currentText) return
|
||||
ctx.currentText.text += value.text
|
||||
if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Text.Delta, {
|
||||
sessionID: ctx.sessionID,
|
||||
textID: value.id,
|
||||
delta: value.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
yield* session.updatePartDelta({
|
||||
sessionID: ctx.currentText.sessionID,
|
||||
messageID: ctx.currentText.messageID,
|
||||
@@ -673,6 +814,7 @@ export const layer = Layer.effect(
|
||||
sessionID: ctx.sessionID,
|
||||
text: ctx.currentText.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
textID: value.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -683,6 +825,7 @@ export const layer = Layer.effect(
|
||||
if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
|
||||
yield* session.updatePart(ctx.currentText)
|
||||
ctx.currentText = undefined
|
||||
ctx.currentTextID = undefined
|
||||
return
|
||||
|
||||
case "finish":
|
||||
@@ -711,6 +854,7 @@ export const layer = Layer.effect(
|
||||
ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }
|
||||
yield* session.updatePart(ctx.currentText)
|
||||
ctx.currentText = undefined
|
||||
ctx.currentTextID = undefined
|
||||
}
|
||||
|
||||
for (const part of Object.values(ctx.reasoningMap)) {
|
||||
@@ -732,6 +876,16 @@ export const layer = Layer.effect(
|
||||
const match = yield* readToolCall(toolCallID)
|
||||
if (!match) continue
|
||||
const part = match.part
|
||||
if (flags.experimentalEventSystem && match.call.assistantMessageID) {
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID: match.call.assistantMessageID,
|
||||
callID: toolCallID,
|
||||
error: { type: "unknown", message: "Tool execution aborted" },
|
||||
provider: { executed: part.metadata?.providerExecuted === true },
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
const end = Date.now()
|
||||
const metadata = "metadata" in part.state && isRecord(part.state.metadata) ? part.state.metadata : {}
|
||||
yield* session.updatePart({
|
||||
@@ -753,6 +907,7 @@ export const layer = Layer.effect(
|
||||
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
|
||||
slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
|
||||
const error = parse(e)
|
||||
yield* flushV2Fragments()
|
||||
if (SessionV1.ContextOverflowError.isInstance(error)) {
|
||||
ctx.needsCompaction = true
|
||||
yield* events.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
|
||||
@@ -763,6 +918,7 @@ export const layer = Layer.effect(
|
||||
if (flags.experimentalEventSystem) {
|
||||
yield* events.publish(SessionEvent.Step.Failed, {
|
||||
sessionID: ctx.sessionID,
|
||||
assistantMessageID: yield* ensureV2AssistantMessage(),
|
||||
error: {
|
||||
type: "unknown",
|
||||
message: errorMessage(e),
|
||||
@@ -787,6 +943,7 @@ export const layer = Layer.effect(
|
||||
return yield* Effect.gen(function* () {
|
||||
yield* Effect.gen(function* () {
|
||||
ctx.currentText = undefined
|
||||
ctx.currentTextID = undefined
|
||||
ctx.reasoningMap = {}
|
||||
yield* status.set(ctx.sessionID, { type: "busy" })
|
||||
const stream = llm.stream(streamInput)
|
||||
@@ -826,7 +983,8 @@ export const layer = Layer.effect(
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
: Effect.void
|
||||
return event.pipe(
|
||||
return flushV2Fragments().pipe(
|
||||
Effect.andThen(event),
|
||||
Effect.andThen(
|
||||
status.set(ctx.sessionID, {
|
||||
type: "retry",
|
||||
|
||||
@@ -53,7 +53,7 @@ import { Database } from "@opencode-ai/core/database/database"
|
||||
import { SessionEvent } from "@opencode-ai/core/session/event"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { AgentAttachment, FileAttachment, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt"
|
||||
import { AgentAttachment, FileAttachment, Prompt, ReferenceAttachment, Source } from "@opencode-ai/core/session/prompt"
|
||||
import { Reference } from "@/reference/reference"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { eq } from "drizzle-orm"
|
||||
@@ -241,7 +241,7 @@ export const layer = Layer.effect(
|
||||
session: Session.Info
|
||||
history: SessionV1.WithParts[]
|
||||
providerID: ProviderV2.ID
|
||||
modelID: ProviderV2.ModelID
|
||||
modelID: ModelV2.ID
|
||||
}) {
|
||||
if (input.session.parentID) return
|
||||
if (!Session.isDefaultTitle(input.session.title)) return
|
||||
@@ -653,7 +653,7 @@ export const layer = Layer.effect(
|
||||
|
||||
const getModel = Effect.fn("SessionPrompt.getModel")(function* (
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
sessionID: SessionID,
|
||||
) {
|
||||
const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit)
|
||||
@@ -681,7 +681,7 @@ export const layer = Layer.effect(
|
||||
if (current?.model) {
|
||||
return {
|
||||
providerID: ProviderV2.ID.make(current.model.providerID),
|
||||
modelID: ProviderV2.ModelID.make(current.model.id),
|
||||
modelID: ModelV2.ID.make(current.model.id),
|
||||
...(current.model.variant && current.model.variant !== "default" ? { variant: current.model.variant } : {}),
|
||||
}
|
||||
}
|
||||
@@ -1191,12 +1191,13 @@ export const layer = Layer.effect(
|
||||
yield* events.publish(SessionEvent.Prompted, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(info.time.created),
|
||||
prompt: {
|
||||
delivery: "steer",
|
||||
prompt: new Prompt({
|
||||
text: nextPrompt.text.join("\n"),
|
||||
files: nextPrompt.files,
|
||||
agents: nextPrompt.agents,
|
||||
references: nextPrompt.references,
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
for (const text of nextPrompt.synthetic) {
|
||||
@@ -1678,7 +1679,7 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
)
|
||||
const ModelRef = Schema.Struct({
|
||||
providerID: ProviderV2.ID,
|
||||
modelID: ProviderV2.ModelID,
|
||||
modelID: ModelV2.ID,
|
||||
})
|
||||
|
||||
export const PromptInput = Schema.Struct({
|
||||
|
||||
@@ -78,7 +78,7 @@ export const layer = Layer.effect(
|
||||
yield* cancelBackgroundJobs(background, sessionID)
|
||||
const data = yield* InstanceState.get(state)
|
||||
const existing = data.runners.get(sessionID)
|
||||
if (!existing || !existing.busy) {
|
||||
if (!existing) {
|
||||
yield* status.set(sessionID, { type: "idle" })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const log = Log.create({ service: "session" })
|
||||
const runtime = makeRuntime(Database.Service, Database.defaultLayer)
|
||||
@@ -82,7 +83,7 @@ export function fromRow(row: SessionRow): Info {
|
||||
agent: row.agent ?? undefined,
|
||||
model: row.model
|
||||
? {
|
||||
id: ProviderV2.ModelID.make(row.model.id),
|
||||
id: ModelV2.ID.make(row.model.id),
|
||||
providerID: ProviderV2.ID.make(row.model.providerID),
|
||||
variant: row.model.variant,
|
||||
}
|
||||
@@ -202,7 +203,7 @@ const Revert = Schema.Struct({
|
||||
})
|
||||
|
||||
const Model = Schema.Struct({
|
||||
id: ProviderV2.ModelID,
|
||||
id: ModelV2.ID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import { PartID } from "./schema"
|
||||
import { Log } from "@opencode-ai/core/util/log"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const log = Log.create({ service: "session.tools" })
|
||||
|
||||
@@ -75,7 +76,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
|
||||
})
|
||||
|
||||
for (const item of yield* registry.tools({
|
||||
modelID: ProviderV2.ModelID.make(input.model.api.id),
|
||||
modelID: ModelV2.ID.make(input.model.api.id),
|
||||
providerID: input.model.providerID,
|
||||
agent: input.agent,
|
||||
})) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Config } from "@/config/config"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { SessionShareTable } from "@opencode-ai/core/share/sql"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
||||
const log = Log.create({ service: "share-next" })
|
||||
@@ -284,7 +285,7 @@ export const layer = Layer.effect(
|
||||
.map((item) => [`${item.providerID}/${item.modelID}`, item] as const),
|
||||
).values(),
|
||||
),
|
||||
(item) => provider.getModel(ProviderV2.ID.make(item.providerID), ProviderV2.ModelID.make(item.modelID)),
|
||||
(item) => provider.getModel(ProviderV2.ID.make(item.providerID), ModelV2.ID.make(item.modelID)),
|
||||
{ concurrency: 8 },
|
||||
)
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import { Reference } from "@/reference/reference"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
|
||||
const log = Log.create({ service: "tool.registry" })
|
||||
|
||||
@@ -74,7 +75,7 @@ export interface Interface {
|
||||
readonly named: () => Effect.Effect<{ task: TaskDef; read: ReadDef }>
|
||||
readonly tools: (model: {
|
||||
providerID: ProviderV2.ID
|
||||
modelID: ProviderV2.ModelID
|
||||
modelID: ModelV2.ID
|
||||
agent: Agent.Info
|
||||
}) => Effect.Effect<Tool.Def[]>
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ export const TaskTool = Tool.define(
|
||||
Effect.orDie,
|
||||
)
|
||||
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
|
||||
const variant = msg.info.variant
|
||||
|
||||
const model = next.model ?? {
|
||||
modelID: msg.info.modelID,
|
||||
@@ -201,6 +202,7 @@ export const TaskTool = Tool.define(
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
variant: next.model ? undefined : variant,
|
||||
agent: next.name,
|
||||
tools: {
|
||||
...(next.permission.some((rule) => rule.permission === "todowrite") ? {} : { todowrite: false }),
|
||||
@@ -221,6 +223,7 @@ export const TaskTool = Tool.define(
|
||||
.prompt({
|
||||
sessionID: ctx.sessionID,
|
||||
agent: currentParent.agent ?? ctx.agent,
|
||||
variant,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { chmod, mkdir, readFile, stat as statFile, writeFile } from "fs/promises"
|
||||
import { createWriteStream, existsSync, statSync } from "fs"
|
||||
import { realpathSync } from "fs"
|
||||
import { dirname, isAbsolute, join, relative, resolve as pathResolve, win32 } from "path"
|
||||
import { dirname, isAbsolute, join, resolve as pathResolve, win32 } from "path"
|
||||
import { Readable } from "stream"
|
||||
import { pipeline } from "stream/promises"
|
||||
import { Glob } from "@opencode-ai/core/util/glob"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
// Fast sync version for metadata checks
|
||||
@@ -163,13 +164,11 @@ export function windowsPath(p: string): string {
|
||||
)
|
||||
}
|
||||
export function overlaps(a: string, b: string) {
|
||||
const relA = relative(a, b)
|
||||
const relB = relative(b, a)
|
||||
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
||||
return FSUtil.overlaps(a, b)
|
||||
}
|
||||
|
||||
export function contains(parent: string, child: string) {
|
||||
return !relative(parent, child).startsWith("..")
|
||||
return FSUtil.contains(parent, child)
|
||||
}
|
||||
|
||||
export async function findUp(
|
||||
|
||||
Reference in New Issue
Block a user