Compare commits

..

2 Commits

Author SHA1 Message Date
Kit Langton 219ded0330 fix(core): prioritize manual compaction 2026-07-08 11:38:43 -04:00
Kit Langton 2062a5a3a8 fix(core): steer manual compaction 2026-07-07 20:44:34 -04:00
161 changed files with 4252 additions and 5098 deletions
@@ -8,7 +8,7 @@ import type {
QuestionRequest,
Session,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
@@ -188,7 +188,7 @@ export function applyDirectoryEvent(input: {
break
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
break
}
@@ -5,7 +5,7 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
@@ -33,7 +33,7 @@ describe("app session cache", () => {
test("dropSessionCaches clears orphaned parts without message rows", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -67,7 +67,7 @@ describe("app session cache", () => {
const m = msg("msg_1", "ses_1")
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -4,7 +4,7 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
@@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40
type SessionCache = {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -13,7 +13,7 @@ import type {
ReferenceInfo,
Session,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
@@ -51,7 +51,7 @@ export type State = {
}
session_working(id: string): boolean
session_diff: {
[sessionID: string]: FileDiffInfo[]
[sessionID: string]: SnapshotFileDiff[]
}
todo: {
[sessionID: string]: Todo[]
+3 -3
View File
@@ -8,7 +8,7 @@ import type {
QuestionRequest,
Session,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { batch } from "solid-js"
@@ -139,7 +139,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const [data, setData] = createStore({
info: {} as Record<string, Session | undefined>,
session_status: {} as Record<string, SessionStatus>,
session_diff: {} as Record<string, FileDiffInfo[]>,
session_diff: {} as Record<string, SnapshotFileDiff[]>,
todo: {} as Record<string, Todo[]>,
permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionRequest[]>,
@@ -769,7 +769,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
return
}
@@ -1,6 +1,6 @@
import { createEffect, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/session-ui/session-review"
import type {
SessionReviewCommentActions,
@@ -14,7 +14,7 @@ import type { LineComment } from "@/context/comments"
export type DiffStyle = "unified" | "split"
type ReviewDiff = FileDiffInfo | VcsFileDiff
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
export interface SessionReviewTabProps {
title?: JSX.Element
@@ -8,7 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -23,6 +23,7 @@ import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
import {
@@ -35,9 +36,15 @@ import {
import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function SessionSidePanel(props: {
canReview: () => boolean
diffs: () => (FileDiffInfo | VcsFileDiff)[]
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
diffsReady: () => boolean
empty: () => string
hasReview: () => boolean
@@ -52,6 +59,7 @@ export function SessionSidePanel(props: {
}) {
const layout = useLayout()
const settings = useSettings()
const sync = useSync()
const file = useFile()
const language = useLanguage()
const command = useCommand()
@@ -80,7 +88,7 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const diffs = createMemo(() => props.diffs())
const diffs = createMemo(() => props.diffs().filter(renderDiff))
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
@@ -1,13 +1,17 @@
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Kind } from "@/components/file-tree-v2"
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
export type RenderDiff = FileDiffInfo | VcsFileDiff
export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
export function normalizePath(p: string) {
return normalizeFileTreeV2Path(p)
}
export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function reviewDiffKinds(diffs: RenderDiff[]) {
const merge = (a: Kind | undefined, b: Kind) => {
if (!a) return b
@@ -1,5 +1,5 @@
import { createMemo, createSignal, Show, type JSX } from "solid-js"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import {
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
@@ -21,11 +21,16 @@ import type {
import FileTreeV2 from "@/components/file-tree-v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { filterReviewFiles, reviewDiffKinds, type RenderDiff } from "@/pages/session/v2/review-diff-kinds"
import {
filterRenderableDiff,
filterReviewFiles,
reviewDiffKinds,
type RenderDiff,
} from "@/pages/session/v2/review-diff-kinds"
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
type ReviewDiff = FileDiffInfo | VcsFileDiff
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
export type ReviewPanelV2Props = {
title?: JSX.Element
@@ -49,7 +54,7 @@ export type ReviewPanelV2Props = {
export function ReviewPanelV2(props: ReviewPanelV2Props) {
const sdk = useSDK()
const diffs = createMemo(() => props.diffs())
const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff))
const filteredFiles = createMemo(() =>
filterReviewFiles(
diffs().map((diff) => diff.file),
+2 -2
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { diffs, message } from "./diffs"
@@ -9,7 +9,7 @@ const item = {
additions: 1,
deletions: 1,
status: "modified",
} satisfies FileDiffInfo
} satisfies SnapshotFileDiff
describe("diffs", () => {
test("keeps valid arrays", () => {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
type Diff = FileDiffInfo
type Diff = SnapshotFileDiff | VcsFileDiff
function diff(value: unknown): value is Diff {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
+2 -3
View File
@@ -37,8 +37,7 @@ function defaultCost(model: CurrentModel) {
export function runAgent(input: CurrentAgent): RunAgent {
return {
id: input.id,
name: input.name,
name: input.id,
description: input.description,
mode: input.mode,
hidden: input.hidden,
@@ -54,7 +53,7 @@ export function runCommand(input: CurrentCommand): RunCommand {
export function runSkill(input: CurrentSkill): RunCommand {
return {
name: input.id,
name: input.name,
description: input.description,
source: "skill",
}
+2 -2
View File
@@ -333,10 +333,10 @@ export function createPromptState(input: PromptInput): PromptState {
.map((item) => ({
kind: "mention",
display: "@" + item.name,
value: item.id,
value: item.name,
part: {
type: "agent",
name: item.id,
name: item.name,
source: {
start: 0,
end: 0,
+1
View File
@@ -281,6 +281,7 @@ export async function runNonInteractivePrompt(input: Input) {
metadata: {
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
providerCall: current.provider,
providerResult: { executed: event.data.executed, state: event.data.resultState },
+8 -5
View File
@@ -89,11 +89,12 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
!explicitModel && !sessionModel
? await client.model
.default({ location: { directory: cwd, workspace } })
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
.then((result) =>
result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined,
)
: undefined
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
if (input.variant && !model)
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (input.variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (model) {
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
const available = await client.model.list({ location: { directory: cwd, workspace } })
@@ -111,7 +112,9 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
if (!session && input.title !== undefined) {
await client.session.rename({
sessionID: selected.id,
title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
title:
input.title ||
prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
})
}
@@ -197,7 +200,7 @@ async function validateAgent(client: OpenCodeClient, directory: string, name?: s
warning(`failed to list agents${server ? ` from ${server}` : ""}. Falling back to default agent`)
return
}
const agent = agents.find((item) => item.id === name)
const agent = agents.find((item) => item.name === name)
if (!agent) {
warning(`agent "${name}" not found. Falling back to default agent`)
return
+8 -6
View File
@@ -16,7 +16,7 @@
// backgrounding is intentionally absent: subagent jobs block the parent
// session, so only whole-session `v2.session.background(parentID)` exists.
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2"
import type { SessionMessage, SessionMessageAssistantTool, ToolPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@opencode-ai/tui/util/locale"
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"
@@ -54,7 +54,7 @@ export function legacyTool(input: {
callID: tool.id,
tool: tool.name,
}
if (tool.state.status === "streaming") {
if (tool.state.status === "pending") {
return {
...base,
state: { status: "pending", input: {}, raw: tool.state.input },
@@ -83,6 +83,7 @@ export function legacyTool(input: {
metadata: {
structured: tool.state.structured,
content: tool.state.content,
outputPaths: tool.state.outputPaths,
result: tool.state.result,
providerCall,
providerResult,
@@ -178,7 +179,7 @@ export type SubagentTrackerInput = {
export type SubagentTracker = {
main(event: V2Event): void
foreign(sessionID: string, event: V2Event): void
hydrate(next: { messages: SessionMessageInfo[]; active: Record<string, unknown> }): Promise<void>
hydrate(next: { messages: SessionMessage[]; active: Record<string, unknown> }): Promise<void>
select(sessionID: string | undefined): void
snapshot(): FooterSubagentState
}
@@ -315,7 +316,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
messageID,
tool: item,
})
if (item.state.status === "streaming") return
if (item.state.status === "pending") return
child.callIDs.add(item.id)
if (item.state.status === "running") {
setFrame(child, `tool:${item.id}`, toolCommit(part, "start"))
@@ -326,7 +327,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
setFrame(child, `tool:${item.id}`, toolCommit(part, "final"))
}
const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => {
const rebuild = (child: ChildState, messages: SessionMessage[]) => {
child.frames = []
child.text.clear()
child.projectedText.clear()
@@ -411,7 +412,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
for (const [id, prompt] of pendingPrompts) {
if (!child.prompts.has(id)) child.prompts.set(id, prompt)
}
rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[])
rebuild(child, structuredClone(response.data).toReversed() as SessionMessage[])
for (const [id, tool] of pendingTools) {
if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
}
@@ -621,6 +622,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
input: current?.input ?? {},
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
},
time: {
+16 -15
View File
@@ -2,7 +2,7 @@ import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/p
import type {
PermissionRequest,
QuestionRequest,
SessionMessageInfo,
SessionMessage,
SessionMessageAssistantTool,
} from "@opencode-ai/sdk/v2"
import { Event } from "@opencode-ai/schema/event"
@@ -389,7 +389,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
messageID,
tool: item,
})
if (item.state.status === "streaming") return
if (item.state.status === "pending") return
if (item.state.status === "running") {
if (state.tools.get(item.id)?.running) return
state.tools.set(item.id, {
@@ -417,7 +417,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
])
}
const renderMessage = (message: SessionMessageInfo, render: boolean, reuseVisibleWait: boolean) => {
const renderMessage = (message: SessionMessage, render: boolean, reuseVisibleWait: boolean) => {
if (message.type === "user") {
const waiting = state.wait?.messageID === message.id
if (waiting && state.wait) state.wait.promoted = true
@@ -438,34 +438,34 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
return
}
if (message.type === "shell") {
state.shellCommands.set(message.shellID, message.command)
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID
state.shellCommands.set(message.shell.id, message.shell.command)
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shell.id
const completed = message.time.completed !== undefined
if (!render) {
// Suppressed history: mark settled shells rendered so live redelivery
// stays silent. A still-running shell stays unmarked and renders in
// full when its live shell.ended event arrives.
if (completed) {
state.shellStarted.add(message.shellID)
state.shellEnded.add(message.shellID)
state.shellStarted.add(message.shell.id)
state.shellEnded.add(message.shell.id)
}
return
}
if (!state.shellStarted.has(message.shellID)) {
state.shellStarted.add(message.shellID)
if (!state.shellStarted.has(message.shell.id)) {
state.shellStarted.add(message.shell.id)
write([
shellCommit(message.shellID, message.command, {
shellCommit(message.shell.id, message.shell.command, {
text: "running shell",
phase: "start",
toolState: "running",
}),
])
}
if (completed && message.output && !state.shellEnded.has(message.shellID)) {
state.shellEnded.add(message.shellID)
write(shellTerminal(message.shellID, message.command, message, message.output))
if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
state.shellEnded.add(message.shell.id)
write(shellTerminal(message.shell.id, message.shell.command, message.shell, message.output))
}
if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve()
if (completed && state.shellWait?.callID === message.shell.id) state.shellWait.resolve()
return
}
if (message.type !== "assistant") return
@@ -534,7 +534,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
input.sdk.question.list({ sessionID: input.sessionID }),
input.sdk.session.active(),
])
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
const projected = structuredClone(messages.data).toReversed() as SessionMessage[]
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
state.permissions = permissions.map(permission)
state.questions = questions.map(question)
@@ -762,6 +762,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
input: current?.input ?? {},
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
},
time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created },
-1
View File
@@ -113,7 +113,6 @@ export type FooterQueuedPrompt = {
}
export type RunAgent = {
id: string
name: string
description?: string
mode: "subagent" | "primary" | "all"
+248 -254
View File
@@ -185,7 +185,6 @@ export type AgentListOutput = {
}
readonly data: ReadonlyArray<{
readonly id: string
readonly name: string
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly request: {
readonly settings: { readonly [x: string]: JsonValue }
@@ -346,12 +345,13 @@ export type SessionListOutput = {
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}>
@@ -408,12 +408,13 @@ export type SessionCreateOutput = {
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}
@@ -446,12 +447,13 @@ export type SessionGetOutput = {
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}
@@ -489,12 +491,13 @@ export type SessionForkOutput = {
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}
@@ -930,12 +933,13 @@ export type SessionRevertStageOutput = {
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}["data"]
@@ -990,6 +994,7 @@ export type SessionContextOutput = {
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly sessionID: string
readonly text: string
readonly description?: string
readonly type: "synthetic"
@@ -1006,7 +1011,6 @@ export type SessionContextOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly type: "skill"
readonly skill: string
readonly name: string
readonly text: string
}
@@ -1015,10 +1019,21 @@ export type SessionContextOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly shellID: string
readonly command: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
@@ -1049,7 +1064,7 @@ export type SessionContextOutput = {
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly state:
| { readonly status: "streaming"; readonly input: string }
| { readonly status: "pending"; readonly input: string }
| {
readonly status: "running"
readonly input: { readonly [x: string]: JsonValue }
@@ -1062,10 +1077,19 @@ export type SessionContextOutput = {
| {
readonly status: "completed"
readonly input: { readonly [x: string]: JsonValue }
readonly attachments?: ReadonlyArray<{
readonly data: string
readonly mime: string
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
readonly name?: string
readonly description?: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly outputPaths?: ReadonlyArray<string>
readonly structured: { readonly [x: string]: JsonValue }
readonly result?: JsonValue
}
@@ -1080,7 +1104,12 @@ export type SessionContextOutput = {
readonly error: { readonly type: string; readonly message: string }
readonly result?: JsonValue
}
readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number }
readonly time: {
readonly created: number
readonly ran?: number
readonly completed?: number
readonly pruned?: number
}
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
@@ -1099,37 +1128,16 @@ export type SessionContextOutput = {
readonly error: { readonly type: string; readonly message: string }
}
}
| (
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "running"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
}
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
}
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "failed"
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string }
}
)
| {
readonly type: "compaction"
readonly status: "queued" | "running" | "completed" | "failed"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
}
>
}["data"]
@@ -1167,7 +1175,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string }
}
@@ -1176,7 +1184,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1188,7 +1196,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.moved"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1201,7 +1209,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string }
}
@@ -1210,7 +1218,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.deleted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 2 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -1219,7 +1227,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
}
@@ -1228,7 +1236,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.promoted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
}
@@ -1237,7 +1245,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1265,7 +1273,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -1274,7 +1282,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -1283,7 +1291,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1295,7 +1303,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" }
}
@@ -1304,7 +1312,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.instructions.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string }
}
@@ -1313,7 +1321,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1327,21 +1335,16 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly id: string
readonly name: string
readonly text: string
}
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1364,7 +1367,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1393,7 +1396,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1408,7 +1411,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1430,7 +1433,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1450,7 +1453,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly ordinal: number }
}
@@ -1459,7 +1462,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1473,7 +1476,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1487,7 +1490,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1502,7 +1505,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1516,7 +1519,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1530,7 +1533,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1546,7 +1549,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.progress"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1564,7 +1567,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1575,6 +1578,7 @@ export type SessionLogOutput =
| { readonly type: "text"; readonly text: string }
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly outputPaths?: ReadonlyArray<string>
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
@@ -1585,7 +1589,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1602,7 +1606,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.retry.scheduled"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1617,7 +1621,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
}
@@ -1626,21 +1630,16 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly reason: "auto" | "manual"
readonly recent: string
readonly inputID?: string
}
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1654,21 +1653,16 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string }
readonly inputID?: string
}
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -1676,12 +1670,13 @@ export type SessionLogOutput =
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}
@@ -1691,7 +1686,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -1700,7 +1695,7 @@ export type SessionLogOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly to: string }
}
@@ -1760,6 +1755,7 @@ export type SessionMessageOutput = {
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly sessionID: string
readonly text: string
readonly description?: string
readonly type: "synthetic"
@@ -1776,7 +1772,6 @@ export type SessionMessageOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly type: "skill"
readonly skill: string
readonly name: string
readonly text: string
}
@@ -1785,10 +1780,21 @@ export type SessionMessageOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly shellID: string
readonly command: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
@@ -1819,7 +1825,7 @@ export type SessionMessageOutput = {
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly state:
| { readonly status: "streaming"; readonly input: string }
| { readonly status: "pending"; readonly input: string }
| {
readonly status: "running"
readonly input: { readonly [x: string]: JsonValue }
@@ -1832,10 +1838,19 @@ export type SessionMessageOutput = {
| {
readonly status: "completed"
readonly input: { readonly [x: string]: JsonValue }
readonly attachments?: ReadonlyArray<{
readonly data: string
readonly mime: string
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
readonly name?: string
readonly description?: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly outputPaths?: ReadonlyArray<string>
readonly structured: { readonly [x: string]: JsonValue }
readonly result?: JsonValue
}
@@ -1850,7 +1865,12 @@ export type SessionMessageOutput = {
readonly error: { readonly type: string; readonly message: string }
readonly result?: JsonValue
}
readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number }
readonly time: {
readonly created: number
readonly ran?: number
readonly completed?: number
readonly pruned?: number
}
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
@@ -1869,37 +1889,16 @@ export type SessionMessageOutput = {
readonly error: { readonly type: string; readonly message: string }
}
}
| (
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "running"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
}
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
}
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "failed"
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string }
}
)
| {
readonly type: "compaction"
readonly status: "queued" | "running" | "completed" | "failed"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
}
}["data"]
export type MessageListInput = {
@@ -1961,6 +1960,7 @@ export type MessageListOutput = {
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly sessionID: string
readonly text: string
readonly description?: string
readonly type: "synthetic"
@@ -1977,7 +1977,6 @@ export type MessageListOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly type: "skill"
readonly skill: string
readonly name: string
readonly text: string
}
@@ -1986,10 +1985,21 @@ export type MessageListOutput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number; readonly completed?: number }
readonly type: "shell"
readonly shellID: string
readonly command: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly shell: {
readonly id: string
readonly status: "running" | "exited" | "timeout" | "killed"
readonly command: string
readonly cwd: string
readonly shell: string
readonly file: string
readonly pid?: number
readonly exit?: number | "Infinity" | "-Infinity" | "NaN"
readonly metadata: { readonly [x: string]: JsonValue }
readonly time: {
readonly started: number | "Infinity" | "-Infinity" | "NaN"
readonly completed?: number | "Infinity" | "-Infinity" | "NaN"
}
}
readonly output?: {
readonly output: string
readonly cursor: number
@@ -2020,7 +2030,7 @@ export type MessageListOutput = {
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly state:
| { readonly status: "streaming"; readonly input: string }
| { readonly status: "pending"; readonly input: string }
| {
readonly status: "running"
readonly input: { readonly [x: string]: JsonValue }
@@ -2033,10 +2043,19 @@ export type MessageListOutput = {
| {
readonly status: "completed"
readonly input: { readonly [x: string]: JsonValue }
readonly attachments?: ReadonlyArray<{
readonly data: string
readonly mime: string
readonly source: { readonly type: "inline" } | { readonly type: "uri"; readonly uri: string }
readonly name?: string
readonly description?: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly outputPaths?: ReadonlyArray<string>
readonly structured: { readonly [x: string]: JsonValue }
readonly result?: JsonValue
}
@@ -2051,7 +2070,12 @@ export type MessageListOutput = {
readonly error: { readonly type: string; readonly message: string }
readonly result?: JsonValue
}
readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number }
readonly time: {
readonly created: number
readonly ran?: number
readonly completed?: number
readonly pruned?: number
}
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
@@ -2070,37 +2094,16 @@ export type MessageListOutput = {
readonly error: { readonly type: string; readonly message: string }
}
}
| (
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "running"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
}
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "completed"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
}
| {
readonly type: "compaction"
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly status: "failed"
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string }
}
)
| {
readonly type: "compaction"
readonly status: "queued" | "running" | "completed" | "failed"
readonly reason: "auto" | "manual"
readonly summary: string
readonly recent: string
readonly id: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
}
>
readonly cursor: { readonly previous?: string | null; readonly next?: string | null }
}
@@ -3905,7 +3908,6 @@ export type SkillListOutput = {
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: ReadonlyArray<{
readonly id: string
readonly name: string
readonly description?: string
readonly slash?: boolean
@@ -3961,7 +3963,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.created"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4023,7 +4025,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4085,7 +4087,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.deleted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4147,7 +4149,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4252,7 +4254,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.removed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string }
}
@@ -4261,7 +4263,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.part.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4500,7 +4502,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "message.part.removed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly messageID: string; readonly partID: string }
}
@@ -4509,7 +4511,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.agent.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly agent: string }
}
@@ -4518,7 +4520,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.model.selected"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4530,7 +4532,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.moved"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4543,7 +4545,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.renamed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly title: string }
}
@@ -4569,7 +4571,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.deleted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 2 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -4578,7 +4580,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.forked"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string }
}
@@ -4587,7 +4589,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.promoted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
}
@@ -4596,7 +4598,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.prompt.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4624,7 +4626,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -4633,7 +4635,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -4642,7 +4644,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly error: { readonly type: string; readonly message: string } }
}
@@ -4651,7 +4653,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" }
}
@@ -4660,7 +4662,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.instructions.updated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly text: string }
}
@@ -4669,7 +4671,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.synthetic"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4683,16 +4685,16 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.skill.activated"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly id: string; readonly name: string; readonly text: string }
readonly data: { readonly sessionID: string; readonly name: string; readonly text: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4715,7 +4717,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.shell.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4744,7 +4746,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4759,7 +4761,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4781,7 +4783,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.step.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4801,7 +4803,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly ordinal: number }
}
@@ -4823,7 +4825,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4837,7 +4839,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4864,7 +4866,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4879,7 +4881,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4906,7 +4908,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.input.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4920,7 +4922,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.called"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4936,7 +4938,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.progress"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4954,7 +4956,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.success"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4965,6 +4967,7 @@ export type EventSubscribeOutput =
| { readonly type: "text"; readonly text: string }
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly outputPaths?: ReadonlyArray<string>
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
@@ -4975,7 +4978,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.tool.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -4992,7 +4995,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.retry.scheduled"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5007,7 +5010,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly inputID: string }
}
@@ -5016,14 +5019,9 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly reason: "auto" | "manual"
readonly recent: string
readonly inputID?: string
}
readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" }
}
| {
readonly id: string
@@ -5038,7 +5036,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5052,21 +5050,16 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.compaction.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly reason: "auto" | "manual"
readonly error: { readonly type: string; readonly message: string }
readonly inputID?: string
}
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.staged"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
@@ -5074,12 +5067,13 @@ export type EventSubscribeOutput =
readonly messageID: string
readonly partID?: string
readonly snapshot?: string
readonly diff?: string
readonly files?: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly path: string
readonly status: "added" | "modified" | "deleted"
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly patch: string
}>
}
}
@@ -5089,7 +5083,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.cleared"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
@@ -5098,7 +5092,7 @@ export type EventSubscribeOutput =
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 }
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly to: string }
}
@@ -6219,11 +6213,11 @@ export type VcsDiffOutput = {
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: ReadonlyArray<{
readonly file: string
readonly patch: string
readonly file?: string
readonly patch?: string
readonly additions: number
readonly deletions: number
readonly status: "added" | "deleted" | "modified"
readonly status?: "added" | "deleted" | "modified"
}>
}
@@ -38,7 +38,7 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
expect(ProjectV2.Directory).toBe(Project.Directory)
expect(ProjectV2.Directories).toBe(Project.Directories)
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
expect(Api.groups["server.session"].identifier).toBe("server.session")
expect(Api.groups["server.project"].identifier).toBe("server.project")
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
+697 -683
View File
@@ -223,7 +223,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Agent.Info"
"$ref": "#/components/schemas/AgentV2.Info"
}
}
},
@@ -595,7 +595,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
@@ -778,7 +778,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
@@ -928,7 +928,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
@@ -2317,7 +2317,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Revert"
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
@@ -2624,7 +2624,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Message.Info"
"$ref": "#/components/schemas/Session.Message"
}
}
},
@@ -3331,7 +3331,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Message.Info"
"$ref": "#/components/schemas/Session.Message"
}
},
"required": [
@@ -3594,7 +3594,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Model.Info"
"$ref": "#/components/schemas/ModelV2.Info"
}
}
},
@@ -3705,7 +3705,7 @@
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Model.Info"
"$ref": "#/components/schemas/ModelV2.Info"
},
{
"type": "null"
@@ -7312,7 +7312,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Command.Info"
"$ref": "#/components/schemas/CommandV2.Info"
}
}
},
@@ -7413,7 +7413,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Skill.Info"
"$ref": "#/components/schemas/SkillV2.Info"
}
}
},
@@ -8508,7 +8508,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
}
},
@@ -8605,7 +8605,7 @@
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
},
"required": [
@@ -8750,7 +8750,7 @@
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
},
"required": [
@@ -8970,7 +8970,7 @@
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
},
"required": [
@@ -10200,7 +10200,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.Info"
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
@@ -10562,15 +10562,12 @@
"$ref": "#/components/schemas/PermissionV2.Rule"
}
},
"Agent.Info": {
"AgentV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
@@ -10611,7 +10608,6 @@
},
"required": [
"id",
"name",
"request",
"mode",
"hidden",
@@ -10631,46 +10627,6 @@
],
"additionalProperties": false
},
"Money.USD": {
"type": "number"
},
"TokenUsage.Info": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"Location.Ref": {
"type": "object",
"properties": {
@@ -10691,14 +10647,19 @@
],
"additionalProperties": false
},
"FileDiff.Info": {
"File.Diff": {
"type": "object",
"properties": {
"file": {
"path": {
"type": "string"
},
"patch": {
"type": "string"
"status": {
"type": "string",
"enum": [
"added",
"modified",
"deleted"
]
},
"additions": {
"type": "integer",
@@ -10716,25 +10677,20 @@
}
]
},
"status": {
"type": "string",
"enum": [
"added",
"deleted",
"modified"
]
"patch": {
"type": "string"
}
},
"required": [
"file",
"patch",
"path",
"status",
"additions",
"deletions",
"status"
"patch"
],
"additionalProperties": false
},
"Session.Revert": {
"Revert.State": {
"type": "object",
"properties": {
"messageID": {
@@ -10751,10 +10707,13 @@
"snapshot": {
"type": "string"
},
"diff": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.Info"
"$ref": "#/components/schemas/File.Diff"
}
}
},
@@ -10763,7 +10722,7 @@
],
"additionalProperties": false
},
"Session.Info": {
"SessionV2.Info": {
"type": "object",
"properties": {
"id": {
@@ -10817,10 +10776,44 @@
"$ref": "#/components/schemas/Model.Ref"
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"time": {
"type": "object",
@@ -10851,7 +10844,7 @@
"type": "string"
},
"revert": {
"$ref": "#/components/schemas/Session.Revert"
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
@@ -10871,7 +10864,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"cursor": {
@@ -11674,6 +11667,14 @@
],
"additionalProperties": false
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"text": {
"type": "string"
},
@@ -11690,6 +11691,7 @@
"required": [
"id",
"time",
"sessionID",
"text",
"type"
],
@@ -11771,9 +11773,6 @@
"skill"
]
},
"skill": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -11785,48 +11784,15 @@
"id",
"time",
"type",
"skill",
"name",
"text"
],
"additionalProperties": false
},
"Session.Message.Shell": {
"Shell": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"shell"
]
},
"shellID": {
"type": "string",
"allOf": [
{
@@ -11834,9 +11800,6 @@
}
]
},
"command": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
@@ -11846,6 +11809,26 @@
"killed"
]
},
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"shell": {
"type": "string"
},
"file": {
"type": "string"
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exit": {
"anyOf": [
{
@@ -11883,6 +11866,143 @@
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"started": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"completed": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"started"
],
"additionalProperties": false
}
},
"required": [
"id",
"status",
"command",
"cwd",
"shell",
"file",
"metadata",
"time"
],
"additionalProperties": false
},
"Session.Message.Shell": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"shell"
]
},
"shell": {
"$ref": "#/components/schemas/Shell"
},
"output": {
"type": "object",
"properties": {
@@ -11922,9 +12042,7 @@
"id",
"time",
"type",
"shellID",
"command",
"status"
"shell"
],
"additionalProperties": false
},
@@ -11987,13 +12105,13 @@
],
"additionalProperties": false
},
"Session.Message.ToolState.Streaming": {
"Session.Message.ToolState.Pending": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"streaming"
"pending"
]
},
"input": {
@@ -12103,12 +12221,24 @@
"input": {
"type": "object"
},
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.FileAttachment"
}
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"outputPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"structured": {
"type": "object"
},
@@ -12200,7 +12330,7 @@
"state": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.ToolState.Streaming"
"$ref": "#/components/schemas/Session.Message.ToolState.Pending"
},
{
"$ref": "#/components/schemas/Session.Message.ToolState.Running"
@@ -12224,6 +12354,9 @@
},
"completed": {
"type": "number"
},
"pruned": {
"type": "number"
}
},
"required": [
@@ -12353,10 +12486,44 @@
]
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"error": {
"$ref": "#/components/schemas/Session.StructuredError"
@@ -12375,7 +12542,7 @@
],
"additionalProperties": false
},
"Session.Message.Compaction.Running": {
"Session.Message.Compaction": {
"type": "object",
"properties": {
"type": {
@@ -12384,158 +12551,12 @@
"compaction"
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": [
"running"
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"summary": {
"type": "string"
},
"recent": {
"type": "string"
}
},
"required": [
"type",
"id",
"time",
"status",
"reason",
"summary",
"recent"
],
"additionalProperties": false
},
"Session.Message.Compaction.Completed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"compaction"
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": [
"completed"
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"summary": {
"type": "string"
},
"recent": {
"type": "string"
}
},
"required": [
"type",
"id",
"time",
"status",
"reason",
"summary",
"recent"
],
"additionalProperties": false
},
"Session.Message.Compaction.Failed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"compaction"
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": [
"queued",
"running",
"completed",
"failed"
]
},
@@ -12546,34 +12567,48 @@
"manual"
]
},
"error": {
"$ref": "#/components/schemas/Session.StructuredError"
"summary": {
"type": "string"
},
"recent": {
"type": "string"
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
}
},
"required": [
"type",
"id",
"time",
"status",
"reason",
"error"
"summary",
"recent",
"id",
"time"
],
"additionalProperties": false
},
"Session.Message.Compaction": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.Compaction.Running"
},
{
"$ref": "#/components/schemas/Session.Message.Compaction.Completed"
},
{
"$ref": "#/components/schemas/Session.Message.Compaction.Failed"
}
]
},
"Session.Message.Info": {
"Session.Message": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.AgentSelected"
@@ -12665,9 +12700,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -12750,9 +12787,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -12835,9 +12874,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -12923,9 +12964,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13008,9 +13051,11 @@
]
},
"version": {
"type": "number",
"enum": [
2
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13089,9 +13134,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13187,9 +13234,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13277,9 +13326,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13379,9 +13430,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13460,9 +13513,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13541,9 +13596,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13626,9 +13683,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13716,9 +13775,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13801,9 +13862,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13892,9 +13955,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13919,9 +13984,6 @@
}
]
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -13931,7 +13993,6 @@
},
"required": [
"sessionID",
"id",
"name",
"text"
],
@@ -13947,7 +14008,7 @@
],
"additionalProperties": false
},
"Shell": {
"Shell1": {
"type": "object",
"properties": {
"id": {
@@ -14125,9 +14186,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14153,7 +14216,7 @@
]
},
"shell": {
"$ref": "#/components/schemas/Shell"
"$ref": "#/components/schemas/Shell1"
}
},
"required": [
@@ -14210,9 +14273,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14238,7 +14303,7 @@
]
},
"shell": {
"$ref": "#/components/schemas/Shell"
"$ref": "#/components/schemas/Shell1"
},
"output": {
"type": "object",
@@ -14330,9 +14395,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14431,9 +14498,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14478,10 +14547,44 @@
]
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"snapshot": {
"type": "string"
@@ -14550,9 +14653,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14589,10 +14694,44 @@
"$ref": "#/components/schemas/Session.StructuredError"
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
}
},
"required": [
@@ -14650,9 +14789,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14749,9 +14890,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14855,9 +14998,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14960,9 +15105,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15066,9 +15213,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15164,9 +15313,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15265,9 +15416,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15370,9 +15523,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15478,9 +15633,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15525,6 +15682,12 @@
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"outputPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"result": {},
"executed": {
"type": "boolean"
@@ -15594,9 +15757,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15700,9 +15865,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15812,9 +15979,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15902,9 +16071,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15935,23 +16106,11 @@
"auto",
"manual"
]
},
"recent": {
"type": "string"
},
"inputID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
}
},
"required": [
"sessionID",
"reason",
"recent"
"reason"
],
"additionalProperties": false
}
@@ -16003,9 +16162,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16100,9 +16261,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16126,30 +16289,10 @@
"pattern": "^ses"
}
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"error": {
"$ref": "#/components/schemas/Session.StructuredError"
},
"inputID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
}
},
"required": [
"sessionID",
"reason",
"error"
"sessionID"
],
"additionalProperties": false
}
@@ -16201,9 +16344,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16229,7 +16374,7 @@
]
},
"revert": {
"$ref": "#/components/schemas/Session.Revert"
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
@@ -16286,9 +16431,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16367,9 +16514,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16419,7 +16568,7 @@
],
"additionalProperties": false
},
"Session.Event.Durable": {
"SessionDurableEvent": {
"oneOf": [
{
"$ref": "#/components/schemas/session.agent.selected"
@@ -16568,7 +16717,7 @@
"SessionLogItem": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Event.Durable"
"$ref": "#/components/schemas/SessionDurableEvent"
},
{
"$ref": "#/components/schemas/EventLog.Synced"
@@ -16588,7 +16737,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Message.Info"
"$ref": "#/components/schemas/Session.Message"
}
},
"cursor": {
@@ -16674,9 +16823,6 @@
],
"additionalProperties": false
},
"Money.USDPerMillionTokens": {
"type": "number"
},
"Model.Cost": {
"type": "object",
"properties": {
@@ -16700,19 +16846,19 @@
"additionalProperties": false
},
"input": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
},
"output": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
},
"write": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
}
},
"required": [
@@ -16729,7 +16875,7 @@
],
"additionalProperties": false
},
"Model.Info": {
"ModelV2.Info": {
"type": "object",
"properties": {
"id": {
@@ -19139,7 +19285,7 @@
],
"additionalProperties": false
},
"Command.Info": {
"CommandV2.Info": {
"type": "object",
"properties": {
"name": {
@@ -19167,12 +19313,9 @@
],
"additionalProperties": false
},
"Skill.Info": {
"SkillV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -19193,7 +19336,6 @@
}
},
"required": [
"id",
"name",
"location",
"content"
@@ -19427,7 +19569,7 @@
],
"additionalProperties": false
},
"FileDiff.LegacyInfo": {
"SnapshotFileDiff": {
"type": "object",
"properties": {
"file": {
@@ -19491,7 +19633,7 @@
"$ref": "#/components/schemas/PermissionRule"
}
},
"SessionV1.Info": {
"Session": {
"type": "object",
"properties": {
"id": {
@@ -19545,7 +19687,7 @@
"diffs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.LegacyInfo"
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
@@ -19760,9 +19902,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -19788,7 +19932,7 @@
]
},
"info": {
"$ref": "#/components/schemas/SessionV1.Info"
"$ref": "#/components/schemas/Session"
}
},
"required": [
@@ -19845,9 +19989,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -19873,7 +20019,7 @@
]
},
"info": {
"$ref": "#/components/schemas/SessionV1.Info"
"$ref": "#/components/schemas/Session"
}
},
"required": [
@@ -19930,9 +20076,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -19958,7 +20106,7 @@
]
},
"info": {
"$ref": "#/components/schemas/SessionV1.Info"
"$ref": "#/components/schemas/Session"
}
},
"required": [
@@ -20120,7 +20268,7 @@
"diffs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.LegacyInfo"
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
@@ -20788,9 +20936,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -20873,9 +21023,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -22341,9 +22493,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -22430,9 +22584,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -22529,10 +22685,44 @@
]
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
}
},
"required": [
@@ -23646,7 +23836,7 @@
"type": "object",
"properties": {
"info": {
"$ref": "#/components/schemas/Shell"
"$ref": "#/components/schemas/Shell1"
}
},
"required": [
@@ -26604,182 +26794,6 @@
],
"additionalProperties": false
},
"Shell1": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"status": {
"type": "string",
"enum": [
"running",
"exited",
"timeout",
"killed"
]
},
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"shell": {
"type": "string"
},
"file": {
"type": "string"
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exit": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"started": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"completed": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"started"
],
"additionalProperties": false
}
},
"required": [
"id",
"status",
"command",
"cwd",
"shell",
"file",
"metadata",
"time"
],
"additionalProperties": false
},
"ShellNotFoundError": {
"type": "object",
"properties": {
-2
View File
@@ -8,8 +8,6 @@ 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 -4
View File
@@ -305,7 +305,6 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const route: AnyRoute = {
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
provider: ProviderID.make(info.providerID),
providerMetadataKey: optionKey,
protocol: "ai-sdk",
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
auth: Auth.none,
@@ -418,7 +417,7 @@ function assistantPart(part: ContentPart): AssistantContent {
case "media":
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
case "reasoning":
return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }]
return [{ type: "reasoning", text: part.text }]
case "tool-call":
return [
{
@@ -427,7 +426,6 @@ function assistantPart(part: ContentPart): AssistantContent {
toolName: part.name,
input: part.input,
providerExecuted: part.providerExecuted,
providerOptions: providerOptions(part.providerMetadata),
},
]
case "tool-result":
@@ -443,7 +441,6 @@ function toolResultPart(part: ContentPart): ToolResultContent[] {
toolCallId: part.id,
toolName: part.name,
output: toolOutput(part.result),
providerOptions: providerOptions(part.providerMetadata),
},
]
}
+2 -3
View File
@@ -1,7 +1,6 @@
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"
@@ -92,8 +91,8 @@ export const Plugin = define({
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
+4 -5
View File
@@ -1,7 +1,6 @@
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)
@@ -18,8 +17,8 @@ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")(
}) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
read: Money.USDPerMillionTokens.pipe(Schema.optional),
write: Money.USDPerMillionTokens.pipe(Schema.optional),
read: Schema.Finite.pipe(Schema.optional),
write: Schema.Finite.pipe(Schema.optional),
}) {}
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
@@ -27,8 +26,8 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
type: Schema.Literal("context"),
size: Schema.Int,
}).pipe(Schema.optional),
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
input: Schema.Finite,
output: Schema.Finite,
cache: Cache.pipe(Schema.optional),
}) {}
-1
View File
@@ -48,6 +48,5 @@ 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[]
@@ -1,227 +0,0 @@
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))
}
+2 -2
View File
@@ -1,6 +1,6 @@
export * as File from "./file"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { Revert } from "@opencode-ai/schema/revert"
export const Diff = FileDiff.Info
export const Diff = Revert.FileDiff
export type Diff = typeof Diff.Type
+1 -1
View File
@@ -606,7 +606,7 @@ const layer = Layer.effect(
file,
])).text
return {
file,
path: file,
status,
additions: binary ? 0 : Number(stats[0] ?? 0),
deletions: binary ? 0 : Number(stats[1] ?? 0),
+13 -14
View File
@@ -2,7 +2,6 @@ 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"
@@ -19,10 +18,10 @@ export type CatalogModelStatus = typeof CatalogModelStatus.Type
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
const CostTier = Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
tier: Schema.Struct({
type: Schema.Literal("context"),
size: Schema.Finite,
@@ -30,17 +29,17 @@ const CostTier = Schema.Struct({
})
const Cost = Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
tiers: Schema.optional(Schema.Array(CostTier)),
context_over_200k: Schema.optional(
Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
}),
),
})
@@ -65,7 +64,7 @@ export const Model = Schema.Struct({
name: Schema.String,
family: Schema.optional(Schema.String),
release_date: Schema.String,
attachment: Schema.optional(Schema.Boolean),
attachment: Schema.Boolean,
reasoning: Schema.Boolean,
reasoning_options: Schema.optional(Schema.Array(ReasoningOption)),
temperature: Schema.optional(Schema.Boolean),
-7
View File
@@ -125,7 +125,6 @@ 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(
@@ -137,7 +136,6 @@ 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(
@@ -157,7 +155,6 @@ 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"
@@ -170,7 +167,6 @@ 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
@@ -193,7 +189,6 @@ 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
@@ -201,7 +196,6 @@ 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
@@ -209,7 +203,6 @@ 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
+2 -2
View File
@@ -31,7 +31,7 @@ import { SessionInstructions } from "../session/instructions"
import { SessionTodo } from "../session/todo"
import { Shell } from "../shell"
import { SkillV2 } from "../skill"
import { PatchTool } from "../tool/patch"
import { ApplyPatchTool } from "../tool/apply-patch"
import { EditTool } from "../tool/edit"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
@@ -127,7 +127,7 @@ const pre = [
SkillPlugin.Plugin,
ModelsDevPlugin,
...ProviderPlugins,
PatchTool.Plugin,
ApplyPatchTool.Plugin,
EditTool.Plugin,
GlobTool.Plugin,
GrepTool.Plugin,
+24 -39
View File
@@ -1,6 +1,5 @@
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import type { ModelInfo } from "@opencode-ai/sdk/v2/types"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
@@ -12,13 +11,13 @@ function released(date: string) {
return Number.isFinite(time) ? time : 0
}
function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
const base = {
input: input?.input ?? Money.USDPerMillionTokens.zero,
output: input?.output ?? Money.USDPerMillionTokens.zero,
input: input?.input ?? 0,
output: input?.output ?? 0,
cache: {
read: input?.cache_read ?? Money.USDPerMillionTokens.zero,
write: input?.cache_write ?? Money.USDPerMillionTokens.zero,
read: input?.cache_read ?? 0,
write: input?.cache_write ?? 0,
},
}
return [
@@ -28,8 +27,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
input: item.input,
output: item.output,
cache: {
read: item.cache_read ?? Money.USDPerMillionTokens.zero,
write: item.cache_write ?? Money.USDPerMillionTokens.zero,
read: item.cache_read ?? 0,
write: item.cache_write ?? 0,
},
})) ?? []),
...(input?.context_over_200k
@@ -42,8 +41,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero,
write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero,
read: input.context_over_200k.cache_read ?? 0,
write: input.context_over_200k.cache_write ?? 0,
},
},
]
@@ -51,13 +50,13 @@ function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
]
}
function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] | undefined) {
function mergeCost(base: ModelV2Info["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: ModelInfo["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelInfo["cost"][number], right: ModelInfo["cost"][number]) => ({
const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
...left,
...right,
tier: right.tier ?? left.tier,
@@ -68,25 +67,12 @@ function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] |
const current = tiers.get(tierKey(item))
tiers.set(tierKey(item), current ? merge(current, item) : item)
}
return [
merge(
baseDefault ?? {
input: Money.USDPerMillionTokens.zero,
output: Money.USDPerMillionTokens.zero,
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
nextDefault,
),
...tiers.values(),
]
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
}
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelInfo["variants"]> {
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options ?? []
const effort = options.find((option) => option.type === "effort")
@@ -131,7 +117,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<ModelInfo["variants"]> {
): NonNullable<ModelV2Info["variants"]> {
const max = option.max
const high =
option.max === undefined
@@ -160,7 +146,7 @@ function modeName(model: ModelsDev.Model, mode: string) {
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
}
function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]>) {
function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
const variants = model.variants ?? []
const existing = new Map(variants.map((variant) => [variant.id, variant]))
const nextIDs = new Set(next.map((variant) => variant.id))
@@ -171,13 +157,13 @@ function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]
}
function applyModel(
draft: ModelInfo,
draft: ModelV2Info,
model: ModelsDev.Model,
input: {
readonly name?: string
readonly cost?: ModelInfo["cost"]
readonly cost?: ModelV2Info["cost"]
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<ModelInfo["variants"]>
readonly variants?: NonNullable<ModelV2Info["variants"]>
} = {},
) {
draft.name = input.name ?? model.name
@@ -185,12 +171,11 @@ function applyModel(
draft.family = model.family ? ModelV2.Family.make(model.family) : undefined
draft.package = model.provider?.npm ? ProviderV2.aisdk(model.provider.npm) : undefined
draft.settings = model.provider?.api ? { ...draft.settings, baseURL: model.provider.api } : draft.settings
const capabilities = ModelV2.Capabilities.defaults({
draft.capabilities = {
tools: model.tool_call,
input: model.modalities?.input ?? (model.attachment === false ? ["text"] : undefined),
output: model.modalities?.output,
})
draft.capabilities = { ...capabilities, input: [...capabilities.input], output: [...capabilities.output] }
input: [...(model.modalities?.input ?? [])],
output: [...(model.modalities?.output ?? [])],
}
mergeVariants(draft, input.variants ?? [])
draft.time.released = released(model.release_date)
draft.cost = (input.cost ?? cost(model.cost)).map((item) => ({
+7 -11
View File
@@ -10,7 +10,6 @@ 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"
@@ -221,23 +220,20 @@ function withoutCredentials(body: Readonly<Record<string, unknown>> | undefined)
function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) {
const base = {
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),
},
input: input.input,
output: input.output,
cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 },
}
if (!input.context_over_200k) return [base]
return [
base,
{
tier: { type: "context" as const, size: 200_000 },
input: Money.USDPerMillionTokens.make(input.context_over_200k.input),
output: Money.USDPerMillionTokens.make(input.context_over_200k.output),
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: Money.USDPerMillionTokens.make(input.context_over_200k.cache_read ?? 0),
write: Money.USDPerMillionTokens.make(input.context_over_200k.cache_write ?? 0),
read: input.context_over_200k.cache_read ?? 0,
write: input.context_over_200k.cache_write ?? 0,
},
},
]
+10 -19
View File
@@ -33,8 +33,7 @@ export const Plugin = define({
SkillV2.EmbeddedSource.make({
type: "embedded",
skill: SkillV2.Info.make({
id: SkillV2.ID.make("opencode"),
name: SkillV2.Name.make("OpenCode"),
name: "opencode",
description: OpencodeDescription,
location: AbsolutePath.make("/builtin/opencode.md"),
content: OpencodeContent,
@@ -45,8 +44,7 @@ export const Plugin = define({
SkillV2.EmbeddedSource.make({
type: "embedded",
skill: SkillV2.Info.make({
id: SkillV2.ID.make("report"),
name: SkillV2.Name.make("Report"),
name: "report",
description: REPORT_DESCRIPTION,
slash: true,
location: AbsolutePath.make("/builtin/report.md"),
@@ -105,22 +103,15 @@ 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"
}
+16 -21
View File
@@ -1,7 +1,7 @@
export * as SessionV2 from "./session"
export * from "./session/schema"
import { Effect, Layer, Schema, Context, Stream, Scope } from "effect"
import { DateTime, 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,7 +19,6 @@ 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"
@@ -35,7 +34,7 @@ import { SessionEvent } from "./session/event"
import { SessionInput } from "./session/input"
import { Snapshot } from "./snapshot"
import { SessionRevert } from "./session/revert"
import { Session } from "@opencode-ai/schema/session"
import { Revert } from "@opencode-ai/schema/revert"
import { FSUtil } from "./fs-util"
import { Mime } from "./mime"
import type { EventLog } from "@opencode-ai/schema/event-log"
@@ -47,8 +46,8 @@ import { Shell as ShellSchema } from "@opencode-ai/schema/shell"
import { KeyedMutex } from "./effect/keyed-mutex"
import { fileURLToPath } from "url"
export const RevertState = Session.Revert
export type RevertState = Session.Revert
export const RevertState = Revert.State
export type RevertState = Revert.State
// get project -> project.locations
//
@@ -137,7 +136,7 @@ export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.Bus
sessionID: SessionSchema.ID,
}) {}
export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundError>()("Session.SkillNotFoundError", {
skill: SkillV2.ID,
skill: Schema.String,
}) {}
export const MessageNotFoundError = SessionRevert.MessageNotFoundError
export type MessageNotFoundError = SessionRevert.MessageNotFoundError
@@ -171,14 +170,14 @@ export interface Interface {
id: SessionMessage.ID
direction: "previous" | "next"
}
}) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
}) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
readonly message: (input: {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
}) => Effect.Effect<SessionMessage.Info | undefined>
}) => Effect.Effect<SessionMessage.Message | undefined>
readonly context: (
sessionID: SessionSchema.ID,
) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
/**
* Durable, ordered, gap-free session log read. Replays public durable
* session events after the exclusive `after` cursor, emits a `Synced`
@@ -192,10 +191,7 @@ export interface Interface {
after?: number
follow?: boolean
}) => Stream.Stream<SessionEvent.DurableEvent | EventLog.Synced, NotFoundError>
readonly switchAgent: (input: {
sessionID: SessionSchema.ID
agent: AgentV2.ID
}) => Effect.Effect<void, NotFoundError>
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect<void, NotFoundError>
readonly switchModel: (input: {
sessionID: SessionSchema.ID
model: ModelV2.Ref
@@ -213,7 +209,7 @@ export interface Interface {
sessionID: SessionSchema.ID
command: string
arguments?: string
agent?: AgentV2.ID
agent?: string
model?: ModelV2.Ref
files?: PromptInput.Prompt["files"]
agents?: PromptInput.Prompt["agents"]
@@ -231,7 +227,7 @@ export interface Interface {
readonly skill: (input: {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
skill: SkillV2.ID
skill: string
resume?: boolean
}) => Effect.Effect<void, NotFoundError | SkillNotFoundError>
readonly compact: (
@@ -254,7 +250,7 @@ export interface Interface {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
files?: boolean
}) => Effect.Effect<Session.Revert, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
}) => Effect.Effect<Revert.State, 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>
}
@@ -277,7 +273,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.Info)
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
const isDurableSessionEvent = Schema.is(SessionEvent.Durable)
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe(
@@ -326,7 +322,7 @@ const layer = Layer.effect(
variant: input.model.variant,
}
: undefined,
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: now, updated: now },
})
@@ -533,7 +529,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: AgentV2.ID.make(agent) })
yield* result.switchAgent({ sessionID: input.sessionID, agent })
if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model })
return yield* result.prompt({
@@ -595,13 +591,12 @@ 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.id === input.skill)
const skill = (yield* skills.list()).find((item) => item.name === 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,
},
+7 -32
View File
@@ -64,21 +64,19 @@ type Dependencies = {
export type AutoInput = {
readonly sessionID: SessionSchema.ID
readonly messages: readonly SessionMessage.Info[]
readonly messages: readonly SessionMessage.Message[]
readonly request: LLMRequest
}
type CompactInput = {
readonly sessionID: SessionSchema.ID
readonly messages: readonly SessionMessage.Info[]
readonly messages: readonly SessionMessage.Message[]
readonly model: Model
readonly inputID?: SessionMessage.ID
}
export type ManualInput = {
readonly session: SessionSchema.Info
readonly messages: readonly SessionMessage.Info[]
readonly inputID: SessionMessage.ID
readonly messages: readonly SessionMessage.Message[]
}
export interface Interface {
@@ -101,7 +99,7 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[
)
.join("\n")
const serialize = (message: SessionMessage.Info) => {
const serialize = (message: SessionMessage.Message) => {
if (message.type === "user") {
const files =
message.files?.map(
@@ -130,7 +128,7 @@ const serialize = (message: SessionMessage.Info) => {
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.command}\n${truncate(message.output?.output ?? "")}`
if (message.type === "shell") return `[Shell]: ${message.shell.command}\n${truncate(message.output?.output ?? "")}`
return ""
}
@@ -149,7 +147,7 @@ const settings = (documents: readonly Config.Entry[]) => {
}
const select = (
messages: readonly SessionMessage.Info[],
messages: readonly SessionMessage.Message[],
tokens: number,
): { readonly head: string; readonly recent: string } | undefined => {
const conversation = messages
@@ -200,7 +198,6 @@ 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
@@ -211,8 +208,6 @@ 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[] = []
@@ -240,27 +235,9 @@ 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()) {
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
}
if (!summarized || failed || !summary.trim()) return false
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
sessionID: input.sessionID,
reason: input.reason,
@@ -307,7 +284,6 @@ const make = (dependencies: Dependencies) => {
),
recent: forcedShortContext ? "" : selected.recent,
output: input.output,
inputID: input.inputID,
})
})
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: CompactInput) {
@@ -351,7 +327,6 @@ export const layer = Layer.effect(
sessionID: input.session.id,
messages: input.messages,
model: resolved.model,
inputID: input.inputID,
})
}),
})
+1 -1
View File
@@ -8,7 +8,7 @@ import { InstructionCheckpointTable, SessionMessageTable } from "./sql"
type DatabaseService = Database.Interface["db"]
const decode = Schema.decodeUnknownEffect(SessionMessage.Info)
const decode = Schema.decodeUnknownEffect(SessionMessage.Message)
export const latestCompaction = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
return yield* db
+4 -7
View File
@@ -1,4 +1,4 @@
import { DateTime, Schema } from "effect"
import { DateTime } from "effect"
import { AgentV2 } from "../agent"
import { Location } from "../location"
import { ModelV2 } from "../model"
@@ -9,10 +9,7 @@ import { WorkspaceV2 } from "../workspace"
import { SessionSchema } from "./schema"
import { SessionTable } from "./sql"
import { SessionMessage } from "./message"
import { PersistedRevert } from "@opencode-ai/schema/session-revert"
import { Money } from "@opencode-ai/schema/money"
const decodeRevert = Schema.decodeUnknownSync(PersistedRevert)
import { Snapshot } from "../snapshot"
export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info {
return SessionSchema.Info.make({
@@ -34,7 +31,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In
variant: ModelV2.VariantID.make(row.model.variant ?? "default"),
}
: undefined,
cost: Money.USD.make(row.cost),
cost: row.cost,
tokens: {
input: row.tokens_input,
output: row.tokens_output,
@@ -49,7 +46,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 ? decodeRevert(row.revert) : undefined,
revert: row.revert ? { ...row.revert, messageID: SessionMessage.ID.make(row.revert.messageID) } : undefined,
time: {
created: DateTime.makeUnsafe(row.time_created),
updated: DateTime.makeUnsafe(row.time_updated),
+3 -3
View File
@@ -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, Info, PromptEntry } from "@opencode-ai/schema/session-input"
import { Admitted, Compaction, Delivery, Entry, 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, Info, PromptEntry }
export { Admitted, Compaction, Delivery, Entry, 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): Info => {
const fromRow = (row: typeof SessionInputTable.$inferSelect): Entry => {
const base = {
admittedSeq: row.admitted_seq,
id: SessionMessage.ID.make(row.id),
+30 -41
View File
@@ -4,7 +4,7 @@ import { SessionEvent } from "./event"
import { SessionMessage } from "./message"
export type MemoryState = {
messages: SessionMessage.Info[]
messages: SessionMessage.Message[]
}
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["shellID"],
shellID: SessionMessage.Shell["shell"]["id"],
) => 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.Info) => Effect.Effect<void, never, never>
readonly appendMessage: (message: SessionMessage.Message) => Effect.Effect<void, never, never>
}
export function memory(state: MemoryState): Adapter {
@@ -29,7 +29,9 @@ 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 === "running")
state.messages.findLastIndex(
(message) => message.type === "compaction" && (message.status === "queued" || 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")
@@ -62,7 +64,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.shellID === shellID
return message.type === "shell" && message.shell.id === shellID
})
})
},
@@ -184,13 +186,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,
@@ -205,10 +207,8 @@ 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,9 +219,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
id: SessionMessage.ID.fromEvent(event.id),
type: "shell",
metadata: event.metadata,
shellID: event.data.shell.id,
command: event.data.shell.command,
status: event.data.shell.status,
shell: event.data.shell,
time: { created: event.created },
}),
)
@@ -232,8 +230,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
if (currentShell) {
yield* adapter.updateShell(
produce(currentShell, (draft) => {
draft.status = event.data.shell.status
draft.exit = event.data.shell.exit
draft.shell = castDraft(event.data.shell)
draft.output = event.data.output
draft.time.completed = event.created
}),
@@ -273,7 +270,6 @@ 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,
@@ -333,7 +329,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
id: event.data.callID,
name: event.data.name,
time: { created: event.created },
state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: "" }),
state: SessionMessage.ToolStatePending.make({ status: "pending", input: "" }),
}),
),
)
@@ -343,7 +339,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 === "streaming") match.state.input = event.data.text
if (match && match.state.status === "pending") match.state.input = event.data.text
})
},
"session.tool.called": (event) => {
@@ -386,6 +382,7 @@ 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,
}),
)
@@ -395,7 +392,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 === "streaming" || match.state.status === "running")) {
if (match && (match.state.status === "pending" || match.state.status === "running")) {
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.time.completed = event.created
@@ -451,30 +448,31 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
}
})
},
"session.compaction.admitted": () => Effect.void,
"session.compaction.started": (event) =>
"session.compaction.admitted": (event) =>
adapter.appendMessage(
SessionMessage.CompactionRunning.make({
id: event.data.inputID ?? SessionMessage.ID.fromEvent(event.id),
SessionMessage.Compaction.make({
id: event.data.inputID,
type: "compaction",
status: "running",
status: "queued",
metadata: event.metadata,
reason: event.data.reason,
reason: "manual",
summary: "",
recent: event.data.recent ?? "",
recent: "",
time: { created: event.created },
}),
),
"session.compaction.delta": (event) =>
"session.compaction.started": (event) =>
Effect.gen(function* () {
if (event.data.reason !== "manual") return
const current = yield* adapter.getCompaction()
if (current?.status !== "running") return
yield* adapter.updateCompaction({ ...current, summary: current.summary + event.data.text })
if (!current) return
yield* adapter.updateCompaction({ ...current, status: "running" })
}),
"session.compaction.delta": () => Effect.void,
"session.compaction.ended": (event) => {
return Effect.gen(function* () {
const current = yield* adapter.getCompaction()
if (current?.status === "running") {
const current = event.data.reason === "manual" ? yield* adapter.getCompaction() : undefined
if (current) {
yield* adapter.updateCompaction({
...current,
status: "completed",
@@ -498,20 +496,11 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
)
})
},
"session.compaction.failed": (event) =>
"session.compaction.failed": () =>
Effect.gen(function* () {
const current = yield* adapter.getCompaction()
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)
if (!current) return
yield* adapter.updateCompaction({ ...current, status: "failed" })
}),
"session.revert.staged": () => Effect.void,
"session.revert.cleared": () => Effect.void,
+40 -40
View File
@@ -24,14 +24,15 @@ 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 CurrentDurableEvent = Extract<SessionEvent.Event, { readonly durable: object }>
type MessageEvent = Exclude<CurrentDurableEvent, typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type>
type MessageEvent = Exclude<
SessionEvent.DurableEvent,
typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type
>
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
const encodeMessage = Schema.encodeSync(SessionMessage.Info)
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
export class SessionAlreadyProjected extends Error {}
@@ -86,14 +87,7 @@ 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
? {
messageID: SessionMessage.ID.make(info.revert.messageID),
partID: info.revert.partID,
snapshot: info.revert.snapshot,
diff: info.revert.diff,
}
: null,
revert: info.revert ? { ...info.revert, messageID: SessionMessage.ID.make(info.revert.messageID) } : null,
permission: info.permission ? [...info.permission] : undefined,
time_created: info.time.created,
time_updated: info.time.updated,
@@ -157,7 +151,7 @@ const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function*
if (!row) return
yield* events.publish(SessionEvent.UsageUpdated, {
sessionID,
cost: Money.USD.make(row.cost),
cost: row.cost,
tokens: {
input: row.input,
output: row.output,
@@ -263,7 +257,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') != 'running'`,
sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`,
),
)
.orderBy(asc(SessionMessageTable.seq))
@@ -286,7 +280,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
seq: row.seq,
time_created: row.time_created,
time_updated: row.time_updated,
data: row.data,
data: row.type === "synthetic" ? { ...row.data, sessionID: event.data.sessionID } : row.data,
}
}),
)
@@ -342,7 +336,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.Info) => {
const updateMessage = (message: SessionMessage.Message) => {
if (event.durable === undefined)
return Effect.die(new Error("Durable Session event is missing aggregate sequence"))
const encoded = encodeMessage(message)
@@ -359,7 +353,7 @@ function run(db: DatabaseService, event: MessageEvent) {
.run()
.pipe(Effect.orDie)
}
const appendMessage = (message: SessionMessage.Info) => insertMessage(db, event, message)
const appendMessage = (message: SessionMessage.Message) => insertMessage(db, event, message)
const adapter: SessionMessageUpdater.Adapter = {
getModel() {
return db
@@ -418,7 +412,7 @@ function run(db: DatabaseService, event: MessageEvent) {
and(
eq(SessionMessageTable.session_id, event.data.sessionID),
eq(SessionMessageTable.type, "shell"),
sql`json_extract(${SessionMessageTable.data}, '$.shellID') = ${shellID}`,
sql`json_extract(${SessionMessageTable.data}, '$.shell.id') = ${shellID}`,
),
)
.orderBy(desc(SessionMessageTable.seq))
@@ -439,7 +433,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') = 'running'`,
sql`json_extract(${SessionMessageTable.data}, '$.status') in ('queued', 'running')`,
),
)
.orderBy(desc(SessionMessageTable.seq))
@@ -460,7 +454,7 @@ function run(db: DatabaseService, event: MessageEvent) {
})
}
function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Info) {
function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Message) {
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
@@ -667,12 +661,14 @@ const layer = Layer.effectDiscard(
Effect.gen(function* () {
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
yield* SessionInput.projectCompactionAdmitted(db, {
const admitted = 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))
@@ -680,7 +676,15 @@ 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) => 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.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))
@@ -726,26 +730,22 @@ 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"))
if (event.data.reason === "manual")
yield* SessionInput.settleCompaction(db, {
sessionID: event.data.sessionID,
handledSeq: event.durable.seq,
})
yield* SessionInput.settleCompaction(db, {
sessionID: event.data.sessionID,
handledSeq: event.durable.seq,
})
}),
)
yield* events.project(SessionEvent.RevertEvent.Staged, (event) =>
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)
}),
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),
)
yield* events.project(SessionEvent.RevertEvent.Cleared, (event) =>
db
+8 -4
View File
@@ -1,7 +1,7 @@
export * as SessionRevert from "./revert"
import { and, asc, eq, gt } from "drizzle-orm"
import { Effect, Schema } from "effect"
import { DateTime, 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.Info)
const decode = Schema.decodeUnknownEffect(SessionMessage.Message)
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(RelativePath.make(file.file), original)
for (const file of input.session.revert?.files ?? []) restore.set(file.path, original)
}
if (input.files !== false) for (const [file, tree] of next) restore.set(file, tree)
if (restore.size) yield* snapshot.restore({ files: restore })
@@ -81,6 +81,10 @@ 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, {
@@ -96,7 +100,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) => [RelativePath.make(file.file), original])),
files: new Map((session.revert.files ?? []).map((file) => [file.path, original])),
})
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.RevertEvent.Cleared, {
+17 -54
View File
@@ -11,7 +11,6 @@ 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"
@@ -68,13 +67,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 Money.USD.zero
return Money.USD.make(
if (!cost) return 0
return (
(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
)
}
@@ -166,7 +165,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 !== "streaming" && tool.state.status !== "running")) continue
if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID,
assistantMessageID: message.id,
@@ -233,14 +232,12 @@ const layer = Layer.effect(
}
const resolved = yield* models.resolve(session)
const model = resolved.model
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const entries = yield* SessionHistory.entriesForRunner(db, session.id, checkpoint.baselineSeq)
const context = entries.map((entry) => entry.message)
const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
const toolMaterialization =
isLastStep || !resolved.capabilities.tools
? undefined
: yield* tools.materialize({ permissions: agent.info?.permissions, model })
const toolMaterialization = isLastStep
? undefined
: yield* tools.materialize({ permissions: agent.info?.permissions, model })
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
const request = LLM.request({
model,
@@ -252,7 +249,7 @@ const layer = Layer.effect(
.filter((part): part is string => part !== undefined && part.length > 0)
.map(SystemPart.make),
messages: [
...toLLMMessages(context, resolved.ref, providerMetadataKey, resolved.capabilities),
...toLLMMessages(context, resolved.ref),
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
],
tools: toolMaterialization?.definitions ?? [],
@@ -295,7 +292,7 @@ const layer = Layer.effect(
// The selected catalog identity, not model.id: route-level ids are provider API
// model ids (for example gpt-5.5-fast resolves to api id gpt-5.5).
model: resolved.ref,
providerMetadataKey,
provider: model.provider,
snapshot: startSnapshot,
assistantMessageID,
})
@@ -303,7 +300,8 @@ 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, error?: SessionError.Error) => serialized(publisher.publish(event, error))
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = [], error?: SessionError.Error) =>
serialized(publisher.publish(event, outputPaths, error))
let overflowFailure: ProviderErrorEvent | undefined
const providerStream = llm.stream(hookedRequest).pipe(
Stream.runForEach((event) =>
@@ -317,25 +315,10 @@ const layer = Layer.effect(
}
yield* publish(event)
if (event.type !== "tool-call" || event.providerExecuted) return
if (!toolMaterialization) {
if (!toolMaterialization || !advertisedTools.has(event.name)) {
yield* serialized(
publisher.failUnsettledTools({
type: "tool.execution",
message: "Tools are disabled after the maximum agent steps",
}),
)
return
}
// A request hook hid this registered tool from the current request. Fail only
// this call durably and continue so the model can react, instead of executing
// a tool that was not advertised. Unregistered tools flow through settle, which
// durably fails them as unknown.
if (!advertisedTools.has(event.name) && availableTools.has(event.name)) {
needsContinuation = true
yield* publish(
LLMEvent.toolError({
id: event.id,
name: event.name,
message: `Tool is not available for this request: ${event.name}`,
}),
)
@@ -361,6 +344,7 @@ const layer = Layer.effect(
result: settlement.result,
output: settlement.output,
}),
settlement.outputPaths ?? [],
settlement.error,
).pipe(
Effect.andThen(
@@ -600,30 +584,12 @@ 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
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,
})
yield* events.publish(SessionEvent.Compaction.Failed, { sessionID })
if (Exit.isFailure(compacted)) return yield* Effect.failCause(compacted.cause)
return true
}),
)
@@ -658,13 +624,10 @@ const layer = Layer.effect(
}
needsContinuation = result.needsContinuation
step = result.step + 1
if (needsContinuation) {
promotion = (yield* SessionInput.pendingCompaction(db, input.sessionID)) ? undefined : "steer"
continue
}
// Manual compaction is a barrier: handle it before continuing or promoting steers.
yield* runPendingCompaction(input.sessionID)
promotion = "steer"
needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer")
if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer")
}
yield* runPendingCompaction(input.sessionID)
const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, "steer")
+1 -10
View File
@@ -82,8 +82,6 @@ export interface Resolved {
readonly model: Model
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
readonly ref: ModelV2.Ref
/** Capabilities of the selected catalog model. */
readonly capabilities: ModelV2.Capabilities
/** Catalog pricing in dollars per million tokens. */
readonly cost: ModelV2.Info["cost"]
}
@@ -98,19 +96,13 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
export const resolved = (
model: Model,
variant?: ModelV2.VariantID,
cost: ModelV2.Info["cost"] = [],
capabilities = ModelV2.Capabilities.defaults(),
): Resolved => ({
export const resolved = (model: Model, variant?: ModelV2.VariantID, cost: ModelV2.Info["cost"] = []): Resolved => ({
model,
ref: ModelV2.Ref.make({
id: ModelV2.ID.make(model.id),
providerID: ProviderV2.ID.make(model.provider),
...(variant === undefined ? {} : { variant }),
}),
capabilities,
cost,
})
@@ -352,7 +344,6 @@ const layer = Layer.effect(
providerID: selected.providerID,
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
}),
capabilities: selected.capabilities,
cost: selected.cost,
}
}),
@@ -6,16 +6,13 @@ 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: AgentV2.ID
readonly agent: string
readonly model: ModelV2.Ref
readonly providerMetadataKey: string
readonly snapshot?: Snapshot.ID
readonly provider: string
readonly snapshot?: string
readonly assistantMessageID?: SessionMessage.ID
}
@@ -87,9 +84,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
assistantMessageID ??= SessionMessage.ID.create()
stepStarted = true
yield* events.publish(SessionEvent.Step.Started, {
sessionID: input.sessionID,
agent: input.agent,
model: input.model,
...input,
assistantMessageID,
snapshot: input.snapshot,
})
@@ -99,7 +94,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
assistantMessageID === undefined
? Effect.die(new Error("Tool event before assistant step start"))
: Effect.succeed(assistantMessageID)
const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.providerMetadataKey]
const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.provider]
const fragments = (
name: string,
ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>,
@@ -230,7 +226,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
})
const publishStepFailure = Effect.fnUntraced(function* (usage?: {
readonly cost: Money.USD
readonly cost: number
readonly tokens: ReturnType<typeof tokens>
}) {
if (stepFailed || stepFailure === undefined) return
@@ -269,7 +265,11 @@ 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, error?: SessionError.Error) {
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (
event: LLMEvent,
outputPaths: ReadonlyArray<string> = [],
error?: SessionError.Error,
) {
switch (event.type) {
case "step-start":
yield* startAssistant()
@@ -353,13 +353,14 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
tool.called = true
tool.providerExecuted = event.providerExecuted === true
const state = providerState(event.providerMetadata)
yield* events.publish(SessionEvent.Tool.Called, {
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
callID: event.id,
input: record(event.input),
executed: tool.providerExecuted,
state: providerState(event.providerMetadata),
state,
})
return
}
@@ -394,6 +395,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
assistantMessageID: tool.assistantMessageID,
callID: event.id,
...result,
outputPaths,
...(executed ? { result: event.result } : {}),
executed,
resultState,
@@ -11,6 +11,8 @@ import type { ModelV2 } from "../../model"
import { SessionMessage } from "../message"
import type { FileAttachment } from "@opencode-ai/schema/prompt"
const imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"])
const media = (file: FileAttachment): ContentPart => ({
type: "media",
mediaType: file.mime,
@@ -19,23 +21,6 @@ const media = (file: FileAttachment): ContentPart => ({
metadata: file.description === undefined ? undefined : { description: file.description },
})
const modality = (mime: string) => {
if (mime.startsWith("image/")) return "image"
if (mime.startsWith("audio/")) return "audio"
if (mime.startsWith("video/")) return "video"
if (mime === "application/pdf") return "pdf"
return undefined
}
const attachment = (file: FileAttachment, capabilities?: ModelV2.Capabilities): ContentPart => {
const type = modality(file.mime)
if (!type || (capabilities?.input ?? ["text", "image"]).includes(type)) return media(file)
return {
type: "text",
text: `ERROR: Cannot read ${file.name ? `"${file.name}"` : type} (this model does not support ${type} input). Inform the user.`,
}
}
const textAttachment = (file: FileAttachment) =>
Message.make({
role: "user",
@@ -84,7 +69,7 @@ const providerMetadata = (
): ProviderMetadata | undefined => (state === undefined ? undefined : { [provider]: state })
const toolInput = (tool: SessionMessage.AssistantTool) =>
tool.state.status === "streaming"
tool.state.status === "pending"
? Option.getOrElse(decodeToolInput(tool.state.input), () => tool.state.input)
: tool.state.input
@@ -128,7 +113,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
}
}
const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, providerMetadataKey: string) => {
const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
const sameModel =
String(message.model.providerID) === String(model.providerID) && String(message.model.id) === String(model.id)
const reuseProviderMetadata = sameModel && message.error === undefined
@@ -140,7 +125,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid
{
type: "reasoning",
text: item.text,
providerMetadata: providerMetadata(providerMetadataKey, item.state),
providerMetadata: providerMetadata(model.providerID, item.state),
},
]
: item.text.length > 0
@@ -148,13 +133,13 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid
: []
const call = toolCall(
item,
reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.providerState) : undefined,
reuseProviderMetadata ? providerMetadata(model.providerID, item.providerState) : undefined,
)
if (item.executed !== true) return [call]
const result = toolResult(
item,
reuseProviderMetadata
? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState)
? providerMetadata(model.providerID, item.providerResultState ?? item.providerState)
: undefined,
)
return result ? [call, result] : [call]
@@ -170,7 +155,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid
toolResult(
item,
reuseProviderMetadata
? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState)
? providerMetadata(model.providerID, item.providerResultState ?? item.providerState)
: undefined,
),
)
@@ -183,12 +168,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, provid
]
}
function toLLMMessage(
message: SessionMessage.Info,
model: ModelV2.Ref,
providerMetadataKey: string,
capabilities?: ModelV2.Capabilities,
): Message[] {
function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Message[] {
switch (message.type) {
case "agent-switched":
case "model-switched":
@@ -203,9 +183,7 @@ function toLLMMessage(
role: "user",
content: [
{ type: "text", text: message.text },
...files
.filter((file) => file.mime !== "text/plain" && file.mime !== "application/x-directory")
.map((file) => attachment(file, capabilities)),
...files.filter((file) => imageMimes.has(file.mime)).map(media),
],
metadata: {
...message.metadata,
@@ -224,12 +202,12 @@ function toLLMMessage(
Message.make({
id: message.id,
role: "user",
content: `Shell command: ${message.command}\n\n${message.output?.output ?? ""}`,
content: `Shell command: ${message.shell.command}\n\n${message.output?.output ?? ""}`,
metadata: message.metadata,
}),
]
case "assistant":
return assistant(message, model, providerMetadataKey)
return assistant(message, model)
case "compaction":
if (message.status !== "completed") return []
return [
@@ -254,9 +232,5 @@ ${message.recent}
}
/** Translate projected V2 Session history into canonical @opencode-ai/llm context. */
export const toLLMMessages = (
messages: readonly SessionMessage.Info[],
model: ModelV2.Ref,
providerMetadataKey: string = model.providerID,
capabilities?: ModelV2.Capabilities,
) => messages.flatMap((message) => toLLMMessage(message, model, providerMetadataKey, capabilities))
export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: ModelV2.Ref) =>
messages.flatMap((message) => toLLMMessage(message, model))
+6 -7
View File
@@ -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 { FileDiff } from "@opencode-ai/schema/file-diff"
import type { Snapshot } from "../snapshot"
import { PermissionV1 } from "../v1/permission"
import { ProjectV2 } from "../project"
import type { SessionSchema } from "./schema"
@@ -13,11 +13,10 @@ 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 { Session } from "@opencode-ai/schema/session"
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
import type { Revert } from "@opencode-ai/schema/revert"
import type { Schema } from "effect"
type SessionMessageData = Omit<(typeof SessionMessage.Info)["Encoded"], "type" | "id">
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
type V1PartData = Omit<SessionV1.Part, "id" | "sessionID" | "messageID">
@@ -42,7 +41,7 @@ export const SessionTable = sqliteTable(
summary_additions: integer(),
summary_deletions: integer(),
summary_files: integer(),
summary_diffs: text({ mode: "json" }).$type<FileDiff.LegacyInfo[]>(),
summary_diffs: text({ mode: "json" }).$type<Snapshot.LegacyFileDiff[]>(),
metadata: text({ mode: "json" }).$type<Record<string, unknown>>(),
cost: real().notNull().default(0),
tokens_input: integer().notNull().default(0),
@@ -50,7 +49,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<Session.Revert | RevertV1>(),
revert: text({ mode: "json" }).$type<Revert.State>(),
permission: text({ mode: "json" }).$type<PermissionV1.Ruleset>(),
agent: text(),
model: text({ mode: "json" }).$type<{
@@ -149,7 +148,7 @@ export const SessionInputTable = sqliteTable(
.$type<SessionSchema.ID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
type: text().$type<SessionInput.Info["type"]>().notNull(),
type: text().$type<SessionInput.Entry["type"]>().notNull(),
prompt: text({ mode: "json" }).$type<Prompt>(),
delivery: text().$type<SessionInput.Delivery>(),
admitted_seq: integer().notNull(),
+3 -3
View File
@@ -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.Info[], MessageDecodeError>
readonly context: (sessionID: Session.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
readonly message: (
messageID: SessionMessage.ID,
) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Info } | undefined>
) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Message } | 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.Info)
const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message)
return Service.of({
get: Effect.fn("SessionStore.get")(function* (sessionID) {
+15 -21
View File
@@ -28,15 +28,11 @@ 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.id, agent.permissions).effect !== "deny")
skills.filter((skill) => PermissionV2.evaluate("skill", skill.name, agent.permissions).effect !== "deny")
const Frontmatter = Schema.Struct({
name: Schema.String.pipe(Schema.optional),
@@ -100,7 +96,7 @@ const layer = Layer.effect(
source: Source.key(source),
type: source.type,
directories: [],
skills: [source.skill.id],
skills: [source.skill.name],
})
return { skills: [source.skill], directories: [] }
}
@@ -116,13 +112,15 @@ const layer = Layer.effect(
if (!markdown) continue
const frontmatter = decodeFrontmatter(markdown.data).valueOrUndefined
if (!frontmatter) continue
const id =
path.dirname(filepath) === directory
? path.basename(filepath, ".md")
: path.basename(path.dirname(filepath))
const name =
frontmatter.name !== undefined
? frontmatter.name
: path.dirname(filepath) === directory
? path.basename(filepath, ".md")
: undefined
if (!name) continue
skills.push({
id: ID.make(id),
name: Name.make(frontmatter.name ?? id),
name,
description: frontmatter.description,
slash: metadataBoolean(frontmatter.metadata, "opencode/slash") ?? frontmatter.slash,
autoinvoke: metadataBoolean(frontmatter.metadata, "opencode/autoinvoke"),
@@ -135,7 +133,7 @@ const layer = Layer.effect(
source: Source.key(source),
type: source.type,
directories,
skills: skills.map((skill) => skill.id),
skills: skills.map((skill) => skill.name),
})
return { skills, directories }
})
@@ -150,7 +148,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.id)),
skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.name)),
})
yield* events.publish(Event.Updated, {}).pipe(Effect.asVoid)
})
@@ -161,12 +159,12 @@ const layer = Layer.effect(
)
const list = Effect.fn("SkillV2.list")(function* () {
const skills = new Map<ID, Info>()
const skills = new Map<string, 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.id, skill)
for (const skill of loaded.skills) skills.set(skill.name, skill)
}
return Array.from(skills.values())
})
@@ -182,8 +180,4 @@ 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] })
+6 -8
View File
@@ -8,8 +8,7 @@ import { SkillV2 } from "../skill"
import { Instructions } from "../instructions/index"
const Summary = Schema.Struct({
id: SkillV2.ID,
name: SkillV2.Name,
name: Schema.String,
description: Schema.String,
})
type Summary = typeof Summary.Type
@@ -17,7 +16,6 @@ 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>",
@@ -36,8 +34,8 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
const diff = Instructions.diffByKey(
previous,
current,
(skill) => skill.id,
(before, after) => before.name !== after.name || before.description !== after.description,
(skill) => skill.name,
(before, after) => 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))
@@ -52,7 +50,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
...(diff.removed.length === 0
? []
: [
`The following skill IDs are no longer available and must not be used: ${diff.removed.map((skill) => skill.id).join(", ")}.`,
`The following skills are no longer available and must not be used: ${diff.removed.map((skill) => skill.name).join(", ")}.`,
]),
].join("\n")
}
@@ -79,9 +77,9 @@ const layer = Layer.effect(
.flatMap((skill) =>
skill.description === undefined || skill.autoinvoke === false
? []
: [{ id: skill.id, name: skill.name, description: skill.description }],
: [{ name: skill.name, description: skill.description }],
)
.toSorted((a, b) => a.id.localeCompare(b.id))
.toSorted((a, b) => a.name.localeCompare(b.name))
return Instructions.make({
key: Instructions.Key.make("core/skill-guidance"),
codec: Schema.toCodecJson(Schema.Array(Summary)),
+11 -2
View File
@@ -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 { ID }
export const ID = Schema.String.pipe(Schema.brand("Snapshot.ID"))
export type ID = typeof ID.Type
export class Error extends Schema.TaggedErrorClass<Error>()("Snapshot.Error", {
operation: Schema.Literals(["capture", "files", "diff", "preview", "restore"]),
@@ -253,3 +253,12 @@ 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"
}
@@ -1,4 +1,4 @@
export * as PatchTool from "./patch"
export * as ApplyPatchTool from "./apply-patch"
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { ToolFailure } from "@opencode-ai/llm"
@@ -55,8 +55,8 @@ type Prepared =
})
export const Plugin = {
id: "opencode.tool.patch",
effect: Effect.fn("PatchTool.Plugin")(function* (ctx: PluginContext) {
id: "opencode.tool.apply-patch",
effect: Effect.fn("ApplyPatchTool.Plugin")(function* (ctx: PluginContext) {
const mutation = yield* LocationMutation.Service
const files = yield* FileMutation.Service
const fs = yield* FSUtil.Service
+8 -8
View File
@@ -13,11 +13,11 @@ export const name = "skill"
const FILE_LIMIT = 10
export const Input = Schema.Struct({
id: SkillV2.ID.annotate({ description: "The ID of the skill from the available skills list" }),
name: Schema.String.annotate({ description: "The name of the skill from the available skills list" }),
})
export const Output = Schema.Struct({
name: SkillV2.Name,
name: Schema.String,
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 ID must match one of the available skills in the instructions.",
"The skill name 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.id === input.id)
if (!skill) return yield* unableToLoad(input.id)
const skill = current.find((skill) => skill.name === input.name)
if (!skill) return yield* unableToLoad(input.name)
return yield* Effect.gen(function* () {
yield* permission.assert({
action: name,
resources: [skill.id],
save: [skill.id],
resources: [skill.name],
save: [skill.name],
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.id, error)))
}).pipe(Effect.mapError((error) => unableToLoad(input.name, error)))
}),
}),
),
+2 -10
View File
@@ -7,7 +7,6 @@ import { ConfigMCPV1 } from "./mcp"
import { ConfigPermissionV1 } from "./permission"
import { ConfigProviderV1 } from "./provider"
import { ConfigProviderOptionsV1 } from "./provider-options"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
const keys = new Set([
@@ -246,15 +245,8 @@ function migrateModel(info: typeof ConfigProviderV1.Model.Type) {
: []),
]
const capabilities =
info.tool_call !== undefined ||
info.attachment !== undefined ||
info.modalities?.input !== undefined ||
info.modalities?.output !== undefined
? ModelV2.Capabilities.defaults({
tools: info.tool_call,
input: info.modalities?.input ?? (info.attachment === false ? ["text"] : undefined),
output: info.modalities?.output,
})
info.tool_call !== undefined || info.modalities?.input !== undefined || info.modalities?.output !== undefined
? { tools: info.tool_call ?? false, input: info.modalities?.input ?? [], output: info.modalities?.output ?? [] }
: undefined
return {
modelID: info.id,
+1 -52
View File
@@ -2,7 +2,7 @@ import type { LanguageModelV3CallOptions } from "@ai-sdk/provider"
import { AISDK } from "@opencode-ai/core/aisdk"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { LLM, Message } from "@opencode-ai/llm"
import { LLM } from "@opencode-ai/llm"
import { LLMClient } from "@opencode-ai/llm/route"
import { expect } from "bun:test"
import { Effect } from "effect"
@@ -67,54 +67,3 @@ it.effect("projects request settings, headers, and body overlays", () =>
expect(body).toEqual({ safety_setting: "strict" })
}),
)
it.effect("projects replay metadata onto AI SDK prompt parts", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* aisdk.hook.sdk((event) => {
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
})
const resolved = yield* aisdk.model(model("@ai-sdk/anthropic"))
expect(resolved.route.providerMetadataKey).toBe("anthropic")
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
LLM.request({
model: resolved,
messages: [
Message.assistant([
{ type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "signed" } } },
{
type: "tool-call",
id: "hosted",
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { anthropic: { blockType: "server_tool_use" } },
},
]),
],
}),
)
expect(prepared.body.prompt).toEqual([
{
role: "assistant",
content: [
{
type: "reasoning",
text: "Think",
providerOptions: { anthropic: { signature: "signed" } },
},
{
type: "tool-call",
toolCallId: "hosted",
toolName: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerOptions: { anthropic: { blockType: "server_tool_use" } },
},
],
},
])
}),
)
+2 -21
View File
@@ -1,5 +1,4 @@
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Fiber, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
@@ -299,31 +298,13 @@ describe("CatalogV2", () => {
catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(1),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
]
model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
model.time.released = Date.now()
})
catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
model.capabilities.input = ["text"]
model.capabilities.output = ["text"]
model.cost = [
{
input: Money.USDPerMillionTokens.make(10),
output: Money.USDPerMillionTokens.make(10),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
]
model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
model.time.released = Date.now()
})
})
+1 -2
View File
@@ -4,7 +4,6 @@ import { describe, expect } from "bun:test"
import { Effect, PubSub, Schema, Stream } from "effect"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { CommandV2 } from "@opencode-ai/core/command"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
import { ConfigCommandPlugin } from "@opencode-ai/core/config/plugin/command"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -88,7 +87,7 @@ Review files`,
name: "review",
template: "Review files",
description: "File review",
agent: AgentV2.ID.make("reviewer"),
agent: "reviewer",
model: {
providerID: ProviderV2.ID.make("anthropic"),
id: ModelV2.ID.make("claude"),
-15
View File
@@ -680,17 +680,9 @@ describe("Config", () => {
options: { apiKey: "secret" },
models: {
model: {
attachment: true,
options: { reasoningEffort: "high" },
variants: { fast: { temperature: 0.2 } },
},
text: {
attachment: false,
},
audio: {
attachment: true,
modalities: { input: ["audio"], output: ["audio"] },
},
},
},
openai: {
@@ -766,16 +758,9 @@ describe("Config", () => {
settings: { apiKey: "secret" },
models: {
model: {
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
settings: { reasoningEffort: "high" },
variants: [{ id: "fast", settings: { temperature: 0.2 } }],
},
text: {
capabilities: { tools: true, input: ["text"], output: ["text"] },
},
audio: {
capabilities: { tools: true, input: ["audio"], output: ["audio"] },
},
},
})
expect(documents[0]?.info.providers?.openai).toMatchObject({
+2 -2
View File
@@ -273,9 +273,9 @@ function withLocation<A, E, R>(
function mutablePlugin(description: string) {
const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/v2/promise/index.ts")).href
return `
import { Plugin } from ${JSON.stringify(plugin)}
import { define } from ${JSON.stringify(plugin)}
export default Plugin.define({
export default EffectPlugin.define({
id: "mutable-plugin",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
+1 -14
View File
@@ -1,5 +1,4 @@
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Schema } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Config } from "@opencode-ai/core/config"
@@ -237,7 +236,6 @@ describe("ConfigProviderPlugin.Plugin", () => {
const provider = required(yield* catalog.provider.get(providerID))
const model = required(yield* catalog.model.get(providerID, modelID))
const defaultModel = required(yield* catalog.model.get(providerID, ModelV2.ID.make("default")))
expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
expect(provider.name).toBe("Renamed")
expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
@@ -253,20 +251,9 @@ describe("ConfigProviderPlugin.Plugin", () => {
expect(model.modelID).toBe(ModelV2.ID.make("api-chat"))
expect(model.name).toBe("Last")
expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
expect(defaultModel.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] })
expect(model.enabled).toBe(false)
expect(model.limit).toEqual({ context: 100, output: 75 })
expect(model.cost).toEqual([
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
tier: undefined,
},
])
expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }])
expect(model.settings).toEqual({ baseURL: "https://example.test", retained: true })
expect(model.headers).toEqual({ first: "first", shared: "last", last: "last" })
expect(model.variants?.map((variant) => variant.id)).toEqual([
+1 -253
View File
@@ -4,7 +4,7 @@ import { fileURLToPath } from "url"
import path from "path"
import { SqliteClient } from "@effect/sql-sqlite-bun"
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
import { Effect, Layer, Schema } from "effect"
import { Effect, Layer } from "effect"
import { eq, inArray, sql } from "drizzle-orm"
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
import { migrations } from "@opencode-ai/core/database/migration.gen"
@@ -17,7 +17,6 @@ import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
import resetSessionEventsMigration from "@opencode-ai/core/database/migration/20260703200000_reset_v2_session_events"
import durableSessionInboxMigration from "@opencode-ai/core/database/migration/20260707010146_durable_session_inbox"
import migratePrelaunchV2StateMigration from "@opencode-ai/core/database/migration/20260707120000_migrate_prelaunch_v2_state"
import renameInstructionsMigration from "@opencode-ai/core/database/migration/20260705180000_rename_instructions"
import addSessionForkMigration from "@opencode-ai/core/database/migration/20260706223930_add-session-fork"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@@ -27,7 +26,6 @@ import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionSchema } from "@opencode-ai/core/session/schema"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionTable } from "@opencode-ai/core/session/sql"
import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata"
import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
@@ -44,256 +42,6 @@ const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
describe("DatabaseMigration", () => {
test("migrates pre-launch V2 state in place", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(
sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, seq integer NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL, data text NOT NULL)`,
)
yield* db.run(
sql`CREATE TABLE session_input (id text PRIMARY KEY, session_id text NOT NULL, type text NOT NULL, prompt text, delivery text, admitted_seq integer NOT NULL, promoted_seq integer, time_created integer NOT NULL)`,
)
yield* db.run(
sql`CREATE TABLE event (id text PRIMARY KEY, aggregate_id text NOT NULL, seq integer NOT NULL, created integer NOT NULL, type text NOT NULL, data text NOT NULL)`,
)
yield* db.run(
sql`CREATE TABLE event_sequence (aggregate_id text PRIMARY KEY, seq integer NOT NULL, owner_id text)`,
)
yield* db.run(
sql`CREATE TABLE instruction_checkpoint (session_id text PRIMARY KEY, baseline text NOT NULL, snapshot text NOT NULL, baseline_seq integer NOT NULL)`,
)
const messages = [
["msg_skill", "skill", { name: "effect", text: "Use Effect", time: { created: 1 } }],
[
"msg_shell",
"shell",
{
shell: { id: "sh_old", command: "pwd", status: "exited", exit: 0, cwd: "/tmp" },
output: { output: "/tmp", cursor: 4, size: 4, truncated: false },
time: { created: 2, completed: 3 },
},
],
[
"msg_assistant",
"assistant",
{
agent: "build",
model: { id: "model", providerID: "provider" },
content: [
{
type: "tool",
id: "call_old",
name: "read",
provider: "removed",
state: { status: "pending", input: '{"path":"README.md"}', title: "removed" },
time: { created: 3 },
},
],
time: { created: 3 },
},
],
[
"msg_failed",
"compaction",
{
status: "failed",
reason: "manual",
summary: "removed",
recent: "removed",
time: { created: 4 },
},
],
[
"msg_queued",
"compaction",
{ status: "queued", reason: "manual", summary: "", recent: "", time: { created: 5 } },
],
[
"msg_synthetic",
"synthetic",
{ sessionID: "ses_test", text: "context", description: "source", time: { created: 6 } },
],
[
"msg_running",
"compaction",
{ status: "running", reason: "auto", summary: "partial", recent: "recent", time: { created: 7 } },
],
[
"msg_completed",
"compaction",
{ status: "completed", reason: "auto", summary: "summary", recent: "recent", time: { created: 8 } },
],
] as const
for (const [id, type, data] of messages)
yield* db.run(
sql`INSERT INTO session_message VALUES (${id}, 'ses_test', ${type}, 1, 10, 11, ${JSON.stringify(data)})`,
)
yield* db.run(
sql`INSERT INTO session_input VALUES ('msg_queued', 'ses_test', 'compaction', NULL, NULL, 4, NULL, 5)`,
)
yield* db.run(sql`INSERT INTO event_sequence VALUES ('ses_test', 9, 'owner')`)
yield* db.run(sql`INSERT INTO instruction_checkpoint VALUES ('ses_test', 'baseline', '{"source":"value"}', 7)`)
const events = [
["evt_skill", 1, 101, "session.skill.activated.1", { sessionID: "ses_test", name: "effect", text: "Use" }],
["evt_started", 2, 102, "session.compaction.started.1", { sessionID: "ses_test", reason: "auto" }],
["evt_delta", 3, 103, "session.compaction.delta.1", { sessionID: "ses_test", text: "partial" }],
["evt_failed", 4, 104, "session.compaction.failed.1", { sessionID: "ses_test" }],
[
"evt_revert",
5,
105,
"session.revert.staged.1",
{
sessionID: "ses_test",
revert: {
messageID: "msg_skill",
snapshot: "tree",
diff: "removed",
files: [{ path: "src/a.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" }],
},
},
],
[
"evt_skill_current",
6,
106,
"session.skill.activated.2",
{ sessionID: "ses_test", id: "effect-id", name: "Effect", text: "Use" },
],
] as const
for (const [id, seq, created, type, data] of events)
yield* db.run(
sql`INSERT INTO event VALUES (${id}, 'ses_test', ${seq}, ${created}, ${type}, ${JSON.stringify(data)})`,
)
yield* DatabaseMigration.applyOnly(db, [migratePrelaunchV2StateMigration])
const rows = yield* db.all<{
id: string
type: string
seq: number
time_created: number
time_updated: number
data: string
}>(sql`SELECT id, type, seq, time_created, time_updated, data FROM session_message ORDER BY id`)
for (const row of rows)
Schema.decodeUnknownSync(SessionMessage.Info)({ ...JSON.parse(row.data), id: row.id, type: row.type })
expect(rows.every((row) => row.seq === 1 && row.time_created === 10 && row.time_updated === 11)).toBe(true)
expect(rows.map((row) => [row.id, JSON.parse(row.data)])).toEqual([
[
"msg_assistant",
expect.objectContaining({
content: [expect.objectContaining({ state: { status: "streaming", input: '{"path":"README.md"}' } })],
}),
],
["msg_completed", expect.objectContaining({ status: "completed", summary: "summary", recent: "recent" })],
[
"msg_failed",
{
time: { created: 4 },
status: "failed",
reason: "manual",
error: {
type: "compaction.failed",
message: "Compaction failed before recording an error",
},
},
],
["msg_running", expect.objectContaining({ status: "running", summary: "partial", recent: "recent" })],
["msg_shell", expect.objectContaining({ shellID: "sh_old", command: "pwd", status: "exited", exit: 0 })],
["msg_skill", { time: { created: 1 }, skill: "effect", name: "effect", text: "Use Effect" }],
["msg_synthetic", { time: { created: 6 }, text: "context", description: "source" }],
])
expect(yield* db.get(sql`SELECT * FROM session_input`)).toEqual({
id: "msg_queued",
session_id: "ses_test",
type: "compaction",
prompt: null,
delivery: null,
admitted_seq: 4,
promoted_seq: null,
time_created: 5,
})
const migratedEvents = yield* db.all<{
id: string
aggregate_id: string
seq: number
created: number
type: string
data: string
}>(sql`SELECT * FROM event ORDER BY seq`)
expect(migratedEvents.map((event) => ({ ...event, data: JSON.parse(event.data) }))).toEqual([
{
id: "evt_skill",
aggregate_id: "ses_test",
seq: 1,
created: 101,
type: "session.skill.activated.1",
data: { sessionID: "ses_test", id: "effect", name: "effect", text: "Use" },
},
{
id: "evt_started",
aggregate_id: "ses_test",
seq: 2,
created: 102,
type: "session.compaction.started.1",
data: { sessionID: "ses_test", reason: "auto", recent: "" },
},
{
id: "evt_failed",
aggregate_id: "ses_test",
seq: 4,
created: 104,
type: "session.compaction.failed.1",
data: {
sessionID: "ses_test",
reason: "auto",
error: {
type: "compaction.failed",
message: "Compaction failed before recording an error",
},
},
},
{
id: "evt_revert",
aggregate_id: "ses_test",
seq: 5,
created: 105,
type: "session.revert.staged.1",
data: {
sessionID: "ses_test",
revert: {
messageID: "msg_skill",
snapshot: "tree",
files: [{ file: "src/a.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" }],
},
},
},
{
id: "evt_skill_current",
aggregate_id: "ses_test",
seq: 6,
created: 106,
type: "session.skill.activated.1",
data: { sessionID: "ses_test", id: "effect-id", name: "Effect", text: "Use" },
},
])
expect(yield* db.get(sql`SELECT * FROM event_sequence`)).toEqual({
aggregate_id: "ses_test",
seq: 9,
owner_id: "owner",
})
expect(yield* db.get(sql`SELECT * FROM instruction_checkpoint`)).toEqual({
session_id: "ses_test",
baseline: "baseline",
snapshot: '{"source":"value"}',
baseline_seq: 7,
})
}),
)
})
test("resets incompatible V2 Session event history", async () => {
await run(
Effect.gen(function* () {
+2 -2
View File
@@ -146,7 +146,7 @@ describe("Git trees", () => {
RelativePath.make("scope/tracked.txt"),
])
const diffs = yield* git.tree.diff({ repository, from: before, to: after, context: 1 })
expect(diffs.map((item) => [item.file, item.status])).toEqual([
expect(diffs.map((item) => [item.path, item.status])).toEqual([
[RelativePath.make("scope/added.txt"), "added"],
[RelativePath.make("scope/tracked.txt"), "modified"],
])
@@ -154,7 +154,7 @@ describe("Git trees", () => {
const files = new Map([[RelativePath.make("scope/tracked.txt"), before]])
const preview = yield* git.tree.preview({ repository, current: after, files, context: 1 })
expect(preview).toHaveLength(1)
expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt"))
expect(preview[0]?.path).toBe(RelativePath.make("scope/tracked.txt"))
yield* git.tree.restore({ repository, files })
expect(yield* read(path.join(root.path, "scope", "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(root.path, "scope", "added.txt"))).toBe("added\n")
+3 -4
View File
@@ -6,7 +6,6 @@ import { Tool } from "@opencode-ai/core/tool/tool"
import { Tools } from "@opencode-ai/core/tool/tools"
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, type Scope } from "effect"
import { host } from "../plugin/host"
export const toolIdentity = {
agent: AgentV2.ID.make("build"),
@@ -49,7 +48,7 @@ export const registerToolPlugin = <R>(plugin: {
}): Effect.Effect<void, never, R | Tools.Service | Scope.Scope> =>
Effect.gen(function* () {
const tools = yield* Tools.Service
const context = host({
const context: Pick<PluginContext, "tool"> = {
tool: {
transform: (callback) =>
Effect.gen(function* () {
@@ -72,8 +71,8 @@ export const registerToolPlugin = <R>(plugin: {
}),
hook: () => Effect.die("registerToolPlugin does not support tool hooks"),
},
})
yield* plugin.effect(context)
}
yield* plugin.effect(context as PluginContext)
})
export const settleTool = (registry: ToolRegistry.Interface, input: ToolRegistry.ExecuteInput, model = testModel) =>
+2 -5
View File
@@ -3,7 +3,6 @@ import path from "path"
import { describe, expect } from "bun:test"
import { Config } from "@opencode-ai/schema/config"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Money } from "@opencode-ai/schema/money"
import { Context, DateTime, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect"
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
@@ -291,7 +290,6 @@ describe("LocationServiceMap", () => {
"edit",
"glob",
"grep",
"patch",
"question",
"read",
"shell",
@@ -309,7 +307,6 @@ describe("LocationServiceMap", () => {
"edit",
"glob",
"grep",
"patch",
"question",
"read",
"shell",
@@ -357,7 +354,7 @@ describe("LocationServiceMap", () => {
id: ModelV2.ID.make("chat"),
providerID: ProviderV2.ID.make("unavailable"),
},
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location,
@@ -406,7 +403,7 @@ describe("LocationServiceMap", () => {
providerID: ProviderV2.ID.make("aliased"),
variant: ModelV2.VariantID.make("high"),
},
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location,
-15
View File
@@ -165,21 +165,6 @@ describe("ModelsDev Service", () => {
}),
)
it.effect("allows models.dev entries without legacy attachment metadata", () =>
Effect.sync(() => {
const result = Schema.decodeUnknownSync(ModelsDev.Model)({
id: "no-attachment-model",
name: "No Attachment Model",
release_date: "2026-01-01",
reasoning: false,
tool_call: true,
limit: { context: 128000, output: 8192 },
})
expect(result.attachment).toBeUndefined()
}),
)
it.live("get() returns providers from disk when cache file exists", () =>
Effect.gen(function* () {
yield* writeCache(fixture)
+1 -3
View File
@@ -83,9 +83,7 @@ export function host(overrides: Overrides = {}): PluginContext {
prompt: () => Effect.die("unused session.prompt"),
command: () => Effect.die("unused session.command"),
interrupt: () => Effect.die("unused session.interrupt"),
// Plugins register session hooks during setup, so a bare host accepts the
// registration; the callback only runs when a test triggers the request pipeline.
hook: () => Effect.succeed({ dispose: Effect.void }),
hook: () => Effect.die("unused session.hook"),
},
}
}
+14 -62
View File
@@ -1,6 +1,5 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Layer } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
@@ -52,31 +51,23 @@ describe("ModelsDevPlugin", () => {
temperature: true,
tool_call: true,
cost: {
input: Money.USDPerMillionTokens.make(2.5),
output: Money.USDPerMillionTokens.make(15),
input: 2.5,
output: 15,
tiers: [
{
tier: { type: "context", size: 272_000 },
input: Money.USDPerMillionTokens.make(3),
output: Money.USDPerMillionTokens.make(18),
cache_read: Money.USDPerMillionTokens.make(0.25),
input: 3,
output: 18,
cache_read: 0.25,
},
],
context_over_200k: {
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(22.5),
cache_read: Money.USDPerMillionTokens.make(0.5),
},
context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 },
},
limit: { context: 1_050_000, input: 922_000, output: 128_000 },
experimental: {
modes: {
fast: {
cost: {
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(30),
cache_read: Money.USDPerMillionTokens.make(0.5),
},
cost: { input: 5, output: 30, cache_read: 0.5 },
provider: {
headers: { "x-mode": "fast" },
body: { service_tier: "priority" },
@@ -85,24 +76,6 @@ describe("ModelsDevPlugin", () => {
},
},
},
default: {
id: "default",
name: "Default",
release_date: "2026-01-01",
reasoning: false,
tool_call: false,
limit: { context: 128_000, output: 8_192 },
},
explicit: {
id: "explicit",
name: "Explicit",
release_date: "2026-01-01",
attachment: true,
reasoning: false,
tool_call: false,
modalities: { input: ["audio"], output: ["audio"] },
limit: { context: 128_000, output: 8_192 },
},
},
},
} satisfies Record<string, ModelsDev.Provider>),
@@ -119,14 +92,9 @@ describe("ModelsDevPlugin", () => {
const providerID = ProviderV2.ID.make("acme")
const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4"))
const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast"))
const defaults = yield* catalog.model.get(providerID, ModelV2.ID.make("default"))
const explicit = yield* catalog.model.get(providerID, ModelV2.ID.make("explicit"))
expect(base?.variants).toEqual([])
expect(base?.body).toEqual({})
expect(base?.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] })
expect(defaults?.capabilities).toEqual({ tools: false, input: ["text", "image"], output: ["text"] })
expect(explicit?.capabilities).toEqual({ tools: false, input: ["audio"], output: ["audio"] })
expect(fast).toMatchObject({
id: "gpt-5.4-fast",
modelID: "gpt-5.4",
@@ -139,31 +107,18 @@ describe("ModelsDevPlugin", () => {
variants: [],
})
expect(fast?.cost).toEqual([
{
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(30),
cache: {
read: Money.USDPerMillionTokens.make(0.5),
write: Money.USDPerMillionTokens.zero,
},
},
{ input: 5, output: 30, cache: { read: 0.5, write: 0 } },
{
tier: { type: "context", size: 272_000 },
input: Money.USDPerMillionTokens.make(3),
output: Money.USDPerMillionTokens.make(18),
cache: {
read: Money.USDPerMillionTokens.make(0.25),
write: Money.USDPerMillionTokens.zero,
},
input: 3,
output: 18,
cache: { read: 0.25, write: 0 },
},
{
tier: { type: "context", size: 200_000 },
input: Money.USDPerMillionTokens.make(5),
output: Money.USDPerMillionTokens.make(22.5),
cache: {
read: Money.USDPerMillionTokens.make(0.5),
write: Money.USDPerMillionTokens.zero,
},
input: 5,
output: 22.5,
cache: { read: 0.5, write: 0 },
},
])
}),
@@ -204,9 +159,6 @@ describe("ModelsDevPlugin", () => {
connections: [],
}),
])
expect(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("gpt-5.5"))).toMatchObject({
capabilities: { tools: true, input: ["text", "image"], output: ["text"] },
})
}).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))),
(previous) =>
Effect.sync(() => {
@@ -1,5 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { Money } from "@opencode-ai/schema/money"
import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect"
@@ -186,16 +185,7 @@ describe("OpenAIPlugin", () => {
draft.package = item.package
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => {
model.cost = [
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: {
read: Money.USDPerMillionTokens.make(0.1),
write: Money.USDPerMillionTokens.zero,
},
},
]
model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }]
})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5-pro"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {})
@@ -1,5 +1,4 @@
import { describe, expect } from "bun:test"
import { Money } from "@opencode-ai/schema/money"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
@@ -66,16 +65,7 @@ function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () =
)
}
const cost = (input: number, output = 0) => [
{
input: Money.USDPerMillionTokens.make(input),
output: Money.USDPerMillionTokens.make(output),
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
]
const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
describe("OpencodePlugin", () => {
it.effect("registers account and service account methods", () =>
+3 -5
View File
@@ -37,19 +37,17 @@ describe("SkillPlugin.Plugin", () => {
Effect.provide(NodeFileSystem.layer),
)
const skills = yield* skill.list()
const report = skills.find((item) => item.id === "report")
const report = skills.find((item) => item.name === "report")
expect(skills).toContainEqual(
expect.objectContaining({
id: "opencode",
name: "OpenCode",
name: "opencode",
description: expect.stringContaining("any question about OpenCode itself"),
}),
)
expect(skills).toContainEqual(
expect.objectContaining({
id: "report",
name: "Report",
name: "report",
description: expect.stringContaining("opencode issue"),
}),
)
+6 -4
View File
@@ -15,7 +15,6 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionInput } from "@opencode-ai/core/session/input"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Prompt } from "@opencode-ai/schema/prompt"
import { SessionProjector } from "@opencode-ai/core/session/projector"
@@ -105,10 +104,13 @@ describe("SessionV2.compact", () => {
expect(second.id).toBe(first.id)
expect(requests).toHaveLength(0)
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, created.id)).toMatchObject({
id: first.id,
expect((yield* session.context(created.id)).find((message) => message.id === first.id)).toMatchObject({
type: "compaction",
status: "queued",
reason: "manual",
summary: "",
recent: "",
})
expect((yield* session.context(created.id)).find((message) => message.id === first.id)).toBeUndefined()
}),
)
})
@@ -141,13 +141,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
.subscribe(SessionEvent.Compaction.Delta)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
expect(
yield* compaction.compactManual({
session,
messages: [userMessage],
inputID: SessionMessage.ID.make("msg_manual_compaction"),
}),
).toBe(true)
expect(yield* compaction.compactManual({ session, messages: [userMessage] })).toBe(true)
expect(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"])
expect(requests).toHaveLength(1)
+8 -9
View File
@@ -1,7 +1,6 @@
import { describe, expect } from "bun:test"
import path from "path"
import { DateTime, Effect, Layer, Stream } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { AgentV2 } from "@opencode-ai/core/agent"
import { asc, eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
@@ -211,7 +210,7 @@ describe("SessionV2.create", () => {
expect(forked.parentID).toBeUndefined()
expect(forkContext).toMatchObject([
{ type: "user", text: "First" },
{ type: "synthetic", text: "parent note" },
{ type: "synthetic", text: "parent note", sessionID: forked.id },
])
expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id))
expect(history).toHaveLength(1)
@@ -274,14 +273,14 @@ describe("SessionV2.create", () => {
yield* events.publish(SessionEvent.Step.Started, {
sessionID: parent.id,
assistantMessageID,
agent: AgentV2.ID.make("build"),
agent: "build",
model,
})
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: parent.id,
assistantMessageID,
finish: "stop",
cost: Money.USD.make(0.75),
cost: 0.75,
tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } },
})
@@ -534,7 +533,7 @@ describe("SessionV2.create", () => {
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
expect(shell).toMatchObject({ type: "shell", command: "echo hello", status: "exited", exit: 0 })
expect(shell).toMatchObject({ type: "shell", shell: { command: "echo hello", status: "exited", exit: 0 } })
expect(shell?.output?.output).toContain("hello")
expect(shell?.output?.truncated).toBe(false)
expect(shell?.time.completed).toBeDefined()
@@ -554,8 +553,8 @@ describe("SessionV2.create", () => {
const messages = yield* session.messages({ sessionID: created.id, order: "asc" })
const shell = messages.find((message): message is SessionMessage.Shell => message.type === "shell")
expect(shell).toMatchObject({ type: "shell", command: "false", status: "exited" })
expect(shell?.exit).not.toBe(0)
expect(shell).toMatchObject({ type: "shell", shell: { command: "false", status: "exited" } })
expect(shell?.shell.exit).not.toBe(0)
expect(shell?.time.completed).toBeDefined()
}),
),
@@ -566,7 +565,7 @@ describe("SessionV2.create", () => {
const session = yield* SessionV2.Service
const created = yield* session.create({ location })
yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("plan") })
yield* session.switchAgent({ sessionID: created.id, agent: "plan" })
expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" })
expect(
@@ -581,7 +580,7 @@ describe("SessionV2.create", () => {
const missing = SessionV2.ID.make("ses_missing_agent_switch")
expect(
yield* session.switchAgent({ sessionID: missing, agent: AgentV2.ID.make("plan") }).pipe(
yield* session.switchAgent({ sessionID: missing, agent: "plan" }).pipe(
Effect.flip,
Effect.map((error) => error._tag),
),
@@ -303,6 +303,7 @@ describe("SessionInstructions", () => {
const synthetic = SessionMessage.Synthetic.make({
id: SessionMessage.ID.make("msg_synthetic"),
type: "synthetic",
sessionID: SessionV2.ID.make("ses_test"),
text: "Instructions from: /repo/sub/AGENTS.md\ncontent",
description: "Loaded /repo/sub/AGENTS.md",
metadata: { instruction: { paths: ["/repo/sub/AGENTS.md"] } },
+3 -4
View File
@@ -1,7 +1,6 @@
import { describe, expect } from "bun:test"
import { Effect, Fiber, Layer, Schema, Stream } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { AgentV2 } from "@opencode-ai/core/agent"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
@@ -88,11 +87,11 @@ describe("SessionV2.log", () => {
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const created = yield* session.create({ location })
yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("one") })
yield* session.switchAgent({ sessionID: created.id, agent: "one" })
// Not in the durable manifest, so reads must skip it without failing.
yield* events.publish(GapEvent, { sessionID: created.id, value: "filtered" })
yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("two") })
yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("three") })
yield* session.switchAgent({ sessionID: created.id, agent: "two" })
yield* session.switchAgent({ sessionID: created.id, agent: "three" })
const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id, after: 1 })))
+32 -131
View File
@@ -1,8 +1,7 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Option, Schema, Stream } from "effect"
import { asc, eq, sql } from "drizzle-orm"
import { asc, eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
import { AgentV2 } from "@opencode-ai/core/agent"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { EventV2 } from "@opencode-ai/core/event"
@@ -16,11 +15,9 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Prompt } from "@opencode-ai/schema/prompt"
import { Money } from "@opencode-ai/schema/money"
import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { fromRow } from "@opencode-ai/core/session/info"
import { SessionInput } from "@opencode-ai/core/session/input"
import { Shell } from "@opencode-ai/schema/shell"
import {
@@ -38,8 +35,7 @@ const sessionID = SessionV2.ID.make("ses_projector_test")
const created = DateTime.makeUnsafe(0)
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
const previousModel = { ...model, variant: ModelV2.VariantID.make("medium") }
const encodeMessage = Schema.encodeSync(SessionMessage.Info)
const build = AgentV2.defaultID
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
const assistantRow = (
id: SessionMessage.ID,
@@ -52,108 +48,12 @@ const assistantRow = (
type,
...data
} = encodeMessage(
SessionMessage.Assistant.make({ id, type: "assistant", agent: build, model, content: [], time, ...usage }),
SessionMessage.Assistant.make({ id, type: "assistant", agent: "build", model, content: [], time, ...usage }),
)
return { id, session_id: sessionID, type, seq, time_created: DateTime.toEpochMillis(time.created), data }
}
describe("SessionProjector", () => {
it.effect("does not settle a pending manual compaction on an auto failure", () =>
Effect.gen(function* () {
const db = (yield* Database.Service).db
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.run()
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "test",
directory: "/project",
title: "test",
version: "test",
})
.run()
const events = yield* EventV2.Service
const inputID = SessionMessage.ID.make("msg_manual_compaction")
yield* SessionInput.admitCompaction(db, events, { id: inputID, sessionID })
yield* events.publish(SessionEvent.Compaction.Failed, {
sessionID,
reason: "auto",
error: { type: "compaction.failed", message: "Auto compaction failed" },
})
expect(yield* SessionInput.pendingCompaction(db, sessionID)).toMatchObject({ id: inputID })
}),
)
it.effect("loads legacy revert storage into canonical state", () =>
Effect.gen(function* () {
const db = (yield* Database.Service).db
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.run()
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "test",
directory: "/project",
title: "test",
version: "test",
})
.run()
const legacy = JSON.stringify({
messageID: "msg_boundary",
snapshot: "tree",
diff: "legacy patch",
files: [{ path: "src/old.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" }],
})
yield* db.run(sql`update session set revert = ${legacy} where id = ${sessionID}`)
const stored = yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()
if (!stored) return yield* Effect.die("Session row missing")
const storedRevert = fromRow(stored).revert
expect(String(storedRevert?.messageID)).toBe("msg_boundary")
expect(String(storedRevert?.snapshot)).toBe("tree")
expect(storedRevert?.files).toEqual([
{ file: "src/old.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" },
])
}),
)
it.effect("folds live compaction deltas into running memory state", () =>
Effect.gen(function* () {
const state = {
messages: [
SessionMessage.CompactionRunning.make({
id: SessionMessage.ID.make("msg_compaction"),
type: "compaction",
status: "running",
reason: "manual",
summary: "partial ",
recent: "recent",
time: { created },
}),
],
}
yield* SessionMessageUpdater.update(
SessionMessageUpdater.memory(state),
SessionEvent.Compaction.Delta.make({
id: EventV2.ID.make("evt_delta"),
type: "session.compaction.delta",
created,
data: { sessionID, text: "summary" },
}),
)
expect(state.messages[0]).toMatchObject({ status: "running", summary: "partial summary", recent: "recent" })
}),
)
it.effect("projects staged, cleared, and committed reverts", () =>
Effect.gen(function* () {
const db = (yield* Database.Service).db
@@ -189,7 +89,7 @@ describe("SessionProjector", () => {
1,
{ created },
{
cost: Money.USD.make(0.5),
cost: 0.5,
tokens: { input: 4, output: 1, reasoning: 1, cache: { read: 1, write: 0 } },
},
),
@@ -198,7 +98,7 @@ describe("SessionProjector", () => {
2,
{ created },
{
cost: Money.USD.make(0.75),
cost: 0.75,
tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } },
},
),
@@ -211,7 +111,7 @@ describe("SessionProjector", () => {
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.RevertEvent.Staged, {
sessionID,
revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), files: [] },
revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), diff: "patch", files: [] },
})
expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toMatchObject({
messageID: boundary,
@@ -232,7 +132,7 @@ describe("SessionProjector", () => {
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
).toEqual([earlier])
expect(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()).toMatchObject({
cost: Money.USD.make(1.25),
cost: 1.25,
tokens_input: 10,
tokens_output: 4,
tokens_reasoning: 2,
@@ -385,7 +285,7 @@ describe("SessionProjector", () => {
yield* events.publish(SessionEvent.AgentSelected, {
sessionID,
agent: build,
agent: "build",
})
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
@@ -427,7 +327,6 @@ describe("SessionProjector", () => {
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID,
reason: "manual",
recent: "recent context",
})
yield* events.publish(SessionEvent.Compaction.Delta, {
sessionID,
@@ -437,18 +336,18 @@ describe("SessionProjector", () => {
yield* db
.select({ id: EventTable.id })
.from(EventTable)
.where(sql`${EventTable.type} like 'session.compaction.delta.%'`)
.where(eq(EventTable.type, SessionEvent.Compaction.Delta.type))
.all()
.pipe(Effect.orDie),
).toHaveLength(0)
).toEqual([])
expect(
yield* db
.select({ data: SessionMessageTable.data })
.select({ id: SessionMessageTable.id })
.from(SessionMessageTable)
.where(eq(SessionMessageTable.type, "compaction"))
.all()
.pipe(Effect.orDie),
).toEqual([{ data: expect.objectContaining({ status: "running", summary: "", recent: "recent context" }) }])
).toEqual([])
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID,
reason: "manual",
@@ -464,7 +363,7 @@ describe("SessionProjector", () => {
.all()
.pipe(Effect.orDie)
const messages = rows.map((row) =>
Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type }),
Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }),
)
expect(messages.map((message) => message.type)).toEqual([
@@ -480,9 +379,7 @@ describe("SessionProjector", () => {
})
expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel })
expect(messages.find((message) => message.type === "shell")).toMatchObject({
command: "pwd",
status: "exited",
exit: 0,
shell: { command: "pwd", status: "exited", exit: 0 },
output: { output: "/project", truncated: false },
time: { completed: DateTime.makeUnsafe(0) },
})
@@ -522,7 +419,11 @@ describe("SessionProjector", () => {
.pipe(Effect.orDie)
const events = yield* EventV2.Service
const id = SessionMessage.ID.make("msg_creator_collision")
const { id: _, type, ...data } = encodeMessage({ id, type: "synthetic", text: "existing", time: { created } })
const {
id: _,
type,
...data
} = encodeMessage({ id, sessionID, type: "synthetic", text: "existing", time: { created } })
yield* db
.insert(SessionMessageTable)
.values({ id, session_id: sessionID, type, seq: 0, time_created: 0, data })
@@ -532,7 +433,7 @@ describe("SessionProjector", () => {
.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID: id,
agent: build,
agent: "build",
model,
})
.pipe(Effect.exit)
@@ -549,7 +450,7 @@ describe("SessionProjector", () => {
const stale = SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_stale"),
type: "assistant",
agent: build,
agent: "build",
model,
content: [],
time: { created },
@@ -557,7 +458,7 @@ describe("SessionProjector", () => {
const completed = SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_completed"),
type: "assistant",
agent: build,
agent: "build",
model,
content: [],
time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) },
@@ -592,7 +493,7 @@ describe("SessionProjector", () => {
const events = yield* EventV2.Service
const first = SessionMessage.ID.make("msg_retry_first")
const second = SessionMessage.ID.make("msg_retry_second")
yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: build, model })
yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: "build", model })
yield* events.publish(SessionEvent.RetryScheduled, {
sessionID,
assistantMessageID: first,
@@ -602,7 +503,7 @@ describe("SessionProjector", () => {
})
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type })
Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type })
const firstRow = yield* db
.select()
.from(SessionMessageTable)
@@ -614,7 +515,7 @@ describe("SessionProjector", () => {
retry: { attempt: 2, at: DateTime.makeUnsafe(2_000), error: { type: "provider.transport" } },
})
yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: build, model })
yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: "build", model })
yield* events.publish(SessionEvent.RetryScheduled, {
sessionID,
assistantMessageID: second,
@@ -673,7 +574,7 @@ describe("SessionProjector", () => {
sessionID,
assistantMessageID: SessionMessage.ID.make("msg_assistant_2"),
finish: "stop",
cost: Money.USD.make(1.25),
cost: 1.25,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
})
@@ -685,13 +586,13 @@ describe("SessionProjector", () => {
.all()
.pipe(Effect.orDie)
const messages = rows.map((row) =>
Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type }),
Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }),
)
expect(messages[0]).not.toHaveProperty("time.completed")
expect(messages[1]).toMatchObject({
type: "assistant",
finish: "stop",
cost: Money.USD.make(1.25),
cost: 1.25,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
time: { completed: DateTime.makeUnsafe(0) },
})
@@ -707,7 +608,7 @@ describe("SessionProjector", () => {
})
expect(Option.getOrThrow(yield* Fiber.join(usageUpdated)).data).toEqual({
sessionID,
cost: Money.USD.make(1.25),
cost: 1.25,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
})
}),
@@ -760,13 +661,13 @@ describe("SessionProjector", () => {
.all()
.pipe(Effect.orDie)
const messages = rows.map((row) =>
Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type }),
Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }),
)
expect(messages).toEqual([
SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_completed"),
type: "assistant",
agent: build,
agent: "build",
model,
content: [SessionMessage.AssistantText.make({ type: "text", text: "" })],
time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) },
@@ -774,7 +675,7 @@ describe("SessionProjector", () => {
SessionMessage.Assistant.make({
id: SessionMessage.ID.make("msg_assistant_stale"),
type: "assistant",
agent: build,
agent: "build",
model,
content: [],
time: { created },
+3 -3
View File
@@ -6,7 +6,6 @@ import path from "path"
import { pathToFileURL } from "url"
import { eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
import { AgentV2 } from "@opencode-ai/core/agent"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
@@ -106,7 +105,7 @@ const eventCount = (type: string) =>
),
)
const encodeMessage = Schema.encodeSync(SessionMessage.Info)
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
const assistantRow = (id: SessionMessage.ID, seq: number) => {
const {
id: _,
@@ -116,7 +115,7 @@ const assistantRow = (id: SessionMessage.ID, seq: number) => {
SessionMessage.Assistant.make({
id,
type: "assistant",
agent: AgentV2.ID.make("build"),
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [],
time: { created: DateTime.makeUnsafe(0) },
@@ -678,6 +677,7 @@ describe("SessionV2.prompt", () => {
...data
} = encodeMessage({
id: messageID,
sessionID,
type: "synthetic",
text: "Existing history",
time: { created: DateTime.makeUnsafe(0) },
+22 -150
View File
@@ -5,14 +5,13 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { AgentAttachment, Base64, FileAttachment } from "@opencode-ai/schema/prompt"
import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message"
import { AgentV2 } from "@opencode-ai/core/agent"
import { SessionV2 } from "@opencode-ai/core/session"
import { Shell } from "@opencode-ai/schema/shell"
import { DateTime } from "effect"
const created = DateTime.makeUnsafe(0)
const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
const model = ModelV2.Ref.make({ id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") })
const build = AgentV2.defaultID
describe("toLLMMessages", () => {
test("omits empty assistant turns", () => {
@@ -20,7 +19,7 @@ describe("toLLMMessages", () => {
SessionMessage.Assistant.make({
id: id(value),
type: "assistant",
agent: build,
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content,
time: { created, completed: created },
@@ -57,7 +56,7 @@ describe("toLLMMessages", () => {
SessionMessage.AgentSelected.make({
id: id("agent"),
type: "agent-switched",
agent: build,
agent: "build",
time: { created },
}),
SessionMessage.ModelSelected.make({
@@ -83,16 +82,24 @@ describe("toLLMMessages", () => {
SessionMessage.Synthetic.make({
id: id("synthetic"),
type: "synthetic",
sessionID: SessionV2.ID.make("ses_translate"),
text: "Synthetic context",
time: { created },
}),
SessionMessage.Shell.make({
id: id("shell"),
type: "shell",
shellID: Shell.ID.make("sh_test"),
status: "exited",
command: "pwd",
exit: 0,
shell: Shell.Info.make({
id: Shell.ID.make("sh_test"),
status: "exited",
command: "pwd",
cwd: "/project",
shell: "/bin/sh",
file: "/tmp/sh_test.out",
exit: 0,
metadata: {},
time: { started: 0, completed: 0 },
}),
output: { output: "/project", cursor: 8, size: 8, truncated: false },
time: { created, completed: created },
}),
@@ -240,7 +247,7 @@ Recent work
expect(messages[1]?.content).toEqual([{ type: "text", text: "Review this directory" }])
})
test("defaults missing model capabilities to text and image input", () => {
test("uses materialized image data as provider media and drops unsupported attachments", () => {
const data = Base64.make("AAECAw==")
const messages = toLLMMessages(
[
@@ -266,113 +273,6 @@ Recent work
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Inspect this image" },
{ type: "media", mediaType: "image/png", data, filename: "image.png" },
{
type: "text",
text: 'ERROR: Cannot read "document.pdf" (this model does not support pdf input). Inform the user.',
},
])
})
test("uses explicit model input capabilities instead of attachment defaults", () => {
const data = Base64.make("AAECAw==")
const messages = toLLMMessages(
[
SessionMessage.User.make({
id: id("user-unsupported-pdf"),
type: "user",
text: "Inspect these files",
files: [
FileAttachment.make({ data, mime: "image/png", source: { type: "inline" }, name: "image.png" }),
FileAttachment.make({
data: Base64.make("JVBERg=="),
mime: "application/pdf",
source: { type: "inline" },
name: "document.pdf",
}),
],
time: { created },
}),
],
model,
model.providerID,
{ tools: true, input: ["text", "pdf"], output: ["text"] },
)
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Inspect these files" },
{
type: "text",
text: 'ERROR: Cannot read "image.png" (this model does not support image input). Inform the user.',
},
{ type: "media", mediaType: "application/pdf", data: "JVBERg==", filename: "document.pdf" },
])
})
test("treats explicit empty input capabilities as authoritative", () => {
const messages = toLLMMessages(
[
SessionMessage.User.make({
id: id("user-empty-capabilities"),
type: "user",
text: "Inspect this image",
files: [
FileAttachment.make({
data: Base64.make("AAECAw=="),
mime: "image/png",
source: { type: "inline" },
name: "image.png",
}),
],
time: { created },
}),
],
model,
model.providerID,
{ tools: false, input: [], output: [] },
)
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Inspect this image" },
{
type: "text",
text: 'ERROR: Cannot read "image.png" (this model does not support image input). Inform the user.',
},
])
})
test("classifies audio and video MIME families through explicit capabilities", () => {
const messages = toLLMMessages(
[
SessionMessage.User.make({
id: id("user-media-capabilities"),
type: "user",
text: "Inspect this media",
files: [
FileAttachment.make({
data: Base64.make("AAECAw=="),
mime: "audio/mpeg",
source: { type: "inline" },
name: "audio.mp3",
}),
FileAttachment.make({
data: Base64.make("AAECAw=="),
mime: "video/webm",
source: { type: "inline" },
name: "video.webm",
}),
],
time: { created },
}),
],
model,
model.providerID,
{ tools: false, input: ["text", "audio", "video"], output: ["text"] },
)
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Inspect this media" },
{ type: "media", mediaType: "audio/mpeg", data: "AAECAw==", filename: "audio.mp3" },
{ type: "media", mediaType: "video/webm", data: "AAECAw==", filename: "video.webm" },
])
})
@@ -382,7 +282,7 @@ Recent work
SessionMessage.Assistant.make({
id: id("assistant"),
type: "assistant",
agent: build,
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantText.make({ type: "text", text: "Checking" }),
@@ -395,7 +295,7 @@ Recent work
type: "tool",
id: "pending",
name: "read",
state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: '{"path":"README.md"}' }),
state: SessionMessage.ToolStatePending.make({ status: "pending", input: '{"path":"README.md"}' }),
time: { created },
}),
SessionMessage.AssistantTool.make({
@@ -537,7 +437,7 @@ Recent work
SessionMessage.Assistant.make({
id: id("assistant-openai-reasoning"),
type: "assistant",
agent: build,
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantReasoning.make({
@@ -561,41 +461,13 @@ Recent work
])
})
test("replays flat state under an OpenCode hosted model's route key", () => {
const opencode = ModelV2.Ref.make({ id: ModelV2.ID.make("claude-fable-5"), providerID: ProviderV2.ID.opencode })
const messages = toLLMMessages(
[
SessionMessage.Assistant.make({
id: id("assistant-opencode-reasoning"),
type: "assistant",
agent: build,
model: opencode,
content: [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
text: "Think",
state: { signature: "signed" },
}),
],
time: { created, completed: created },
}),
],
opencode,
"anthropic",
)
expect(messages[0]?.content).toEqual([
{ type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "signed" } } },
])
})
test("lowers failed assistant reasoning to text", () => {
const messages = toLLMMessages(
[
SessionMessage.Assistant.make({
id: id("assistant-failed"),
type: "assistant",
agent: build,
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantReasoning.make({
@@ -664,7 +536,7 @@ Recent work
SessionMessage.Assistant.make({
id: id("assistant-old-model"),
type: "assistant",
agent: build,
agent: "build",
model: { id: ModelV2.ID.make("old-model"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantReasoning.make({
@@ -759,7 +631,7 @@ Recent work
SessionMessage.Assistant.make({
id: id("assistant-alias"),
type: "assistant",
agent: build,
agent: "build",
model: { id: ModelV2.ID.make("fast"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantReasoning.make({
@@ -2,7 +2,6 @@ import { describe, expect } from "bun:test"
import { LLM, Model } from "@opencode-ai/llm"
import { LLMClient } from "@opencode-ai/llm/route"
import { DateTime, Effect } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { Headers } from "effect/unstable/http"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
@@ -53,7 +52,6 @@ describe("SessionRunnerModel", () => {
expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" })
expect(resolved.route).toMatchObject({
id: "openai-responses",
providerMetadataKey: "openai",
endpoint: { baseURL: "https://openai.example/v1" },
defaults: {
headers: { "x-test": "header" },
@@ -133,7 +131,7 @@ describe("SessionRunnerModel", () => {
providerID: catalog.providerID,
variant: ModelV2.VariantID.make("high"),
},
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
@@ -172,7 +170,7 @@ describe("SessionRunnerModel", () => {
projectID: ProjectV2.ID.global,
title: "test",
model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") },
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
@@ -202,7 +200,7 @@ describe("SessionRunnerModel", () => {
providerID: catalog.providerID,
variant: ModelV2.VariantID.make("unknown"),
},
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
@@ -238,7 +236,7 @@ describe("SessionRunnerModel", () => {
projectID: ProjectV2.ID.global,
title: "test",
model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") },
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) },
location: { directory: AbsolutePath.make("/project") },
@@ -265,7 +263,6 @@ describe("SessionRunnerModel", () => {
expect(resolved.route).toMatchObject({
id: "anthropic-messages",
providerMetadataKey: "anthropic",
endpoint: { baseURL: "https://anthropic.example/v1" },
})
}),
@@ -1,9 +1,7 @@
import { expect, test } from "bun:test"
import { Effect, Schema, Stream } from "effect"
import { LLMEvent } from "@opencode-ai/llm"
import { Money } from "@opencode-ai/schema/money"
import { EventV2 } from "@opencode-ai/core/event"
import { AgentV2 } from "@opencode-ai/core/agent"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionV2 } from "@opencode-ai/core/session"
@@ -14,7 +12,7 @@ import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publis
const sessionID = SessionV2.ID.make("ses_tool_event_test")
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
const capture = (providerMetadataKey = "anthropic") => {
const capture = () => {
const published: Array<{ readonly type: string; readonly data: unknown }> = []
const events = EventV2.Service.of({
publish: (definition, data) =>
@@ -42,12 +40,12 @@ const capture = (providerMetadataKey = "anthropic") => {
published,
publisher: createLLMEventPublisher(events, {
sessionID,
agent: AgentV2.ID.make("build"),
agent: "build",
model: {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.opencode,
providerID: ProviderV2.ID.make("provider"),
},
providerMetadataKey,
provider: "openai",
}),
}
}
@@ -99,76 +97,35 @@ test("provider-executed success retains its raw provider result", async () => {
expect(success?.data).toHaveProperty("result")
})
test("provider metadata is flattened using the route key", async () => {
test("provider state uses the route provider instead of the catalog provider", async () => {
const { published, publisher } = capture()
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { anthropic: { signature: "signed" } } }),
LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { openai: { itemId: "reasoning" } } }),
),
)
expect(published.find((event) => event.type === "session.reasoning.started.1")?.data).toMatchObject({
state: { signature: "signed" },
state: { itemId: "reasoning" },
})
})
test("reasoning state from start, empty delta, and end is merged", async () => {
test("reasoning state from an empty delta is retained at reasoning end", async () => {
const { published, publisher } = capture()
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { anthropic: { blockType: "thinking" } } }),
),
)
await Effect.runPromise(publisher.publish(LLMEvent.reasoningStart({ id: "reasoning" })))
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningDelta({
id: "reasoning",
text: "",
providerMetadata: { anthropic: { signature: "signed" }, gateway: { traceID: "trace" } },
providerMetadata: { openai: { signature: "signed" } },
}),
),
)
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningEnd({ id: "reasoning", providerMetadata: { anthropic: { stopReason: "tool_use" } } }),
),
)
await Effect.runPromise(publisher.publish(LLMEvent.reasoningEnd({ id: "reasoning" })))
expect(published.find((event) => event.type === "session.reasoning.ended.1")?.data).toMatchObject({
state: { blockType: "thinking", signature: "signed", stopReason: "tool_use" },
})
})
test("provider-executed tool metadata is flattened using the route key", async () => {
const { published, publisher } = capture("openai")
await Effect.runPromise(
publisher.publish(
LLMEvent.toolCall({
id: "hosted",
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "call" } },
}),
),
)
await Effect.runPromise(
publisher.publish(
LLMEvent.toolResult({
id: "hosted",
name: "web_search",
result: { type: "json", value: { found: true } },
providerExecuted: true,
providerMetadata: { openai: { itemId: "result" } },
}),
),
)
expect(published.find((event) => event.type === "session.tool.called.1")?.data).toMatchObject({
state: { itemId: "call" },
})
expect(published.find((event) => event.type === "session.tool.success.1")?.data).toMatchObject({
resultState: { itemId: "result" },
state: { signature: "signed" },
})
})
@@ -236,7 +193,7 @@ test("content-filter finish retains failure evidence until step closeout", async
if (!settlement) throw new Error("Expected content-filter settlement")
await Effect.runPromise(
publisher.publishStepFailure({
cost: Money.USD.make(1.25),
cost: 1.25,
tokens: settlement.tokens,
}),
)
+974 -438
View File
@@ -30,7 +30,6 @@ import { SessionEvent } from "@opencode-ai/core/session/event"
import { SessionInput } from "@opencode-ai/core/session/input"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { PromptInput } from "@opencode-ai/schema/prompt-input"
import { Money } from "@opencode-ai/schema/money"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionRunCoordinator } from "@opencode-ai/core/session/run-coordinator"
@@ -104,20 +103,6 @@ const client = Layer.succeed(
generate: () => Effect.die("unused"),
}),
)
const reply = {
stop: () => [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
text: (text: string, id: string) => fragmentFixture("text", id, [text]).completeEvents,
tool: (id: string, name: string, input: unknown) => [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id, name, input }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
}
const model = Model.make({ id: "fake-model", provider: "fake", route: OpenAIChat.route })
const defaultSystem = SessionRunnerSystemPrompt.provider(model)
const replacementModel = Model.make({ id: "replacement", provider: "fake", route: OpenAIChat.route })
@@ -136,23 +121,8 @@ test("calculates step cost using the matching context tier", () => {
expect(
SessionRunnerLLM.calculateCost(
[
{
input: Money.USDPerMillionTokens.make(1),
output: Money.USDPerMillionTokens.make(2),
cache: {
read: Money.USDPerMillionTokens.make(0.1),
write: Money.USDPerMillionTokens.make(0.5),
},
},
{
tier: { type: "context", size: 100 },
input: Money.USDPerMillionTokens.make(3),
output: Money.USDPerMillionTokens.make(4),
cache: {
read: Money.USDPerMillionTokens.make(0.2),
write: Money.USDPerMillionTokens.make(0.6),
},
},
{ input: 1, output: 2, cache: { read: 0.1, write: 0.5 } },
{ tier: { type: "context", size: 100 }, input: 3, output: 4, cache: { read: 0.2, write: 0.6 } },
],
{ input: 80, output: 10, reasoning: 2, cache: { read: 20, write: 1 } },
),
@@ -162,20 +132,10 @@ test("calculates step cost using the matching context tier", () => {
test("does not apply an ineligible tier without base pricing", () => {
expect(
SessionRunnerLLM.calculateCost(
[
{
tier: { type: "context", size: 100 },
input: Money.USDPerMillionTokens.make(3),
output: Money.USDPerMillionTokens.make(4),
cache: {
read: Money.USDPerMillionTokens.make(0.2),
write: Money.USDPerMillionTokens.make(0.6),
},
},
],
[{ tier: { type: "context", size: 100 }, input: 3, output: 4, cache: { read: 0.2, write: 0.6 } }],
{ input: 80, output: 10, reasoning: 2, cache: { read: 20, write: 0 } },
),
).toBe(Money.USD.zero)
).toBe(0)
})
const authorizations: Tool.Context[] = []
@@ -235,15 +195,12 @@ const echo = Layer.effectDiscard(
const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] })
let modelResolveHook = Effect.void
let currentModel = model
let modelCapabilities = ModelV2.Capabilities.defaults()
const models = SessionRunnerModel.layerWith((session) =>
modelResolveHook.pipe(
Effect.as(
SessionRunnerModel.resolved(
session.model?.id === "replacement" ? replacementModel : currentModel,
session.model?.variant,
[],
modelCapabilities,
),
),
),
@@ -382,8 +339,6 @@ const it = testEffect(
)
const sessionID = SessionV2.ID.make("ses_runner_test")
const otherSessionID = SessionV2.ID.make("ses_runner_other")
const admit = (session: SessionV2.Interface, text: string) =>
session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text }), resume: false })
const insertSession = (id: SessionV2.ID) =>
Effect.gen(function* () {
@@ -405,9 +360,6 @@ const insertSession = (id: SessionV2.ID) =>
const setup = Effect.gen(function* () {
const { db } = yield* Database.Service
requests.length = 0
authorizations.length = 0
executions.length = 0
response = []
systemBaseline = "Initial context"
systemRemoved = false
@@ -415,7 +367,6 @@ const setup = Effect.gen(function* () {
systemLoadHook = Effect.void
modelResolveHook = Effect.void
currentModel = model
modelCapabilities = ModelV2.Capabilities.defaults()
skillBaselines.clear()
responses = undefined
streamFailure = undefined
@@ -434,7 +385,6 @@ const setup = Effect.gen(function* () {
.run()
.pipe(Effect.orDie)
yield* insertSession(sessionID)
return yield* SessionV2.Service
})
const providerUnavailable = () =>
@@ -459,9 +409,14 @@ const rateLimited = (retryAfterMs?: number) =>
})
const setupOverflowRecovery = Effect.gen(function* () {
const session = yield* setup
response = reply.text("Earlier answer", "text-earlier")
yield* admit(session, "Earlier question ".repeat(700))
yield* setup
const session = yield* SessionV2.Service
response = fragmentFixture("text", "text-earlier", ["Earlier answer"]).completeEvents
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Earlier question ".repeat(700) }),
resume: false,
})
yield* session.resume(sessionID)
currentModel = recoveryModel
requests.length = 0
@@ -512,10 +467,7 @@ const recordedStepSettlementEvents = (id: SessionV2.ID, assistantMessageID: Sess
)
})
const hostedCall = (id: string, query: string) =>
LLMEvent.toolCall({ id, name: "web_search", input: { query }, providerExecuted: true })
const requireAssistant = (messages: readonly SessionMessage.Info[]) => {
const requireAssistant = (messages: readonly SessionMessage.Message[]) => {
const assistant = messages.find((message) => message.type === "assistant")
if (!assistant) throw new Error("Assistant message missing")
return assistant
@@ -611,7 +563,7 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
LLMEvent.toolInputStart({ id, name: "echo" }),
...chunks.map((text) => LLMEvent.toolInputDelta({ id, name: "echo", text })),
]
const expectedContent = { type: "tool", id, state: { status: "streaming", input: text } }
const expectedContent = { type: "tool", id, state: { status: "pending", input: text } }
return {
delta: SessionEvent.Tool.Input.Delta,
partialEvents,
@@ -625,12 +577,14 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
const verifyEphemeralDeltas = (kind: FragmentKind) =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
requests.length = 0
const session = yield* SessionV2.Service
const prompt = `Stream ${kind}`
const chunks = Array.from({ length: 32 }, (_, index) => `${index},`)
const fixture = fragmentFixture(kind, fragmentID(kind, "many"), chunks)
const expectedContext = [{ type: "user", text: prompt }, fixture.expectedAssistant]
yield* admit(session, prompt)
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: prompt }), resume: false })
const events = yield* EventV2.Service
const live = yield* events.subscribe(fixture.delta).pipe(Stream.take(32), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
@@ -656,11 +610,13 @@ const verifyEphemeralDeltas = (kind: FragmentKind) =>
const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
requests.length = 0
const session = yield* SessionV2.Service
const prompt = `Fail after ${kind}`
const fixture = fragmentFixture(kind, fragmentID(kind, "partial"), ["Partial"])
const failure = providerUnavailable()
yield* admit(session, prompt)
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: prompt }), resume: false })
responseStream = Stream.concat(Stream.fromIterable(fixture.partialEvents), Stream.fail(failure))
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
@@ -678,11 +634,12 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const prompt = `Interrupt after ${kind}`
const fixture = fragmentFixture(kind, fragmentID(kind, "interrupted"), ["Partial"])
const streamed = yield* Deferred.make<void>()
yield* admit(session, prompt)
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: prompt }), resume: false })
responseStream = Stream.concat(
Stream.fromIterable(fixture.partialEvents),
Stream.fromEffect(Deferred.succeed(streamed, undefined)).pipe(Stream.flatMap(() => Stream.never)),
@@ -710,8 +667,9 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
describe("SessionRunnerLLM", () => {
it.effect("advertises and executes a location registered tool", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const registry = yield* ToolRegistry.Service
const session = yield* SessionV2.Service
const contexts: Tool.Context[] = []
yield* registry.register({
location_context: Tool.make({
@@ -725,8 +683,20 @@ describe("SessionRunnerLLM", () => {
}),
}),
})
yield* admit(session, "Use application context")
responses = [reply.tool("call-location", "location_context", { query: "hello" }), []]
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Use application context" }),
resume: false,
})
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-location", name: "location_context", input: { query: "hello" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[],
]
yield* session.resume(sessionID)
@@ -757,7 +727,13 @@ describe("SessionRunnerLLM", () => {
it.effect("starts a real runner turn after default prompt recording", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
requests.length = 0
responses = undefined
streamGate = undefined
streamStarted = undefined
response = []
const message = yield* session.prompt({
sessionID,
@@ -774,10 +750,16 @@ describe("SessionRunnerLLM", () => {
it.effect("streams one request with registry definitions from chronological V2 user history", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "First")
yield* admit(session, "Second")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
requests.length = 0
responses = undefined
streamGate = undefined
streamStarted = undefined
response = []
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
@@ -791,25 +773,10 @@ describe("SessionRunnerLLM", () => {
}),
)
it.effect("does not advertise tools to a model without tool capability", () =>
Effect.gen(function* () {
yield* setup
modelCapabilities = ModelV2.Capabilities.defaults({ tools: false })
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "No tools" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(requests[0]?.tools).toEqual([])
}),
)
it.effect("retries the first provider turn after system context becomes available", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
const messageID = SessionMessage.ID.create()
systemUnavailable = true
@@ -819,6 +786,7 @@ describe("SessionRunnerLLM", () => {
prompt: PromptInput.Prompt.make({ text: "First" }),
resume: false,
})
requests.length = 0
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
@@ -845,10 +813,13 @@ describe("SessionRunnerLLM", () => {
it.effect("interrupts a source Location runner after a Session moves", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const { db } = yield* Database.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.Moved, {
@@ -863,7 +834,7 @@ describe("SessionRunnerLLM", () => {
.get(),
).toBeUndefined()
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
@@ -874,9 +845,11 @@ describe("SessionRunnerLLM", () => {
it.effect("copies the context checkpoint to a fork", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = []
yield* session.resume(sessionID)
const forked = yield* session.fork({ sessionID })
@@ -901,9 +874,11 @@ describe("SessionRunnerLLM", () => {
it.effect("heals an undecodable stored applied record by re-announcing context", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = []
yield* session.resume(sessionID)
yield* db
.update(InstructionCheckpointTable)
@@ -911,7 +886,7 @@ describe("SessionRunnerLLM", () => {
.where(eq(InstructionCheckpointTable.session_id, sessionID))
.run()
.pipe(Effect.orDie)
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
requests.length = 0
yield* session.resume(sessionID)
@@ -933,12 +908,15 @@ describe("SessionRunnerLLM", () => {
it.effect("reuses one durable baseline after the context producer changes", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "First")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemBaseline = "Changed context"
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -964,11 +942,13 @@ describe("SessionRunnerLLM", () => {
it.effect("uses the selected model family prompt when the agent does not override it", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
yield* admit(session, "First")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = reply.text("Done", "text-provider-prompt")
requests.length = 0
response = fragmentFixture("text", "text-provider-prompt", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([
@@ -980,7 +960,7 @@ describe("SessionRunnerLLM", () => {
it.effect("uses the selected model family prompt when the agent system override is empty", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
currentModel = Model.make({ id: "gpt-5", provider: "openai", route: OpenAIChat.route })
const agent = yield* AgentV2.Service
yield* agent.transform((editor) =>
@@ -989,9 +969,11 @@ describe("SessionRunnerLLM", () => {
agent.mode = "primary"
}),
)
yield* admit(session, "First")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = reply.text("Done", "text-empty-agent-system")
requests.length = 0
response = fragmentFixture("text", "text-empty-agent-system", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual([
@@ -1003,7 +985,7 @@ describe("SessionRunnerLLM", () => {
it.effect("includes the effective default agent system before durable context", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const agent = yield* AgentV2.Service
yield* agent.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
@@ -1011,9 +993,11 @@ describe("SessionRunnerLLM", () => {
agent.mode = "primary"
}),
)
yield* admit(session, "First")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = reply.text("Done", "text-build")
requests.length = 0
response = fragmentFixture("text", "text-build", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"])
@@ -1022,7 +1006,7 @@ describe("SessionRunnerLLM", () => {
it.effect("uses the configured default agent system for omitted-agent sessions", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const agent = yield* AgentV2.Service
yield* agent.transform((editor) => {
editor.update(AgentV2.ID.make("build"), (agent) => {
@@ -1035,9 +1019,11 @@ describe("SessionRunnerLLM", () => {
})
editor.default(AgentV2.ID.make("reviewer"))
})
yield* admit(session, "First")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = reply.text("Done", "text-reviewer")
requests.length = 0
response = fragmentFixture("text", "text-reviewer", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"])
@@ -1047,7 +1033,7 @@ describe("SessionRunnerLLM", () => {
it.effect("uses only the agent prompt and durable baseline as system parts", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const agent = yield* AgentV2.Service
yield* agent.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
@@ -1055,9 +1041,11 @@ describe("SessionRunnerLLM", () => {
agent.mode = "primary"
}),
)
yield* admit(session, "First")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = reply.text("Done", "text-no-system")
requests.length = 0
response = fragmentFixture("text", "text-no-system", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Build agent instructions", "Initial context"])
@@ -1066,7 +1054,7 @@ describe("SessionRunnerLLM", () => {
it.effect("uses an explicitly selected non-build agent system", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const { db } = yield* Database.Service
const agent = yield* AgentV2.Service
yield* agent.transform((editor) =>
@@ -1081,9 +1069,11 @@ describe("SessionRunnerLLM", () => {
.where(eq(SessionTable.id, sessionID))
.run()
.pipe(Effect.orDie)
yield* admit(session, "First")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
response = reply.text("Done", "text-selected")
requests.length = 0
response = fragmentFixture("text", "text-selected", ["Done"]).completeEvents
yield* session.resume(sessionID)
expect(requests.at(-1)?.system.map((part) => part.text)).toEqual(["Reviewer instructions", "Initial context"])
@@ -1093,18 +1083,21 @@ describe("SessionRunnerLLM", () => {
it.effect("updates selected-agent skill guidance after an agent switch", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
skillBaselines.set(AgentV2.ID.make("build"), "Build skills")
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills")
yield* events.publish(SessionEvent.AgentSelected, {
sessionID,
agent: AgentV2.ID.make("reviewer"),
agent: "reviewer",
})
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1117,7 +1110,8 @@ describe("SessionRunnerLLM", () => {
it.effect("keeps the sampled agent when selection changes during observation", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
skillBaselines.set(AgentV2.ID.make("build"), "Build skills")
skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills")
@@ -1128,12 +1122,14 @@ describe("SessionRunnerLLM", () => {
return events
.publish(SessionEvent.AgentSelected, {
sessionID,
agent: AgentV2.ID.make("reviewer"),
agent: "reviewer",
})
.pipe(Effect.asVoid)
})
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1144,7 +1140,8 @@ describe("SessionRunnerLLM", () => {
it.effect("keeps the sampled model when selection changes during model resolution", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
let switched = false
modelResolveHook = Effect.suspend(() => {
@@ -1157,8 +1154,10 @@ describe("SessionRunnerLLM", () => {
})
.pipe(Effect.asVoid)
})
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
expect(requests.map((request) => request.model)).toEqual([model])
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1169,12 +1168,15 @@ describe("SessionRunnerLLM", () => {
it.effect("admits removed context as a chronological System message", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "First")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemRemoved = true
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
@@ -1187,11 +1189,14 @@ describe("SessionRunnerLLM", () => {
it.effect("renders API context entries through the belief lifecycle", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const contextEntries = yield* InstructionEntry.Service
yield* contextEntries.put({ sessionID, key: "deploy-target", value: "production" })
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
// String values render verbatim inside the tagged block at baseline.
@@ -1202,7 +1207,7 @@ describe("SessionRunnerLLM", () => {
// Non-string JSON pretty-prints; the change narrates as a System update.
yield* contextEntries.put({ sessionID, key: "deploy-target", value: { region: "us-east-1" } })
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests[1]?.messages.map((message) => message.role)).toEqual(["user", "system", "user"])
@@ -1223,7 +1228,7 @@ describe("SessionRunnerLLM", () => {
// Deleting the row announces removal through the stored removal text.
yield* contextEntries.remove({ sessionID, key: "deploy-target" })
yield* admit(session, "Third")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests[2]?.messages.map((message) => message.role)).toEqual(["user", "system", "user", "system", "user"])
@@ -1236,20 +1241,23 @@ describe("SessionRunnerLLM", () => {
it.effect("keeps the baseline and chronological System updates after a model switch", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemBaseline = "Changed context"
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemBaseline = "Replacement context"
yield* admit(session, "Third")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1269,28 +1277,31 @@ describe("SessionRunnerLLM", () => {
])
yield* replaySessionProjection(sessionID)
expect(yield* session.messages({ sessionID })).toHaveLength(6)
yield* admit(session, "Fourth")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fourth" }), resume: false })
yield* session.resume(sessionID)
}),
)
it.effect("preserves the baseline while context is temporarily unavailable", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.ModelSelected, {
sessionID,
model: { id: ModelV2.ID.make("replacement"), providerID: ProviderV2.ID.make("fake") },
})
systemUnavailable = true
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
systemUnavailable = false
systemBaseline = "Replacement context"
yield* admit(session, "Third")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1303,15 +1314,17 @@ describe("SessionRunnerLLM", () => {
it.effect("rebuilds the baseline directly after completed compaction", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID,
reason: "manual",
recent: "",
})
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID,
@@ -1320,7 +1333,7 @@ describe("SessionRunnerLLM", () => {
recent: "",
})
systemBaseline = "Replacement context"
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
expect(requests.map((request) => request.system.map((part) => part.text))).toEqual([
@@ -1328,36 +1341,55 @@ describe("SessionRunnerLLM", () => {
[defaultSystem, "Replacement context"],
])
yield* replaySessionProjection(sessionID)
yield* admit(session, "Third")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
}),
)
it.effect("runs one durable compaction barrier before later steer and queued prompts", () =>
it.effect("runs compaction at the next step boundary before continuation, steer, and queue", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
requests.length = 0
executions.length = 0
currentModel = recoveryModel
const session = yield* SessionV2.Service
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
responses = [
reply.text("Active complete", "text-active"),
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-before-compaction", name: "echo", input: { text: "before compaction" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[LLMEvent.textDelta({ id: "summary", text: "durable summary" })],
reply.text("Steer complete", "text-steer"),
reply.text("Queue complete", "text-queue"),
fragmentFixture("text", "text-steer", ["Steer complete"]).completeEvents,
fragmentFixture("text", "text-queue", ["Queue complete"]).completeEvents,
]
yield* admit(session, "Active work")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Active work" }), resume: false })
const active = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
const first = yield* session.compact({ sessionID })
const second = yield* session.compact({ sessionID })
expect(second.id).toBe(first.id)
const [exactRetry, coalesced] = yield* Effect.all(
[session.compact({ id: first.id, sessionID }), session.compact({ sessionID })],
{ concurrency: "unbounded" },
)
expect(exactRetry.id).toBe(first.id)
expect(coalesced.id).toBe(first.id)
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toMatchObject({
id: first.id,
})
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toBeUndefined()
expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toMatchObject({
type: "compaction",
status: "queued",
})
yield* admit(session, "Steer after compaction")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Steer after compaction" }),
resume: false,
})
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Queue after compaction" }),
@@ -1370,6 +1402,8 @@ describe("SessionRunnerLLM", () => {
yield* Fiber.join(active)
expect(requests).toHaveLength(4)
expect(executions).toEqual(["before compaction"])
expect(userTexts(requests[0])).toContain("Active work")
expect(userTexts(requests[1])[0]).toContain("Create a new anchored summary")
expect(userTexts(requests[2])).toContain("Steer after compaction")
expect(userTexts(requests[3])).toContain("Queue after compaction")
@@ -1384,16 +1418,18 @@ describe("SessionRunnerLLM", () => {
it.effect("releases queued prompts when durable compaction fails", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
requests.length = 0
currentModel = recoveryModel
const session = yield* SessionV2.Service
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
responses = [
reply.text("Active complete", "text-active-failure"),
fragmentFixture("text", "text-active-failure", ["Active complete"]).completeEvents,
[],
reply.text("Continued", "text-after-failure"),
fragmentFixture("text", "text-after-failure", ["Continued"]).completeEvents,
]
yield* admit(session, "Active work")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Active work" }), resume: false })
const active = yield* session.resume(sessionID).pipe(Effect.forkChild)
yield* Deferred.await(streamStarted)
@@ -1414,74 +1450,32 @@ describe("SessionRunnerLLM", () => {
type: "compaction",
status: "failed",
})
expect(
(yield* recordedEventTypes(sessionID)).filter(
(type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1),
),
).toHaveLength(1)
}),
)
it.effect("settles an admitted manual compaction that cannot start", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const compaction = yield* session.compact({ sessionID })
yield* session.resume(sessionID)
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
type: "compaction",
status: "failed",
reason: "manual",
error: { message: "Compaction could not start" },
})
expect(
(yield* recordedEventTypes(sessionID)).filter(
(type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1),
),
).toHaveLength(1)
}),
)
it.effect("settles an admitted manual compaction when pre-start resolution throws", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const compaction = yield* session.compact({ sessionID })
modelResolveHook = Effect.die("model resolution failed")
expect(yield* Effect.exit(session.resume(sessionID))).toMatchObject({ _tag: "Failure" })
expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toBeUndefined()
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
type: "compaction",
status: "failed",
reason: "manual",
})
expect(
(yield* recordedEventTypes(sessionID)).filter(
(type) => type === EventV2.versionedType(SessionEvent.Compaction.Failed.type, 1),
),
).toHaveLength(1)
}),
)
it.effect("automatically compacts into a completed summary and retained recent turn", () =>
Effect.gen(function* () {
const session = yield* setup
response = reply.text("Earlier answer", "text-first")
yield* admit(session, "Earlier question ".repeat(180))
yield* setup
const session = yield* SessionV2.Service
response = fragmentFixture("text", "text-first", ["Earlier answer"]).completeEvents
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Earlier question ".repeat(180) }),
resume: false,
})
yield* session.resume(sessionID)
currentModel = compactModel
requests.length = 0
responses = [
reply.text("## Objective\n- Preserve the task", "text-summary"),
reply.text("Continued", "text-final"),
fragmentFixture("text", "text-summary", ["## Objective\n- Preserve the task"]).completeEvents,
fragmentFixture("text", "text-final", ["Continued"]).completeEvents,
]
yield* admit(session, "Recent exact request ".repeat(180))
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Recent exact request ".repeat(180) }),
resume: false,
})
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
@@ -1500,10 +1494,14 @@ describe("SessionRunnerLLM", () => {
requests.length = 0
executions.length = 0
responses = [
reply.text("## Objective\n- Preserve the updated task", "text-summary-2"),
reply.text("Continued again", "text-final-2"),
fragmentFixture("text", "text-summary-2", ["## Objective\n- Preserve the updated task"]).completeEvents,
fragmentFixture("text", "text-final-2", ["Continued again"]).completeEvents,
]
yield* admit(session, "Newest exact request ".repeat(180))
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Newest exact request ".repeat(180) }),
resume: false,
})
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
@@ -1526,10 +1524,10 @@ describe("SessionRunnerLLM", () => {
LLMEvent.stepStart({ index: 0 }),
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
],
reply.text("## Objective\n- Recover overflow", "text-summary"),
reply.text("Recovered", "text-final"),
fragmentFixture("text", "text-summary", ["## Objective\n- Recover overflow"]).completeEvents,
fragmentFixture("text", "text-final", ["Recovered"]).completeEvents,
]
yield* admit(session, "Continue")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
@@ -1554,8 +1552,12 @@ describe("SessionRunnerLLM", () => {
LLMEvent.stepStart({ index: 0 }),
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
]
responses = [overflow(), reply.text("## Objective\n- Recover once", "text-summary"), overflow()]
yield* admit(session, "Continue")
responses = [
overflow(),
fragmentFixture("text", "text-summary", ["## Objective\n- Recover once"]).completeEvents,
overflow(),
]
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
expect(requests).toHaveLength(3)
@@ -1580,10 +1582,10 @@ describe("SessionRunnerLLM", () => {
}),
)
responses = [
reply.text("## Objective\n- Recover raw overflow", "text-summary"),
reply.text("Recovered", "text-final"),
fragmentFixture("text", "text-summary", ["## Objective\n- Recover raw overflow"]).completeEvents,
fragmentFixture("text", "text-final", ["Recovered"]).completeEvents,
]
yield* admit(session, "Continue")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
@@ -1601,15 +1603,14 @@ describe("SessionRunnerLLM", () => {
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
[LLMEvent.providerError({ message: "summary unavailable" })],
]
yield* admit(session, "Continue")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
expect(requests).toHaveLength(2)
const context = yield* session.context(sessionID)
expect(context).toContainEqual(expect.objectContaining({ type: "compaction", status: "failed", reason: "auto" }))
expect(context.slice(-3)).toMatchObject([
expect(context.some((message) => message.type === "compaction")).toBe(false)
expect(context.slice(-2)).toMatchObject([
{ type: "user", text: "Continue" },
{ type: "compaction", status: "failed", reason: "auto" },
{ type: "assistant", finish: "error", error: { message: "prompt too long" } },
])
}),
@@ -1620,12 +1621,12 @@ describe("SessionRunnerLLM", () => {
const session = yield* setupOverflowRecovery
responses = [
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
reply.text("## Objective\n- Interrupted", "text-summary"),
fragmentFixture("text", "text-summary", ["## Objective\n- Interrupted"]).completeEvents,
]
const firstGate = yield* Deferred.make<void>()
const summaryGate = yield* Deferred.make<void>()
streamGate = firstGate
yield* admit(session, "Continue")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
streamGate = summaryGate
@@ -1636,26 +1637,26 @@ describe("SessionRunnerLLM", () => {
expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" })
streamGate = undefined
expect(requests).toHaveLength(2)
expect(yield* session.context(sessionID)).toContainEqual(
expect.objectContaining({ type: "compaction", status: "failed", reason: "auto" }),
)
expect((yield* session.context(sessionID)).some((message) => message.type === "compaction")).toBe(false)
}),
)
it.effect("rebaselines after compaction from the last-applied belief while unobservable", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "First")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "First" }), resume: false })
requests.length = 0
response = []
yield* session.resume(sessionID)
systemBaseline = "Changed context"
yield* admit(session, "Second")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Second" }), resume: false })
yield* session.resume(sessionID)
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID,
reason: "manual",
recent: "",
})
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID,
@@ -1664,7 +1665,7 @@ describe("SessionRunnerLLM", () => {
recent: "",
})
systemUnavailable = true
yield* admit(session, "Third")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Third" }), resume: false })
yield* session.resume(sessionID)
// The rebaseline proceeds while the source is unobservable, restating the model's belief.
@@ -1675,9 +1676,14 @@ describe("SessionRunnerLLM", () => {
it.effect("projects reasoning and tool events without executing or continuing tools", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Use tools")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Use tools" }), resume: false })
requests.length = 0
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.reasoningStart({ id: "reasoning-1" }),
@@ -1694,7 +1700,7 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
input: { query: "hello" },
providerExecuted: true,
providerMetadata: { openai: { source: "provider" } },
providerMetadata: { fake: { source: "provider" } },
}),
LLMEvent.toolResult({
id: "call-provider",
@@ -1707,7 +1713,7 @@ describe("SessionRunnerLLM", () => {
],
},
providerExecuted: true,
providerMetadata: { openai: { source: "provider" } },
providerMetadata: { fake: { source: "provider" } },
}),
LLMEvent.stepFinish({
index: 0,
@@ -1771,10 +1777,31 @@ describe("SessionRunnerLLM", () => {
it.effect("continues with reloaded history after durably settling one local tool call", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Echo this")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo this" }), resume: false })
responses = [reply.tool("call-echo", "echo", { text: "hello" }), reply.text("Done", "text-final")]
requests.length = 0
authorizations.length = 0
executions.length = 0
streamGate = undefined
streamStarted = undefined
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-echo", name: "echo", input: { text: "hello" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-final" }),
LLMEvent.textDelta({ id: "text-final", text: "Done" }),
LLMEvent.textEnd({ id: "text-final" }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
yield* session.resume(sessionID)
@@ -1816,11 +1843,25 @@ describe("SessionRunnerLLM", () => {
it.effect("reloads a model switch before a tool-driven continuation turn", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "Echo this")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo this" }), resume: false })
responses = [reply.tool("call-echo", "echo", { text: "hello" }), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-echo", name: "echo", input: { text: "hello" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
toolExecutionGate = yield* Deferred.make<void>()
toolExecutionsStarted = yield* Deferred.make<void>()
toolExecutionsReady = 1
@@ -1845,30 +1886,32 @@ describe("SessionRunnerLLM", () => {
it.effect("restores durable reasoning provider metadata in a second-turn request", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Think first")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Think first" }), resume: false })
requests.length = 0
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.reasoningStart({ id: "reasoning-anthropic" }),
LLMEvent.reasoningDelta({ id: "reasoning-anthropic", text: "Signed thought" }),
LLMEvent.reasoningEnd({
id: "reasoning-anthropic",
providerMetadata: { openai: { signature: "sig_1" }, anthropic: { ignored: true } },
providerMetadata: { fake: { signature: "sig_1" }, anthropic: { ignored: true } },
}),
LLMEvent.reasoningStart({
id: "reasoning-openai",
providerMetadata: {
openai: { itemId: "rs_1", reasoningEncryptedContent: null },
anthropic: { ignored: true },
fake: { itemId: "rs_1", reasoningEncryptedContent: null },
openai: { ignored: true },
},
}),
LLMEvent.reasoningDelta({ id: "reasoning-openai", text: "Encrypted thought" }),
LLMEvent.reasoningEnd({
id: "reasoning-openai",
providerMetadata: {
openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
anthropic: { ignored: true },
fake: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
openai: { ignored: true },
},
}),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
@@ -1882,11 +1925,7 @@ describe("SessionRunnerLLM", () => {
{
type: "assistant",
content: [
{
type: "reasoning",
text: "Signed thought",
state: { signature: "sig_1" },
},
{ type: "reasoning", text: "Signed thought", state: { signature: "sig_1" } },
{
type: "reasoning",
text: "Encrypted thought",
@@ -1896,20 +1935,16 @@ describe("SessionRunnerLLM", () => {
},
])
yield* admit(session, "Continue")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
response = []
yield* session.resume(sessionID)
expect(requests[1]?.messages[1]?.content).toEqual([
{
type: "reasoning",
text: "Signed thought",
providerMetadata: { openai: { signature: "sig_1" } },
},
{ type: "reasoning", text: "Signed thought", providerMetadata: { fake: { signature: "sig_1" } } },
{
type: "reasoning",
text: "Encrypted thought",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { fake: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
}),
@@ -1917,9 +1952,11 @@ describe("SessionRunnerLLM", () => {
it.effect("replays durable provider-executed tool results inline in a second-turn request", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Search first")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Search first" }), resume: false })
requests.length = 0
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({
@@ -1927,14 +1964,14 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "hosted-search" }, fake: { ignored: true } },
providerMetadata: { fake: { itemId: "hosted-search" }, openai: { ignored: true } },
}),
LLMEvent.toolResult({
id: "hosted-search",
name: "web_search",
result: { type: "json", value: [{ title: "Effect" }] },
providerExecuted: true,
providerMetadata: { openai: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } },
providerMetadata: { fake: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } },
}),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
@@ -1942,7 +1979,7 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
yield* replaySessionProjection(sessionID)
yield* admit(session, "Continue")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Continue" }), resume: false })
response = []
yield* session.resume(sessionID)
@@ -1954,7 +1991,7 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "hosted-search" } },
providerMetadata: { fake: { itemId: "hosted-search" } },
},
{
type: "tool-result",
@@ -1962,7 +1999,7 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
result: { type: "json", value: [{ title: "Effect" }] },
providerExecuted: true,
providerMetadata: { openai: { blockType: "web_search_tool_result" } },
providerMetadata: { fake: { blockType: "web_search_tool_result" } },
},
])
}),
@@ -1970,12 +2007,17 @@ describe("SessionRunnerLLM", () => {
it.effect("starts recorded local tools eagerly and awaits settlement before continuing", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Echo five times")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo five times" }), resume: false })
requests.length = 0
executions.length = 0
toolExecutionGate = yield* Deferred.make<void>()
toolExecutionsStarted = yield* Deferred.make<void>()
const providerGate = yield* Deferred.make<void>()
response = []
responses = undefined
const initial = Stream.fromIterable([
LLMEvent.stepStart({ index: 0 }),
...Array.from({ length: 5 }, (_, index) =>
@@ -1986,6 +2028,7 @@ describe("SessionRunnerLLM", () => {
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
])
streamGate = undefined
responseStream = Stream.concat(
initial,
Stream.fromEffect(Deferred.await(providerGate)).pipe(Stream.flatMap(() => final)),
@@ -2025,12 +2068,25 @@ describe("SessionRunnerLLM", () => {
it.effect("settles repeated provider-local tool call IDs against their owning assistant messages", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Echo twice")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Echo twice" }), resume: false })
requests.length = 0
executions.length = 0
responses = [
reply.tool("tool_0", "echo", { text: "first" }),
reply.tool("tool_0", "echo", { text: "second" }),
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "tool_0", name: "echo", input: { text: "first" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "tool_0", name: "echo", input: { text: "second" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[],
]
@@ -2100,10 +2156,20 @@ describe("SessionRunnerLLM", () => {
it.effect("joins concurrent resume calls into one active provider run", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Run once")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run once" }), resume: false })
response = reply.text("Once", "text-once")
requests.length = 0
responses = undefined
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-once" }),
LLMEvent.textDelta({ id: "text-once", text: "Once" }),
LLMEvent.textEnd({ id: "text-once" }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2129,10 +2195,23 @@ describe("SessionRunnerLLM", () => {
it.effect("steers an active provider turn with newly recorded prompts", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start working")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
responses = [reply.stop(), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2159,10 +2238,29 @@ describe("SessionRunnerLLM", () => {
it.effect("promotes queued input after continuation ends", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start working")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
responses = [reply.tool("call-echo", "echo", { text: "hello" }), reply.stop(), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-echo", name: "echo", input: { text: "hello" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2187,11 +2285,24 @@ describe("SessionRunnerLLM", () => {
it.effect("preserves durable queued input for a later wake after interruption", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* admit(session, "Interrupt current work")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Interrupt current work" }),
resume: false,
})
responses = [[], reply.stop()]
requests.length = 0
responses = [
[],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2221,11 +2332,24 @@ describe("SessionRunnerLLM", () => {
it.effect("preserves durable steering input for a later resume after interruption", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const { db } = yield* Database.Service
yield* admit(session, "Interrupt current work")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Interrupt current work" }),
resume: false,
})
responses = [[], reply.stop()]
requests.length = 0
responses = [
[],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2255,10 +2379,28 @@ describe("SessionRunnerLLM", () => {
it.effect("promotes queued inputs one at a time in FIFO order", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start working")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
responses = [reply.stop(), reply.stop(), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2280,8 +2422,9 @@ describe("SessionRunnerLLM", () => {
it.effect("promotes queued input after steering continuation ends", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start steering")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start steering" }), resume: false })
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Queue for later" }),
@@ -2289,7 +2432,19 @@ describe("SessionRunnerLLM", () => {
resume: false,
})
responses = [reply.stop(), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
yield* session.resume(sessionID)
@@ -2301,10 +2456,33 @@ describe("SessionRunnerLLM", () => {
it.effect("promotes steers before the next queued input", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start working")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
responses = [reply.stop(), reply.stop(), reply.stop(), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
const firstGate = yield* Deferred.make<void>()
const secondGate = yield* Deferred.make<void>()
streamGate = firstGate
@@ -2346,10 +2524,23 @@ describe("SessionRunnerLLM", () => {
it.effect("coalesces multiple active steering prompts into one continuation turn", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start working")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
responses = [reply.stop(), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2373,9 +2564,13 @@ describe("SessionRunnerLLM", () => {
it.effect("runs steering input accepted while the active provider turn fails", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Start working")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start working" }), resume: false })
requests.length = 0
responses = undefined
response = []
streamFailure = invalidRequest()
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2398,15 +2593,20 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails local tools left running by a prior process before continuing", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "Recover interrupted tool")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Recover interrupted tool" }),
resume: false,
})
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
const assistantMessageID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
agent: AgentV2.ID.make("build"),
agent: "build",
model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") },
})
yield* events.publish(SessionEvent.Tool.Input.Started, {
@@ -2455,15 +2655,20 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails hosted tools left running by a prior process before continuing inline", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "Recover interrupted hosted tool")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Recover interrupted hosted tool" }),
resume: false,
})
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
const assistantMessageID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
agent: AgentV2.ID.make("build"),
agent: "build",
model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") },
})
yield* events.publish(SessionEvent.Tool.Input.Started, {
@@ -2497,7 +2702,7 @@ describe("SessionRunnerLLM", () => {
type: "tool-call",
id: "call-hosted-interrupted",
providerExecuted: true,
providerMetadata: { openai: { itemId: "call-hosted-interrupted" } },
providerMetadata: { fake: { itemId: "call-hosted-interrupted" } },
},
{ type: "tool-result", id: "call-hosted-interrupted", providerExecuted: true, result: { type: "error" } },
])
@@ -2506,15 +2711,20 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails pending tool input left by a prior process before continuing", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* admit(session, "Recover interrupted tool input")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Recover interrupted tool input" }),
resume: false,
})
yield* SessionInput.promoteSteers((yield* Database.Service).db, events, sessionID)
const assistantMessageID = SessionMessage.ID.create()
yield* events.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
agent: AgentV2.ID.make("build"),
agent: "build",
model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") },
})
yield* events.publish(SessionEvent.Tool.Input.Started, {
@@ -2538,7 +2748,8 @@ describe("SessionRunnerLLM", () => {
it.effect("promotes the first queued input when woken while idle", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Wait in queue" }),
@@ -2546,6 +2757,7 @@ describe("SessionRunnerLLM", () => {
resume: false,
})
requests.length = 0
yield* (yield* SessionExecution.Service).wake(sessionID)
yield* Effect.yieldNow
@@ -2556,17 +2768,26 @@ describe("SessionRunnerLLM", () => {
it.effect("retries inbox input after prompt projection rolls back", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
const defect = new Error("fail after prompt promotion")
let fail = true
yield* events.project(SessionEvent.PromptPromoted, () => (fail ? Effect.die(defect) : Effect.void))
yield* admit(session, "Recover promoted input")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Recover promoted input" }),
resume: false,
})
expect(yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
fail = false
requests.length = 0
response = reply.stop()
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
]
yield* (yield* SessionExecution.Service).wake(sessionID)
while (requests.length === 0) yield* Effect.yieldNow
@@ -2577,15 +2798,21 @@ describe("SessionRunnerLLM", () => {
it.effect("does not strand a committed promotion when a post-commit listener defects", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const events = yield* EventV2.Service
yield* events.listen((event) =>
event.type === SessionEvent.PromptPromoted.type
? Effect.die("fail after prompt promotion commits")
: Effect.void,
)
yield* admit(session, "Run committed promotion")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Run committed promotion" }),
resume: false,
})
requests.length = 0
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
@@ -2595,15 +2822,19 @@ describe("SessionRunnerLLM", () => {
it.effect("runs different sessions concurrently", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
yield* insertSession(otherSessionID)
yield* admit(session, "Run first")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Run first" }), resume: false })
yield* session.prompt({
sessionID: otherSessionID,
prompt: PromptInput.Prompt.make({ text: "Run second" }),
resume: false,
})
requests.length = 0
responses = undefined
response = []
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2628,11 +2859,12 @@ describe("SessionRunnerLLM", () => {
it.effect("bounds 64-character session prompt cache keys", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const longSessionID = SessionV2.ID.make(`ses_${"a".repeat(64)}`)
const otherLongSessionID = SessionV2.ID.make(`ses_${"b".repeat(64)}`)
yield* insertSession(longSessionID)
yield* insertSession(otherLongSessionID)
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID: longSessionID,
prompt: PromptInput.Prompt.make({ text: "Run long session" }),
@@ -2644,6 +2876,7 @@ describe("SessionRunnerLLM", () => {
resume: false,
})
requests.length = 0
yield* session.resume(longSessionID)
yield* session.resume(otherLongSessionID)
@@ -2656,9 +2889,17 @@ describe("SessionRunnerLLM", () => {
it.effect("fans out one failed run and allows a later retry", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Retry after failure")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Retry after failure" }),
resume: false,
})
requests.length = 0
responses = undefined
response = []
streamFailure = invalidRequest()
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2683,10 +2924,30 @@ describe("SessionRunnerLLM", () => {
it.effect("durably settles local tool failures before continuing", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Call missing")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call missing" }), resume: false })
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-missing", name: "missing", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-after-error" }),
LLMEvent.textDelta({ id: "text-after-error", text: "Recovered" }),
LLMEvent.textEnd({ id: "text-after-error" }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = undefined
streamStarted = undefined
responses = [reply.tool("call-missing", "missing", {}), reply.text("Recovered", "text-after-error")]
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
@@ -2698,10 +2959,7 @@ describe("SessionRunnerLLM", () => {
{
type: "tool",
id: "call-missing",
state: {
status: "error",
error: { type: "tool.unknown", message: "Unknown tool: missing" },
},
state: { status: "error", error: { message: "Unknown tool: missing" } },
},
],
},
@@ -2712,10 +2970,27 @@ describe("SessionRunnerLLM", () => {
it.effect("returns unexpected local tool defects to the model and continues", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Call defect")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call defect" }), resume: false })
responses = [reply.tool("call-defect", "defect", {}), reply.text("Recovered", "text-after-defect")]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-defect", name: "defect", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-after-defect" }),
LLMEvent.textDelta({ id: "text-after-defect", text: "Recovered" }),
LLMEvent.textEnd({ id: "text-after-defect" }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
yield* session.resume(sessionID)
@@ -2751,7 +3026,8 @@ describe("SessionRunnerLLM", () => {
it.effect("returns policy-blocked tools to the model and continues", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
yield* registry.register({
blocked: Tool.make({
@@ -2764,9 +3040,22 @@ describe("SessionRunnerLLM", () => {
),
}),
})
yield* admit(session, "Call blocked")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call blocked" }), resume: false })
responses = [reply.tool("call-blocked", "blocked", {}), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-blocked", name: "blocked", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
yield* session.resume(sessionID)
@@ -2786,7 +3075,8 @@ describe("SessionRunnerLLM", () => {
it.effect("interrupts runner continuation when permission approval is declined", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
yield* registry.register({
declined: Tool.make({
@@ -2796,9 +3086,15 @@ describe("SessionRunnerLLM", () => {
execute: () => Effect.die(new PermissionV2.DeclinedError()),
}),
})
yield* admit(session, "Call declined")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call declined" }), resume: false })
response = reply.tool("call-declined", "declined", {})
requests.length = 0
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-declined", name: "declined", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
]
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
@@ -2823,7 +3119,8 @@ describe("SessionRunnerLLM", () => {
it.effect("returns permission corrections to the model and continues", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
yield* registry.register({
corrected: Tool.make({
@@ -2836,9 +3133,22 @@ describe("SessionRunnerLLM", () => {
),
}),
})
yield* admit(session, "Call corrected")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call corrected" }), resume: false })
responses = [reply.tool("call-corrected", "corrected", {}), reply.stop()]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-corrected", name: "corrected", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
yield* session.resume(sessionID)
@@ -2858,10 +3168,20 @@ describe("SessionRunnerLLM", () => {
it.effect("fails the drain when tool output persistence fails", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Call storefail")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Call storefail" }), resume: false })
responses = [reply.tool("call-storefail", "storefail", {}), []]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-storefail", name: "storefail", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[],
]
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
@@ -2893,7 +3213,8 @@ describe("SessionRunnerLLM", () => {
it.effect("preserves permission rejection and stops before continuation", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
yield* registry.register({
permissionfail: Tool.make({
@@ -2911,9 +3232,19 @@ describe("SessionRunnerLLM", () => {
}),
}),
})
yield* admit(session, "Reject permission")
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Reject permission" }),
resume: false,
})
requests.length = 0
responses = [
reply.tool("call-permission", "permissionfail", {}),
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-permission", name: "permissionfail", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: "stop" })],
]
@@ -2951,7 +3282,8 @@ describe("SessionRunnerLLM", () => {
it.effect("interrupts runner continuation when a question is cancelled", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
yield* registry.register({
question: Tool.make({
@@ -2961,9 +3293,18 @@ describe("SessionRunnerLLM", () => {
execute: () => Effect.die(new QuestionTool.CancelledError()),
}),
})
yield* admit(session, "Ask then stop")
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Ask then stop" }), resume: false })
responses = [reply.tool("call-question", "question", {}), []]
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-question", name: "question", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[],
]
const run = yield* session.resume(sessionID).pipe(Effect.exit, Effect.forkChild)
const exit = yield* Fiber.join(run)
@@ -2989,8 +3330,13 @@ describe("SessionRunnerLLM", () => {
it.effect("awaits started local tools before surfacing provider stream failure", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Settle before failing")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Settle before failing" }),
resume: false,
})
const failure = providerUnavailable()
toolExecutionGate = yield* Deferred.make<void>()
responseStream = Stream.concat(
@@ -3030,8 +3376,14 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails blocked local tools when a provider turn is interrupted", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Interrupt blocked tool")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Interrupt blocked tool" }),
resume: false,
})
executions.length = 0
toolExecutionGate = yield* Deferred.make<void>()
responseStream = Stream.concat(
Stream.fromIterable([
@@ -3086,8 +3438,15 @@ describe("SessionRunnerLLM", () => {
it.effect("interrupts a blocked provider turn without local tool execution", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Interrupt provider")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Interrupt provider" }),
resume: false,
})
requests.length = 0
response = []
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -3111,12 +3470,23 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails blocked local tools when interrupted while awaiting settlement", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Interrupt tool settlement")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Interrupt tool settlement" }),
resume: false,
})
executions.length = 0
toolExecutionGate = yield* Deferred.make<void>()
toolExecutionsStarted = yield* Deferred.make<void>()
toolExecutionsReady = 1
response = reply.tool("call-await-interrupt", "echo", { text: "blocked" })
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-await-interrupt", name: "echo", input: { text: "blocked" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
]
const runner = yield* SessionRunner.Service
const run = yield* runner.drain({ sessionID, force: true }).pipe(Effect.forkChild)
@@ -3148,18 +3518,35 @@ describe("SessionRunnerLLM", () => {
it.effect("forces a text response on an agent's configured final step", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const agents = yield* AgentV2.Service
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.steps = 2
}),
)
yield* admit(session, "Finish at the limit")
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Finish at the limit" }),
resume: false,
})
requests.length = 0
executions.length = 0
responses = [
reply.tool("call-terminal", "echo", { text: "done" }),
reply.tool("call-forbidden", "echo", { text: "forbidden" }),
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-terminal", name: "echo", input: { text: "done" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-forbidden", name: "echo", input: { text: "forbidden" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
]
yield* session.resume(sessionID)
@@ -3183,19 +3570,36 @@ describe("SessionRunnerLLM", () => {
it.effect("resets the configured step allowance when steering input promotes", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const agents = yield* AgentV2.Service
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.steps = 2
}),
)
yield* admit(session, "Start work")
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Start work" }), resume: false })
requests.length = 0
executions.length = 0
responses = [
reply.tool("call-before-steer", "echo", { text: "before" }),
reply.tool("call-after-steer", "echo", { text: "after" }),
reply.stop(),
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-before-steer", name: "echo", input: { text: "before" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-after-steer", name: "echo", input: { text: "after" } }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
],
]
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -3218,9 +3622,14 @@ describe("SessionRunnerLLM", () => {
it.effect("projects provider errors as terminal assistant step failures", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail durably")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail durably" }), resume: false })
requests.length = 0
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [LLMEvent.stepStart({ index: 0 }), LLMEvent.providerError({ message: "Provider unavailable" })]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
@@ -3235,9 +3644,11 @@ describe("SessionRunnerLLM", () => {
it.effect("projects provider errors emitted before assistant step start", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail before step")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Fail before step" }), resume: false })
requests.length = 0
response = [LLMEvent.providerError({ message: "Provider unavailable" })]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
@@ -3252,8 +3663,9 @@ describe("SessionRunnerLLM", () => {
it.effect("projects content-filter finishes as visible terminal failures", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Blocked response")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Blocked response" }), resume: false })
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "partial" }),
@@ -3288,8 +3700,13 @@ describe("SessionRunnerLLM", () => {
it.effect("settles a local tool before one content-filter step failure", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Tool before blocked response")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Tool before blocked response" }),
resume: false,
})
toolExecutionGate = yield* Deferred.make<void>()
toolExecutionsStarted = yield* Deferred.make<void>()
toolExecutionsReady = 1
@@ -3323,9 +3740,15 @@ describe("SessionRunnerLLM", () => {
it.effect("does not recover context overflow after durable assistant output", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail after output")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Fail after output" }),
resume: false,
})
requests.length = 0
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-partial" }),
@@ -3350,8 +3773,13 @@ describe("SessionRunnerLLM", () => {
it.effect("projects raw provider stream failures as terminal assistant step failures", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail raw stream durably")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Fail raw stream durably" }),
resume: false,
})
const failure = invalidRequest()
responseStream = Stream.fail(failure)
@@ -3366,10 +3794,12 @@ describe("SessionRunnerLLM", () => {
it.effect("retries eligible pre-output failures after exponential backoff", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Retry transport")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Retry transport" }), resume: false })
requests.length = 0
responseStream = Stream.fail(providerUnavailable())
response = reply.text("Recovered", "retry-success")
response = fragmentFixture("text", "retry-success", ["Recovered"]).completeEvents
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
@@ -3393,10 +3823,12 @@ describe("SessionRunnerLLM", () => {
it.effect("uses a larger provider retry-after delay", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Retry rate limit")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Retry rate limit" }), resume: false })
requests.length = 0
responseStream = Stream.fail(rateLimited(5_000))
response = reply.text("Recovered", "retry-after-success")
response = fragmentFixture("text", "retry-after-success", ["Recovered"]).completeEvents
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
@@ -3410,8 +3842,10 @@ describe("SessionRunnerLLM", () => {
it.effect("stops after five total retry attempts", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Exhaust retries")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Exhaust retries" }), resume: false })
requests.length = 0
streamFailure = providerUnavailable()
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
@@ -3444,14 +3878,20 @@ describe("SessionRunnerLLM", () => {
it.effect("counts retry attempts against the agent step allowance", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const agents = yield* AgentV2.Service
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.steps = 2
}),
)
yield* admit(session, "Bound retries by steps")
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Bound retries by steps" }),
resume: false,
})
requests.length = 0
const failure = providerUnavailable()
responseStream = Stream.fail(failure)
streamFailure = failure
@@ -3471,8 +3911,10 @@ describe("SessionRunnerLLM", () => {
it.effect("does not retry non-eligible provider failures", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Do not retry")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Do not retry" }), resume: false })
requests.length = 0
const failure = invalidRequest()
streamFailure = failure
@@ -3484,9 +3926,16 @@ describe("SessionRunnerLLM", () => {
it.effect("does not continue automatically after a provider error follows a local tool call", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Do not continue failed provider")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Do not continue failed provider" }),
resume: false,
})
requests.length = 0
const executionCount = executions.length
toolExecutionGate = yield* Deferred.make<void>()
toolExecutionsStarted = yield* Deferred.make<void>()
toolExecutionsReady = 1
@@ -3504,7 +3953,7 @@ describe("SessionRunnerLLM", () => {
toolExecutionsStarted = undefined
expect(requests).toHaveLength(1)
expect(executions).toEqual(["settled"])
expect(executions.slice(executionCount)).toEqual(["settled"])
const context = yield* session.context(sessionID)
const assistant = requireAssistant(context)
expect((yield* recordedStepSettlementEvents(sessionID, assistant.id)).map((event) => event.type)).toEqual([
@@ -3518,12 +3967,23 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails a hosted tool when its provider errors before returning a result", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail hosted tool durably")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Fail hosted tool durably" }),
resume: false,
})
requests.length = 0
response = [
LLMEvent.stepStart({ index: 0 }),
hostedCall("call-hosted-provider-error", "effect"),
LLMEvent.toolCall({
id: "call-hosted-provider-error",
name: "web_search",
input: { query: "effect" },
providerExecuted: true,
}),
LLMEvent.providerError({ message: "Provider unavailable" }),
]
@@ -3550,8 +4010,13 @@ describe("SessionRunnerLLM", () => {
it.effect("preserves a tool defect before provider failure settlement", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Defect while provider fails")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Defect while provider fails" }),
resume: false,
})
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }),
@@ -3575,9 +4040,22 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails a hosted tool left unresolved at normal provider EOF", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail hosted tool at EOF")
response = [LLMEvent.stepStart({ index: 0 }), hostedCall("call-hosted-eof", "effect")]
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Fail hosted tool at EOF" }),
resume: false,
})
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({
id: "call-hosted-eof",
name: "web_search",
input: { query: "effect" },
providerExecuted: true,
}),
]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider did not return a tool result")
const assistant = requireAssistant(yield* session.context(sessionID))
@@ -3607,11 +4085,21 @@ describe("SessionRunnerLLM", () => {
it.effect("fails an unresolved hosted tool before one clean step end", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Settle hosted tool before ending")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Settle hosted tool before ending" }),
resume: false,
})
response = [
LLMEvent.stepStart({ index: 0 }),
hostedCall("call-hosted-clean-end", "effect"),
LLMEvent.toolCall({
id: "call-hosted-clean-end",
name: "web_search",
input: { query: "effect" },
providerExecuted: true,
}),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
]
@@ -3634,8 +4122,13 @@ describe("SessionRunnerLLM", () => {
it.effect("settles unresolved local and hosted tools before one raw provider failure", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail unresolved tools")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Fail unresolved tools" }),
resume: false,
})
const failure = invalidRequest()
const providerFailed = yield* Deferred.make<void>()
toolExecutionGate = yield* Deferred.make<void>()
@@ -3643,7 +4136,12 @@ describe("SessionRunnerLLM", () => {
Stream.fromIterable([
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-local-raw-failure", name: "defect", input: {} }),
hostedCall("call-hosted-raw-failure-pair", "effect"),
LLMEvent.toolCall({
id: "call-hosted-raw-failure-pair",
name: "web_search",
input: { query: "effect" },
providerExecuted: true,
}),
]),
Stream.fromEffect(Deferred.succeed(providerFailed, undefined)).pipe(Stream.flatMap(() => Stream.fail(failure))),
)
@@ -3672,11 +4170,25 @@ describe("SessionRunnerLLM", () => {
it.effect("durably fails a hosted tool left unresolved by a raw provider stream failure", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Fail hosted tool on raw failure")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Fail hosted tool on raw failure" }),
resume: false,
})
requests.length = 0
const failure = providerUnavailable()
responseStream = Stream.concat(
Stream.fromIterable([LLMEvent.stepStart({ index: 0 }), hostedCall("call-hosted-raw-failure", "effect")]),
Stream.fromIterable([
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({
id: "call-hosted-raw-failure",
name: "web_search",
input: { query: "effect" },
providerExecuted: true,
}),
]),
Stream.fail(failure),
)
@@ -3708,9 +4220,13 @@ describe("SessionRunnerLLM", () => {
it.effect("rejects a second text start before the open fragment ends", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Two blocks")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Two blocks" }), resume: false })
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-1" }),
@@ -3726,9 +4242,13 @@ describe("SessionRunnerLLM", () => {
it.effect("projects sequential text fragments as separate content parts", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Two blocks")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Two blocks" }), resume: false })
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-1" }),
@@ -3770,7 +4290,11 @@ describe("SessionRunnerLLM", () => {
it.effect("rejects duplicate streamed text starts", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [LLMEvent.textStart({ id: "text-1" }), LLMEvent.textStart({ id: "text-1" })]
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
@@ -3782,15 +4306,23 @@ describe("SessionRunnerLLM", () => {
it.effect("transitions streamed raw tool input to parsed called input", () =>
Effect.gen(function* () {
const session = yield* setup
yield* admit(session, "Call provider tool")
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({
sessionID,
prompt: PromptInput.Prompt.make({ text: "Call provider tool" }),
resume: false,
})
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolInputStart({ id: "call-parsed", name: "web_search" }),
LLMEvent.toolInputDelta({ id: "call-parsed", name: "web_search", text: '{"query":"hello"}' }),
LLMEvent.toolInputEnd({ id: "call-parsed", name: "web_search" }),
hostedCall("call-parsed", "hello"),
LLMEvent.toolCall({ id: "call-parsed", name: "web_search", input: { query: "hello" }, providerExecuted: true }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
]
@@ -3809,7 +4341,11 @@ describe("SessionRunnerLLM", () => {
it.effect("rejects malformed streamed tool input ordering", () =>
Effect.gen(function* () {
const session = yield* setup
yield* setup
const session = yield* SessionV2.Service
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [LLMEvent.toolInputDelta({ id: "call-1", name: "read", text: "{}" })]
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
+3 -4
View File
@@ -26,8 +26,7 @@ const skills = Layer.mock(SkillV2.Service, {
list: () =>
Effect.succeed([
SkillV2.Info.make({
id: SkillV2.ID.make("effect"),
name: SkillV2.Name.make("Effect"),
name: "effect",
description: "Effect guidance",
location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")),
content: "Use Effect",
@@ -61,10 +60,10 @@ describe("SessionV2.skill", () => {
const session = yield* sessions.create({ location })
const id = SessionMessage.ID.make("msg_caller_skill")
yield* sessions.skill({ id, sessionID: session.id, skill: SkillV2.ID.make("effect"), resume: false })
yield* sessions.skill({ id, sessionID: session.id, skill: "effect", resume: false })
expect(yield* sessions.messages({ sessionID: session.id })).toContainEqual(
expect.objectContaining({ id, type: "skill", skill: "effect", name: "Effect", text: "Use Effect" }),
expect.objectContaining({ id, type: "skill", name: "effect", text: "Use Effect" }),
)
}),
)
@@ -4,7 +4,6 @@ import { DateTime, Effect, Schema } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { AgentV2 } from "@opencode-ai/core/agent"
import { EventTable } from "@opencode-ai/core/event/sql"
import { ModelV2 } from "@opencode-ai/core/model"
import { Project } from "@opencode-ai/core/project"
@@ -52,7 +51,7 @@ describe("Tool.Progress", () => {
yield* service.publish(SessionEvent.Step.Started, {
sessionID,
assistantMessageID,
agent: AgentV2.ID.make("build"),
agent: "build",
model,
})
const readAssistant = Effect.gen(function* () {
+2 -3
View File
@@ -76,7 +76,6 @@ test("Core reuses the canonical shared schemas", async () => {
const schemas = [
[AgentV2.ID, Agent.ID],
[AgentV2.Name, Agent.Name],
[AgentV2.Color, Agent.Color],
[AgentV2.Info, Agent.Info],
[coreCommand.Info, Command.Info],
@@ -146,7 +145,7 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSessionMessage.Synthetic, SessionMessage.Synthetic],
[coreSessionMessage.System, SessionMessage.System],
[coreSessionMessage.Shell, SessionMessage.Shell],
[coreSessionMessage.ToolStateStreaming, SessionMessage.ToolStateStreaming],
[coreSessionMessage.ToolStatePending, SessionMessage.ToolStatePending],
[coreSessionMessage.ToolStateRunning, SessionMessage.ToolStateRunning],
[coreSessionMessage.ToolStateCompleted, SessionMessage.ToolStateCompleted],
[coreSessionMessage.ToolStateError, SessionMessage.ToolStateError],
@@ -157,7 +156,7 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSessionMessage.AssistantContent, SessionMessage.AssistantContent],
[coreSessionMessage.Assistant, SessionMessage.Assistant],
[coreSessionMessage.Compaction, SessionMessage.Compaction],
[coreSessionMessage.Info, SessionMessage.Info],
[coreSessionMessage.Message, SessionMessage.Message],
[coreSessionTodo.Info, SessionTodo.Info],
[coreSessionTodo.Event, SessionTodo.Event],
[coreSkill.DirectorySource, Skill.DirectorySource],
+5 -8
View File
@@ -88,15 +88,13 @@ describe("SkillV2", () => {
])
expect(yield* skill.list()).toEqual([
SkillV2.Info.make({
id: SkillV2.ID.make("foo"),
name: SkillV2.Name.make("foo"),
name: "foo",
slash: true,
location: AbsolutePath.make(path.join(first, "foo.md")),
content: "# foo",
}),
{
id: SkillV2.ID.make("review"),
name: SkillV2.Name.make("review"),
name: "review",
description: "Second",
location: AbsolutePath.make(path.join(second, "review", "SKILL.md")),
content: "# review",
@@ -131,8 +129,8 @@ describe("SkillV2", () => {
const skill = yield* SkillV2.Service
yield* skill.transform((editor) => editor.source({ type: "url", url: "https://example.test/skills/" }))
expect((yield* skill.list()).map((item) => item.name)).toEqual([SkillV2.Name.make("deploy")])
expect((yield* skill.list()).map((item) => item.name)).toEqual([SkillV2.Name.make("deploy")])
expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
expect(pulls).toBe(1)
expect(SkillV2.available(yield* skill.list(), (yield* agents.get(AgentV2.ID.make("reviewer")))!)).toEqual([])
}),
@@ -167,8 +165,7 @@ metadata:
expect(yield* skill.list()).toEqual([
{
id: SkillV2.ID.make("manual"),
name: SkillV2.Name.make("manual"),
name: "manual",
description: "Manual only",
slash: true,
autoinvoke: false,
+10 -17
View File
@@ -11,28 +11,24 @@ import { it } from "../lib/effect"
const build = AgentV2.ID.make("build")
const effect = SkillV2.Info.make({
id: SkillV2.ID.make("effect"),
name: SkillV2.Name.make("Effect"),
name: "effect",
description: "Build applications with Effect",
location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")),
content: "Effect guidance",
})
const hidden = SkillV2.Info.make({
id: SkillV2.ID.make("hidden"),
name: SkillV2.Name.make("Hidden"),
name: "hidden",
location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")),
content: "Undescribed guidance",
})
const denied = SkillV2.Info.make({
id: SkillV2.ID.make("denied"),
name: SkillV2.Name.make("Denied"),
name: "denied",
description: "Must not be advertised",
location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")),
content: "Denied guidance",
})
const manual = SkillV2.Info.make({
id: SkillV2.ID.make("manual"),
name: SkillV2.Name.make("Manual"),
name: "manual",
description: "Load only when explicitly selected",
autoinvoke: false,
location: AbsolutePath.make(path.resolve("/skills/manual/SKILL.md")),
@@ -63,8 +59,7 @@ describe("SkillGuidance", () => {
"Use the skill tool to load a skill when a task matches its description.",
"<available_skills>",
" <skill>",
" <id>effect</id>",
" <name>Effect</name>",
" <name>effect</name>",
" <description>Build applications with Effect</description>",
" </skill>",
"</available_skills>",
@@ -79,7 +74,7 @@ describe("SkillGuidance", () => {
.pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))),
).toMatchObject({
_tag: "Updated",
text: "The following skill IDs are no longer available and must not be used: effect.",
text: "The following skills are no longer available and must not be used: effect.",
})
}).pipe(Effect.provide(layer(() => skills)))
})
@@ -87,8 +82,7 @@ describe("SkillGuidance", () => {
it.effect("announces added and removed skills as deltas without restating the list", () => {
const agent = AgentV2.Info.make(AgentV2.Info.empty(build))
const debugging = SkillV2.Info.make({
id: SkillV2.ID.make("debugging"),
name: SkillV2.Name.make("Debugging"),
name: "debugging",
description: "Diagnose hard bugs",
location: AbsolutePath.make(path.resolve("/skills/debugging/SKILL.md")),
content: "Debugging guidance",
@@ -109,8 +103,7 @@ describe("SkillGuidance", () => {
text: [
"New skills are available in addition to those previously listed:",
" <skill>",
" <id>debugging</id>",
" <name>Debugging</name>",
" <name>debugging</name>",
" <description>Diagnose hard bugs</description>",
" </skill>",
].join("\n"),
@@ -124,7 +117,7 @@ describe("SkillGuidance", () => {
)
expect(removed).toMatchObject({
_tag: "Updated",
text: "The following skill IDs are no longer available and must not be used: effect.",
text: "The following skills are no longer available and must not be used: effect.",
})
}).pipe(Effect.provide(layer(() => skills)))
})
@@ -199,7 +192,7 @@ describe("SkillGuidance", () => {
const guidance = yield* SkillGuidance.Service
expect(
(yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).text,
).toContain("<name>Effect</name>")
).toContain("<name>effect</name>")
}).pipe(Effect.provide(layer(() => [effect])))
})
+1 -1
View File
@@ -56,7 +56,7 @@ describe("Snapshot", () => {
const plan = new Map([[RelativePath.make("scope/tracked.txt"), before]])
const preview = yield* snapshot.preview({ files: plan, context: 1 })
expect(preview).toHaveLength(1)
expect(preview[0]?.file).toBe(RelativePath.make("scope/tracked.txt"))
expect(preview[0]?.path).toBe(RelativePath.make("scope/tracked.txt"))
yield* snapshot.restore({ files: plan })
expect(yield* read(path.join(location, "tracked.txt"))).toBe("one\n")
expect(yield* read(path.join(location, "added.txt"))).toBe("added\n")
@@ -13,16 +13,16 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { PatchTool } from "@opencode-ai/core/tool/patch"
import { ApplyPatchTool } from "@opencode-ai/core/tool/apply-patch"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { makeLocationNode } from "@opencode-ai/core/effect/app-node"
import { testEffect } from "./lib/effect"
import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool"
const patchToolNode = makeLocationNode({
name: "test/patch-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(PatchTool.Plugin)),
const applyPatchToolNode = makeLocationNode({
name: "test/apply-patch-tool-plugin",
layer: Layer.effectDiscard(registerToolPlugin(ApplyPatchTool.Plugin)),
deps: [ToolRegistry.toolsNode, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node],
})
@@ -116,7 +116,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
ToolRegistry.toolsNode,
LocationMutation.node,
FileMutation.node,
patchToolNode,
applyPatchToolNode,
]),
[
[FSUtil.node, filesystem],
@@ -129,7 +129,7 @@ const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Inte
)
}
const call = (patchText: string, id = "call-patch") => ({
const call = (patchText: string, id = "call-apply-patch") => ({
sessionID,
...toolIdentity,
call: { type: "tool-call" as const, id, name: "patch", input: { patchText } },
@@ -147,7 +147,7 @@ const exists = (target: string) =>
)
const it = testEffect(Layer.empty)
describe("PatchTool", () => {
describe("ApplyPatchTool", () => {
it.live("registers and sequentially applies add, update, and delete hunks", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
+5 -3
View File
@@ -3,7 +3,6 @@ import { realpathSync } from "node:fs"
import path from "path"
import { describe, expect, test } from "bun:test"
import { DateTime, Duration, Effect, Fiber, Layer, Scope } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
@@ -105,7 +104,7 @@ const executionNode = makeGlobalNode({
sessionID: id,
assistantMessageID,
finish: "stop",
cost: Money.USD.zero,
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
})
})
@@ -443,7 +442,10 @@ describe("ShellTool", () => {
reset()
return withSession(tmp.path, (registry) =>
Effect.gen(function* () {
const settled = yield* settleTool(registry, call({ command: idleCommand, timeout: 50, background: true }))
const settled = yield* settleTool(
registry,
call({ command: idleCommand, timeout: 50, background: true }),
)
const structured = settled.output?.structured as Record<string, unknown> | undefined
const shellID = typeof structured?.shellID === "string" ? structured.shellID : undefined
expect(settled.output?.structured).toMatchObject({ truncated: false })
+9 -11
View File
@@ -26,7 +26,7 @@ const skillToolNode = makeLocationNode({
const sessionID = SessionV2.ID.make("ses_skill_tool_test")
describe("SkillTool", () => {
it.live("lists available skills, authorizes the selected ID, and loads model-facing content", () =>
it.live("lists available skills, authorizes the selected name, and loads model-facing content", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
@@ -42,8 +42,7 @@ describe("SkillTool", () => {
)
const info: SkillV2.Info = {
id: SkillV2.ID.make("effect"),
name: SkillV2.Name.make("Effect"),
name: "effect",
description: "Use Effect",
location: AbsolutePath.make(location),
content: "# Effect\n\nGuidance",
@@ -103,7 +102,7 @@ describe("SkillTool", () => {
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-skill", name: "skill", input: { id: "effect" } },
call: { type: "tool-call", id: "call-skill", name: "skill", input: { name: "effect" } },
}),
).toEqual({
type: "text",
@@ -114,11 +113,11 @@ describe("SkillTool", () => {
yield* settleTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { id: "effect" } },
call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { name: "effect" } },
}),
).toMatchObject({
result: { type: "text", value: SkillTool.toModelOutput(info, [reference]) },
output: { structured: { name: "Effect" } },
output: { structured: { name: "effect" } },
})
expect(assertions).toMatchObject([
{ sessionID, action: "skill", resources: ["effect"], save: ["effect"] },
@@ -128,7 +127,7 @@ describe("SkillTool", () => {
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-missing-skill", name: "skill", input: { id: "missing" } },
call: { type: "tool-call", id: "call-missing-skill", name: "skill", input: { name: "missing" } },
}),
).toEqual({ type: "error", value: "Unable to load skill missing" })
deny = true
@@ -136,13 +135,12 @@ describe("SkillTool", () => {
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-denied-skill", name: "skill", input: { id: "effect" } },
call: { type: "tool-call", id: "call-denied-skill", name: "skill", input: { name: "effect" } },
}),
).toEqual({ type: "error", value: "Unable to load skill effect" })
deny = false
const flat = SkillV2.Info.make({
id: SkillV2.ID.make("public"),
name: SkillV2.Name.make("Public"),
name: "public",
description: "Public guidance",
location: AbsolutePath.make(path.join(tmp.path, "public.md")),
content: "Public",
@@ -158,7 +156,7 @@ describe("SkillTool", () => {
yield* executeTool(registry, {
sessionID,
...toolIdentity,
call: { type: "tool-call", id: "call-flat-skill", name: "skill", input: { id: "public" } },
call: { type: "tool-call", id: "call-flat-skill", name: "skill", input: { name: "public" } },
}),
).toEqual({ type: "text", value: SkillTool.toModelOutput(flat, []) })
}).pipe(Effect.provide(skillToolLayer))
+1 -7
View File
@@ -1,6 +1,5 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Layer, Schema } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
@@ -71,7 +70,7 @@ const executionNode = makeGlobalNode({
sessionID,
assistantMessageID,
finish: "stop",
cost: Money.USD.zero,
cost: 0,
tokens,
})
})
@@ -108,11 +107,6 @@ const withSubagent = (location: Location.Ref) =>
const locations = yield* LocationServiceMap.Service
yield* AgentV2.Service.use((agents) =>
agents.transform((draft) => {
// The caller identity used by executeTool; subagent permission asserts against it.
draft.update(toolIdentity.agent, (agent) => {
agent.mode = "primary"
agent.permissions.push({ action: "*", resource: "*", effect: "allow" })
})
draft.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.mode = "subagent"
agent.model = childModel
+697 -683
View File
@@ -223,7 +223,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Agent.Info"
"$ref": "#/components/schemas/AgentV2.Info"
}
}
},
@@ -595,7 +595,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
@@ -778,7 +778,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
@@ -928,7 +928,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
@@ -2317,7 +2317,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Revert"
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
@@ -2624,7 +2624,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Message.Info"
"$ref": "#/components/schemas/Session.Message"
}
}
},
@@ -3331,7 +3331,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Message.Info"
"$ref": "#/components/schemas/Session.Message"
}
},
"required": [
@@ -3594,7 +3594,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Model.Info"
"$ref": "#/components/schemas/ModelV2.Info"
}
}
},
@@ -3705,7 +3705,7 @@
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Model.Info"
"$ref": "#/components/schemas/ModelV2.Info"
},
{
"type": "null"
@@ -7312,7 +7312,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Command.Info"
"$ref": "#/components/schemas/CommandV2.Info"
}
}
},
@@ -7413,7 +7413,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Skill.Info"
"$ref": "#/components/schemas/SkillV2.Info"
}
}
},
@@ -8508,7 +8508,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
}
},
@@ -8605,7 +8605,7 @@
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
},
"required": [
@@ -8750,7 +8750,7 @@
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
},
"required": [
@@ -8970,7 +8970,7 @@
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
"$ref": "#/components/schemas/Shell"
}
},
"required": [
@@ -10200,7 +10200,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.Info"
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
@@ -10562,15 +10562,12 @@
"$ref": "#/components/schemas/PermissionV2.Rule"
}
},
"Agent.Info": {
"AgentV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
@@ -10611,7 +10608,6 @@
},
"required": [
"id",
"name",
"request",
"mode",
"hidden",
@@ -10631,46 +10627,6 @@
],
"additionalProperties": false
},
"Money.USD": {
"type": "number"
},
"TokenUsage.Info": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"Location.Ref": {
"type": "object",
"properties": {
@@ -10691,14 +10647,19 @@
],
"additionalProperties": false
},
"FileDiff.Info": {
"File.Diff": {
"type": "object",
"properties": {
"file": {
"path": {
"type": "string"
},
"patch": {
"type": "string"
"status": {
"type": "string",
"enum": [
"added",
"modified",
"deleted"
]
},
"additions": {
"type": "integer",
@@ -10716,25 +10677,20 @@
}
]
},
"status": {
"type": "string",
"enum": [
"added",
"deleted",
"modified"
]
"patch": {
"type": "string"
}
},
"required": [
"file",
"patch",
"path",
"status",
"additions",
"deletions",
"status"
"patch"
],
"additionalProperties": false
},
"Session.Revert": {
"Revert.State": {
"type": "object",
"properties": {
"messageID": {
@@ -10751,10 +10707,13 @@
"snapshot": {
"type": "string"
},
"diff": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.Info"
"$ref": "#/components/schemas/File.Diff"
}
}
},
@@ -10763,7 +10722,7 @@
],
"additionalProperties": false
},
"Session.Info": {
"SessionV2.Info": {
"type": "object",
"properties": {
"id": {
@@ -10817,10 +10776,44 @@
"$ref": "#/components/schemas/Model.Ref"
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"time": {
"type": "object",
@@ -10851,7 +10844,7 @@
"type": "string"
},
"revert": {
"$ref": "#/components/schemas/Session.Revert"
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
@@ -10871,7 +10864,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Info"
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"cursor": {
@@ -11674,6 +11667,14 @@
],
"additionalProperties": false
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"text": {
"type": "string"
},
@@ -11690,6 +11691,7 @@
"required": [
"id",
"time",
"sessionID",
"text",
"type"
],
@@ -11771,9 +11773,6 @@
"skill"
]
},
"skill": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -11785,48 +11784,15 @@
"id",
"time",
"type",
"skill",
"name",
"text"
],
"additionalProperties": false
},
"Session.Message.Shell": {
"Shell": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"shell"
]
},
"shellID": {
"type": "string",
"allOf": [
{
@@ -11834,9 +11800,6 @@
}
]
},
"command": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
@@ -11846,6 +11809,26 @@
"killed"
]
},
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"shell": {
"type": "string"
},
"file": {
"type": "string"
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exit": {
"anyOf": [
{
@@ -11883,6 +11866,143 @@
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"started": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"completed": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"started"
],
"additionalProperties": false
}
},
"required": [
"id",
"status",
"command",
"cwd",
"shell",
"file",
"metadata",
"time"
],
"additionalProperties": false
},
"Session.Message.Shell": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"shell"
]
},
"shell": {
"$ref": "#/components/schemas/Shell"
},
"output": {
"type": "object",
"properties": {
@@ -11922,9 +12042,7 @@
"id",
"time",
"type",
"shellID",
"command",
"status"
"shell"
],
"additionalProperties": false
},
@@ -11987,13 +12105,13 @@
],
"additionalProperties": false
},
"Session.Message.ToolState.Streaming": {
"Session.Message.ToolState.Pending": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"streaming"
"pending"
]
},
"input": {
@@ -12103,12 +12221,24 @@
"input": {
"type": "object"
},
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.FileAttachment"
}
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"outputPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"structured": {
"type": "object"
},
@@ -12200,7 +12330,7 @@
"state": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.ToolState.Streaming"
"$ref": "#/components/schemas/Session.Message.ToolState.Pending"
},
{
"$ref": "#/components/schemas/Session.Message.ToolState.Running"
@@ -12224,6 +12354,9 @@
},
"completed": {
"type": "number"
},
"pruned": {
"type": "number"
}
},
"required": [
@@ -12353,10 +12486,44 @@
]
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"error": {
"$ref": "#/components/schemas/Session.StructuredError"
@@ -12375,7 +12542,7 @@
],
"additionalProperties": false
},
"Session.Message.Compaction.Running": {
"Session.Message.Compaction": {
"type": "object",
"properties": {
"type": {
@@ -12384,158 +12551,12 @@
"compaction"
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": [
"running"
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"summary": {
"type": "string"
},
"recent": {
"type": "string"
}
},
"required": [
"type",
"id",
"time",
"status",
"reason",
"summary",
"recent"
],
"additionalProperties": false
},
"Session.Message.Compaction.Completed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"compaction"
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": [
"completed"
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"summary": {
"type": "string"
},
"recent": {
"type": "string"
}
},
"required": [
"type",
"id",
"time",
"status",
"reason",
"summary",
"recent"
],
"additionalProperties": false
},
"Session.Message.Compaction.Failed": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"compaction"
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"status": {
"type": "string",
"enum": [
"queued",
"running",
"completed",
"failed"
]
},
@@ -12546,34 +12567,48 @@
"manual"
]
},
"error": {
"$ref": "#/components/schemas/Session.StructuredError"
"summary": {
"type": "string"
},
"recent": {
"type": "string"
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
}
},
"required": [
"type",
"id",
"time",
"status",
"reason",
"error"
"summary",
"recent",
"id",
"time"
],
"additionalProperties": false
},
"Session.Message.Compaction": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.Compaction.Running"
},
{
"$ref": "#/components/schemas/Session.Message.Compaction.Completed"
},
{
"$ref": "#/components/schemas/Session.Message.Compaction.Failed"
}
]
},
"Session.Message.Info": {
"Session.Message": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.AgentSelected"
@@ -12665,9 +12700,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -12750,9 +12787,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -12835,9 +12874,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -12923,9 +12964,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13008,9 +13051,11 @@
]
},
"version": {
"type": "number",
"enum": [
2
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13089,9 +13134,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13187,9 +13234,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13277,9 +13326,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13379,9 +13430,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13460,9 +13513,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13541,9 +13596,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13626,9 +13683,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13716,9 +13775,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13801,9 +13862,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13892,9 +13955,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -13919,9 +13984,6 @@
}
]
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -13931,7 +13993,6 @@
},
"required": [
"sessionID",
"id",
"name",
"text"
],
@@ -13947,7 +14008,7 @@
],
"additionalProperties": false
},
"Shell": {
"Shell1": {
"type": "object",
"properties": {
"id": {
@@ -14125,9 +14186,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14153,7 +14216,7 @@
]
},
"shell": {
"$ref": "#/components/schemas/Shell"
"$ref": "#/components/schemas/Shell1"
}
},
"required": [
@@ -14210,9 +14273,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14238,7 +14303,7 @@
]
},
"shell": {
"$ref": "#/components/schemas/Shell"
"$ref": "#/components/schemas/Shell1"
},
"output": {
"type": "object",
@@ -14330,9 +14395,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14431,9 +14498,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14478,10 +14547,44 @@
]
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"snapshot": {
"type": "string"
@@ -14550,9 +14653,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14589,10 +14694,44 @@
"$ref": "#/components/schemas/Session.StructuredError"
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
}
},
"required": [
@@ -14650,9 +14789,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14749,9 +14890,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14855,9 +14998,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -14960,9 +15105,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15066,9 +15213,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15164,9 +15313,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15265,9 +15416,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15370,9 +15523,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15478,9 +15633,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15525,6 +15682,12 @@
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"outputPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"result": {},
"executed": {
"type": "boolean"
@@ -15594,9 +15757,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15700,9 +15865,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15812,9 +15979,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15902,9 +16071,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -15935,23 +16106,11 @@
"auto",
"manual"
]
},
"recent": {
"type": "string"
},
"inputID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
}
},
"required": [
"sessionID",
"reason",
"recent"
"reason"
],
"additionalProperties": false
}
@@ -16003,9 +16162,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16100,9 +16261,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16126,30 +16289,10 @@
"pattern": "^ses"
}
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"error": {
"$ref": "#/components/schemas/Session.StructuredError"
},
"inputID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
}
},
"required": [
"sessionID",
"reason",
"error"
"sessionID"
],
"additionalProperties": false
}
@@ -16201,9 +16344,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16229,7 +16374,7 @@
]
},
"revert": {
"$ref": "#/components/schemas/Session.Revert"
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
@@ -16286,9 +16431,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16367,9 +16514,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -16419,7 +16568,7 @@
],
"additionalProperties": false
},
"Session.Event.Durable": {
"SessionDurableEvent": {
"oneOf": [
{
"$ref": "#/components/schemas/session.agent.selected"
@@ -16568,7 +16717,7 @@
"SessionLogItem": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Event.Durable"
"$ref": "#/components/schemas/SessionDurableEvent"
},
{
"$ref": "#/components/schemas/EventLog.Synced"
@@ -16588,7 +16737,7 @@
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Message.Info"
"$ref": "#/components/schemas/Session.Message"
}
},
"cursor": {
@@ -16674,9 +16823,6 @@
],
"additionalProperties": false
},
"Money.USDPerMillionTokens": {
"type": "number"
},
"Model.Cost": {
"type": "object",
"properties": {
@@ -16700,19 +16846,19 @@
"additionalProperties": false
},
"input": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
},
"output": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
},
"write": {
"$ref": "#/components/schemas/Money.USDPerMillionTokens"
"type": "number"
}
},
"required": [
@@ -16729,7 +16875,7 @@
],
"additionalProperties": false
},
"Model.Info": {
"ModelV2.Info": {
"type": "object",
"properties": {
"id": {
@@ -19139,7 +19285,7 @@
],
"additionalProperties": false
},
"Command.Info": {
"CommandV2.Info": {
"type": "object",
"properties": {
"name": {
@@ -19167,12 +19313,9 @@
],
"additionalProperties": false
},
"Skill.Info": {
"SkillV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -19193,7 +19336,6 @@
}
},
"required": [
"id",
"name",
"location",
"content"
@@ -19427,7 +19569,7 @@
],
"additionalProperties": false
},
"FileDiff.LegacyInfo": {
"SnapshotFileDiff": {
"type": "object",
"properties": {
"file": {
@@ -19491,7 +19633,7 @@
"$ref": "#/components/schemas/PermissionRule"
}
},
"SessionV1.Info": {
"Session": {
"type": "object",
"properties": {
"id": {
@@ -19545,7 +19687,7 @@
"diffs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.LegacyInfo"
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
@@ -19760,9 +19902,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -19788,7 +19932,7 @@
]
},
"info": {
"$ref": "#/components/schemas/SessionV1.Info"
"$ref": "#/components/schemas/Session"
}
},
"required": [
@@ -19845,9 +19989,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -19873,7 +20019,7 @@
]
},
"info": {
"$ref": "#/components/schemas/SessionV1.Info"
"$ref": "#/components/schemas/Session"
}
},
"required": [
@@ -19930,9 +20076,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -19958,7 +20106,7 @@
]
},
"info": {
"$ref": "#/components/schemas/SessionV1.Info"
"$ref": "#/components/schemas/Session"
}
},
"required": [
@@ -20120,7 +20268,7 @@
"diffs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileDiff.LegacyInfo"
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
@@ -20788,9 +20936,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -20873,9 +21023,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -22341,9 +22493,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -22430,9 +22584,11 @@
]
},
"version": {
"type": "number",
"enum": [
1
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
@@ -22529,10 +22685,44 @@
]
},
"cost": {
"$ref": "#/components/schemas/Money.USD"
"type": "number"
},
"tokens": {
"$ref": "#/components/schemas/TokenUsage.Info"
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
}
},
"required": [
@@ -23646,7 +23836,7 @@
"type": "object",
"properties": {
"info": {
"$ref": "#/components/schemas/Shell"
"$ref": "#/components/schemas/Shell1"
}
},
"required": [
@@ -26604,182 +26794,6 @@
],
"additionalProperties": false
},
"Shell1": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"status": {
"type": "string",
"enum": [
"running",
"exited",
"timeout",
"killed"
]
},
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"shell": {
"type": "string"
},
"file": {
"type": "string"
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exit": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"started": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"completed": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"started"
],
"additionalProperties": false
}
},
"required": [
"id",
"status",
"command",
"cwd",
"shell",
"file",
"metadata",
"time"
],
"additionalProperties": false
},
"ShellNotFoundError": {
"type": "object",
"properties": {
+2 -2
View File
@@ -1,4 +1,4 @@
import { FileDiffInfo, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import { iife } from "@opencode-ai/core/util/iife"
import z from "zod"
import { Storage } from "./storage"
@@ -30,7 +30,7 @@ export namespace Share {
}),
z.object({
type: z.literal("session_diff"),
data: z.custom<FileDiffInfo[]>(),
data: z.custom<SnapshotFileDiff[]>(),
}),
z.object({
type: z.literal("model"),
@@ -1,4 +1,4 @@
import { FileDiffInfo, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2"
import { SessionTurn } from "@opencode-ai/session-ui/session-turn"
import { SessionReview } from "@opencode-ai/session-ui/session-review"
import { DataProvider } from "@opencode-ai/session-ui/context"
@@ -65,7 +65,7 @@ const getData = query(async (shareID) => {
shareID: string
session: Session[]
session_diff: {
[sessionID: string]: FileDiffInfo[]
[sessionID: string]: SnapshotFileDiff[]
}
session_status: {
[sessionID: string]: SessionStatus
@@ -876,7 +876,6 @@ export const protocol = Protocol.make({
export const route = Route.make({
id: ADAPTER,
provider: "anthropic",
providerMetadataKey: "anthropic",
protocol,
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
auth: Auth.none,
@@ -657,7 +657,6 @@ export const protocol = Protocol.make({
export const route = Route.make({
id: ADAPTER,
provider: "bedrock",
providerMetadataKey: "bedrock",
protocol,
// Bedrock's URL embeds the region in the route endpoint host and the
// validated modelId in the path. We read the validated body so the URL
-1
View File
@@ -500,7 +500,6 @@ export const protocol = Protocol.make({
export const route = Route.make({
id: ADAPTER,
provider: "google",
providerMetadataKey: "google",
protocol,
// Gemini's path embeds the model id and pins SSE framing at the URL level.
endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, {
@@ -493,7 +493,6 @@ export const httpTransport = HttpTransport.sseJson.with<OpenAIChatBody>()
export const route = Route.make({
id: ADAPTER,
provider: "openai",
providerMetadataKey: "openai",
protocol,
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
auth: Auth.none,
@@ -16,7 +16,6 @@ export type OpenAICompatibleChatModelInput = RouteRoutedModelInput
*/
export const route = Route.make({
id: ADAPTER,
providerMetadataKey: "openai",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: Framing.sse,

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