Compare commits

..

1 Commits

Author SHA1 Message Date
Kit Langton 3302589ef3 feat(codemode): support property deletion 2026-07-07 17:40:57 +00:00
351 changed files with 8986 additions and 13790 deletions
@@ -20,7 +20,7 @@ const profiles = [
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
{
name: "multi patch",
tool: "patch",
tool: "apply_patch",
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
},
] as const
@@ -25,7 +25,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
userMessage(),
assistantMessage(
[
toolPart(patchID, "patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
textPart(followingID, "Following incremental patch"),
],
{ completed: false },
@@ -49,7 +49,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
partUpdated(
toolPart(
patchID,
"patch",
"apply_patch",
"running",
{ files: [first.filePath, second.filePath] },
{ metadata: { files: [first, second] } },
@@ -61,7 +61,7 @@ test("adds patch files incrementally without resetting outer expansion", async (
partUpdated(
toolPart(
patchID,
"patch",
"apply_patch",
"completed",
{ files: [first.filePath, second.filePath, third.filePath] },
{ metadata: { files: [first, second, third] } },
@@ -295,7 +295,7 @@ function performanceTurn(index: number) {
messageID: assistantID,
type: "tool",
callID: `call_0000_${suffix}_patch`,
tool: "patch",
tool: "apply_patch",
state: {
status: "completed",
input: { patchText: realisticPatch(index) },
@@ -131,7 +131,7 @@ function toolPart(
): MessagePart {
const metadata =
metadataOverride ??
(tool === "patch"
(tool === "apply_patch"
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
: tool === "edit" || tool === "write"
? {
@@ -219,7 +219,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
: []),
...(index % 8 === 0
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
: []),
...(index % 7 === 0
? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)]
@@ -24,7 +24,7 @@ test("renders a completed single-file patch", async ({ page }) => {
assistantMessage([
toolPart(
id,
"patch",
"apply_patch",
"completed",
{ files: ["src/a.ts"] },
{
@@ -35,7 +35,7 @@ test("preserves nested patch file state through outer collapse and reopen", asyn
assistantMessage([
toolPart(
patchID,
"patch",
"apply_patch",
"completed",
{ files: files.map((file) => file.filePath) },
{ metadata: { files } },
@@ -246,7 +246,7 @@ function editPart(id: string) {
function patchPart(id: string) {
return toolPart(
id,
"patch",
"apply_patch",
"completed",
{ files: ["src/a.ts", "src/b.ts"] },
{
@@ -8,7 +8,7 @@ import {
} from "../performance/timeline-stability/fixture"
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
const ordinary = ["bash", "edit", "write", "patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
const parts = ordinary.map((tool, index) =>
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
)
@@ -90,7 +90,7 @@ function questionInput() {
function errorInput(tool: string) {
if (tool === "bash") return { command: "exit 1" }
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
if (tool === "patch") return { files: ["src/error.ts"] }
if (tool === "apply_patch") return { files: ["src/error.ts"] }
if (tool === "webfetch") return { url: "https://example.com" }
if (tool === "websearch") return { query: "failure" }
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
@@ -120,7 +120,7 @@ function toolPart(
outputLength = 160,
): MessagePart {
const metadata =
tool === "patch"
tool === "apply_patch"
? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
: tool === "edit" || tool === "write"
? {
@@ -199,7 +199,7 @@ function turn(index: number): Message[] {
? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)]
: []),
...(index % 8 === 0
? [toolPart(index, 8, "patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)]
: []),
...(index % 7 === 0 ? [toolPart(index, 4, "bash", { command: "bun typecheck" }, 620)] : []),
...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []),
+1 -1
View File
@@ -55,7 +55,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const path = url.pathname
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
if (path === "/global/health") return json(route, { healthy: true })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
if (path === "/permission")
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
if (path === "/question")
@@ -8,7 +8,7 @@ import type {
QuestionRequest,
Session,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
@@ -188,7 +188,7 @@ export function applyDirectoryEvent(input: {
break
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" }))
break
}
@@ -5,7 +5,7 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
@@ -33,7 +33,7 @@ describe("app session cache", () => {
test("dropSessionCaches clears orphaned parts without message rows", () => {
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -67,7 +67,7 @@ describe("app session cache", () => {
const m = msg("msg_1", "ses_1")
const store: {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -4,7 +4,7 @@ import type {
PermissionRequest,
QuestionRequest,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
@@ -12,7 +12,7 @@ export const SESSION_CACHE_LIMIT = 40
type SessionCache = {
session_status: Record<string, SessionStatus | undefined>
session_diff: Record<string, FileDiffInfo[] | undefined>
session_diff: Record<string, SnapshotFileDiff[] | undefined>
todo: Record<string, Todo[] | undefined>
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
@@ -13,7 +13,7 @@ import type {
ReferenceInfo,
Session,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
@@ -51,7 +51,7 @@ export type State = {
}
session_working(id: string): boolean
session_diff: {
[sessionID: string]: FileDiffInfo[]
[sessionID: string]: SnapshotFileDiff[]
}
todo: {
[sessionID: string]: Todo[]
+3 -3
View File
@@ -8,7 +8,7 @@ import type {
QuestionRequest,
Session,
SessionStatus,
FileDiffInfo,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { batch } from "solid-js"
@@ -139,7 +139,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
const [data, setData] = createStore({
info: {} as Record<string, Session | undefined>,
session_status: {} as Record<string, SessionStatus>,
session_diff: {} as Record<string, FileDiffInfo[]>,
session_diff: {} as Record<string, SnapshotFileDiff[]>,
todo: {} as Record<string, Todo[]>,
permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionRequest[]>,
@@ -769,7 +769,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
return
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: FileDiffInfo[] }
const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] }
setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" }))
return
}
@@ -1,6 +1,6 @@
import { createEffect, onCleanup, type JSX } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/session-ui/session-review"
import type {
SessionReviewCommentActions,
@@ -14,7 +14,7 @@ import type { LineComment } from "@/context/comments"
export type DiffStyle = "unified" | "split"
type ReviewDiff = FileDiffInfo | VcsFileDiff
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
export interface SessionReviewTabProps {
title?: JSX.Element
@@ -8,7 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -23,6 +23,7 @@ import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
import { useSettings } from "@/context/settings"
import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
import {
@@ -35,9 +36,15 @@ import {
import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function SessionSidePanel(props: {
canReview: () => boolean
diffs: () => (FileDiffInfo | VcsFileDiff)[]
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
diffsReady: () => boolean
empty: () => string
hasReview: () => boolean
@@ -52,6 +59,7 @@ export function SessionSidePanel(props: {
}) {
const layout = useLayout()
const settings = useSettings()
const sync = useSync()
const file = useFile()
const language = useLanguage()
const command = useCommand()
@@ -80,7 +88,7 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const diffs = createMemo(() => props.diffs())
const diffs = createMemo(() => props.diffs().filter(renderDiff))
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
@@ -1245,7 +1245,7 @@ export function MessageTimeline(props: {
const value = row()
if (value._tag !== "AssistantPart" || value.group.type !== "part") return false
const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
return part?.type === "tool" && ["edit", "write", "patch", "apply_patch"].includes(part.tool)
return part?.type === "tool" && ["edit", "write", "apply_patch"].includes(part.tool)
}
const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
let contentMeasureFrame: number | undefined
@@ -1,13 +1,17 @@
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Kind } from "@/components/file-tree-v2"
import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model"
export type RenderDiff = FileDiffInfo | VcsFileDiff
export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
export function normalizePath(p: string) {
return normalizeFileTreeV2Path(p)
}
export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function reviewDiffKinds(diffs: RenderDiff[]) {
const merge = (a: Kind | undefined, b: Kind) => {
if (!a) return b
@@ -1,5 +1,5 @@
import { createMemo, createSignal, Show, type JSX } from "solid-js"
import type { FileDiffInfo, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import {
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX,
SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN,
@@ -21,11 +21,16 @@ import type {
import FileTreeV2 from "@/components/file-tree-v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { filterReviewFiles, reviewDiffKinds, type RenderDiff } from "@/pages/session/v2/review-diff-kinds"
import {
filterRenderableDiff,
filterReviewFiles,
reviewDiffKinds,
type RenderDiff,
} from "@/pages/session/v2/review-diff-kinds"
import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state"
import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2"
type ReviewDiff = FileDiffInfo | VcsFileDiff
type ReviewDiff = SnapshotFileDiff | VcsFileDiff
export type ReviewPanelV2Props = {
title?: JSX.Element
@@ -49,7 +54,7 @@ export type ReviewPanelV2Props = {
export function ReviewPanelV2(props: ReviewPanelV2Props) {
const sdk = useSDK()
const diffs = createMemo(() => props.diffs())
const diffs = createMemo(() => props.diffs().filter(filterRenderableDiff))
const filteredFiles = createMemo(() =>
filterReviewFiles(
diffs().map((diff) => diff.file),
+2 -2
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
import { diffs, message } from "./diffs"
@@ -9,7 +9,7 @@ const item = {
additions: 1,
deletions: 1,
status: "modified",
} satisfies FileDiffInfo
} satisfies SnapshotFileDiff
describe("diffs", () => {
test("keeps valid arrays", () => {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { FileDiffInfo } from "@opencode-ai/sdk/v2"
import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2"
import type { Message } from "@opencode-ai/sdk/v2/client"
type Diff = FileDiffInfo
type Diff = SnapshotFileDiff | VcsFileDiff
function diff(value: unknown): value is Diff {
if (!value || typeof value !== "object" || Array.isArray(value)) return false
+2 -3
View File
@@ -37,8 +37,7 @@ function defaultCost(model: CurrentModel) {
export function runAgent(input: CurrentAgent): RunAgent {
return {
id: input.id,
name: input.name,
name: input.id,
description: input.description,
mode: input.mode,
hidden: input.hidden,
@@ -54,7 +53,7 @@ export function runCommand(input: CurrentCommand): RunCommand {
export function runSkill(input: CurrentSkill): RunCommand {
return {
name: input.id,
name: input.name,
description: input.description,
source: "skill",
}
+2 -2
View File
@@ -628,11 +628,11 @@ function emitEdit(state: State): void {
function emitPatch(state: State): void {
const file = path.join(process.cwd(), "src", "demo-format.ts")
const ref = make(state, "patch", {
const ref = make(state, "apply_patch", {
patchText: "*** Begin Patch\n*** End Patch",
})
doneTool(state, ref, {
title: "patch",
title: "apply_patch",
output: "",
metadata: {
files: [
+2 -2
View File
@@ -333,10 +333,10 @@ export function createPromptState(input: PromptInput): PromptState {
.map((item) => ({
kind: "mention",
display: "@" + item.name,
value: item.id,
value: item.name,
part: {
type: "agent",
name: item.id,
name: item.name,
source: {
start: 0,
end: 0,
+2
View File
@@ -84,6 +84,7 @@ type RunFooterOptions = {
theme: RunTheme
keymap: Keymap<Renderable, KeyEvent>
tuiConfig: RunTuiConfig
backgroundSubagents: boolean
diffStyle: RunDiffStyle
onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void>
@@ -325,6 +326,7 @@ export class RunFooter implements FooterApi {
theme: footer.theme,
diffStyle: options.diffStyle,
tuiConfig: options.tuiConfig,
backgroundSubagents: options.backgroundSubagents,
history: footer.history,
agent: options.agentLabel,
onSubmit: footer.handlePrompt,
+4 -1
View File
@@ -89,6 +89,7 @@ type RunFooterViewProps = {
theme: () => RunTheme
diffStyle?: RunDiffStyle
tuiConfig: RunTuiConfig
backgroundSubagents: boolean
history?: () => RunPrompt[]
agent: string
onSubmit: (input: RunPrompt) => boolean
@@ -168,7 +169,9 @@ export function RunFooterView(props: RunFooterViewProps) {
return tabs().findIndex((item) => item.sessionID === sessionID) + 1
})
const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background))
const foregroundSubagents = createMemo(
() => props.backgroundSubagents && activeTabs().some((item) => !item.background),
)
const model = createMemo(() => {
const current = props.currentModel()
return current ? modelInfo(props.providers(), current) : { model: props.state().model, provider: undefined }
+4
View File
@@ -1,5 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { truthy } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Effect } from "effect"
import path from "node:path"
@@ -83,6 +84,9 @@ export async function runMini(input: MiniCommandInput) {
files: [],
initialInput,
thinking: true,
backgroundSubagents:
truthy("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS") ||
(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS === undefined && truthy("OPENCODE_EXPERIMENTAL")),
replay: input.replay ?? true,
replayLimit: input.replayLimit,
demo: input.demo,
+1
View File
@@ -281,6 +281,7 @@ export async function runNonInteractivePrompt(input: Input) {
metadata: {
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
providerCall: current.provider,
providerResult: { executed: event.data.executed, state: event.data.resultState },
+8 -5
View File
@@ -89,11 +89,12 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
!explicitModel && !sessionModel
? await client.model
.default({ location: { directory: cwd, workspace } })
.then((result) => (result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined))
.then((result) =>
result.data ? { providerID: result.data.providerID, modelID: result.data.id } : undefined,
)
: undefined
const model = pickRunModel(explicitModel, input.variant, sessionModel, defaultModel)
if (input.variant && !model)
return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (input.variant && !model) return reportError(input, "Cannot select a variant before selecting a model", session?.id)
if (model) {
await waitForCatalogReady({ sdk: client, directory: cwd, workspace, model })
const available = await client.model.list({ location: { directory: cwd, workspace } })
@@ -111,7 +112,9 @@ async function execute(input: RunCommandInput, prepared: Prepared, transport: Tr
if (!session && input.title !== undefined) {
await client.session.rename({
sessionID: selected.id,
title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
title:
input.title ||
prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""),
})
}
@@ -197,7 +200,7 @@ async function validateAgent(client: OpenCodeClient, directory: string, name?: s
warning(`failed to list agents${server ? ` from ${server}` : ""}. Falling back to default agent`)
return
}
const agent = agents.find((item) => item.id === name)
const agent = agents.find((item) => item.name === name)
if (!agent) {
warning(`agent "${name}" not found. Falling back to default agent`)
return
@@ -64,6 +64,7 @@ export type LifecycleInput = {
model: RunInput["model"]
variant: string | undefined
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
backgroundSubagents: boolean
onPermissionReply: (input: PermissionReply) => void | Promise<void>
onQuestionReply: (input: QuestionReply) => void | Promise<void>
onQuestionReject: (input: QuestionReject) => void | Promise<void>
@@ -235,6 +236,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
wrote,
keymap,
tuiConfig,
backgroundSubagents: input.backgroundSubagents,
diffStyle: tuiConfig.diff_style ?? "auto",
onPermissionReply: input.onPermissionReply,
onQuestionReply: input.onQuestionReply,
+5
View File
@@ -56,6 +56,7 @@ type RunRuntimeInput = {
files: RunInput["files"]
initialInput?: string
thinking: boolean
backgroundSubagents: boolean
replay?: boolean
replayLimit?: number
demo?: RunInput["demo"]
@@ -74,6 +75,7 @@ type RunDeferredInput = {
files: RunInput["files"]
initialInput?: string
thinking: boolean
backgroundSubagents: boolean
replay?: boolean
replayLimit?: number
demo?: RunInput["demo"]
@@ -268,6 +270,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
model: state.model,
variant: state.activeVariant,
tuiConfig: tuiConfigTask,
backgroundSubagents: input.backgroundSubagents,
onPermissionReply: async (next) => {
if (state.demo?.permission(next)) {
return
@@ -873,6 +876,7 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?:
files: input.files,
initialInput: input.initialInput,
thinking: input.thinking,
backgroundSubagents: input.backgroundSubagents,
replay: input.replay,
replayLimit: input.replayLimit,
demo: input.demo,
@@ -924,6 +928,7 @@ export async function runInteractiveMode(
files: input.files,
initialInput: input.initialInput,
thinking: input.thinking,
backgroundSubagents: input.backgroundSubagents,
replay: input.replay,
replayLimit: input.replayLimit,
demo: input.demo,
+8 -6
View File
@@ -16,7 +16,7 @@
// backgrounding is intentionally absent: subagent jobs block the parent
// session, so only whole-session `v2.session.background(parentID)` exists.
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2"
import type { SessionMessage, SessionMessageAssistantTool, ToolPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@opencode-ai/tui/util/locale"
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"
@@ -54,7 +54,7 @@ export function legacyTool(input: {
callID: tool.id,
tool: tool.name,
}
if (tool.state.status === "streaming") {
if (tool.state.status === "pending") {
return {
...base,
state: { status: "pending", input: {}, raw: tool.state.input },
@@ -83,6 +83,7 @@ export function legacyTool(input: {
metadata: {
structured: tool.state.structured,
content: tool.state.content,
outputPaths: tool.state.outputPaths,
result: tool.state.result,
providerCall,
providerResult,
@@ -178,7 +179,7 @@ export type SubagentTrackerInput = {
export type SubagentTracker = {
main(event: V2Event): void
foreign(sessionID: string, event: V2Event): void
hydrate(next: { messages: SessionMessageInfo[]; active: Record<string, unknown> }): Promise<void>
hydrate(next: { messages: SessionMessage[]; active: Record<string, unknown> }): Promise<void>
select(sessionID: string | undefined): void
snapshot(): FooterSubagentState
}
@@ -315,7 +316,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
messageID,
tool: item,
})
if (item.state.status === "streaming") return
if (item.state.status === "pending") return
child.callIDs.add(item.id)
if (item.state.status === "running") {
setFrame(child, `tool:${item.id}`, toolCommit(part, "start"))
@@ -326,7 +327,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
setFrame(child, `tool:${item.id}`, toolCommit(part, "final"))
}
const rebuild = (child: ChildState, messages: SessionMessageInfo[]) => {
const rebuild = (child: ChildState, messages: SessionMessage[]) => {
child.frames = []
child.text.clear()
child.projectedText.clear()
@@ -411,7 +412,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
for (const [id, prompt] of pendingPrompts) {
if (!child.prompts.has(id)) child.prompts.set(id, prompt)
}
rebuild(child, structuredClone(response.data).toReversed() as SessionMessageInfo[])
rebuild(child, structuredClone(response.data).toReversed() as SessionMessage[])
for (const [id, tool] of pendingTools) {
if (!child.finishedTools.has(id) && !child.tools.has(id)) child.tools.set(id, tool)
}
@@ -621,6 +622,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
input: current?.input ?? {},
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
},
time: {
+16 -15
View File
@@ -2,7 +2,7 @@ import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/p
import type {
PermissionRequest,
QuestionRequest,
SessionMessageInfo,
SessionMessage,
SessionMessageAssistantTool,
} from "@opencode-ai/sdk/v2"
import { Event } from "@opencode-ai/schema/event"
@@ -389,7 +389,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
messageID,
tool: item,
})
if (item.state.status === "streaming") return
if (item.state.status === "pending") return
if (item.state.status === "running") {
if (state.tools.get(item.id)?.running) return
state.tools.set(item.id, {
@@ -417,7 +417,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
])
}
const renderMessage = (message: SessionMessageInfo, render: boolean, reuseVisibleWait: boolean) => {
const renderMessage = (message: SessionMessage, render: boolean, reuseVisibleWait: boolean) => {
if (message.type === "user") {
const waiting = state.wait?.messageID === message.id
if (waiting && state.wait) state.wait.promoted = true
@@ -438,34 +438,34 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
return
}
if (message.type === "shell") {
state.shellCommands.set(message.shellID, message.command)
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shellID
state.shellCommands.set(message.shell.id, message.shell.command)
if (state.shellWait?.messageID === message.id) state.shellWait.callID = message.shell.id
const completed = message.time.completed !== undefined
if (!render) {
// Suppressed history: mark settled shells rendered so live redelivery
// stays silent. A still-running shell stays unmarked and renders in
// full when its live shell.ended event arrives.
if (completed) {
state.shellStarted.add(message.shellID)
state.shellEnded.add(message.shellID)
state.shellStarted.add(message.shell.id)
state.shellEnded.add(message.shell.id)
}
return
}
if (!state.shellStarted.has(message.shellID)) {
state.shellStarted.add(message.shellID)
if (!state.shellStarted.has(message.shell.id)) {
state.shellStarted.add(message.shell.id)
write([
shellCommit(message.shellID, message.command, {
shellCommit(message.shell.id, message.shell.command, {
text: "running shell",
phase: "start",
toolState: "running",
}),
])
}
if (completed && message.output && !state.shellEnded.has(message.shellID)) {
state.shellEnded.add(message.shellID)
write(shellTerminal(message.shellID, message.command, message, message.output))
if (completed && message.output && !state.shellEnded.has(message.shell.id)) {
state.shellEnded.add(message.shell.id)
write(shellTerminal(message.shell.id, message.shell.command, message.shell, message.output))
}
if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve()
if (completed && state.shellWait?.callID === message.shell.id) state.shellWait.resolve()
return
}
if (message.type !== "assistant") return
@@ -534,7 +534,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
input.sdk.question.list({ sessionID: input.sessionID }),
input.sdk.session.active(),
])
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
const projected = structuredClone(messages.data).toReversed() as SessionMessage[]
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
state.permissions = permissions.map(permission)
state.questions = questions.map(question)
@@ -762,6 +762,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
input: current?.input ?? {},
structured: event.data.structured,
content: event.data.content,
outputPaths: event.data.outputPaths,
result: event.data.result,
},
time: { created: current?.started ?? event.created, ran: current?.started, completed: event.created },
+2 -2
View File
@@ -119,7 +119,7 @@ type ToolName =
| "bash"
| "write"
| "edit"
| "patch"
| "apply_patch"
| "batch"
| "task"
| "todowrite"
@@ -1094,7 +1094,7 @@ const TOOL_RULES = {
},
permission: permEdit,
},
patch: {
apply_patch: {
view: {
output: false,
final: true,
+1 -1
View File
@@ -113,7 +113,6 @@ export type FooterQueuedPrompt = {
}
export type RunAgent = {
id: string
name: string
description?: string
mode: "subagent" | "primary" | "all"
@@ -136,6 +135,7 @@ export type RunInput = {
files: RunFilePart[]
initialInput?: string
thinking: boolean
backgroundSubagents: boolean
demo?: boolean
}
+3 -2
View File
@@ -3,14 +3,15 @@ import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from
import {
ClientApi,
effectOmitEndpoints,
endpointNames,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
import { Effect } from "effect"
import { fileURLToPath } from "url"
const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints })
const promiseContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
const effectContract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: effectOmitEndpoints })
await Effect.runPromise(
Effect.all(
+1
View File
@@ -1,6 +1,7 @@
export {
ClientApi,
effectOmitEndpoints,
endpointNames,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
+116 -139
View File
@@ -111,167 +111,157 @@ export type Endpoint4_8Input = {
export type Endpoint4_8Output = EffectValue<ReturnType<RawClient["server.session"]["session.rename"]>>
export type SessionRenameOperation<E = never> = (input: Endpoint4_8Input) => Effect.Effect<Endpoint4_8Output, E>
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
export type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly destination: Endpoint4_9Request["payload"]["destination"]
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
readonly id?: Endpoint4_9Request["payload"]["id"]
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"]
}
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.move"]>>
export type SessionMoveOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
export type Endpoint4_9Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint4_9Input) => Effect.Effect<Endpoint4_9Output, E>
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
export type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
readonly command: Endpoint4_10Request["payload"]["command"]
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
readonly agent?: Endpoint4_10Request["payload"]["agent"]
readonly model?: Endpoint4_10Request["payload"]["model"]
readonly files?: Endpoint4_10Request["payload"]["files"]
readonly agents?: Endpoint4_10Request["payload"]["agents"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.prompt"]>>["data"]
export type SessionPromptOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
export type Endpoint4_10Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint4_10Input) => Effect.Effect<Endpoint4_10Output, E>
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
export type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"]
readonly command: Endpoint4_11Request["payload"]["command"]
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
readonly agent?: Endpoint4_11Request["payload"]["agent"]
readonly model?: Endpoint4_11Request["payload"]["model"]
readonly files?: Endpoint4_11Request["payload"]["files"]
readonly agents?: Endpoint4_11Request["payload"]["agents"]
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
readonly skill: Endpoint4_11Request["payload"]["skill"]
readonly resume?: Endpoint4_11Request["payload"]["resume"]
}
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.command"]>>["data"]
export type SessionCommandOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
export type Endpoint4_11Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSkillOperation<E = never> = (input: Endpoint4_11Input) => Effect.Effect<Endpoint4_11Output, E>
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
export type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly id?: Endpoint4_12Request["payload"]["id"]
readonly skill: Endpoint4_12Request["payload"]["skill"]
readonly resume?: Endpoint4_12Request["payload"]["resume"]
readonly text: Endpoint4_12Request["payload"]["text"]
readonly description?: Endpoint4_12Request["payload"]["description"]
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
}
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.skill"]>>
export type SessionSkillOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
export type Endpoint4_12Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_12Input) => Effect.Effect<Endpoint4_12Output, E>
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
export type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly text: Endpoint4_13Request["payload"]["text"]
readonly description?: Endpoint4_13Request["payload"]["description"]
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
readonly resume?: Endpoint4_13Request["payload"]["resume"]
readonly id?: Endpoint4_13Request["payload"]["id"]
readonly command: Endpoint4_13Request["payload"]["command"]
}
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.synthetic"]>>
export type SessionSyntheticOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
export type Endpoint4_13Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionShellOperation<E = never> = (input: Endpoint4_13Input) => Effect.Effect<Endpoint4_13Output, E>
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
export type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
readonly command: Endpoint4_14Request["payload"]["command"]
}
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.shell"]>>
export type SessionShellOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
export type Endpoint4_14Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionCompactOperation<E = never> = (input: Endpoint4_14Input) => Effect.Effect<Endpoint4_14Output, E>
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
export type Endpoint4_15Input = {
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
readonly id?: Endpoint4_15Request["payload"]["id"]
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_16Input = {
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
readonly files?: Endpoint4_16Request["payload"]["files"]
}
export type Endpoint4_15Output = EffectValue<ReturnType<RawClient["server.session"]["session.compact"]>>["data"]
export type SessionCompactOperation<E = never> = (input: Endpoint4_15Input) => Effect.Effect<Endpoint4_15Output, E>
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
export type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
export type Endpoint4_16Output = EffectValue<ReturnType<RawClient["server.session"]["session.wait"]>>
export type SessionWaitOperation<E = never> = (input: Endpoint4_16Input) => Effect.Effect<Endpoint4_16Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
export type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
export type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
readonly files?: Endpoint4_17Request["payload"]["files"]
}
export type Endpoint4_17Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.stage"]>>["data"]
export type SessionRevertStageOperation<E = never> = (input: Endpoint4_17Input) => Effect.Effect<Endpoint4_17Output, E>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
export type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.clear"]>>
export type SessionRevertClearOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
export type Endpoint4_18Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_18Input) => Effect.Effect<Endpoint4_18Output, E>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
export type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.revert.commit"]>>
export type SessionRevertCommitOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
export type Endpoint4_19Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_19Input) => Effect.Effect<Endpoint4_19Output, E>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
export type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
export type Endpoint4_20Output = EffectValue<ReturnType<RawClient["server.session"]["session.context"]>>["data"]
export type SessionContextOperation<E = never> = (input: Endpoint4_20Input) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
export type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
export type Endpoint4_21Output = EffectValue<
export type Endpoint4_20Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.list"]>
>["data"]
export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint4_20Input,
) => Effect.Effect<Endpoint4_20Output, E>
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
export type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"]
readonly value: Endpoint4_21Request["payload"]["value"]
}
export type Endpoint4_21Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint4_21Input,
) => Effect.Effect<Endpoint4_21Output, E>
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
export type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"]
readonly value: Endpoint4_22Request["payload"]["value"]
}
export type Endpoint4_22Output = EffectValue<ReturnType<RawClient["server.session"]["session.instructions.entry.put"]>>
export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint4_22Input,
) => Effect.Effect<Endpoint4_22Output, E>
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
export type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly key: Endpoint4_23Request["params"]["key"]
}
export type Endpoint4_23Output = EffectValue<
export type Endpoint4_22Output = EffectValue<
ReturnType<RawClient["server.session"]["session.instructions.entry.remove"]>
>
export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint4_23Input,
) => Effect.Effect<Endpoint4_23Output, E>
input: Endpoint4_22Input,
) => Effect.Effect<Endpoint4_22Output, E>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
export type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly after?: Endpoint4_24Request["query"]["after"]
readonly follow?: Endpoint4_24Request["query"]["follow"]
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
export type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly after?: Endpoint4_23Request["query"]["after"]
readonly follow?: Endpoint4_23Request["query"]["follow"]
}
export type Endpoint4_24Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint4_24Input) => Stream.Stream<Endpoint4_24Output, E>
export type Endpoint4_23Output = StreamValue<EffectValue<ReturnType<RawClient["server.session"]["session.log"]>>>
export type SessionLogOperation<E = never> = (input: Endpoint4_23Input) => Stream.Stream<Endpoint4_23Output, E>
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
export type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
export type Endpoint4_24Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_24Input) => Effect.Effect<Endpoint4_24Output, E>
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.interrupt"]>>
export type SessionInterruptOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
export type Endpoint4_25Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_25Input) => Effect.Effect<Endpoint4_25Output, E>
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
export type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.background"]>>
export type SessionBackgroundOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_27Input = {
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
readonly messageID: Endpoint4_27Request["params"]["messageID"]
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
export type Endpoint4_26Input = {
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
readonly messageID: Endpoint4_26Request["params"]["messageID"]
}
export type Endpoint4_27Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_27Input) => Effect.Effect<Endpoint4_27Output, E>
export type Endpoint4_26Output = EffectValue<ReturnType<RawClient["server.session"]["session.message"]>>["data"]
export type SessionMessageOperation<E = never> = (input: Endpoint4_26Input) => Effect.Effect<Endpoint4_26Output, E>
export interface SessionApi<E = never> {
readonly list: SessionListOperation<E>
@@ -283,7 +273,6 @@ export interface SessionApi<E = never> {
readonly switchAgent: SessionSwitchAgentOperation<E>
readonly switchModel: SessionSwitchModelOperation<E>
readonly rename: SessionRenameOperation<E>
readonly move: SessionMoveOperation<E>
readonly prompt: SessionPromptOperation<E>
readonly command: SessionCommandOperation<E>
readonly skill: SessionSkillOperation<E>
@@ -291,11 +280,9 @@ export interface SessionApi<E = never> {
readonly shell: SessionShellOperation<E>
readonly compact: SessionCompactOperation<E>
readonly wait: SessionWaitOperation<E>
readonly revert: {
readonly stage: SessionRevertStageOperation<E>
readonly clear: SessionRevertClearOperation<E>
readonly commit: SessionRevertCommitOperation<E>
}
readonly revertStage: SessionRevertStageOperation<E>
readonly revertClear: SessionRevertClearOperation<E>
readonly revertCommit: SessionRevertCommitOperation<E>
readonly context: SessionContextOperation<E>
readonly instructions: {
readonly entry: {
@@ -440,15 +427,11 @@ export type IntegrationAttemptCancelOperation<E = never> = (
export interface IntegrationApi<E = never> {
readonly list: IntegrationListOperation<E>
readonly get: IntegrationGetOperation<E>
readonly connect: {
readonly key: IntegrationConnectKeyOperation<E>
readonly oauth: IntegrationConnectOauthOperation<E>
}
readonly attempt: {
readonly status: IntegrationAttemptStatusOperation<E>
readonly complete: IntegrationAttemptCompleteOperation<E>
readonly cancel: IntegrationAttemptCancelOperation<E>
}
readonly connectKey: IntegrationConnectKeyOperation<E>
readonly connectOauth: IntegrationConnectOauthOperation<E>
readonly attemptStatus: IntegrationAttemptStatusOperation<E>
readonly attemptComplete: IntegrationAttemptCompleteOperation<E>
readonly attemptCancel: IntegrationAttemptCancelOperation<E>
}
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -456,16 +439,8 @@ export type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query
export type Endpoint10_0Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.list"]>>
export type ServerMcpListOperation<E = never> = (input?: Endpoint10_0Input) => Effect.Effect<Endpoint10_0Output, E>
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
export type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.mcp"]["mcp.resource.catalog"]>>
export type ServerMcpResourceCatalogOperation<E = never> = (
input?: Endpoint10_1Input,
) => Effect.Effect<Endpoint10_1Output, E>
export interface ServerMcpApi<E = never> {
readonly list: ServerMcpListOperation<E>
readonly resource: { readonly catalog: ServerMcpResourceCatalogOperation<E> }
}
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
@@ -515,7 +490,7 @@ export interface ProjectApi<E = never> {
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
export type FormRequestListOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
export type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
@@ -569,7 +544,7 @@ export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
export interface FormApi<E = never> {
readonly request: { readonly list: FormRequestListOperation<E> }
readonly listRequests: FormListRequestsOperation<E>
readonly list: FormListOperation<E>
readonly create: FormCreateOperation<E>
readonly get: FormGetOperation<E>
@@ -581,7 +556,7 @@ export interface FormApi<E = never> {
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
export type PermissionRequestListOperation<E = never> = (
export type PermissionListRequestsOperation<E = never> = (
input?: Endpoint14_0Input,
) => Effect.Effect<Endpoint14_0Output, E>
@@ -590,14 +565,14 @@ export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["quer
export type Endpoint14_1Output = EffectValue<
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
>["data"]
export type PermissionSavedListOperation<E = never> = (
export type PermissionListSavedOperation<E = never> = (
input?: Endpoint14_1Input,
) => Effect.Effect<Endpoint14_1Output, E>
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
export type PermissionSavedRemoveOperation<E = never> = (
export type PermissionRemoveSavedOperation<E = never> = (
input: Endpoint14_2Input,
) => Effect.Effect<Endpoint14_2Output, E>
@@ -645,8 +620,9 @@ export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permis
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
export interface PermissionApi<E = never> {
readonly request: { readonly list: PermissionRequestListOperation<E> }
readonly saved: { readonly list: PermissionSavedListOperation<E>; readonly remove: PermissionSavedRemoveOperation<E> }
readonly listRequests: PermissionListRequestsOperation<E>
readonly listSaved: PermissionListSavedOperation<E>
readonly removeSaved: PermissionRemoveSavedOperation<E>
readonly create: PermissionCreateOperation<E>
readonly list: PermissionListOperation<E>
readonly get: PermissionGetOperation<E>
@@ -815,7 +791,7 @@ export interface ShellApi<E = never> {
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
export type QuestionRequestListOperation<E = never> = (
export type QuestionListRequestsOperation<E = never> = (
input?: Endpoint21_0Input,
) => Effect.Effect<Endpoint21_0Output, E>
@@ -842,7 +818,7 @@ export type Endpoint21_3Output = EffectValue<ReturnType<RawClient["server.questi
export type QuestionRejectOperation<E = never> = (input: Endpoint21_3Input) => Effect.Effect<Endpoint21_3Output, E>
export interface QuestionApi<E = never> {
readonly request: { readonly list: QuestionRequestListOperation<E> }
readonly listRequests: QuestionListRequestsOperation<E>
readonly list: QuestionListOperation<E>
readonly reply: QuestionReplyOperation<E>
readonly reject: QuestionRejectOperation<E>
@@ -912,15 +888,16 @@ export interface VcsApi<E = never> {
}
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export type DebugEvictLocationOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export interface DebugApi<E = never> {
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
readonly location: DebugLocationOperation<E>
readonly evictLocation: DebugEvictLocationOperation<E>
}
export interface AppApi<E = never> {
+122 -142
View File
@@ -141,27 +141,15 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
Effect.mapError(mapClientError),
)
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.move"]>[0]
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_9Input = {
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
readonly destination: Endpoint4_9Request["payload"]["destination"]
readonly moveChanges?: Endpoint4_9Request["payload"]["moveChanges"]
readonly id?: Endpoint4_9Request["payload"]["id"]
readonly prompt: Endpoint4_9Request["payload"]["prompt"]
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
readonly resume?: Endpoint4_9Request["payload"]["resume"]
}
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
raw["session.move"]({
params: { sessionID: input["sessionID"] },
payload: { destination: input["destination"], moveChanges: input["moveChanges"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly prompt: Endpoint4_10Request["payload"]["prompt"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
raw["session.prompt"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
@@ -170,20 +158,20 @@ const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10I
Effect.map((value) => value.data),
)
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"]
readonly command: Endpoint4_11Request["payload"]["command"]
readonly arguments?: Endpoint4_11Request["payload"]["arguments"]
readonly agent?: Endpoint4_11Request["payload"]["agent"]
readonly model?: Endpoint4_11Request["payload"]["model"]
readonly files?: Endpoint4_11Request["payload"]["files"]
readonly agents?: Endpoint4_11Request["payload"]["agents"]
readonly delivery?: Endpoint4_11Request["payload"]["delivery"]
readonly resume?: Endpoint4_11Request["payload"]["resume"]
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.command"]>[0]
type Endpoint4_10Input = {
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
readonly id?: Endpoint4_10Request["payload"]["id"]
readonly command: Endpoint4_10Request["payload"]["command"]
readonly arguments?: Endpoint4_10Request["payload"]["arguments"]
readonly agent?: Endpoint4_10Request["payload"]["agent"]
readonly model?: Endpoint4_10Request["payload"]["model"]
readonly files?: Endpoint4_10Request["payload"]["files"]
readonly agents?: Endpoint4_10Request["payload"]["agents"]
readonly delivery?: Endpoint4_10Request["payload"]["delivery"]
readonly resume?: Endpoint4_10Request["payload"]["resume"]
}
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
raw["session.command"]({
params: { sessionID: input["sessionID"] },
payload: {
@@ -202,73 +190,67 @@ const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11I
Effect.map((value) => value.data),
)
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly id?: Endpoint4_12Request["payload"]["id"]
readonly skill: Endpoint4_12Request["payload"]["skill"]
readonly resume?: Endpoint4_12Request["payload"]["resume"]
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
type Endpoint4_11Input = {
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
readonly id?: Endpoint4_11Request["payload"]["id"]
readonly skill: Endpoint4_11Request["payload"]["skill"]
readonly resume?: Endpoint4_11Request["payload"]["resume"]
}
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
raw["session.skill"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly text: Endpoint4_13Request["payload"]["text"]
readonly description?: Endpoint4_13Request["payload"]["description"]
readonly metadata?: Endpoint4_13Request["payload"]["metadata"]
readonly resume?: Endpoint4_13Request["payload"]["resume"]
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
type Endpoint4_12Input = {
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
readonly text: Endpoint4_12Request["payload"]["text"]
readonly description?: Endpoint4_12Request["payload"]["description"]
readonly metadata?: Endpoint4_12Request["payload"]["metadata"]
}
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
raw["session.synthetic"]({
params: { sessionID: input["sessionID"] },
payload: {
text: input["text"],
description: input["description"],
metadata: input["metadata"],
resume: input["resume"],
},
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
readonly command: Endpoint4_14Request["payload"]["command"]
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.shell"]>[0]
type Endpoint4_13Input = {
readonly sessionID: Endpoint4_13Request["params"]["sessionID"]
readonly id?: Endpoint4_13Request["payload"]["id"]
readonly command: Endpoint4_13Request["payload"]["command"]
}
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
raw["session.shell"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], command: input["command"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_15Input = {
readonly sessionID: Endpoint4_15Request["params"]["sessionID"]
readonly id?: Endpoint4_15Request["payload"]["id"]
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
type Endpoint4_14Input = {
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
readonly id?: Endpoint4_14Request["payload"]["id"]
}
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_17Input = {
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
readonly messageID: Endpoint4_17Request["payload"]["messageID"]
readonly files?: Endpoint4_17Request["payload"]["files"]
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
type Endpoint4_16Input = {
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
readonly messageID: Endpoint4_16Request["payload"]["messageID"]
readonly files?: Endpoint4_16Request["payload"]["files"]
}
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
raw["session.revert.stage"]({
params: { sessionID: input["sessionID"] },
payload: { messageID: input["messageID"], files: input["files"] },
@@ -277,61 +259,61 @@ const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17I
Effect.map((value) => value.data),
)
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context"]>[0]
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint4_21Input = { readonly sessionID: Endpoint4_21Request["params"]["sessionID"] }
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.instructions.entry.list"]>[0]
type Endpoint4_20Input = { readonly sessionID: Endpoint4_20Request["params"]["sessionID"] }
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"]
readonly value: Endpoint4_22Request["payload"]["value"]
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.instructions.entry.put"]>[0]
type Endpoint4_21Input = {
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
readonly key: Endpoint4_21Request["params"]["key"]
readonly value: Endpoint4_21Request["payload"]["value"]
}
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
raw["session.instructions.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly key: Endpoint4_23Request["params"]["key"]
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.instructions.entry.remove"]>[0]
type Endpoint4_22Input = {
readonly sessionID: Endpoint4_22Request["params"]["sessionID"]
readonly key: Endpoint4_22Request["params"]["key"]
}
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_24Input = {
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
readonly after?: Endpoint4_24Request["query"]["after"]
readonly follow?: Endpoint4_24Request["query"]["follow"]
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.log"]>[0]
type Endpoint4_23Input = {
readonly sessionID: Endpoint4_23Request["params"]["sessionID"]
readonly after?: Endpoint4_23Request["query"]["after"]
readonly follow?: Endpoint4_23Request["query"]["follow"]
}
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
Stream.unwrap(
raw["session.log"]({
params: { sessionID: input["sessionID"] },
@@ -342,22 +324,22 @@ const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24I
),
)
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
type Endpoint4_24Input = { readonly sessionID: Endpoint4_24Request["params"]["sessionID"] }
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_26Input = { readonly sessionID: Endpoint4_26Request["params"]["sessionID"] }
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
type Endpoint4_25Request = Parameters<RawClient["server.session"]["session.background"]>[0]
type Endpoint4_25Input = { readonly sessionID: Endpoint4_25Request["params"]["sessionID"] }
const Endpoint4_25 = (raw: RawClient["server.session"]) => (input: Endpoint4_25Input) =>
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint4_27Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_27Input = {
readonly sessionID: Endpoint4_27Request["params"]["sessionID"]
readonly messageID: Endpoint4_27Request["params"]["messageID"]
type Endpoint4_26Request = Parameters<RawClient["server.session"]["session.message"]>[0]
type Endpoint4_26Input = {
readonly sessionID: Endpoint4_26Request["params"]["sessionID"]
readonly messageID: Endpoint4_26Request["params"]["messageID"]
}
const Endpoint4_27 = (raw: RawClient["server.session"]) => (input: Endpoint4_27Input) =>
const Endpoint4_26 = (raw: RawClient["server.session"]) => (input: Endpoint4_26Input) =>
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
@@ -373,21 +355,22 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
switchAgent: Endpoint4_6(raw),
switchModel: Endpoint4_7(raw),
rename: Endpoint4_8(raw),
move: Endpoint4_9(raw),
prompt: Endpoint4_10(raw),
command: Endpoint4_11(raw),
skill: Endpoint4_12(raw),
synthetic: Endpoint4_13(raw),
shell: Endpoint4_14(raw),
compact: Endpoint4_15(raw),
wait: Endpoint4_16(raw),
revert: { stage: Endpoint4_17(raw), clear: Endpoint4_18(raw), commit: Endpoint4_19(raw) },
context: Endpoint4_20(raw),
instructions: { entry: { list: Endpoint4_21(raw), put: Endpoint4_22(raw), remove: Endpoint4_23(raw) } },
log: Endpoint4_24(raw),
interrupt: Endpoint4_25(raw),
background: Endpoint4_26(raw),
message: Endpoint4_27(raw),
prompt: Endpoint4_9(raw),
command: Endpoint4_10(raw),
skill: Endpoint4_11(raw),
synthetic: Endpoint4_12(raw),
shell: Endpoint4_13(raw),
compact: Endpoint4_14(raw),
wait: Endpoint4_15(raw),
revertStage: Endpoint4_16(raw),
revertClear: Endpoint4_17(raw),
revertCommit: Endpoint4_18(raw),
context: Endpoint4_19(raw),
instructions: { entry: { list: Endpoint4_20(raw), put: Endpoint4_21(raw), remove: Endpoint4_22(raw) } },
log: Endpoint4_23(raw),
interrupt: Endpoint4_24(raw),
background: Endpoint4_25(raw),
message: Endpoint4_26(raw),
})
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
@@ -534,8 +517,11 @@ const Endpoint9_6 = (raw: RawClient["server.integration"]) => (input: Endpoint9_
const adaptGroup9 = (raw: RawClient["server.integration"]) => ({
list: Endpoint9_0(raw),
get: Endpoint9_1(raw),
connect: { key: Endpoint9_2(raw), oauth: Endpoint9_3(raw) },
attempt: { status: Endpoint9_4(raw), complete: Endpoint9_5(raw), cancel: Endpoint9_6(raw) },
connectKey: Endpoint9_2(raw),
connectOauth: Endpoint9_3(raw),
attemptStatus: Endpoint9_4(raw),
attemptComplete: Endpoint9_5(raw),
attemptCancel: Endpoint9_6(raw),
})
type Endpoint10_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]
@@ -543,15 +529,7 @@ type Endpoint10_0Input = { readonly location?: Endpoint10_0Request["query"]["loc
const Endpoint10_0 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_0Input) =>
raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint10_1Request = Parameters<RawClient["server.mcp"]["mcp.resource.catalog"]>[0]
type Endpoint10_1Input = { readonly location?: Endpoint10_1Request["query"]["location"] }
const Endpoint10_1 = (raw: RawClient["server.mcp"]) => (input?: Endpoint10_1Input) =>
raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({
list: Endpoint10_0(raw),
resource: { catalog: Endpoint10_1(raw) },
})
const adaptGroup10 = (raw: RawClient["server.mcp"]) => ({ list: Endpoint10_0(raw) })
type Endpoint11_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
type Endpoint11_0Input = {
@@ -688,7 +666,7 @@ const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Inpu
)
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
request: { list: Endpoint13_0(raw) },
listRequests: Endpoint13_0(raw),
list: Endpoint13_1(raw),
create: Endpoint13_2(raw),
get: Endpoint13_3(raw),
@@ -776,8 +754,9 @@ const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14
}).pipe(Effect.mapError(mapClientError))
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
request: { list: Endpoint14_0(raw) },
saved: { list: Endpoint14_1(raw), remove: Endpoint14_2(raw) },
listRequests: Endpoint14_0(raw),
listSaved: Endpoint14_1(raw),
removeSaved: Endpoint14_2(raw),
create: Endpoint14_3(raw),
list: Endpoint14_4(raw),
get: Endpoint14_5(raw),
@@ -1010,7 +989,7 @@ const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3
)
const adaptGroup21 = (raw: RawClient["server.question"]) => ({
request: { list: Endpoint21_0(raw) },
listRequests: Endpoint21_0(raw),
list: Endpoint21_1(raw),
reply: Endpoint21_2(raw),
reject: Endpoint21_3(raw),
@@ -1096,7 +1075,8 @@ const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1In
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
location: Endpoint25_0(raw),
evictLocation: Endpoint25_1(raw),
})
const adaptClient = (raw: RawClient) => ({
+193 -244
View File
@@ -23,8 +23,6 @@ import type {
SessionSwitchModelOutput,
SessionRenameInput,
SessionRenameOutput,
SessionMoveInput,
SessionMoveOutput,
SessionPromptInput,
SessionPromptOutput,
SessionCommandInput,
@@ -89,8 +87,6 @@ import type {
IntegrationAttemptCancelOutput,
ServerMcpListInput,
ServerMcpListOutput,
ServerMcpResourceCatalogInput,
ServerMcpResourceCatalogOutput,
CredentialUpdateInput,
CredentialUpdateOutput,
CredentialRemoveInput,
@@ -100,8 +96,8 @@ import type {
ProjectCurrentOutput,
ProjectDirectoriesInput,
ProjectDirectoriesOutput,
FormRequestListInput,
FormRequestListOutput,
FormListRequestsInput,
FormListRequestsOutput,
FormListInput,
FormListOutput,
FormCreateInput,
@@ -114,12 +110,12 @@ import type {
FormReplyOutput,
FormCancelInput,
FormCancelOutput,
PermissionRequestListInput,
PermissionRequestListOutput,
PermissionSavedListInput,
PermissionSavedListOutput,
PermissionSavedRemoveInput,
PermissionSavedRemoveOutput,
PermissionListRequestsInput,
PermissionListRequestsOutput,
PermissionListSavedInput,
PermissionListSavedOutput,
PermissionRemoveSavedInput,
PermissionRemoveSavedOutput,
PermissionCreateInput,
PermissionCreateOutput,
PermissionListInput,
@@ -161,8 +157,8 @@ import type {
ShellOutputOutput,
ShellRemoveInput,
ShellRemoveOutput,
QuestionRequestListInput,
QuestionRequestListOutput,
QuestionListRequestsInput,
QuestionListRequestsOutput,
QuestionListInput,
QuestionListOutput,
QuestionReplyInput,
@@ -181,9 +177,9 @@ import type {
VcsStatusOutput,
VcsDiffInput,
VcsDiffOutput,
DebugLocationListOutput,
DebugLocationEvictInput,
DebugLocationEvictOutput,
DebugLocationOutput,
DebugEvictLocationInput,
DebugEvictLocationOutput,
} from "./types"
import { ClientError } from "./client-error"
@@ -491,18 +487,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
move: (input: SessionMoveInput, requestOptions?: RequestOptions) =>
request<SessionMoveOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
body: { destination: input["destination"], moveChanges: input["moveChanges"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
prompt: (input: SessionPromptInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionPromptOutput }>(
{
@@ -554,12 +538,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
body: {
text: input["text"],
description: input["description"],
metadata: input["metadata"],
resume: input["resume"],
},
body: { text: input["text"], description: input["description"], metadata: input["metadata"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
@@ -601,42 +580,40 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
revert: {
stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionRevertStageOutput }>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] },
successStatus: 200,
declaredStatuses: [404, 409, 500, 400, 401],
empty: false,
},
requestOptions,
).then((value) => value.data),
clear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
request<SessionRevertClearOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
successStatus: 204,
declaredStatuses: [404, 409, 500, 400, 401],
empty: true,
},
requestOptions,
),
commit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
request<SessionRevertCommitOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
successStatus: 204,
declaredStatuses: [404, 409, 400, 401],
empty: true,
},
requestOptions,
),
},
revertStage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionRevertStageOutput }>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] },
successStatus: 200,
declaredStatuses: [404, 409, 500, 400, 401],
empty: false,
},
requestOptions,
).then((value) => value.data),
revertClear: (input: SessionRevertClearInput, requestOptions?: RequestOptions) =>
request<SessionRevertClearOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
successStatus: 204,
declaredStatuses: [404, 409, 500, 400, 401],
empty: true,
},
requestOptions,
),
revertCommit: (input: SessionRevertCommitInput, requestOptions?: RequestOptions) =>
request<SessionRevertCommitOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
successStatus: 204,
declaredStatuses: [404, 409, 400, 401],
empty: true,
},
requestOptions,
),
context: (input: SessionContextInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionContextOutput }>(
{
@@ -838,73 +815,69 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
connect: {
key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectKeyOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
query: { location: input["location"] },
body: { key: input["key"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
oauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectOauthOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
query: { location: input["location"] },
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
},
requestOptions,
),
},
attempt: {
status: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
{
method: "GET",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
complete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>(
{
method: "POST",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
cancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>(
{
method: "DELETE",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
connectKey: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectKeyOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
query: { location: input["location"] },
body: { key: input["key"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
connectOauth: (input: IntegrationConnectOauthInput, requestOptions?: RequestOptions) =>
request<IntegrationConnectOauthOutput>(
{
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
query: { location: input["location"] },
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
},
requestOptions,
),
attemptStatus: (input: IntegrationAttemptStatusInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptStatusOutput>(
{
method: "GET",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
attemptComplete: (input: IntegrationAttemptCompleteInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCompleteOutput>(
{
method: "POST",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
},
requestOptions,
),
attemptCancel: (input: IntegrationAttemptCancelInput, requestOptions?: RequestOptions) =>
request<IntegrationAttemptCancelOutput>(
{
method: "DELETE",
path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
"server.mcp": {
list: (input?: ServerMcpListInput, requestOptions?: RequestOptions) =>
@@ -919,20 +892,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
resource: {
catalog: (input?: ServerMcpResourceCatalogInput, requestOptions?: RequestOptions) =>
request<ServerMcpResourceCatalogOutput>(
{
method: "GET",
path: `/api/mcp/resource`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
},
credential: {
update: (input: CredentialUpdateInput, requestOptions?: RequestOptions) =>
@@ -993,20 +952,18 @@ export function make(options: ClientOptions) {
),
},
form: {
request: {
list: (input?: FormRequestListInput, requestOptions?: RequestOptions) =>
request<FormRequestListOutput>(
{
method: "GET",
path: `/api/form/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
request<FormListRequestsOutput>(
{
method: "GET",
path: `/api/form/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
list: (input: FormListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: FormListOutput }>(
{
@@ -1084,45 +1041,41 @@ export function make(options: ClientOptions) {
),
},
permission: {
request: {
list: (input?: PermissionRequestListInput, requestOptions?: RequestOptions) =>
request<PermissionRequestListOutput>(
{
method: "GET",
path: `/api/permission/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
saved: {
list: (input?: PermissionSavedListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionSavedListOutput }>(
{
method: "GET",
path: `/api/permission/saved`,
query: { projectID: input?.["projectID"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
).then((value) => value.data),
remove: (input: PermissionSavedRemoveInput, requestOptions?: RequestOptions) =>
request<PermissionSavedRemoveOutput>(
{
method: "DELETE",
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
request<PermissionListRequestsOutput>(
{
method: "GET",
path: `/api/permission/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
listSaved: (input?: PermissionListSavedInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionListSavedOutput }>(
{
method: "GET",
path: `/api/permission/saved`,
query: { projectID: input?.["projectID"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
).then((value) => value.data),
removeSaved: (input: PermissionRemoveSavedInput, requestOptions?: RequestOptions) =>
request<PermissionRemoveSavedOutput>(
{
method: "DELETE",
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
create: (input: PermissionCreateInput, requestOptions?: RequestOptions) =>
request<{ readonly data: PermissionCreateOutput }>(
{
@@ -1404,20 +1357,18 @@ export function make(options: ClientOptions) {
),
},
question: {
request: {
list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) =>
request<QuestionRequestListOutput>(
{
method: "GET",
path: `/api/question/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
listRequests: (input?: QuestionListRequestsInput, requestOptions?: RequestOptions) =>
request<QuestionListRequestsOutput>(
{
method: "GET",
path: `/api/question/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: QuestionListOutput }>(
{
@@ -1534,31 +1485,29 @@ export function make(options: ClientOptions) {
),
},
debug: {
location: {
list: (requestOptions?: RequestOptions) =>
request<DebugLocationListOutput>(
{
method: "GET",
path: `/api/debug/location`,
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
evict: (input?: DebugLocationEvictInput, requestOptions?: RequestOptions) =>
request<DebugLocationEvictOutput>(
{
method: "DELETE",
path: `/api/debug/location`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
location: (requestOptions?: RequestOptions) =>
request<DebugLocationOutput>(
{
method: "GET",
path: `/api/debug/location`,
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
evictLocation: (input?: DebugEvictLocationInput, requestOptions?: RequestOptions) =>
request<DebugEvictLocationOutput>(
{
method: "DELETE",
path: `/api/debug/location`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
}
}
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message"
import { Workspace } from "@opencode-ai/schema/workspace"
import { Api } from "@opencode-ai/server/api"
import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
import { ClientApi, groupNames, promiseOmitEndpoints } from "../src/contract"
import { ClientApi, endpointNames, groupNames, promiseOmitEndpoints } from "../src/contract"
const Client = await import("../src/effect")
@@ -38,7 +38,7 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
expect(ProjectV2.Directory).toBe(Project.Directory)
expect(ProjectV2.Directories).toBe(Project.Directories)
expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
expect(CoreSessionMessage.Info).toBe(SessionMessage.Info)
expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
expect(Api.groups["server.session"].identifier).toBe("server.session")
expect(Api.groups["server.project"].identifier).toBe("server.project")
expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
@@ -49,8 +49,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () =>
})
test("client and Server contracts generate identically", () => {
const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints })
const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints })
const server = compile(Api, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints: promiseOmitEndpoints })
expect(emitPromise(client)).toEqual(emitPromise(server))
})
+9 -27
View File
@@ -33,11 +33,16 @@ test("exposes every standard HTTP API group", () => {
"debug",
])
expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "attempt"])
expect(Object.keys(client.integration.connect)).toEqual(["key", "oauth"])
expect(Object.keys(client.integration.attempt)).toEqual(["status", "complete", "cancel"])
expect(Object.keys(client.integration)).toEqual([
"list",
"get",
"connectKey",
"connectOauth",
"attemptStatus",
"attemptComplete",
"attemptCancel",
])
expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
@@ -45,29 +50,6 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
})
test("MCP resource catalog uses the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input) => {
request = input instanceof Request ? input : new Request(input)
return Response.json({
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
data: {
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
},
})
},
})
const result = await client["server.mcp"].resource.catalog({ location: { directory: "/tmp/project" } })
expect(result.data.resources[0]?.uri).toBe("docs://readme")
expect(request?.method).toBe("GET")
expect(request?.url).toBe("http://localhost:3000/api/mcp/resource?location%5Bdirectory%5D=%2Ftmp%2Fproject")
})
test("file.read returns binary content from the public HTTP contract", async () => {
let request: Request | undefined
const client = OpenCode.make({
+2 -2
View File
@@ -237,11 +237,11 @@ A host cannot define its own `$codemode` top-level namespace.
CodeMode executes a deliberately bounded JavaScript subset. It supports:
- Plain data literals, property access, assignment, destructuring, and sequence expressions (the comma operator, evaluated left to right with the final value returned).
- Plain data literals, property access, assignment, property deletion, destructuring, and sequence expressions (the comma operator, evaluated left to right with the final value returned).
- `if`, conditional expressions, `switch`, `for`, `for...of` (arrays, strings, Maps, Sets, including assignment-form destructuring such as `for ([key, value] of entries)`), `for...in` (own keys of plain objects, index strings of arrays, and namespace/tool names of `tools` references - anything else is an error suggesting `for...of` or `Object.keys`, rather than real JS's surprising behavior of indices for strings and zero iterations for Maps/Sets), `while`, and `do...while`.
- Arrow functions and function declarations with closures, defaults, rest parameters, and destructuring.
- Optional chaining, nullish coalescing, templates, spread (arrays, strings, Maps, Sets), and `try`/`catch`.
- Common array, string, number, `Object`, `Math`, and `JSON` operations, including primitive-number `valueOf`, the standard non-finite `Number` constants, and host-backed `Math.random`. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
- Common array, string, number, `Object`, `Math`, and `JSON` operations. Mutating array methods include `push`/`pop`/`shift`/`unshift`/`splice` (removes in place and returns the removed elements)/`fill`/`copyWithin`; array `keys`/`values`/`entries` return **arrays** (matching the Map/Set convention) and work with `for...of` and spread. String methods include `localeCompare` (locale/options arguments ignored), `normalize`, and the `trimLeft`/`trimRight` aliases. `Object.keys` also accepts arrays (index strings, as in JS) and tool references: `Object.keys(tools)` lists the top-level namespaces, including `$codemode`, and `Object.keys(tools.ns)` lists the names at that node (a callable tool enumerates as `[]`; an unknown path is an `UnknownTool` diagnostic). `Object.values`/`Object.entries` on a tool reference fail with a pointer at `Object.keys(tools)` and `tools.$codemode.search`.
- `Date` - `Date.now()`/`Date.parse()`/`Date.UTC()`, `new Date(...)`, the getter methods, and date arithmetic/comparison via the time value. Dates stringify as ISO (`toString` included, for determinism across host timezones).
- Regular expressions - `/literals/` and `new RegExp(...)` with `test`/`exec` (stateful `lastIndex` for `g`), plus string `match`/`matchAll`/`replace`/`replaceAll`/`split`/`search` with patterns. Match results are arrays carrying `index` and named `groups` as own properties (`input` is omitted). `replace` and `replaceAll` accept function replacers with captures, offset, input, and named groups; callbacks run sequentially, may await tool calls, and have their results coerced to strings. Invalid patterns, invalid flags, and missing-`g` calls fail with catchable errors that say what was wrong and how to fix it (escaping hints, the exact `/pattern/g` to write). Patterns run on the host engine, so pathological backtracking is bounded only by the execution timeout.
- `Map` and `Set` - construction from entries/arrays/strings, `get`/`set`/`add`/`has`/`delete`/`clear`/`size`/`forEach`, and `keys`/`values`/`entries` returning **arrays** (not iterators).
+1 -1
View File
@@ -158,7 +158,7 @@ current omissions to implement, not intentional product boundaries.
- [ ] Add `Object.is` after runtime method and tool references have stable identity semantics.
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
composition methods, and `Array.prototype.toSpliced`.
- [ ] Decide whether iterable `Math.sumPrecise` belongs in the runtime.
- [ ] Decide whether nondeterministic `Math.random` and iterable `Math.sumPrecise` belong in the runtime.
- [ ] Refine diagnostics so user throws, expected tool failures, unexpected host/tool defects, and genuine interpreter
defects are distinguishable without leaking private causes.
+34 -17
View File
@@ -530,29 +530,17 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
if (args[0] instanceof SandboxURLSearchParams) {
return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
}
const source = args[0]
if (source instanceof SandboxPromise) {
throw new InterpreterRuntimeError(
"Array.from received an un-awaited Promise; await it before creating the array.",
node,
"InvalidDataValue",
)
}
const source = boundedData(args[0], "Array.from input")
if (typeof source === "string") return Array.from(source)
if (Array.isArray(source)) return [...source]
if (
source !== null &&
typeof source === "object" &&
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
typeof (source as { length?: unknown }).length === "number"
) {
return Array.from(source as ArrayLike<unknown>)
}
throw new InterpreterRuntimeError(
"Array.from expects an array, string, Map, Set, or array-like value.",
node,
"InvalidDataValue",
)
throw new InterpreterRuntimeError("Array.from expects an array, string, Map, Set, or array-like value.", node)
}
default:
throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
@@ -1835,6 +1823,13 @@ class Interpreter<R> {
private evaluateUnaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
const operator = getString(node, "operator")
const argument = getNode(node, "argument")
if (operator === "delete") {
const target = argument.type === "ChainExpression" ? getNode(argument, "expression") : argument
if (target.type !== "MemberExpression") {
throw new InterpreterRuntimeError("Delete target must be a data property in CodeMode.", argument)
}
return this.deleteMember(target)
}
// `typeof undeclaredIdentifier` is `"undefined"` in JS (never a ReferenceError), so
// feature-detection guards like `typeof x !== "undefined"` don't crash. Short-circuit before
// evaluating the argument; a declared-but-TDZ binding still falls through to the normal throw.
@@ -2017,9 +2012,6 @@ class Interpreter<R> {
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
return invokeGlobalMethod(callable, args, node)
}
if (callable.namespace === "Array" && (callable.name === "from" || callable.name === "of")) {
return invokeGlobalMethod(callable, args, node)
}
return boundedData(invokeGlobalMethod(callable, args, node), `${callable.namespace}.${callable.name} result`)
}
if (callable instanceof CoercionFunction) {
@@ -3082,6 +3074,7 @@ class Interpreter<R> {
private getMemberReference(
node: AstNode,
options?: { readonly allowUnknownArrayProperty: boolean },
): Effect.Effect<
| MemberReference
| ToolReference
@@ -3248,6 +3241,7 @@ class Interpreter<R> {
typeof key !== "number" &&
!/^\d+$/.test(key)
) {
if (options?.allowUnknownArrayProperty === true) return { target: objectValue, key }
// Own non-index properties read through (match results carry index/groups); like JS,
// they are readable in place and dropped by JSON at data boundaries.
if (typeof key === "string" && Object.hasOwn(objectValue, key)) {
@@ -3293,6 +3287,29 @@ class Interpreter<R> {
return this.modifyMember(node, () => Effect.succeed({ write: true, next: value, result: value }))
}
private deleteMember(node: AstNode): Effect.Effect<boolean, unknown, R> {
return Effect.map(this.getMemberReference(node, { allowUnknownArrayProperty: true }), (reference) => {
if (reference === OptionalShortCircuit) return true
if (
reference instanceof ComputedValue ||
reference === undefined ||
reference instanceof ToolReference ||
reference instanceof PromiseMethodReference ||
reference instanceof IntrinsicReference ||
reference instanceof GlobalMethodReference ||
reference.target instanceof SandboxURL
) {
throw new InterpreterRuntimeError("Only data properties may be deleted in CodeMode.", node)
}
if (Array.isArray(reference.target)) {
if (reference.key === "length" || (typeof reference.key === "string" && arrayMethods.has(reference.key))) {
throw new InterpreterRuntimeError("Array length and methods cannot be deleted in CodeMode.", node)
}
}
return Reflect.deleteProperty(reference.target, reference.key)
})
}
// Resolves the member reference EXACTLY ONCE (so a side-effecting object/key expression
// runs once), then lets `compute` decide whether to write - enabling compound assignment,
// updates, plain writes, and short-circuiting logical assignment to share one safe path.
-2
View File
@@ -1,7 +1,6 @@
export const mathConstants = new Set(["PI", "E", "LN2", "LN10", "LOG2E", "LOG10E", "SQRT2", "SQRT1_2"])
export const mathMethods = new Set([
"random",
"max",
"min",
"abs",
@@ -41,7 +40,6 @@ export const mathMethods = new Set([
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available in CodeMode.`, node)
if (name === "random") return Math.random()
const nums = args.map((arg) => {
if (typeof arg !== "number") throw new InterpreterRuntimeError(`Math.${name} expects number arguments.`, node)
return arg
+2 -14
View File
@@ -1,15 +1,6 @@
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString", "valueOf"])
export const numberMethods = new Set(["toFixed", "toPrecision", "toExponential", "toString"])
export const numberConstants = new Set([
"MAX_SAFE_INTEGER",
"MIN_SAFE_INTEGER",
"MAX_VALUE",
"MIN_VALUE",
"EPSILON",
"NaN",
"POSITIVE_INFINITY",
"NEGATIVE_INFINITY",
])
export const numberConstants = new Set(["MAX_SAFE_INTEGER", "MIN_SAFE_INTEGER", "MAX_VALUE", "MIN_VALUE", "EPSILON"])
export const numberStatics = new Set(["isInteger", "isFinite", "isNaN", "isSafeInteger", "parseInt", "parseFloat"])
@@ -41,9 +32,6 @@ export const invokeNumberMethod = (value: number, name: string, args: Array<unkn
result = value.toString(radix)
break
}
case "valueOf":
result = value
break
default:
throw new InterpreterRuntimeError(`Number method '${name}' is not available in CodeMode.`, node)
}
+15 -18
View File
@@ -1,6 +1,6 @@
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { isBlockedMember } from "../tool-runtime.js"
import { isSandboxValue, SandboxMap, SandboxPromise, SandboxSet, SandboxURLSearchParams } from "../values.js"
import { isSandboxValue, SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"
import { boundedData, coerceToString } from "./value.js"
export const objectStatics = new Set(["keys", "values", "entries", "hasOwn", "assign", "fromEntries"])
@@ -10,23 +10,13 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
if (!objectStatics.has(name)) throw new InterpreterRuntimeError(`Object.${name} is not available in CodeMode.`, node)
const requireObject = (): Record<string, unknown> => {
const input = args[0]
const value = boundedData(args[0], `Object.${name} input`)
if (Array.isArray(input)) return input as unknown as Record<string, unknown>
if (isSandboxValue(input)) return {}
if (input instanceof SandboxPromise) {
throw new InterpreterRuntimeError(
`Object.${name} received an un-awaited Promise; await it before inspecting the result.`,
node,
"InvalidDataValue",
)
if (isSandboxValue(value)) return {}
if (value === null || typeof value !== "object") {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node)
}
if (input === null || typeof input !== "object") {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
const prototype = Object.getPrototypeOf(input)
if (prototype !== null && prototype !== Object.prototype) {
throw new InterpreterRuntimeError(`Object.${name} expects a data object or array.`, node, "InvalidDataValue")
}
return input as Record<string, unknown>
return value as Record<string, unknown>
}
const guardedSet = (out: Record<string, unknown>, key: string, item: unknown): void => {
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, node)
@@ -38,8 +28,15 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
guardedSet(out, coerceToString(key), item)
}
switch (name) {
case "keys":
return Object.keys(requireObject())
case "keys": {
const value = boundedData(args[0], "Object.keys input")
if (isSandboxValue(value)) return []
if (Array.isArray(value)) return Object.keys(value)
if (value === null || typeof value !== "object") {
throw new InterpreterRuntimeError("Object.keys expects a data object or array.", node)
}
return Object.keys(value)
}
case "values":
return Object.values(requireObject())
case "entries":
-1
View File
@@ -617,7 +617,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
"",
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, unlisted methods, and promise chaining are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
]
-3
View File
@@ -658,9 +658,6 @@ describe("CodeMode public contract", () => {
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
expect(instructions).not.toContain("host globals")
expect(instructions).toContain("Use Code Mode tools for external operations")
expect(instructions).toContain(
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
)
expect(instructions).toContain(
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
)
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -177,13 +177,13 @@ describe("OpenAPI.fromSpec", () => {
const spec = await opencodeSpec()
const result = OpenAPI.fromSpec({ spec, baseUrl })
expect(result.skipped).toHaveLength(4)
expect(result.skipped).toHaveLength(5)
expect(result.skipped).toContainEqual({
method: "GET",
path: "/api/pty/{ptyID}/connect",
reason: "WebSocket operations are not supported",
})
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(2)
expect(result.skipped.filter((item) => item.reason === "SSE operations are not supported")).toHaveLength(3)
expect(result.skipped).toContainEqual({
method: "GET",
path: "/api/fs/read/*",
@@ -210,11 +210,11 @@ describe("OpenAPI.fromSpec", () => {
if (!Tool.isDefinition(instructionPut)) throw new Error("v2.session.instructions.entry.put was not generated")
expect(inputTypeScript(instructionPut)).toBe("{ sessionID: string; key: string; value: unknown }")
expect(toolAt(result.tools, "v2_session_instructions_entry_put_2")).toBeUndefined()
expect(Tool.isDefinition(toolAt(result.tools, "v2.pty.connect"))).toBe(false)
expect(toolAt(result.tools, "v2.pty.connect")).toBeUndefined()
expect(toolAt(result.tools, "v2.session.log")).toBeUndefined()
expect(toolAt(result.tools, "v2.event.subscribe")).toBeUndefined()
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connect.token")).not.toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined()
})
test("preserves operation path sanitization and collision handling", () => {
+63
View File
@@ -89,6 +89,69 @@ describe("H6: object spread of null/undefined is a no-op", () => {
})
})
describe("delete data properties", () => {
test("removes an object property and reports success", async () => {
expect(await value(`const item = { keep: 1, remove: 2 }; const removed = delete item.remove; return { item, removed }`)).toEqual({
item: { keep: 1 },
removed: true,
})
})
test("evaluates a computed deletion key once", async () => {
expect(
await value(`
const item = { a: 1, b: 2 }
let calls = 0
const key = () => { calls += 1; return "b" }
const removed = delete item[key()]
return { item, calls, removed }
`),
).toEqual({ item: { a: 1 }, calls: 1, removed: true })
})
test("deleting an array element leaves a hole without changing length", async () => {
expect(
await value(`
const items = ["a", "b", "c"]
const removed = delete items[1]
return { removed, length: items.length, hasIndex: 1 in items, keys: Object.keys(items) }
`),
).toEqual({ removed: true, length: 3, hasIndex: false, keys: ["0", "2"] })
})
test("deleting an absent property succeeds", async () => {
expect(await value(`const item = { keep: 1 }; return delete item.missing`)).toBe(true)
})
test("optional deletion of a nullish receiver succeeds", async () => {
expect(
await value(`let calls = 0; const item = null; const removed = delete item?.[calls++]; return { calls, removed }`),
).toEqual({ calls: 0, removed: true })
})
test("deleting an absent or non-canonical array key does not remove an element", async () => {
expect(
await value(`
const items = ["a", "b"]
const absent = delete items.missing
const nonCanonical = delete items["01"]
return { absent, nonCanonical, items, hasIndex: 1 in items }
`),
).toEqual({ absent: true, nonCanonical: true, items: ["a", "b"], hasIndex: true })
})
test("rejects deletion of runtime and protected members", async () => {
for (const code of [
`delete tools.missing`,
`delete new URL("https://example.test").pathname`,
`delete [1].length`,
`delete [1].map`,
]) {
expect((await error(code)).message).toContain("deleted")
}
})
})
describe("H4: typeof on an undeclared identifier is 'undefined'", () => {
test("feature-detection guard does not throw", async () => {
expect(await value(`return typeof foo === "undefined" ? "safe" : "no"`)).toBe("safe")
-6
View File
@@ -245,12 +245,6 @@ describe("promises at data boundaries", () => {
expect(diagnostic.message).toContain("await tools.ns.tool(...)")
})
test("collection helpers do not let un-awaited promises cross the result boundary", async () => {
const diagnostic = await error(`return Array.from([Promise.resolve(1)])`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("un-awaited Promise")
})
test("passing an un-awaited promise as a tool argument is a clear diagnostic", async () => {
const diagnostic = await error(`return await tools.host.sleepy({ id: tools.host.sleepy({ id: 1 }) })`)
expect(diagnostic.kind).toBe("InvalidDataValue")
-106
View File
@@ -19,28 +19,6 @@ const error = async (code: string) => {
return result.error
}
describe("Number and Math", () => {
test("Math.random returns a number in [0, 1)", async () => {
expect(await value(`const n = Math.random(); return typeof n === "number" && n >= 0 && n < 1`)).toBe(true)
})
test("Number exposes native non-finite constants", async () => {
expect(
await value(
`return [Number.isNaN(Number.NaN), Number.POSITIVE_INFINITY === Infinity, Number.NEGATIVE_INFINITY === -Infinity]`,
),
).toEqual([true, true, true])
})
test("Number valueOf returns its primitive receiver", async () => {
expect(await value(`return (42).valueOf()`)).toBe(42)
})
test("Number valueOf does not enable boxed numbers", async () => {
expect((await error(`return new Number(42)`)).kind).toBe("UnsupportedSyntax")
})
})
describe("Date", () => {
test("Date.now() returns a number", async () => {
expect(await value(`return typeof Date.now()`)).toBe("number")
@@ -773,43 +751,6 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
)
})
test("Object.values/entries preserve nested object identity", async () => {
expect(
await value(`
const child = { selected: false }
const rows = { a: child }
Object.values(rows)[0].selected = true
return child.selected
`),
).toBe(true)
expect(
await value(`
const child = { selected: false }
const rows = { a: child }
Object.entries(rows)[0][1].selected = true
return child.selected
`),
).toBe(true)
})
test("Object enumeration preserves promises and callable references", async () => {
expect(
await value(`
const pending = Promise.resolve(1)
const source = { pending }
return [Object.keys(source), Object.hasOwn(source, "pending"), await Object.values(source)[0], await Object.entries(source)[0][1]]
`),
).toEqual([["pending"], true, 1, 1])
expect(await value(`return Object.values({ max: Math.max })[0](1, 2)`)).toBe(2)
})
test("Object enumeration rejects invalid receivers and gives promises an await hint", async () => {
const diagnostic = await error(`return Object.keys(Promise.resolve({ a: 1 }))`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("await")
expect((await error(`return Object.keys(Math)`)).kind).toBe("InvalidDataValue")
})
test("Object.assign keeps Maps usable", async () => {
expect(await value(`const merged = Object.assign({}, { m: new Map([["a", 1]]) }); return merged.m.get("a")`)).toBe(
1,
@@ -832,53 +773,6 @@ describe("sandbox values at intra-sandbox checkpoints", () => {
expect(await value(`return Array.from([new Date(5)])[0].getTime()`)).toBe(5)
})
test("Array.from and Array.of preserve nested object identity", async () => {
expect(
await value(`
const child = { selected: false }
Array.from([child])[0].selected = true
return child.selected
`),
).toBe(true)
expect(
await value(`
const child = { selected: false }
Array.of(child)[0].selected = true
return child.selected
`),
).toBe(true)
})
test("Array.from and Array.of preserve promises and callable references", async () => {
expect(
await value(`
const pending = Promise.resolve(1)
return [await Array.from([pending])[0], await Array.of(pending)[0]]
`),
).toEqual([1, 1])
expect(await value(`return [Array.from([Math.max])[0](1, 2), Array.of(Math.max)[0](3, 4)]`)).toEqual([2, 4])
})
test("Array.from preserves identity across supported collection shapes", async () => {
expect(
await value(`
const child = { selected: false }
const fromArrayLike = Array.from({ 0: child, length: 1 })
const fromMap = Array.from(new Map([["child", child]]))
const fromSet = Array.from(new Set([child]))
fromArrayLike[0].selected = true
return [fromMap[0][1] === child, fromSet[0] === child, child.selected]
`),
).toEqual([true, true, true])
})
test("Array.from rejects invalid receivers and gives promises an await hint", async () => {
const diagnostic = await error(`return Array.from(Promise.resolve([1]))`)
expect(diagnostic.kind).toBe("InvalidDataValue")
expect(diagnostic.message).toContain("await")
expect((await error(`return Array.from(() => 1)`)).kind).toBe("InvalidDataValue")
})
test("regexes stay callable through Object.values", async () => {
expect(await value(`return Object.values({ r: /ab+/ })[0].test("abb")`)).toBe(true)
})
-2
View File
@@ -8,8 +8,6 @@ import { State } from "./state"
export const ID = Agent.ID
export type ID = typeof ID.Type
export const Name = Agent.Name
export type Name = Agent.Name
export const defaultID = ID.make("build")
export const Color = Agent.Color
+3 -7
View File
@@ -6,7 +6,6 @@ import type {
JSONValue,
LanguageModelV3,
LanguageModelV3CallOptions,
LanguageModelV3FinishReason,
LanguageModelV3FunctionTool,
LanguageModelV3Message,
LanguageModelV3Prompt,
@@ -305,7 +304,6 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
const route: AnyRoute = {
id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`,
provider: ProviderID.make(info.providerID),
providerMetadataKey: optionKey,
protocol: "ai-sdk",
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
auth: Auth.none,
@@ -418,7 +416,7 @@ function assistantPart(part: ContentPart): AssistantContent {
case "media":
return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }]
case "reasoning":
return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }]
return [{ type: "reasoning", text: part.text }]
case "tool-call":
return [
{
@@ -427,7 +425,6 @@ function assistantPart(part: ContentPart): AssistantContent {
toolName: part.name,
input: part.input,
providerExecuted: part.providerExecuted,
providerOptions: providerOptions(part.providerMetadata),
},
]
case "tool-result":
@@ -443,7 +440,6 @@ function toolResultPart(part: ContentPart): ToolResultContent[] {
toolCallId: part.id,
toolName: part.name,
output: toolOutput(part.result),
providerOptions: providerOptions(part.providerMetadata),
},
]
}
@@ -628,8 +624,8 @@ function usage(input: Extract<LanguageModelV3StreamPart, { type: "finish" }>["us
return Object.values(output).some((value) => value !== undefined) ? output : undefined
}
function finishReason(value: LanguageModelV3FinishReason): FinishReason {
return value.unified === "other" ? "unknown" : value.unified
function finishReason(value: unknown): FinishReason {
return Schema.is(FinishReason)(value) ? value : "unknown"
}
function providerMetadata(value: unknown) {
+2 -3
View File
@@ -1,7 +1,6 @@
export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import { Effect, Stream } from "effect"
import { Config } from "../../config"
import { ModelV2 } from "../../model"
@@ -92,8 +91,8 @@ export const Plugin = define({
input: cost.input,
output: cost.output,
cache: {
read: cost.cache?.read ?? Money.USDPerMillionTokens.zero,
write: cost.cache?.write ?? Money.USDPerMillionTokens.zero,
read: cost.cache?.read ?? 0,
write: cost.cache?.write ?? 0,
},
}))
}
+4 -5
View File
@@ -1,7 +1,6 @@
export * as ConfigProvider from "./provider"
import { Schema } from "effect"
import { Money } from "@opencode-ai/schema/money"
import { ModelV2 } from "../model"
const JsonRecord = Schema.Record(Schema.String, Schema.Json)
@@ -18,8 +17,8 @@ export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")(
}) {}
class Cache extends Schema.Class<Cache>("ConfigV2.Model.Cost.Cache")({
read: Money.USDPerMillionTokens.pipe(Schema.optional),
write: Money.USDPerMillionTokens.pipe(Schema.optional),
read: Schema.Finite.pipe(Schema.optional),
write: Schema.Finite.pipe(Schema.optional),
}) {}
class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
@@ -27,8 +26,8 @@ class Cost extends Schema.Class<Cost>("ConfigV2.Model.Cost")({
type: Schema.Literal("context"),
size: Schema.Int,
}).pipe(Schema.optional),
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
input: Schema.Finite,
output: Schema.Finite,
cache: Cache.pipe(Schema.optional),
}) {}
-1
View File
@@ -48,6 +48,5 @@ export const migrations = (
import("./migration/20260705180000_rename_instructions"),
import("./migration/20260706223930_add-session-fork"),
import("./migration/20260707010146_durable_session_inbox"),
import("./migration/20260707120000_migrate_prelaunch_v2_state"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -1,227 +0,0 @@
import { sql } from "drizzle-orm"
import { Effect, Schema } from "effect"
import type { DatabaseMigration } from "../migration"
const decodeJson = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)
const isObject = Schema.is(Schema.Record(Schema.String, Schema.Unknown))
export default {
id: "20260707120000_migrate_prelaunch_v2_state",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(
sql`DELETE FROM session_message WHERE type = 'compaction' AND json_extract(data, '$.status') = 'queued'`,
)
const messages = yield* tx.all<{ id: string; type: string; data: string }>(
sql`SELECT id, type, data FROM session_message WHERE type IN ('skill', 'shell', 'assistant', 'compaction', 'synthetic')`,
)
for (const row of messages) {
const data = object(decodeJson(row.data))
yield* tx.run(
sql`UPDATE session_message SET data = ${JSON.stringify(messageData(row.type, data))} WHERE id = ${row.id}`,
)
}
yield* tx.run(sql`DELETE FROM event WHERE type = 'session.compaction.delta.1'`)
const events = yield* tx.all<{ id: string; aggregateID: string; seq: number; type: string; data: string }>(sql`
SELECT id, aggregate_id as aggregateID, seq, type, data
FROM event
WHERE type IN (
'session.skill.activated.1',
'session.skill.activated.2',
'session.compaction.started.1',
'session.compaction.started.2',
'session.compaction.ended.1',
'session.compaction.failed.1',
'session.compaction.failed.2',
'session.revert.staged.1',
'session.revert.staged.2'
)
ORDER BY aggregate_id, seq
`)
const compactionReasons = new Map<string, "auto" | "manual">()
for (const row of events) {
const data = object(decodeJson(row.data))
if (row.type.startsWith("session.compaction.ended.")) {
compactionReasons.delete(row.aggregateID)
continue
}
const event = eventData(row.type, data, compactionReasons.get(row.aggregateID))
if (row.type.startsWith("session.compaction.started."))
compactionReasons.set(row.aggregateID, event.data.reason === "auto" ? "auto" : "manual")
if (row.type.startsWith("session.compaction.failed.")) compactionReasons.delete(row.aggregateID)
yield* tx.run(
sql`UPDATE event SET type = ${event.type}, data = ${JSON.stringify(event.data)} WHERE id = ${row.id}`,
)
}
})
},
} satisfies DatabaseMigration.Migration
function messageData(type: string, data: Record<string, unknown>) {
if (type === "skill")
return defined({
metadata: data.metadata,
time: data.time,
skill: data.skill ?? data.id ?? data.name,
name: data.name,
text: data.text,
})
if (type === "shell") {
const shell = object(data.shell)
return defined({
metadata: data.metadata,
time: data.time,
shellID: data.shellID ?? shell.id,
command: data.command ?? shell.command,
status: data.status ?? shell.status,
exit: data.exit ?? shell.exit,
output: data.output,
})
}
if (type === "assistant")
return defined({
metadata: data.metadata,
time: data.time,
agent: data.agent,
model: data.model,
content: Array.isArray(data.content) ? data.content.map(assistantContent) : data.content,
snapshot: data.snapshot,
finish: data.finish,
cost: data.cost,
tokens: data.tokens,
error: data.error,
retry: data.retry,
})
if (type === "compaction") {
if (data.status === "failed")
return defined({
metadata: data.metadata,
time: data.time,
status: data.status,
reason: data.reason,
error: data.error ?? genericCompactionError,
})
return defined({
metadata: data.metadata,
time: data.time,
status: data.status,
reason: data.reason,
summary: data.summary,
recent: data.recent,
})
}
if (type === "synthetic")
return defined({ metadata: data.metadata, time: data.time, text: data.text, description: data.description })
const { sessionID: _, ...current } = data
return current
}
function assistantContent(value: unknown) {
const content = object(value)
if (content.type === "text") return defined({ type: content.type, text: content.text })
if (content.type === "reasoning")
return defined({ type: content.type, text: content.text, state: content.state, time: content.time })
if (content.type !== "tool") return content
return defined({
type: content.type,
id: content.id,
name: content.name,
executed: content.executed,
providerState: content.providerState,
providerResultState: content.providerResultState,
state: toolState(content.state),
time: content.time,
})
}
function toolState(value: unknown) {
const state = object(value)
if (state.status === "pending" || state.status === "streaming")
return defined({ status: "streaming", input: state.input })
if (state.status === "running")
return defined({ status: state.status, input: state.input, structured: state.structured, content: state.content })
if (state.status === "completed")
return defined({
status: state.status,
input: state.input,
structured: state.structured,
content: state.content,
result: state.result,
})
if (state.status === "error")
return defined({
status: state.status,
input: state.input,
structured: state.structured,
content: state.content,
error: state.error,
result: state.result,
})
return state
}
function eventData(type: string, data: Record<string, unknown>, compactionReason?: "auto" | "manual") {
if (type.startsWith("session.skill.activated."))
return {
type: "session.skill.activated.1",
data: defined({ sessionID: data.sessionID, id: data.id ?? data.name, name: data.name, text: data.text }),
}
if (type.startsWith("session.compaction.started."))
return {
type: "session.compaction.started.1",
data: defined({
sessionID: data.sessionID,
reason: data.reason,
recent: data.recent ?? "",
inputID: data.inputID,
}),
}
if (type.startsWith("session.compaction.failed."))
return {
type: "session.compaction.failed.1",
data: defined({
sessionID: data.sessionID,
reason: data.reason ?? compactionReason ?? "manual",
error: data.error ?? genericCompactionError,
inputID: data.inputID,
}),
}
const revert = object(data.revert)
return {
type: "session.revert.staged.1",
data: defined({
sessionID: data.sessionID,
revert: defined({
messageID: revert.messageID,
partID: revert.partID,
snapshot: revert.snapshot,
files: Array.isArray(revert.files)
? revert.files.map((value) => {
const file = object(value)
return defined({
file: file.file ?? file.path,
patch: file.patch,
additions: file.additions,
deletions: file.deletions,
status: file.status,
})
})
: undefined,
}),
}),
}
}
const genericCompactionError = {
type: "compaction.failed",
message: "Compaction failed before recording an error",
}
function object(value: unknown): Record<string, unknown> {
return isObject(value) ? value : {}
}
function defined(value: Record<string, unknown>) {
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== undefined))
}
+1 -1
View File
@@ -200,6 +200,6 @@ export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.no
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after V2 snapshot design exists.
// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists.
// TODO: Design multi-file transactions / rollback if patch needs atomic edits.
// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits.
// Until then, edits are sequential and report partial application.
// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement.
+2 -2
View File
@@ -1,6 +1,6 @@
export * as File from "./file"
import { FileDiff } from "@opencode-ai/schema/file-diff"
import { Revert } from "@opencode-ai/schema/revert"
export const Diff = FileDiff.Info
export const Diff = Revert.FileDiff
export type Diff = typeof Diff.Type
+1 -1
View File
@@ -606,7 +606,7 @@ const layer = Layer.effect(
file,
])).text
return {
file,
path: file,
status,
additions: binary ? 0 : Number(stats[0] ?? 0),
deletions: binary ? 0 : Number(stats[1] ?? 0),
+16 -118
View File
@@ -3,7 +3,7 @@ export * as MCPClient from "./client"
import path from "node:path"
import { execFile } from "node:child_process"
import { pathToFileURL } from "node:url"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { Client, type ClientOptions } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
@@ -21,7 +21,6 @@ import {
ListToolsResultSchema,
PromptListChangedNotificationSchema,
PromptSchema,
ResourceListChangedNotificationSchema,
type LoggingMessageNotification,
LoggingMessageNotificationSchema,
ToolListChangedNotificationSchema,
@@ -69,13 +68,11 @@ export interface ToolDefinition {
export interface PromptDefinition {
readonly name: string
readonly description: string | undefined
readonly arguments:
| ReadonlyArray<{
readonly name: string
readonly description: string | undefined
readonly required: boolean | undefined
}>
| undefined
readonly arguments: ReadonlyArray<{
readonly name: string
readonly description: string | undefined
readonly required: boolean | undefined
}> | undefined
}
export interface PromptMessage {
@@ -87,28 +84,6 @@ export interface PromptResult {
readonly messages: ReadonlyArray<PromptMessage>
}
export interface ResourceDefinition {
readonly name: string
readonly uri: string
readonly description: string | undefined
readonly mimeType: string | undefined
}
export interface ResourceTemplateDefinition {
readonly name: string
readonly uriTemplate: string
readonly description: string | undefined
readonly mimeType: string | undefined
}
export type ResourceContentPart =
| { readonly type: "text"; readonly uri: string; readonly text: string; readonly mimeType: string | undefined }
| { readonly type: "blob"; readonly uri: string; readonly blob: string; readonly mimeType: string | undefined }
export interface ReadResourceResult {
readonly contents: ReadonlyArray<ResourceContentPart>
}
export type CallToolContent =
| { readonly type: "text"; readonly text: string }
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
@@ -149,12 +124,6 @@ export interface Connection {
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
/** Lists the server's resources; returns [] when the server doesn't advertise resource support. */
readonly resources: () => Effect.Effect<ResourceDefinition[], Error>
/** Lists the server's resource templates; returns [] when the server doesn't advertise resource support. */
readonly resourceTemplates: () => Effect.Effect<ResourceTemplateDefinition[], Error>
/** Reads one resource; returns undefined when the server doesn't advertise resource support. */
readonly readResource: (input: { readonly uri: string }) => Effect.Effect<ReadResourceResult | undefined, Error>
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
readonly prompt: (input: {
readonly name: string
@@ -172,8 +141,6 @@ export interface Connection {
readonly onToolsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
readonly onPromptsChanged: (callback: () => void) => void
/** Registers a callback fired when the server announces its resource catalog changed. */
readonly onResourcesChanged: (callback: () => void) => void
}
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
@@ -201,8 +168,7 @@ export const connect = Effect.fnUntraced(function* (
},
})
}
if (!URL.canParse(config.url))
return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
if (!URL.canParse(config.url)) return yield* new ConnectError({ server, message: `Invalid MCP URL for "${server}"` })
return new StreamableHTTPClientTransport(new URL(config.url), {
requestInit: config.headers ? { headers: config.headers } : undefined,
authProvider,
@@ -236,7 +202,10 @@ export const connect = Effect.fnUntraced(function* (
}).pipe(Effect.exit)
if (Exit.isSuccess(exit)) {
yield* Effect.addFinalizer(() =>
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
cleanupStdioDescendants(transport).pipe(
Effect.andThen(Effect.promise(() => client.close())),
Effect.ignore,
),
)
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
@@ -288,9 +257,7 @@ export const connect = Effect.fnUntraced(function* (
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP prompts", { server, error: error.message }),
),
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })),
)
return prompts.map((prompt) => ({
name: prompt.name,
@@ -302,74 +269,6 @@ export const connect = Effect.fnUntraced(function* (
})),
}))
}),
resources: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return []
const resources = yield* Effect.tryPromise({
try: () =>
paginate(
(cursor) =>
client.listResources(cursor === undefined ? undefined : { cursor }, { timeout: catalogTimeout }),
(result) => result.resources,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resources", { server, error: error.message }),
),
)
return resources.map((resource) => ({
name: resource.name,
uri: resource.uri,
description: resource.description,
mimeType: resource.mimeType,
}))
}),
resourceTemplates: () =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return []
const templates = yield* Effect.tryPromise({
try: () =>
paginate(
(cursor) =>
client.listResourceTemplates(cursor === undefined ? undefined : { cursor }, {
timeout: catalogTimeout,
}),
(result) => result.resourceTemplates,
),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to list MCP resource templates", { server, error: error.message }),
),
)
return templates.map((template) => ({
name: template.name,
uriTemplate: template.uriTemplate,
description: template.description,
mimeType: template.mimeType,
}))
}),
readResource: (input) =>
Effect.gen(function* () {
if (!client.getServerCapabilities()?.resources) return undefined
const result = yield* Effect.tryPromise({
try: (signal) => client.readResource({ uri: input.uri }, { signal, timeout: executionTimeout }),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}).pipe(
Effect.tapError((error) =>
Effect.logWarning("failed to read MCP resource", { server, uri: input.uri, error: error.message }),
),
)
return {
contents: result.contents.map(
(part): ResourceContentPart =>
"text" in part
? { type: "text", uri: part.uri, text: part.text, mimeType: part.mimeType }
: { type: "blob", uri: part.uri, blob: part.blob, mimeType: part.mimeType },
),
}
}),
prompt: (input) =>
Effect.tryPromise({
try: (signal) =>
@@ -429,14 +328,13 @@ export const connect = Effect.fnUntraced(function* (
if (!client.getServerCapabilities()?.prompts?.listChanged) return
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
},
onResourcesChanged: (callback) => {
if (!client.getServerCapabilities()?.resources?.listChanged) return
client.setNotificationHandler(ResourceListChangedNotificationSchema, async () => callback())
},
} satisfies Connection
}
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
yield* cleanupStdioDescendants(transport).pipe(
Effect.andThen(Effect.promise(() => transport.close())),
Effect.ignore,
)
const error = Cause.squash(exit.cause)
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
+50 -80
View File
@@ -83,16 +83,48 @@ export class PromptResult extends Schema.Class<PromptResult>("MCP.PromptResult")
messages: Schema.Array(PromptMessage),
}) {}
export const Resource = Mcp.Resource
export type Resource = Mcp.Resource
export const ResourceTemplate = Mcp.ResourceTemplate
export type ResourceTemplate = Mcp.ResourceTemplate
export const ResourceCatalog = Mcp.ResourceCatalog
export type ResourceCatalog = Mcp.ResourceCatalog
export const ResourceContentPart = Mcp.ResourceContentPart
export type ResourceContentPart = Mcp.ResourceContentPart
export const ResourceContent = Mcp.ResourceContent
export type ResourceContent = Mcp.ResourceContent
export class Resource extends Schema.Class<Resource>("MCP.Resource")({
server: ServerName,
name: Schema.String,
uri: Schema.String,
description: Schema.String.pipe(Schema.optional),
mimeType: Schema.String.pipe(Schema.optional),
}) {}
export class ResourceTemplate extends Schema.Class<ResourceTemplate>("MCP.ResourceTemplate")({
server: ServerName,
name: Schema.String,
uriTemplate: Schema.String,
description: Schema.String.pipe(Schema.optional),
mimeType: Schema.String.pipe(Schema.optional),
}) {}
export class ResourceCatalog extends Schema.Class<ResourceCatalog>("MCP.ResourceCatalog")({
resources: Schema.Array(Resource),
templates: Schema.Array(ResourceTemplate),
}) {}
export const ResourceContentPart = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
uri: Schema.String,
text: Schema.String,
mimeType: Schema.String.pipe(Schema.optional),
}),
Schema.Struct({
type: Schema.Literal("blob"),
uri: Schema.String,
blob: Schema.String,
mimeType: Schema.String.pipe(Schema.optional),
}),
]).pipe(Schema.toTaggedUnion("type"))
export type ResourceContentPart = typeof ResourceContentPart.Type
export class ResourceContent extends Schema.Class<ResourceContent>("MCP.ResourceContent")({
server: ServerName,
uri: Schema.String,
contents: Schema.Array(ResourceContentPart),
}) {}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("MCP.NotFoundError", {
server: ServerName,
@@ -383,24 +415,6 @@ export const layer = Layer.effect(
),
})
const toResource = (server: ServerName, def: MCPClient.ResourceDefinition) =>
Resource.make({
server,
name: def.name,
uri: def.uri,
description: def.description,
mimeType: def.mimeType,
})
const toResourceTemplate = (server: ServerName, def: MCPClient.ResourceTemplateDefinition) =>
ResourceTemplate.make({
server,
name: def.name,
uriTemplate: def.uriTemplate,
description: def.description,
mimeType: def.mimeType,
})
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
connection.tools().pipe(
Effect.map((defs) => {
@@ -429,7 +443,6 @@ export const layer = Layer.effect(
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" }
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
})
@@ -445,10 +458,6 @@ export const layer = Layer.effect(
connection.onPromptsChanged(() => {
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
})
connection.onResourcesChanged(() => {
if (entry.client !== connection) return
fork(events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore))
})
}
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
@@ -492,7 +501,6 @@ export const layer = Layer.effect(
// after the initial registration sweep and emits no list-changed notification would otherwise
// stay invisible to the model.
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
return
@@ -549,6 +557,11 @@ export const layer = Layer.effect(
concurrency: "unbounded",
discard: true,
})
const gate = Effect.fnUntraced(function* (server: ServerName | string) {
const target = yield* requireServer(server)
yield* Deferred.await(target.entry.startup)
})
return Service.of({
servers: Effect.fn("MCP.servers")(function* () {
const entries = Array.from(runtime).toSorted(([a], [b]) => a.localeCompare(b))
@@ -624,54 +637,11 @@ export const layer = Layer.effect(
}),
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
yield* whenAllReady
const catalogs = yield* Effect.forEach(
Array.from(runtime),
([name, entry]) => {
if (!entry.client) return Effect.succeed({ resources: [], templates: [] })
return Effect.all(
{
resources: entry.client.resources().pipe(Effect.catch(() => Effect.succeed([]))),
templates: entry.client.resourceTemplates().pipe(Effect.catch(() => Effect.succeed([]))),
},
{ concurrency: "unbounded" },
).pipe(
Effect.map((catalog) => ({
resources: catalog.resources.map((def) => toResource(name, def)),
templates: catalog.templates.map((def) => toResourceTemplate(name, def)),
})),
)
},
{ concurrency: "unbounded" },
)
return ResourceCatalog.make({
resources: catalogs
.flatMap((catalog) => catalog.resources)
.toSorted(
(a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name) || a.uri.localeCompare(b.uri),
),
templates: catalogs
.flatMap((catalog) => catalog.templates)
.toSorted(
(a, b) =>
a.server.localeCompare(b.server) ||
a.name.localeCompare(b.name) ||
a.uriTemplate.localeCompare(b.uriTemplate),
),
})
return new ResourceCatalog({ resources: [], templates: [] })
}),
readResource: Effect.fn("MCP.readResource")(function* (input) {
const target = yield* requireServer(input.server)
yield* Deferred.await(target.entry.startup)
if (!target.entry.client) return undefined
const result = yield* target.entry.client
.readResource({ uri: input.uri })
.pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!result) return undefined
return ResourceContent.make({
server: target.name,
uri: input.uri,
contents: result.contents,
})
yield* gate(input.server)
return undefined
}),
})
}),
+12 -13
View File
@@ -2,7 +2,6 @@ import path from "path"
import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { ModelsDev } from "@opencode-ai/schema/models-dev"
import { Money } from "@opencode-ai/schema/money"
import { Global } from "./global"
import { Flag } from "./flag/flag"
import { Flock } from "./util/flock"
@@ -19,10 +18,10 @@ export type CatalogModelStatus = typeof CatalogModelStatus.Type
const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}`
const CostTier = Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
tier: Schema.Struct({
type: Schema.Literal("context"),
size: Schema.Finite,
@@ -30,17 +29,17 @@ const CostTier = Schema.Struct({
})
const Cost = Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
tiers: Schema.optional(Schema.Array(CostTier)),
context_over_200k: Schema.optional(
Schema.Struct({
input: Money.USDPerMillionTokens,
output: Money.USDPerMillionTokens,
cache_read: Schema.optional(Money.USDPerMillionTokens),
cache_write: Schema.optional(Money.USDPerMillionTokens),
input: Schema.Finite,
output: Schema.Finite,
cache_read: Schema.optional(Schema.Finite),
cache_write: Schema.optional(Schema.Finite),
}),
),
})
+6 -13
View File
@@ -1,6 +1,6 @@
export * as PluginV2 from "./plugin"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Event, ID, type Info } from "@opencode-ai/schema/plugin"
import { makeLocationNode } from "./effect/app-node"
import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect"
@@ -18,7 +18,6 @@ import { SkillV2 } from "./skill"
import { State } from "./state"
import { ToolRegistry } from "./tool/registry"
import { ToolHooks } from "./tool/hooks"
import { PluginHooks } from "./plugin/hooks"
export interface Interface {
readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect<void>
@@ -58,13 +57,12 @@ const layer = Layer.effect(
generation.length === definitions.length &&
generation.every(
(plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version,
) &&
definitions.every((definition) => active.has(definition.id))
)
) {
return
}
generation = undefined
yield* State.batch(
const exit = yield* State.batch(
Effect.gen(function* () {
const scopes = Array.from(active.values()).toReversed()
active.clear()
@@ -83,17 +81,13 @@ const layer = Layer.effect(
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
Effect.exit,
)
if (Exit.isFailure(loaded)) {
yield* Effect.logWarning("failed to load plugin", {
"plugin.id": definition.id,
cause: loaded.cause,
})
continue
}
if (Exit.isFailure(loaded)) return loaded
active.set(definition.id, child)
}
return Exit.void
}),
)
if (Exit.isFailure(exit)) return yield* exit
generation = definitions.map((definition) => ({
id: definition.id,
...(definition.version === undefined ? {} : { version: definition.version }),
@@ -137,7 +131,6 @@ export const node = makeLocationNode({
SkillV2.node,
ToolRegistry.toolsNode,
ToolHooks.node,
PluginHooks.node,
PluginRuntime.node,
],
})
-7
View File
@@ -125,7 +125,6 @@ export const Plugin = define({
yield* ctx.agent.transform((draft) => {
draft.update(AgentV2.defaultID, (item) => {
item.name = AgentV2.Name.make("Build")
item.description = "The default agent. Executes tools based on configured permissions."
item.mode = "primary"
item.permissions.push(
@@ -137,7 +136,6 @@ export const Plugin = define({
})
draft.update(AgentV2.ID.make("plan"), (item) => {
item.name = AgentV2.Name.make("Plan")
item.description = "Plan mode. Disallows all edit tools."
item.mode = "primary"
item.permissions.push(
@@ -157,7 +155,6 @@ export const Plugin = define({
})
draft.update(AgentV2.ID.make("general"), (item) => {
item.name = AgentV2.Name.make("General")
item.description =
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
item.mode = "subagent"
@@ -170,7 +167,6 @@ export const Plugin = define({
})
draft.update(AgentV2.ID.make("explore"), (item) => {
item.name = AgentV2.Name.make("Explore")
item.description =
'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
item.system = PROMPT_EXPLORE
@@ -193,7 +189,6 @@ export const Plugin = define({
})
draft.update(AgentV2.ID.make("compaction"), (item) => {
item.name = AgentV2.Name.make("Compaction")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_COMPACTION
@@ -201,7 +196,6 @@ export const Plugin = define({
})
draft.update(AgentV2.ID.make("title"), (item) => {
item.name = AgentV2.Name.make("Title")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_TITLE
@@ -209,7 +203,6 @@ export const Plugin = define({
})
draft.update(AgentV2.ID.make("summary"), (item) => {
item.name = AgentV2.Name.make("Summary")
item.mode = "primary"
item.hidden = true
item.system = PROMPT_SUMMARY
-67
View File
@@ -1,67 +0,0 @@
export * as PluginHooks from "./hooks"
import type { AISDKHooks } from "@opencode-ai/plugin/v2/effect/aisdk"
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
import type { ToolHooks } from "@opencode-ai/plugin/v2/effect/tool"
import { Context, Effect, Layer, Scope } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { State } from "../state"
export interface Domains {
readonly aisdk: AISDKHooks
readonly session: SessionHooks
readonly tool: ToolHooks
}
type Callback<Event> = (event: Event) => Effect.Effect<void>
export interface Interface {
readonly register: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
domain: Domain,
name: Name,
callback: Callback<Domains[Domain][Name]>,
) => Effect.Effect<State.Registration, never, Scope.Scope>
readonly trigger: <Domain extends keyof Domains, Name extends keyof Domains[Domain]>(
domain: Domain,
name: Name,
event: Domains[Domain][Name],
) => Effect.Effect<Domains[Domain][Name]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginHooks") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const callbacks = new Map<string, Function[]>()
const key = (domain: keyof Domains, name: PropertyKey) => `${domain}.${String(name)}`
const register: Interface["register"] = Effect.fn("PluginHooks.register")(function* (domain, name, callback) {
const scope = yield* Scope.Scope
const id = key(domain, name)
let active = true
callbacks.set(id, [...(callbacks.get(id) ?? []), callback])
const dispose = Effect.sync(() => {
if (!active) return
active = false
const next = (callbacks.get(id) ?? []).filter((item) => item !== callback)
if (next.length === 0) callbacks.delete(id)
else callbacks.set(id, next)
})
yield* Scope.addFinalizer(scope, dispose)
return { dispose }
})
const trigger: Interface["trigger"] = Effect.fn("PluginHooks.trigger")(function* (domain, name, event) {
for (const callback of callbacks.get(key(domain, name)) ?? []) {
const result: Effect.Effect<void> = Reflect.apply(callback, undefined, [event])
yield* result
}
return event
})
return Service.of({ register, trigger })
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [] })
+68 -74
View File
@@ -1,6 +1,6 @@
export * as PluginHost from "./host"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent"
@@ -22,7 +22,6 @@ import { Tool } from "../tool/tool"
import { Tools } from "../tool/tools"
import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
import { PluginHooks } from "./hooks"
const mutable = <T>(value: T) => value as DeepMutable<T>
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
@@ -37,7 +36,6 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
const skill = yield* SkillV2.Service
const tools = yield* Tools.Service
const toolHooks = yield* ToolHooks.Service
const hooks = yield* PluginHooks.Service
const runtime = yield* PluginRuntime.Service
const locationInfo = () =>
new Location.Info({
@@ -45,7 +43,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
workspaceID: location.workspaceID,
project: location.project,
})
const locationRef = (input?: Parameters<Plugin.Context["agent"]["list"]>[0]) =>
const locationRef = (input?: Parameters<PluginContext["agent"]["list"]>[0]) =>
input?.location === undefined
? undefined
: Location.Ref.make({
@@ -81,32 +79,32 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
aisdk: {
hook: (name, callback) => {
if (name === "sdk") {
return aisdk.hook.sdk((event) => {
const output = {
model: mutable(event.model),
package: event.package,
options: event.options,
sdk: event.sdk,
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
})
}
return aisdk.hook.language((event) => {
sdk: (callback) =>
aisdk.hook.sdk((event) => {
const output = {
model: mutable(event.model),
package: event.package,
options: event.options,
sdk: event.sdk,
}
const result = callback(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
}),
language: (callback) =>
aisdk.hook.language((event) => {
const output = {
model: mutable(event.model),
sdk: event.sdk,
options: event.options,
language: event.language,
}
return Reflect.apply(callback, undefined, [output]).pipe(
const result = callback(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
)
})
},
}),
},
catalog: {
provider: {
@@ -166,29 +164,25 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
integration: {
list: () => response(integration.list()),
get: (input) => response(integration.get(Integration.ID.make(input.integrationID))),
connect: {
key: (input) =>
integration.connection.key({
connectKey: (input) =>
integration.connection.key({
integrationID: Integration.ID.make(input.integrationID),
key: input.key,
label: input.label,
}),
connectOauth: (input) =>
response(
integration.connection.oauth({
integrationID: Integration.ID.make(input.integrationID),
key: input.key,
methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs,
label: input.label,
}),
oauth: (input) =>
response(
integration.connection.oauth({
integrationID: Integration.ID.make(input.integrationID),
methodID: Integration.MethodID.make(input.methodID),
inputs: input.inputs,
label: input.label,
}),
),
},
attempt: {
status: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
complete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
cancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
},
),
attemptStatus: (input) => response(integration.attempt.status(Integration.AttemptID.make(input.attemptID))),
attemptComplete: (input) =>
integration.attempt.complete({ attemptID: Integration.AttemptID.make(input.attemptID), code: input.code }),
attemptCancel: (input) => integration.attempt.cancel(Integration.AttemptID.make(input.attemptID)),
reload: integration.reload,
connection: {
active: (id) => integration.connection.active(Integration.ID.make(id)),
@@ -323,12 +317,11 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
registrations,
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
{ discard: true },
).pipe(Effect.orDie)
return { dispose: Effect.void }
)
}),
hook: (name, callback) => {
if (name === "execute.before") {
return toolHooks.hook.before((event) => {
execute: {
before: (callback) =>
toolHooks.hook.before((event) => {
const output = {
tool: event.tool,
sessionID: event.sessionID,
@@ -337,37 +330,38 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
toolCallID: event.toolCallID,
input: event.input,
}
return Reflect.apply(callback, undefined, [output]).pipe(
const result = callback(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
)
})
}
return toolHooks.hook.after((event) => {
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
assistantMessageID: event.assistantMessageID,
toolCallID: event.toolCallID,
input: event.input,
result: event.result,
output: event.output,
outputPaths: event.outputPaths,
}
return Reflect.apply(callback, undefined, [output]).pipe(
Effect.tap(() =>
Effect.sync(() => {
event.result = output.result
event.output = output.output
event.outputPaths = output.outputPaths
}),
),
)
})
}),
after: (callback) =>
toolHooks.hook.after((event) => {
const output = {
tool: event.tool,
sessionID: event.sessionID,
agent: event.agent,
assistantMessageID: event.assistantMessageID,
toolCallID: event.toolCallID,
input: event.input,
result: event.result,
output: event.output,
outputPaths: event.outputPaths,
}
const result = callback(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() =>
Effect.sync(() => {
event.result = output.result
event.output = output.output
event.outputPaths = output.outputPaths
}),
),
)
}),
},
},
session: {
hook: (name, callback) => hooks.register("session", name, callback),
create: (input) =>
runtime.session.create({
id: input?.id,
@@ -381,5 +375,5 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
command: runtime.session.command,
interrupt: (input) => runtime.session.interrupt(input.sessionID),
},
} satisfies Plugin.Context
} satisfies PluginContext
})
+3 -3
View File
@@ -1,6 +1,6 @@
export * as PluginInternal from "./internal"
import type { Plugin } from "@opencode-ai/plugin/v2/effect/plugin"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import { Context, Effect, Scope } from "effect"
import { HttpClient } from "effect/unstable/http"
import { AgentV2 } from "../agent"
@@ -31,7 +31,7 @@ import { SessionInstructions } from "../session/instructions"
import { SessionTodo } from "../session/todo"
import { Shell } from "../shell"
import { SkillV2 } from "../skill"
import { PatchTool } from "../tool/patch"
import { ApplyPatchTool } from "../tool/apply-patch"
import { EditTool } from "../tool/edit"
import { GlobTool } from "../tool/glob"
import { GrepTool } from "../tool/grep"
@@ -127,7 +127,7 @@ const pre = [
SkillPlugin.Plugin,
ModelsDevPlugin,
...ProviderPlugins,
PatchTool.Plugin,
ApplyPatchTool.Plugin,
EditTool.Plugin,
GlobTool.Plugin,
GrepTool.Plugin,
+20 -34
View File
@@ -1,6 +1,5 @@
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
import { Money } from "@opencode-ai/schema/money"
import type { ModelInfo } from "@opencode-ai/sdk/v2/types"
import type { ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelV2 } from "../model"
@@ -12,13 +11,13 @@ function released(date: string) {
return Number.isFinite(time) ? time : 0
}
function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] {
const base = {
input: input?.input ?? Money.USDPerMillionTokens.zero,
output: input?.output ?? Money.USDPerMillionTokens.zero,
input: input?.input ?? 0,
output: input?.output ?? 0,
cache: {
read: input?.cache_read ?? Money.USDPerMillionTokens.zero,
write: input?.cache_write ?? Money.USDPerMillionTokens.zero,
read: input?.cache_read ?? 0,
write: input?.cache_write ?? 0,
},
}
return [
@@ -28,8 +27,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
input: item.input,
output: item.output,
cache: {
read: item.cache_read ?? Money.USDPerMillionTokens.zero,
write: item.cache_write ?? Money.USDPerMillionTokens.zero,
read: item.cache_read ?? 0,
write: item.cache_write ?? 0,
},
})) ?? []),
...(input?.context_over_200k
@@ -42,8 +41,8 @@ function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: input.context_over_200k.cache_read ?? Money.USDPerMillionTokens.zero,
write: input.context_over_200k.cache_write ?? Money.USDPerMillionTokens.zero,
read: input.context_over_200k.cache_read ?? 0,
write: input.context_over_200k.cache_write ?? 0,
},
},
]
@@ -51,13 +50,13 @@ function cost(input: ModelsDev.Model["cost"]): ModelInfo["cost"] {
]
}
function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] | undefined) {
function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) {
if (!override) return base
const next = cost(override)
const [baseDefault, ...baseTiers] = base
const [nextDefault, ...nextTiers] = next
const tierKey = (item: ModelInfo["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelInfo["cost"][number], right: ModelInfo["cost"][number]) => ({
const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}`
const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({
...left,
...right,
tier: right.tier ?? left.tier,
@@ -68,25 +67,12 @@ function mergeCost(base: ModelInfo["cost"], override: ModelsDev.Model["cost"] |
const current = tiers.get(tierKey(item))
tiers.set(tierKey(item), current ? merge(current, item) : item)
}
return [
merge(
baseDefault ?? {
input: Money.USDPerMillionTokens.zero,
output: Money.USDPerMillionTokens.zero,
cache: {
read: Money.USDPerMillionTokens.zero,
write: Money.USDPerMillionTokens.zero,
},
},
nextDefault,
),
...tiers.values(),
]
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
}
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelInfo["variants"]> {
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): NonNullable<ModelV2Info["variants"]> {
const npm = model.provider?.npm ?? provider.npm
const options = model.reasoning_options ?? []
const effort = options.find((option) => option.type === "effort")
@@ -131,7 +117,7 @@ function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.
function budgetVariants(
npm: string | undefined,
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
): NonNullable<ModelInfo["variants"]> {
): NonNullable<ModelV2Info["variants"]> {
const max = option.max
const high =
option.max === undefined
@@ -160,7 +146,7 @@ function modeName(model: ModelsDev.Model, mode: string) {
return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}`
}
function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]>) {
function mergeVariants(model: ModelV2Info, next: NonNullable<ModelV2Info["variants"]>) {
const variants = model.variants ?? []
const existing = new Map(variants.map((variant) => [variant.id, variant]))
const nextIDs = new Set(next.map((variant) => variant.id))
@@ -171,13 +157,13 @@ function mergeVariants(model: ModelInfo, next: NonNullable<ModelInfo["variants"]
}
function applyModel(
draft: ModelInfo,
draft: ModelV2Info,
model: ModelsDev.Model,
input: {
readonly name?: string
readonly cost?: ModelInfo["cost"]
readonly cost?: ModelV2Info["cost"]
readonly request?: NonNullable<NonNullable<ModelsDev.Model["experimental"]>["modes"]>[string]["provider"]
readonly variants?: NonNullable<ModelInfo["variants"]>
readonly variants?: NonNullable<ModelV2Info["variants"]>
} = {},
) {
draft.name = input.name ?? model.name
+14 -24
View File
@@ -1,12 +1,11 @@
export * as PluginPromise from "./promise"
import { Plugin } from "@opencode-ai/plugin/v2/effect"
import { define } from "@opencode-ai/plugin/v2/effect"
import type { Plugin, PluginContext } from "@opencode-ai/plugin/v2/promise"
import { Effect, Scope, Stream } from "effect"
type HostRegistration = { readonly dispose: Effect.Effect<void> }
type Registration = { readonly dispose: () => Promise<void> }
type PromisePlugin = import("@opencode-ai/plugin/v2/plugin").Plugin
type PromisePluginContext = import("@opencode-ai/plugin/v2/plugin").Context
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
@@ -17,8 +16,8 @@ type PromisePluginContext = import("@opencode-ai/plugin/v2/plugin").Context
* preserves boot-time batching, so Promise-plugin transforms still coalesce
* into one reload per domain.
*/
export function fromPromise(plugin: PromisePlugin) {
return Plugin.define({
export function fromPromise(plugin: Plugin) {
return define({
id: plugin.id,
effect: (host) =>
Effect.gen(function* () {
@@ -44,7 +43,7 @@ export function fromPromise(plugin: PromisePlugin) {
}),
)
const context2: PromisePluginContext = {
const context2: PluginContext = {
options: host.options,
agent: {
list: (input) => run(host.agent.list(input)),
@@ -52,8 +51,10 @@ export function fromPromise(plugin: PromisePlugin) {
reload: () => run(host.agent.reload()),
},
aisdk: {
hook: (name, callback) =>
register(host.aisdk.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
sdk: (callback) =>
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
language: (callback) =>
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
catalog: {
provider: {
@@ -78,15 +79,11 @@ export function fromPromise(plugin: PromisePlugin) {
integration: {
list: (input) => run(host.integration.list(input)),
get: (input) => run(host.integration.get(input)),
connect: {
key: (input) => run(host.integration.connect.key(input)),
oauth: (input) => run(host.integration.connect.oauth(input)),
},
attempt: {
status: (input) => run(host.integration.attempt.status(input)),
complete: (input) => run(host.integration.attempt.complete(input)),
cancel: (input) => run(host.integration.attempt.cancel(input)),
},
connectKey: (input) => run(host.integration.connectKey(input)),
connectOauth: (input) => run(host.integration.connectOauth(input)),
attemptStatus: (input) => run(host.integration.attemptStatus(input)),
attemptComplete: (input) => run(host.integration.attemptComplete(input)),
attemptCancel: (input) => run(host.integration.attemptCancel(input)),
transform: transform(host.integration),
reload: () => run(host.integration.reload()),
connection: {
@@ -107,19 +104,12 @@ export function fromPromise(plugin: PromisePlugin) {
transform: transform(host.skill),
reload: () => run(host.skill.reload()),
},
tool: {
transform: transform(host.tool),
hook: (name, callback) =>
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
session: {
create: (input) => run(host.session.create(input)),
get: (input) => run(host.session.get(input)),
prompt: (input) => run(host.session.prompt(input)),
command: (input) => run(host.session.command(input)),
interrupt: (input) => run(host.session.interrupt(input)),
hook: (name, callback) =>
register(host.session.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
}
+1 -2
View File
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const AlibabaPlugin = define({
id: "opencode.provider.alibaba",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/alibaba") return
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
@@ -75,8 +75,7 @@ export const AmazonBedrockPlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
const options = { ...evt.options }
@@ -109,8 +108,7 @@ export const AmazonBedrockPlugin = define({
evt.sdk = mod.createAmazonBedrock(options)
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (
@@ -17,8 +17,7 @@ export const AnthropicPlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
+3 -6
View File
@@ -26,8 +26,7 @@ export const AzurePlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) {
@@ -45,8 +44,7 @@ export const AzurePlugin = define({
evt.sdk = mod.createAzure(evt.options)
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage(
@@ -77,8 +75,7 @@ export const AzureCognitiveServicesPlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(
@@ -14,8 +14,7 @@ export const CerebrasPlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
@@ -6,8 +6,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CloudflareAIGatewayPlugin = define({
id: "opencode.provider.cloudflare-ai-gateway",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "ai-gateway-provider") return
if (evt.options.baseURL) return
@@ -19,8 +19,7 @@ export const CloudflareWorkersAIPlugin = define({
if (accountId) provider.settings = { ...provider.settings, baseURL: workersEndpoint(accountId) }
})
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return
if (evt.package !== "@ai-sdk/openai-compatible") return
@@ -36,8 +35,7 @@ export const CloudflareWorkersAIPlugin = define({
)
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.modelID ?? evt.model.id)
+1 -2
View File
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const CoherePlugin = define({
id: "opencode.provider.cohere",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cohere") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const DeepInfraPlugin = define({
id: "opencode.provider.deepinfra",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/deepinfra") return
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
+1 -2
View File
@@ -7,8 +7,7 @@ export const DynamicProviderPlugin = define({
id: "opencode.provider.dynamic",
effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.sdk) return
+1 -2
View File
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GatewayPlugin = define({
id: "opencode.provider.gateway",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/gateway") return
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
@@ -23,16 +23,14 @@ export const GithubCopilotPlugin = define({
model.enabled = false
})
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/github-copilot") return
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
evt.sdk = mod.createOpenaiCompatible(evt.options)
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
+2 -4
View File
@@ -7,8 +7,7 @@ import { ProviderV2 } from "../../provider"
export const GitLabPlugin = define({
id: "opencode.provider.gitlab",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "gitlab-ai-provider") return
const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
@@ -32,8 +31,7 @@ export const GitLabPlugin = define({
})
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags =
@@ -85,8 +85,7 @@ export const GoogleVertexPlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch)
@@ -105,8 +104,7 @@ export const GoogleVertexPlugin = define({
})
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
@@ -137,8 +135,7 @@ export const GoogleVertexAnthropicPlugin = define({
})
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
@@ -164,8 +161,7 @@ export const GoogleVertexAnthropicPlugin = define({
})
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.modelID ?? evt.model.id).trim())
+1 -2
View File
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GooglePlugin = define({
id: "opencode.provider.google",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
+1 -2
View File
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const GroqPlugin = define({
id: "opencode.provider.groq",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/groq") return
const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
+1 -2
View File
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const MistralPlugin = define({
id: "opencode.provider.mistral",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/mistral") return
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const OpenAICompatiblePlugin = define({
id: "opencode.provider.openai-compatible",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.sdk) return
if (!evt.package.includes("@ai-sdk/openai-compatible")) return
+2 -4
View File
@@ -210,16 +210,14 @@ export const OpenAIPlugin = define({
Effect.forkScoped({ startImmediately: true }),
)
yield* refresh().pipe(Effect.forkScoped)
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/openai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
evt.sdk = mod.createOpenAI(evt.options)
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.modelID ?? evt.model.id)
+7 -11
View File
@@ -10,7 +10,6 @@ import { Integration } from "../../integration"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { ConfigProviderV1 } from "../../v1/config/provider"
import { Money } from "@opencode-ai/schema/money"
import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options"
import { ConfigV1 } from "../../v1/config/config"
@@ -221,23 +220,20 @@ function withoutCredentials(body: Readonly<Record<string, unknown>> | undefined)
function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) {
const base = {
input: Money.USDPerMillionTokens.make(input.input),
output: Money.USDPerMillionTokens.make(input.output),
cache: {
read: Money.USDPerMillionTokens.make(input.cache_read ?? 0),
write: Money.USDPerMillionTokens.make(input.cache_write ?? 0),
},
input: input.input,
output: input.output,
cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 },
}
if (!input.context_over_200k) return [base]
return [
base,
{
tier: { type: "context" as const, size: 200_000 },
input: Money.USDPerMillionTokens.make(input.context_over_200k.input),
output: Money.USDPerMillionTokens.make(input.context_over_200k.output),
input: input.context_over_200k.input,
output: input.context_over_200k.output,
cache: {
read: Money.USDPerMillionTokens.make(input.context_over_200k.cache_read ?? 0),
write: Money.USDPerMillionTokens.make(input.context_over_200k.cache_write ?? 0),
read: input.context_over_200k.cache_read ?? 0,
write: input.context_over_200k.cache_write ?? 0,
},
},
]
@@ -23,8 +23,7 @@ export const OpenRouterPlugin = define({
}
}
})
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@openrouter/ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
@@ -4,8 +4,7 @@ import { define } from "@opencode-ai/plugin/v2/effect/plugin"
export const PerplexityPlugin = define({
id: "opencode.provider.perplexity",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/perplexity") return
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
@@ -8,8 +8,7 @@ export const SapAICorePlugin = define({
id: "opencode.provider.sap-ai-core",
effect: Effect.fn(function* (ctx) {
const npm = yield* Npm.Service
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
const serviceKey =
@@ -38,8 +37,7 @@ export const SapAICorePlugin = define({
)
}),
)
yield* ctx.aisdk.hook(
"language",
yield* ctx.aisdk.language(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.modelID ?? evt.model.id)
@@ -67,8 +67,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
export const SnowflakeCortexPlugin = define({
id: "opencode.provider.snowflake-cortex",
effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook(
"sdk",
yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
const token =

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