refactor(schema): apply session review decisions (#35793)
This commit is contained in:
parent
d27746e5b3
commit
ed6ad272ec
142 changed files with 4239 additions and 3174 deletions
|
|
@ -8,6 +8,8 @@ import { State } from "./state"
|
|||
|
||||
export const ID = Agent.ID
|
||||
export type ID = typeof ID.Type
|
||||
export const Name = Agent.Name
|
||||
export type Name = Agent.Name
|
||||
export const defaultID = ID.make("build")
|
||||
|
||||
export const Color = Agent.Color
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as ConfigProviderPlugin from "./provider"
|
||||
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
|
|
@ -91,8 +92,8 @@ export const Plugin = define({
|
|||
input: cost.input,
|
||||
output: cost.output,
|
||||
cache: {
|
||||
read: cost.cache?.read ?? 0,
|
||||
write: cost.cache?.write ?? 0,
|
||||
read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
|
||||
write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export * as ConfigProvider from "./provider"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { ModelV2 } from "../model"
|
||||
|
||||
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
|
||||
|
|
@ -17,8 +18,8 @@ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")(
|
|||
}) {}
|
||||
|
||||
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
|
||||
read: Schema.Finite.pipe(Schema.optional),
|
||||
write: Schema.Finite.pipe(Schema.optional),
|
||||
read: Money.USDPerMillionTokens.pipe(Schema.optional),
|
||||
write: Money.USDPerMillionTokens.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
|
||||
|
|
@ -26,8 +27,8 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
|
|||
type: Schema.Literal("context"),
|
||||
size: Schema.Int,
|
||||
}).pipe(Schema.optional),
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
input: Money.USDPerMillionTokens,
|
||||
output: Money.USDPerMillionTokens,
|
||||
cache: Cache.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
|
|
|
|||
1
packages/core/src/database/migration.gen.ts
generated
1
packages/core/src/database/migration.gen.ts
generated
|
|
@ -48,5 +48,6 @@ export const migrations = (
|
|||
import("./migration/20260705180000_rename_instructions"),
|
||||
import("./migration/20260706223930_add-session-fork"),
|
||||
import("./migration/20260707010146_durable_session_inbox"),
|
||||
import("./migration/20260707120000_migrate_prelaunch_v2_state"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
import { sql } from "drizzle-orm"
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)
|
||||
const isObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown))
|
||||
|
||||
export default {
|
||||
id: "20260707120000_migrate_prelaunch_v2_state",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(
|
||||
sql`DELETE FROM session_message WHERE type = 'compaction' AND json_extract(data, '$.status') = 'queued'`,
|
||||
)
|
||||
const messages = yield* tx.all<{ id: string; type: string; data: string }>(
|
||||
sql`SELECT id, type, data FROM session_message WHERE type IN ('skill', 'shell', 'assistant', 'compaction', 'synthetic')`,
|
||||
)
|
||||
for (const row of messages) {
|
||||
const data = object(decodeJson(row.data))
|
||||
yield* tx.run(
|
||||
sql`UPDATE session_message SET data = ${JSON.stringify(messageData(row.type, data))} WHERE id = ${row.id}`,
|
||||
)
|
||||
}
|
||||
|
||||
yield* tx.run(sql`DELETE FROM event WHERE type = 'session.compaction.delta.1'`)
|
||||
const events = yield* tx.all<{ id: string; aggregateID: string; seq: number; type: string; data: string }>(sql`
|
||||
SELECT id, aggregate_id as aggregateID, seq, type, data
|
||||
FROM event
|
||||
WHERE type IN (
|
||||
'session.skill.activated.1',
|
||||
'session.skill.activated.2',
|
||||
'session.compaction.started.1',
|
||||
'session.compaction.started.2',
|
||||
'session.compaction.ended.1',
|
||||
'session.compaction.failed.1',
|
||||
'session.compaction.failed.2',
|
||||
'session.revert.staged.1',
|
||||
'session.revert.staged.2'
|
||||
)
|
||||
ORDER BY aggregate_id, seq
|
||||
`)
|
||||
const compactionReasons = new Map<string, "auto" | "manual">()
|
||||
for (const row of events) {
|
||||
const data = object(decodeJson(row.data))
|
||||
if (row.type.startsWith("session.compaction.ended.")) {
|
||||
compactionReasons.delete(row.aggregateID)
|
||||
continue
|
||||
}
|
||||
const event = eventData(row.type, data, compactionReasons.get(row.aggregateID))
|
||||
if (row.type.startsWith("session.compaction.started."))
|
||||
compactionReasons.set(row.aggregateID, event.data.reason === "auto" ? "auto" : "manual")
|
||||
if (row.type.startsWith("session.compaction.failed.")) compactionReasons.delete(row.aggregateID)
|
||||
yield* tx.run(
|
||||
sql`UPDATE event SET type = ${event.type}, data = ${JSON.stringify(event.data)} WHERE id = ${row.id}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
|
||||
function messageData(type: string, data: Record<string, unknown>) {
|
||||
if (type === "skill")
|
||||
return defined({
|
||||
metadata: data.metadata,
|
||||
time: data.time,
|
||||
skill: data.skill ?? data.id ?? data.name,
|
||||
name: data.name,
|
||||
text: data.text,
|
||||
})
|
||||
if (type === "shell") {
|
||||
const shell = object(data.shell)
|
||||
return defined({
|
||||
metadata: data.metadata,
|
||||
time: data.time,
|
||||
shellID: data.shellID ?? shell.id,
|
||||
command: data.command ?? shell.command,
|
||||
status: data.status ?? shell.status,
|
||||
exit: data.exit ?? shell.exit,
|
||||
output: data.output,
|
||||
})
|
||||
}
|
||||
if (type === "assistant")
|
||||
return defined({
|
||||
metadata: data.metadata,
|
||||
time: data.time,
|
||||
agent: data.agent,
|
||||
model: data.model,
|
||||
content: Array.isArray(data.content) ? data.content.map(assistantContent) : data.content,
|
||||
snapshot: data.snapshot,
|
||||
finish: data.finish,
|
||||
cost: data.cost,
|
||||
tokens: data.tokens,
|
||||
error: data.error,
|
||||
retry: data.retry,
|
||||
})
|
||||
if (type === "compaction") {
|
||||
if (data.status === "failed")
|
||||
return defined({
|
||||
metadata: data.metadata,
|
||||
time: data.time,
|
||||
status: data.status,
|
||||
reason: data.reason,
|
||||
error: data.error ?? genericCompactionError,
|
||||
})
|
||||
return defined({
|
||||
metadata: data.metadata,
|
||||
time: data.time,
|
||||
status: data.status,
|
||||
reason: data.reason,
|
||||
summary: data.summary,
|
||||
recent: data.recent,
|
||||
})
|
||||
}
|
||||
if (type === "synthetic")
|
||||
return defined({ metadata: data.metadata, time: data.time, text: data.text, description: data.description })
|
||||
const { sessionID: _, ...current } = data
|
||||
return current
|
||||
}
|
||||
|
||||
function assistantContent(value: unknown) {
|
||||
const content = object(value)
|
||||
if (content.type === "text") return defined({ type: content.type, text: content.text })
|
||||
if (content.type === "reasoning")
|
||||
return defined({ type: content.type, text: content.text, state: content.state, time: content.time })
|
||||
if (content.type !== "tool") return content
|
||||
return defined({
|
||||
type: content.type,
|
||||
id: content.id,
|
||||
name: content.name,
|
||||
executed: content.executed,
|
||||
providerState: content.providerState,
|
||||
providerResultState: content.providerResultState,
|
||||
state: toolState(content.state),
|
||||
time: content.time,
|
||||
})
|
||||
}
|
||||
|
||||
function toolState(value: unknown) {
|
||||
const state = object(value)
|
||||
if (state.status === "pending" || state.status === "streaming")
|
||||
return defined({ status: "streaming", input: state.input })
|
||||
if (state.status === "running")
|
||||
return defined({ status: state.status, input: state.input, structured: state.structured, content: state.content })
|
||||
if (state.status === "completed")
|
||||
return defined({
|
||||
status: state.status,
|
||||
input: state.input,
|
||||
structured: state.structured,
|
||||
content: state.content,
|
||||
result: state.result,
|
||||
})
|
||||
if (state.status === "error")
|
||||
return defined({
|
||||
status: state.status,
|
||||
input: state.input,
|
||||
structured: state.structured,
|
||||
content: state.content,
|
||||
error: state.error,
|
||||
result: state.result,
|
||||
})
|
||||
return state
|
||||
}
|
||||
|
||||
function eventData(type: string, data: Record<string, unknown>, compactionReason?: "auto" | "manual") {
|
||||
if (type.startsWith("session.skill.activated."))
|
||||
return {
|
||||
type: "session.skill.activated.1",
|
||||
data: defined({ sessionID: data.sessionID, id: data.id ?? data.name, name: data.name, text: data.text }),
|
||||
}
|
||||
if (type.startsWith("session.compaction.started."))
|
||||
return {
|
||||
type: "session.compaction.started.1",
|
||||
data: defined({
|
||||
sessionID: data.sessionID,
|
||||
reason: data.reason,
|
||||
recent: data.recent ?? "",
|
||||
inputID: data.inputID,
|
||||
}),
|
||||
}
|
||||
if (type.startsWith("session.compaction.failed."))
|
||||
return {
|
||||
type: "session.compaction.failed.1",
|
||||
data: defined({
|
||||
sessionID: data.sessionID,
|
||||
reason: data.reason ?? compactionReason ?? "manual",
|
||||
error: data.error ?? genericCompactionError,
|
||||
inputID: data.inputID,
|
||||
}),
|
||||
}
|
||||
const revert = object(data.revert)
|
||||
return {
|
||||
type: "session.revert.staged.1",
|
||||
data: defined({
|
||||
sessionID: data.sessionID,
|
||||
revert: defined({
|
||||
messageID: revert.messageID,
|
||||
partID: revert.partID,
|
||||
snapshot: revert.snapshot,
|
||||
files: Array.isArray(revert.files)
|
||||
? revert.files.map((value) => {
|
||||
const file = object(value)
|
||||
return defined({
|
||||
file: file.file ?? file.path,
|
||||
patch: file.patch,
|
||||
additions: file.additions,
|
||||
deletions: file.deletions,
|
||||
status: file.status,
|
||||
})
|
||||
})
|
||||
: undefined,
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const genericCompactionError = {
|
||||
type: "compaction.failed",
|
||||
message: "Compaction failed before recording an error",
|
||||
}
|
||||
|
||||
function object(value: unknown): Record<string, unknown> {
|
||||
return isObject(value) ? value : {}
|
||||
}
|
||||
|
||||
function defined(value: Record<string, unknown>) {
|
||||
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined))
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
export * as File from "./file"
|
||||
|
||||
import { Revert } from "@opencode-ai/schema/revert"
|
||||
import { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
|
||||
export const Diff = Revert.FileDiff
|
||||
export const Diff = FileDiff.Info
|
||||
export type Diff = typeof Diff.Type
|
||||
|
|
|
|||
|
|
@ -606,7 +606,7 @@ const layer = Layer.effect(
|
|||
file,
|
||||
])).text
|
||||
return {
|
||||
path: file,
|
||||
file,
|
||||
status,
|
||||
additions: binary ? 0 : Number(stats[0] ?? 0),
|
||||
deletions: binary ? 0 : Number(stats[1] ?? 0),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import path from "path"
|
|||
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { ModelsDev } from "@opencode-ai/schema/models-dev"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Global } from "./global"
|
||||
import { Flag } from "./flag/flag"
|
||||
import { Flock } from "./util/flock"
|
||||
|
|
@ -18,10 +19,10 @@ export type CatalogModelStatus = typeof CatalogModelStatus.Type
|
|||
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
|
||||
|
||||
const CostTier = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache_read: Schema.optional(Schema.Finite),
|
||||
cache_write: Schema.optional(Schema.Finite),
|
||||
input: Money.USDPerMillionTokens,
|
||||
output: Money.USDPerMillionTokens,
|
||||
cache_read: Schema.optional(Money.USDPerMillionTokens),
|
||||
cache_write: Schema.optional(Money.USDPerMillionTokens),
|
||||
tier: Schema.Struct({
|
||||
type: Schema.Literal("context"),
|
||||
size: Schema.Finite,
|
||||
|
|
@ -29,17 +30,17 @@ const CostTier = Schema.Struct({
|
|||
})
|
||||
|
||||
const Cost = Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache_read: Schema.optional(Schema.Finite),
|
||||
cache_write: Schema.optional(Schema.Finite),
|
||||
input: Money.USDPerMillionTokens,
|
||||
output: Money.USDPerMillionTokens,
|
||||
cache_read: Schema.optional(Money.USDPerMillionTokens),
|
||||
cache_write: Schema.optional(Money.USDPerMillionTokens),
|
||||
tiers: Schema.optional(Schema.Array(CostTier)),
|
||||
context_over_200k: Schema.optional(
|
||||
Schema.Struct({
|
||||
input: Schema.Finite,
|
||||
output: Schema.Finite,
|
||||
cache_read: Schema.optional(Schema.Finite),
|
||||
cache_write: Schema.optional(Schema.Finite),
|
||||
input: Money.USDPerMillionTokens,
|
||||
output: Money.USDPerMillionTokens,
|
||||
cache_read: Schema.optional(Money.USDPerMillionTokens),
|
||||
cache_write: Schema.optional(Money.USDPerMillionTokens),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export const Plugin = define({
|
|||
|
||||
yield* ctx.agent.transform((draft) => {
|
||||
draft.update(AgentV2.defaultID, (item) => {
|
||||
item.name = AgentV2.Name.make("Build")
|
||||
item.description = "The default agent. Executes tools based on configured permissions."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
|
|
@ -136,6 +137,7 @@ export const Plugin = define({
|
|||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("plan"), (item) => {
|
||||
item.name = AgentV2.Name.make("Plan")
|
||||
item.description = "Plan mode. Disallows all edit tools."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
|
|
@ -155,6 +157,7 @@ export const Plugin = define({
|
|||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("general"), (item) => {
|
||||
item.name = AgentV2.Name.make("General")
|
||||
item.description =
|
||||
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
||||
item.mode = "subagent"
|
||||
|
|
@ -167,6 +170,7 @@ export const Plugin = define({
|
|||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("explore"), (item) => {
|
||||
item.name = AgentV2.Name.make("Explore")
|
||||
item.description =
|
||||
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
|
||||
item.system = PROMPT_EXPLORE
|
||||
|
|
@ -189,6 +193,7 @@ export const Plugin = define({
|
|||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("compaction"), (item) => {
|
||||
item.name = AgentV2.Name.make("Compaction")
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_COMPACTION
|
||||
|
|
@ -196,6 +201,7 @@ export const Plugin = define({
|
|||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("title"), (item) => {
|
||||
item.name = AgentV2.Name.make("Title")
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_TITLE
|
||||
|
|
@ -203,6 +209,7 @@ export const Plugin = define({
|
|||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("summary"), (item) => {
|
||||
item.name = AgentV2.Name.make("Summary")
|
||||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_SUMMARY
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import type { ModelInfo } from "@opencode-ai/sdk/v2/types"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { EventV2 } from "../event"
|
||||
import { ModelV2 } from "../model"
|
||||
|
|
@ -11,13 +12,13 @@ function released(date: string) {
|
|||
return Number.isFinite(time) ? time : 0
|
||||
}
|
||||
|
||||
function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
||||
function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
|
||||
const base = {
|
||||
input: input?.input ?? 0,
|
||||
output: input?.output ?? 0,
|
||||
input: input?.input ?? Money.USDPerMillionTokens.zero,
|
||||
output: input?.output ?? Money.USDPerMillionTokens.zero,
|
||||
cache: {
|
||||
read: input?.cache_read ?? 0,
|
||||
write: input?.cache_write ?? 0,
|
||||
read: input?.cache_read ?? Money.USDPerMillionTokens.zero,
|
||||
write: input?.cache_write ?? Money.USDPerMillionTokens.zero,
|
||||
},
|
||||
}
|
||||
return [
|
||||
|
|
@ -27,8 +28,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
|||
input: item.input,
|
||||
output: item.output,
|
||||
cache: {
|
||||
read: item.cache_read ?? 0,
|
||||
write: item.cache_write ?? 0,
|
||||
read: item.cache_read ?? Money.USDPerMillionTokens.zero,
|
||||
write: item.cache_write ?? Money.USDPerMillionTokens.zero,
|
||||
},
|
||||
})) ?? []),
|
||||
...(input?.context_over_200k
|
||||
|
|
@ -41,8 +42,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
|||
input: input.context_over_200k.input,
|
||||
output: input.context_over_200k.output,
|
||||
cache: {
|
||||
read: input.context_over_200k.cache_read ?? 0,
|
||||
write: input.context_over_200k.cache_write ?? 0,
|
||||
read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero,
|
||||
write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -50,13 +51,13 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
|
|||
]
|
||||
}
|
||||
|
||||
function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) {
|
||||
function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] | undefined) {
|
||||
if (!override) return base
|
||||
const next = cost(override)
|
||||
const [baseDefault, ...baseTiers] = base
|
||||
const [nextDefault, ...nextTiers] = next
|
||||
const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
|
||||
const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
|
||||
const tierKey = (item: ModelInfo["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
|
||||
const merge = (left: ModelInfo["cost"][number], right: ModelInfo["cost"][number]) => ({
|
||||
...left,
|
||||
...right,
|
||||
tier: right.tier ?? left.tier,
|
||||
|
|
@ -67,12 +68,25 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
|
|||
const current = tiers.get(tierKey(item))
|
||||
tiers.set(tierKey(item), current ? merge(current, item) : item)
|
||||
}
|
||||
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
|
||||
return [
|
||||
merge(
|
||||
baseDefault ?? {
|
||||
input: Money.USDPerMillionTokens.zero,
|
||||
output: Money.USDPerMillionTokens.zero,
|
||||
cache: {
|
||||
read: Money.USDPerMillionTokens.zero,
|
||||
write: Money.USDPerMillionTokens.zero,
|
||||
},
|
||||
},
|
||||
nextDefault,
|
||||
),
|
||||
...tiers.values(),
|
||||
]
|
||||
}
|
||||
|
||||
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
|
||||
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
|
||||
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelInfo["variants"]> {
|
||||
const npm = model.provider?.npm ?? provider.npm
|
||||
const options = model.reasoning_options ?? []
|
||||
const effort = options.find((option) => option.type === "effort")
|
||||
|
|
@ -117,7 +131,7 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.
|
|||
function budgetVariants(
|
||||
npm: string | undefined,
|
||||
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
||||
): NonNullable<ModelV2Info["variants"]> {
|
||||
): NonNullable<ModelInfo["variants"]> {
|
||||
const max = option.max
|
||||
const high =
|
||||
option.max === undefined
|
||||
|
|
@ -146,7 +160,7 @@ function modeName(model: ModelsDev.Model, mode: string) {
|
|||
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
|
||||
}
|
||||
|
||||
function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
|
||||
function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]>) {
|
||||
const variants = model.variants ?? []
|
||||
const existing = new Map(variants.map((variant) => [variant.id, variant]))
|
||||
const nextIDs = new Set(next.map((variant) => variant.id))
|
||||
|
|
@ -157,13 +171,13 @@ function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["varian
|
|||
}
|
||||
|
||||
function applyModel(
|
||||
draft: ModelV2Info,
|
||||
draft: ModelInfo,
|
||||
model: ModelsDev.Model,
|
||||
input: {
|
||||
readonly name?: string
|
||||
readonly cost?: ModelV2Info["cost"]
|
||||
readonly cost?: ModelInfo["cost"]
|
||||
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
|
||||
readonly variants?: NonNullable<ModelV2Info["variants"]>
|
||||
readonly variants?: NonNullable<ModelInfo["variants"]>
|
||||
} = {},
|
||||
) {
|
||||
draft.name = input.name ?? model.name
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Integration } from "../../integration"
|
|||
import { ModelV2 } from "../../model"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { ConfigProviderV1 } from "../../v1/config/provider"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
|
||||
import { ConfigV1 } from "../../v1/config/config"
|
||||
|
||||
|
|
@ -220,20 +221,23 @@ function withoutCredentials(body: Readonly<Record<string, unknown>> | undefined)
|
|||
|
||||
function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) {
|
||||
const base = {
|
||||
input: input.input,
|
||||
output: input.output,
|
||||
cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 },
|
||||
input: Money.USDPerMillionTokens.make(input.input),
|
||||
output: Money.USDPerMillionTokens.make(input.output),
|
||||
cache: {
|
||||
read: Money.USDPerMillionTokens.make(input.cache_read ?? 0),
|
||||
write: Money.USDPerMillionTokens.make(input.cache_write ?? 0),
|
||||
},
|
||||
}
|
||||
if (!input.context_over_200k) return [base]
|
||||
return [
|
||||
base,
|
||||
{
|
||||
tier: { type: "context" as const, size: 200_000 },
|
||||
input: input.context_over_200k.input,
|
||||
output: input.context_over_200k.output,
|
||||
input: Money.USDPerMillionTokens.make(input.context_over_200k.input),
|
||||
output: Money.USDPerMillionTokens.make(input.context_over_200k.output),
|
||||
cache: {
|
||||
read: input.context_over_200k.cache_read ?? 0,
|
||||
write: input.context_over_200k.cache_write ?? 0,
|
||||
read: Money.USDPerMillionTokens.make(input.context_over_200k.cache_read ?? 0),
|
||||
write: Money.USDPerMillionTokens.make(input.context_over_200k.cache_write ?? 0),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ export const Plugin = define({
|
|||
SkillV2.EmbeddedSource.make({
|
||||
type: "embedded",
|
||||
skill: SkillV2.Info.make({
|
||||
name: "opencode",
|
||||
id: SkillV2.ID.make("opencode"),
|
||||
name: SkillV2.Name.make("OpenCode"),
|
||||
description: OpencodeDescription,
|
||||
location: AbsolutePath.make("/builtin/opencode.md"),
|
||||
content: OpencodeContent,
|
||||
|
|
@ -44,7 +45,8 @@ export const Plugin = define({
|
|||
SkillV2.EmbeddedSource.make({
|
||||
type: "embedded",
|
||||
skill: SkillV2.Info.make({
|
||||
name: "report",
|
||||
id: SkillV2.ID.make("report"),
|
||||
name: SkillV2.Name.make("Report"),
|
||||
description: REPORT_DESCRIPTION,
|
||||
slash: true,
|
||||
location: AbsolutePath.make("/builtin/report.md"),
|
||||
|
|
@ -103,15 +105,22 @@ const configuredPlugins = Effect.fn("SkillPlugin.configuredPlugins")(function* (
|
|||
})
|
||||
|
||||
function terminal() {
|
||||
return [
|
||||
process.env.TERM_PROGRAM ? `TERM_PROGRAM=${process.env.TERM_PROGRAM}` : undefined,
|
||||
process.env.TERM ? `TERM=${process.env.TERM}` : undefined,
|
||||
process.env.COLORTERM ? `COLORTERM=${process.env.COLORTERM}` : undefined,
|
||||
]
|
||||
.filter((item): item is string => item !== undefined)
|
||||
.join(", ") || "Unavailable: terminal environment variables are not set"
|
||||
return (
|
||||
[
|
||||
process.env.TERM_PROGRAM ? `TERM_PROGRAM=${process.env.TERM_PROGRAM}` : undefined,
|
||||
process.env.TERM ? `TERM=${process.env.TERM}` : undefined,
|
||||
process.env.COLORTERM ? `COLORTERM=${process.env.COLORTERM}` : undefined,
|
||||
]
|
||||
.filter((item): item is string => item !== undefined)
|
||||
.join(", ") || "Unavailable: terminal environment variables are not set"
|
||||
)
|
||||
}
|
||||
|
||||
function shell() {
|
||||
return process.env.SHELL ?? process.env.ComSpec ?? process.env.COMSPEC ?? "Unavailable: shell environment variable is not set"
|
||||
return (
|
||||
process.env.SHELL ??
|
||||
process.env.ComSpec ??
|
||||
process.env.COMSPEC ??
|
||||
"Unavailable: shell environment variable is not set"
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as SessionV2 from "./session"
|
||||
export * from "./session/schema"
|
||||
|
||||
import { DateTime, Effect, Layer, Schema, Context, Stream, Scope } from "effect"
|
||||
import { Effect, Layer, Schema, Context, Stream, Scope } from "effect"
|
||||
import { ListAnchor } from "@opencode-ai/schema/session"
|
||||
import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm"
|
||||
import { ProjectV2 } from "./project"
|
||||
|
|
@ -19,6 +19,7 @@ import { SessionSchema } from "./session/schema"
|
|||
import { AbsolutePath, PositiveInt, RelativePath } from "./schema"
|
||||
import { AgentV2 } from "./agent"
|
||||
import { SessionV1 } from "./v1/session"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { InstallationVersion } from "./installation/version"
|
||||
import { Slug } from "./util/slug"
|
||||
import { ProjectTable } from "./project/sql"
|
||||
|
|
@ -34,7 +35,7 @@ import { SessionEvent } from "./session/event"
|
|||
import { SessionInput } from "./session/input"
|
||||
import { Snapshot } from "./snapshot"
|
||||
import { SessionRevert } from "./session/revert"
|
||||
import { Revert } from "@opencode-ai/schema/revert"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { Mime } from "./mime"
|
||||
import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||
|
|
@ -46,8 +47,8 @@ import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
|
|||
import { KeyedMutex } from "./effect/keyed-mutex"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
export const RevertState = Revert.State
|
||||
export type RevertState = Revert.State
|
||||
export const RevertState = Session.Revert
|
||||
export type RevertState = Session.Revert
|
||||
|
||||
// get project -> project.locations
|
||||
//
|
||||
|
|
@ -136,7 +137,7 @@ export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.Bus
|
|||
sessionID: SessionSchema.ID,
|
||||
}) {}
|
||||
export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundError>()("Session.SkillNotFoundError", {
|
||||
skill: Schema.String,
|
||||
skill: SkillV2.ID,
|
||||
}) {}
|
||||
export const MessageNotFoundError = SessionRevert.MessageNotFoundError
|
||||
export type MessageNotFoundError = SessionRevert.MessageNotFoundError
|
||||
|
|
@ -170,14 +171,14 @@ export interface Interface {
|
|||
id: SessionMessage.ID
|
||||
direction: "previous" | "next"
|
||||
}
|
||||
}) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
|
||||
}) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
|
||||
readonly message: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
messageID: SessionMessage.ID
|
||||
}) => Effect.Effect<SessionMessage.Message | undefined>
|
||||
}) => Effect.Effect<SessionMessage.Info | undefined>
|
||||
readonly context: (
|
||||
sessionID: SessionSchema.ID,
|
||||
) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
|
||||
) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
|
||||
/**
|
||||
* Durable, ordered, gap-free session log read. Replays public durable
|
||||
* session events after the exclusive `after` cursor, emits a `Synced`
|
||||
|
|
@ -191,7 +192,10 @@ export interface Interface {
|
|||
after?: number
|
||||
follow?: boolean
|
||||
}) => Stream.Stream<SessionEvent.DurableEvent | EventLog.Synced, NotFoundError>
|
||||
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchAgent: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
agent: AgentV2.ID
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchModel: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
model: ModelV2.Ref
|
||||
|
|
@ -209,7 +213,7 @@ export interface Interface {
|
|||
sessionID: SessionSchema.ID
|
||||
command: string
|
||||
arguments?: string
|
||||
agent?: string
|
||||
agent?: AgentV2.ID
|
||||
model?: ModelV2.Ref
|
||||
files?: PromptInput.Prompt["files"]
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
|
|
@ -227,7 +231,7 @@ export interface Interface {
|
|||
readonly skill: (input: {
|
||||
id?: SessionMessage.ID
|
||||
sessionID: SessionSchema.ID
|
||||
skill: string
|
||||
skill: SkillV2.ID
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<void, NotFoundError | SkillNotFoundError>
|
||||
readonly compact: (
|
||||
|
|
@ -250,7 +254,7 @@ export interface Interface {
|
|||
sessionID: SessionSchema.ID
|
||||
messageID: SessionMessage.ID
|
||||
files?: boolean
|
||||
}) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
|
||||
}) => Effect.Effect<Session.Revert, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
|
||||
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
|
||||
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
|
||||
}
|
||||
|
|
@ -273,7 +277,7 @@ const layer = Layer.effect(
|
|||
const scope = yield* Scope.Scope
|
||||
const activeShells = new Set<SessionSchema.ID>()
|
||||
const shellLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
|
||||
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Info)
|
||||
const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
|
||||
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(
|
||||
|
|
@ -322,7 +326,7 @@ const layer = Layer.effect(
|
|||
variant: input.model.variant,
|
||||
}
|
||||
: undefined,
|
||||
cost: 0,
|
||||
cost: Money.USD.zero,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: now, updated: now },
|
||||
})
|
||||
|
|
@ -529,7 +533,7 @@ const layer = Layer.effect(
|
|||
})
|
||||
const model = command.model ?? commandAgent?.model ?? input.model
|
||||
if (agent !== undefined && session.agent !== AgentV2.ID.make(agent))
|
||||
yield* result.switchAgent({ sessionID: input.sessionID, agent })
|
||||
yield* result.switchAgent({ sessionID: input.sessionID, agent: AgentV2.ID.make(agent) })
|
||||
if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model })
|
||||
|
||||
return yield* result.prompt({
|
||||
|
|
@ -591,12 +595,13 @@ const layer = Layer.effect(
|
|||
skill: Effect.fn("V2Session.skill")(function* (input) {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
const skills = yield* SkillV2.Service.pipe(Effect.provide(locations.get(session.location)))
|
||||
const skill = (yield* skills.list()).find((item) => item.name === input.skill)
|
||||
const skill = (yield* skills.list()).find((item) => item.id === input.skill)
|
||||
if (!skill) return yield* new SkillNotFoundError({ skill: input.skill })
|
||||
yield* events.publish(
|
||||
SessionEvent.Skill.Activated,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
id: skill.id,
|
||||
name: skill.name,
|
||||
text: skill.content,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -64,19 +64,21 @@ type Dependencies = {
|
|||
|
||||
export type AutoInput = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly messages: readonly SessionMessage.Message[]
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly request: LLMRequest
|
||||
}
|
||||
|
||||
type CompactInput = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly messages: readonly SessionMessage.Message[]
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly model: Model
|
||||
readonly inputID?: SessionMessage.ID
|
||||
}
|
||||
|
||||
export type ManualInput = {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly messages: readonly SessionMessage.Message[]
|
||||
readonly messages: readonly SessionMessage.Info[]
|
||||
readonly inputID: SessionMessage.ID
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
|
|
@ -99,7 +101,7 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[
|
|||
)
|
||||
.join("\n")
|
||||
|
||||
const serialize = (message: SessionMessage.Message) => {
|
||||
const serialize = (message: SessionMessage.Info) => {
|
||||
if (message.type === "user") {
|
||||
const files =
|
||||
message.files?.map(
|
||||
|
|
@ -128,7 +130,7 @@ const serialize = (message: SessionMessage.Message) => {
|
|||
if (message.type === "system") return `[System update]: ${message.text}`
|
||||
if (message.type === "synthetic") return `[Synthetic context]: ${message.text}`
|
||||
if (message.type === "skill") return `[Skill activated: ${message.name}]\n${message.text}`
|
||||
if (message.type === "shell") return `[Shell]: ${message.shell.command}\n${truncate(message.output?.output ?? "")}`
|
||||
if (message.type === "shell") return `[Shell]: ${message.command}\n${truncate(message.output?.output ?? "")}`
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +149,7 @@ const settings = (documents: readonly Config.Entry[]) => {
|
|||
}
|
||||
|
||||
const select = (
|
||||
messages: readonly SessionMessage.Message[],
|
||||
messages: readonly SessionMessage.Info[],
|
||||
tokens: number,
|
||||
): { readonly head: string; readonly recent: string } | undefined => {
|
||||
const conversation = messages
|
||||
|
|
@ -198,6 +200,7 @@ const make = (dependencies: Dependencies) => {
|
|||
readonly context: readonly string[]
|
||||
readonly recent: string
|
||||
readonly output?: number
|
||||
readonly inputID?: SessionMessage.ID
|
||||
}) {
|
||||
const context = input.model.route.defaults.limits?.context
|
||||
if (context === undefined || context <= 0) return false
|
||||
|
|
@ -208,6 +211,8 @@ const make = (dependencies: Dependencies) => {
|
|||
yield* dependencies.events.publish(SessionEvent.Compaction.Started, {
|
||||
sessionID: input.sessionID,
|
||||
reason: input.reason,
|
||||
recent: input.recent,
|
||||
inputID: input.inputID,
|
||||
})
|
||||
|
||||
const chunks: string[] = []
|
||||
|
|
@ -235,9 +240,27 @@ const make = (dependencies: Dependencies) => {
|
|||
}),
|
||||
Effect.as(true),
|
||||
Effect.catchTag("LLM.Error", () => Effect.succeed(false)),
|
||||
Effect.onInterrupt(() =>
|
||||
input.reason === "auto"
|
||||
? dependencies.events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
reason: input.reason,
|
||||
error: { type: "compaction.interrupted", message: "Compaction was interrupted" },
|
||||
inputID: input.inputID,
|
||||
})
|
||||
: Effect.void,
|
||||
),
|
||||
)
|
||||
const summary = chunks.join("")
|
||||
if (!summarized || failed || !summary.trim()) return false
|
||||
if (!summarized || failed || !summary.trim()) {
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID: input.sessionID,
|
||||
reason: input.reason,
|
||||
error: { type: "compaction.failed", message: "Compaction produced no summary" },
|
||||
inputID: input.inputID,
|
||||
})
|
||||
return false
|
||||
}
|
||||
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
|
||||
sessionID: input.sessionID,
|
||||
reason: input.reason,
|
||||
|
|
@ -284,6 +307,7 @@ const make = (dependencies: Dependencies) => {
|
|||
),
|
||||
recent: forcedShortContext ? "" : selected.recent,
|
||||
output: input.output,
|
||||
inputID: input.inputID,
|
||||
})
|
||||
})
|
||||
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: CompactInput) {
|
||||
|
|
@ -327,6 +351,7 @@ export const layer = Layer.effect(
|
|||
sessionID: input.session.id,
|
||||
messages: input.messages,
|
||||
model: resolved.model,
|
||||
inputID: input.inputID,
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { InstructionCheckpointTable, SessionMessageTable } from "./sql"
|
|||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
const decode = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||
const decode = Schema.decodeUnknownEffect(SessionMessage.Info)
|
||||
|
||||
export const latestCompaction = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
return yield* db
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { DateTime } from "effect"
|
||||
import { DateTime, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Location } from "../location"
|
||||
import { ModelV2 } from "../model"
|
||||
|
|
@ -9,7 +9,10 @@ import { WorkspaceV2 } from "../workspace"
|
|||
import { SessionSchema } from "./schema"
|
||||
import { SessionTable } from "./sql"
|
||||
import { SessionMessage } from "./message"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { PersistedRevert } from "@opencode-ai/schema/session-revert"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
|
||||
const decodeRevert = Schema.decodeUnknownSync(PersistedRevert)
|
||||
|
||||
export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info {
|
||||
return SessionSchema.Info.make({
|
||||
|
|
@ -31,7 +34,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
|||
variant: ModelV2.VariantID.make(row.model.variant ?? "default"),
|
||||
}
|
||||
: undefined,
|
||||
cost: row.cost,
|
||||
cost: Money.USD.make(row.cost),
|
||||
tokens: {
|
||||
input: row.tokens_input,
|
||||
output: row.tokens_output,
|
||||
|
|
@ -46,7 +49,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
|
|||
workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined,
|
||||
}),
|
||||
subpath: row.path ? RelativePath.make(row.path) : undefined,
|
||||
revert: row.revert ? { ...row.revert, messageID: SessionMessage.ID.make(row.revert.messageID) } : undefined,
|
||||
revert: row.revert ? decodeRevert(row.revert) : undefined,
|
||||
time: {
|
||||
created: DateTime.makeUnsafe(row.time_created),
|
||||
updated: DateTime.makeUnsafe(row.time_updated),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export * as SessionInput from "./input"
|
|||
|
||||
import { and, asc, eq, isNull } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { Admitted, Compaction, Delivery, Entry, PromptEntry } from "@opencode-ai/schema/session-input"
|
||||
import { Admitted, Compaction, Delivery, Info, PromptEntry } from "@opencode-ai/schema/session-input"
|
||||
import type { Database } from "../database/database"
|
||||
import type { EventV2 } from "../event"
|
||||
import { KeyedMutex } from "../effect/keyed-mutex"
|
||||
|
|
@ -14,7 +14,7 @@ import { SessionInputTable, SessionMessageTable } from "./sql"
|
|||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
export { Admitted, Compaction, Delivery, Entry, PromptEntry }
|
||||
export { Admitted, Compaction, Delivery, Info, PromptEntry }
|
||||
|
||||
const decodePrompt = Schema.decodeUnknownSync(Prompt)
|
||||
const encodePrompt = Schema.encodeSync(Prompt)
|
||||
|
|
@ -24,7 +24,7 @@ export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict
|
|||
id: SessionMessage.ID,
|
||||
}) {}
|
||||
|
||||
const fromRow = (row: typeof SessionInputTable.$inferSelect): Entry => {
|
||||
const fromRow = (row: typeof SessionInputTable.$inferSelect): Info => {
|
||||
const base = {
|
||||
admittedSeq: row.admitted_seq,
|
||||
id: SessionMessage.ID.make(row.id),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { SessionEvent } from "./event"
|
|||
import { SessionMessage } from "./message"
|
||||
|
||||
export type MemoryState = {
|
||||
messages: SessionMessage.Message[]
|
||||
messages: SessionMessage.Info[]
|
||||
}
|
||||
|
||||
export interface Adapter {
|
||||
|
|
@ -14,13 +14,13 @@ export interface Adapter {
|
|||
messageID: SessionMessage.ID,
|
||||
) => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
|
||||
readonly getShell: (
|
||||
shellID: SessionMessage.Shell["shell"]["id"],
|
||||
shellID: SessionMessage.Shell["shellID"],
|
||||
) => Effect.Effect<SessionMessage.Shell | undefined, never, never>
|
||||
readonly getCompaction: () => Effect.Effect<SessionMessage.Compaction | undefined, never, never>
|
||||
readonly updateAssistant: (assistant: SessionMessage.Assistant) => Effect.Effect<void, never, never>
|
||||
readonly updateShell: (shell: SessionMessage.Shell) => Effect.Effect<void, never, never>
|
||||
readonly updateCompaction: (compaction: SessionMessage.Compaction) => Effect.Effect<void, never, never>
|
||||
readonly appendMessage: (message: SessionMessage.Message) => Effect.Effect<void, never, never>
|
||||
readonly appendMessage: (message: SessionMessage.Info) => Effect.Effect<void, never, never>
|
||||
}
|
||||
|
||||
export function memory(state: MemoryState): Adapter {
|
||||
|
|
@ -29,9 +29,7 @@ export function memory(state: MemoryState): Adapter {
|
|||
const shellIndex = (messageID: SessionMessage.ID) =>
|
||||
state.messages.findLastIndex((message) => message.id === messageID)
|
||||
const compactionIndex = () =>
|
||||
state.messages.findLastIndex(
|
||||
(message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"),
|
||||
)
|
||||
state.messages.findLastIndex((message) => message.type === "compaction" && message.status === "running")
|
||||
// A newer step supersedes stale incomplete rows; never resume an older assistant projection.
|
||||
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
|
||||
|
||||
|
|
@ -64,7 +62,7 @@ export function memory(state: MemoryState): Adapter {
|
|||
getShell(shellID) {
|
||||
return Effect.sync(() => {
|
||||
return state.messages.find((message): message is SessionMessage.Shell => {
|
||||
return message.type === "shell" && message.shell.id === shellID
|
||||
return message.type === "shell" && message.shellID === shellID
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
@ -186,13 +184,13 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "system",
|
||||
text: event.data.text,
|
||||
metadata: event.metadata,
|
||||
time: { created: event.created },
|
||||
}),
|
||||
),
|
||||
"session.synthetic": (event) => {
|
||||
return adapter.appendMessage(
|
||||
SessionMessage.Synthetic.make({
|
||||
sessionID: event.data.sessionID,
|
||||
text: event.data.text,
|
||||
description: event.data.description,
|
||||
metadata: event.data.metadata,
|
||||
|
|
@ -207,8 +205,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
SessionMessage.Skill.make({
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "skill",
|
||||
skill: event.data.id,
|
||||
name: event.data.name,
|
||||
text: event.data.text,
|
||||
metadata: event.metadata,
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
|
|
@ -219,7 +219,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "shell",
|
||||
metadata: event.metadata,
|
||||
shell: event.data.shell,
|
||||
shellID: event.data.shell.id,
|
||||
command: event.data.shell.command,
|
||||
status: event.data.shell.status,
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
|
|
@ -230,7 +232,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
if (currentShell) {
|
||||
yield* adapter.updateShell(
|
||||
produce(currentShell, (draft) => {
|
||||
draft.shell = castDraft(event.data.shell)
|
||||
draft.status = event.data.shell.status
|
||||
draft.exit = event.data.shell.exit
|
||||
draft.output = event.data.output
|
||||
draft.time.completed = event.created
|
||||
}),
|
||||
|
|
@ -270,6 +273,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
type: "assistant",
|
||||
agent: event.data.agent,
|
||||
model: event.data.model,
|
||||
metadata: event.metadata,
|
||||
time: { created: event.created },
|
||||
content: [],
|
||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||
|
|
@ -329,7 +333,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
id: event.data.callID,
|
||||
name: event.data.name,
|
||||
time: { created: event.created },
|
||||
state: SessionMessage.ToolStatePending.make({ status: "pending", input: "" }),
|
||||
state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: "" }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -339,7 +343,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
"session.tool.input.ended": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "pending") match.state.input = event.data.text
|
||||
if (match && match.state.status === "streaming") match.state.input = event.data.text
|
||||
})
|
||||
},
|
||||
"session.tool.called": (event) => {
|
||||
|
|
@ -382,7 +386,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
input: match.state.input,
|
||||
structured: event.data.structured,
|
||||
content: [...event.data.content],
|
||||
outputPaths: event.data.outputPaths ? [...event.data.outputPaths] : [],
|
||||
result: event.data.result,
|
||||
}),
|
||||
)
|
||||
|
|
@ -392,7 +395,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
"session.tool.failed": (event) => {
|
||||
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && (match.state.status === "pending" || match.state.status === "running")) {
|
||||
if (match && (match.state.status === "streaming" || match.state.status === "running")) {
|
||||
match.executed = event.data.executed || match.executed === true
|
||||
match.providerResultState = event.data.resultState
|
||||
match.time.completed = event.created
|
||||
|
|
@ -448,31 +451,30 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
}
|
||||
})
|
||||
},
|
||||
"session.compaction.admitted": (event) =>
|
||||
"session.compaction.admitted": () => Effect.void,
|
||||
"session.compaction.started": (event) =>
|
||||
adapter.appendMessage(
|
||||
SessionMessage.Compaction.make({
|
||||
id: event.data.inputID,
|
||||
SessionMessage.CompactionRunning.make({
|
||||
id: event.data.inputID ?? SessionMessage.ID.fromEvent(event.id),
|
||||
type: "compaction",
|
||||
status: "queued",
|
||||
status: "running",
|
||||
metadata: event.metadata,
|
||||
reason: "manual",
|
||||
reason: event.data.reason,
|
||||
summary: "",
|
||||
recent: "",
|
||||
recent: event.data.recent ?? "",
|
||||
time: { created: event.created },
|
||||
}),
|
||||
),
|
||||
"session.compaction.started": (event) =>
|
||||
"session.compaction.delta": (event) =>
|
||||
Effect.gen(function* () {
|
||||
if (event.data.reason !== "manual") return
|
||||
const current = yield* adapter.getCompaction()
|
||||
if (!current) return
|
||||
yield* adapter.updateCompaction({ ...current, status: "running" })
|
||||
if (current?.status !== "running") return
|
||||
yield* adapter.updateCompaction({ ...current, summary: current.summary + event.data.text })
|
||||
}),
|
||||
"session.compaction.delta": () => Effect.void,
|
||||
"session.compaction.ended": (event) => {
|
||||
return Effect.gen(function* () {
|
||||
const current = event.data.reason === "manual" ? yield* adapter.getCompaction() : undefined
|
||||
if (current) {
|
||||
const current = yield* adapter.getCompaction()
|
||||
if (current?.status === "running") {
|
||||
yield* adapter.updateCompaction({
|
||||
...current,
|
||||
status: "completed",
|
||||
|
|
@ -496,11 +498,20 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
|||
)
|
||||
})
|
||||
},
|
||||
"session.compaction.failed": () =>
|
||||
"session.compaction.failed": (event) =>
|
||||
Effect.gen(function* () {
|
||||
const current = yield* adapter.getCompaction()
|
||||
if (!current) return
|
||||
yield* adapter.updateCompaction({ ...current, status: "failed" })
|
||||
const failed = SessionMessage.CompactionFailed.make({
|
||||
id: current?.id ?? event.data.inputID ?? SessionMessage.ID.fromEvent(event.id),
|
||||
type: "compaction",
|
||||
status: "failed",
|
||||
metadata: current?.metadata ?? event.metadata,
|
||||
reason: event.data.reason,
|
||||
error: event.data.error,
|
||||
time: current?.time ?? { created: event.created },
|
||||
})
|
||||
if (current?.status === "running") return yield* adapter.updateCompaction(failed)
|
||||
yield* adapter.appendMessage(failed)
|
||||
}),
|
||||
"session.revert.staged": () => Effect.void,
|
||||
"session.revert.cleared": () => Effect.void,
|
||||
|
|
|
|||
|
|
@ -24,15 +24,14 @@ import {
|
|||
} from "./sql"
|
||||
import type { DeepMutable } from "../schema"
|
||||
import { Slug } from "../util/slug"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
type MessageEvent = Exclude<
|
||||
SessionEvent.DurableEvent,
|
||||
typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type
|
||||
>
|
||||
type CurrentDurableEvent = Extract<SessionEvent.Event, { readonly durable: object }>
|
||||
type MessageEvent = Exclude<CurrentDurableEvent, typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type>
|
||||
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
|
||||
const encodeMessage = Schema.encodeSync(SessionMessage.Info)
|
||||
|
||||
export class SessionAlreadyProjected extends Error {}
|
||||
|
||||
|
|
@ -87,7 +86,14 @@ function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInse
|
|||
tokens_reasoning: (info.tokens ?? { reasoning: 0 }).reasoning,
|
||||
tokens_cache_read: (info.tokens ?? { cache: { read: 0 } }).cache.read,
|
||||
tokens_cache_write: (info.tokens ?? { cache: { write: 0 } }).cache.write,
|
||||
revert: info.revert ? { ...info.revert, messageID: SessionMessage.ID.make(info.revert.messageID) } : null,
|
||||
revert: info.revert
|
||||
? {
|
||||
messageID: SessionMessage.ID.make(info.revert.messageID),
|
||||
partID: info.revert.partID,
|
||||
snapshot: info.revert.snapshot,
|
||||
diff: info.revert.diff,
|
||||
}
|
||||
: null,
|
||||
permission: info.permission ? [...info.permission] : undefined,
|
||||
time_created: info.time.created,
|
||||
time_updated: info.time.updated,
|
||||
|
|
@ -151,7 +157,7 @@ const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function*
|
|||
if (!row) return
|
||||
yield* events.publish(SessionEvent.UsageUpdated, {
|
||||
sessionID,
|
||||
cost: row.cost,
|
||||
cost: Money.USD.make(row.cost),
|
||||
tokens: {
|
||||
input: row.input,
|
||||
output: row.output,
|
||||
|
|
@ -257,7 +263,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
eq(SessionMessageTable.session_id, event.data.parentID),
|
||||
gt(SessionMessageTable.seq, cursor),
|
||||
lt(SessionMessageTable.seq, copiedSeq + 1),
|
||||
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`,
|
||||
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`,
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionMessageTable.seq))
|
||||
|
|
@ -280,7 +286,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
|||
seq: row.seq,
|
||||
time_created: row.time_created,
|
||||
time_updated: row.time_updated,
|
||||
data: row.type === "synthetic" ? { ...row.data, sessionID: event.data.sessionID } : row.data,
|
||||
data: row.data,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
@ -336,7 +342,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
return Effect.gen(function* () {
|
||||
const decodeRow = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
const updateMessage = (message: SessionMessage.Message) => {
|
||||
const updateMessage = (message: SessionMessage.Info) => {
|
||||
if (event.durable === undefined)
|
||||
return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const encoded = encodeMessage(message)
|
||||
|
|
@ -353,7 +359,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
const appendMessage = (message: SessionMessage.Message) => insertMessage(db, event, message)
|
||||
const appendMessage = (message: SessionMessage.Info) => insertMessage(db, event, message)
|
||||
const adapter: SessionMessageUpdater.Adapter = {
|
||||
getModel() {
|
||||
return db
|
||||
|
|
@ -412,7 +418,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.sessionID),
|
||||
eq(SessionMessageTable.type, "shell"),
|
||||
sql`json_extract(${SessionMessageTable.data}, '$.shell.id') = ${shellID}`,
|
||||
sql`json_extract(${SessionMessageTable.data}, '$.shellID') = ${shellID}`,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
|
|
@ -433,7 +439,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.sessionID),
|
||||
eq(SessionMessageTable.type, "compaction"),
|
||||
sql`json_extract(${SessionMessageTable.data}, '$.status') in ('queued', 'running')`,
|
||||
sql`json_extract(${SessionMessageTable.data}, '$.status') = 'running'`,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
|
|
@ -454,7 +460,7 @@ function run(db: DatabaseService, event: MessageEvent) {
|
|||
})
|
||||
}
|
||||
|
||||
function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Message) {
|
||||
function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Info) {
|
||||
if (event.durable === undefined) return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const encoded = encodeMessage(message)
|
||||
const { id, type, ...data } = encoded
|
||||
|
|
@ -661,14 +667,12 @@ const layer = Layer.effectDiscard(
|
|||
Effect.gen(function* () {
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
const admitted = yield* SessionInput.projectCompactionAdmitted(db, {
|
||||
yield* SessionInput.projectCompactionAdmitted(db, {
|
||||
admittedSeq: event.durable.seq,
|
||||
id: event.data.inputID,
|
||||
sessionID: event.data.sessionID,
|
||||
timeCreated: event.created,
|
||||
})
|
||||
if (admitted.id !== event.data.inputID) return
|
||||
yield* run(db, event)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
|
||||
|
|
@ -676,15 +680,7 @@ const layer = Layer.effectDiscard(
|
|||
yield* events.project(SessionEvent.Execution.Interrupted, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.InstructionsUpdated, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
|
||||
yield* events.project(SessionEvent.Skill.Activated, (event) =>
|
||||
insertMessage(db, event, {
|
||||
id: SessionMessage.ID.fromEvent(event.id),
|
||||
type: "skill",
|
||||
name: event.data.name,
|
||||
text: event.data.text,
|
||||
time: { created: event.created },
|
||||
}),
|
||||
)
|
||||
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))
|
||||
|
|
@ -730,22 +726,26 @@ const layer = Layer.effectDiscard(
|
|||
yield* run(db, event)
|
||||
if (event.durable === undefined)
|
||||
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
|
||||
yield* SessionInput.settleCompaction(db, {
|
||||
sessionID: event.data.sessionID,
|
||||
handledSeq: event.durable.seq,
|
||||
})
|
||||
if (event.data.reason === "manual")
|
||||
yield* SessionInput.settleCompaction(db, {
|
||||
sessionID: event.data.sessionID,
|
||||
handledSeq: event.durable.seq,
|
||||
})
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.RevertEvent.Staged, (event) =>
|
||||
db
|
||||
.update(SessionTable)
|
||||
.set({
|
||||
revert: { ...event.data.revert, files: event.data.revert.files ? [...event.data.revert.files] : undefined },
|
||||
time_updated: DateTime.toEpochMillis(event.created),
|
||||
})
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie, Effect.asVoid),
|
||||
Effect.gen(function* () {
|
||||
const revert = event.data.revert
|
||||
yield* db
|
||||
.update(SessionTable)
|
||||
.set({
|
||||
revert: { ...revert, files: revert.files ? [...revert.files] : undefined },
|
||||
time_updated: DateTime.toEpochMillis(event.created),
|
||||
})
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionEvent.RevertEvent.Cleared, (event) =>
|
||||
db
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export * as SessionRevert from "./revert"
|
||||
|
||||
import { and, asc, eq, gt } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { RelativePath } from "../schema"
|
||||
|
|
@ -46,7 +46,7 @@ const plan = Effect.fn("SessionRevert.plan")(function* (input: BoundaryInput) {
|
|||
.orderBy(asc(SessionMessageTable.seq))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
const decode = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||
const decode = Schema.decodeUnknownEffect(SessionMessage.Info)
|
||||
const files = new Map<RelativePath, Snapshot.ID>()
|
||||
for (const row of rows) {
|
||||
const message = yield* decode({ ...row.data, id: row.id, type: row.type }).pipe(Effect.orDie)
|
||||
|
|
@ -70,7 +70,7 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
|
|||
const next = yield* plan({ sessionID: input.session.id, messageID: input.messageID })
|
||||
const restore = new Map<RelativePath, Snapshot.ID>()
|
||||
if (original) {
|
||||
for (const file of input.session.revert?.files ?? []) restore.set(file.path, original)
|
||||
for (const file of input.session.revert?.files ?? []) restore.set(RelativePath.make(file.file), original)
|
||||
}
|
||||
if (input.files !== false) for (const [file, tree] of next) restore.set(file, tree)
|
||||
if (restore.size) yield* snapshot.restore({ files: restore })
|
||||
|
|
@ -81,10 +81,6 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: {
|
|||
const revert = {
|
||||
messageID: input.messageID,
|
||||
snapshot: original,
|
||||
diff: files
|
||||
.map((file) => file.patch)
|
||||
.join("")
|
||||
.trim(),
|
||||
files,
|
||||
} satisfies SessionSchema.Info["revert"]
|
||||
yield* events.publish(SessionEvent.RevertEvent.Staged, {
|
||||
|
|
@ -100,7 +96,7 @@ export const clear = Effect.fn("SessionRevert.clear")(function* (session: Sessio
|
|||
const original = session.revert.snapshot ? Snapshot.ID.make(session.revert.snapshot) : undefined
|
||||
if (original)
|
||||
yield* snapshot.restore({
|
||||
files: new Map((session.revert.files ?? []).map((file) => [file.path, original])),
|
||||
files: new Map((session.revert.files ?? []).map((file) => [RelativePath.make(file.file), original])),
|
||||
})
|
||||
const events = yield* EventV2.Service
|
||||
yield* events.publish(SessionEvent.RevertEvent.Cleared, {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
type ProviderErrorEvent,
|
||||
} from "@opencode-ai/llm"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Money } from "@opencode-ai/schema/money"
|
||||
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
|
||||
import { AgentV2 } from "../../agent"
|
||||
import { Config } from "../../config"
|
||||
|
|
@ -67,13 +68,13 @@ export function calculateCost(costs: ModelV2.Info["cost"], tokens: StepTokens) {
|
|||
.filter((cost) => cost.tier?.type === "context" && context > cost.tier.size)
|
||||
.toSorted((a, b) => (b.tier?.size ?? 0) - (a.tier?.size ?? 0))[0]
|
||||
const cost = tier ?? costs.find((cost) => cost.tier === undefined)
|
||||
if (!cost) return 0
|
||||
return (
|
||||
if (!cost) return Money.USD.zero
|
||||
return Money.USD.make(
|
||||
(tokens.input * cost.input +
|
||||
(tokens.output + tokens.reasoning) * cost.output +
|
||||
tokens.cache.read * cost.cache.read +
|
||||
tokens.cache.write * cost.cache.write) /
|
||||
1_000_000
|
||||
1_000_000,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +166,7 @@ const layer = Layer.effect(
|
|||
for (const message of yield* store.context(sessionID)) {
|
||||
if (message.type !== "assistant") continue
|
||||
for (const tool of message.content) {
|
||||
if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue
|
||||
if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue
|
||||
yield* events.publish(SessionEvent.Tool.Failed, {
|
||||
sessionID,
|
||||
assistantMessageID: message.id,
|
||||
|
|
@ -300,8 +301,7 @@ const layer = Layer.effect(
|
|||
// Durable publishes are serialized so tool fibers and step settlement never interleave
|
||||
// mid-event.
|
||||
const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
|
||||
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = [], error?: SessionError.Error) =>
|
||||
serialized(publisher.publish(event, outputPaths, error))
|
||||
const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error))
|
||||
let overflowFailure: ProviderErrorEvent | undefined
|
||||
const providerStream = llm.stream(hookedRequest).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
|
|
@ -359,7 +359,6 @@ const layer = Layer.effect(
|
|||
result: settlement.result,
|
||||
output: settlement.output,
|
||||
}),
|
||||
settlement.outputPaths ?? [],
|
||||
settlement.error,
|
||||
).pipe(
|
||||
Effect.andThen(
|
||||
|
|
@ -599,12 +598,30 @@ const layer = Layer.effect(
|
|||
return yield* compaction.compactManual({
|
||||
session,
|
||||
messages: yield* store.context(sessionID),
|
||||
inputID: pending.id,
|
||||
})
|
||||
}),
|
||||
).pipe(Effect.exit)
|
||||
if (Exit.isSuccess(compacted) && compacted.value) return true
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, { sessionID })
|
||||
if (Exit.isFailure(compacted)) return yield* Effect.failCause(compacted.cause)
|
||||
if (Exit.isFailure(compacted)) {
|
||||
const unsettled = yield* SessionInput.pendingCompaction(db, sessionID)
|
||||
if (unsettled)
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
reason: "manual",
|
||||
error: { type: "compaction.failed", message: Cause.pretty(compacted.cause) },
|
||||
inputID: unsettled.id,
|
||||
})
|
||||
return yield* Effect.failCause(compacted.cause)
|
||||
}
|
||||
const unsettled = yield* SessionInput.pendingCompaction(db, sessionID)
|
||||
if (unsettled)
|
||||
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||
sessionID,
|
||||
reason: "manual",
|
||||
error: { type: "compaction.failed", message: "Compaction could not start" },
|
||||
inputID: unsettled.id,
|
||||
})
|
||||
return true
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ 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 { Snapshot } from "../../snapshot"
|
||||
|
||||
type Input = {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly agent: string
|
||||
readonly agent: AgentV2.ID
|
||||
readonly model: ModelV2.Ref
|
||||
readonly provider: string
|
||||
readonly snapshot?: string
|
||||
readonly snapshot?: Snapshot.ID
|
||||
readonly assistantMessageID?: SessionMessage.ID
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +229,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
})
|
||||
|
||||
const publishStepFailure = Effect.fnUntraced(function* (usage?: {
|
||||
readonly cost: number
|
||||
readonly cost: Money.USD
|
||||
readonly tokens: ReturnType<typeof tokens>
|
||||
}) {
|
||||
if (stepFailed || stepFailure === undefined) return
|
||||
|
|
@ -265,11 +268,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`))
|
||||
}
|
||||
|
||||
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (
|
||||
event: LLMEvent,
|
||||
outputPaths: ReadonlyArray<string> = [],
|
||||
error?: SessionError.Error,
|
||||
) {
|
||||
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (event: LLMEvent, error?: SessionError.Error) {
|
||||
switch (event.type) {
|
||||
case "step-start":
|
||||
yield* startAssistant()
|
||||
|
|
@ -395,7 +394,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
|
|||
assistantMessageID: tool.assistantMessageID,
|
||||
callID: event.id,
|
||||
...result,
|
||||
outputPaths,
|
||||
...(executed ? { result: event.result } : {}),
|
||||
executed,
|
||||
resultState,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const providerMetadata = (
|
|||
): ProviderMetadata | undefined => (state === undefined ? undefined : { [provider]: state })
|
||||
|
||||
const toolInput = (tool: SessionMessage.AssistantTool) =>
|
||||
tool.state.status === "pending"
|
||||
tool.state.status === "streaming"
|
||||
? Option.getOrElse(decodeToolInput(tool.state.input), () => tool.state.input)
|
||||
: tool.state.input
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
|
|||
]
|
||||
}
|
||||
|
||||
function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Message[] {
|
||||
function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref): Message[] {
|
||||
switch (message.type) {
|
||||
case "agent-switched":
|
||||
case "model-switched":
|
||||
|
|
@ -202,7 +202,7 @@ function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Mess
|
|||
Message.make({
|
||||
id: message.id,
|
||||
role: "user",
|
||||
content: `Shell command: ${message.shell.command}\n\n${message.output?.output ?? ""}`,
|
||||
content: `Shell command: ${message.command}\n\n${message.output?.output ?? ""}`,
|
||||
metadata: message.metadata,
|
||||
}),
|
||||
]
|
||||
|
|
@ -232,5 +232,5 @@ ${message.recent}
|
|||
}
|
||||
|
||||
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
|
||||
export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: ModelV2.Ref) =>
|
||||
export const toLLMMessages = (messages: readonly SessionMessage.Info[], model: ModelV2.Ref) =>
|
||||
messages.flatMap((message) => toLLMMessage(message, model))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ProjectTable } from "../project/sql"
|
|||
import type { SessionMessage } from "./message"
|
||||
import type { Prompt } from "@opencode-ai/schema/prompt"
|
||||
import type { SessionInput } from "./input"
|
||||
import type { Snapshot } from "../snapshot"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import { PermissionV1 } from "../v1/permission"
|
||||
import { ProjectV2 } from "../project"
|
||||
import type { SessionSchema } from "./schema"
|
||||
|
|
@ -13,10 +13,11 @@ import type { MessageID, PartID, SessionV1 } from "../v1/session"
|
|||
import { WorkspaceV2 } from "../workspace"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
import type { Instructions } from "../instructions/index"
|
||||
import type { Revert } from "@opencode-ai/schema/revert"
|
||||
import type { Session } from "@opencode-ai/schema/session"
|
||||
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
|
||||
import type { Schema } from "effect"
|
||||
|
||||
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
||||
type SessionMessageData = Omit<(typeof SessionMessage.Info)["Encoded"], "type" | "id">
|
||||
type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
|
||||
type V1PartData = Omit<SessionV1.Part, "id" | "sessionID" | "messageID">
|
||||
|
||||
|
|
@ -41,7 +42,7 @@ export const SessionTable = sqliteTable(
|
|||
summary_additions: integer(),
|
||||
summary_deletions: integer(),
|
||||
summary_files: integer(),
|
||||
summary_diffs: text({ mode: "json" }).$type<Snapshot.LegacyFileDiff[]>(),
|
||||
summary_diffs: text({ mode: "json" }).$type<FileDiff.LegacyInfo[]>(),
|
||||
metadata: text({ mode: "json" }).$type<Record<string, unknown>>(),
|
||||
cost: real().notNull().default(0),
|
||||
tokens_input: integer().notNull().default(0),
|
||||
|
|
@ -49,7 +50,7 @@ export const SessionTable = sqliteTable(
|
|||
tokens_reasoning: integer().notNull().default(0),
|
||||
tokens_cache_read: integer().notNull().default(0),
|
||||
tokens_cache_write: integer().notNull().default(0),
|
||||
revert: text({ mode: "json" }).$type<Revert.State>(),
|
||||
revert: text({ mode: "json" }).$type<Session.Revert | RevertV1>(),
|
||||
permission: text({ mode: "json" }).$type<PermissionV1.Ruleset>(),
|
||||
agent: text(),
|
||||
model: text({ mode: "json" }).$type<{
|
||||
|
|
@ -148,7 +149,7 @@ export const SessionInputTable = sqliteTable(
|
|||
.$type<SessionSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionInput.Entry["type"]>().notNull(),
|
||||
type: text().$type<SessionInput.Info["type"]>().notNull(),
|
||||
prompt: text({ mode: "json" }).$type<Prompt>(),
|
||||
delivery: text().$type<SessionInput.Delivery>(),
|
||||
admitted_seq: integer().notNull(),
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import { fromRow } from "./info"
|
|||
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: Session.ID) => Effect.Effect<Session.Info | undefined>
|
||||
readonly context: (sessionID: Session.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||
readonly context: (sessionID: Session.ID) => Effect.Effect<SessionMessage.Info[], MessageDecodeError>
|
||||
readonly message: (
|
||||
messageID: SessionMessage.ID,
|
||||
) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Message } | undefined>
|
||||
) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Info } | undefined>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionStore") {}
|
||||
|
|
@ -25,7 +25,7 @@ const layer = Layer.effect(
|
|||
Service,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
|
||||
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Info)
|
||||
|
||||
return Service.of({
|
||||
get: Effect.fn("SessionStore.get")(function* (sessionID) {
|
||||
|
|
|
|||
|
|
@ -28,11 +28,15 @@ export type Source = typeof Source.Type
|
|||
|
||||
export const Info = Skill.Info
|
||||
export type Info = Skill.Info
|
||||
export const ID = Skill.ID
|
||||
export type ID = Skill.ID
|
||||
export const Name = Skill.Name
|
||||
export type Name = Skill.Name
|
||||
|
||||
export const Event = Skill.Event
|
||||
|
||||
export const available = (skills: ReadonlyArray<Info>, agent: AgentV2.Info) =>
|
||||
skills.filter((skill) => PermissionV2.evaluate("skill", skill.name, agent.permissions).effect !== "deny")
|
||||
skills.filter((skill) => PermissionV2.evaluate("skill", skill.id, agent.permissions).effect !== "deny")
|
||||
|
||||
const Frontmatter = Schema.Struct({
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
|
|
@ -96,7 +100,7 @@ const layer = Layer.effect(
|
|||
source: Source.key(source),
|
||||
type: source.type,
|
||||
directories: [],
|
||||
skills: [source.skill.name],
|
||||
skills: [source.skill.id],
|
||||
})
|
||||
return { skills: [source.skill], directories: [] }
|
||||
}
|
||||
|
|
@ -112,15 +116,13 @@ const layer = Layer.effect(
|
|||
if (!markdown) continue
|
||||
const frontmatter = decodeFrontmatter(markdown.data).valueOrUndefined
|
||||
if (!frontmatter) continue
|
||||
const name =
|
||||
frontmatter.name !== undefined
|
||||
? frontmatter.name
|
||||
: path.dirname(filepath) === directory
|
||||
? path.basename(filepath, ".md")
|
||||
: undefined
|
||||
if (!name) continue
|
||||
const id =
|
||||
path.dirname(filepath) === directory
|
||||
? path.basename(filepath, ".md")
|
||||
: path.basename(path.dirname(filepath))
|
||||
skills.push({
|
||||
name,
|
||||
id: ID.make(id),
|
||||
name: Name.make(frontmatter.name ?? id),
|
||||
description: frontmatter.description,
|
||||
slash: metadataBoolean(frontmatter.metadata, "opencode/slash") ?? frontmatter.slash,
|
||||
autoinvoke: metadataBoolean(frontmatter.metadata, "opencode/autoinvoke"),
|
||||
|
|
@ -133,7 +135,7 @@ const layer = Layer.effect(
|
|||
source: Source.key(source),
|
||||
type: source.type,
|
||||
directories,
|
||||
skills: skills.map((skill) => skill.name),
|
||||
skills: skills.map((skill) => skill.id),
|
||||
})
|
||||
return { skills, directories }
|
||||
})
|
||||
|
|
@ -148,7 +150,7 @@ const layer = Layer.effect(
|
|||
yield* Effect.logInfo("skill cache invalidated", {
|
||||
file,
|
||||
sources: invalidated.map(([key]) => key),
|
||||
skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.name)),
|
||||
skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.id)),
|
||||
})
|
||||
yield* events.publish(Event.Updated, {}).pipe(Effect.asVoid)
|
||||
})
|
||||
|
|
@ -159,12 +161,12 @@ const layer = Layer.effect(
|
|||
)
|
||||
|
||||
const list = Effect.fn("SkillV2.list")(function* () {
|
||||
const skills = new Map<string, Info>()
|
||||
const skills = new Map<ID, Info>()
|
||||
for (const source of state.get().sources) {
|
||||
const key = Source.key(source)
|
||||
const loaded = cache.get(key) ?? (yield* load(source))
|
||||
cache.set(key, loaded)
|
||||
for (const skill of loaded.skills) skills.set(skill.name, skill)
|
||||
for (const skill of loaded.skills) skills.set(skill.id, skill)
|
||||
}
|
||||
return Array.from(skills.values())
|
||||
})
|
||||
|
|
@ -180,4 +182,8 @@ const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [SkillDiscovery.node, FSUtil.node, EventV2.node] })
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [SkillDiscovery.node, FSUtil.node, EventV2.node],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { SkillV2 } from "../skill"
|
|||
import { Instructions } from "../instructions/index"
|
||||
|
||||
const Summary = Schema.Struct({
|
||||
name: Schema.String,
|
||||
id: SkillV2.ID,
|
||||
name: SkillV2.Name,
|
||||
description: Schema.String,
|
||||
})
|
||||
type Summary = typeof Summary.Type
|
||||
|
|
@ -16,6 +17,7 @@ type Summary = typeof Summary.Type
|
|||
const entries = (skills: ReadonlyArray<Summary>) =>
|
||||
skills.flatMap((skill) => [
|
||||
" <skill>",
|
||||
` <id>${skill.id}</id>`,
|
||||
` <name>${skill.name}</name>`,
|
||||
` <description>${skill.description}</description>`,
|
||||
" </skill>",
|
||||
|
|
@ -34,8 +36,8 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
|||
const diff = Instructions.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(skill) => skill.name,
|
||||
(before, after) => before.description !== after.description,
|
||||
(skill) => skill.id,
|
||||
(before, after) => before.name !== after.name || before.description !== after.description,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
|
|
@ -50,7 +52,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
|||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
`The following skills are no longer available and must not be used: ${diff.removed.map((skill) => skill.name).join(", ")}.`,
|
||||
`The following skill IDs are no longer available and must not be used: ${diff.removed.map((skill) => skill.id).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
|
@ -77,9 +79,9 @@ const layer = Layer.effect(
|
|||
.flatMap((skill) =>
|
||||
skill.description === undefined || skill.autoinvoke === false
|
||||
? []
|
||||
: [{ name: skill.name, description: skill.description }],
|
||||
: [{ id: skill.id, name: skill.name, description: skill.description }],
|
||||
)
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
.toSorted((a, b) => a.id.localeCompare(b.id))
|
||||
return Instructions.make({
|
||||
key: Instructions.Key.make("core/skill-guidance"),
|
||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import { Git } from "./git"
|
|||
import { Global } from "./global"
|
||||
import { Location } from "./location"
|
||||
import { AbsolutePath, RelativePath } from "./schema"
|
||||
import { ID } from "@opencode-ai/schema/snapshot"
|
||||
import { Hash } from "./util/hash"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("Snapshot.ID"))
|
||||
export type ID = typeof ID.Type
|
||||
export { ID }
|
||||
|
||||
export class Error extends Schema.TaggedErrorClass<Error>()("Snapshot.Error", {
|
||||
operation: Schema.Literals(["capture", "files", "diff", "preview", "restore"]),
|
||||
|
|
@ -253,12 +253,3 @@ function failure(operation: Error["operation"], cause: unknown) {
|
|||
cause,
|
||||
})
|
||||
}
|
||||
|
||||
/** Legacy persisted session diff shape. */
|
||||
export type LegacyFileDiff = {
|
||||
file?: string
|
||||
patch?: string
|
||||
additions: number
|
||||
deletions: number
|
||||
status?: "added" | "deleted" | "modified"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ export const name = "skill"
|
|||
const FILE_LIMIT = 10
|
||||
|
||||
export const Input = Schema.Struct({
|
||||
name: Schema.String.annotate({ description: "The name of the skill from the available skills list" }),
|
||||
id: SkillV2.ID.annotate({ description: "The ID of the skill from the available skills list" }),
|
||||
})
|
||||
|
||||
export const Output = Schema.Struct({
|
||||
name: Schema.String,
|
||||
name: SkillV2.Name,
|
||||
directory: Schema.String,
|
||||
output: Schema.String,
|
||||
})
|
||||
|
|
@ -27,7 +27,7 @@ export const description = [
|
|||
"",
|
||||
"Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.",
|
||||
"",
|
||||
"The skill name must match one of the available skills in the instructions.",
|
||||
"The skill ID must match one of the available skills in the instructions.",
|
||||
].join("\n")
|
||||
|
||||
export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray<string>) => {
|
||||
|
|
@ -70,13 +70,13 @@ export const Plugin = {
|
|||
execute: (input, context) =>
|
||||
Effect.gen(function* () {
|
||||
const current = yield* skills.list()
|
||||
const skill = current.find((skill) => skill.name === input.name)
|
||||
if (!skill) return yield* unableToLoad(input.name)
|
||||
const skill = current.find((skill) => skill.id === input.id)
|
||||
if (!skill) return yield* unableToLoad(input.id)
|
||||
return yield* Effect.gen(function* () {
|
||||
yield* permission.assert({
|
||||
action: name,
|
||||
resources: [skill.name],
|
||||
save: [skill.name],
|
||||
resources: [skill.id],
|
||||
save: [skill.id],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
|
||||
|
|
@ -94,7 +94,7 @@ export const Plugin = {
|
|||
directory,
|
||||
output: toModelOutput(skill, files),
|
||||
}
|
||||
}).pipe(Effect.mapError((error) => unableToLoad(input.name, error)))
|
||||
}).pipe(Effect.mapError((error) => unableToLoad(input.id, error)))
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue