From ed6ad272ec16b4f78016a2c8bc9b8c79d70587d5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 7 Jul 2026 22:10:11 -0400 Subject: [PATCH] refactor(schema): apply session review decisions (#35793) --- .../src/context/global-sync/event-reducer.ts | 4 +- .../context/global-sync/session-cache.test.ts | 6 +- .../src/context/global-sync/session-cache.ts | 4 +- packages/app/src/context/global-sync/types.ts | 4 +- packages/app/src/context/server-session.ts | 6 +- packages/app/src/pages/session/review-tab.tsx | 4 +- .../src/pages/session/session-side-panel.tsx | 14 +- .../src/pages/session/v2/review-diff-kinds.ts | 8 +- .../src/pages/session/v2/review-panel-v2.tsx | 13 +- packages/app/src/utils/diffs.test.ts | 4 +- packages/app/src/utils/diffs.ts | 4 +- packages/cli/src/mini/catalog.shared.ts | 5 +- packages/cli/src/mini/footer.prompt.tsx | 4 +- packages/cli/src/mini/noninteractive.ts | 1 - packages/cli/src/mini/run.ts | 13 +- packages/cli/src/mini/stream-v2.subagent.ts | 14 +- packages/cli/src/mini/stream-v2.transport.ts | 31 +- packages/cli/src/mini/types.ts | 1 + .../client/src/promise/generated/types.ts | 502 +++--- .../client/test/contract-identity.test.ts | 2 +- .../test/fixtures/opencode-v2-openapi.json | 1400 ++++++++--------- packages/core/src/agent.ts | 2 + packages/core/src/config/plugin/provider.ts | 5 +- packages/core/src/config/provider.ts | 9 +- packages/core/src/database/migration.gen.ts | 1 + ...260707120000_migrate_prelaunch_v2_state.ts | 227 +++ packages/core/src/file.ts | 4 +- packages/core/src/git.ts | 2 +- packages/core/src/models-dev.ts | 25 +- packages/core/src/plugin/agent.ts | 7 + packages/core/src/plugin/models-dev.ts | 54 +- packages/core/src/plugin/provider/opencode.ts | 18 +- packages/core/src/plugin/skill.ts | 29 +- packages/core/src/session.ts | 37 +- packages/core/src/session/compaction.ts | 39 +- packages/core/src/session/history.ts | 2 +- packages/core/src/session/info.ts | 11 +- packages/core/src/session/input.ts | 6 +- packages/core/src/session/message-updater.ts | 71 +- packages/core/src/session/projector.ts | 80 +- packages/core/src/session/revert.ts | 12 +- packages/core/src/session/runner/llm.ts | 35 +- .../src/session/runner/publish-llm-event.ts | 16 +- .../core/src/session/runner/to-llm-message.ts | 8 +- packages/core/src/session/sql.ts | 13 +- packages/core/src/session/store.ts | 6 +- packages/core/src/skill.ts | 36 +- packages/core/src/skill/guidance.ts | 14 +- packages/core/src/snapshot.ts | 13 +- packages/core/src/tool/skill.ts | 16 +- packages/core/test/catalog.test.ts | 23 +- packages/core/test/config/command.test.ts | 3 +- packages/core/test/config/provider.test.ts | 13 +- packages/core/test/database-migration.test.ts | 254 ++- packages/core/test/git.test.ts | 4 +- packages/core/test/location-layer.test.ts | 5 +- packages/core/test/plugin/models-dev.test.ts | 50 +- .../core/test/plugin/provider-openai.test.ts | 12 +- .../test/plugin/provider-opencode.test.ts | 12 +- packages/core/test/plugin/skill.test.ts | 8 +- packages/core/test/session-compact.test.ts | 10 +- packages/core/test/session-compaction.test.ts | 8 +- packages/core/test/session-create.test.ts | 17 +- .../core/test/session-instructions.test.ts | 1 - packages/core/test/session-log.test.ts | 7 +- packages/core/test/session-projector.test.ts | 163 +- packages/core/test/session-prompt.test.ts | 6 +- .../core/test/session-runner-message.test.ts | 35 +- .../core/test/session-runner-model.test.ts | 9 +- .../test/session-runner-tool-events.test.ts | 6 +- packages/core/test/session-runner.test.ts | 115 +- packages/core/test/session-skill.test.ts | 7 +- .../core/test/session-tool-progress.test.ts | 3 +- packages/core/test/shared-schema.test.ts | 5 +- packages/core/test/skill.test.ts | 13 +- packages/core/test/skill/guidance.test.ts | 27 +- packages/core/test/snapshot.test.ts | 2 +- packages/core/test/tool-shell.test.ts | 8 +- packages/core/test/tool-skill.test.ts | 20 +- packages/core/test/tool-subagent.test.ts | 3 +- packages/docs/openapi.json | 1400 ++++++++--------- packages/enterprise/src/core/share.ts | 4 +- .../enterprise/src/routes/share/[shareID].tsx | 4 +- packages/opencode/src/session/session.ts | 2 +- packages/opencode/src/share/share-next.ts | 2 +- packages/opencode/src/snapshot/index.ts | 4 +- .../test/cli/run/stream-v2.transport.test.ts | 43 +- .../test/server/httpapi-session.test.ts | 38 +- .../test/v2/session-message-updater.test.ts | 44 +- packages/plugin/src/v2/effect/agent.ts | 8 +- packages/plugin/src/v2/effect/catalog.ts | 8 +- packages/plugin/src/v2/effect/command.ts | 8 +- packages/plugin/src/v2/effect/skill.ts | 6 +- packages/protocol/src/errors.ts | 3 +- packages/protocol/src/groups/message.ts | 2 +- packages/protocol/src/groups/session.ts | 12 +- packages/schema/src/agent.ts | 9 +- packages/schema/src/command.ts | 5 +- packages/schema/src/event.ts | 22 +- packages/schema/src/file-diff.ts | 22 +- packages/schema/src/index.ts | 4 +- packages/schema/src/model.ts | 19 +- packages/schema/src/money.ts | 18 + packages/schema/src/revert.ts | 24 - packages/schema/src/session-event.ts | 57 +- packages/schema/src/session-input.ts | 11 +- packages/schema/src/session-message.ts | 121 +- packages/schema/src/session-revert.ts | 86 + packages/schema/src/session.ts | 23 +- packages/schema/src/shell.ts | 2 +- packages/schema/src/skill.ts | 29 +- packages/schema/src/snapshot.ts | 6 + packages/schema/src/token-usage.ts | 14 + packages/schema/src/v1/session.ts | 10 +- packages/schema/test/contract-hygiene.test.ts | 90 +- packages/schema/test/event-manifest.test.ts | 7 + packages/sdk/js/src/v2/client.ts | 4 + packages/sdk/js/src/v2/gen/sdk.gen.ts | 163 +- packages/sdk/js/src/v2/gen/types.gen.ts | 1063 +++++++------ packages/server/src/handlers/message.ts | 2 +- .../session-ui/src/components/session-diff.ts | 5 +- .../src/components/session-review.tsx | 10 +- .../src/components/session-turn.tsx | 16 +- packages/session-ui/src/context/data.tsx | 4 +- .../session-review-file-preview-v2.tsx | 4 +- .../tui/src/component/dialog-session-list.tsx | 33 +- packages/tui/src/component/dialog-skill.tsx | 4 +- .../tui/src/component/prompt/autocomplete.tsx | 6 +- packages/tui/src/component/prompt/index.tsx | 12 +- packages/tui/src/context/data.tsx | 163 +- packages/tui/src/context/sync.tsx | 4 +- .../feature-plugins/system/diff-viewer.tsx | 4 +- packages/tui/src/routes/session/index.tsx | 89 +- .../tui/src/routes/session/permission.tsx | 2 +- packages/tui/src/routes/session/rows.ts | 16 +- packages/tui/src/util/session.ts | 9 +- packages/tui/src/util/tool-display.ts | 2 +- .../test/cli/cmd/tui/notifications.test.ts | 2 +- packages/tui/test/cli/tui/data.test.tsx | 68 +- .../tui/test/cli/tui/session-rows.test.ts | 28 +- packages/tui/test/util/session.test.ts | 4 +- packages/tui/test/util/tool-display.test.ts | 2 +- 142 files changed, 4321 insertions(+), 3256 deletions(-) create mode 100644 packages/core/src/database/migration/20260707120000_migrate_prelaunch_v2_state.ts create mode 100644 packages/schema/src/money.ts delete mode 100644 packages/schema/src/revert.ts create mode 100644 packages/schema/src/session-revert.ts create mode 100644 packages/schema/src/snapshot.ts create mode 100644 packages/schema/src/token-usage.ts diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 15d125df391..40b7acbbd37 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -8,7 +8,7 @@ import type { QuestionRequest, Session, SessionStatus, - SnapshotFileDiff, + FileDiffInfo, 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: SnapshotFileDiff[] } + const props = event.properties as { sessionID: string; diff: FileDiffInfo[] } input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" })) break } diff --git a/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app/src/context/global-sync/session-cache.test.ts index 4b2be505eaa..3dda5a54293 100644 --- a/packages/app/src/context/global-sync/session-cache.test.ts +++ b/packages/app/src/context/global-sync/session-cache.test.ts @@ -5,7 +5,7 @@ import type { PermissionRequest, QuestionRequest, SessionStatus, - SnapshotFileDiff, + FileDiffInfo, 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 - session_diff: Record + session_diff: Record todo: Record message: Record part: Record @@ -67,7 +67,7 @@ describe("app session cache", () => { const m = msg("msg_1", "ses_1") const store: { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record part: Record diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts index 05cdc846438..39535abe2f9 100644 --- a/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app/src/context/global-sync/session-cache.ts @@ -4,7 +4,7 @@ import type { PermissionRequest, QuestionRequest, SessionStatus, - SnapshotFileDiff, + FileDiffInfo, Todo, } from "@opencode-ai/sdk/v2/client" @@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40 type SessionCache = { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record part: Record diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 86b489cd090..a117e4d0079 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -13,7 +13,7 @@ import type { ReferenceInfo, Session, SessionStatus, - SnapshotFileDiff, + FileDiffInfo, Todo, VcsInfo, } from "@opencode-ai/sdk/v2/client" @@ -51,7 +51,7 @@ export type State = { } session_working(id: string): boolean session_diff: { - [sessionID: string]: SnapshotFileDiff[] + [sessionID: string]: FileDiffInfo[] } todo: { [sessionID: string]: Todo[] diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index 6898cc23049..a46eb744d17 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -8,7 +8,7 @@ import type { QuestionRequest, Session, SessionStatus, - SnapshotFileDiff, + FileDiffInfo, 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, session_status: {} as Record, - session_diff: {} as Record, + session_diff: {} as Record, todo: {} as Record, permission: {} as Record, question: {} as Record, @@ -769,7 +769,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?: return } case "session.diff": { - const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } + const props = event.properties as { sessionID: string; diff: FileDiffInfo[] } setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" })) return } diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 3854bf0276e..586942399d8 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,6 +1,6 @@ import { createEffect, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, 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 = SnapshotFileDiff | VcsFileDiff +type ReviewDiff = FileDiffInfo | VcsFileDiff export interface SessionReviewTabProps { title?: JSX.Element diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 3f44aba4883..01384a6ee97 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -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 { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -23,7 +23,6 @@ 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 { @@ -36,15 +35,9 @@ 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: () => (SnapshotFileDiff | VcsFileDiff)[] + diffs: () => (FileDiffInfo | VcsFileDiff)[] diffsReady: () => boolean empty: () => string hasReview: () => boolean @@ -59,7 +52,6 @@ export function SessionSidePanel(props: { }) { const layout = useLayout() const settings = useSettings() - const sync = useSync() const file = useFile() const language = useLanguage() const command = useCommand() @@ -88,7 +80,7 @@ export function SessionSidePanel(props: { }) const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) - const diffs = createMemo(() => props.diffs().filter(renderDiff)) + const diffs = createMemo(() => props.diffs()) const diffFiles = createMemo(() => diffs().map((d) => d.file)) const kinds = createMemo(() => { const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.ts b/packages/app/src/pages/session/v2/review-diff-kinds.ts index c288c357068..760ace33111 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.ts @@ -1,17 +1,13 @@ -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, 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 = (SnapshotFileDiff & { file: string }) | VcsFileDiff +export type RenderDiff = FileDiffInfo | 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 diff --git a/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app/src/pages/session/v2/review-panel-v2.tsx index fcd52756ab0..41f75a081e3 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -1,5 +1,5 @@ import { createMemo, createSignal, Show, type JSX } from "solid-js" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import { SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX, SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN, @@ -21,16 +21,11 @@ import type { import FileTreeV2 from "@/components/file-tree-v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" -import { - filterRenderableDiff, - filterReviewFiles, - reviewDiffKinds, - type RenderDiff, -} from "@/pages/session/v2/review-diff-kinds" +import { 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 = SnapshotFileDiff | VcsFileDiff +type ReviewDiff = FileDiffInfo | VcsFileDiff export type ReviewPanelV2Props = { title?: JSX.Element @@ -54,7 +49,7 @@ export type ReviewPanelV2Props = { export function ReviewPanelV2(props: ReviewPanelV2Props) { const sdk = useSDK() - const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff)) + const diffs = createMemo(() => props.diffs()) const filteredFiles = createMemo(() => filterReviewFiles( diffs().map((diff) => diff.file), diff --git a/packages/app/src/utils/diffs.test.ts b/packages/app/src/utils/diffs.test.ts index 5fbca469b71..f6d768e1de2 100644 --- a/packages/app/src/utils/diffs.test.ts +++ b/packages/app/src/utils/diffs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } 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 SnapshotFileDiff +} satisfies FileDiffInfo describe("diffs", () => { test("keeps valid arrays", () => { diff --git a/packages/app/src/utils/diffs.ts b/packages/app/src/utils/diffs.ts index 0cb2504fbe9..60df039410b 100644 --- a/packages/app/src/utils/diffs.ts +++ b/packages/app/src/utils/diffs.ts @@ -1,7 +1,7 @@ -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/sdk/v2" import type { Message } from "@opencode-ai/sdk/v2/client" -type Diff = SnapshotFileDiff | VcsFileDiff +type Diff = FileDiffInfo function diff(value: unknown): value is Diff { if (!value || typeof value !== "object" || Array.isArray(value)) return false diff --git a/packages/cli/src/mini/catalog.shared.ts b/packages/cli/src/mini/catalog.shared.ts index 535147e6519..0868d3a94bb 100644 --- a/packages/cli/src/mini/catalog.shared.ts +++ b/packages/cli/src/mini/catalog.shared.ts @@ -37,7 +37,8 @@ function defaultCost(model: CurrentModel) { export function runAgent(input: CurrentAgent): RunAgent { return { - name: input.id, + id: input.id, + name: input.name, description: input.description, mode: input.mode, hidden: input.hidden, @@ -53,7 +54,7 @@ export function runCommand(input: CurrentCommand): RunCommand { export function runSkill(input: CurrentSkill): RunCommand { return { - name: input.name, + name: input.id, description: input.description, source: "skill", } diff --git a/packages/cli/src/mini/footer.prompt.tsx b/packages/cli/src/mini/footer.prompt.tsx index 6ba7940ab6c..4c6214d9a85 100644 --- a/packages/cli/src/mini/footer.prompt.tsx +++ b/packages/cli/src/mini/footer.prompt.tsx @@ -333,10 +333,10 @@ export function createPromptState(input: PromptInput): PromptState { .map((item) => ({ kind: "mention", display: "@" + item.name, - value: item.name, + value: item.id, part: { type: "agent", - name: item.name, + name: item.id, source: { start: 0, end: 0, diff --git a/packages/cli/src/mini/noninteractive.ts b/packages/cli/src/mini/noninteractive.ts index 8549b36d372..bc356eb743f 100644 --- a/packages/cli/src/mini/noninteractive.ts +++ b/packages/cli/src/mini/noninteractive.ts @@ -281,7 +281,6 @@ 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 }, diff --git a/packages/cli/src/mini/run.ts b/packages/cli/src/mini/run.ts index d8e3dcd228e..a6aedbb97d7 100644 --- a/packages/cli/src/mini/run.ts +++ b/packages/cli/src/mini/run.ts @@ -89,12 +89,11 @@ 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 } }) @@ -112,9 +111,7 @@ 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 ? "..." : ""), }) } @@ -200,7 +197,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.name === name) + const agent = agents.find((item) => item.id === name) if (!agent) { warning(`agent "${name}" not found. Falling back to default agent`) return diff --git a/packages/cli/src/mini/stream-v2.subagent.ts b/packages/cli/src/mini/stream-v2.subagent.ts index 4aae8e12784..091ff296eed 100644 --- a/packages/cli/src/mini/stream-v2.subagent.ts +++ b/packages/cli/src/mini/stream-v2.subagent.ts @@ -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 { SessionMessage, SessionMessageAssistantTool, ToolPart } from "@opencode-ai/sdk/v2" +import type { SessionMessageAssistantTool, SessionMessageInfo, 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 === "pending") { + if (tool.state.status === "streaming") { return { ...base, state: { status: "pending", input: {}, raw: tool.state.input }, @@ -83,7 +83,6 @@ export function legacyTool(input: { metadata: { structured: tool.state.structured, content: tool.state.content, - outputPaths: tool.state.outputPaths, result: tool.state.result, providerCall, providerResult, @@ -179,7 +178,7 @@ export type SubagentTrackerInput = { export type SubagentTracker = { main(event: V2Event): void foreign(sessionID: string, event: V2Event): void - hydrate(next: { messages: SessionMessage[]; active: Record }): Promise + hydrate(next: { messages: SessionMessageInfo[]; active: Record }): Promise select(sessionID: string | undefined): void snapshot(): FooterSubagentState } @@ -316,7 +315,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac messageID, tool: item, }) - if (item.state.status === "pending") return + if (item.state.status === "streaming") return child.callIDs.add(item.id) if (item.state.status === "running") { setFrame(child, `tool:${item.id}`, toolCommit(part, "start")) @@ -327,7 +326,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac setFrame(child, `tool:${item.id}`, toolCommit(part, "final")) } - const rebuild = (child: ChildState, messages: SessionMessage[]) => { + const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => { child.frames = [] child.text.clear() child.projectedText.clear() @@ -412,7 +411,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 SessionMessage[]) + rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[]) for (const [id, tool] of pendingTools) { if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool) } @@ -622,7 +621,6 @@ 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: { diff --git a/packages/cli/src/mini/stream-v2.transport.ts b/packages/cli/src/mini/stream-v2.transport.ts index a82fea2e58c..ae854ef2e3d 100644 --- a/packages/cli/src/mini/stream-v2.transport.ts +++ b/packages/cli/src/mini/stream-v2.transport.ts @@ -2,7 +2,7 @@ import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/p import type { PermissionRequest, QuestionRequest, - SessionMessage, + SessionMessageInfo, SessionMessageAssistantTool, } from "@opencode-ai/sdk/v2" import { Event } from "@opencode-ai/schema/event" @@ -389,7 +389,7 @@ export async function createSessionTransport(input: StreamInput): Promise { + const renderMessage = (message: SessionMessageInfo, 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 } }> @@ -408,13 +408,12 @@ export type SessionCreateOutput = { readonly messageID: string readonly partID?: string readonly snapshot?: string - readonly diff?: string readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" + readonly file: string + readonly patch: string readonly additions: number readonly deletions: number - readonly patch: string + readonly status: "added" | "deleted" | "modified" }> } } @@ -447,13 +446,12 @@ export type SessionGetOutput = { readonly messageID: string readonly partID?: string readonly snapshot?: string - readonly diff?: string readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" + readonly file: string + readonly patch: string readonly additions: number readonly deletions: number - readonly patch: string + readonly status: "added" | "deleted" | "modified" }> } } @@ -491,13 +489,12 @@ export type SessionForkOutput = { readonly messageID: string readonly partID?: string readonly snapshot?: string - readonly diff?: string readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" + readonly file: string + readonly patch: string readonly additions: number readonly deletions: number - readonly patch: string + readonly status: "added" | "deleted" | "modified" }> } } @@ -933,13 +930,12 @@ export type SessionRevertStageOutput = { readonly messageID: string readonly partID?: string readonly snapshot?: string - readonly diff?: string readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" + readonly file: string + readonly patch: string readonly additions: number readonly deletions: number - readonly patch: string + readonly status: "added" | "deleted" | "modified" }> } }["data"] @@ -994,7 +990,6 @@ 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" @@ -1011,6 +1006,7 @@ 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 } @@ -1019,21 +1015,10 @@ export type SessionContextOutput = { readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number; readonly completed?: number } readonly type: "shell" - 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 shellID: string + readonly command: string + readonly status: "running" | "exited" | "timeout" | "killed" + readonly exit?: number | "Infinity" | "-Infinity" | "NaN" readonly output?: { readonly output: string readonly cursor: number @@ -1064,7 +1049,7 @@ export type SessionContextOutput = { readonly providerState?: { readonly [x: string]: JsonValue } readonly providerResultState?: { readonly [x: string]: JsonValue } readonly state: - | { readonly status: "pending"; readonly input: string } + | { readonly status: "streaming"; readonly input: string } | { readonly status: "running" readonly input: { readonly [x: string]: JsonValue } @@ -1077,19 +1062,10 @@ 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 readonly structured: { readonly [x: string]: JsonValue } readonly result?: JsonValue } @@ -1104,12 +1080,7 @@ 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 pruned?: number - } + readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number } } > readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } @@ -1128,16 +1099,37 @@ export type SessionContextOutput = { 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 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 } + } + ) > }["data"] @@ -1175,7 +1167,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly agent: string } } @@ -1184,7 +1176,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1196,7 +1188,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1209,7 +1201,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly title: string } } @@ -1218,7 +1210,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 2 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -1227,7 +1219,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string } } @@ -1236,7 +1228,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly inputID: string } } @@ -1245,7 +1237,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1273,7 +1265,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -1282,7 +1274,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -1291,7 +1283,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1303,7 +1295,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" } } @@ -1312,7 +1304,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly text: string } } @@ -1321,7 +1313,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1335,16 +1327,21 @@ 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly sessionID: string; readonly name: string; readonly text: string } + readonly data: { + readonly sessionID: string + readonly id: 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1367,7 +1364,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1396,7 +1393,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1411,7 +1408,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1433,7 +1430,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1453,7 +1450,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly ordinal: number } } @@ -1462,7 +1459,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1476,7 +1473,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1490,7 +1487,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1505,7 +1502,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1519,7 +1516,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1533,7 +1530,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1549,7 +1546,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1567,7 +1564,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1578,7 +1575,6 @@ export type SessionLogOutput = | { readonly type: "text"; readonly text: string } | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } > - readonly outputPaths?: ReadonlyArray readonly result?: unknown readonly executed: boolean readonly resultState?: { readonly [x: string]: unknown } @@ -1589,7 +1585,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1606,7 +1602,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1621,7 +1617,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly inputID: string } } @@ -1630,16 +1626,21 @@ 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" } + readonly data: { + readonly sessionID: string + readonly reason: "auto" | "manual" + readonly recent: string + readonly inputID?: string + } } | { 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1653,16 +1654,21 @@ 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly sessionID: string } + readonly data: { + readonly sessionID: string + readonly reason: "auto" | "manual" + readonly error: { readonly type: string; readonly message: string } + readonly inputID?: 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -1670,13 +1676,12 @@ export type SessionLogOutput = readonly messageID: string readonly partID?: string readonly snapshot?: string - readonly diff?: string readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" + readonly file: string + readonly patch: string readonly additions: number readonly deletions: number - readonly patch: string + readonly status: "added" | "deleted" | "modified" }> } } @@ -1686,7 +1691,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -1695,7 +1700,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly to: string } } @@ -1755,7 +1760,6 @@ 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" @@ -1772,6 +1776,7 @@ 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 } @@ -1780,21 +1785,10 @@ export type SessionMessageOutput = { readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number; readonly completed?: number } readonly type: "shell" - 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 shellID: string + readonly command: string + readonly status: "running" | "exited" | "timeout" | "killed" + readonly exit?: number | "Infinity" | "-Infinity" | "NaN" readonly output?: { readonly output: string readonly cursor: number @@ -1825,7 +1819,7 @@ export type SessionMessageOutput = { readonly providerState?: { readonly [x: string]: JsonValue } readonly providerResultState?: { readonly [x: string]: JsonValue } readonly state: - | { readonly status: "pending"; readonly input: string } + | { readonly status: "streaming"; readonly input: string } | { readonly status: "running" readonly input: { readonly [x: string]: JsonValue } @@ -1838,19 +1832,10 @@ 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 readonly structured: { readonly [x: string]: JsonValue } readonly result?: JsonValue } @@ -1865,12 +1850,7 @@ 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 pruned?: number - } + readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number } } > readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } @@ -1889,16 +1869,37 @@ export type SessionMessageOutput = { 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 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 } + } + ) }["data"] export type MessageListInput = { @@ -1960,7 +1961,6 @@ 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,6 +1977,7 @@ 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 } @@ -1985,21 +1986,10 @@ export type MessageListOutput = { readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number; readonly completed?: number } readonly type: "shell" - 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 shellID: string + readonly command: string + readonly status: "running" | "exited" | "timeout" | "killed" + readonly exit?: number | "Infinity" | "-Infinity" | "NaN" readonly output?: { readonly output: string readonly cursor: number @@ -2030,7 +2020,7 @@ export type MessageListOutput = { readonly providerState?: { readonly [x: string]: JsonValue } readonly providerResultState?: { readonly [x: string]: JsonValue } readonly state: - | { readonly status: "pending"; readonly input: string } + | { readonly status: "streaming"; readonly input: string } | { readonly status: "running" readonly input: { readonly [x: string]: JsonValue } @@ -2043,19 +2033,10 @@ 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 readonly structured: { readonly [x: string]: JsonValue } readonly result?: JsonValue } @@ -2070,12 +2051,7 @@ 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 pruned?: number - } + readonly time: { readonly created: number; readonly ran?: number; readonly completed?: number } } > readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray } @@ -2094,16 +2070,37 @@ export type MessageListOutput = { 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 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 cursor: { readonly previous?: string | null; readonly next?: string | null } } @@ -3908,6 +3905,7 @@ 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 @@ -3963,7 +3961,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4025,7 +4023,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4087,7 +4085,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4149,7 +4147,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4254,7 +4252,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly messageID: string } } @@ -4263,7 +4261,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4502,7 +4500,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly messageID: string; readonly partID: string } } @@ -4511,7 +4509,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly agent: string } } @@ -4520,7 +4518,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4532,7 +4530,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4545,7 +4543,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly title: string } } @@ -4571,7 +4569,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 2 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -4580,7 +4578,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly parentID: string; readonly from?: string } } @@ -4589,7 +4587,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly inputID: string } } @@ -4598,7 +4596,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4626,7 +4624,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -4635,7 +4633,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -4644,7 +4642,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly error: { readonly type: string; readonly message: string } } } @@ -4653,7 +4651,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" } } @@ -4662,7 +4660,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly text: string } } @@ -4671,7 +4669,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4685,16 +4683,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly sessionID: string; readonly name: string; readonly text: string } + readonly data: { readonly sessionID: string; readonly id: 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4717,7 +4715,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4746,7 +4744,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4761,7 +4759,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4783,7 +4781,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4803,7 +4801,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly ordinal: number } } @@ -4825,7 +4823,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4839,7 +4837,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4866,7 +4864,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4881,7 +4879,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4908,7 +4906,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4922,7 +4920,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4938,7 +4936,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4956,7 +4954,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4967,7 +4965,6 @@ export type EventSubscribeOutput = | { readonly type: "text"; readonly text: string } | { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string } > - readonly outputPaths?: ReadonlyArray readonly result?: unknown readonly executed: boolean readonly resultState?: { readonly [x: string]: unknown } @@ -4978,7 +4975,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -4995,7 +4992,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -5010,7 +5007,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly inputID: string } } @@ -5019,9 +5016,14 @@ 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly sessionID: string; readonly reason: "auto" | "manual" } + readonly data: { + readonly sessionID: string + readonly reason: "auto" | "manual" + readonly recent: string + readonly inputID?: string + } } | { readonly id: string @@ -5036,7 +5038,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -5050,16 +5052,21 @@ 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } - readonly data: { readonly sessionID: string } + readonly data: { + readonly sessionID: string + readonly reason: "auto" | "manual" + readonly error: { readonly type: string; readonly message: string } + readonly inputID?: 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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string @@ -5067,13 +5074,12 @@ export type EventSubscribeOutput = readonly messageID: string readonly partID?: string readonly snapshot?: string - readonly diff?: string readonly files?: ReadonlyArray<{ - readonly path: string - readonly status: "added" | "modified" | "deleted" + readonly file: string + readonly patch: string readonly additions: number readonly deletions: number - readonly patch: string + readonly status: "added" | "deleted" | "modified" }> } } @@ -5083,7 +5089,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string } } @@ -5092,7 +5098,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: number } + readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: 1 } readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { readonly sessionID: string; readonly to: string } } @@ -6213,11 +6219,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" }> } diff --git a/packages/client/test/contract-identity.test.ts b/packages/client/test/contract-identity.test.ts index 8e14ad1ee8d..553c120f7a4 100644 --- a/packages/client/test/contract-identity.test.ts +++ b/packages/client/test/contract-identity.test.ts @@ -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.Message).toBe(SessionMessage.Message) + expect(CoreSessionMessage.Info).toBe(SessionMessage.Info) 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)) diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index a37a3c5bd78..c2cd5a1d213 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -223,7 +223,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/AgentV2.Info" + "$ref": "#/components/schemas/Agent.Info" } } }, @@ -595,7 +595,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "required": [ @@ -778,7 +778,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "required": [ @@ -928,7 +928,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "required": [ @@ -2317,7 +2317,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ @@ -2624,7 +2624,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } } }, @@ -3331,7 +3331,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } }, "required": [ @@ -3594,7 +3594,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ModelV2.Info" + "$ref": "#/components/schemas/Model.Info" } } }, @@ -3705,7 +3705,7 @@ "data": { "anyOf": [ { - "$ref": "#/components/schemas/ModelV2.Info" + "$ref": "#/components/schemas/Model.Info" }, { "type": "null" @@ -7312,7 +7312,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/CommandV2.Info" + "$ref": "#/components/schemas/Command.Info" } } }, @@ -7413,7 +7413,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SkillV2.Info" + "$ref": "#/components/schemas/Skill.Info" } } }, @@ -8508,7 +8508,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } } }, @@ -8605,7 +8605,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } }, "required": [ @@ -8750,7 +8750,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } }, "required": [ @@ -8970,7 +8970,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } }, "required": [ @@ -10200,7 +10200,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.Info" } } }, @@ -10562,12 +10562,15 @@ "$ref": "#/components/schemas/PermissionV2.Rule" } }, - "AgentV2.Info": { + "Agent.Info": { "type": "object", "properties": { "id": { "type": "string" }, + "name": { + "type": "string" + }, "model": { "$ref": "#/components/schemas/Model.Ref" }, @@ -10608,6 +10611,7 @@ }, "required": [ "id", + "name", "request", "mode", "hidden", @@ -10627,6 +10631,46 @@ ], "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": { @@ -10647,19 +10691,14 @@ ], "additionalProperties": false }, - "File.Diff": { + "FileDiff.Info": { "type": "object", "properties": { - "path": { + "file": { "type": "string" }, - "status": { - "type": "string", - "enum": [ - "added", - "modified", - "deleted" - ] + "patch": { + "type": "string" }, "additions": { "type": "integer", @@ -10677,20 +10716,25 @@ } ] }, - "patch": { - "type": "string" + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] } }, "required": [ - "path", - "status", + "file", + "patch", "additions", "deletions", - "patch" + "status" ], "additionalProperties": false }, - "Revert.State": { + "Session.Revert": { "type": "object", "properties": { "messageID": { @@ -10707,13 +10751,10 @@ "snapshot": { "type": "string" }, - "diff": { - "type": "string" - }, "files": { "type": "array", "items": { - "$ref": "#/components/schemas/File.Diff" + "$ref": "#/components/schemas/FileDiff.Info" } } }, @@ -10722,7 +10763,7 @@ ], "additionalProperties": false }, - "SessionV2.Info": { + "Session.Info": { "type": "object", "properties": { "id": { @@ -10776,44 +10817,10 @@ "$ref": "#/components/schemas/Model.Ref" }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" }, "time": { "type": "object", @@ -10844,7 +10851,7 @@ "type": "string" }, "revert": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ @@ -10864,7 +10871,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "cursor": { @@ -11667,14 +11674,6 @@ ], "additionalProperties": false }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, "text": { "type": "string" }, @@ -11691,7 +11690,6 @@ "required": [ "id", "time", - "sessionID", "text", "type" ], @@ -11773,6 +11771,9 @@ "skill" ] }, + "skill": { + "type": "string" + }, "name": { "type": "string" }, @@ -11784,187 +11785,12 @@ "id", "time", "type", + "skill", "name", "text" ], "additionalProperties": false }, - "Shell": { - "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 - }, "Session.Message.Shell": { "type": "object", "properties": { @@ -12000,8 +11826,62 @@ "shell" ] }, - "shell": { - "$ref": "#/components/schemas/Shell" + "shellID": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "command": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "exit": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] }, "output": { "type": "object", @@ -12042,7 +11922,9 @@ "id", "time", "type", - "shell" + "shellID", + "command", + "status" ], "additionalProperties": false }, @@ -12105,13 +11987,13 @@ ], "additionalProperties": false }, - "Session.Message.ToolState.Pending": { + "Session.Message.ToolState.Streaming": { "type": "object", "properties": { "status": { "type": "string", "enum": [ - "pending" + "streaming" ] }, "input": { @@ -12221,24 +12103,12 @@ "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" }, @@ -12330,7 +12200,7 @@ "state": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Message.ToolState.Pending" + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" }, { "$ref": "#/components/schemas/Session.Message.ToolState.Running" @@ -12354,9 +12224,6 @@ }, "completed": { "type": "number" - }, - "pruned": { - "type": "number" } }, "required": [ @@ -12486,44 +12353,10 @@ ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" }, "error": { "$ref": "#/components/schemas/Session.StructuredError" @@ -12542,7 +12375,7 @@ ], "additionalProperties": false }, - "Session.Message.Compaction": { + "Session.Message.Compaction.Running": { "type": "object", "properties": { "type": { @@ -12551,28 +12384,6 @@ "compaction" ] }, - "status": { - "type": "string", - "enum": [ - "queued", - "running", - "completed", - "failed" - ] - }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "summary": { - "type": "string" - }, - "recent": { - "type": "string" - }, "id": { "type": "string", "allOf": [ @@ -12595,20 +12406,174 @@ "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", - "id", - "time" + "recent" ], "additionalProperties": false }, - "Session.Message": { + "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": [ + "failed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "error" + ], + "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": { "anyOf": [ { "$ref": "#/components/schemas/Session.Message.AgentSelected" @@ -12700,11 +12665,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -12787,11 +12750,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -12874,11 +12835,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -12964,11 +12923,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13051,11 +13008,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 2 ] } }, @@ -13134,11 +13089,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13234,11 +13187,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13326,11 +13277,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13430,11 +13379,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13513,11 +13460,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13596,11 +13541,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13683,11 +13626,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13775,11 +13716,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13862,11 +13801,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13955,11 +13892,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13984,6 +13919,9 @@ } ] }, + "id": { + "type": "string" + }, "name": { "type": "string" }, @@ -13993,6 +13931,7 @@ }, "required": [ "sessionID", + "id", "name", "text" ], @@ -14008,7 +13947,7 @@ ], "additionalProperties": false }, - "Shell1": { + "Shell": { "type": "object", "properties": { "id": { @@ -14186,11 +14125,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14216,7 +14153,7 @@ ] }, "shell": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Shell" } }, "required": [ @@ -14273,11 +14210,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14303,7 +14238,7 @@ ] }, "shell": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Shell" }, "output": { "type": "object", @@ -14395,11 +14330,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14498,11 +14431,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14547,44 +14478,10 @@ ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" }, "snapshot": { "type": "string" @@ -14653,11 +14550,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14694,44 +14589,10 @@ "$ref": "#/components/schemas/Session.StructuredError" }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": [ @@ -14789,11 +14650,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14890,11 +14749,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14998,11 +14855,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15105,11 +14960,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15213,11 +15066,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15313,11 +15164,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15416,11 +15265,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15523,11 +15370,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15633,11 +15478,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15682,12 +15525,6 @@ "$ref": "#/components/schemas/LLM.ToolContent" } }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, "result": {}, "executed": { "type": "boolean" @@ -15757,11 +15594,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15865,11 +15700,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15979,11 +15812,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16071,11 +15902,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16106,11 +15935,23 @@ "auto", "manual" ] + }, + "recent": { + "type": "string" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, "required": [ "sessionID", - "reason" + "reason", + "recent" ], "additionalProperties": false } @@ -16162,11 +16003,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16261,11 +16100,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16289,10 +16126,30 @@ "pattern": "^ses" } ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, "required": [ - "sessionID" + "sessionID", + "reason", + "error" ], "additionalProperties": false } @@ -16344,11 +16201,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16374,7 +16229,7 @@ ] }, "revert": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ @@ -16431,11 +16286,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16514,11 +16367,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16568,7 +16419,7 @@ ], "additionalProperties": false }, - "SessionDurableEvent": { + "Session.Event.Durable": { "oneOf": [ { "$ref": "#/components/schemas/session.agent.selected" @@ -16717,7 +16568,7 @@ "SessionLogItem": { "anyOf": [ { - "$ref": "#/components/schemas/SessionDurableEvent" + "$ref": "#/components/schemas/Session.Event.Durable" }, { "$ref": "#/components/schemas/EventLog.Synced" @@ -16737,7 +16588,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } }, "cursor": { @@ -16823,6 +16674,9 @@ ], "additionalProperties": false }, + "Money.USDPerMillionTokens": { + "type": "number" + }, "Model.Cost": { "type": "object", "properties": { @@ -16846,19 +16700,19 @@ "additionalProperties": false }, "input": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "output": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "cache": { "type": "object", "properties": { "read": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "write": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" } }, "required": [ @@ -16875,7 +16729,7 @@ ], "additionalProperties": false }, - "ModelV2.Info": { + "Model.Info": { "type": "object", "properties": { "id": { @@ -19285,7 +19139,7 @@ ], "additionalProperties": false }, - "CommandV2.Info": { + "Command.Info": { "type": "object", "properties": { "name": { @@ -19313,9 +19167,12 @@ ], "additionalProperties": false }, - "SkillV2.Info": { + "Skill.Info": { "type": "object", "properties": { + "id": { + "type": "string" + }, "name": { "type": "string" }, @@ -19336,6 +19193,7 @@ } }, "required": [ + "id", "name", "location", "content" @@ -19569,7 +19427,7 @@ ], "additionalProperties": false }, - "SnapshotFileDiff": { + "FileDiff.LegacyInfo": { "type": "object", "properties": { "file": { @@ -19633,7 +19491,7 @@ "$ref": "#/components/schemas/PermissionRule" } }, - "Session": { + "SessionV1.Info": { "type": "object", "properties": { "id": { @@ -19687,7 +19545,7 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.LegacyInfo" } } }, @@ -19902,11 +19760,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -19932,7 +19788,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, "required": [ @@ -19989,11 +19845,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -20019,7 +19873,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, "required": [ @@ -20076,11 +19930,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -20106,7 +19958,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, "required": [ @@ -20268,7 +20120,7 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.LegacyInfo" } } }, @@ -20936,11 +20788,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -21023,11 +20873,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -22493,11 +22341,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -22584,11 +22430,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -22685,44 +22529,10 @@ ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": [ @@ -23836,7 +23646,7 @@ "type": "object", "properties": { "info": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Shell" } }, "required": [ @@ -26794,6 +26604,182 @@ ], "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": { diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 2dd54123ed9..8ba976399d3 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -8,6 +8,8 @@ import { State } from "./state" export const ID = Agent.ID export type ID = typeof ID.Type +export const Name = Agent.Name +export type Name = Agent.Name export const defaultID = ID.make("build") export const Color = Agent.Color diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 3cfc799d72f..1bb05e0fa19 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -1,6 +1,7 @@ export * as ConfigProviderPlugin from "./provider" import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Money } from "@opencode-ai/schema/money" import { Effect, Stream } from "effect" import { Config } from "../../config" import { ModelV2 } from "../../model" @@ -91,8 +92,8 @@ export const Plugin = define({ input: cost.input, output: cost.output, cache: { - read: cost.cache?.read ?? 0, - write: cost.cache?.write ?? 0, + read: cost.cache?.read ?? Money.USDPerMillionTokens.zero, + write: cost.cache?.write ?? Money.USDPerMillionTokens.zero, }, })) } diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts index ec371823958..e2fa972806e 100644 --- a/packages/core/src/config/provider.ts +++ b/packages/core/src/config/provider.ts @@ -1,6 +1,7 @@ export * as ConfigProvider from "./provider" import { Schema } from "effect" +import { Money } from "@opencode-ai/schema/money" import { ModelV2 } from "../model" const JsonRecord = Schema.Record(Schema.String, Schema.Json) @@ -17,8 +18,8 @@ export class Request extends Schema.Class("ConfigV2.Provider.Request")( }) {} class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ - read: Schema.Finite.pipe(Schema.optional), - write: Schema.Finite.pipe(Schema.optional), + read: Money.USDPerMillionTokens.pipe(Schema.optional), + write: Money.USDPerMillionTokens.pipe(Schema.optional), }) {} class Cost extends Schema.Class("ConfigV2.Model.Cost")({ @@ -26,8 +27,8 @@ class Cost extends Schema.Class("ConfigV2.Model.Cost")({ type: Schema.Literal("context"), size: Schema.Int, }).pipe(Schema.optional), - input: Schema.Finite, - output: Schema.Finite, + input: Money.USDPerMillionTokens, + output: Money.USDPerMillionTokens, cache: Cache.pipe(Schema.optional), }) {} diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index 99ca206c1f4..5b1735573d6 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -48,5 +48,6 @@ export const migrations = ( import("./migration/20260705180000_rename_instructions"), import("./migration/20260706223930_add-session-fork"), import("./migration/20260707010146_durable_session_inbox"), + import("./migration/20260707120000_migrate_prelaunch_v2_state"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260707120000_migrate_prelaunch_v2_state.ts b/packages/core/src/database/migration/20260707120000_migrate_prelaunch_v2_state.ts new file mode 100644 index 00000000000..e921d409047 --- /dev/null +++ b/packages/core/src/database/migration/20260707120000_migrate_prelaunch_v2_state.ts @@ -0,0 +1,227 @@ +import { sql } from "drizzle-orm" +import { Effect, Schema } from "effect" +import type { DatabaseMigration } from "../migration" + +const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString) +const isObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown)) + +export default { + id: "20260707120000_migrate_prelaunch_v2_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run( + sql`DELETE FROM session_message WHERE type = 'compaction' AND json_extract(data, '$.status') = 'queued'`, + ) + const messages = yield* tx.all<{ id: string; type: string; data: string }>( + sql`SELECT id, type, data FROM session_message WHERE type IN ('skill', 'shell', 'assistant', 'compaction', 'synthetic')`, + ) + for (const row of messages) { + const data = object(decodeJson(row.data)) + yield* tx.run( + sql`UPDATE session_message SET data = ${JSON.stringify(messageData(row.type, data))} WHERE id = ${row.id}`, + ) + } + + yield* tx.run(sql`DELETE FROM event WHERE type = 'session.compaction.delta.1'`) + const events = yield* tx.all<{ id: string; aggregateID: string; seq: number; type: string; data: string }>(sql` + SELECT id, aggregate_id as aggregateID, seq, type, data + FROM event + WHERE type IN ( + 'session.skill.activated.1', + 'session.skill.activated.2', + 'session.compaction.started.1', + 'session.compaction.started.2', + 'session.compaction.ended.1', + 'session.compaction.failed.1', + 'session.compaction.failed.2', + 'session.revert.staged.1', + 'session.revert.staged.2' + ) + ORDER BY aggregate_id, seq + `) + const compactionReasons = new Map() + 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) { + 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, 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 { + return isObject(value) ? value : {} +} + +function defined(value: Record) { + return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined)) +} diff --git a/packages/core/src/file.ts b/packages/core/src/file.ts index 87745c01eeb..0f48d9a04c0 100644 --- a/packages/core/src/file.ts +++ b/packages/core/src/file.ts @@ -1,6 +1,6 @@ export * as File from "./file" -import { Revert } from "@opencode-ai/schema/revert" +import { FileDiff } from "@opencode-ai/schema/file-diff" -export const Diff = Revert.FileDiff +export const Diff = FileDiff.Info export type Diff = typeof Diff.Type diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts index 19dcde8961a..eea11c95f77 100644 --- a/packages/core/src/git.ts +++ b/packages/core/src/git.ts @@ -606,7 +606,7 @@ const layer = Layer.effect( file, ])).text return { - path: file, + file, status, additions: binary ? 0 : Number(stats[0] ?? 0), deletions: binary ? 0 : Number(stats[1] ?? 0), diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts index a88aebfe07c..faeee213fba 100644 --- a/packages/core/src/models-dev.ts +++ b/packages/core/src/models-dev.ts @@ -2,6 +2,7 @@ import path from "path" import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" import { ModelsDev } from "@opencode-ai/schema/models-dev" +import { Money } from "@opencode-ai/schema/money" import { Global } from "./global" import { Flag } from "./flag/flag" import { Flock } from "./util/flock" @@ -18,10 +19,10 @@ export type CatalogModelStatus = typeof CatalogModelStatus.Type const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` const CostTier = Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - cache_read: Schema.optional(Schema.Finite), - cache_write: Schema.optional(Schema.Finite), + input: Money.USDPerMillionTokens, + output: Money.USDPerMillionTokens, + cache_read: Schema.optional(Money.USDPerMillionTokens), + cache_write: Schema.optional(Money.USDPerMillionTokens), tier: Schema.Struct({ type: Schema.Literal("context"), size: Schema.Finite, @@ -29,17 +30,17 @@ const CostTier = Schema.Struct({ }) const Cost = Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - cache_read: Schema.optional(Schema.Finite), - cache_write: Schema.optional(Schema.Finite), + input: Money.USDPerMillionTokens, + output: Money.USDPerMillionTokens, + cache_read: Schema.optional(Money.USDPerMillionTokens), + cache_write: Schema.optional(Money.USDPerMillionTokens), tiers: Schema.optional(Schema.Array(CostTier)), context_over_200k: Schema.optional( Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - cache_read: Schema.optional(Schema.Finite), - cache_write: Schema.optional(Schema.Finite), + input: Money.USDPerMillionTokens, + output: Money.USDPerMillionTokens, + cache_read: Schema.optional(Money.USDPerMillionTokens), + cache_write: Schema.optional(Money.USDPerMillionTokens), }), ), }) diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 7549089bf36..e6e03a4f151 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -125,6 +125,7 @@ export const Plugin = define({ yield* ctx.agent.transform((draft) => { draft.update(AgentV2.defaultID, (item) => { + item.name = AgentV2.Name.make("Build") item.description = "The default agent. Executes tools based on configured permissions." item.mode = "primary" item.permissions.push( @@ -136,6 +137,7 @@ export const Plugin = define({ }) draft.update(AgentV2.ID.make("plan"), (item) => { + item.name = AgentV2.Name.make("Plan") item.description = "Plan mode. Disallows all edit tools." item.mode = "primary" item.permissions.push( @@ -155,6 +157,7 @@ export const Plugin = define({ }) draft.update(AgentV2.ID.make("general"), (item) => { + item.name = AgentV2.Name.make("General") item.description = "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." item.mode = "subagent" @@ -167,6 +170,7 @@ export const Plugin = define({ }) draft.update(AgentV2.ID.make("explore"), (item) => { + item.name = AgentV2.Name.make("Explore") item.description = 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' item.system = PROMPT_EXPLORE @@ -189,6 +193,7 @@ export const Plugin = define({ }) draft.update(AgentV2.ID.make("compaction"), (item) => { + item.name = AgentV2.Name.make("Compaction") item.mode = "primary" item.hidden = true item.system = PROMPT_COMPACTION @@ -196,6 +201,7 @@ export const Plugin = define({ }) draft.update(AgentV2.ID.make("title"), (item) => { + item.name = AgentV2.Name.make("Title") item.mode = "primary" item.hidden = true item.system = PROMPT_TITLE @@ -203,6 +209,7 @@ export const Plugin = define({ }) draft.update(AgentV2.ID.make("summary"), (item) => { + item.name = AgentV2.Name.make("Summary") item.mode = "primary" item.hidden = true item.system = PROMPT_SUMMARY diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 67cf0069dc1..ef59535e81f 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,5 +1,6 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin" -import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import { Money } from "@opencode-ai/schema/money" +import type { ModelInfo } from "@opencode-ai/sdk/v2/types" import { Effect, Stream } from "effect" import { EventV2 } from "../event" import { ModelV2 } from "../model" @@ -11,13 +12,13 @@ function released(date: string) { return Number.isFinite(time) ? time : 0 } -function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { +function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] { const base = { - input: input?.input ?? 0, - output: input?.output ?? 0, + input: input?.input ?? Money.USDPerMillionTokens.zero, + output: input?.output ?? Money.USDPerMillionTokens.zero, cache: { - read: input?.cache_read ?? 0, - write: input?.cache_write ?? 0, + read: input?.cache_read ?? Money.USDPerMillionTokens.zero, + write: input?.cache_write ?? Money.USDPerMillionTokens.zero, }, } return [ @@ -27,8 +28,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { input: item.input, output: item.output, cache: { - read: item.cache_read ?? 0, - write: item.cache_write ?? 0, + read: item.cache_read ?? Money.USDPerMillionTokens.zero, + write: item.cache_write ?? Money.USDPerMillionTokens.zero, }, })) ?? []), ...(input?.context_over_200k @@ -41,8 +42,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { input: input.context_over_200k.input, output: input.context_over_200k.output, cache: { - read: input.context_over_200k.cache_read ?? 0, - write: input.context_over_200k.cache_write ?? 0, + read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero, + write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero, }, }, ] @@ -50,13 +51,13 @@ function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { ] } -function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) { +function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] | undefined) { if (!override) return base const next = cost(override) const [baseDefault, ...baseTiers] = base const [nextDefault, ...nextTiers] = next - const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` - const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({ + const tierKey = (item: ModelInfo["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` + const merge = (left: ModelInfo["cost"][number], right: ModelInfo["cost"][number]) => ({ ...left, ...right, tier: right.tier ?? left.tier, @@ -67,12 +68,25 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] const current = tiers.get(tierKey(item)) tiers.set(tierKey(item), current ? merge(current, item) : item) } - return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()] + return [ + merge( + baseDefault ?? { + input: Money.USDPerMillionTokens.zero, + output: Money.USDPerMillionTokens.zero, + cache: { + read: Money.USDPerMillionTokens.zero, + write: Money.USDPerMillionTokens.zero, + }, + }, + nextDefault, + ), + ...tiers.values(), + ] } const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] -function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable { +function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable { const npm = model.provider?.npm ?? provider.npm const options = model.reasoning_options ?? [] const effort = options.find((option) => option.type === "effort") @@ -117,7 +131,7 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2. function budgetVariants( npm: string | undefined, option: Extract[number], { type: "budget_tokens" }>, -): NonNullable { +): NonNullable { const max = option.max const high = option.max === undefined @@ -146,7 +160,7 @@ function modeName(model: ModelsDev.Model, mode: string) { return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}` } -function mergeVariants(model: ModelV2Info, next: NonNullable) { +function mergeVariants(model: ModelInfo, next: NonNullable) { const variants = model.variants ?? [] const existing = new Map(variants.map((variant) => [variant.id, variant])) const nextIDs = new Set(next.map((variant) => variant.id)) @@ -157,13 +171,13 @@ function mergeVariants(model: ModelV2Info, next: NonNullable["modes"]>[string]["provider"] - readonly variants?: NonNullable + readonly variants?: NonNullable } = {}, ) { draft.name = input.name ?? model.name diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 58690df0b28..cca6a2dfa73 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -10,6 +10,7 @@ import { Integration } from "../../integration" import { ModelV2 } from "../../model" import { ProviderV2 } from "../../provider" import { ConfigProviderV1 } from "../../v1/config/provider" +import { Money } from "@opencode-ai/schema/money" import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" import { ConfigV1 } from "../../v1/config/config" @@ -220,20 +221,23 @@ function withoutCredentials(body: Readonly> | undefined) function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) { const base = { - input: input.input, - output: input.output, - cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 }, + input: Money.USDPerMillionTokens.make(input.input), + output: Money.USDPerMillionTokens.make(input.output), + cache: { + read: Money.USDPerMillionTokens.make(input.cache_read ?? 0), + write: Money.USDPerMillionTokens.make(input.cache_write ?? 0), + }, } if (!input.context_over_200k) return [base] return [ base, { tier: { type: "context" as const, size: 200_000 }, - input: input.context_over_200k.input, - output: input.context_over_200k.output, + input: Money.USDPerMillionTokens.make(input.context_over_200k.input), + output: Money.USDPerMillionTokens.make(input.context_over_200k.output), cache: { - read: input.context_over_200k.cache_read ?? 0, - write: input.context_over_200k.cache_write ?? 0, + read: Money.USDPerMillionTokens.make(input.context_over_200k.cache_read ?? 0), + write: Money.USDPerMillionTokens.make(input.context_over_200k.cache_write ?? 0), }, }, ] diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts index 91c5297c0e2..439b77fd365 100644 --- a/packages/core/src/plugin/skill.ts +++ b/packages/core/src/plugin/skill.ts @@ -33,7 +33,8 @@ export const Plugin = define({ SkillV2.EmbeddedSource.make({ type: "embedded", skill: SkillV2.Info.make({ - name: "opencode", + id: SkillV2.ID.make("opencode"), + name: SkillV2.Name.make("OpenCode"), description: OpencodeDescription, location: AbsolutePath.make("/builtin/opencode.md"), content: OpencodeContent, @@ -44,7 +45,8 @@ export const Plugin = define({ SkillV2.EmbeddedSource.make({ type: "embedded", skill: SkillV2.Info.make({ - name: "report", + id: SkillV2.ID.make("report"), + name: SkillV2.Name.make("Report"), description: REPORT_DESCRIPTION, slash: true, location: AbsolutePath.make("/builtin/report.md"), @@ -103,15 +105,22 @@ const configuredPlugins = Effect.fn("SkillPlugin.configuredPlugins")(function* ( }) function terminal() { - return [ - process.env.TERM_PROGRAM ? `TERM_PROGRAM=${process.env.TERM_PROGRAM}` : undefined, - process.env.TERM ? `TERM=${process.env.TERM}` : undefined, - process.env.COLORTERM ? `COLORTERM=${process.env.COLORTERM}` : undefined, - ] - .filter((item): item is string => item !== undefined) - .join(", ") || "Unavailable: terminal environment variables are not set" + return ( + [ + process.env.TERM_PROGRAM ? `TERM_PROGRAM=${process.env.TERM_PROGRAM}` : undefined, + process.env.TERM ? `TERM=${process.env.TERM}` : undefined, + process.env.COLORTERM ? `COLORTERM=${process.env.COLORTERM}` : undefined, + ] + .filter((item): item is string => item !== undefined) + .join(", ") || "Unavailable: terminal environment variables are not set" + ) } function shell() { - return process.env.SHELL ?? process.env.ComSpec ?? process.env.COMSPEC ?? "Unavailable: shell environment variable is not set" + return ( + process.env.SHELL ?? + process.env.ComSpec ?? + process.env.COMSPEC ?? + "Unavailable: shell environment variable is not set" + ) } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index ea18ed39093..ad539409715 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,7 +1,7 @@ export * as SessionV2 from "./session" export * from "./session/schema" -import { DateTime, Effect, Layer, Schema, Context, Stream, Scope } from "effect" +import { Effect, Layer, Schema, Context, Stream, Scope } from "effect" import { ListAnchor } from "@opencode-ai/schema/session" import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm" import { ProjectV2 } from "./project" @@ -19,6 +19,7 @@ import { SessionSchema } from "./session/schema" import { AbsolutePath, PositiveInt, RelativePath } from "./schema" import { AgentV2 } from "./agent" import { SessionV1 } from "./v1/session" +import { Money } from "@opencode-ai/schema/money" import { InstallationVersion } from "./installation/version" import { Slug } from "./util/slug" import { ProjectTable } from "./project/sql" @@ -34,7 +35,7 @@ import { SessionEvent } from "./session/event" import { SessionInput } from "./session/input" import { Snapshot } from "./snapshot" import { SessionRevert } from "./session/revert" -import { Revert } from "@opencode-ai/schema/revert" +import { Session } from "@opencode-ai/schema/session" import { FSUtil } from "./fs-util" import { Mime } from "./mime" import type { EventLog } from "@opencode-ai/schema/event-log" @@ -46,8 +47,8 @@ import { Shell as ShellSchema } from "@opencode-ai/schema/shell" import { KeyedMutex } from "./effect/keyed-mutex" import { fileURLToPath } from "url" -export const RevertState = Revert.State -export type RevertState = Revert.State +export const RevertState = Session.Revert +export type RevertState = Session.Revert // get project -> project.locations // @@ -136,7 +137,7 @@ export class BusyError extends Schema.TaggedErrorClass()("Session.Bus sessionID: SessionSchema.ID, }) {} export class SkillNotFoundError extends Schema.TaggedErrorClass()("Session.SkillNotFoundError", { - skill: Schema.String, + skill: SkillV2.ID, }) {} export const MessageNotFoundError = SessionRevert.MessageNotFoundError export type MessageNotFoundError = SessionRevert.MessageNotFoundError @@ -170,14 +171,14 @@ export interface Interface { id: SessionMessage.ID direction: "previous" | "next" } - }) => Effect.Effect + }) => Effect.Effect readonly message: (input: { sessionID: SessionSchema.ID messageID: SessionMessage.ID - }) => Effect.Effect + }) => Effect.Effect readonly context: ( sessionID: SessionSchema.ID, - ) => Effect.Effect + ) => Effect.Effect /** * Durable, ordered, gap-free session log read. Replays public durable * session events after the exclusive `after` cursor, emits a `Synced` @@ -191,7 +192,10 @@ export interface Interface { after?: number follow?: boolean }) => Stream.Stream - readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect + readonly switchAgent: (input: { + sessionID: SessionSchema.ID + agent: AgentV2.ID + }) => Effect.Effect readonly switchModel: (input: { sessionID: SessionSchema.ID model: ModelV2.Ref @@ -209,7 +213,7 @@ export interface Interface { sessionID: SessionSchema.ID command: string arguments?: string - agent?: string + agent?: AgentV2.ID model?: ModelV2.Ref files?: PromptInput.Prompt["files"] agents?: PromptInput.Prompt["agents"] @@ -227,7 +231,7 @@ export interface Interface { readonly skill: (input: { id?: SessionMessage.ID sessionID: SessionSchema.ID - skill: string + skill: SkillV2.ID resume?: boolean }) => Effect.Effect readonly compact: ( @@ -250,7 +254,7 @@ export interface Interface { sessionID: SessionSchema.ID messageID: SessionMessage.ID files?: boolean - }) => Effect.Effect + }) => Effect.Effect readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect } @@ -273,7 +277,7 @@ const layer = Layer.effect( const scope = yield* Scope.Scope const activeShells = new Set() const shellLocks = KeyedMutex.makeUnsafe() - const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Info) const isDurableSessionEvent = Schema.is(SessionEvent.Durable) const decode = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe( @@ -322,7 +326,7 @@ const layer = Layer.effect( variant: input.model.variant, } : undefined, - cost: 0, + cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: now, updated: now }, }) @@ -529,7 +533,7 @@ const layer = Layer.effect( }) const model = command.model ?? commandAgent?.model ?? input.model if (agent !== undefined && session.agent !== AgentV2.ID.make(agent)) - yield* result.switchAgent({ sessionID: input.sessionID, agent }) + yield* result.switchAgent({ sessionID: input.sessionID, agent: AgentV2.ID.make(agent) }) if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model }) return yield* result.prompt({ @@ -591,12 +595,13 @@ const layer = Layer.effect( skill: Effect.fn("V2Session.skill")(function* (input) { const session = yield* result.get(input.sessionID) const skills = yield* SkillV2.Service.pipe(Effect.provide(locations.get(session.location))) - const skill = (yield* skills.list()).find((item) => item.name === input.skill) + const skill = (yield* skills.list()).find((item) => item.id === input.skill) if (!skill) return yield* new SkillNotFoundError({ skill: input.skill }) yield* events.publish( SessionEvent.Skill.Activated, { sessionID: input.sessionID, + id: skill.id, name: skill.name, text: skill.content, }, diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 8b975e32981..ab5e41e405a 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -64,19 +64,21 @@ type Dependencies = { export type AutoInput = { readonly sessionID: SessionSchema.ID - readonly messages: readonly SessionMessage.Message[] + readonly messages: readonly SessionMessage.Info[] readonly request: LLMRequest } type CompactInput = { readonly sessionID: SessionSchema.ID - readonly messages: readonly SessionMessage.Message[] + readonly messages: readonly SessionMessage.Info[] readonly model: Model + readonly inputID?: SessionMessage.ID } export type ManualInput = { readonly session: SessionSchema.Info - readonly messages: readonly SessionMessage.Message[] + readonly messages: readonly SessionMessage.Info[] + readonly inputID: SessionMessage.ID } export interface Interface { @@ -99,7 +101,7 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[ ) .join("\n") -const serialize = (message: SessionMessage.Message) => { +const serialize = (message: SessionMessage.Info) => { if (message.type === "user") { const files = message.files?.map( @@ -128,7 +130,7 @@ const serialize = (message: SessionMessage.Message) => { if (message.type === "system") return `[System update]: ${message.text}` if (message.type === "synthetic") return `[Synthetic context]: ${message.text}` if (message.type === "skill") return `[Skill activated: ${message.name}]\n${message.text}` - if (message.type === "shell") return `[Shell]: ${message.shell.command}\n${truncate(message.output?.output ?? "")}` + if (message.type === "shell") return `[Shell]: ${message.command}\n${truncate(message.output?.output ?? "")}` return "" } @@ -147,7 +149,7 @@ const settings = (documents: readonly Config.Entry[]) => { } const select = ( - messages: readonly SessionMessage.Message[], + messages: readonly SessionMessage.Info[], tokens: number, ): { readonly head: string; readonly recent: string } | undefined => { const conversation = messages @@ -198,6 +200,7 @@ const make = (dependencies: Dependencies) => { readonly context: readonly string[] readonly recent: string readonly output?: number + readonly inputID?: SessionMessage.ID }) { const context = input.model.route.defaults.limits?.context if (context === undefined || context <= 0) return false @@ -208,6 +211,8 @@ const make = (dependencies: Dependencies) => { yield* dependencies.events.publish(SessionEvent.Compaction.Started, { sessionID: input.sessionID, reason: input.reason, + recent: input.recent, + inputID: input.inputID, }) const chunks: string[] = [] @@ -235,9 +240,27 @@ const make = (dependencies: Dependencies) => { }), Effect.as(true), Effect.catchTag("LLM.Error", () => Effect.succeed(false)), + Effect.onInterrupt(() => + input.reason === "auto" + ? dependencies.events.publish(SessionEvent.Compaction.Failed, { + sessionID: input.sessionID, + reason: input.reason, + error: { type: "compaction.interrupted", message: "Compaction was interrupted" }, + inputID: input.inputID, + }) + : Effect.void, + ), ) const summary = chunks.join("") - if (!summarized || failed || !summary.trim()) return false + if (!summarized || failed || !summary.trim()) { + yield* dependencies.events.publish(SessionEvent.Compaction.Failed, { + sessionID: input.sessionID, + reason: input.reason, + error: { type: "compaction.failed", message: "Compaction produced no summary" }, + inputID: input.inputID, + }) + return false + } yield* dependencies.events.publish(SessionEvent.Compaction.Ended, { sessionID: input.sessionID, reason: input.reason, @@ -284,6 +307,7 @@ const make = (dependencies: Dependencies) => { ), recent: forcedShortContext ? "" : selected.recent, output: input.output, + inputID: input.inputID, }) }) const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: CompactInput) { @@ -327,6 +351,7 @@ export const layer = Layer.effect( sessionID: input.session.id, messages: input.messages, model: resolved.model, + inputID: input.inputID, }) }), }) diff --git a/packages/core/src/session/history.ts b/packages/core/src/session/history.ts index e719366940d..f2e3122c96a 100644 --- a/packages/core/src/session/history.ts +++ b/packages/core/src/session/history.ts @@ -8,7 +8,7 @@ import { InstructionCheckpointTable, SessionMessageTable } from "./sql" type DatabaseService = Database.Interface["db"] -const decode = Schema.decodeUnknownEffect(SessionMessage.Message) +const decode = Schema.decodeUnknownEffect(SessionMessage.Info) export const latestCompaction = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) { return yield* db diff --git a/packages/core/src/session/info.ts b/packages/core/src/session/info.ts index 1c8ddf153dd..2c507d00589 100644 --- a/packages/core/src/session/info.ts +++ b/packages/core/src/session/info.ts @@ -1,4 +1,4 @@ -import { DateTime } from "effect" +import { DateTime, Schema } from "effect" import { AgentV2 } from "../agent" import { Location } from "../location" import { ModelV2 } from "../model" @@ -9,7 +9,10 @@ import { WorkspaceV2 } from "../workspace" import { SessionSchema } from "./schema" import { SessionTable } from "./sql" import { SessionMessage } from "./message" -import { Snapshot } from "../snapshot" +import { PersistedRevert } from "@opencode-ai/schema/session-revert" +import { Money } from "@opencode-ai/schema/money" + +const decodeRevert = Schema.decodeUnknownSync(PersistedRevert) export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.Info { return SessionSchema.Info.make({ @@ -31,7 +34,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In variant: ModelV2.VariantID.make(row.model.variant ?? "default"), } : undefined, - cost: row.cost, + cost: Money.USD.make(row.cost), tokens: { input: row.tokens_input, output: row.tokens_output, @@ -46,7 +49,7 @@ export function fromRow(row: typeof SessionTable.$inferSelect): SessionSchema.In workspaceID: row.workspace_id ? WorkspaceV2.ID.make(row.workspace_id) : undefined, }), subpath: row.path ? RelativePath.make(row.path) : undefined, - revert: row.revert ? { ...row.revert, messageID: SessionMessage.ID.make(row.revert.messageID) } : undefined, + revert: row.revert ? decodeRevert(row.revert) : undefined, time: { created: DateTime.makeUnsafe(row.time_created), updated: DateTime.makeUnsafe(row.time_updated), diff --git a/packages/core/src/session/input.ts b/packages/core/src/session/input.ts index c08439d0c17..41eb36ea5b4 100644 --- a/packages/core/src/session/input.ts +++ b/packages/core/src/session/input.ts @@ -2,7 +2,7 @@ export * as SessionInput from "./input" import { and, asc, eq, isNull } from "drizzle-orm" import { DateTime, Effect, Schema } from "effect" -import { Admitted, Compaction, Delivery, Entry, PromptEntry } from "@opencode-ai/schema/session-input" +import { Admitted, Compaction, Delivery, Info, PromptEntry } from "@opencode-ai/schema/session-input" import type { Database } from "../database/database" import type { EventV2 } from "../event" import { KeyedMutex } from "../effect/keyed-mutex" @@ -14,7 +14,7 @@ import { SessionInputTable, SessionMessageTable } from "./sql" type DatabaseService = Database.Interface["db"] -export { Admitted, Compaction, Delivery, Entry, PromptEntry } +export { Admitted, Compaction, Delivery, Info, PromptEntry } const decodePrompt = Schema.decodeUnknownSync(Prompt) const encodePrompt = Schema.encodeSync(Prompt) @@ -24,7 +24,7 @@ export class LifecycleConflict extends Schema.TaggedErrorClass { +const fromRow = (row: typeof SessionInputTable.$inferSelect): Info => { const base = { admittedSeq: row.admitted_seq, id: SessionMessage.ID.make(row.id), diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index dc61f96dd48..01e045ce5f6 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -4,7 +4,7 @@ import { SessionEvent } from "./event" import { SessionMessage } from "./message" export type MemoryState = { - messages: SessionMessage.Message[] + messages: SessionMessage.Info[] } export interface Adapter { @@ -14,13 +14,13 @@ export interface Adapter { messageID: SessionMessage.ID, ) => Effect.Effect readonly getShell: ( - shellID: SessionMessage.Shell["shell"]["id"], + shellID: SessionMessage.Shell["shellID"], ) => Effect.Effect readonly getCompaction: () => Effect.Effect readonly updateAssistant: (assistant: SessionMessage.Assistant) => Effect.Effect readonly updateShell: (shell: SessionMessage.Shell) => Effect.Effect readonly updateCompaction: (compaction: SessionMessage.Compaction) => Effect.Effect - readonly appendMessage: (message: SessionMessage.Message) => Effect.Effect + readonly appendMessage: (message: SessionMessage.Info) => Effect.Effect } export function memory(state: MemoryState): Adapter { @@ -29,9 +29,7 @@ export function memory(state: MemoryState): Adapter { const shellIndex = (messageID: SessionMessage.ID) => state.messages.findLastIndex((message) => message.id === messageID) const compactionIndex = () => - state.messages.findLastIndex( - (message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"), - ) + state.messages.findLastIndex((message) => message.type === "compaction" && message.status === "running") // A newer step supersedes stale incomplete rows; never resume an older assistant projection. const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant") @@ -64,7 +62,7 @@ export function memory(state: MemoryState): Adapter { getShell(shellID) { return Effect.sync(() => { return state.messages.find((message): message is SessionMessage.Shell => { - return message.type === "shell" && message.shell.id === shellID + return message.type === "shell" && message.shellID === shellID }) }) }, @@ -186,13 +184,13 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { id: SessionMessage.ID.fromEvent(event.id), type: "system", text: event.data.text, + metadata: event.metadata, time: { created: event.created }, }), ), "session.synthetic": (event) => { return adapter.appendMessage( SessionMessage.Synthetic.make({ - sessionID: event.data.sessionID, text: event.data.text, description: event.data.description, metadata: event.data.metadata, @@ -207,8 +205,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { SessionMessage.Skill.make({ id: SessionMessage.ID.fromEvent(event.id), type: "skill", + skill: event.data.id, name: event.data.name, text: event.data.text, + metadata: event.metadata, time: { created: event.created }, }), ) @@ -219,7 +219,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { id: SessionMessage.ID.fromEvent(event.id), type: "shell", metadata: event.metadata, - shell: event.data.shell, + shellID: event.data.shell.id, + command: event.data.shell.command, + status: event.data.shell.status, time: { created: event.created }, }), ) @@ -230,7 +232,8 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { if (currentShell) { yield* adapter.updateShell( produce(currentShell, (draft) => { - draft.shell = castDraft(event.data.shell) + draft.status = event.data.shell.status + draft.exit = event.data.shell.exit draft.output = event.data.output draft.time.completed = event.created }), @@ -270,6 +273,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { type: "assistant", agent: event.data.agent, model: event.data.model, + metadata: event.metadata, time: { created: event.created }, content: [], snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, @@ -329,7 +333,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { id: event.data.callID, name: event.data.name, time: { created: event.created }, - state: SessionMessage.ToolStatePending.make({ status: "pending", input: "" }), + state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: "" }), }), ), ) @@ -339,7 +343,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.tool.input.ended": (event) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { const match = latestTool(draft, event.data.callID) - if (match && match.state.status === "pending") match.state.input = event.data.text + if (match && match.state.status === "streaming") match.state.input = event.data.text }) }, "session.tool.called": (event) => { @@ -382,7 +386,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { input: match.state.input, structured: event.data.structured, content: [...event.data.content], - outputPaths: event.data.outputPaths ? [...event.data.outputPaths] : [], result: event.data.result, }), ) @@ -392,7 +395,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { "session.tool.failed": (event) => { return updateOwnedAssistant(event.data.assistantMessageID, (draft) => { const match = latestTool(draft, event.data.callID) - if (match && (match.state.status === "pending" || match.state.status === "running")) { + if (match && (match.state.status === "streaming" || match.state.status === "running")) { match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState match.time.completed = event.created @@ -448,31 +451,30 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { } }) }, - "session.compaction.admitted": (event) => + "session.compaction.admitted": () => Effect.void, + "session.compaction.started": (event) => adapter.appendMessage( - SessionMessage.Compaction.make({ - id: event.data.inputID, + SessionMessage.CompactionRunning.make({ + id: event.data.inputID ?? SessionMessage.ID.fromEvent(event.id), type: "compaction", - status: "queued", + status: "running", metadata: event.metadata, - reason: "manual", + reason: event.data.reason, summary: "", - recent: "", + recent: event.data.recent ?? "", time: { created: event.created }, }), ), - "session.compaction.started": (event) => + "session.compaction.delta": (event) => Effect.gen(function* () { - if (event.data.reason !== "manual") return const current = yield* adapter.getCompaction() - if (!current) return - yield* adapter.updateCompaction({ ...current, status: "running" }) + if (current?.status !== "running") return + yield* adapter.updateCompaction({ ...current, summary: current.summary + event.data.text }) }), - "session.compaction.delta": () => Effect.void, "session.compaction.ended": (event) => { return Effect.gen(function* () { - const current = event.data.reason === "manual" ? yield* adapter.getCompaction() : undefined - if (current) { + const current = yield* adapter.getCompaction() + if (current?.status === "running") { yield* adapter.updateCompaction({ ...current, status: "completed", @@ -496,11 +498,20 @@ export function update(adapter: Adapter, event: SessionEvent.Event) { ) }) }, - "session.compaction.failed": () => + "session.compaction.failed": (event) => Effect.gen(function* () { const current = yield* adapter.getCompaction() - if (!current) return - yield* adapter.updateCompaction({ ...current, status: "failed" }) + const failed = SessionMessage.CompactionFailed.make({ + id: current?.id ?? event.data.inputID ?? SessionMessage.ID.fromEvent(event.id), + type: "compaction", + status: "failed", + metadata: current?.metadata ?? event.metadata, + reason: event.data.reason, + error: event.data.error, + time: current?.time ?? { created: event.created }, + }) + if (current?.status === "running") return yield* adapter.updateCompaction(failed) + yield* adapter.appendMessage(failed) }), "session.revert.staged": () => Effect.void, "session.revert.cleared": () => Effect.void, diff --git a/packages/core/src/session/projector.ts b/packages/core/src/session/projector.ts index f0a2f83d3fa..dcf79ff3a19 100644 --- a/packages/core/src/session/projector.ts +++ b/packages/core/src/session/projector.ts @@ -24,15 +24,14 @@ import { } from "./sql" import type { DeepMutable } from "../schema" import { Slug } from "../util/slug" +import { Money } from "@opencode-ai/schema/money" type DatabaseService = Database.Interface["db"] -type MessageEvent = Exclude< - SessionEvent.DurableEvent, - typeof SessionEvent.Forked.Type | typeof SessionEvent.Deleted.Type -> +type CurrentDurableEvent = Extract +type MessageEvent = Exclude -const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) -const encodeMessage = Schema.encodeSync(SessionMessage.Message) +const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info) +const encodeMessage = Schema.encodeSync(SessionMessage.Info) export class SessionAlreadyProjected extends Error {} @@ -87,7 +86,14 @@ function sessionRow(info: SessionV1.SessionInfo): typeof SessionTable.$inferInse tokens_reasoning: (info.tokens ?? { reasoning: 0 }).reasoning, tokens_cache_read: (info.tokens ?? { cache: { read: 0 } }).cache.read, tokens_cache_write: (info.tokens ?? { cache: { write: 0 } }).cache.write, - revert: info.revert ? { ...info.revert, messageID: SessionMessage.ID.make(info.revert.messageID) } : null, + revert: info.revert + ? { + messageID: SessionMessage.ID.make(info.revert.messageID), + partID: info.revert.partID, + snapshot: info.revert.snapshot, + diff: info.revert.diff, + } + : null, permission: info.permission ? [...info.permission] : undefined, time_created: info.time.created, time_updated: info.time.updated, @@ -151,7 +157,7 @@ const publishSessionUsage = Effect.fn("SessionProjector.publishUsage")(function* if (!row) return yield* events.publish(SessionEvent.UsageUpdated, { sessionID, - cost: row.cost, + cost: Money.USD.make(row.cost), tokens: { input: row.input, output: row.output, @@ -257,7 +263,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( eq(SessionMessageTable.session_id, event.data.parentID), gt(SessionMessageTable.seq, cursor), lt(SessionMessageTable.seq, copiedSeq + 1), - sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') not in ('queued', 'running')`, + sql`${SessionMessageTable.type} != 'compaction' or json_extract(${SessionMessageTable.data}, '$.status') != 'running'`, ), ) .orderBy(asc(SessionMessageTable.seq)) @@ -280,7 +286,7 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* ( seq: row.seq, time_created: row.time_created, time_updated: row.time_updated, - data: row.type === "synthetic" ? { ...row.data, sessionID: event.data.sessionID } : row.data, + data: row.data, } }), ) @@ -336,7 +342,7 @@ function run(db: DatabaseService, event: MessageEvent) { return Effect.gen(function* () { const decodeRow = (row: typeof SessionMessageTable.$inferSelect) => decodeMessage({ ...row.data, id: row.id, type: row.type }) - const updateMessage = (message: SessionMessage.Message) => { + const updateMessage = (message: SessionMessage.Info) => { if (event.durable === undefined) return Effect.die(new Error("Durable Session event is missing aggregate sequence")) const encoded = encodeMessage(message) @@ -353,7 +359,7 @@ function run(db: DatabaseService, event: MessageEvent) { .run() .pipe(Effect.orDie) } - const appendMessage = (message: SessionMessage.Message) => insertMessage(db, event, message) + const appendMessage = (message: SessionMessage.Info) => insertMessage(db, event, message) const adapter: SessionMessageUpdater.Adapter = { getModel() { return db @@ -412,7 +418,7 @@ function run(db: DatabaseService, event: MessageEvent) { and( eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "shell"), - sql`json_extract(${SessionMessageTable.data}, '$.shell.id') = ${shellID}`, + sql`json_extract(${SessionMessageTable.data}, '$.shellID') = ${shellID}`, ), ) .orderBy(desc(SessionMessageTable.seq)) @@ -433,7 +439,7 @@ function run(db: DatabaseService, event: MessageEvent) { and( eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.type, "compaction"), - sql`json_extract(${SessionMessageTable.data}, '$.status') in ('queued', 'running')`, + sql`json_extract(${SessionMessageTable.data}, '$.status') = 'running'`, ), ) .orderBy(desc(SessionMessageTable.seq)) @@ -454,7 +460,7 @@ function run(db: DatabaseService, event: MessageEvent) { }) } -function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Message) { +function insertMessage(db: DatabaseService, event: SessionEvent.DurableEvent, message: SessionMessage.Info) { if (event.durable === undefined) return Effect.die(new Error("Durable Session event is missing aggregate sequence")) const encoded = encodeMessage(message) const { id, type, ...data } = encoded @@ -661,14 +667,12 @@ const layer = Layer.effectDiscard( Effect.gen(function* () { if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) - const admitted = yield* SessionInput.projectCompactionAdmitted(db, { + yield* SessionInput.projectCompactionAdmitted(db, { admittedSeq: event.durable.seq, id: event.data.inputID, sessionID: event.data.sessionID, timeCreated: event.created, }) - if (admitted.id !== event.data.inputID) return - yield* run(db, event) }), ) yield* events.project(SessionEvent.Execution.Succeeded, (event) => run(db, event)) @@ -676,15 +680,7 @@ const layer = Layer.effectDiscard( yield* events.project(SessionEvent.Execution.Interrupted, (event) => run(db, event)) yield* events.project(SessionEvent.InstructionsUpdated, (event) => run(db, event)) yield* events.project(SessionEvent.Synthetic, (event) => run(db, event)) - yield* events.project(SessionEvent.Skill.Activated, (event) => - insertMessage(db, event, { - id: SessionMessage.ID.fromEvent(event.id), - type: "skill", - name: event.data.name, - text: event.data.text, - time: { created: event.created }, - }), - ) + yield* events.project(SessionEvent.Skill.Activated, (event) => run(db, event)) yield* events.project(SessionEvent.Shell.Started, (event) => run(db, event)) yield* events.project(SessionEvent.Shell.Ended, (event) => run(db, event)) yield* events.project(SessionEvent.Step.Started, (event) => run(db, event)) @@ -730,22 +726,26 @@ const layer = Layer.effectDiscard( yield* run(db, event) if (event.durable === undefined) return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence")) - yield* SessionInput.settleCompaction(db, { - sessionID: event.data.sessionID, - handledSeq: event.durable.seq, - }) + if (event.data.reason === "manual") + yield* SessionInput.settleCompaction(db, { + sessionID: event.data.sessionID, + handledSeq: event.durable.seq, + }) }), ) yield* events.project(SessionEvent.RevertEvent.Staged, (event) => - db - .update(SessionTable) - .set({ - revert: { ...event.data.revert, files: event.data.revert.files ? [...event.data.revert.files] : undefined }, - time_updated: DateTime.toEpochMillis(event.created), - }) - .where(eq(SessionTable.id, event.data.sessionID)) - .run() - .pipe(Effect.orDie, Effect.asVoid), + Effect.gen(function* () { + const revert = event.data.revert + yield* db + .update(SessionTable) + .set({ + revert: { ...revert, files: revert.files ? [...revert.files] : undefined }, + time_updated: DateTime.toEpochMillis(event.created), + }) + .where(eq(SessionTable.id, event.data.sessionID)) + .run() + .pipe(Effect.orDie) + }), ) yield* events.project(SessionEvent.RevertEvent.Cleared, (event) => db diff --git a/packages/core/src/session/revert.ts b/packages/core/src/session/revert.ts index 80ec1874726..42bc34e6943 100644 --- a/packages/core/src/session/revert.ts +++ b/packages/core/src/session/revert.ts @@ -1,7 +1,7 @@ export * as SessionRevert from "./revert" import { and, asc, eq, gt } from "drizzle-orm" -import { DateTime, Effect, Schema } from "effect" +import { Effect, Schema } from "effect" import { Database } from "../database/database" import { EventV2 } from "../event" import { RelativePath } from "../schema" @@ -46,7 +46,7 @@ const plan = Effect.fn("SessionRevert.plan")(function* (input: BoundaryInput) { .orderBy(asc(SessionMessageTable.seq)) .all() .pipe(Effect.orDie) - const decode = Schema.decodeUnknownEffect(SessionMessage.Message) + const decode = Schema.decodeUnknownEffect(SessionMessage.Info) const files = new Map() 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() if (original) { - for (const file of input.session.revert?.files ?? []) restore.set(file.path, original) + for (const file of input.session.revert?.files ?? []) restore.set(RelativePath.make(file.file), original) } if (input.files !== false) for (const [file, tree] of next) restore.set(file, tree) if (restore.size) yield* snapshot.restore({ files: restore }) @@ -81,10 +81,6 @@ export const stage = Effect.fn("SessionRevert.stage")(function* (input: { const revert = { messageID: input.messageID, snapshot: original, - diff: files - .map((file) => file.patch) - .join("") - .trim(), files, } satisfies SessionSchema.Info["revert"] yield* events.publish(SessionEvent.RevertEvent.Staged, { @@ -100,7 +96,7 @@ export const clear = Effect.fn("SessionRevert.clear")(function* (session: Sessio const original = session.revert.snapshot ? Snapshot.ID.make(session.revert.snapshot) : undefined if (original) yield* snapshot.restore({ - files: new Map((session.revert.files ?? []).map((file) => [file.path, original])), + files: new Map((session.revert.files ?? []).map((file) => [RelativePath.make(file.file), original])), }) const events = yield* EventV2.Service yield* events.publish(SessionEvent.RevertEvent.Cleared, { diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 3da43acd8c6..dea133ebb9b 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -11,6 +11,7 @@ import { type ProviderErrorEvent, } from "@opencode-ai/llm" import { SessionError } from "@opencode-ai/schema/session-error" +import { Money } from "@opencode-ai/schema/money" import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect" import { AgentV2 } from "../../agent" import { Config } from "../../config" @@ -67,13 +68,13 @@ export function calculateCost(costs: ModelV2.Info["cost"], tokens: StepTokens) { .filter((cost) => cost.tier?.type === "context" && context > cost.tier.size) .toSorted((a, b) => (b.tier?.size ?? 0) - (a.tier?.size ?? 0))[0] const cost = tier ?? costs.find((cost) => cost.tier === undefined) - if (!cost) return 0 - return ( + if (!cost) return Money.USD.zero + return Money.USD.make( (tokens.input * cost.input + (tokens.output + tokens.reasoning) * cost.output + tokens.cache.read * cost.cache.read + tokens.cache.write * cost.cache.write) / - 1_000_000 + 1_000_000, ) } @@ -165,7 +166,7 @@ const layer = Layer.effect( for (const message of yield* store.context(sessionID)) { if (message.type !== "assistant") continue for (const tool of message.content) { - if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue + if (tool.type !== "tool" || (tool.state.status !== "streaming" && tool.state.status !== "running")) continue yield* events.publish(SessionEvent.Tool.Failed, { sessionID, assistantMessageID: message.id, @@ -300,8 +301,7 @@ const layer = Layer.effect( // Durable publishes are serialized so tool fibers and step settlement never interleave // mid-event. const serialized = (effect: Effect.Effect) => publication.withPermit(effect) - const publish = (event: LLMEvent, outputPaths: ReadonlyArray = [], error?: SessionError.Error) => - serialized(publisher.publish(event, outputPaths, error)) + const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error)) let overflowFailure: ProviderErrorEvent | undefined const providerStream = llm.stream(hookedRequest).pipe( Stream.runForEach((event) => @@ -359,7 +359,6 @@ const layer = Layer.effect( result: settlement.result, output: settlement.output, }), - settlement.outputPaths ?? [], settlement.error, ).pipe( Effect.andThen( @@ -599,12 +598,30 @@ const layer = Layer.effect( return yield* compaction.compactManual({ session, messages: yield* store.context(sessionID), + inputID: pending.id, }) }), ).pipe(Effect.exit) if (Exit.isSuccess(compacted) && compacted.value) return true - yield* events.publish(SessionEvent.Compaction.Failed, { sessionID }) - if (Exit.isFailure(compacted)) return yield* Effect.failCause(compacted.cause) + if (Exit.isFailure(compacted)) { + const unsettled = yield* SessionInput.pendingCompaction(db, sessionID) + if (unsettled) + yield* events.publish(SessionEvent.Compaction.Failed, { + sessionID, + reason: "manual", + error: { type: "compaction.failed", message: Cause.pretty(compacted.cause) }, + inputID: unsettled.id, + }) + return yield* Effect.failCause(compacted.cause) + } + const unsettled = yield* SessionInput.pendingCompaction(db, sessionID) + if (unsettled) + yield* events.publish(SessionEvent.Compaction.Failed, { + sessionID, + reason: "manual", + error: { type: "compaction.failed", message: "Compaction could not start" }, + inputID: unsettled.id, + }) return true }), ) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index c93223c3078..e7f5454c395 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -6,13 +6,16 @@ import { SessionEvent } from "../event" import { SessionMessage } from "../message" import { SessionSchema } from "../schema" import { SessionError } from "@opencode-ai/schema/session-error" +import { Money } from "@opencode-ai/schema/money" +import { AgentV2 } from "../../agent" +import { Snapshot } from "../../snapshot" type Input = { readonly sessionID: SessionSchema.ID - readonly agent: string + readonly agent: AgentV2.ID readonly model: ModelV2.Ref readonly provider: string - readonly snapshot?: string + readonly snapshot?: Snapshot.ID readonly assistantMessageID?: SessionMessage.ID } @@ -226,7 +229,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) }) const publishStepFailure = Effect.fnUntraced(function* (usage?: { - readonly cost: number + readonly cost: Money.USD readonly tokens: ReturnType }) { if (stepFailed || stepFailure === undefined) return @@ -265,11 +268,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) return tool ? Effect.succeed(tool.assistantMessageID) : Effect.die(new Error(`Unknown tool call: ${callID}`)) } - const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* ( - event: LLMEvent, - outputPaths: ReadonlyArray = [], - error?: SessionError.Error, - ) { + const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (event: LLMEvent, error?: SessionError.Error) { switch (event.type) { case "step-start": yield* startAssistant() @@ -395,7 +394,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) assistantMessageID: tool.assistantMessageID, callID: event.id, ...result, - outputPaths, ...(executed ? { result: event.result } : {}), executed, resultState, diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 23de3691022..048374f519b 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -69,7 +69,7 @@ const providerMetadata = ( ): ProviderMetadata | undefined => (state === undefined ? undefined : { [provider]: state }) const toolInput = (tool: SessionMessage.AssistantTool) => - tool.state.status === "pending" + tool.state.status === "streaming" ? Option.getOrElse(decodeToolInput(tool.state.input), () => tool.state.input) : tool.state.input @@ -168,7 +168,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => { ] } -function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Message[] { +function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref): Message[] { switch (message.type) { case "agent-switched": case "model-switched": @@ -202,7 +202,7 @@ function toLLMMessage(message: SessionMessage.Message, model: ModelV2.Ref): Mess Message.make({ id: message.id, role: "user", - content: `Shell command: ${message.shell.command}\n\n${message.output?.output ?? ""}`, + content: `Shell command: ${message.command}\n\n${message.output?.output ?? ""}`, metadata: message.metadata, }), ] @@ -232,5 +232,5 @@ ${message.recent} } /** Translate projected V2 Session history into canonical @opencode-ai/llm context. */ -export const toLLMMessages = (messages: readonly SessionMessage.Message[], model: ModelV2.Ref) => +export const toLLMMessages = (messages: readonly SessionMessage.Info[], model: ModelV2.Ref) => messages.flatMap((message) => toLLMMessage(message, model)) diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index e7d3bdca58b..6e09505e3c5 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -5,7 +5,7 @@ import { ProjectTable } from "../project/sql" import type { SessionMessage } from "./message" import type { Prompt } from "@opencode-ai/schema/prompt" import type { SessionInput } from "./input" -import type { Snapshot } from "../snapshot" +import type { FileDiff } from "@opencode-ai/schema/file-diff" import { PermissionV1 } from "../v1/permission" import { ProjectV2 } from "../project" import type { SessionSchema } from "./schema" @@ -13,10 +13,11 @@ import type { MessageID, PartID, SessionV1 } from "../v1/session" import { WorkspaceV2 } from "../workspace" import { Timestamps } from "../database/schema.sql" import type { Instructions } from "../instructions/index" -import type { Revert } from "@opencode-ai/schema/revert" +import type { Session } from "@opencode-ai/schema/session" +import type { RevertV1 } from "@opencode-ai/schema/session-revert" import type { Schema } from "effect" -type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id"> +type SessionMessageData = Omit<(typeof SessionMessage.Info)["Encoded"], "type" | "id"> type V1MessageData = Omit type V1PartData = Omit @@ -41,7 +42,7 @@ export const SessionTable = sqliteTable( summary_additions: integer(), summary_deletions: integer(), summary_files: integer(), - summary_diffs: text({ mode: "json" }).$type(), + summary_diffs: text({ mode: "json" }).$type(), metadata: text({ mode: "json" }).$type>(), cost: real().notNull().default(0), tokens_input: integer().notNull().default(0), @@ -49,7 +50,7 @@ export const SessionTable = sqliteTable( tokens_reasoning: integer().notNull().default(0), tokens_cache_read: integer().notNull().default(0), tokens_cache_write: integer().notNull().default(0), - revert: text({ mode: "json" }).$type(), + revert: text({ mode: "json" }).$type(), permission: text({ mode: "json" }).$type(), agent: text(), model: text({ mode: "json" }).$type<{ @@ -148,7 +149,7 @@ export const SessionInputTable = sqliteTable( .$type() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - type: text().$type().notNull(), + type: text().$type().notNull(), prompt: text({ mode: "json" }).$type(), delivery: text().$type(), admitted_seq: integer().notNull(), diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts index 6458aa85fb7..24c99cf515c 100644 --- a/packages/core/src/session/store.ts +++ b/packages/core/src/session/store.ts @@ -13,10 +13,10 @@ import { fromRow } from "./info" export interface Interface { readonly get: (sessionID: Session.ID) => Effect.Effect - readonly context: (sessionID: Session.ID) => Effect.Effect + readonly context: (sessionID: Session.ID) => Effect.Effect readonly message: ( messageID: SessionMessage.ID, - ) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Message } | undefined> + ) => Effect.Effect<{ readonly sessionID: Session.ID; readonly message: SessionMessage.Info } | undefined> } export class Service extends Context.Service()("@opencode/v2/SessionStore") {} @@ -25,7 +25,7 @@ const layer = Layer.effect( Service, Effect.gen(function* () { const { db } = yield* Database.Service - const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Info) return Service.of({ get: Effect.fn("SessionStore.get")(function* (sessionID) { diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index 511e02af875..13b93004e4e 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -28,11 +28,15 @@ export type Source = typeof Source.Type export const Info = Skill.Info export type Info = Skill.Info +export const ID = Skill.ID +export type ID = Skill.ID +export const Name = Skill.Name +export type Name = Skill.Name export const Event = Skill.Event export const available = (skills: ReadonlyArray, agent: AgentV2.Info) => - skills.filter((skill) => PermissionV2.evaluate("skill", skill.name, agent.permissions).effect !== "deny") + skills.filter((skill) => PermissionV2.evaluate("skill", skill.id, agent.permissions).effect !== "deny") const Frontmatter = Schema.Struct({ name: Schema.String.pipe(Schema.optional), @@ -96,7 +100,7 @@ const layer = Layer.effect( source: Source.key(source), type: source.type, directories: [], - skills: [source.skill.name], + skills: [source.skill.id], }) return { skills: [source.skill], directories: [] } } @@ -112,15 +116,13 @@ const layer = Layer.effect( if (!markdown) continue const frontmatter = decodeFrontmatter(markdown.data).valueOrUndefined if (!frontmatter) continue - const name = - frontmatter.name !== undefined - ? frontmatter.name - : path.dirname(filepath) === directory - ? path.basename(filepath, ".md") - : undefined - if (!name) continue + const id = + path.dirname(filepath) === directory + ? path.basename(filepath, ".md") + : path.basename(path.dirname(filepath)) skills.push({ - name, + id: ID.make(id), + name: Name.make(frontmatter.name ?? id), description: frontmatter.description, slash: metadataBoolean(frontmatter.metadata, "opencode/slash") ?? frontmatter.slash, autoinvoke: metadataBoolean(frontmatter.metadata, "opencode/autoinvoke"), @@ -133,7 +135,7 @@ const layer = Layer.effect( source: Source.key(source), type: source.type, directories, - skills: skills.map((skill) => skill.name), + skills: skills.map((skill) => skill.id), }) return { skills, directories } }) @@ -148,7 +150,7 @@ const layer = Layer.effect( yield* Effect.logInfo("skill cache invalidated", { file, sources: invalidated.map(([key]) => key), - skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.name)), + skills: invalidated.flatMap(([, loaded]) => loaded.skills.map((skill) => skill.id)), }) yield* events.publish(Event.Updated, {}).pipe(Effect.asVoid) }) @@ -159,12 +161,12 @@ const layer = Layer.effect( ) const list = Effect.fn("SkillV2.list")(function* () { - const skills = new Map() + const skills = new Map() for (const source of state.get().sources) { const key = Source.key(source) const loaded = cache.get(key) ?? (yield* load(source)) cache.set(key, loaded) - for (const skill of loaded.skills) skills.set(skill.name, skill) + for (const skill of loaded.skills) skills.set(skill.id, skill) } return Array.from(skills.values()) }) @@ -180,4 +182,8 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [SkillDiscovery.node, FSUtil.node, EventV2.node] }) +export const node = makeLocationNode({ + service: Service, + layer, + deps: [SkillDiscovery.node, FSUtil.node, EventV2.node], +}) diff --git a/packages/core/src/skill/guidance.ts b/packages/core/src/skill/guidance.ts index 95ebe87eedb..5eae66065bc 100644 --- a/packages/core/src/skill/guidance.ts +++ b/packages/core/src/skill/guidance.ts @@ -8,7 +8,8 @@ import { SkillV2 } from "../skill" import { Instructions } from "../instructions/index" const Summary = Schema.Struct({ - name: Schema.String, + id: SkillV2.ID, + name: SkillV2.Name, description: Schema.String, }) type Summary = typeof Summary.Type @@ -16,6 +17,7 @@ type Summary = typeof Summary.Type const entries = (skills: ReadonlyArray) => skills.flatMap((skill) => [ " ", + ` ${skill.id}`, ` ${skill.name}`, ` ${skill.description}`, " ", @@ -34,8 +36,8 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray skill.name, - (before, after) => before.description !== after.description, + (skill) => skill.id, + (before, after) => before.name !== after.name || before.description !== after.description, ) // Additions and removals render as small deltas; anything else restates the full list. if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0)) @@ -50,7 +52,7 @@ const update = (previous: ReadonlyArray, current: ReadonlyArray skill.name).join(", ")}.`, + `The following skill IDs are no longer available and must not be used: ${diff.removed.map((skill) => skill.id).join(", ")}.`, ]), ].join("\n") } @@ -77,9 +79,9 @@ const layer = Layer.effect( .flatMap((skill) => skill.description === undefined || skill.autoinvoke === false ? [] - : [{ name: skill.name, description: skill.description }], + : [{ id: skill.id, name: skill.name, description: skill.description }], ) - .toSorted((a, b) => a.name.localeCompare(b.name)) + .toSorted((a, b) => a.id.localeCompare(b.id)) return Instructions.make({ key: Instructions.Key.make("core/skill-guidance"), codec: Schema.toCodecJson(Schema.Array(Summary)), diff --git a/packages/core/src/snapshot.ts b/packages/core/src/snapshot.ts index 9843e692262..8ef532f488d 100644 --- a/packages/core/src/snapshot.ts +++ b/packages/core/src/snapshot.ts @@ -10,10 +10,10 @@ import { Git } from "./git" import { Global } from "./global" import { Location } from "./location" import { AbsolutePath, RelativePath } from "./schema" +import { ID } from "@opencode-ai/schema/snapshot" import { Hash } from "./util/hash" -export const ID = Schema.String.pipe(Schema.brand("Snapshot.ID")) -export type ID = typeof ID.Type +export { ID } export class Error extends Schema.TaggedErrorClass()("Snapshot.Error", { operation: Schema.Literals(["capture", "files", "diff", "preview", "restore"]), @@ -253,12 +253,3 @@ function failure(operation: Error["operation"], cause: unknown) { cause, }) } - -/** Legacy persisted session diff shape. */ -export type LegacyFileDiff = { - file?: string - patch?: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" -} diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts index 8e9133b029d..1f80bd9684a 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/skill.ts @@ -13,11 +13,11 @@ export const name = "skill" const FILE_LIMIT = 10 export const Input = Schema.Struct({ - name: Schema.String.annotate({ description: "The name of the skill from the available skills list" }), + id: SkillV2.ID.annotate({ description: "The ID of the skill from the available skills list" }), }) export const Output = Schema.Struct({ - name: Schema.String, + name: SkillV2.Name, directory: Schema.String, output: Schema.String, }) @@ -27,7 +27,7 @@ export const description = [ "", "Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.", "", - "The skill name must match one of the available skills in the instructions.", + "The skill ID must match one of the available skills in the instructions.", ].join("\n") export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray) => { @@ -70,13 +70,13 @@ export const Plugin = { execute: (input, context) => Effect.gen(function* () { const current = yield* skills.list() - const skill = current.find((skill) => skill.name === input.name) - if (!skill) return yield* unableToLoad(input.name) + const skill = current.find((skill) => skill.id === input.id) + if (!skill) return yield* unableToLoad(input.id) return yield* Effect.gen(function* () { yield* permission.assert({ action: name, - resources: [skill.name], - save: [skill.name], + resources: [skill.id], + save: [skill.id], sessionID: context.sessionID, agent: context.agent, source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, @@ -94,7 +94,7 @@ export const Plugin = { directory, output: toModelOutput(skill, files), } - }).pipe(Effect.mapError((error) => unableToLoad(input.name, error))) + }).pipe(Effect.mapError((error) => unableToLoad(input.id, error))) }), }), ), diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 83f71c33986..ad667f288c2 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -1,4 +1,5 @@ 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" @@ -298,13 +299,31 @@ describe("CatalogV2", () => { catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => { model.capabilities.input = ["text"] model.capabilities.output = ["text"] - model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }] + model.cost = [ + { + input: Money.USDPerMillionTokens.make(1), + output: Money.USDPerMillionTokens.make(1), + cache: { + read: Money.USDPerMillionTokens.zero, + write: Money.USDPerMillionTokens.zero, + }, + }, + ] 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: 10, output: 10, cache: { read: 0, write: 0 } }] + model.cost = [ + { + input: Money.USDPerMillionTokens.make(10), + output: Money.USDPerMillionTokens.make(10), + cache: { + read: Money.USDPerMillionTokens.zero, + write: Money.USDPerMillionTokens.zero, + }, + }, + ] model.time.released = Date.now() }) }) diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index f371e195438..f3d1defc686 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -4,6 +4,7 @@ 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" @@ -87,7 +88,7 @@ Review files`, name: "review", template: "Review files", description: "File review", - agent: "reviewer", + agent: AgentV2.ID.make("reviewer"), model: { providerID: ProviderV2.ID.make("anthropic"), id: ModelV2.ID.make("claude"), diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index d1d27dc6e9b..0c7c85a1ca6 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -1,4 +1,5 @@ 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" @@ -253,7 +254,17 @@ describe("ConfigProviderPlugin.Plugin", () => { expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(model.enabled).toBe(false) expect(model.limit).toEqual({ context: 100, output: 75 }) - expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }]) + 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.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([ diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 4f147c0f3d1..10f02a73428 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -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 } from "effect" +import { Effect, Layer, Schema } 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,6 +17,7 @@ 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" @@ -26,6 +27,7 @@ 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" @@ -42,6 +44,256 @@ const run = (effect: Effect.Effect) => 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* () { diff --git a/packages/core/test/git.test.ts b/packages/core/test/git.test.ts index f77e32fd8ba..7441ff83db5 100644 --- a/packages/core/test/git.test.ts +++ b/packages/core/test/git.test.ts @@ -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.path, item.status])).toEqual([ + expect(diffs.map((item) => [item.file, 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]?.path).toBe(RelativePath.make("scope/tracked.txt")) + expect(preview[0]?.file).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") diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index c538d1151fb..dcfecdb0ae2 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -3,6 +3,7 @@ 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" @@ -356,7 +357,7 @@ describe("LocationServiceMap", () => { id: ModelV2.ID.make("chat"), providerID: ProviderV2.ID.make("unavailable"), }, - cost: 0, + cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, location, @@ -405,7 +406,7 @@ describe("LocationServiceMap", () => { providerID: ProviderV2.ID.make("aliased"), variant: ModelV2.VariantID.make("high"), }, - cost: 0, + cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, location, diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 545e09e0c38..c336666c865 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -1,5 +1,6 @@ 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" @@ -51,23 +52,31 @@ describe("ModelsDevPlugin", () => { temperature: true, tool_call: true, cost: { - input: 2.5, - output: 15, + input: Money.USDPerMillionTokens.make(2.5), + output: Money.USDPerMillionTokens.make(15), tiers: [ { tier: { type: "context", size: 272_000 }, - input: 3, - output: 18, - cache_read: 0.25, + input: Money.USDPerMillionTokens.make(3), + output: Money.USDPerMillionTokens.make(18), + cache_read: Money.USDPerMillionTokens.make(0.25), }, ], - context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, + context_over_200k: { + input: Money.USDPerMillionTokens.make(5), + output: Money.USDPerMillionTokens.make(22.5), + cache_read: Money.USDPerMillionTokens.make(0.5), + }, }, limit: { context: 1_050_000, input: 922_000, output: 128_000 }, experimental: { modes: { fast: { - cost: { input: 5, output: 30, cache_read: 0.5 }, + cost: { + input: Money.USDPerMillionTokens.make(5), + output: Money.USDPerMillionTokens.make(30), + cache_read: Money.USDPerMillionTokens.make(0.5), + }, provider: { headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, @@ -107,18 +116,31 @@ describe("ModelsDevPlugin", () => { variants: [], }) expect(fast?.cost).toEqual([ - { input: 5, output: 30, cache: { read: 0.5, write: 0 } }, + { + input: Money.USDPerMillionTokens.make(5), + output: Money.USDPerMillionTokens.make(30), + cache: { + read: Money.USDPerMillionTokens.make(0.5), + write: Money.USDPerMillionTokens.zero, + }, + }, { tier: { type: "context", size: 272_000 }, - input: 3, - output: 18, - cache: { read: 0.25, write: 0 }, + input: Money.USDPerMillionTokens.make(3), + output: Money.USDPerMillionTokens.make(18), + cache: { + read: Money.USDPerMillionTokens.make(0.25), + write: Money.USDPerMillionTokens.zero, + }, }, { tier: { type: "context", size: 200_000 }, - input: 5, - output: 22.5, - cache: { read: 0.5, write: 0 }, + input: Money.USDPerMillionTokens.make(5), + output: Money.USDPerMillionTokens.make(22.5), + cache: { + read: Money.USDPerMillionTokens.make(0.5), + write: Money.USDPerMillionTokens.zero, + }, }, ]) }), diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index f20091d514d..13109550c7f 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -1,4 +1,5 @@ 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" @@ -185,7 +186,16 @@ describe("OpenAIPlugin", () => { draft.package = item.package }) catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5"), (model) => { - model.cost = [{ input: 1, output: 2, cache: { read: 0.1, write: 0 } }] + model.cost = [ + { + input: Money.USDPerMillionTokens.make(1), + output: Money.USDPerMillionTokens.make(2), + cache: { + read: Money.USDPerMillionTokens.make(0.1), + write: Money.USDPerMillionTokens.zero, + }, + }, + ] }) catalog.model.update(item.id, ModelV2.ID.make("gpt-5.5-pro"), () => {}) catalog.model.update(item.id, ModelV2.ID.make("gpt-4.1"), () => {}) diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index c8b3757d3d8..f8334026f90 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -1,4 +1,5 @@ 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" @@ -65,7 +66,16 @@ function withEnv(vars: Record, effect: () = ) } -const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }] +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, + }, + }, +] describe("OpencodePlugin", () => { it.effect("registers account and service account methods", () => diff --git a/packages/core/test/plugin/skill.test.ts b/packages/core/test/plugin/skill.test.ts index 2bcf675052f..de0f745183b 100644 --- a/packages/core/test/plugin/skill.test.ts +++ b/packages/core/test/plugin/skill.test.ts @@ -37,17 +37,19 @@ describe("SkillPlugin.Plugin", () => { Effect.provide(NodeFileSystem.layer), ) const skills = yield* skill.list() - const report = skills.find((item) => item.name === "report") + const report = skills.find((item) => item.id === "report") expect(skills).toContainEqual( expect.objectContaining({ - name: "opencode", + id: "opencode", + name: "OpenCode", description: expect.stringContaining("any question about OpenCode itself"), }), ) expect(skills).toContainEqual( expect.objectContaining({ - name: "report", + id: "report", + name: "Report", description: expect.stringContaining("opencode issue"), }), ) diff --git a/packages/core/test/session-compact.test.ts b/packages/core/test/session-compact.test.ts index 40d9f48a199..703b098f638 100644 --- a/packages/core/test/session-compact.test.ts +++ b/packages/core/test/session-compact.test.ts @@ -15,6 +15,7 @@ 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" @@ -104,13 +105,10 @@ describe("SessionV2.compact", () => { expect(second.id).toBe(first.id) expect(requests).toHaveLength(0) - expect((yield* session.context(created.id)).find((message) => message.id === first.id)).toMatchObject({ - type: "compaction", - status: "queued", - reason: "manual", - summary: "", - recent: "", + 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)).toBeUndefined() }), ) }) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index d4d96abbc43..e8fb3addecc 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -141,7 +141,13 @@ 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] })).toBe(true) + expect( + yield* compaction.compactManual({ + session, + messages: [userMessage], + inputID: SessionMessage.ID.make("msg_manual_compaction"), + }), + ).toBe(true) expect(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"]) expect(requests).toHaveLength(1) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index 86e593c7225..3c4bb65a441 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -1,6 +1,7 @@ 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" @@ -210,7 +211,7 @@ describe("SessionV2.create", () => { expect(forked.parentID).toBeUndefined() expect(forkContext).toMatchObject([ { type: "user", text: "First" }, - { type: "synthetic", text: "parent note", sessionID: forked.id }, + { type: "synthetic", text: "parent note" }, ]) expect(forkContext.map((message) => message.id)).not.toEqual(parentContext.map((message) => message.id)) expect(history).toHaveLength(1) @@ -273,14 +274,14 @@ describe("SessionV2.create", () => { yield* events.publish(SessionEvent.Step.Started, { sessionID: parent.id, assistantMessageID, - agent: "build", + agent: AgentV2.ID.make("build"), model, }) yield* events.publish(SessionEvent.Step.Ended, { sessionID: parent.id, assistantMessageID, finish: "stop", - cost: 0.75, + cost: Money.USD.make(0.75), tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } }, }) @@ -533,7 +534,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", shell: { command: "echo hello", status: "exited", exit: 0 } }) + expect(shell).toMatchObject({ type: "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() @@ -553,8 +554,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", shell: { command: "false", status: "exited" } }) - expect(shell?.shell.exit).not.toBe(0) + expect(shell).toMatchObject({ type: "shell", command: "false", status: "exited" }) + expect(shell?.exit).not.toBe(0) expect(shell?.time.completed).toBeDefined() }), ), @@ -565,7 +566,7 @@ describe("SessionV2.create", () => { const session = yield* SessionV2.Service const created = yield* session.create({ location }) - yield* session.switchAgent({ sessionID: created.id, agent: "plan" }) + yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("plan") }) expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" }) expect( @@ -580,7 +581,7 @@ describe("SessionV2.create", () => { const missing = SessionV2.ID.make("ses_missing_agent_switch") expect( - yield* session.switchAgent({ sessionID: missing, agent: "plan" }).pipe( + yield* session.switchAgent({ sessionID: missing, agent: AgentV2.ID.make("plan") }).pipe( Effect.flip, Effect.map((error) => error._tag), ), diff --git a/packages/core/test/session-instructions.test.ts b/packages/core/test/session-instructions.test.ts index 854cd73d4be..34439836a4a 100644 --- a/packages/core/test/session-instructions.test.ts +++ b/packages/core/test/session-instructions.test.ts @@ -303,7 +303,6 @@ 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"] } }, diff --git a/packages/core/test/session-log.test.ts b/packages/core/test/session-log.test.ts index ac2fe59496e..a6c7797e0f5 100644 --- a/packages/core/test/session-log.test.ts +++ b/packages/core/test/session-log.test.ts @@ -1,6 +1,7 @@ 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" @@ -87,11 +88,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: "one" }) + yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("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: "two" }) - yield* session.switchAgent({ sessionID: created.id, agent: "three" }) + yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("two") }) + yield* session.switchAgent({ sessionID: created.id, agent: AgentV2.ID.make("three") }) const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id, after: 1 }))) diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index cc0655c55fb..1eb7f6b6fd4 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -1,7 +1,8 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Fiber, Option, Schema, Stream } from "effect" -import { asc, eq } from "drizzle-orm" +import { asc, eq, sql } 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" @@ -15,9 +16,11 @@ 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 { @@ -35,7 +38,8 @@ 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.Message) +const encodeMessage = Schema.encodeSync(SessionMessage.Info) +const build = AgentV2.defaultID const assistantRow = ( id: SessionMessage.ID, @@ -48,12 +52,108 @@ 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 @@ -89,7 +189,7 @@ describe("SessionProjector", () => { 1, { created }, { - cost: 0.5, + cost: Money.USD.make(0.5), tokens: { input: 4, output: 1, reasoning: 1, cache: { read: 1, write: 0 } }, }, ), @@ -98,7 +198,7 @@ describe("SessionProjector", () => { 2, { created }, { - cost: 0.75, + cost: Money.USD.make(0.75), tokens: { input: 6, output: 3, reasoning: 1, cache: { read: 2, write: 1 } }, }, ), @@ -111,7 +211,7 @@ describe("SessionProjector", () => { const events = yield* EventV2.Service yield* events.publish(SessionEvent.RevertEvent.Staged, { sessionID, - revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), diff: "patch", files: [] }, + revert: { messageID: boundary, snapshot: Snapshot.ID.make("tree"), files: [] }, }) expect((yield* db.select({ revert: SessionTable.revert }).from(SessionTable).get())?.revert).toMatchObject({ messageID: boundary, @@ -132,7 +232,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: 1.25, + cost: Money.USD.make(1.25), tokens_input: 10, tokens_output: 4, tokens_reasoning: 2, @@ -285,7 +385,7 @@ describe("SessionProjector", () => { yield* events.publish(SessionEvent.AgentSelected, { sessionID, - agent: "build", + agent: build, }) yield* events.publish(SessionEvent.ModelSelected, { sessionID, @@ -327,6 +427,7 @@ describe("SessionProjector", () => { yield* events.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", + recent: "recent context", }) yield* events.publish(SessionEvent.Compaction.Delta, { sessionID, @@ -336,18 +437,18 @@ describe("SessionProjector", () => { yield* db .select({ id: EventTable.id }) .from(EventTable) - .where(eq(EventTable.type, SessionEvent.Compaction.Delta.type)) + .where(sql`${EventTable.type} like 'session.compaction.delta.%'`) .all() .pipe(Effect.orDie), - ).toEqual([]) + ).toHaveLength(0) expect( yield* db - .select({ id: SessionMessageTable.id }) + .select({ data: SessionMessageTable.data }) .from(SessionMessageTable) .where(eq(SessionMessageTable.type, "compaction")) .all() .pipe(Effect.orDie), - ).toEqual([]) + ).toEqual([{ data: expect.objectContaining({ status: "running", summary: "", recent: "recent context" }) }]) yield* events.publish(SessionEvent.Compaction.Ended, { sessionID, reason: "manual", @@ -363,7 +464,7 @@ describe("SessionProjector", () => { .all() .pipe(Effect.orDie) const messages = rows.map((row) => - Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), + Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type }), ) expect(messages.map((message) => message.type)).toEqual([ @@ -379,7 +480,9 @@ describe("SessionProjector", () => { }) expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel }) expect(messages.find((message) => message.type === "shell")).toMatchObject({ - shell: { command: "pwd", status: "exited", exit: 0 }, + command: "pwd", + status: "exited", + exit: 0, output: { output: "/project", truncated: false }, time: { completed: DateTime.makeUnsafe(0) }, }) @@ -419,11 +522,7 @@ describe("SessionProjector", () => { .pipe(Effect.orDie) const events = yield* EventV2.Service const id = SessionMessage.ID.make("msg_creator_collision") - const { - id: _, - type, - ...data - } = encodeMessage({ id, sessionID, type: "synthetic", text: "existing", time: { created } }) + const { id: _, type, ...data } = encodeMessage({ id, type: "synthetic", text: "existing", time: { created } }) yield* db .insert(SessionMessageTable) .values({ id, session_id: sessionID, type, seq: 0, time_created: 0, data }) @@ -433,7 +532,7 @@ describe("SessionProjector", () => { .publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: id, - agent: "build", + agent: build, model, }) .pipe(Effect.exit) @@ -450,7 +549,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 }, @@ -458,7 +557,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) }, @@ -493,7 +592,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, @@ -503,7 +602,7 @@ describe("SessionProjector", () => { }) const decode = (row: typeof SessionMessageTable.$inferSelect) => - Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }) + Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type }) const firstRow = yield* db .select() .from(SessionMessageTable) @@ -515,7 +614,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, @@ -574,7 +673,7 @@ describe("SessionProjector", () => { sessionID, assistantMessageID: SessionMessage.ID.make("msg_assistant_2"), finish: "stop", - cost: 1.25, + cost: Money.USD.make(1.25), tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } }, }) @@ -586,13 +685,13 @@ describe("SessionProjector", () => { .all() .pipe(Effect.orDie) const messages = rows.map((row) => - Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), + Schema.decodeUnknownSync(SessionMessage.Info)({ ...row.data, id: row.id, type: row.type }), ) expect(messages[0]).not.toHaveProperty("time.completed") expect(messages[1]).toMatchObject({ type: "assistant", finish: "stop", - cost: 1.25, + cost: Money.USD.make(1.25), tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } }, time: { completed: DateTime.makeUnsafe(0) }, }) @@ -608,7 +707,7 @@ describe("SessionProjector", () => { }) expect(Option.getOrThrow(yield* Fiber.join(usageUpdated)).data).toEqual({ sessionID, - cost: 1.25, + cost: Money.USD.make(1.25), tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } }, }) }), @@ -661,13 +760,13 @@ describe("SessionProjector", () => { .all() .pipe(Effect.orDie) const messages = rows.map((row) => - Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type }), + Schema.decodeUnknownSync(SessionMessage.Info)({ ...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) }, @@ -675,7 +774,7 @@ describe("SessionProjector", () => { SessionMessage.Assistant.make({ id: SessionMessage.ID.make("msg_assistant_stale"), type: "assistant", - agent: "build", + agent: build, model, content: [], time: { created }, diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 3e76ddbb424..34b1ca1487b 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -6,6 +6,7 @@ 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" @@ -105,7 +106,7 @@ const eventCount = (type: string) => ), ) -const encodeMessage = Schema.encodeSync(SessionMessage.Message) +const encodeMessage = Schema.encodeSync(SessionMessage.Info) const assistantRow = (id: SessionMessage.ID, seq: number) => { const { id: _, @@ -115,7 +116,7 @@ const assistantRow = (id: SessionMessage.ID, seq: number) => { SessionMessage.Assistant.make({ id, type: "assistant", - agent: "build", + agent: AgentV2.ID.make("build"), model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }, content: [], time: { created: DateTime.makeUnsafe(0) }, @@ -677,7 +678,6 @@ describe("SessionV2.prompt", () => { ...data } = encodeMessage({ id: messageID, - sessionID, type: "synthetic", text: "Existing history", time: { created: DateTime.makeUnsafe(0) }, diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index fb79b35d11a..5910c4e4fbd 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -5,13 +5,14 @@ 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 { SessionV2 } from "@opencode-ai/core/session" +import { AgentV2 } from "@opencode-ai/core/agent" 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", () => { @@ -19,7 +20,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 }, @@ -56,7 +57,7 @@ describe("toLLMMessages", () => { SessionMessage.AgentSelected.make({ id: id("agent"), type: "agent-switched", - agent: "build", + agent: build, time: { created }, }), SessionMessage.ModelSelected.make({ @@ -82,24 +83,16 @@ 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", - 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 }, - }), + shellID: Shell.ID.make("sh_test"), + status: "exited", + command: "pwd", + exit: 0, output: { output: "/project", cursor: 8, size: 8, truncated: false }, time: { created, completed: created }, }), @@ -282,7 +275,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" }), @@ -295,7 +288,7 @@ Recent work type: "tool", id: "pending", name: "read", - state: SessionMessage.ToolStatePending.make({ status: "pending", input: '{"path":"README.md"}' }), + state: SessionMessage.ToolStateStreaming.make({ status: "streaming", input: '{"path":"README.md"}' }), time: { created }, }), SessionMessage.AssistantTool.make({ @@ -437,7 +430,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({ @@ -467,7 +460,7 @@ Recent work 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({ @@ -536,7 +529,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({ @@ -631,7 +624,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({ diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts index 14bfc37bbe6..c2e2dd25570 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/session-runner-model.test.ts @@ -2,6 +2,7 @@ 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" @@ -131,7 +132,7 @@ describe("SessionRunnerModel", () => { providerID: catalog.providerID, variant: ModelV2.VariantID.make("high"), }, - cost: 0, + cost: Money.USD.zero, 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") }, @@ -170,7 +171,7 @@ describe("SessionRunnerModel", () => { projectID: ProjectV2.ID.global, title: "test", model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") }, - cost: 0, + cost: Money.USD.zero, 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") }, @@ -200,7 +201,7 @@ describe("SessionRunnerModel", () => { providerID: catalog.providerID, variant: ModelV2.VariantID.make("unknown"), }, - cost: 0, + cost: Money.USD.zero, 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") }, @@ -236,7 +237,7 @@ describe("SessionRunnerModel", () => { projectID: ProjectV2.ID.global, title: "test", model: { id: catalog.id, providerID: catalog.providerID, variant: ModelV2.VariantID.make("high") }, - cost: 0, + cost: Money.USD.zero, 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") }, diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index d3aaf152c4f..4b53ae47291 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -1,7 +1,9 @@ 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" @@ -40,7 +42,7 @@ const capture = () => { published, publisher: createLLMEventPublisher(events, { sessionID, - agent: "build", + agent: AgentV2.ID.make("build"), model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider"), @@ -193,7 +195,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: 1.25, + cost: Money.USD.make(1.25), tokens: settlement.tokens, }), ) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index da8d9539c77..fd34a1954ef 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -30,6 +30,7 @@ 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" @@ -135,8 +136,23 @@ test("calculates step cost using the matching context tier", () => { expect( SessionRunnerLLM.calculateCost( [ - { 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: 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: 80, output: 10, reasoning: 2, cache: { read: 20, write: 1 } }, ), @@ -146,10 +162,20 @@ 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: 3, output: 4, cache: { read: 0.2, write: 0.6 } }], + [ + { + 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: 80, output: 10, reasoning: 2, cache: { read: 20, write: 0 } }, ), - ).toBe(0) + ).toBe(Money.USD.zero) }) const authorizations: Tool.Context[] = [] @@ -485,7 +511,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.Message[]) => { +const requireAssistant = (messages: readonly SessionMessage.Info[]) => { const assistant = messages.find((message) => message.type === "assistant") if (!assistant) throw new Error("Assistant message missing") return assistant @@ -581,7 +607,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: "pending", input: text } } + const expectedContent = { type: "tool", id, state: { status: "streaming", input: text } } return { delta: SessionEvent.Tool.Input.Delta, partialEvents, @@ -1056,7 +1082,7 @@ describe("SessionRunnerLLM", () => { skillBaselines.set(AgentV2.ID.make("reviewer"), "Reviewer skills") yield* events.publish(SessionEvent.AgentSelected, { sessionID, - agent: "reviewer", + agent: AgentV2.ID.make("reviewer"), }) yield* admit(session, "Second") yield* session.resume(sessionID) @@ -1082,7 +1108,7 @@ describe("SessionRunnerLLM", () => { return events .publish(SessionEvent.AgentSelected, { sessionID, - agent: "reviewer", + agent: AgentV2.ID.make("reviewer"), }) .pipe(Effect.asVoid) }) @@ -1265,6 +1291,7 @@ describe("SessionRunnerLLM", () => { yield* events.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", + recent: "", }) yield* events.publish(SessionEvent.Compaction.Ended, { sessionID, @@ -1308,10 +1335,7 @@ describe("SessionRunnerLLM", () => { expect(yield* SessionInput.pendingCompaction((yield* Database.Service).db, sessionID)).toMatchObject({ id: first.id, }) - expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toMatchObject({ - type: "compaction", - status: "queued", - }) + expect((yield* session.messages({ sessionID })).find((message) => message.id === first.id)).toBeUndefined() yield* admit(session, "Steer after compaction") yield* session.prompt({ @@ -1370,6 +1394,57 @@ 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) }), ) @@ -1511,9 +1586,10 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) const context = yield* session.context(sessionID) - expect(context.some((message) => message.type === "compaction")).toBe(false) - expect(context.slice(-2)).toMatchObject([ + expect(context).toContainEqual(expect.objectContaining({ type: "compaction", status: "failed", reason: "auto" })) + expect(context.slice(-3)).toMatchObject([ { type: "user", text: "Continue" }, + { type: "compaction", status: "failed", reason: "auto" }, { type: "assistant", finish: "error", error: { message: "prompt too long" } }, ]) }), @@ -1540,7 +1616,9 @@ describe("SessionRunnerLLM", () => { expect(yield* Fiber.await(run)).toMatchObject({ _tag: "Failure" }) streamGate = undefined expect(requests).toHaveLength(2) - expect((yield* session.context(sessionID)).some((message) => message.type === "compaction")).toBe(false) + expect(yield* session.context(sessionID)).toContainEqual( + expect.objectContaining({ type: "compaction", status: "failed", reason: "auto" }), + ) }), ) @@ -1557,6 +1635,7 @@ describe("SessionRunnerLLM", () => { yield* events.publish(SessionEvent.Compaction.Started, { sessionID, reason: "manual", + recent: "", }) yield* events.publish(SessionEvent.Compaction.Ended, { sessionID, @@ -2299,7 +2378,7 @@ describe("SessionRunnerLLM", () => { yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: "build", + agent: AgentV2.ID.make("build"), model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, }) yield* events.publish(SessionEvent.Tool.Input.Started, { @@ -2356,7 +2435,7 @@ describe("SessionRunnerLLM", () => { yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: "build", + agent: AgentV2.ID.make("build"), model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, }) yield* events.publish(SessionEvent.Tool.Input.Started, { @@ -2407,7 +2486,7 @@ describe("SessionRunnerLLM", () => { yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: "build", + agent: AgentV2.ID.make("build"), model: { id: ModelV2.ID.make("fake-model"), providerID: ProviderV2.ID.make("fake") }, }) yield* events.publish(SessionEvent.Tool.Input.Started, { diff --git a/packages/core/test/session-skill.test.ts b/packages/core/test/session-skill.test.ts index 8f9803ef4a6..d8cd7fbb563 100644 --- a/packages/core/test/session-skill.test.ts +++ b/packages/core/test/session-skill.test.ts @@ -26,7 +26,8 @@ const skills = Layer.mock(SkillV2.Service, { list: () => Effect.succeed([ SkillV2.Info.make({ - name: "effect", + id: SkillV2.ID.make("effect"), + name: SkillV2.Name.make("Effect"), description: "Effect guidance", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Use Effect", @@ -60,10 +61,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: "effect", resume: false }) + yield* sessions.skill({ id, sessionID: session.id, skill: SkillV2.ID.make("effect"), resume: false }) expect(yield* sessions.messages({ sessionID: session.id })).toContainEqual( - expect.objectContaining({ id, type: "skill", name: "effect", text: "Use Effect" }), + expect.objectContaining({ id, type: "skill", skill: "effect", name: "Effect", text: "Use Effect" }), ) }), ) diff --git a/packages/core/test/session-tool-progress.test.ts b/packages/core/test/session-tool-progress.test.ts index 8344771d2a6..e61bb54b636 100644 --- a/packages/core/test/session-tool-progress.test.ts +++ b/packages/core/test/session-tool-progress.test.ts @@ -4,6 +4,7 @@ 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" @@ -51,7 +52,7 @@ describe("Tool.Progress", () => { yield* service.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID, - agent: "build", + agent: AgentV2.ID.make("build"), model, }) const readAssistant = Effect.gen(function* () { diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index ae68d6c7ff3..100d44cfe26 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -76,6 +76,7 @@ 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], @@ -145,7 +146,7 @@ test("Core reuses the canonical shared schemas", async () => { [coreSessionMessage.Synthetic, SessionMessage.Synthetic], [coreSessionMessage.System, SessionMessage.System], [coreSessionMessage.Shell, SessionMessage.Shell], - [coreSessionMessage.ToolStatePending, SessionMessage.ToolStatePending], + [coreSessionMessage.ToolStateStreaming, SessionMessage.ToolStateStreaming], [coreSessionMessage.ToolStateRunning, SessionMessage.ToolStateRunning], [coreSessionMessage.ToolStateCompleted, SessionMessage.ToolStateCompleted], [coreSessionMessage.ToolStateError, SessionMessage.ToolStateError], @@ -156,7 +157,7 @@ test("Core reuses the canonical shared schemas", async () => { [coreSessionMessage.AssistantContent, SessionMessage.AssistantContent], [coreSessionMessage.Assistant, SessionMessage.Assistant], [coreSessionMessage.Compaction, SessionMessage.Compaction], - [coreSessionMessage.Message, SessionMessage.Message], + [coreSessionMessage.Info, SessionMessage.Info], [coreSessionTodo.Info, SessionTodo.Info], [coreSessionTodo.Event, SessionTodo.Event], [coreSkill.DirectorySource, Skill.DirectorySource], diff --git a/packages/core/test/skill.test.ts b/packages/core/test/skill.test.ts index b9193023bfa..1b8d846e69a 100644 --- a/packages/core/test/skill.test.ts +++ b/packages/core/test/skill.test.ts @@ -88,13 +88,15 @@ describe("SkillV2", () => { ]) expect(yield* skill.list()).toEqual([ SkillV2.Info.make({ - name: "foo", + id: SkillV2.ID.make("foo"), + name: SkillV2.Name.make("foo"), slash: true, location: AbsolutePath.make(path.join(first, "foo.md")), content: "# foo", }), { - name: "review", + id: SkillV2.ID.make("review"), + name: SkillV2.Name.make("review"), description: "Second", location: AbsolutePath.make(path.join(second, "review", "SKILL.md")), content: "# review", @@ -129,8 +131,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(["deploy"]) - expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"]) + 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(pulls).toBe(1) expect(SkillV2.available(yield* skill.list(), (yield* agents.get(AgentV2.ID.make("reviewer")))!)).toEqual([]) }), @@ -165,7 +167,8 @@ metadata: expect(yield* skill.list()).toEqual([ { - name: "manual", + id: SkillV2.ID.make("manual"), + name: SkillV2.Name.make("manual"), description: "Manual only", slash: true, autoinvoke: false, diff --git a/packages/core/test/skill/guidance.test.ts b/packages/core/test/skill/guidance.test.ts index 704f42b803b..1c5b5b28392 100644 --- a/packages/core/test/skill/guidance.test.ts +++ b/packages/core/test/skill/guidance.test.ts @@ -11,24 +11,28 @@ import { it } from "../lib/effect" const build = AgentV2.ID.make("build") const effect = SkillV2.Info.make({ - name: "effect", + id: SkillV2.ID.make("effect"), + name: SkillV2.Name.make("Effect"), description: "Build applications with Effect", location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")), content: "Effect guidance", }) const hidden = SkillV2.Info.make({ - name: "hidden", + id: SkillV2.ID.make("hidden"), + name: SkillV2.Name.make("Hidden"), location: AbsolutePath.make(path.resolve("/skills/hidden/SKILL.md")), content: "Undescribed guidance", }) const denied = SkillV2.Info.make({ - name: "denied", + id: SkillV2.ID.make("denied"), + name: SkillV2.Name.make("Denied"), description: "Must not be advertised", location: AbsolutePath.make(path.resolve("/skills/denied/SKILL.md")), content: "Denied guidance", }) const manual = SkillV2.Info.make({ - name: "manual", + id: SkillV2.ID.make("manual"), + name: SkillV2.Name.make("Manual"), description: "Load only when explicitly selected", autoinvoke: false, location: AbsolutePath.make(path.resolve("/skills/manual/SKILL.md")), @@ -59,7 +63,8 @@ describe("SkillGuidance", () => { "Use the skill tool to load a skill when a task matches its description.", "", " ", - " effect", + " effect", + " Effect", " Build applications with Effect", " ", "", @@ -74,7 +79,7 @@ describe("SkillGuidance", () => { .pipe(Effect.flatMap((context) => Instructions.reconcile(context, initialized.applied))), ).toMatchObject({ _tag: "Updated", - text: "The following skills are no longer available and must not be used: effect.", + text: "The following skill IDs are no longer available and must not be used: effect.", }) }).pipe(Effect.provide(layer(() => skills))) }) @@ -82,7 +87,8 @@ 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({ - name: "debugging", + id: SkillV2.ID.make("debugging"), + name: SkillV2.Name.make("Debugging"), description: "Diagnose hard bugs", location: AbsolutePath.make(path.resolve("/skills/debugging/SKILL.md")), content: "Debugging guidance", @@ -103,7 +109,8 @@ describe("SkillGuidance", () => { text: [ "New skills are available in addition to those previously listed:", " ", - " debugging", + " debugging", + " Debugging", " Diagnose hard bugs", " ", ].join("\n"), @@ -117,7 +124,7 @@ describe("SkillGuidance", () => { ) expect(removed).toMatchObject({ _tag: "Updated", - text: "The following skills are no longer available and must not be used: effect.", + text: "The following skill IDs are no longer available and must not be used: effect.", }) }).pipe(Effect.provide(layer(() => skills))) }) @@ -192,7 +199,7 @@ describe("SkillGuidance", () => { const guidance = yield* SkillGuidance.Service expect( (yield* guidance.load({ id: agent.id, info: agent }).pipe(Effect.flatMap(Instructions.initialize))).text, - ).toContain("effect") + ).toContain("Effect") }).pipe(Effect.provide(layer(() => [effect]))) }) diff --git a/packages/core/test/snapshot.test.ts b/packages/core/test/snapshot.test.ts index 5e01fefc17d..3bc0f341013 100644 --- a/packages/core/test/snapshot.test.ts +++ b/packages/core/test/snapshot.test.ts @@ -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]?.path).toBe(RelativePath.make("scope/tracked.txt")) + expect(preview[0]?.file).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") diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index e92465fc3db..6a6a53e171d 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -3,6 +3,7 @@ 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" @@ -104,7 +105,7 @@ const executionNode = makeGlobalNode({ sessionID: id, assistantMessageID, finish: "stop", - cost: 0, + cost: Money.USD.zero, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, }) }) @@ -442,10 +443,7 @@ 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 | undefined const shellID = typeof structured?.shellID === "string" ? structured.shellID : undefined expect(settled.output?.structured).toMatchObject({ truncated: false }) diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index 6a49fa22fad..8d81917d8c4 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -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 name, and loads model-facing content", () => + it.live("lists available skills, authorizes the selected ID, and loads model-facing content", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -42,7 +42,8 @@ describe("SkillTool", () => { ) const info: SkillV2.Info = { - name: "effect", + id: SkillV2.ID.make("effect"), + name: SkillV2.Name.make("Effect"), description: "Use Effect", location: AbsolutePath.make(location), content: "# Effect\n\nGuidance", @@ -102,7 +103,7 @@ describe("SkillTool", () => { yield* executeTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-skill", name: "skill", input: { name: "effect" } }, + call: { type: "tool-call", id: "call-skill", name: "skill", input: { id: "effect" } }, }), ).toEqual({ type: "text", @@ -113,11 +114,11 @@ describe("SkillTool", () => { yield* settleTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { name: "effect" } }, + call: { type: "tool-call", id: "call-skill-overflow", name: "skill", input: { id: "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"] }, @@ -127,7 +128,7 @@ describe("SkillTool", () => { yield* executeTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-missing-skill", name: "skill", input: { name: "missing" } }, + call: { type: "tool-call", id: "call-missing-skill", name: "skill", input: { id: "missing" } }, }), ).toEqual({ type: "error", value: "Unable to load skill missing" }) deny = true @@ -135,12 +136,13 @@ describe("SkillTool", () => { yield* executeTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-denied-skill", name: "skill", input: { name: "effect" } }, + call: { type: "tool-call", id: "call-denied-skill", name: "skill", input: { id: "effect" } }, }), ).toEqual({ type: "error", value: "Unable to load skill effect" }) deny = false const flat = SkillV2.Info.make({ - name: "public", + id: SkillV2.ID.make("public"), + name: SkillV2.Name.make("Public"), description: "Public guidance", location: AbsolutePath.make(path.join(tmp.path, "public.md")), content: "Public", @@ -156,7 +158,7 @@ describe("SkillTool", () => { yield* executeTool(registry, { sessionID, ...toolIdentity, - call: { type: "tool-call", id: "call-flat-skill", name: "skill", input: { name: "public" } }, + call: { type: "tool-call", id: "call-flat-skill", name: "skill", input: { id: "public" } }, }), ).toEqual({ type: "text", value: SkillTool.toModelOutput(flat, []) }) }).pipe(Effect.provide(skillToolLayer)) diff --git a/packages/core/test/tool-subagent.test.ts b/packages/core/test/tool-subagent.test.ts index 92dc0e833b5..d2e63967105 100644 --- a/packages/core/test/tool-subagent.test.ts +++ b/packages/core/test/tool-subagent.test.ts @@ -1,5 +1,6 @@ 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" @@ -70,7 +71,7 @@ const executionNode = makeGlobalNode({ sessionID, assistantMessageID, finish: "stop", - cost: 0, + cost: Money.USD.zero, tokens, }) }) diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index a37a3c5bd78..c2cd5a1d213 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -223,7 +223,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/AgentV2.Info" + "$ref": "#/components/schemas/Agent.Info" } } }, @@ -595,7 +595,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "required": [ @@ -778,7 +778,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "required": [ @@ -928,7 +928,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "required": [ @@ -2317,7 +2317,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ @@ -2624,7 +2624,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } } }, @@ -3331,7 +3331,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } }, "required": [ @@ -3594,7 +3594,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ModelV2.Info" + "$ref": "#/components/schemas/Model.Info" } } }, @@ -3705,7 +3705,7 @@ "data": { "anyOf": [ { - "$ref": "#/components/schemas/ModelV2.Info" + "$ref": "#/components/schemas/Model.Info" }, { "type": "null" @@ -7312,7 +7312,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/CommandV2.Info" + "$ref": "#/components/schemas/Command.Info" } } }, @@ -7413,7 +7413,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SkillV2.Info" + "$ref": "#/components/schemas/Skill.Info" } } }, @@ -8508,7 +8508,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } } }, @@ -8605,7 +8605,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } }, "required": [ @@ -8750,7 +8750,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } }, "required": [ @@ -8970,7 +8970,7 @@ "$ref": "#/components/schemas/Location.Info" }, "data": { - "$ref": "#/components/schemas/Shell" + "$ref": "#/components/schemas/Shell1" } }, "required": [ @@ -10200,7 +10200,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.Info" } } }, @@ -10562,12 +10562,15 @@ "$ref": "#/components/schemas/PermissionV2.Rule" } }, - "AgentV2.Info": { + "Agent.Info": { "type": "object", "properties": { "id": { "type": "string" }, + "name": { + "type": "string" + }, "model": { "$ref": "#/components/schemas/Model.Ref" }, @@ -10608,6 +10611,7 @@ }, "required": [ "id", + "name", "request", "mode", "hidden", @@ -10627,6 +10631,46 @@ ], "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": { @@ -10647,19 +10691,14 @@ ], "additionalProperties": false }, - "File.Diff": { + "FileDiff.Info": { "type": "object", "properties": { - "path": { + "file": { "type": "string" }, - "status": { - "type": "string", - "enum": [ - "added", - "modified", - "deleted" - ] + "patch": { + "type": "string" }, "additions": { "type": "integer", @@ -10677,20 +10716,25 @@ } ] }, - "patch": { - "type": "string" + "status": { + "type": "string", + "enum": [ + "added", + "deleted", + "modified" + ] } }, "required": [ - "path", - "status", + "file", + "patch", "additions", "deletions", - "patch" + "status" ], "additionalProperties": false }, - "Revert.State": { + "Session.Revert": { "type": "object", "properties": { "messageID": { @@ -10707,13 +10751,10 @@ "snapshot": { "type": "string" }, - "diff": { - "type": "string" - }, "files": { "type": "array", "items": { - "$ref": "#/components/schemas/File.Diff" + "$ref": "#/components/schemas/FileDiff.Info" } } }, @@ -10722,7 +10763,7 @@ ], "additionalProperties": false }, - "SessionV2.Info": { + "Session.Info": { "type": "object", "properties": { "id": { @@ -10776,44 +10817,10 @@ "$ref": "#/components/schemas/Model.Ref" }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" }, "time": { "type": "object", @@ -10844,7 +10851,7 @@ "type": "string" }, "revert": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ @@ -10864,7 +10871,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/SessionV2.Info" + "$ref": "#/components/schemas/Session.Info" } }, "cursor": { @@ -11667,14 +11674,6 @@ ], "additionalProperties": false }, - "sessionID": { - "type": "string", - "allOf": [ - { - "pattern": "^ses" - } - ] - }, "text": { "type": "string" }, @@ -11691,7 +11690,6 @@ "required": [ "id", "time", - "sessionID", "text", "type" ], @@ -11773,6 +11771,9 @@ "skill" ] }, + "skill": { + "type": "string" + }, "name": { "type": "string" }, @@ -11784,187 +11785,12 @@ "id", "time", "type", + "skill", "name", "text" ], "additionalProperties": false }, - "Shell": { - "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 - }, "Session.Message.Shell": { "type": "object", "properties": { @@ -12000,8 +11826,62 @@ "shell" ] }, - "shell": { - "$ref": "#/components/schemas/Shell" + "shellID": { + "type": "string", + "allOf": [ + { + "pattern": "^sh_" + } + ] + }, + "command": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "running", + "exited", + "timeout", + "killed" + ] + }, + "exit": { + "anyOf": [ + { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string", + "enum": [ + "NaN" + ] + }, + { + "type": "string", + "enum": [ + "Infinity" + ] + }, + { + "type": "string", + "enum": [ + "-Infinity" + ] + } + ] + }, + { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + } + ] }, "output": { "type": "object", @@ -12042,7 +11922,9 @@ "id", "time", "type", - "shell" + "shellID", + "command", + "status" ], "additionalProperties": false }, @@ -12105,13 +11987,13 @@ ], "additionalProperties": false }, - "Session.Message.ToolState.Pending": { + "Session.Message.ToolState.Streaming": { "type": "object", "properties": { "status": { "type": "string", "enum": [ - "pending" + "streaming" ] }, "input": { @@ -12221,24 +12103,12 @@ "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" }, @@ -12330,7 +12200,7 @@ "state": { "anyOf": [ { - "$ref": "#/components/schemas/Session.Message.ToolState.Pending" + "$ref": "#/components/schemas/Session.Message.ToolState.Streaming" }, { "$ref": "#/components/schemas/Session.Message.ToolState.Running" @@ -12354,9 +12224,6 @@ }, "completed": { "type": "number" - }, - "pruned": { - "type": "number" } }, "required": [ @@ -12486,44 +12353,10 @@ ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" }, "error": { "$ref": "#/components/schemas/Session.StructuredError" @@ -12542,7 +12375,7 @@ ], "additionalProperties": false }, - "Session.Message.Compaction": { + "Session.Message.Compaction.Running": { "type": "object", "properties": { "type": { @@ -12551,28 +12384,6 @@ "compaction" ] }, - "status": { - "type": "string", - "enum": [ - "queued", - "running", - "completed", - "failed" - ] - }, - "reason": { - "type": "string", - "enum": [ - "auto", - "manual" - ] - }, - "summary": { - "type": "string" - }, - "recent": { - "type": "string" - }, "id": { "type": "string", "allOf": [ @@ -12595,20 +12406,174 @@ "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", - "id", - "time" + "recent" ], "additionalProperties": false }, - "Session.Message": { + "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": [ + "failed" + ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + } + }, + "required": [ + "type", + "id", + "time", + "status", + "reason", + "error" + ], + "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": { "anyOf": [ { "$ref": "#/components/schemas/Session.Message.AgentSelected" @@ -12700,11 +12665,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -12787,11 +12750,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -12874,11 +12835,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -12964,11 +12923,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13051,11 +13008,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 2 ] } }, @@ -13134,11 +13089,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13234,11 +13187,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13326,11 +13277,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13430,11 +13379,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13513,11 +13460,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13596,11 +13541,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13683,11 +13626,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13775,11 +13716,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13862,11 +13801,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13955,11 +13892,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -13984,6 +13919,9 @@ } ] }, + "id": { + "type": "string" + }, "name": { "type": "string" }, @@ -13993,6 +13931,7 @@ }, "required": [ "sessionID", + "id", "name", "text" ], @@ -14008,7 +13947,7 @@ ], "additionalProperties": false }, - "Shell1": { + "Shell": { "type": "object", "properties": { "id": { @@ -14186,11 +14125,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14216,7 +14153,7 @@ ] }, "shell": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Shell" } }, "required": [ @@ -14273,11 +14210,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14303,7 +14238,7 @@ ] }, "shell": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Shell" }, "output": { "type": "object", @@ -14395,11 +14330,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14498,11 +14431,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14547,44 +14478,10 @@ ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" }, "snapshot": { "type": "string" @@ -14653,11 +14550,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14694,44 +14589,10 @@ "$ref": "#/components/schemas/Session.StructuredError" }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": [ @@ -14789,11 +14650,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14890,11 +14749,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -14998,11 +14855,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15105,11 +14960,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15213,11 +15066,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15313,11 +15164,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15416,11 +15265,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15523,11 +15370,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15633,11 +15478,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15682,12 +15525,6 @@ "$ref": "#/components/schemas/LLM.ToolContent" } }, - "outputPaths": { - "type": "array", - "items": { - "type": "string" - } - }, "result": {}, "executed": { "type": "boolean" @@ -15757,11 +15594,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15865,11 +15700,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -15979,11 +15812,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16071,11 +15902,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16106,11 +15935,23 @@ "auto", "manual" ] + }, + "recent": { + "type": "string" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, "required": [ "sessionID", - "reason" + "reason", + "recent" ], "additionalProperties": false } @@ -16162,11 +16003,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16261,11 +16100,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16289,10 +16126,30 @@ "pattern": "^ses" } ] + }, + "reason": { + "type": "string", + "enum": [ + "auto", + "manual" + ] + }, + "error": { + "$ref": "#/components/schemas/Session.StructuredError" + }, + "inputID": { + "type": "string", + "allOf": [ + { + "pattern": "^msg_" + } + ] } }, "required": [ - "sessionID" + "sessionID", + "reason", + "error" ], "additionalProperties": false } @@ -16344,11 +16201,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16374,7 +16229,7 @@ ] }, "revert": { - "$ref": "#/components/schemas/Revert.State" + "$ref": "#/components/schemas/Session.Revert" } }, "required": [ @@ -16431,11 +16286,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16514,11 +16367,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -16568,7 +16419,7 @@ ], "additionalProperties": false }, - "SessionDurableEvent": { + "Session.Event.Durable": { "oneOf": [ { "$ref": "#/components/schemas/session.agent.selected" @@ -16717,7 +16568,7 @@ "SessionLogItem": { "anyOf": [ { - "$ref": "#/components/schemas/SessionDurableEvent" + "$ref": "#/components/schemas/Session.Event.Durable" }, { "$ref": "#/components/schemas/EventLog.Synced" @@ -16737,7 +16588,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Session.Message" + "$ref": "#/components/schemas/Session.Message.Info" } }, "cursor": { @@ -16823,6 +16674,9 @@ ], "additionalProperties": false }, + "Money.USDPerMillionTokens": { + "type": "number" + }, "Model.Cost": { "type": "object", "properties": { @@ -16846,19 +16700,19 @@ "additionalProperties": false }, "input": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "output": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "cache": { "type": "object", "properties": { "read": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" }, "write": { - "type": "number" + "$ref": "#/components/schemas/Money.USDPerMillionTokens" } }, "required": [ @@ -16875,7 +16729,7 @@ ], "additionalProperties": false }, - "ModelV2.Info": { + "Model.Info": { "type": "object", "properties": { "id": { @@ -19285,7 +19139,7 @@ ], "additionalProperties": false }, - "CommandV2.Info": { + "Command.Info": { "type": "object", "properties": { "name": { @@ -19313,9 +19167,12 @@ ], "additionalProperties": false }, - "SkillV2.Info": { + "Skill.Info": { "type": "object", "properties": { + "id": { + "type": "string" + }, "name": { "type": "string" }, @@ -19336,6 +19193,7 @@ } }, "required": [ + "id", "name", "location", "content" @@ -19569,7 +19427,7 @@ ], "additionalProperties": false }, - "SnapshotFileDiff": { + "FileDiff.LegacyInfo": { "type": "object", "properties": { "file": { @@ -19633,7 +19491,7 @@ "$ref": "#/components/schemas/PermissionRule" } }, - "Session": { + "SessionV1.Info": { "type": "object", "properties": { "id": { @@ -19687,7 +19545,7 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.LegacyInfo" } } }, @@ -19902,11 +19760,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -19932,7 +19788,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, "required": [ @@ -19989,11 +19845,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -20019,7 +19873,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, "required": [ @@ -20076,11 +19930,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -20106,7 +19958,7 @@ ] }, "info": { - "$ref": "#/components/schemas/Session" + "$ref": "#/components/schemas/SessionV1.Info" } }, "required": [ @@ -20268,7 +20120,7 @@ "diffs": { "type": "array", "items": { - "$ref": "#/components/schemas/SnapshotFileDiff" + "$ref": "#/components/schemas/FileDiff.LegacyInfo" } } }, @@ -20936,11 +20788,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -21023,11 +20873,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -22493,11 +22341,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -22584,11 +22430,9 @@ ] }, "version": { - "type": "integer", - "allOf": [ - { - "minimum": 1 - } + "type": "number", + "enum": [ + 1 ] } }, @@ -22685,44 +22529,10 @@ ] }, "cost": { - "type": "number" + "$ref": "#/components/schemas/Money.USD" }, "tokens": { - "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 + "$ref": "#/components/schemas/TokenUsage.Info" } }, "required": [ @@ -23836,7 +23646,7 @@ "type": "object", "properties": { "info": { - "$ref": "#/components/schemas/Shell1" + "$ref": "#/components/schemas/Shell" } }, "required": [ @@ -26794,6 +26604,182 @@ ], "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": { diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index 781bcd5cbeb..ce429323d88 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,4 +1,4 @@ -import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import { FileDiffInfo, Message, Model, Part, Session } 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(), + data: z.custom(), }), z.object({ type: z.literal("model"), diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 8c8ac59f7ab..91cb50891de 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -1,4 +1,4 @@ -import { Message, Model, Part, Session, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" +import { FileDiffInfo, Message, Model, Part, Session, SessionStatus, 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]: SnapshotFileDiff[] + [sessionID: string]: FileDiffInfo[] } session_status: { [sessionID: string]: SessionStatus diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 641ae2ddf8c..2ce86abe43c 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -64,7 +64,7 @@ export function fromRow(row: SessionRow): Info { messageID: MessageID.make(row.revert.messageID), partID: row.revert.partID ? PartID.make(row.revert.partID) : undefined, snapshot: row.revert.snapshot, - diff: row.revert.diff, + diff: "diff" in row.revert ? row.revert.diff : undefined, } : undefined return { diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 60112e10d96..a42c367ec2c 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -51,7 +51,7 @@ type State = { type Data = | { type: "session" - data: SDK.Session + data: SDK.SessionV1Info } | { type: "message" diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 4da9bc3ca86..f393caa1a7d 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -9,7 +9,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { Hash } from "@opencode-ai/core/util/hash" import { Config } from "@/config/config" import { Global } from "@opencode-ai/core/global" -import { Info } from "@opencode-ai/schema/file-diff" +import { LegacyInfo } from "@opencode-ai/schema/file-diff" export const Patch = Schema.Struct({ hash: Schema.String, @@ -17,7 +17,7 @@ export const Patch = Schema.Struct({ }) export type Patch = typeof Patch.Type -export const FileDiff = Info +export const FileDiff = LegacyInfo export type FileDiff = typeof FileDiff.Type const prune = "7.days" diff --git a/packages/opencode/test/cli/run/stream-v2.transport.test.ts b/packages/opencode/test/cli/run/stream-v2.transport.test.ts index be248d3a3dc..5d0894c0d79 100644 --- a/packages/opencode/test/cli/run/stream-v2.transport.test.ts +++ b/packages/opencode/test/cli/run/stream-v2.transport.test.ts @@ -53,7 +53,13 @@ function connected(id = "evt_connected") { return { id, type: "server.connected", data: {} } satisfies RunV2Event } -function durable(sessionID: string, seq = 0, version = 1) { +function durable(sessionID: string, seq?: number): { aggregateID: string; seq: number; version: 1 } +function durable( + sessionID: string, + seq: number, + version: Version, +): { aggregateID: string; seq: number; version: Version } +function durable(sessionID: string, seq = 0, version: 1 | 2 = 1) { return { aggregateID: sessionID, seq, version } } @@ -1311,17 +1317,10 @@ describe("V2 mini transport", () => { { id: "msg_shell", type: "shell" as const, - shell: { - id: "sh_1", - status: "exited", - command: "ls", - cwd: "/tmp", - shell: "/bin/sh", - file: "/tmp/opencode-shell", - exit: 0, - metadata: {}, - time: { started: 0, completed: 1 }, - }, + shellID: "sh_1", + status: "exited", + command: "ls", + exit: 0, output: { output: "file.txt", cursor: 8, size: 8, truncated: false }, time: { created: 1, completed: 2 }, }, @@ -1378,17 +1377,10 @@ describe("V2 mini transport", () => { { id: "msg_failed_shell", type: "shell" as const, - shell: { - id: "sh_failed", - status: "exited", - command: "false", - cwd: "/tmp", - shell: "/bin/sh", - file: "/tmp/failed", - exit: 7, - metadata: {}, - time: { started: 0, completed: 1 }, - }, + shellID: "sh_failed", + status: "exited", + command: "false", + exit: 7, output: { output: "failure output", cursor: 14, size: 14, truncated: false }, time: { created: 1, completed: 2 }, }, @@ -1551,6 +1543,7 @@ describe("V2 mini transport", () => { durable: durable("ses_1"), data: { sessionID: "ses_1", + id: input.skill ?? "tigerstyle", name: input.skill ?? "tigerstyle", text: "skill instructions", }, @@ -1633,6 +1626,7 @@ describe("V2 mini transport", () => { durable: durable("ses_1"), data: { sessionID: "ses_1", + id: "other", name: "other", text: "other instructions", }, @@ -1655,6 +1649,7 @@ describe("V2 mini transport", () => { durable: durable("ses_1"), data: { sessionID: "ses_1", + id: "tigerstyle", name: "tigerstyle", text: "skill instructions", }, @@ -1722,6 +1717,7 @@ describe("V2 mini transport", () => { { id: "msg_skill", type: "skill" as const, + skill: "tigerstyle", name: "tigerstyle", text: "skill instructions", time: { created: 2 }, @@ -1745,6 +1741,7 @@ describe("V2 mini transport", () => { durable: durable("ses_1"), data: { sessionID: "ses_1", + id: "tigerstyle", name: "tigerstyle", text: "skill instructions", }, diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 9d7643cb330..a5677d5274e 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -28,6 +28,7 @@ import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from ". import { Database } from "@opencode-ai/core/database/database" import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql" import { SessionMessage } from "@opencode-ai/core/session/message" +import { Agent } from "@opencode-ai/schema/agent" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" import * as DateTime from "effect/DateTime" @@ -76,7 +77,7 @@ function createTextMessage(sessionID: SessionIDType, text: string) { id: MessageID.ascending(), role: "user", sessionID, - agent: "build", + agent: Agent.ID.make("build"), model: { providerID: ProviderV2.ID.make("test"), modelID: ModelV2.ID.make("test") }, time: { created: Date.now() }, }) @@ -123,7 +124,7 @@ const insertLegacyAssistantMessage = (sessionID: SessionIDType, seq = 1, time = const message = SessionMessage.Assistant.make({ id: SessionMessage.ID.create(), type: "assistant", - agent: "build", + agent: Agent.ID.make("build"), model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider"), @@ -380,7 +381,7 @@ describe("session HttpApi", () => { yield* insertLegacyAssistantMessage(parent.id) expect( - (yield* requestJson<{ data: SessionMessage.Message[] }>(`/api/session/${parent.id}/message`, { + (yield* requestJson<{ data: SessionMessage.Info[] }>(`/api/session/${parent.id}/message`, { headers, })).data, ).toMatchObject([{ type: "assistant" }]) @@ -474,7 +475,7 @@ describe("session HttpApi", () => { }) const messagePage = yield* request(`/api/session/${session.id}/message?limit=1`, { headers }) - const messageBody = yield* json<{ data: SessionMessage.Message[]; cursor: { next?: string } }>(messagePage) + const messageBody = yield* json<{ data: SessionMessage.Info[]; cursor: { next?: string } }>(messagePage) const messageCursor = messageBody.cursor.next expect(messageCursor).toBeTruthy() expect(messageBody.data.map((message) => message.id)).toEqual([secondMessage.id]) @@ -488,7 +489,7 @@ describe("session HttpApi", () => { headers, }) expect( - (yield* json<{ data: SessionMessage.Message[] }>(nextMessagePage)).data.map((message) => message.id), + (yield* json<{ data: SessionMessage.Info[] }>(nextMessagePage)).data.map((message) => message.id), ).toEqual([firstMessage.id]) const legacyMessageCursor = Buffer.from( @@ -498,7 +499,7 @@ describe("session HttpApi", () => { headers, }) expect( - (yield* json<{ data: SessionMessage.Message[] }>(legacyMessagePage)).data.map((message) => message.id), + (yield* json<{ data: SessionMessage.Info[] }>(legacyMessagePage)).data.map((message) => message.id), ).toEqual([firstMessage.id]) const messageCursorWithOrder = yield* request( @@ -625,7 +626,7 @@ describe("session HttpApi", () => { }) expect(wake.status).toBe(200) const message = yield* pollWithTimeout( - requestJson<{ data: SessionMessage.Message[] }>(`/api/session/${session.id}/message`, { headers }).pipe( + requestJson<{ data: SessionMessage.Info[] }>(`/api/session/${session.id}/message`, { headers }).pipe( Effect.map(({ data }) => data.find((message) => message.id === wakeID)), ), "V2 prompt was not promoted after wake", @@ -637,28 +638,25 @@ describe("session HttpApi", () => { ) it.instance( - "returns v2 public unavailable errors for unfinished session mutations", + "supports current session compact and wait endpoints", () => Effect.gen(function* () { const test = yield* TestInstance const headers = { "x-opencode-directory": test.directory } const session = yield* createSession({ title: "v2 unavailable" }) - const compact = yield* request(`/api/session/${session.id}/compact`, { method: "POST", headers }) - expect(compact.status).toBe(503) - expect(yield* responseJson(compact)).toEqual({ - _tag: "ServiceUnavailableError", - message: "Session compact is not available yet", - service: "session.compact", + const compact = yield* request(`/api/session/${session.id}/compact`, { + method: "POST", + headers: { ...headers, "content-type": "application/json" }, + body: JSON.stringify({}), + }) + expect(compact.status).toBe(200) + expect(yield* responseJson(compact)).toMatchObject({ + data: { type: "compaction", sessionID: session.id }, }) const wait = yield* request(`/api/session/${session.id}/wait`, { method: "POST", headers }) - expect(wait.status).toBe(503) - expect(yield* responseJson(wait)).toEqual({ - _tag: "ServiceUnavailableError", - message: "Session wait is not available yet", - service: "session.wait", - }) + expect(wait.status).toBe(204) }), { git: true, config: { formatter: false, lsp: false } }, ) diff --git a/packages/opencode/test/v2/session-message-updater.test.ts b/packages/opencode/test/v2/session-message-updater.test.ts index 8f8286d5628..d29a7398f46 100644 --- a/packages/opencode/test/v2/session-message-updater.test.ts +++ b/packages/opencode/test/v2/session-message-updater.test.ts @@ -1,6 +1,5 @@ import { expect, test } from "bun:test" -import { Effect } from "effect" -import * as DateTime from "effect/DateTime" +import { DateTime, Effect } from "effect" import { SessionID } from "../../src/session/schema" import { EventV2 } from "@opencode-ai/core/event" import { ModelV2 } from "@opencode-ai/core/model" @@ -8,6 +7,9 @@ import { ProviderV2 } from "@opencode-ai/core/provider" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessageUpdater } from "@opencode-ai/core/session/message-updater" import { SessionMessage } from "@opencode-ai/core/session/message" +import { Agent } from "@opencode-ai/schema/agent" +import { Money } from "@opencode-ai/schema/money" +import { Snapshot } from "@opencode-ai/schema/snapshot" function durable(sessionID: SessionID, seq = 0, version = 1) { return { aggregateID: sessionID, seq: EventV2.Seq.make(seq), version: EventV2.Version.make(version) } @@ -27,13 +29,13 @@ test.skip("step snapshots carry over to assistant messages", () => { data: { sessionID, assistantMessageID, - agent: "build", + agent: Agent.ID.make("build"), model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider"), variant: ModelV2.VariantID.make("default"), }, - snapshot: "before", + snapshot: Snapshot.ID.make("before"), }, } satisfies SessionEvent.Event), ) @@ -50,21 +52,24 @@ test.skip("step snapshots carry over to assistant messages", () => { sessionID, assistantMessageID, finish: "stop", - cost: 0, + cost: Money.USD.zero, tokens: { input: 1, output: 2, reasoning: 0, cache: { read: 0, write: 0 }, }, - snapshot: "after", + snapshot: Snapshot.ID.make("after"), }, } satisfies SessionEvent.Event), ) expect(state.messages[0]?.type).toBe("assistant") if (state.messages[0]?.type !== "assistant") return - expect(state.messages[0].snapshot).toEqual({ start: "before", end: "after" }) + expect(state.messages[0].snapshot).toEqual({ + start: Snapshot.ID.make("before"), + end: Snapshot.ID.make("after"), + }) expect(state.messages[0].finish).toBe("stop") }) @@ -82,7 +87,7 @@ test.skip("text ended populates assistant text content", () => { data: { sessionID, assistantMessageID, - agent: "build", + agent: Agent.ID.make("build"), model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider"), @@ -141,7 +146,7 @@ test.skip("tool completion stores completed timestamp", () => { data: { sessionID, assistantMessageID, - agent: "build", + agent: Agent.ID.make("build"), model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider"), @@ -213,7 +218,7 @@ test.skip("tool completion stores completed timestamp", () => { }) }) -test("compaction events reduce to compaction message only when completed", () => { +test("compaction events reduce to a compaction message through completion", () => { const state: SessionMessageUpdater.MemoryState = { messages: [] } const sessionID = SessionID.make("session") const id = EventV2.ID.create() @@ -224,15 +229,25 @@ test("compaction events reduce to compaction message only when completed", () => id, created: DateTime.makeUnsafe(0), type: "session.compaction.started", - durable: durable(sessionID), + durable: durable(sessionID, 0, 2), data: { sessionID, reason: "auto", + recent: "recent context", }, } satisfies SessionEvent.Event), ) - expect(state.messages).toEqual([]) + expect(state.messages).toMatchObject([ + { + id: SessionMessage.ID.fromEvent(id), + type: "compaction", + reason: "auto", + recent: "recent context", + status: "running", + summary: "", + }, + ]) Effect.runSync( SessionMessageUpdater.update(SessionMessageUpdater.memory(state), { @@ -263,7 +278,7 @@ test("compaction events reduce to compaction message only when completed", () => id: endedID, created: DateTime.makeUnsafe(0), type: "session.compaction.ended", - durable: durable(sessionID, 1), + durable: durable(sessionID, 3), data: { sessionID, reason: "auto", @@ -275,9 +290,10 @@ test("compaction events reduce to compaction message only when completed", () => expect(state.messages).toHaveLength(1) expect(state.messages[0]).toMatchObject({ - id: SessionMessage.ID.fromEvent(endedID), + id: SessionMessage.ID.fromEvent(id), type: "compaction", reason: "auto", + status: "completed", summary: "final summary", recent: "recent context", time: { created: DateTime.makeUnsafe(0) }, diff --git a/packages/plugin/src/v2/effect/agent.ts b/packages/plugin/src/v2/effect/agent.ts index 80932c5e9ef..d5125cca2a1 100644 --- a/packages/plugin/src/v2/effect/agent.ts +++ b/packages/plugin/src/v2/effect/agent.ts @@ -1,13 +1,13 @@ import type { AgentApi } from "@opencode-ai/client/effect/api" -import type { AgentV2Info } from "@opencode-ai/sdk/v2/types" +import type { AgentInfo } from "@opencode-ai/sdk/v2/types" import type { Effect } from "effect" import type { Transform } from "./registration.js" export interface AgentDraft { - list(): readonly AgentV2Info[] - get(id: string): AgentV2Info | undefined + list(): readonly AgentInfo[] + get(id: string): AgentInfo | undefined default(id: string | undefined): void - update(id: string, update: (agent: AgentV2Info) => void): void + update(id: string, update: (agent: AgentInfo) => void): void remove(id: string): void } diff --git a/packages/plugin/src/v2/effect/catalog.ts b/packages/plugin/src/v2/effect/catalog.ts index 0792d5f39c9..b96ceec4a83 100644 --- a/packages/plugin/src/v2/effect/catalog.ts +++ b/packages/plugin/src/v2/effect/catalog.ts @@ -1,11 +1,11 @@ -import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types" +import type { ModelInfo, ProviderV2Info } from "@opencode-ai/sdk/v2/types" import type { CatalogApi } from "@opencode-ai/client/effect/api" import type { Effect } from "effect" import type { Transform } from "./registration.js" export interface CatalogProviderRecord { readonly provider: ProviderV2Info - readonly models: ReadonlyMap + readonly models: ReadonlyMap } export interface CatalogDraft { @@ -16,8 +16,8 @@ export interface CatalogDraft { remove(providerID: string): void } readonly model: { - get(providerID: string, modelID: string): ModelV2Info | undefined - update(providerID: string, modelID: string, update: (model: ModelV2Info) => void): void + get(providerID: string, modelID: string): ModelInfo | undefined + update(providerID: string, modelID: string, update: (model: ModelInfo) => void): void remove(providerID: string, modelID: string): void readonly default: { get(): { providerID: string; modelID: string } | undefined diff --git a/packages/plugin/src/v2/effect/command.ts b/packages/plugin/src/v2/effect/command.ts index bd9ebfa9195..6e4764a4855 100644 --- a/packages/plugin/src/v2/effect/command.ts +++ b/packages/plugin/src/v2/effect/command.ts @@ -1,12 +1,12 @@ -import type { CommandV2Info } from "@opencode-ai/sdk/v2/types" +import type { CommandInfo } from "@opencode-ai/sdk/v2/types" import type { CommandApi } from "@opencode-ai/client/effect/api" import type { Effect } from "effect" import type { Transform } from "./registration.js" export interface CommandDraft { - list(): readonly CommandV2Info[] - get(name: string): CommandV2Info | undefined - update(name: string, update: (command: CommandV2Info) => void): void + list(): readonly CommandInfo[] + get(name: string): CommandInfo | undefined + update(name: string, update: (command: CommandInfo) => void): void remove(name: string): void } diff --git a/packages/plugin/src/v2/effect/skill.ts b/packages/plugin/src/v2/effect/skill.ts index 4437e846e3a..32daf0fb0a5 100644 --- a/packages/plugin/src/v2/effect/skill.ts +++ b/packages/plugin/src/v2/effect/skill.ts @@ -1,11 +1,11 @@ -import type { SkillV2Source } from "@opencode-ai/sdk/v2/types" +import type { SkillSource } from "@opencode-ai/sdk/v2/types" import type { SkillApi } from "@opencode-ai/client/effect/api" import type { Effect } from "effect" import type { Transform } from "./registration.js" export interface SkillDraft { - source(source: SkillV2Source): void - list(): readonly SkillV2Source[] + source(source: SkillSource): void + list(): readonly SkillSource[] } export interface SkillDomain extends SkillApi { diff --git a/packages/protocol/src/errors.ts b/packages/protocol/src/errors.ts index a21d3d86d25..d6d1482f301 100644 --- a/packages/protocol/src/errors.ts +++ b/packages/protocol/src/errors.ts @@ -1,4 +1,5 @@ import { Schema } from "effect" +import { Skill } from "@opencode-ai/schema/skill" export class InvalidRequestError extends Schema.TaggedErrorClass()( "InvalidRequestError", @@ -83,7 +84,7 @@ export class MessageNotFoundError extends Schema.TaggedErrorClass()( "SkillNotFoundError", { - skill: Schema.String, + skill: Skill.ID, message: Schema.String, }, { httpApiStatus: 404 }, diff --git a/packages/protocol/src/groups/message.ts b/packages/protocol/src/groups/message.ts index cab0d8bb0f6..5b47146316d 100644 --- a/packages/protocol/src/groups/message.ts +++ b/packages/protocol/src/groups/message.ts @@ -27,7 +27,7 @@ export const MessageGroup = HttpApiGroup.make("server.message") params: { sessionID: Session.ID }, query: SessionMessagesQuery, success: Schema.Struct({ - data: Schema.Array(SessionMessage.Message), + data: Schema.Array(SessionMessage.Info), cursor: Schema.Struct({ previous: Schema.String.pipe(Schema.optional), next: Schema.String.pipe(Schema.optional), diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index eac40a4c221..e595716873a 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -23,9 +23,9 @@ import { UnknownError, } from "../errors.js" import { Agent } from "@opencode-ai/schema/agent" +import { Skill } from "@opencode-ai/schema/skill" import { Model } from "@opencode-ai/schema/model" import { Location } from "@opencode-ai/schema/location" -import { Revert } from "@opencode-ai/schema/revert" import { SessionEvent } from "@opencode-ai/schema/session-event" import { EventLog } from "@opencode-ai/schema/event-log" @@ -316,7 +316,7 @@ export const makeSessionGroup = (sessionLo id: SessionMessage.ID.pipe(Schema.optional), command: Schema.String, arguments: Schema.String.pipe(Schema.optional), - agent: Schema.String.pipe(Schema.optional), + agent: Agent.ID.pipe(Schema.optional), model: Model.Ref.pipe(Schema.optional), files: PromptInput.Prompt.fields.files, agents: PromptInput.Prompt.fields.agents, @@ -341,7 +341,7 @@ export const makeSessionGroup = (sessionLo params: { sessionID: Session.ID }, payload: Schema.Struct({ id: SessionMessage.ID.pipe(Schema.optional), - skill: Schema.String, + skill: Skill.ID, resume: Schema.Boolean.pipe(Schema.optional), }), success: HttpApiSchema.NoContent, @@ -432,7 +432,7 @@ export const makeSessionGroup = (sessionLo HttpApiEndpoint.post("session.revert.stage", "/api/session/:sessionID/revert/stage", { params: { sessionID: Session.ID }, payload: Schema.Struct({ messageID: SessionMessage.ID, files: Schema.Boolean.pipe(Schema.optional) }), - success: Schema.Struct({ data: Revert.State }), + success: Schema.Struct({ data: Session.Revert }), error: [MessageNotFoundError, SessionNotFoundError, SessionBusyError, UnknownError], }) .middleware(sessionLocationMiddleware) @@ -467,7 +467,7 @@ export const makeSessionGroup = (sessionLo .add( HttpApiEndpoint.get("session.context", "/api/session/:sessionID/context", { params: { sessionID: Session.ID }, - success: Schema.Struct({ data: Schema.Array(SessionMessage.Message) }), + success: Schema.Struct({ data: Schema.Array(SessionMessage.Info) }), error: [SessionNotFoundError, UnknownError], }) .middleware(sessionLocationMiddleware) @@ -583,7 +583,7 @@ export const makeSessionGroup = (sessionLo .add( HttpApiEndpoint.get("session.message", "/api/session/:sessionID/message/:messageID", { params: { sessionID: Session.ID, messageID: SessionMessage.ID }, - success: Schema.Struct({ data: SessionMessage.Message }), + success: Schema.Struct({ data: SessionMessage.Info }), error: [SessionNotFoundError, MessageNotFoundError], }) .middleware(sessionLocationMiddleware) diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index eb84cf500be..81bdfe8c981 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -10,9 +10,12 @@ import { PositiveInt, statics } from "./schema.js" const Updated = ephemeral({ type: "agent.updated", schema: {} }) -export const ID = Schema.String.pipe(Schema.brand("AgentV2.ID")) +export const ID = Schema.String.pipe(Schema.brand("Agent.ID")) export type ID = typeof ID.Type +export const Name = Schema.String.pipe(Schema.brand("Agent.Name")) +export type Name = typeof Name.Type + export const Color = Schema.Union([ Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), @@ -22,6 +25,7 @@ export type Color = typeof Color.Type export interface Info extends Schema.Schema.Type {} export const Info = Schema.Struct({ id: ID, + name: Name, model: Model.Ref.pipe(optional), request: Provider.Request, system: Schema.String.pipe(optional), @@ -32,12 +36,13 @@ export const Info = Schema.Struct({ steps: PositiveInt.pipe(optional), permissions: Permission.Ruleset, }) - .annotate({ identifier: "AgentV2.Info" }) + .annotate({ identifier: "Agent.Info" }) .pipe( statics((schema) => ({ empty: (id: ID) => schema.make({ id, + name: Name.make(id), request: { settings: {}, headers: {}, body: {} }, mode: "all", hidden: false, diff --git a/packages/schema/src/command.ts b/packages/schema/src/command.ts index 81e37157c6a..ef32acb8211 100644 --- a/packages/schema/src/command.ts +++ b/packages/schema/src/command.ts @@ -4,6 +4,7 @@ import { Schema } from "effect" import { ephemeral, inventory } from "./event.js" import { optional } from "./schema.js" import { Model } from "./model.js" +import { Agent } from "./agent.js" const Updated = ephemeral({ type: "command.updated", schema: {} }) @@ -12,10 +13,10 @@ export const Info = Schema.Struct({ name: Schema.String, template: Schema.String, description: Schema.String.pipe(optional), - agent: Schema.String.pipe(optional), + agent: Agent.ID.pipe(optional), model: Model.Ref.pipe(optional), subtask: Schema.Boolean.pipe(optional), -}).annotate({ identifier: "CommandV2.Info" }) +}).annotate({ identifier: "Command.Info" }) export const Event = { Updated, diff --git a/packages/schema/src/event.ts b/packages/schema/src/event.ts index 34e44958073..e4f2f9b8139 100644 --- a/packages/schema/src/event.ts +++ b/packages/schema/src/event.ts @@ -1,6 +1,6 @@ export * as Event from "./event.js" -import { Schema } from "effect" +import { Schema, SchemaTransformation } from "effect" import { optional } from "./schema.js" import { ascending } from "./identifier.js" import { Location } from "./location.js" @@ -72,6 +72,7 @@ export type Payload = D extends DurableDefini type Input>>> = { readonly type: Type + readonly identifier?: string readonly durable?: { readonly version: number readonly aggregate: string @@ -84,16 +85,29 @@ export function durable< const Fields extends Readonly>>, >(input: Input & { readonly durable: NonNullable["durable"]> }) { const data = Schema.Struct(input.schema) + const durable = Schema.Struct({ + aggregateID: DurableEnvelope.fields.aggregateID, + seq: DurableEnvelope.fields.seq, + version: Schema.Literal(input.durable.version).pipe( + Schema.decodeTo( + Schema.toType(Version), + SchemaTransformation.transform({ + decode: () => Version.make(input.durable.version), + encode: () => input.durable.version, + }), + ), + ), + }) return Schema.Struct({ id: ID, created: DateTimeUtcFromMillis, metadata: optional(Schema.Record(Schema.String, Schema.Unknown)), type: Schema.Literal(input.type), - durable: DurableEnvelope, + durable, location: optional(Location.Ref), data, }) - .annotate({ identifier: input.type }) + .annotate({ identifier: input.identifier ?? input.type }) .pipe( statics(() => ({ type: input.type, @@ -117,7 +131,7 @@ export function ephemeral< location: optional(Location.Ref), data, }) - .annotate({ identifier: input.type }) + .annotate({ identifier: input.identifier ?? input.type }) .pipe( statics(() => ({ type: input.type, diff --git a/packages/schema/src/file-diff.ts b/packages/schema/src/file-diff.ts index 9847a10574b..ff467c58e46 100644 --- a/packages/schema/src/file-diff.ts +++ b/packages/schema/src/file-diff.ts @@ -1,13 +1,23 @@ export * as FileDiff from "./file-diff.js" import { Schema } from "effect" -import { optional } from "./schema.js" +import { NonNegativeInt, optional } from "./schema.js" export const Info = Schema.Struct({ - file: optional(Schema.String), - patch: optional(Schema.String), + file: Schema.String, + patch: Schema.String, + additions: NonNegativeInt, + deletions: NonNegativeInt, + status: Schema.Literals(["added", "deleted", "modified"]), +}).annotate({ identifier: "FileDiff.Info" }) +export interface Info extends Schema.Schema.Type {} + +/** V1 snapshot and persisted session diff shape. */ +export const LegacyInfo = Schema.Struct({ + file: Schema.String.pipe(optional), + patch: Schema.String.pipe(optional), additions: Schema.Finite, deletions: Schema.Finite, - status: optional(Schema.Literals(["added", "deleted", "modified"])), -}).annotate({ identifier: "SnapshotFileDiff" }) -export interface Info extends Schema.Schema.Type {} + status: Schema.Literals(["added", "deleted", "modified"]).pipe(optional), +}).annotate({ identifier: "FileDiff.LegacyInfo" }) +export interface LegacyInfo extends Schema.Schema.Type {} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 4e77cc4151a..56bf6fa3c50 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -11,20 +11,22 @@ export { LLM } from "./llm.js" export { Location } from "./location.js" export { Mcp } from "./mcp.js" export { Model } from "./model.js" +export { Money } from "./money.js" export { Permission } from "./permission.js" export { PermissionSaved } from "./permission-saved.js" export { Project } from "./project.js" export { ProjectCopy } from "./project-copy.js" export { Provider } from "./provider.js" export { Reference } from "./reference.js" -export { Revert } from "./revert.js" export { Session } from "./session.js" export { Vcs } from "./vcs.js" export { SessionInput } from "./session-input.js" export { SessionError } from "./session-error.js" export { SessionMessage } from "./session-message.js" +export { Snapshot } from "./snapshot.js" export { Shell } from "./shell.js" export { Skill } from "./skill.js" +export { TokenUsage } from "./token-usage.js" export { Pty } from "./pty.js" export { PtyTicket } from "./pty-ticket.js" export { Question } from "./question.js" diff --git a/packages/schema/src/model.ts b/packages/schema/src/model.ts index fc6b64712f3..3539c87f770 100644 --- a/packages/schema/src/model.ts +++ b/packages/schema/src/model.ts @@ -3,11 +3,12 @@ export * as Model from "./model.js" import { Schema } from "effect" import { optional, statics } from "./schema.js" import { Provider } from "./provider.js" +import { Money } from "./money.js" -export const ID = Schema.String.pipe(Schema.brand("ModelV2.ID")) +export const ID = Schema.String.pipe(Schema.brand("Model.ID")) export type ID = typeof ID.Type -export const VariantID = Schema.String.pipe(Schema.brand("VariantID")) +export const VariantID = Schema.String.pipe(Schema.brand("Model.VariantID")) export type VariantID = typeof VariantID.Type export const Ref = Schema.Struct({ @@ -17,7 +18,7 @@ export const Ref = Schema.Struct({ }).annotate({ identifier: "Model.Ref" }) export interface Ref extends Schema.Schema.Type {} -export const Family = Schema.String.pipe(Schema.brand("Family")) +export const Family = Schema.String.pipe(Schema.brand("Model.Family")) export type Family = typeof Family.Type export interface Capabilities extends Schema.Schema.Type {} @@ -30,14 +31,14 @@ export const Capabilities = Schema.Struct({ export interface Cost extends Schema.Schema.Type {} export const Cost = Schema.Struct({ tier: Schema.Struct({ - type: Schema.Literal("context"), + type: Schema.tag("context"), size: Schema.Int, }).pipe(optional), - input: Schema.Finite, - output: Schema.Finite, + input: Money.USDPerMillionTokens, + output: Money.USDPerMillionTokens, cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, + read: Money.USDPerMillionTokens, + write: Money.USDPerMillionTokens, }), }).annotate({ identifier: "Model.Cost" }) @@ -70,7 +71,7 @@ export const Info = Schema.Struct({ output: Schema.Int, }), }) - .annotate({ identifier: "ModelV2.Info" }) + .annotate({ identifier: "Model.Info" }) .pipe( statics((schema) => ({ empty: (providerID: Provider.ID, id: ID) => diff --git a/packages/schema/src/money.ts b/packages/schema/src/money.ts new file mode 100644 index 00000000000..f21a390be82 --- /dev/null +++ b/packages/schema/src/money.ts @@ -0,0 +1,18 @@ +export * as Money from "./money.js" + +import { Schema } from "effect" +import { statics } from "./schema.js" + +export const USD = Schema.Finite.pipe( + Schema.brand("Money.USD"), + Schema.annotate({ identifier: "Money.USD" }), + statics((schema) => ({ zero: schema.make(0) })), +) +export type USD = typeof USD.Type + +export const USDPerMillionTokens = Schema.Finite.pipe( + Schema.brand("Money.USDPerMillionTokens"), + Schema.annotate({ identifier: "Money.USDPerMillionTokens" }), + statics((schema) => ({ zero: schema.make(0) })), +) +export type USDPerMillionTokens = typeof USDPerMillionTokens.Type diff --git a/packages/schema/src/revert.ts b/packages/schema/src/revert.ts deleted file mode 100644 index ab211a60f32..00000000000 --- a/packages/schema/src/revert.ts +++ /dev/null @@ -1,24 +0,0 @@ -export * as Revert from "./revert.js" - -import { Schema } from "effect" -import { optional } from "./schema.js" -import { NonNegativeInt, RelativePath } from "./schema.js" -import { SessionMessage } from "./session-message.js" - -export const FileDiff = Schema.Struct({ - path: RelativePath, - status: Schema.Literals(["added", "modified", "deleted"]), - additions: NonNegativeInt, - deletions: NonNegativeInt, - patch: Schema.String, -}).annotate({ identifier: "File.Diff" }) -export interface FileDiff extends Schema.Schema.Type {} - -export const State = Schema.Struct({ - messageID: SessionMessage.ID, - partID: Schema.String.pipe(optional), - snapshot: Schema.String.pipe(optional), - diff: Schema.String.pipe(optional), - files: Schema.Array(FileDiff).pipe(optional), -}).annotate({ identifier: "Revert.State" }) -export interface State extends Schema.Schema.Type {} diff --git a/packages/schema/src/session-event.ts b/packages/schema/src/session-event.ts index 9f98b352268..9c2417e0537 100644 --- a/packages/schema/src/session-event.ts +++ b/packages/schema/src/session-event.ts @@ -12,9 +12,14 @@ import { FileAttachment, Prompt } from "./prompt.js" import { SessionID } from "./session-id.js" import { Location } from "./location.js" import { SessionMessage } from "./session-message.js" -import { Revert } from "./revert.js" +import { Revert } from "./session-revert.js" import { Shell as ShellSchema } from "./shell.js" import { SessionError } from "./session-error.js" +import { Agent } from "./agent.js" +import { Skill as SkillSchema } from "./skill.js" +import { Money } from "./money.js" +import { Snapshot } from "./snapshot.js" +import { TokenUsage } from "./token-usage.js" export { FileAttachment } @@ -23,22 +28,13 @@ export const Source = Schema.Struct({ end: NonNegativeInt, text: Schema.String, }).annotate({ - identifier: "session.event.source", + identifier: "Session.Event.Source", }) export interface Source extends Schema.Schema.Type {} const Base = { sessionID: SessionID, } -const Tokens = Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), -}) const PromptFields = { ...Base, inputID: SessionMessage.ID, @@ -57,7 +53,7 @@ export const AgentSelected = Event.durable({ ...options, schema: { ...Base, - agent: Schema.String, + agent: Agent.ID, }, }) export type AgentSelected = typeof AgentSelected.Type @@ -97,8 +93,8 @@ export const UsageUpdated = Event.ephemeral({ type: "session.usage.updated", schema: { ...Base, - cost: Schema.Finite, - tokens: Tokens, + cost: Money.USD, + tokens: TokenUsage.Info, }, }) export type UsageUpdated = typeof UsageUpdated.Type @@ -191,7 +187,8 @@ export namespace Skill { ...options, schema: { ...Base, - name: Schema.String, + id: SkillSchema.ID, + name: SkillSchema.Name, text: Schema.String, }, }) @@ -228,9 +225,9 @@ export namespace Step { schema: { ...Base, assistantMessageID: SessionMessage.ID, - agent: Schema.String, + agent: Agent.ID, model: Model.Ref, - snapshot: Schema.String.pipe(optional), + snapshot: Snapshot.ID.pipe(optional), }, }) export type Started = typeof Started.Type @@ -242,9 +239,9 @@ export namespace Step { ...Base, assistantMessageID: SessionMessage.ID, finish: FinishReason, - cost: Schema.Finite, - tokens: Tokens, - snapshot: Schema.String.pipe(optional), + cost: Money.USD, + tokens: TokenUsage.Info, + snapshot: Snapshot.ID.pipe(optional), files: Schema.Array(RelativePath).pipe(optional), }, }) @@ -257,8 +254,8 @@ export namespace Step { ...Base, assistantMessageID: SessionMessage.ID, error: SessionError.Error, - cost: Schema.Finite.pipe(optional), - tokens: Tokens.pipe(optional), + cost: Money.USD.pipe(optional), + tokens: TokenUsage.Info.pipe(optional), }, }) export type Failed = typeof Failed.Type @@ -413,7 +410,6 @@ export namespace Tool { ...ToolBase, structured: Schema.Record(Schema.String, Schema.Unknown), content: Schema.Array(ToolContent), - outputPaths: Schema.Array(Schema.String).pipe(optional), result: Schema.Unknown.pipe(optional), executed: Schema.Boolean, resultState: SessionMessage.ProviderState.pipe(optional), @@ -464,7 +460,9 @@ export namespace Compaction { ...options, schema: { ...Base, - reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]), + reason: Schema.Literals(["auto", "manual"]), + recent: Schema.String, + inputID: SessionMessage.ID.pipe(optional), }, }) export type Started = typeof Started.Type @@ -493,7 +491,12 @@ export namespace Compaction { export const Failed = Event.durable({ type: "session.compaction.failed", ...options, - schema: Base, + schema: { + ...Base, + reason: Started.data.fields.reason, + error: SessionError.Error, + inputID: SessionMessage.ID.pipe(optional), + }, }) export type Failed = typeof Failed.Type } @@ -502,7 +505,7 @@ export namespace RevertEvent { export const Staged = Event.durable({ type: "session.revert.staged", ...options, - schema: { ...Base, revert: Revert.State }, + schema: { ...Base, revert: Revert }, }) export const Cleared = Event.durable({ type: "session.revert.cleared", ...options, schema: Base }) export const Committed = Event.durable({ @@ -564,7 +567,7 @@ export const DurableDefinitions = Event.inventory( export const Durable = Schema.Union(DurableDefinitions, { mode: "oneOf" }) .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "SessionDurableEvent" }) + .annotate({ identifier: "Session.Event.Durable" }) export type DurableEvent = typeof Durable.Type export const All = Schema.Union(Definitions, { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type")) diff --git a/packages/schema/src/session-input.ts b/packages/schema/src/session-input.ts index a59c00b40e7..d1837ba08f9 100644 --- a/packages/schema/src/session-input.ts +++ b/packages/schema/src/session-input.ts @@ -24,13 +24,13 @@ export const Admitted = Schema.Struct({ export interface PromptEntry extends Schema.Schema.Type {} export const PromptEntry = Schema.Struct({ - type: Schema.Literal("prompt"), + type: Schema.tag("prompt"), ...Admitted.fields, }).annotate({ identifier: "SessionInput.PromptEntry" }) export interface Compaction extends Schema.Schema.Type {} export const Compaction = Schema.Struct({ - type: Schema.Literal("compaction"), + type: Schema.tag("compaction"), admittedSeq: NonNegativeInt, id: SessionMessage.ID, sessionID: SessionID, @@ -38,5 +38,8 @@ export const Compaction = Schema.Struct({ handledSeq: NonNegativeInt.pipe(optional), }).annotate({ identifier: "SessionInput.Compaction" }) -export const Entry = Schema.Union([PromptEntry, Compaction]).pipe(Schema.toTaggedUnion("type")) -export type Entry = typeof Entry.Type +export const Info = Schema.Union([PromptEntry, Compaction]).pipe( + Schema.toTaggedUnion("type"), + Schema.annotate({ identifier: "SessionInput.Info" }), +) +export type Info = typeof Info.Type diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index 62e1a1038d3..3d45e283e60 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -4,14 +4,18 @@ import { Schema } from "effect" import { optional } from "./schema.js" import { ToolContent } from "./llm.js" import { Model } from "./model.js" -import { FileAttachment, Prompt } from "./prompt.js" +import { Prompt } from "./prompt.js" import { DateTimeUtcFromMillis, PositiveInt, RelativePath, statics } from "./schema.js" -import { SessionID } from "./session-id.js" import { ascending } from "./identifier.js" import { Event } from "./event.js" import { Shell as ShellSchema } from "./shell.js" import { FinishReason } from "./llm.js" import { SessionError } from "./session-error.js" +import { Agent } from "./agent.js" +import { Skill as SkillSchema } from "./skill.js" +import { Money } from "./money.js" +import { Snapshot } from "./snapshot.js" +import { TokenUsage } from "./token-usage.js" export const ID = Schema.String.check(Schema.isStartsWith("msg_")).pipe( Schema.brand("Session.Message.ID"), @@ -36,14 +40,14 @@ export type ProviderState = typeof ProviderState.Type export interface AgentSelected extends Schema.Schema.Type {} export const AgentSelected = Schema.Struct({ ...Base, - type: Schema.Literal("agent-switched"), - agent: Schema.String, + type: Schema.tag("agent-switched"), + agent: Agent.ID, }).annotate({ identifier: "Session.Message.AgentSelected" }) export interface ModelSelected extends Schema.Schema.Type {} export const ModelSelected = Schema.Struct({ ...Base, - type: Schema.Literal("model-switched"), + type: Schema.tag("model-switched"), model: Model.Ref, previous: Model.Ref.pipe(optional), }).annotate({ identifier: "Session.Message.ModelSelected" }) @@ -54,38 +58,41 @@ export const User = Schema.Struct({ text: Prompt.fields.text, files: Prompt.fields.files, agents: Prompt.fields.agents, - type: Schema.Literal("user"), + type: Schema.tag("user"), }).annotate({ identifier: "Session.Message.User" }) export interface Synthetic extends Schema.Schema.Type {} export const Synthetic = Schema.Struct({ ...Base, - sessionID: SessionID, text: Schema.String, description: Schema.String.pipe(optional), - type: Schema.Literal("synthetic"), + type: Schema.tag("synthetic"), }).annotate({ identifier: "Session.Message.Synthetic" }) export interface System extends Schema.Schema.Type {} export const System = Schema.Struct({ ...Base, - type: Schema.Literal("system"), + type: Schema.tag("system"), text: Schema.String, }).annotate({ identifier: "Session.Message.System" }) export interface Skill extends Schema.Schema.Type {} export const Skill = Schema.Struct({ ...Base, - type: Schema.Literal("skill"), - name: Schema.String, + type: Schema.tag("skill"), + skill: SkillSchema.ID, + name: SkillSchema.Name, text: Schema.String, }).annotate({ identifier: "Session.Message.Skill" }) export interface Shell extends Schema.Schema.Type {} export const Shell = Schema.Struct({ ...Base, - type: Schema.Literal("shell"), - shell: ShellSchema.Info, + type: Schema.tag("shell"), + shellID: ShellSchema.ID, + command: Schema.String, + status: ShellSchema.Status, + exit: Schema.Number.pipe(optional), output: ShellSchema.Output.pipe(optional), time: Schema.Struct({ created: DateTimeUtcFromMillis, @@ -93,15 +100,15 @@ export const Shell = Schema.Struct({ }), }).annotate({ identifier: "Session.Message.Shell" }) -export interface ToolStatePending extends Schema.Schema.Type {} -export const ToolStatePending = Schema.Struct({ - status: Schema.Literal("pending"), +export interface ToolStateStreaming extends Schema.Schema.Type {} +export const ToolStateStreaming = Schema.Struct({ + status: Schema.tag("streaming"), input: Schema.String, -}).annotate({ identifier: "Session.Message.ToolState.Pending" }) +}).annotate({ identifier: "Session.Message.ToolState.Streaming" }) export interface ToolStateRunning extends Schema.Schema.Type {} export const ToolStateRunning = Schema.Struct({ - status: Schema.Literal("running"), + status: Schema.tag("running"), input: Schema.Record(Schema.String, Schema.Unknown), structured: Schema.Record(Schema.String, Schema.Unknown), content: ToolContent.pipe(Schema.Array), @@ -109,18 +116,16 @@ export const ToolStateRunning = Schema.Struct({ export interface ToolStateCompleted extends Schema.Schema.Type {} export const ToolStateCompleted = Schema.Struct({ - status: Schema.Literal("completed"), + status: Schema.tag("completed"), input: Schema.Record(Schema.String, Schema.Unknown), - attachments: FileAttachment.pipe(Schema.Array, optional), content: ToolContent.pipe(Schema.Array), - outputPaths: Schema.Array(Schema.String).pipe(optional), structured: Schema.Record(Schema.String, Schema.Unknown), result: Schema.Unknown.pipe(optional), }).annotate({ identifier: "Session.Message.ToolState.Completed" }) export interface ToolStateError extends Schema.Schema.Type {} export const ToolStateError = Schema.Struct({ - status: Schema.Literal("error"), + status: Schema.tag("error"), input: Schema.Record(Schema.String, Schema.Unknown), content: ToolContent.pipe(Schema.Array), structured: Schema.Record(Schema.String, Schema.Unknown), @@ -128,14 +133,14 @@ export const ToolStateError = Schema.Struct({ result: Schema.Unknown.pipe(optional), }).annotate({ identifier: "Session.Message.ToolState.Error" }) -export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( +export const ToolState = Schema.Union([ToolStateStreaming, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe( Schema.toTaggedUnion("status"), ) -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError +export type ToolState = ToolStateStreaming | ToolStateRunning | ToolStateCompleted | ToolStateError export interface AssistantTool extends Schema.Schema.Type {} export const AssistantTool = Schema.Struct({ - type: Schema.Literal("tool"), + type: Schema.tag("tool"), id: Schema.String, name: Schema.String, executed: Schema.Boolean.pipe(optional), @@ -146,19 +151,18 @@ export const AssistantTool = Schema.Struct({ created: DateTimeUtcFromMillis, ran: DateTimeUtcFromMillis.pipe(optional), completed: DateTimeUtcFromMillis.pipe(optional), - pruned: DateTimeUtcFromMillis.pipe(optional), }), }).annotate({ identifier: "Session.Message.Assistant.Tool" }) export interface AssistantText extends Schema.Schema.Type {} export const AssistantText = Schema.Struct({ - type: Schema.Literal("text"), + type: Schema.tag("text"), text: Schema.String, }).annotate({ identifier: "Session.Message.Assistant.Text" }) export interface AssistantReasoning extends Schema.Schema.Type {} export const AssistantReasoning = Schema.Struct({ - type: Schema.Literal("reasoning"), + type: Schema.tag("reasoning"), text: Schema.String, state: ProviderState.pipe(optional), time: Schema.Struct({ @@ -182,23 +186,18 @@ export const AssistantRetry = Schema.Struct({ export interface Assistant extends Schema.Schema.Type {} export const Assistant = Schema.Struct({ ...Base, - type: Schema.Literal("assistant"), - agent: Schema.String, + type: Schema.tag("assistant"), + agent: Agent.ID, model: Model.Ref, content: AssistantContent.pipe(Schema.Array), snapshot: Schema.Struct({ - start: Schema.String.pipe(optional), - end: Schema.String.pipe(optional), + start: Snapshot.ID.pipe(optional), + end: Snapshot.ID.pipe(optional), files: Schema.Array(RelativePath).pipe(optional), }).pipe(optional), finish: FinishReason.pipe(optional), - cost: Schema.Finite.pipe(optional), - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ read: Schema.Finite, write: Schema.Finite }), - }).pipe(optional), + cost: Money.USD.pipe(optional), + tokens: TokenUsage.Info.pipe(optional), error: SessionError.Error.pipe(optional), retry: AssistantRetry.pipe(optional), time: Schema.Struct({ @@ -207,17 +206,41 @@ export const Assistant = Schema.Struct({ }), }).annotate({ identifier: "Session.Message.Assistant" }) -export interface Compaction extends Schema.Schema.Type {} -export const Compaction = Schema.Struct({ - type: Schema.Literal("compaction"), - status: Schema.Literals(["queued", "running", "completed", "failed"]), +const CompactionBase = { type: Schema.tag("compaction"), ...Base } + +export interface CompactionRunning extends Schema.Schema.Type {} +export const CompactionRunning = Schema.Struct({ + ...CompactionBase, + status: Schema.tag("running"), reason: Schema.Literals(["auto", "manual"]), summary: Schema.String, recent: Schema.String, - ...Base, -}).annotate({ identifier: "Session.Message.Compaction" }) +}).annotate({ identifier: "Session.Message.Compaction.Running" }) -export const Message = Schema.Union([ +export interface CompactionCompleted extends Schema.Schema.Type {} +export const CompactionCompleted = Schema.Struct({ + ...CompactionBase, + status: Schema.tag("completed"), + reason: Schema.Literals(["auto", "manual"]), + summary: Schema.String, + recent: Schema.String, +}).annotate({ identifier: "Session.Message.Compaction.Completed" }) + +export interface CompactionFailed extends Schema.Schema.Type {} +export const CompactionFailed = Schema.Struct({ + ...CompactionBase, + status: Schema.tag("failed"), + reason: Schema.Literals(["auto", "manual"]), + error: SessionError.Error, +}).annotate({ identifier: "Session.Message.Compaction.Failed" }) + +export const Compaction = Schema.Union([CompactionRunning, CompactionCompleted, CompactionFailed]).pipe( + Schema.toTaggedUnion("status"), + Schema.annotate({ identifier: "Session.Message.Compaction" }), +) +export type Compaction = CompactionRunning | CompactionCompleted | CompactionFailed + +export const Info = Schema.Union([ AgentSelected, ModelSelected, User, @@ -229,6 +252,6 @@ export const Message = Schema.Union([ Compaction, ]) .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Session.Message" }) -export type Message = AgentSelected | ModelSelected | User | Synthetic | System | Skill | Shell | Assistant | Compaction -export type Type = Message["type"] + .annotate({ identifier: "Session.Message.Info" }) +export type Info = AgentSelected | ModelSelected | User | Synthetic | System | Skill | Shell | Assistant | Compaction +export type Type = Info["type"] diff --git a/packages/schema/src/session-revert.ts b/packages/schema/src/session-revert.ts new file mode 100644 index 00000000000..bbbc3b0f9bb --- /dev/null +++ b/packages/schema/src/session-revert.ts @@ -0,0 +1,86 @@ +import { Schema, SchemaTransformation } from "effect" +import { FileDiff } from "./file-diff.js" +import { optional } from "./schema.js" +import { SessionMessage } from "./session-message.js" +import { Snapshot } from "./snapshot.js" + +export interface Revert extends Schema.Schema.Type {} +export const Revert = Schema.Struct({ + messageID: SessionMessage.ID, + /** Legacy V1 compatibility state. */ + partID: Schema.String.pipe(optional), + snapshot: Snapshot.ID.pipe(optional), + files: Schema.Array(FileDiff.Info).pipe(optional), +}).annotate({ identifier: "Session.Revert" }) + +const FileDiffV1 = Schema.Struct({ + path: Schema.String, + status: Schema.Literals(["added", "modified", "deleted"]), + additions: Schema.Finite, + deletions: Schema.Finite, + patch: Schema.String, +}) + +export interface RevertV1 extends Schema.Schema.Type {} +export const RevertV1 = Schema.Struct({ + messageID: SessionMessage.ID, + partID: Schema.String.pipe(optional), + snapshot: Schema.String.pipe(optional), + diff: Schema.String.pipe(optional), + files: Schema.Array(FileDiffV1).pipe(optional), +}).annotate({ identifier: "Session.RevertV1" }) + +const PersistedCurrent = Revert.pipe( + Schema.decodeTo( + Schema.Struct({ source: Schema.tag("current"), revert: Schema.toType(Revert) }), + SchemaTransformation.transform({ + decode: (revert): { readonly source: "current"; readonly revert: Revert } => ({ + source: "current", + revert, + }), + encode: (value) => value.revert, + }), + ), +) +const PersistedLegacy = RevertV1.pipe( + Schema.decodeTo( + Schema.Struct({ source: Schema.tag("legacy"), revert: Schema.toType(RevertV1) }), + SchemaTransformation.transform({ + decode: (revert): { readonly source: "legacy"; readonly revert: RevertV1 } => ({ + source: "legacy", + revert, + }), + encode: (value) => value.revert, + }), + ), +) + +/** Storage decoder for revert state written before FileDiff became canonical. */ +export const PersistedRevert = Schema.Union([PersistedCurrent, PersistedLegacy]).pipe( + Schema.toTaggedUnion("source"), + Schema.decodeTo( + Schema.toType(Revert), + SchemaTransformation.transform({ + decode: (persisted): Revert => { + if (persisted.source === "current") return persisted.revert + return Revert.make({ + messageID: persisted.revert.messageID, + partID: persisted.revert.partID, + snapshot: persisted.revert.snapshot ? Snapshot.ID.make(persisted.revert.snapshot) : undefined, + files: persisted.revert.files?.map((file) => ({ + file: file.path, + status: file.status, + additions: file.additions, + deletions: file.deletions, + patch: file.patch, + })), + }) + }, + encode: (revert): { readonly source: "current"; readonly revert: Revert } => ({ + source: "current", + revert, + }), + }), + ), + Schema.annotate({ identifier: "Session.Revert.Persisted" }), +) diff --git a/packages/schema/src/session.ts b/packages/schema/src/session.ts index 1d5a5ffef04..fffb4b7b3d2 100644 --- a/packages/schema/src/session.ts +++ b/packages/schema/src/session.ts @@ -9,34 +9,31 @@ import { DateTimeUtcFromMillis, optional, RelativePath } from "./schema.js" import { SessionEvent } from "./session-event.js" import { SessionID } from "./session-id.js" import { SessionMessage } from "./session-message.js" -import { Revert } from "./revert.js" +import { Money } from "./money.js" +import { TokenUsage } from "./token-usage.js" +import { Revert } from "./session-revert.js" export const ID = SessionID export type ID = SessionID export const Event = SessionEvent +export { Revert } + export interface Info extends Schema.Schema.Type {} export const Info = Schema.Struct({ id: ID, parentID: ID.pipe(optional), fork: Schema.Struct({ sessionID: ID, + /** Messages before this exclusive boundary are copied into the fork. */ messageID: SessionMessage.ID.pipe(optional), }).pipe(optional), projectID: Project.ID, agent: Agent.ID.pipe(optional), model: Model.Ref.pipe(optional), - cost: Schema.Finite, - tokens: Schema.Struct({ - input: Schema.Finite, - output: Schema.Finite, - reasoning: Schema.Finite, - cache: Schema.Struct({ - read: Schema.Finite, - write: Schema.Finite, - }), - }), + cost: Money.USD, + tokens: TokenUsage.Info, time: Schema.Struct({ created: DateTimeUtcFromMillis, updated: DateTimeUtcFromMillis, @@ -45,8 +42,8 @@ export const Info = Schema.Struct({ title: Schema.String, location: Location.Ref, subpath: RelativePath.pipe(optional), - revert: Revert.State.pipe(optional), -}).annotate({ identifier: "SessionV2.Info" }) + revert: Revert.pipe(optional), +}).annotate({ identifier: "Session.Info" }) export const ListAnchor = Schema.Struct({ id: ID, diff --git a/packages/schema/src/shell.ts b/packages/schema/src/shell.ts index 1826752d379..59ac25decf4 100644 --- a/packages/schema/src/shell.ts +++ b/packages/schema/src/shell.ts @@ -6,7 +6,7 @@ import { ephemeral, inventory } from "./event.js" import { ascending } from "./identifier.js" import { NonNegativeInt, statics } from "./schema.js" -const IDSchema = Schema.String.check(Schema.isStartsWith("sh_")).pipe(Schema.brand("ShellID")) +const IDSchema = Schema.String.check(Schema.isStartsWith("sh_")).pipe(Schema.brand("Shell.ID")) export const ID = IDSchema.pipe( statics((schema: typeof IDSchema) => { diff --git a/packages/schema/src/skill.ts b/packages/schema/src/skill.ts index 184266f2ada..69819eee26d 100644 --- a/packages/schema/src/skill.ts +++ b/packages/schema/src/skill.ts @@ -5,49 +5,56 @@ import { optional } from "./schema.js" import { AbsolutePath } from "./schema.js" import { ephemeral, inventory } from "./event.js" +export const ID = Schema.String.pipe(Schema.brand("Skill.ID")) +export type ID = typeof ID.Type + +export const Name = Schema.String.pipe(Schema.brand("Skill.Name")) +export type Name = typeof Name.Type + export interface DirectorySource extends Schema.Schema.Type {} export const DirectorySource = Schema.Struct({ - type: Schema.Literal("directory"), + type: Schema.tag("directory"), path: AbsolutePath, -}).annotate({ identifier: "SkillV2.DirectorySource" }) +}).annotate({ identifier: "Skill.DirectorySource" }) export interface UrlSource extends Schema.Schema.Type {} export const UrlSource = Schema.Struct({ - type: Schema.Literal("url"), + type: Schema.tag("url"), url: Schema.String, -}).annotate({ identifier: "SkillV2.UrlSource" }) +}).annotate({ identifier: "Skill.UrlSource" }) export interface Info extends Schema.Schema.Type {} export const Info = Schema.Struct({ - name: Schema.String, + id: ID, + name: Name, description: Schema.String.pipe(optional), slash: Schema.Boolean.pipe(optional), autoinvoke: Schema.Boolean.pipe(optional), location: AbsolutePath, content: Schema.String, -}).annotate({ identifier: "SkillV2.Info" }) +}).annotate({ identifier: "Skill.Info" }) const Updated = ephemeral({ type: "skill.updated", schema: {} }) export const Event = { Updated, Definitions: inventory(Updated) } export interface EmbeddedSource extends Schema.Schema.Type {} export const EmbeddedSource = Schema.Struct({ - type: Schema.Literal("embedded"), + type: Schema.tag("embedded"), skill: Schema.suspend(() => Info), -}).annotate({ identifier: "SkillV2.EmbeddedSource" }) +}).annotate({ identifier: "Skill.EmbeddedSource" }) export type Source = DirectorySource | UrlSource | EmbeddedSource export const Source = Object.assign( Schema.Union([DirectorySource, UrlSource, EmbeddedSource]).pipe( Schema.toTaggedUnion("type"), - Schema.annotate({ identifier: "SkillV2.Source" }), + Schema.annotate({ identifier: "Skill.Source" }), ), { equals: (a: Source, b: Source) => { if (a.type !== b.type) return false if (a.type === "directory" && b.type === "directory") return a.path === b.path if (a.type === "url" && b.type === "url") return a.url === b.url - if (a.type === "embedded" && b.type === "embedded") return a.skill.name === b.skill.name + if (a.type === "embedded" && b.type === "embedded") return a.skill.id === b.skill.id return false }, key: (source: Source) => @@ -55,6 +62,6 @@ export const Source = Object.assign( ? `directory:${source.path}` : source.type === "url" ? `url:${source.url}` - : `embedded:${source.skill.name}`, + : `embedded:${source.skill.id}`, }, ) diff --git a/packages/schema/src/snapshot.ts b/packages/schema/src/snapshot.ts new file mode 100644 index 00000000000..375981b4159 --- /dev/null +++ b/packages/schema/src/snapshot.ts @@ -0,0 +1,6 @@ +export * as Snapshot from "./snapshot.js" + +import { Schema } from "effect" + +export const ID = Schema.String.pipe(Schema.brand("Snapshot.ID")) +export type ID = typeof ID.Type diff --git a/packages/schema/src/token-usage.ts b/packages/schema/src/token-usage.ts new file mode 100644 index 00000000000..6f08add0cc6 --- /dev/null +++ b/packages/schema/src/token-usage.ts @@ -0,0 +1,14 @@ +export * as TokenUsage from "./token-usage.js" + +import { Schema } from "effect" + +export interface Info extends Schema.Schema.Type {} +export const Info = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), +}).annotate({ identifier: "TokenUsage.Info" }) diff --git a/packages/schema/src/v1/session.ts b/packages/schema/src/v1/session.ts index 1c2827f0d82..3932cae88a2 100644 --- a/packages/schema/src/v1/session.ts +++ b/packages/schema/src/v1/session.ts @@ -2,7 +2,6 @@ export * as SessionV1 from "./session.js" import { Effect, Schema, Types } from "effect" import { durable, ephemeral, inventory } from "../event.js" -import { FileDiff } from "../file-diff.js" import { Project } from "../project.js" import { Provider } from "../provider.js" import { Model } from "../model.js" @@ -11,6 +10,7 @@ import { ascending } from "../identifier.js" import { SessionID } from "../session-id.js" import { WorkspaceID } from "../workspace-id.js" import { PermissionV1 } from "./permission.js" +import { FileDiff } from "../file-diff.js" const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) @@ -340,7 +340,7 @@ export const User = Schema.Struct({ Schema.Struct({ title: Schema.optional(Schema.String), body: Schema.optional(Schema.String), - diffs: Schema.Array(FileDiff.Info), + diffs: Schema.Array(FileDiff.LegacyInfo), }), ), agent: Schema.String, @@ -510,7 +510,7 @@ const SessionSummary = Schema.Struct({ additions: Schema.Finite, deletions: Schema.Finite, files: Schema.Finite, - diffs: optional(Schema.Array(FileDiff.Info)), + diffs: optional(Schema.Array(FileDiff.LegacyInfo)), }) const SessionTokens = Schema.Struct({ @@ -565,7 +565,7 @@ export const SessionInfo = Schema.Struct({ }), permission: optional(PermissionV1.Ruleset), revert: optional(SessionRevert), -}).annotate({ identifier: "Session" }) +}).annotate({ identifier: "SessionV1.Info" }) export type SessionInfo = typeof SessionInfo.Type const events = { @@ -644,7 +644,7 @@ export const Diff = ephemeral({ type: "session.diff", schema: { sessionID: SessionID, - diff: Schema.Array(FileDiff.Info), + diff: Schema.Array(FileDiff.LegacyInfo), }, }) diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index 0cc1c71974b..f5f310d8d7e 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -10,10 +10,30 @@ import { Pty } from "../src/pty.js" import { Question } from "../src/question.js" import { Session } from "../src/session.js" import { SessionMessage } from "../src/session-message.js" +import { SessionInput } from "../src/session-input.js" +import { FileDiff } from "../src/file-diff.js" +import { Money } from "../src/money.js" +import { Skill } from "../src/skill.js" +import { Shell } from "../src/shell.js" +import { PersistedRevert } from "../src/session-revert.js" import { SessionTodo } from "../src/session-todo.js" import { optional } from "../src/schema.js" describe("contract hygiene", () => { + test("keeps absolute costs distinct from model rates", () => { + const usd = Money.USD.make(1) + const rate = Money.USDPerMillionTokens.make(1) + // @ts-expect-error Model rates are not absolute costs. + const invalidUSD: Money.USD = rate + // @ts-expect-error Absolute costs are not model rates. + const invalidRate: Money.USDPerMillionTokens = usd + + expect(invalidUSD).toBe(Money.USD.make(1)) + expect(invalidRate).toBe(Money.USDPerMillionTokens.make(1)) + expect(Money.USD.zero).toBe(Money.USD.make(0)) + expect(Money.USDPerMillionTokens.zero).toBe(Money.USDPerMillionTokens.make(0)) + }) + test("optional properties preserve transformations and omit undefined while encoding", () => { const Value = Schema.Struct({ value: optional(Schema.FiniteFromString) }) expect(Schema.decodeUnknownSync(Value)({ value: "1" })).toEqual({ value: 1 }) @@ -71,6 +91,7 @@ describe("contract hygiene", () => { Project.Info, Pty.Info, Session.ListAnchor, + Session.Revert, ].map((schema) => schema.ast.annotations?.identifier) expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true) @@ -104,9 +125,76 @@ describe("contract hygiene", () => { name: "search", executed: true, providerState: { itemId: "item_1" }, - state: { status: "pending", input: "" }, + state: { status: "streaming", input: "" }, time: { created: DateTime.makeUnsafe(0) }, }), ).not.toHaveProperty("provider") }) + + test("reviewed session contracts use their canonical current shapes", () => { + expect(SessionMessage.Info.ast.annotations?.identifier).toBe("Session.Message.Info") + expect(SessionInput.Info.ast.annotations?.identifier).toBe("SessionInput.Info") + expect(Money.USD).not.toBe(Money.USDPerMillionTokens) + expect( + FileDiff.Info.make({ file: "src/index.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" }), + ).toEqual({ file: "src/index.ts", patch: "@@", additions: 1, deletions: 0, status: "modified" }) + expect( + SessionMessage.Shell.make({ + id: SessionMessage.ID.make("msg_shell"), + type: "shell", + shellID: Shell.ID.make("sh_test"), + command: "pwd", + status: "exited", + exit: 0, + time: { created: DateTime.makeUnsafe(0) }, + }), + ).not.toHaveProperty("shell") + expect( + SessionMessage.Skill.make({ + id: SessionMessage.ID.make("msg_skill"), + type: "skill", + skill: Skill.ID.make("effect"), + name: Skill.Name.make("Effect"), + text: "Use Effect", + time: { created: DateTime.makeUnsafe(0) }, + }), + ).toMatchObject({ skill: "effect", name: "Effect" }) + expect( + SessionMessage.CompactionFailed.make({ + id: SessionMessage.ID.make("msg_compaction"), + type: "compaction", + status: "failed", + reason: "manual", + error: { type: "compaction.failed", message: "failed" }, + time: { created: DateTime.makeUnsafe(0) }, + }), + ).not.toHaveProperty("summary") + }) + + test("keeps shared persisted revert compatibility", () => { + expect( + Schema.decodeUnknownSync(Session.Revert)({ + messageID: "msg_legacy", + snapshot: "tree", + diff: "legacy patch", + }), + ).not.toHaveProperty("diff") + + const revert = Schema.decodeUnknownSync(PersistedRevert)({ + messageID: "msg_legacy", + snapshot: "tree", + diff: "legacy patch", + files: [{ path: "src/index.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" }], + }) + expect(String(revert.messageID)).toBe("msg_legacy") + expect(String(revert.snapshot)).toBe("tree") + expect(revert.files).toEqual([ + { file: "src/index.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" }, + ]) + expect(Schema.encodeSync(PersistedRevert)(revert)).toEqual({ + messageID: "msg_legacy", + snapshot: "tree", + files: [{ file: "src/index.ts", status: "modified", additions: 1, deletions: 0, patch: "@@" }], + }) + }) }) diff --git a/packages/schema/test/event-manifest.test.ts b/packages/schema/test/event-manifest.test.ts index 8f8f1121761..971ac68f247 100644 --- a/packages/schema/test/event-manifest.test.ts +++ b/packages/schema/test/event-manifest.test.ts @@ -145,10 +145,17 @@ describe("public event manifest", () => { SessionEvent.Definitions.filter((definition) => definition.durability === "durable"), ) expect(SessionEvent.UsageUpdated.durability).toBe("ephemeral") + expect(SessionEvent.Compaction.Delta.durability).toBe("ephemeral") + expect(EventManifest.Durable.has("session.compaction.delta.1")).toBe(false) expect(EventManifest.ServerDefinitions).toContain(SessionEvent.UsageUpdated) expect(EventManifest.Definitions.every((definition) => definition.durability !== undefined)).toBe(true) }) + test("uses the current Session skill event as durable version 1", () => { + expect(EventManifest.Durable.get("session.skill.activated.1")).toBe(SessionEvent.Skill.Activated) + expect(EventManifest.Latest.get("session.skill.activated")).toBe(SessionEvent.Skill.Activated) + }) + test("keeps simplified session fragment and tool payloads on durable version 1", () => { const sessionID = SessionID.make("ses_test") const assistantMessageID = SessionMessage.ID.make("msg_test") diff --git a/packages/sdk/js/src/v2/client.ts b/packages/sdk/js/src/v2/client.ts index c1956cffe03..bd8f984d7fb 100644 --- a/packages/sdk/js/src/v2/client.ts +++ b/packages/sdk/js/src/v2/client.ts @@ -1,5 +1,9 @@ export * from "./gen/types.gen.js" export type { FileSystemEntry as LocationFileSystemEntry } from "./gen/types.gen.js" +import type { UserMessage } from "./gen/types.gen.js" + +/** @deprecated V1 snapshot compatibility. Use FileDiffInfo for current API responses. */ +export type SnapshotFileDiff = NonNullable["diffs"]>[number] import { createClient } from "./gen/client/client.gen.js" import { type Config } from "./gen/client/types.gen.js" diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index a88ad8e008a..03d76cc5de5 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -5185,86 +5185,6 @@ export class Plugin extends HeyApiClient { } } -export class Switch extends HeyApiClient { - /** - * Switch session agent - * - * Switch the agent used by subsequent provider turns. - */ - public agent( - parameters: { - sessionID: string - agent?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "agent" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionSwitchAgentResponses, - V2SessionSwitchAgentErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/agent", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Switch session model - * - * Switch the model used by subsequent provider turns. - */ - public model( - parameters: { - sessionID: string - model?: ModelRef - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "body", key: "model" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionSwitchModelResponses, - V2SessionSwitchModelErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/model", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - export class Revert extends HeyApiClient { /** * Stage session revert @@ -6068,6 +5988,84 @@ export class Session3 extends HeyApiClient { }) } + /** + * Switch session agent + * + * Switch the agent used by subsequent provider turns. + */ + public switchAgent( + parameters: { + sessionID: string + agent?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "agent" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionSwitchAgentResponses, + V2SessionSwitchAgentErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/agent", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Switch session model + * + * Switch the model used by subsequent provider turns. + */ + public switchModel( + parameters: { + sessionID: string + model?: ModelRef + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "model" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionSwitchModelResponses, + V2SessionSwitchModelErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/model", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + /** * Rename session * @@ -6528,11 +6526,6 @@ export class Session3 extends HeyApiClient { }) } - private _switch?: Switch - get switch(): Switch { - return (this._switch ??= new Switch({ client: this.client })) - } - private _revert?: Revert get revert(): Revert { return (this._revert ??= new Revert({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 59afb28c069..0edaac8a7c1 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -171,14 +171,6 @@ export type MoveSessionError = { } } -export type SnapshotFileDiff = { - file?: string - patch?: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" -} - export type PermissionAction = "allow" | "deny" | "ask" export type PermissionRule = { @@ -189,59 +181,6 @@ export type PermissionRule = { export type PermissionRuleset = Array -export type Session = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - export type OutputFormatText = { type: "text" } @@ -269,7 +208,7 @@ export type UserMessage = { summary?: { title?: string body?: string - diffs: Array + diffs: Array } agent: string model: { @@ -812,7 +751,7 @@ export type GlobalEvent = { type: "session.created" properties: { sessionID: string - info: Session + info: SessionV1Info } } | { @@ -820,7 +759,7 @@ export type GlobalEvent = { type: "session.updated" properties: { sessionID: string - info: Session + info: SessionV1Info } } | { @@ -902,16 +841,8 @@ export type GlobalEvent = { type: "session.usage.updated" properties: { sessionID: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo } } | { @@ -996,6 +927,7 @@ export type GlobalEvent = { type: "session.skill.activated" properties: { sessionID: string + id: string name: string text: string } @@ -1040,16 +972,8 @@ export type GlobalEvent = { sessionID: string assistantMessageID: string finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo snapshot?: string files?: Array } @@ -1061,16 +985,8 @@ export type GlobalEvent = { sessionID: string assistantMessageID: string error: SessionStructuredError - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo } } | { @@ -1201,7 +1117,6 @@ export type GlobalEvent = { [key: string]: unknown } content: Array - outputPaths?: Array result?: unknown executed: boolean resultState?: SessionMessageProviderState @@ -1245,6 +1160,8 @@ export type GlobalEvent = { properties: { sessionID: string reason: "auto" | "manual" + recent: string + inputID?: string } } | { @@ -1270,6 +1187,9 @@ export type GlobalEvent = { type: "session.compaction.failed" properties: { sessionID: string + reason: "auto" | "manual" + error: SessionStructuredError + inputID?: string } } | { @@ -1277,7 +1197,7 @@ export type GlobalEvent = { type: "session.revert.staged" properties: { sessionID: string - revert: RevertState + revert: SessionRevert } } | { @@ -1311,7 +1231,7 @@ export type GlobalEvent = { type: "session.diff" properties: { sessionID: string - diff: Array + diff: Array } } | { @@ -2394,7 +2314,7 @@ export type GlobalSession = { additions: number deletions: number files: number - diffs?: Array + diffs?: Array } cost?: number tokens?: { @@ -2730,6 +2650,59 @@ export type ProviderAuthError1 = { } } +export type Session = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + share?: { + url: string + } + title: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + version: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + export type NotFoundError = { name: "NotFoundError" data: { @@ -2874,7 +2847,7 @@ export type UnauthorizedError = { } export type SessionsResponse = { - data: Array + data: Array cursor: { previous?: string next?: string @@ -2963,52 +2936,12 @@ export type Shell1 = { } } -export type SessionDurableEvent = - | SessionAgentSelected - | SessionModelSelected - | SessionMoved - | SessionRenamed - | SessionDeleted - | SessionForked - | SessionPromptPromoted - | SessionPromptAdmitted - | SessionExecutionStarted - | SessionExecutionSucceeded - | SessionExecutionFailed - | SessionExecutionInterrupted - | SessionInstructionsUpdated - | SessionSynthetic - | SessionSkillActivated - | SessionShellStarted - | SessionShellEnded - | SessionStepStarted - | SessionStepEnded - | SessionStepFailed - | SessionTextStarted - | SessionTextEnded - | SessionReasoningStarted - | SessionReasoningEnded - | SessionToolInputStarted - | SessionToolInputEnded - | SessionToolCalled - | SessionToolProgress - | SessionToolSuccess - | SessionToolFailed - | SessionRetryScheduled - | SessionCompactionAdmitted - | SessionCompactionStarted - | SessionCompactionEnded - | SessionCompactionFailed - | SessionRevertStaged - | SessionRevertCleared - | SessionRevertCommitted - -export type SessionLogItem = SessionDurableEvent | EventLogSynced +export type SessionLogItem = SessionEventDurable | EventLogSynced export type SessionLogItemStream = string export type SessionMessagesResponse = { - data: Array + data: Array cursor: { previous?: string next?: string @@ -3337,12 +3270,73 @@ export type IntegrationRef = { name: string } -export type SkillV2Source = SkillV2DirectorySource | SkillV2UrlSource | SkillV2EmbeddedSource +export type SkillSource = SkillDirectorySource | SkillUrlSource | SkillEmbeddedSource export type MoveSessionDestination = { directory: string } +export type FileDiffLegacyInfo = { + file?: string + patch?: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +export type SessionV1Info = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + share?: { + url: string + } + title: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + version: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + export type ModelRef = { id: string providerID: string @@ -3354,6 +3348,18 @@ export type LocationRef = { workspaceID?: string } +export type MoneyUsd = number + +export type TokenUsageInfo = { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } +} + export type PromptBase64 = string export type PromptFileSource = @@ -3408,20 +3414,19 @@ export type ToolFileContent = { export type LlmToolContent = ToolTextContent | ToolFileContent -export type FileDiff = { - path: string - status: "added" | "modified" | "deleted" +export type FileDiffInfo = { + file: string + patch: string additions: number deletions: number - patch: string + status: "added" | "deleted" | "modified" } -export type RevertState = { +export type SessionRevert = { messageID: string partID?: string snapshot?: string - diff?: string - files?: Array + files?: Array } export type PermissionV2Source = { @@ -3621,7 +3626,7 @@ export type SyncEventSessionCreated = { aggregateID: string data: { sessionID: string - info: Session + info: SessionV1Info } } } @@ -3636,7 +3641,7 @@ export type SyncEventSessionUpdated = { aggregateID: string data: { sessionID: string - info: Session + info: SessionV1Info } } } @@ -3928,6 +3933,7 @@ export type SyncEventSessionSkillActivated = { aggregateID: string data: { sessionID: string + id: string name: string text: string } @@ -4000,16 +4006,8 @@ export type SyncEventSessionStepEnded = { sessionID: string assistantMessageID: string finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo snapshot?: string files?: Array } @@ -4028,16 +4026,8 @@ export type SyncEventSessionStepFailed = { sessionID: string assistantMessageID: string error: SessionStructuredError - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo } } } @@ -4201,7 +4191,6 @@ export type SyncEventSessionToolSuccess = { [key: string]: unknown } content: Array - outputPaths?: Array result?: unknown executed: boolean resultState?: SessionMessageProviderState @@ -4273,6 +4262,8 @@ export type SyncEventSessionCompactionStarted = { data: { sessionID: string reason: "auto" | "manual" + recent: string + inputID?: string } } } @@ -4304,6 +4295,9 @@ export type SyncEventSessionCompactionFailed = { aggregateID: string data: { sessionID: string + reason: "auto" | "manual" + error: SessionStructuredError + inputID?: string } } } @@ -4318,7 +4312,7 @@ export type SyncEventSessionRevertStaged = { aggregateID: string data: { sessionID: string - revert: RevertState + revert: SessionRevert } } } @@ -4417,8 +4411,9 @@ export type PermissionV2Rule = { export type PermissionV2Ruleset = Array -export type AgentV2Info = { +export type AgentInfo = { id: string + name: string model?: ModelRef request: ProviderRequest system?: string @@ -4434,7 +4429,7 @@ export type PluginInfo = { id: string } -export type SessionV2Info = { +export type SessionInfo = { id: string parentID?: string fork?: { @@ -4444,16 +4439,8 @@ export type SessionV2Info = { projectID: string agent?: string model?: ModelRef - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo time: { created: number updated: number @@ -4462,7 +4449,7 @@ export type SessionV2Info = { title: string location: LocationRef subpath?: string - revert?: RevertState + revert?: SessionRevert } export type PromptInputFileAttachment = { @@ -4538,7 +4525,6 @@ export type SessionMessageSynthetic = { time: { created: number } - sessionID: string text: string description?: string type: "synthetic" @@ -4565,6 +4551,7 @@ export type SessionMessageSkill = { created: number } type: "skill" + skill: string name: string text: string } @@ -4579,7 +4566,10 @@ export type SessionMessageShell = { completed?: number } type: "shell" - shell: Shell + shellID: string + command: string + status: "running" | "exited" | "timeout" | "killed" + exit?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" output?: { output: string cursor: number @@ -4603,8 +4593,8 @@ export type SessionMessageAssistantReasoning = { } } -export type SessionMessageToolStatePending = { - status: "pending" +export type SessionMessageToolStateStreaming = { + status: "streaming" input: string } @@ -4624,9 +4614,7 @@ export type SessionMessageToolStateCompleted = { input: { [key: string]: unknown } - attachments?: Array content: Array - outputPaths?: Array structured: { [key: string]: unknown } @@ -4654,7 +4642,7 @@ export type SessionMessageAssistantTool = { providerState?: SessionMessageProviderState providerResultState?: SessionMessageProviderState state: - | SessionMessageToolStatePending + | SessionMessageToolStateStreaming | SessionMessageToolStateRunning | SessionMessageToolStateCompleted | SessionMessageToolStateError @@ -4662,7 +4650,6 @@ export type SessionMessageAssistantTool = { created: number ran?: number completed?: number - pruned?: number } } @@ -4691,26 +4678,14 @@ export type SessionMessageAssistant = { files?: Array } finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo error?: SessionStructuredError retry?: SessionMessageAssistantRetry } -export type SessionMessageCompaction = { +export type SessionMessageCompactionRunning = { type: "compaction" - status: "queued" | "running" | "completed" | "failed" - reason: "auto" | "manual" - summary: string - recent: string id: string metadata?: { [key: string]: unknown @@ -4718,9 +4693,47 @@ export type SessionMessageCompaction = { time: { created: number } + status: "running" + reason: "auto" | "manual" + summary: string + recent: string } -export type SessionMessage = +export type SessionMessageCompactionCompleted = { + type: "compaction" + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + status: "completed" + reason: "auto" | "manual" + summary: string + recent: string +} + +export type SessionMessageCompactionFailed = { + type: "compaction" + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + status: "failed" + reason: "auto" | "manual" + error: SessionStructuredError +} + +export type SessionMessageCompaction = + | SessionMessageCompactionRunning + | SessionMessageCompactionCompleted + | SessionMessageCompactionFailed + +export type SessionMessageInfo = | SessionMessageAgentSelected | SessionMessageModelSelected | SessionMessageUser @@ -4748,7 +4761,7 @@ export type SessionAgentSelected = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4767,7 +4780,7 @@ export type SessionModelSelected = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4786,7 +4799,7 @@ export type SessionMoved = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4806,7 +4819,7 @@ export type SessionRenamed = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4825,7 +4838,7 @@ export type SessionDeleted = { durable: { aggregateID: string seq: number - version: number + version: 2 } location?: LocationRef data: { @@ -4843,7 +4856,7 @@ export type SessionForked = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4863,7 +4876,7 @@ export type SessionPromptPromoted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4882,7 +4895,7 @@ export type SessionPromptAdmitted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4903,7 +4916,7 @@ export type SessionExecutionStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4921,7 +4934,7 @@ export type SessionExecutionSucceeded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4939,7 +4952,7 @@ export type SessionExecutionFailed = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4958,7 +4971,7 @@ export type SessionExecutionInterrupted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4977,7 +4990,7 @@ export type SessionInstructionsUpdated = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -4996,7 +5009,7 @@ export type SessionSynthetic = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5019,11 +5032,12 @@ export type SessionSkillActivated = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string + id: string name: string text: string } @@ -5039,7 +5053,7 @@ export type SessionShellStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5058,7 +5072,7 @@ export type SessionShellEnded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5083,7 +5097,7 @@ export type SessionStepStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5105,23 +5119,15 @@ export type SessionStepEnded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string assistantMessageID: string finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo snapshot?: string files?: Array } @@ -5137,23 +5143,15 @@ export type SessionStepFailed = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string assistantMessageID: string error: SessionStructuredError - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo } } @@ -5167,7 +5165,7 @@ export type SessionTextStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5187,7 +5185,7 @@ export type SessionTextEnded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5208,7 +5206,7 @@ export type SessionReasoningStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5229,7 +5227,7 @@ export type SessionReasoningEnded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5251,7 +5249,7 @@ export type SessionToolInputStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5272,7 +5270,7 @@ export type SessionToolInputEnded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5293,7 +5291,7 @@ export type SessionToolCalled = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5318,7 +5316,7 @@ export type SessionToolProgress = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5342,7 +5340,7 @@ export type SessionToolSuccess = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5353,7 +5351,6 @@ export type SessionToolSuccess = { [key: string]: unknown } content: Array - outputPaths?: Array result?: unknown executed: boolean resultState?: SessionMessageProviderState @@ -5370,7 +5367,7 @@ export type SessionToolFailed = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5394,7 +5391,7 @@ export type SessionRetryScheduled = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5416,7 +5413,7 @@ export type SessionCompactionAdmitted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5435,12 +5432,14 @@ export type SessionCompactionStarted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string reason: "auto" | "manual" + recent: string + inputID?: string } } @@ -5454,7 +5453,7 @@ export type SessionCompactionEnded = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5475,11 +5474,14 @@ export type SessionCompactionFailed = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string + reason: "auto" | "manual" + error: SessionStructuredError + inputID?: string } } @@ -5493,12 +5495,12 @@ export type SessionRevertStaged = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string - revert: RevertState + revert: SessionRevert } } @@ -5512,7 +5514,7 @@ export type SessionRevertCleared = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5530,7 +5532,7 @@ export type SessionRevertCommitted = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5539,6 +5541,46 @@ export type SessionRevertCommitted = { } } +export type SessionEventDurable = + | SessionAgentSelected + | SessionModelSelected + | SessionMoved + | SessionRenamed + | SessionDeleted + | SessionForked + | SessionPromptPromoted + | SessionPromptAdmitted + | SessionExecutionStarted + | SessionExecutionSucceeded + | SessionExecutionFailed + | SessionExecutionInterrupted + | SessionInstructionsUpdated + | SessionSynthetic + | SessionSkillActivated + | SessionShellStarted + | SessionShellEnded + | SessionStepStarted + | SessionStepEnded + | SessionStepFailed + | SessionTextStarted + | SessionTextEnded + | SessionReasoningStarted + | SessionReasoningEnded + | SessionToolInputStarted + | SessionToolInputEnded + | SessionToolCalled + | SessionToolProgress + | SessionToolSuccess + | SessionToolFailed + | SessionRetryScheduled + | SessionCompactionAdmitted + | SessionCompactionStarted + | SessionCompactionEnded + | SessionCompactionFailed + | SessionRevertStaged + | SessionRevertCleared + | SessionRevertCommitted + export type EventLogSynced = { type: "log.synced" aggregateID: string @@ -5564,20 +5606,22 @@ export type ModelVariant = { } } +export type MoneyUsdPerMillionTokens = number + export type ModelCost = { tier?: { type: "context" size: number } - input: number - output: number + input: MoneyUsdPerMillionTokens + output: MoneyUsdPerMillionTokens cache: { - read: number - write: number + read: MoneyUsdPerMillionTokens + write: MoneyUsdPerMillionTokens } } -export type ModelV2Info = { +export type ModelInfo = { id: string modelID: string providerID: string @@ -5835,7 +5879,7 @@ export type FileSystemEntry = { type: "file" | "directory" } -export type CommandV2Info = { +export type CommandInfo = { name: string template: string description?: string @@ -5844,7 +5888,8 @@ export type CommandV2Info = { subtask?: boolean } -export type SkillV2Info = { +export type SkillInfo = { + id: string name: string description?: string slash?: boolean @@ -5928,12 +5973,12 @@ export type SessionCreated = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string - info: Session + info: SessionV1Info } } @@ -5947,12 +5992,12 @@ export type SessionUpdated = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { sessionID: string - info: Session + info: SessionV1Info } } @@ -5966,7 +6011,7 @@ export type MessageUpdated = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -5985,7 +6030,7 @@ export type MessageRemoved = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -6004,7 +6049,7 @@ export type MessagePartUpdated = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -6024,7 +6069,7 @@ export type MessagePartRemoved = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRef data: { @@ -6044,16 +6089,8 @@ export type SessionUsageUpdated = { location?: LocationRef data: { sessionID: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo } } @@ -6146,7 +6183,7 @@ export type SessionDiff = { location?: LocationRef data: { sessionID: string - diff: Array + diff: Array } } @@ -7029,7 +7066,7 @@ export type EventSessionCreated = { type: "session.created" properties: { sessionID: string - info: Session + info: SessionV1Info } } @@ -7038,7 +7075,7 @@ export type EventSessionUpdated = { type: "session.updated" properties: { sessionID: string - info: Session + info: SessionV1Info } } @@ -7130,16 +7167,8 @@ export type EventSessionUsageUpdated = { type: "session.usage.updated" properties: { sessionID: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo } } @@ -7234,6 +7263,7 @@ export type EventSessionSkillActivated = { type: "session.skill.activated" properties: { sessionID: string + id: string name: string text: string } @@ -7282,16 +7312,8 @@ export type EventSessionStepEnded = { sessionID: string assistantMessageID: string finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo snapshot?: string files?: Array } @@ -7304,16 +7326,8 @@ export type EventSessionStepFailed = { sessionID: string assistantMessageID: string error: SessionStructuredError - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo } } @@ -7456,7 +7470,6 @@ export type EventSessionToolSuccess = { [key: string]: unknown } content: Array - outputPaths?: Array result?: unknown executed: boolean resultState?: SessionMessageProviderState @@ -7504,6 +7517,8 @@ export type EventSessionCompactionStarted = { properties: { sessionID: string reason: "auto" | "manual" + recent: string + inputID?: string } } @@ -7532,6 +7547,9 @@ export type EventSessionCompactionFailed = { type: "session.compaction.failed" properties: { sessionID: string + reason: "auto" | "manual" + error: SessionStructuredError + inputID?: string } } @@ -7540,7 +7558,7 @@ export type EventSessionRevertStaged = { type: "session.revert.staged" properties: { sessionID: string - revert: RevertState + revert: SessionRevert } } @@ -7578,7 +7596,7 @@ export type EventSessionDiff = { type: "session.diff" properties: { sessionID: string - diff: Array + diff: Array } } @@ -8096,19 +8114,19 @@ export type CredentialKey = { } } -export type SkillV2DirectorySource = { +export type SkillDirectorySource = { type: "directory" path: string } -export type SkillV2UrlSource = { +export type SkillUrlSource = { type: "url" url: string } -export type SkillV2EmbeddedSource = { +export type SkillEmbeddedSource = { type: "embedded" - skill: SkillV2Info + skill: SkillInfo } export type BadRequestError = { @@ -8127,7 +8145,7 @@ export type InvalidRequestErrorV2 = { } export type SessionsResponseV2 = { - data: Array + data: Array cursor: { previous?: string | null next?: string | null @@ -8167,77 +8185,24 @@ export type ShellV2 = { shell: string file: string pid?: number - exit?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + exit?: number | "NaN" | "Infinity" | "-Infinity" metadata: { [key: string]: unknown } time: { - started: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" - completed?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + started: number | "NaN" | "Infinity" | "-Infinity" + completed?: number | "NaN" | "Infinity" | "-Infinity" } } export type SessionMessagesResponseV2 = { - data: Array + data: Array cursor: { previous?: string | null next?: string | null } } -export type SessionV2 = { - id: string - slug: string - projectID: string - workspaceID?: string - directory: string - path?: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - share?: { - url: string - } - title: string - agent?: string - model?: { - id: string - providerID: string - variant?: string - } - version: string - metadata?: { - [key: string]: unknown - } - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - export type OutputFormatV2 = | { type: "text" @@ -8259,7 +8224,7 @@ export type UserMessageV2 = { summary?: { title?: string | null body?: string | null - diffs: Array + diffs: Array } | null agent: string model: { @@ -8777,8 +8742,9 @@ export type LocationInfoV2 = { export type AgentColorV2 = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" -export type AgentV2InfoV2 = { +export type AgentInfoV2 = { id: string + name: string model?: ModelRef request: ProviderRequest system?: string @@ -8795,23 +8761,22 @@ export type LocationRefV2 = { workspaceID?: string } -export type FileDiffV2 = { - path: string - status: "added" | "modified" | "deleted" +export type FileDiffInfoV2 = { + file: string + patch: string additions: number deletions: number - patch: string + status: "added" | "deleted" | "modified" } -export type RevertStateV2 = { +export type SessionRevertV2 = { messageID: string partID?: string snapshot?: string - diff?: string - files?: Array + files?: Array } -export type SessionV2InfoV2 = { +export type SessionInfoV2 = { id: string parentID?: string fork?: { @@ -8821,16 +8786,8 @@ export type SessionV2InfoV2 = { projectID: string agent?: string model?: ModelRef - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo time: { created: number updated: number @@ -8839,7 +8796,7 @@ export type SessionV2InfoV2 = { title: string location: LocationRefV2 subpath?: string - revert?: RevertStateV2 + revert?: SessionRevertV2 } export type PromptBase64V2 = string @@ -8910,7 +8867,6 @@ export type SessionMessageSyntheticV2 = { time: { created: number } - sessionID: string text: string description?: string type: "synthetic" @@ -8937,6 +8893,7 @@ export type SessionMessageSkillV2 = { created: number } type: "skill" + skill: string name: string text: string } @@ -8951,7 +8908,10 @@ export type SessionMessageShellV2 = { completed?: number } type: "shell" - shell: ShellV2 + shellID: string + command: string + status: "running" | "exited" | "timeout" | "killed" + exit?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" output?: { output: string cursor: number @@ -8985,26 +8945,14 @@ export type SessionMessageAssistantV2 = { files?: Array } finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo error?: SessionStructuredError retry?: SessionMessageAssistantRetryV2 } -export type SessionMessageCompactionV2 = { +export type SessionMessageCompactionRunningV2 = { type: "compaction" - status: "queued" | "running" | "completed" | "failed" - reason: "auto" | "manual" - summary: string - recent: string id: string metadata?: { [key: string]: unknown @@ -9012,6 +8960,39 @@ export type SessionMessageCompactionV2 = { time: { created: number } + status: "running" + reason: "auto" | "manual" + summary: string + recent: string +} + +export type SessionMessageCompactionCompletedV2 = { + type: "compaction" + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + status: "completed" + reason: "auto" | "manual" + summary: string + recent: string +} + +export type SessionMessageCompactionFailedV2 = { + type: "compaction" + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + status: "failed" + reason: "auto" | "manual" + error: SessionStructuredError } /** @@ -9029,7 +9010,7 @@ export type SessionAgentSelectedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9048,7 +9029,7 @@ export type SessionModelSelectedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9067,7 +9048,7 @@ export type SessionMovedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9087,7 +9068,7 @@ export type SessionRenamedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9106,7 +9087,7 @@ export type SessionDeletedV2 = { durable: { aggregateID: string seq: number - version: number + version: 2 } location?: LocationRefV2 data: { @@ -9124,7 +9105,7 @@ export type SessionForkedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9144,7 +9125,7 @@ export type SessionPromptPromotedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9163,7 +9144,7 @@ export type SessionPromptAdmittedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9184,7 +9165,7 @@ export type SessionExecutionStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9202,7 +9183,7 @@ export type SessionExecutionSucceededV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9220,7 +9201,7 @@ export type SessionExecutionFailedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9239,7 +9220,7 @@ export type SessionExecutionInterruptedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9258,7 +9239,7 @@ export type SessionInstructionsUpdatedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9277,7 +9258,7 @@ export type SessionSyntheticV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9300,11 +9281,12 @@ export type SessionSkillActivatedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string + id: string name: string text: string } @@ -9320,7 +9302,7 @@ export type SessionShellStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9339,7 +9321,7 @@ export type SessionShellEndedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9364,7 +9346,7 @@ export type SessionStepStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9386,23 +9368,15 @@ export type SessionStepEndedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string assistantMessageID: string finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo snapshot?: string files?: Array } @@ -9418,23 +9392,15 @@ export type SessionStepFailedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string assistantMessageID: string error: SessionStructuredError - cost?: number - tokens?: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost?: MoneyUsd + tokens?: TokenUsageInfo } } @@ -9448,7 +9414,7 @@ export type SessionTextStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9468,7 +9434,7 @@ export type SessionTextEndedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9493,7 +9459,7 @@ export type SessionReasoningStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9518,7 +9484,7 @@ export type SessionReasoningEndedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9540,7 +9506,7 @@ export type SessionToolInputStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9561,7 +9527,7 @@ export type SessionToolInputEndedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9586,7 +9552,7 @@ export type SessionToolCalledV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9611,7 +9577,7 @@ export type SessionToolProgressV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9639,7 +9605,7 @@ export type SessionToolSuccessV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9650,7 +9616,6 @@ export type SessionToolSuccessV2 = { [key: string]: unknown } content: Array - outputPaths?: Array result?: unknown executed: boolean resultState?: SessionMessageProviderState6 @@ -9671,7 +9636,7 @@ export type SessionToolFailedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9695,7 +9660,7 @@ export type SessionRetryScheduledV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9717,7 +9682,7 @@ export type SessionCompactionAdmittedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9736,12 +9701,14 @@ export type SessionCompactionStartedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string reason: "auto" | "manual" + recent: string + inputID?: string } } @@ -9755,7 +9722,7 @@ export type SessionCompactionEndedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9776,11 +9743,14 @@ export type SessionCompactionFailedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string + reason: "auto" | "manual" + error: SessionStructuredError + inputID?: string } } @@ -9794,12 +9764,12 @@ export type SessionRevertStagedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string - revert: RevertStateV2 + revert: SessionRevertV2 } } @@ -9813,7 +9783,7 @@ export type SessionRevertClearedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -9831,7 +9801,7 @@ export type SessionRevertCommittedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -10037,6 +10007,59 @@ export type AgentUpdatedV2 = { | Array } +export type SessionV1InfoV2 = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + share?: { + url: string + } + title: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + version: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + export type SessionCreatedV2 = { id: string created: number @@ -10047,12 +10070,12 @@ export type SessionCreatedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string - info: SessionV2 + info: SessionV1InfoV2 } } @@ -10066,12 +10089,12 @@ export type SessionUpdatedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string - info: SessionV2 + info: SessionV1InfoV2 } } @@ -10085,12 +10108,12 @@ export type SessionDeleted1 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { sessionID: string - info: SessionV2 + info: SessionV1InfoV2 } } @@ -10104,7 +10127,7 @@ export type MessageUpdatedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -10123,7 +10146,7 @@ export type MessageRemovedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -10142,7 +10165,7 @@ export type MessagePartUpdatedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -10162,7 +10185,7 @@ export type MessagePartRemovedV2 = { durable: { aggregateID: string seq: number - version: number + version: 1 } location?: LocationRefV2 data: { @@ -10182,16 +10205,8 @@ export type SessionUsageUpdatedV2 = { location?: LocationRefV2 data: { sessionID: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } + cost: MoneyUsd + tokens: TokenUsageInfo } } @@ -13626,7 +13641,7 @@ export type SessionDiffResponses = { /** * Successfully retrieved diff */ - 200: Array + 200: Array } export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] @@ -15204,7 +15219,7 @@ export type V2AgentListResponses = { */ 200: { location: LocationInfoV2 - data: Array + data: Array } } @@ -15322,7 +15337,7 @@ export type V2SessionCreateResponses = { * Success */ 200: { - data: SessionV2InfoV2 + data: SessionInfoV2 } } @@ -15427,7 +15442,7 @@ export type V2SessionGetResponses = { * Success */ 200: { - data: SessionV2InfoV2 + data: SessionInfoV2 } } @@ -15466,7 +15481,7 @@ export type V2SessionForkResponses = { * Success */ 200: { - data: SessionV2InfoV2 + data: SessionInfoV2 } } @@ -15967,7 +15982,7 @@ export type V2SessionRevertStageResponses = { * Success */ 200: { - data: RevertStateV2 + data: SessionRevertV2 } } @@ -16090,7 +16105,7 @@ export type V2SessionContextResponses = { * Success */ 200: { - data: Array + data: Array } } @@ -16357,7 +16372,7 @@ export type V2SessionMessageResponses = { * Success */ 200: { - data: SessionMessage + data: SessionMessageInfo } } @@ -16447,7 +16462,7 @@ export type V2ModelListResponses = { */ 200: { location: LocationInfoV2 - data: Array + data: Array } } @@ -16488,7 +16503,7 @@ export type V2ModelDefaultResponses = { */ 200: { location: LocationInfoV2 - data: ModelV2Info | null + data: ModelInfo | null } } @@ -17829,7 +17844,7 @@ export type V2CommandListResponses = { */ 200: { location: LocationInfoV2 - data: Array + data: Array } } @@ -17866,7 +17881,7 @@ export type V2SkillListResponses = { */ 200: { location: LocationInfoV2 - data: Array + data: Array } } @@ -18833,7 +18848,7 @@ export type V2VcsDiffResponses = { */ 200: { location: LocationInfoV2 - data: Array + data: Array } } diff --git a/packages/server/src/handlers/message.ts b/packages/server/src/handlers/message.ts index 93734c628d8..40e2ba1077f 100644 --- a/packages/server/src/handlers/message.ts +++ b/packages/server/src/handlers/message.ts @@ -16,7 +16,7 @@ const Cursor = Schema.Struct({ const decodeCursor = Schema.decodeUnknownSync(Cursor) const cursor = { - encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") { + encode(message: SessionMessage.Info, order: "asc" | "desc", direction: "previous" | "next") { return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url") }, decode(input: string) { diff --git a/packages/session-ui/src/components/session-diff.ts b/packages/session-ui/src/components/session-diff.ts index 48e8eee3108..6fccfc305de 100644 --- a/packages/session-ui/src/components/session-diff.ts +++ b/packages/session-ui/src/components/session-diff.ts @@ -1,6 +1,6 @@ import { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs" import { parsePatch } from "diff" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" type LegacyDiff = { file: string @@ -12,8 +12,7 @@ type LegacyDiff = { status?: "added" | "deleted" | "modified" } -type SnapshotDiff = SnapshotFileDiff & { file: string } -type ReviewDiff = SnapshotDiff | VcsFileDiff | LegacyDiff +type ReviewDiff = FileDiffInfo | VcsFileDiff | LegacyDiff export type DiffSource = Pick export type ViewDiff = { diff --git a/packages/session-ui/src/components/session-review.tsx b/packages/session-ui/src/components/session-review.tsx index 8db21f025b7..ec94af2f2d0 100644 --- a/packages/session-ui/src/components/session-review.tsx +++ b/packages/session-ui/src/components/session-review.tsx @@ -15,7 +15,7 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { checksum } from "@opencode-ai/core/util/encode" import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js" import { createStore } from "solid-js/store" -import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2" +import { type FileContent, type FileDiffInfo, type VcsFileDiff } from "@opencode-ai/sdk/v2" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type SelectedLineRange } from "@pierre/diffs" import { Dynamic } from "solid-js/web" @@ -62,14 +62,12 @@ export type SessionReviewCommentActions = { export type SessionReviewFocus = { file: string; id: string } -type RawReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { +type RawReviewDiff = (FileDiffInfo | VcsFileDiff) & { preloaded?: PreloadMultiFileDiffResult } -type ReviewDiff = ((SnapshotFileDiff & { file: string }) | VcsFileDiff) & { +type ReviewDiff = (FileDiffInfo | VcsFileDiff) & { preloaded?: PreloadMultiFileDiffResult } -type Item = ViewDiff & { preloaded?: PreloadMultiFileDiffResult } - function diff(value: unknown): value is ReviewDiff { if (!value || typeof value !== "object" || Array.isArray(value)) return false if (!("file" in value) || typeof value.file !== "string") return false @@ -185,7 +183,7 @@ export const SessionReview = (props: SessionReviewProps) => { const itemsMap = createMemo(() => Object.fromEntries(list(props.diffs).map((diff) => [diff.file, { ...normalize(diff), preloaded: diff.preloaded }])), ) - const files = createMemo(() => props.diffs.map((diff) => diff.file!)) + const files = createMemo(() => props.diffs.map((diff) => diff.file)) const grouped = createMemo(() => { const next = new Map() for (const comment of props.comments ?? []) { diff --git a/packages/session-ui/src/components/session-turn.tsx b/packages/session-ui/src/components/session-turn.tsx index 4ebf6459b15..cda95c0e8e7 100644 --- a/packages/session-ui/src/components/session-turn.tsx +++ b/packages/session-ui/src/components/session-turn.tsx @@ -1,8 +1,9 @@ import { AssistantMessage, - type SnapshotFileDiff, + type FileDiffInfo, Message as MessageType, Part as PartType, + type UserMessage, } from "@opencode-ai/sdk/v2/client" import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" @@ -90,10 +91,17 @@ function list(value: T[] | undefined | null, fallback: T[]) { return fallback } -type SummaryDiff = SnapshotFileDiff & { file: string } +type SummaryDiffInput = NonNullable["diffs"]>[number] +type SummaryDiff = FileDiffInfo -function summaryDiff(value: SnapshotFileDiff): value is SummaryDiff { - return typeof value.file === "string" +function summaryDiff(value: SummaryDiffInput): value is SummaryDiff { + return ( + typeof value.file === "string" && + typeof value.patch === "string" && + typeof value.additions === "number" && + typeof value.deletions === "number" && + value.status !== undefined + ) } const hidden = new Set(["todowrite"]) diff --git a/packages/session-ui/src/context/data.tsx b/packages/session-ui/src/context/data.tsx index 999ff510d5f..d505249931f 100644 --- a/packages/session-ui/src/context/data.tsx +++ b/packages/session-ui/src/context/data.tsx @@ -1,4 +1,4 @@ -import type { Message, Session, Part, SnapshotFileDiff, SessionStatus, Provider } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, Message, Part, Provider, Session, SessionStatus } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "@opencode-ai/ui/context" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -21,7 +21,7 @@ type Data = { [sessionID: string]: SessionStatus } session_diff: { - [sessionID: string]: SnapshotFileDiff[] + [sessionID: string]: FileDiffInfo[] } session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[] diff --git a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx index 3e7bd830519..c05fe3a7454 100644 --- a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx @@ -6,7 +6,7 @@ import { useFileComponent } from "@opencode-ai/ui/context/file" import { useI18n } from "@opencode-ai/ui/context/i18n" import { mediaKindFromPath } from "../../pierre/media" import { cloneSelectedLineRange, previewSelectedLines } from "../../pierre/selection-bridge" -import type { FileContent, SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileContent, FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2" import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" @@ -27,7 +27,7 @@ import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import "./session-review-v2.css" -type ReviewDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff +type ReviewDiff = FileDiffInfo | VcsFileDiff export type SessionReviewFilePreviewV2Props = { file: string diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index 72de26d2780..025c0968b8b 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -1,6 +1,6 @@ import { createMemo, createResource, createSignal, onMount } from "solid-js" import path from "path" -import type { SessionV2Info } from "@opencode-ai/sdk/v2" +import type { SessionInfo } from "@opencode-ai/sdk/v2" import { useDialog } from "../ui/dialog" import { DialogSelect } from "../ui/dialog-select" import { useRoute } from "../context/route" @@ -44,7 +44,7 @@ export function DialogSessionList() { workspace: location.workspaceID, }) // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; session list UI reuses legacy mutable session types. - return { query, sessions: structuredClone(response.data) as SessionV2Info[] } + return { query, sessions: structuredClone(response.data) as SessionInfo[] } }) const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) @@ -77,7 +77,7 @@ export function DialogSessionList() { const pinnedSet = new Set(pinned) const slotByID = new Map(local.session.slots().map((sessionID, index) => [sessionID, index + 1])) - const option = (session: SessionV2Info, category: string) => { + const option = (session: SessionInfo, category: string) => { const directory = session.location.directory const footer = directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : "" const slot = slotByID.get(session.id) @@ -88,12 +88,11 @@ export function DialogSessionList() { category, footer, bg: deleting ? theme.error : undefined, - gutter: - data.session.family(session.id).some((id) => data.session.status(id) === "running") - ? () => - : slot === undefined - ? undefined - : () => {slot}, + gutter: data.session.family(session.id).some((id) => data.session.status(id) === "running") + ? () => + : slot === undefined + ? undefined + : () => {slot}, } } @@ -135,16 +134,14 @@ export function DialogSessionList() { setToDelete(option.value) return } - void sdk.client.v2.session - .remove({ sessionID: option.value }, { throwOnError: true }) - .catch((error) => { - setToDelete(undefined) - toast.show({ - message: `Failed to delete session: ${errorMessage(error)}`, - variant: "error", - duration: 5000, - }) + void sdk.client.v2.session.remove({ sessionID: option.value }, { throwOnError: true }).catch((error) => { + setToDelete(undefined) + toast.show({ + message: `Failed to delete session: ${errorMessage(error)}`, + variant: "error", + duration: 5000, }) + }) }, }, { diff --git a/packages/tui/src/component/dialog-skill.tsx b/packages/tui/src/component/dialog-skill.tsx index 61c173a3842..bb54dd87133 100644 --- a/packages/tui/src/component/dialog-skill.tsx +++ b/packages/tui/src/component/dialog-skill.tsx @@ -45,10 +45,10 @@ export function DialogSkill(props: DialogSkillProps) { return list.map((skill) => ({ title: skill.name.padEnd(maxWidth), description: skill.description?.replace(/\s+/g, " ").trim(), - value: skill.name, + value: skill.id, category: "Skills", onSelect: () => { - props.onSelect(skill.name) + props.onSelect(skill.id) dialog.clear() }, })) diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 65bff40838d..1b86dbe5822 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -448,12 +448,12 @@ export function Autocomplete(props: { for (const skill of data.location.skill .list(location()) - ?.filter((skill) => skill.slash === true && !commandNames.has(skill.name)) ?? []) { + ?.filter((skill) => skill.slash === true && !commandNames.has(skill.id)) ?? []) { results.push({ - display: "/" + skill.name, + display: "/" + skill.id, description: skill.description, onSelect: () => { - const newText = "/" + skill.name + " " + const newText = "/" + skill.id + " " const cursor = props.input().logicalCursor props.input().deleteRange(0, 0, cursor.row, cursor.col) props.input().insertText(newText) diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 14b34f93c28..0fcbfa96d58 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -37,7 +37,7 @@ import { usePromptStash } from "../../prompt/stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import type { SessionV2Info, UserMessage } from "@opencode-ai/sdk/v2" +import type { SessionInfo, UserMessage } from "@opencode-ai/sdk/v2" import { Locale } from "../../util/locale" import { errorMessage } from "../../util/error" import { createColors, createFrames } from "../../ui/spinner" @@ -165,9 +165,9 @@ export function Prompt(props: PromptProps) { const status = createMemo(() => data.session.status(props.sessionID ?? "")) const activeSubagents = createMemo(() => { if (!props.sessionID) return 0 - return data.session.family(props.sessionID).filter( - (id) => id !== props.sessionID && data.session.status(id) === "running", - ).length + return data.session + .family(props.sessionID) + .filter((id) => id !== props.sessionID && data.session.status(id) === "running").length }) const runningShells = createMemo( () => data.shell.list(currentLocation()).filter((shell) => shell.metadata.sessionID === props.sessionID).length, @@ -1028,7 +1028,7 @@ export function Prompt(props: PromptProps) { sessionID = created.id // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- generated client output is readonly; prompt state still uses legacy mutable session types. - session = structuredClone(created) as SessionV2Info + session = structuredClone(created) as SessionInfo } const inputText = expandTrackedPastedText( @@ -1099,7 +1099,7 @@ export function Prompt(props: PromptProps) { } else if ( inputText.startsWith("/") && (data.location.skill.list(currentLocation()) ?? []).some( - (skill) => skill.slash === true && skill.name === inputText.split("\n")[0].split(" ")[0].slice(1), + (skill) => skill.slash === true && skill.id === inputText.split("\n")[0].split(" ")[0].slice(1), ) ) { move.startSubmit() diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 22f505682db..da44a0f8e95 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -1,24 +1,24 @@ import type { - AgentV2Info, - CommandV2Info, + AgentInfo, + CommandInfo, FormFormInfo, FormUrlInfo, IntegrationInfo, LocationRef, McpServer, - ModelV2Info, + ModelInfo, PermissionSavedInfo, PermissionV2Request, ProviderV2Info, ReferenceInfo, - SessionMessage, + SessionMessageInfo, SessionMessageAssistant, SessionMessageAssistantReasoning, SessionMessageAssistantText, SessionMessageAssistantTool, - SessionV2Info, + SessionInfo, Shell, - SkillV2Info, + SkillInfo, V2Event, } from "@opencode-ai/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" @@ -33,30 +33,28 @@ const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_") export type FormInfo = FormFormInfo | FormUrlInfo type LocationData = { - agent?: AgentV2Info[] - command?: CommandV2Info[] + agent?: AgentInfo[] + command?: CommandInfo[] integration?: IntegrationInfo[] mcp?: McpServer[] - model?: ModelV2Info[] + model?: ModelInfo[] provider?: ProviderV2Info[] reference?: ReferenceInfo[] // Currently running shell commands for this location, keyed by shell id. Entries are removed // once the command exits or is deleted, so this only ever holds in-flight shells. shell?: Record - skill?: SkillV2Info[] + skill?: SkillInfo[] } type Data = { session: { - info: Record + info: Record // Family index keyed by a family's root (or furthest-known-ancestor when the // true root is not yet loaded). The value is a flat deduplicated list of every // session ID in that family, including the key itself once its info arrives. family: Record status: Record - compaction: Partial> - compactionReason: Partial> - message: Record + message: Record input: Record permission: Record // Pending forms keyed by session ID. @@ -92,8 +90,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ info: {}, family: {}, status: {}, - compaction: {}, - compactionReason: {}, message: {}, input: {}, permission: {}, @@ -112,7 +108,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ const messageIndex = new Map>() const sessionRefreshGeneration = new Map() const sessionRefreshApplied = new Map() - const sessionUsage = new Map() + const sessionUsage = new Map() let connectionGeneration = 0 let statusChanges: Set | undefined let bootstrapping: Promise | undefined @@ -134,14 +130,14 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return true } - function updateSessionUsage(sessionID: string, cost: number, tokens: SessionV2Info["tokens"]) { + function updateSessionUsage(sessionID: string, cost: number, tokens: SessionInfo["tokens"]) { sessionUsage.set(sessionID, { generation: (sessionUsage.get(sessionID)?.generation ?? 0) + 1, cost, tokens }) if (!store.session.info[sessionID]) return setStore("session", "info", sessionID, { cost, tokens }) } const message = { - update(sessionID: string, fn: (messages: SessionMessage[], index: Map) => void) { + update(sessionID: string, fn: (messages: SessionMessageInfo[], index: Map) => void) { setStore( "session", "message", @@ -150,28 +146,26 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }), ) }, - append(messages: SessionMessage[], index: Map, item: SessionMessage) { + append(messages: SessionMessageInfo[], index: Map, item: SessionMessageInfo) { if (index.has(item.id)) return index.set(item.id, messages.length) messages.push(item) }, - activeAssistant(messages: SessionMessage[]) { + activeAssistant(messages: SessionMessageInfo[]) { const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed) return item?.type === "assistant" ? item : undefined }, - assistant(messages: SessionMessage[], index: Map, messageID: string) { + assistant(messages: SessionMessageInfo[], index: Map, messageID: string) { const position = index.get(messageID) const item = position === undefined ? undefined : messages[position] return item?.type === "assistant" ? item : undefined }, - shell(messages: SessionMessage[], shellID: string) { - const item = messages.findLast((item) => item.type === "shell" && item.shell.id === shellID) + shell(messages: SessionMessageInfo[], shellID: string) { + const item = messages.findLast((item) => item.type === "shell" && item.shellID === shellID) return item?.type === "shell" ? item : undefined }, - compaction(messages: SessionMessage[]) { - const item = messages.findLast( - (item) => item.type === "compaction" && (item.status === "queued" || item.status === "running"), - ) + compaction(messages: SessionMessageInfo[]) { + const item = messages.findLast((item) => item.type === "compaction" && item.status === "running") return item?.type === "compaction" ? item : undefined }, latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { @@ -251,7 +245,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ produce((draft) => { delete draft.info[sessionID] delete draft.status[sessionID] - delete draft.compaction[sessionID] delete draft.message[sessionID] delete draft.input[sessionID] delete draft.permission[sessionID] @@ -381,6 +374,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ id: messageIDFromEvent(event.id), type: "system", text: event.data.text, + metadata: event.metadata, time: { created: event.created }, }) }) @@ -390,9 +384,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.append(draft, index, { id: messageIDFromEvent(event.id), type: "synthetic", - sessionID: event.data.sessionID, text: event.data.text, description: event.data.description, + metadata: event.data.metadata, time: { created: event.created }, }) }) @@ -402,7 +396,11 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.append(draft, index, { id: messageIDFromEvent(event.id), type: "shell", - shell: event.data.shell, + shellID: event.data.shell.id, + command: event.data.shell.command, + status: event.data.shell.status, + exit: event.data.shell.exit, + metadata: event.metadata, time: { created: event.created }, }) }) @@ -411,7 +409,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.update(event.data.sessionID, (draft) => { const match = message.shell(draft, event.data.shell.id) if (!match) return - match.shell = event.data.shell + match.status = event.data.shell.status + match.exit = event.data.shell.exit match.output = event.data.output match.time.completed = event.created }) @@ -440,6 +439,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ type: "assistant", agent: event.data.agent, model: event.data.model, + metadata: event.metadata, content: [], snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, time: { created: event.created }, @@ -500,7 +500,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ id: event.data.callID, name: event.data.name, time: { created: event.created }, - state: { status: "pending", input: "" }, + state: { status: "streaming", input: "" }, }) }) break @@ -510,7 +510,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) - if (match?.state.status === "pending") match.state.input += event.data.delta + if (match?.state.status === "streaming") match.state.input += event.data.delta }) break case "session.tool.input.ended": @@ -519,7 +519,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) - if (match?.state.status === "pending") match.state.input = event.data.text + if (match?.state.status === "streaming") match.state.input = event.data.text }) break case "session.tool.called": @@ -571,7 +571,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ message.assistant(draft, index, event.data.assistantMessageID), event.data.callID, ) - if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return + if (!match || (match.state.status !== "streaming" && match.state.status !== "running")) return match.state = { status: "error", error: event.data.error, @@ -626,36 +626,24 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ setSessionStatus(event.data.sessionID, "running") break case "session.compaction.admitted": + break + case "session.compaction.started": message.update(event.data.sessionID, (draft, index) => { - if (message.compaction(draft)) return message.append(draft, index, { - id: event.data.inputID, + id: event.data.inputID ?? messageIDFromEvent(event.id), type: "compaction", - status: "queued", - reason: "manual", + status: "running", + reason: event.data.reason, summary: "", - recent: "", + recent: event.data.recent ?? "", time: { created: event.created }, }) }) break - case "session.compaction.started": - setStore("session", "compaction", event.data.sessionID, "") - setStore("session", "compactionReason", event.data.sessionID, event.data.reason) - if (event.data.reason === "manual") - message.update(event.data.sessionID, (draft) => { - const current = message.compaction(draft) - if (current) current.status = "running" - }) - break case "session.execution.succeeded": case "session.execution.failed": case "session.execution.interrupted": setSessionStatus(event.data.sessionID, "idle") - if (store.session.compaction[event.data.sessionID] !== undefined) - setStore("session", "compaction", event.data.sessionID, undefined) - if (store.session.compactionReason[event.data.sessionID] !== undefined) - setStore("session", "compactionReason", event.data.sessionID, undefined) message.update(event.data.sessionID, (draft) => { const currentAssistant = message.activeAssistant(draft) if (currentAssistant) currentAssistant.retry = undefined @@ -686,23 +674,26 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }) break case "session.compaction.delta": - setStore("session", "compaction", event.data.sessionID, (text) => (text ?? "") + event.data.text) - if (store.session.compactionReason[event.data.sessionID] === "manual") - message.update(event.data.sessionID, (draft) => { - const current = message.compaction(draft) - if (current) current.summary += event.data.text - }) + message.update(event.data.sessionID, (draft) => { + const current = message.compaction(draft) + if (current?.status === "running") current.summary += event.data.text + }) break case "session.compaction.ended": - setStore("session", "compaction", event.data.sessionID, undefined) - setStore("session", "compactionReason", event.data.sessionID, undefined) message.update(event.data.sessionID, (draft, index) => { - const current = event.data.reason === "manual" ? message.compaction(draft) : undefined - if (current) { - current.status = "completed" - current.reason = event.data.reason - current.summary = event.data.text - current.recent = event.data.recent + const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running") + const current = draft[position] + if (current?.type === "compaction") { + draft[position] = { + id: current.id, + type: "compaction", + status: "completed", + reason: event.data.reason, + summary: event.data.text, + recent: event.data.recent, + metadata: current.metadata, + time: current.time, + } return } message.append(draft, index, { @@ -717,11 +708,26 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ }) break case "session.compaction.failed": - setStore("session", "compaction", event.data.sessionID, undefined) - setStore("session", "compactionReason", event.data.sessionID, undefined) - message.update(event.data.sessionID, (draft) => { - const current = message.compaction(draft) - if (current) current.status = "failed" + message.update(event.data.sessionID, (draft, index) => { + const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running") + const current = draft[position] + const failed: Extract = { + id: current?.id ?? event.data.inputID ?? messageIDFromEvent(event.id), + type: "compaction", + status: "failed", + reason: event.data.reason ?? "manual", + error: event.data.error ?? { + type: "compaction.failed", + message: "Compaction failed before recording an error", + }, + metadata: current?.type === "compaction" ? current.metadata : event.metadata, + time: current?.type === "compaction" ? current.time : { created: event.created }, + } + if (current?.type === "compaction") { + draft[position] = failed + return + } + message.append(draft, index, failed) }) break case "permission.v2.asked": @@ -839,9 +845,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ return store.session.input[sessionID]?.includes(inputID) ?? false }, }, - compaction(sessionID: string) { - return store.session.compaction[sessionID] - }, async refresh(sessionID: string) { const generation = nextSessionRefresh(sessionID) const usageGeneration = sessionUsage.get(sessionID)?.generation ?? 0 @@ -886,14 +889,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ ].toSorted((a, b) => a.time.created - b.time.created) messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index]))) setStore("session", "message", sessionID, messages) - const running = messages.find((message) => message.type === "compaction" && message.status === "running") - setStore("session", "compaction", sessionID, running?.type === "compaction" ? running.summary : undefined) - setStore( - "session", - "compactionReason", - sessionID, - running?.type === "compaction" ? running.reason : undefined, - ) }, }, permission: { diff --git a/packages/tui/src/context/sync.tsx b/packages/tui/src/context/sync.tsx index b218660e9f0..40eac011ae5 100644 --- a/packages/tui/src/context/sync.tsx +++ b/packages/tui/src/context/sync.tsx @@ -15,7 +15,7 @@ import type { ProviderListResponse, QuestionRequest, Session, - SnapshotFileDiff, + FileDiffInfo, Todo, VcsInfo, } from "@opencode-ai/sdk/v2" @@ -49,7 +49,7 @@ export const { question: Record config: Config session: Session[] - session_diff: Record + session_diff: Record todo: Record message: Record part: Record diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index ed88a1107f9..1bee45dedc4 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @opentui/solid */ import type { TuiPlugin, TuiPluginApi, TuiRouteCurrent } from "@opencode-ai/plugin/tui" -import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo, SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" import { TextAttributes, type BorderSides, @@ -56,7 +56,7 @@ type DiffFile = { readonly status: "added" | "deleted" | "modified" } -const normalizeDiffs = (diffs: readonly (VcsFileDiff | SnapshotFileDiff)[]): DiffFile[] => +const normalizeDiffs = (diffs: readonly (VcsFileDiff | FileDiffInfo | SnapshotFileDiff)[]): DiffFile[] => diffs.flatMap((item) => item.file ? [ diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 3743abed059..e37fae92494 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -26,14 +26,14 @@ import { createSyntaxStyleMemo, generateSubtleSyntax, useTheme } from "../../con import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" import { Prompt, type PromptRef } from "../../component/prompt" import type { - ModelV2Info, - SessionMessage, + ModelInfo, + SessionMessageInfo, SessionMessageAssistant, SessionMessageAssistantReasoning, SessionMessageAssistantText, SessionMessageAssistantTool, SessionMessageUser, - SessionV2Info, + SessionInfo, } from "@opencode-ai/sdk/v2" import { useLocal } from "../../context/local" import { Locale } from "../../util/locale" @@ -130,7 +130,7 @@ const context = createContext<{ showGenericToolOutput: () => boolean groupExploration: () => boolean diffWrapMode: () => "word" | "none" - models: () => ModelV2Info[] + models: () => ModelInfo[] tui: ReturnType }>() @@ -172,16 +172,6 @@ export function Session() { }) onCleanup(() => setEpilogue()) const messages = sessionMessages - const transientCompaction = createMemo(() => { - if ( - messages().some( - (message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"), - ) - ) - return - const text = data.session.compaction(route.sessionID) - return text === undefined ? undefined : { text } - }) const descendantSessionIDs = createMemo(() => { if (session()?.parentID) return [] return data.session.family(route.sessionID).filter((id) => id !== route.sessionID) @@ -936,9 +926,6 @@ export function Session() { /> )} - - {(compaction) => } - SessionMessage | undefined }) { +function SessionRowView(props: { row: SessionRow; message: (messageID: string) => SessionMessageInfo | undefined }) { return ( @@ -1062,7 +1049,7 @@ function SessionRowView(props: { row: SessionRow; message: (messageID: string) = ) } -function BackgroundToolHint(props: { messages: SessionMessage[] }) { +function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) { const { theme } = useTheme() const shortcut = useCommandShortcut("session.background") const visible = createMemo(() => { @@ -1090,14 +1077,14 @@ function BackgroundToolHint(props: { messages: SessionMessage[] }) { ) } -function SessionMessageView(props: { message: SessionMessage }) { +function SessionMessageView(props: { message: SessionMessageInfo }) { return ( - } /> + } /> @@ -1106,17 +1093,17 @@ function SessionMessageView(props: { message: SessionMessage }) { when={props.message.type === "system" || props.message.type === "synthetic" || props.message.type === "skill"} > }> - } /> + } /> - } /> + } /> ) } -function SessionPartView(props: { partRef: PartRef; message: (messageID: string) => SessionMessage | undefined }) { +function SessionPartView(props: { partRef: PartRef; message: (messageID: string) => SessionMessageInfo | undefined }) { const message = createMemo(() => props.message(props.partRef.messageID)) const part = createMemo(() => { const item = message() @@ -1150,7 +1137,7 @@ function SessionGroupView(props: { refs: PartRef[] pending: PartRef[] completed: boolean - message: (messageID: string) => SessionMessage | undefined + message: (messageID: string) => SessionMessageInfo | undefined }) { const { theme } = useTheme() const ctx = use() @@ -1260,7 +1247,7 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) { ) } -function SessionSwitchMessageV2(props: { message: SessionMessage }) { +function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) { const ctx = use() const { theme } = useTheme() const text = () => { @@ -1276,7 +1263,7 @@ function SessionSwitchMessageV2(props: { message: SessionMessage }) { ) } -function SessionNoticeMessageV2(props: { message: SessionMessage }) { +function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) { const { theme } = useTheme() const text = () => { if (props.message.type === "system") return "Instructions updated" @@ -1290,7 +1277,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessage }) { ) } -function SessionSkillMessage(props: { message: Extract }) { +function SessionSkillMessage(props: { message: Extract }) { const { theme } = useTheme() return ( @@ -1300,7 +1287,7 @@ function SessionSkillMessage(props: { message: Extract + message?: Extract status?: "running" text?: string }) { @@ -1308,9 +1295,10 @@ function CompactionMessage(props: { const kv = useKV() const { theme, syntax } = useTheme() const status = () => props.message?.status ?? props.status - const text = () => props.message?.summary ?? props.text ?? "" + const text = () => + props.message?.status === "failed" ? props.message.error.message : (props.message?.summary ?? props.text ?? "") const color = () => (status() === "failed" ? theme.error : status() === "completed" ? theme.success : theme.textMuted) - const border = () => (status() === "queued" ? theme.border : color()) + const border = color return ( @@ -1328,11 +1316,8 @@ function CompactionMessage(props: { - - - - {status() === "queued" ? "Compaction queued" : "Compaction"} + Compaction @@ -1363,7 +1348,7 @@ function statusLabel(status: "added" | "modified" | "deleted") { function RevertMessage(props: { count: number files: ReadonlyArray<{ - readonly path: string + readonly file: string readonly status: "added" | "modified" | "deleted" readonly additions: number readonly deletions: number @@ -1412,7 +1397,7 @@ function RevertMessage(props: { {statusLabel(file.status)} - {Locale.truncateLeft(file.path, 60)} + {Locale.truncateLeft(file.file, 60)} 0}> +{file.additions} @@ -1433,7 +1418,7 @@ function RevertMessage(props: { ) } -function ShellMessage(props: { message: Extract }) { +function ShellMessage(props: { message: Extract }) { const { theme } = useTheme() const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? "")) @@ -1448,7 +1433,7 @@ function ShellMessage(props: { message: Extract - $ {props.message.shell.command} + $ {props.message.command} {output()} @@ -1560,7 +1545,7 @@ function AssistantMessage(props: { message: SessionMessageAssistant; last: boole .map((part) => part.type === "tool" && ["read", "glob", "grep"].includes(toolDisplay(part.name)) && - part.state.status !== "pending" + part.state.status !== "streaming" ? part : undefined, ) @@ -1832,7 +1817,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) { const data = useData() const display = createMemo(() => toolDisplay(props.part.name)) const activeBackgroundWork = createMemo(() => { - if (props.part.state.status === "pending") return false + if (props.part.state.status === "streaming") return false if (display() === "shell") { const shellID = stringValue(props.part.state.structured.shellID) return Boolean(shellID && data.shell.get(shellID)) @@ -1856,13 +1841,13 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) { const toolprops = { get metadata() { - return props.part.state.status === "pending" ? {} : props.part.state.structured + return props.part.state.status === "streaming" ? {} : props.part.state.structured }, get input() { return typeof props.part.state.input === "string" ? {} : props.part.state.input }, get output() { - if (props.part.state.status === "pending") return undefined + if (props.part.state.status === "streaming") return undefined return props.part.state.content .flatMap((content) => (content.type === "text" ? [content.text] : [content.name ?? content.uri])) .join("\n") @@ -2187,7 +2172,7 @@ function Shell(props: ToolProps) { const isRunning = createMemo(() => props.part.state.status === "running" || backgroundRunning()) const command = createMemo(() => stringValue(props.input.command)) const output = createMemo(() => { - if (props.part.state.status === "pending") return "" + if (props.part.state.status === "streaming") return "" if (shellID()) return "" const content = props.part.state.content[0] return stripAnsi(content?.type === "text" ? content.text.trim() : "") @@ -2209,7 +2194,7 @@ function Shell(props: ToolProps) { Writing command... ) : ( Writing command... @@ -2418,7 +2403,7 @@ function executeCalls(value: unknown): ExecuteCall[] { function Execute(props: ToolProps) { const ctx = use() const { theme } = useTheme() - const isLoading = createMemo(() => props.part.state.status === "pending" || props.part.state.status === "running") + const isLoading = createMemo(() => props.part.state.status === "streaming" || props.part.state.status === "running") const calls = createMemo(() => executeCalls(props.metadata.toolCalls)) const output = createMemo(() => stripAnsi(props.output?.trim() ?? "")) const hasRuntimeError = createMemo(() => props.metadata.error === true) @@ -2514,7 +2499,7 @@ function Edit(props: ToolProps) { : "# Preparing edit..." } part={props.part} - spinner={props.part.state.status === "pending"} + spinner={props.part.state.status === "streaming"} /> @@ -2606,7 +2591,7 @@ function ApplyPatch(props: ToolProps) { : "# Preparing patch..." } part={props.part} - spinner={props.part.state.status === "pending"} + spinner={props.part.state.status === "streaming"} /> @@ -2756,15 +2741,15 @@ function recordValue(value: unknown): Record | undefined { } function formatSessionTranscript( - session: SessionV2Info, - messages: SessionMessage[], + session: SessionInfo, + messages: SessionMessageInfo[], thinking: boolean, toolDetails: boolean, ) { const body = messages.flatMap((message) => { if (message.type === "user") return [`## User\n\n${message.text}`] if (message.type === "shell") - return [`## Shell\n\n\`\`\`\n$ ${message.shell.command}\n${message.output?.output ?? ""}\n\`\`\``] + return [`## Shell\n\n\`\`\`\n$ ${message.command}\n${message.output?.output ?? ""}\n\`\`\``] if (message.type !== "assistant") return [] const content = message.content.flatMap((item) => { if (item.type === "text") return [item.text] @@ -2774,7 +2759,7 @@ function formatSessionTranscript( const output = item.state.status === "error" ? item.state.error.message - : item.state.status === "pending" + : item.state.status === "streaming" ? "" : item.state.content .flatMap((entry) => (entry.type === "text" ? [entry.text] : [entry.name ?? entry.uri])) diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index a137d6272ab..a0ca4775c3b 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -148,7 +148,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director const message = data.session.message.get(props.request.sessionID, tool.messageID) if (message?.type !== "assistant") return {} const part = message.content.find((part) => part.type === "tool" && part.id === tool.callID) - if (part?.type === "tool" && part.state.status !== "pending") return part.state.input + if (part?.type === "tool" && part.state.status !== "streaming") return part.state.input return {} }) diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 5d2a3614520..c8764f3030a 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -1,4 +1,4 @@ -import type { SessionMessage, SessionMessageAssistant } from "@opencode-ai/sdk/v2" +import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2" import { createEffect, on, onCleanup, type Accessor } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useData } from "../../context/data" @@ -88,7 +88,7 @@ export function createSessionRows(sessionID: Accessor) { { id: message.id, created: message.time.created, - input: message.status === "queued" || message.status === "running", + input: message.status === "running", }, ] : [], @@ -157,7 +157,7 @@ export function createSessionRows(sessionID: Accessor) { const isPending = (messageID: string) => { const message = data.session.message.get(sessionID(), messageID) if (message?.type === "user") return data.session.input.has(sessionID(), messageID) - return message?.type === "compaction" && (message.status === "queued" || message.status === "running") + return message?.type === "compaction" && message.status === "running" } const queuedStart = (rows: SessionRow[]) => { @@ -173,7 +173,7 @@ export function createSessionRows(sessionID: Accessor) { } const subscriptions = [ data.on("session.prompt.admitted", input), - data.on("session.compaction.admitted", input), + data.on("session.compaction.started", message), data.on("session.instructions.updated", message), data.on("session.synthetic", (event) => { if (event.data.sessionID === sessionID() && event.data.description?.trim()) @@ -224,11 +224,9 @@ export function createSessionRows(sessionID: Accessor) { return rows } -export function reduceSessionRows(messages: SessionMessage[], inputs = new Set()) { - const isInput = (message: SessionMessage) => inputs.has(message.id) - const pendingCompactions = messages.filter( - (message) => message.type === "compaction" && (message.status === "queued" || message.status === "running"), - ) +export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set()) { + const isInput = (message: SessionMessageInfo) => inputs.has(message.id) + const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) return [ ...messages.filter((message) => !pending.has(message.id)), diff --git a/packages/tui/src/util/session.ts b/packages/tui/src/util/session.ts index 3a49ea1a9cb..41f583b1c67 100644 --- a/packages/tui/src/util/session.ts +++ b/packages/tui/src/util/session.ts @@ -1,17 +1,14 @@ -import type { SessionMessage, SessionMessageAssistant } from "@opencode-ai/sdk/v2" +import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2" export function isDefaultTitle(title: string) { return /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title) } -export function lastAssistantWithUsage(messages: ReadonlyArray, boundary?: string) { +export function lastAssistantWithUsage(messages: ReadonlyArray, boundary?: string) { const boundaryIndex = boundary ? messages.findIndex((message) => message.id === boundary) : -1 if (boundary && boundaryIndex === -1) return undefined return messages.findLast( - ( - message, - index, - ): message is SessionMessageAssistant & { tokens: NonNullable } => + (message, index): message is SessionMessageAssistant & { tokens: NonNullable } => message.type === "assistant" && message.tokens !== undefined && (boundaryIndex === -1 || index < boundaryIndex), ) } diff --git a/packages/tui/src/util/tool-display.ts b/packages/tui/src/util/tool-display.ts index 18e256bd34f..c9d92434e20 100644 --- a/packages/tui/src/util/tool-display.ts +++ b/packages/tui/src/util/tool-display.ts @@ -6,7 +6,7 @@ export function webSearchProviderLabel(provider: unknown) { export function toolDisplayMetadata(state: unknown): Record { if (!state || typeof state !== "object" || Array.isArray(state)) return {} - if (!("status" in state) || state.status === "pending") return {} + if (!("status" in state) || state.status === "streaming") return {} if (!("structured" in state) || !state.structured || typeof state.structured !== "object") return {} if (Array.isArray(state.structured)) return {} return state.structured as Record diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index c08b557eee3..ac3abc95815 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -93,7 +93,7 @@ function permission(id: string, sessionID = "session"): PermissionRequest { } } -function durable(sessionID: string) { +function durable(sessionID: string): { aggregateID: string; seq: number; version: 1 } { return { aggregateID: sessionID, seq: 0, version: 1 } } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index fdd5980fb50..c116f431787 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -24,6 +24,12 @@ function emitEvent(events: ReturnType, event: V2Event) events.emit({ ...event, location: { directory } }) } +function durable(sessionID: string, seq?: number): { aggregateID: string; seq: number; version: 1 } +function durable( + sessionID: string, + seq: number, + version: Version, +): { aggregateID: string; seq: number; version: Version } function durable(sessionID: string, seq = 0, version = 1) { return { aggregateID: sessionID, seq, version } } @@ -265,7 +271,7 @@ test("applies absolute usage events without losing full session updates", async id: "evt_usage_deleted", created: 9, type: "session.deleted", - durable: durable(sessionID, 9), + durable: durable(sessionID, 9, 2), data: { sessionID }, }) await Bun.sleep(20) @@ -513,7 +519,11 @@ test("restores running manual compaction before applying live deltas", async () try { await data.session.message.refresh("session-compaction") - expect(data.session.compaction("session-compaction")).toBe("Existing ") + expect(data.session.message.get("session-compaction", "message-compaction")).toMatchObject({ + type: "compaction", + status: "running", + summary: "Existing ", + }) emitEvent(events, { id: "evt_compaction_delta", @@ -524,7 +534,7 @@ test("restores running manual compaction before applying live deltas", async () await wait(() => { const message = data.session.message.get("session-compaction", "message-compaction") - return message?.type === "compaction" && message.summary === "Existing summary" + return message?.type === "compaction" && message.status === "running" && message.summary === "Existing summary" }) } finally { app.renderer.destroy() @@ -904,7 +914,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_step_ended", created: 0, type: "session.step.ended", - durable: durable("session-live", 1, 2), + durable: durable("session-live", 1), data: { sessionID: "session-live", assistantMessageID: "message-live", @@ -938,7 +948,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_execution_succeeded", created: 0, type: "session.execution.succeeded", - durable: durable("session-live", 1, 3), + durable: durable("session-live", 1), data: { sessionID: "session-live" }, }) await wait(() => data.session.status("session-live") === "idle") @@ -969,7 +979,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_step_failed", created: 0, type: "session.step.failed", - durable: durable("session-failed", 1, 2), + durable: durable("session-failed", 1), data: { sessionID: "session-failed", assistantMessageID: "message-failed", @@ -1009,7 +1019,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_failed_execution_failed", created: 0, type: "session.execution.failed", - durable: durable("session-failed", 1, 3), + durable: durable("session-failed", 1), data: { sessionID: "session-failed", error: { type: "provider.content-filter", message: "Provider blocked the response" }, @@ -1028,7 +1038,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_retry_step_started", created: 0, type: "session.step.started", - durable: durable("session-retry", 1, 2), + durable: durable("session-retry", 1), data: { sessionID: "session-retry", assistantMessageID: "message-retry", @@ -1040,7 +1050,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_retry_scheduled", created: 0, type: "session.retry.scheduled", - durable: durable("session-retry", 1, 3), + durable: durable("session-retry", 1), data: { sessionID: "session-retry", assistantMessageID: "message-retry", @@ -1058,7 +1068,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_retry_next_step", created: 2_000, type: "session.step.started", - durable: durable("session-retry", 1, 4), + durable: durable("session-retry", 1), data: { sessionID: "session-retry", assistantMessageID: "message-retry", @@ -1076,7 +1086,7 @@ test("tracks session status from active sessions and execution events", async () id: "evt_retry_scheduled_again", created: 2_000, type: "session.retry.scheduled", - durable: durable("session-retry", 1, 5), + durable: durable("session-retry", 1), data: { sessionID: "session-retry", assistantMessageID: "message-retry", @@ -1093,29 +1103,18 @@ test("tracks session status from active sessions and execution events", async () id: "evt_retry_interrupted", created: 2_000, type: "session.execution.interrupted", - durable: durable("session-retry", 1, 6), + durable: durable("session-retry", 1), data: { sessionID: "session-retry", reason: "shutdown" }, }) await wait(() => data.session.status("session-retry") === "idle") expect(data.session.message.get("session-retry", "message-retry")).not.toHaveProperty("retry") - emitEvent(events, { - id: "evt_compaction_admitted", - created: 0, - type: "session.compaction.admitted", - durable: durable("session-manual", 1), - data: { sessionID: "session-manual", inputID: "message-compaction" }, - }) - await wait(() => { - const message = data.session.message.get("session-manual", "message-compaction") - return message?.type === "compaction" && message.status === "queued" - }) emitEvent(events, { id: "evt_manual_compaction_started", created: 1, type: "session.compaction.started", durable: durable("session-manual", 2), - data: { sessionID: "session-manual", reason: "manual" }, + data: { sessionID: "session-manual", reason: "manual", recent: "", inputID: "message-compaction" }, }) emitEvent(events, { id: "evt_manual_compaction_delta", @@ -1125,13 +1124,13 @@ test("tracks session status from active sessions and execution events", async () }) await wait(() => { const message = data.session.message.get("session-manual", "message-compaction") - return message?.type === "compaction" && message.summary === "Streamed summary" + return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary" }) emitEvent(events, { id: "evt_manual_compaction_ended", created: 3, type: "session.compaction.ended", - durable: durable("session-manual", 3), + durable: durable("session-manual", 4), data: { sessionID: "session-manual", reason: "manual", text: "Streamed summary", recent: "recent" }, }) await wait(() => { @@ -1147,7 +1146,7 @@ test("tracks session status from active sessions and execution events", async () created: 0, type: "session.compaction.started", durable: durable("session-live", 2), - data: { sessionID: "session-live", reason: "auto" }, + data: { sessionID: "session-live", reason: "auto", recent: "" }, }) emitEvent(events, { id: "evt_compaction_delta_1", @@ -1161,18 +1160,25 @@ test("tracks session status from active sessions and execution events", async () type: "session.compaction.delta", data: { sessionID: "session-live", text: "summary" }, }) - await wait(() => data.session.compaction("session-live") === "Live summary") + await wait(() => { + const message = data.session.message.get("session-live", "msg_compaction_started") + return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary" + }) emitEvent(events, { id: "evt_compaction_ended", created: 0, type: "session.compaction.ended", - durable: durable("session-live", 3), + durable: durable("session-live", 5), data: { sessionID: "session-live", reason: "auto", text: "Live summary", recent: "recent" }, }) - await wait(() => data.session.compaction("session-live") === undefined) - expect(data.session.message.get("session-live", "msg_compaction_ended")).toMatchObject({ + await wait(() => { + const message = data.session.message.get("session-live", "msg_compaction_started") + return message?.type === "compaction" && message.status === "completed" + }) + expect(data.session.message.get("session-live", "msg_compaction_started")).toMatchObject({ type: "compaction", + status: "completed", summary: "Live summary", }) } finally { diff --git a/packages/tui/test/cli/tui/session-rows.test.ts b/packages/tui/test/cli/tui/session-rows.test.ts index 362ab40369a..c43a6faf9df 100644 --- a/packages/tui/test/cli/tui/session-rows.test.ts +++ b/packages/tui/test/cli/tui/session-rows.test.ts @@ -1,9 +1,9 @@ import { expect, test } from "bun:test" -import type { SessionMessage, SessionMessageAssistant } from "@opencode-ai/sdk/v2" +import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2" import { reduceSessionRows } from "../../../src/routes/session/rows" test("groups exploration parts across assistant messages until a delimiter", () => { - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ { type: "user", id: "user-1", text: "Explore", time: { created: 0 } }, assistant("assistant-1", [ { type: "text", text: "Looking" }, @@ -35,7 +35,7 @@ test("groups exploration parts across assistant messages until a delimiter", () }) test("keeps non-exploration tools as individual part rows", () => { - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ assistant("assistant-1", [ { type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 1 } }, { type: "tool", id: "bash-1", name: "bash", state: pending(), time: { created: 2 } }, @@ -63,7 +63,7 @@ test("keeps non-exploration tools as individual part rows", () => { }) test("assigns stable kind ordinals within an assistant message", () => { - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ assistant("assistant-1", [ { type: "text", text: "First" }, { type: "reasoning", text: "Think" }, @@ -81,7 +81,7 @@ test("assigns stable kind ordinals within an assistant message", () => { }) test("groups across empty assistant reasoning parts", () => { - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ assistant("assistant-1", [ { type: "reasoning", text: "Looking" }, { type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 2 } }, @@ -112,7 +112,7 @@ test("completes exploration groups when another row follows", () => { { type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } }, ]) finished.finish = "stop" - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ assistant("assistant-1", [{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 1 } }]), { type: "user", id: "user-1", text: "Continue", time: { created: 2 } }, finished, @@ -139,12 +139,11 @@ test("completes exploration groups when another row follows", () => { }) test("hides synthetic messages without descriptions", () => { - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ assistant("assistant-1", [{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 1 } }]), { type: "synthetic", id: "synthetic-1", - sessionID: "session-1", text: "internal context", time: { created: 2 }, }, @@ -166,12 +165,11 @@ test("hides synthetic messages without descriptions", () => { }) test("renders synthetic messages with descriptions", () => { - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ assistant("assistant-1", [{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 1 } }]), { type: "synthetic", id: "synthetic-1", - sessionID: "session-1", text: "internal context", description: "Explicit notice", time: { created: 2 }, @@ -209,19 +207,19 @@ test("renders a footer for a pre-output retry assistant after replay", () => { expect(reduceSessionRows([message])).toEqual([{ type: "assistant-footer", messageID: "assistant-retry" }]) }) -test("places a pending compaction barrier before every queued user message", () => { - const queued = (id: string, text: string, created: number): SessionMessage => ({ +test("places a running compaction barrier before every queued user message", () => { + const queued = (id: string, text: string, created: number): SessionMessageInfo => ({ type: "user", id, text, time: { created }, }) - const messages: SessionMessage[] = [ + const messages: SessionMessageInfo[] = [ queued("user-before", "Before", 1), { type: "compaction", id: "compaction", - status: "queued", + status: "running", reason: "manual", summary: "", recent: "", @@ -249,5 +247,5 @@ function assistant(id: string, content: SessionMessageAssistant["content"]): Ses } function pending() { - return { status: "pending" as const, input: "" } + return { status: "streaming" as const, input: "" } } diff --git a/packages/tui/test/util/session.test.ts b/packages/tui/test/util/session.test.ts index 752f490247a..38ec243770c 100644 --- a/packages/tui/test/util/session.test.ts +++ b/packages/tui/test/util/session.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { SessionMessage } from "@opencode-ai/sdk/v2" +import type { SessionMessageInfo } from "@opencode-ai/sdk/v2" import { isDefaultTitle, lastAssistantWithUsage } from "../../src/util/session" describe("util.session", () => { @@ -10,7 +10,7 @@ describe("util.session", () => { }) test("tracks usage across undo and redo boundaries", () => { - const assistant = (id: string, input: number): SessionMessage => ({ + const assistant = (id: string, input: number): SessionMessageInfo => ({ id, type: "assistant", agent: "build", diff --git a/packages/tui/test/util/tool-display.test.ts b/packages/tui/test/util/tool-display.test.ts index f77f54cbab3..ca08e7d974e 100644 --- a/packages/tui/test/util/tool-display.test.ts +++ b/packages/tui/test/util/tool-display.test.ts @@ -31,7 +31,7 @@ describe("toolDisplayMetadata", () => { }) test("does not expose pending or malformed metadata", () => { - expect(toolDisplayMetadata({ status: "pending", structured: { provider: "exa" } })).toEqual({}) + expect(toolDisplayMetadata({ status: "streaming", structured: { provider: "exa" } })).toEqual({}) expect(toolDisplayMetadata({ status: "completed" })).toEqual({}) expect(toolDisplayMetadata({ status: "completed", structured: null })).toEqual({}) expect(toolDisplayMetadata({ status: "completed", structured: [] })).toEqual({})