Compare commits

..

23 Commits

Author SHA1 Message Date
Kit Langton 910af9a122 fix(cli): restart stale clients after updates 2026-07-05 13:31:19 -04:00
Aiden Cline f9d1d3b259 refactor(codemode): namespace public types (#35435) 2026-07-05 11:51:50 -05:00
Aiden Cline f950497173 refactor(codemode): remove generic agent tool (#35420) 2026-07-05 10:41:21 -05:00
Aiden Cline 391aa38281 test(core): serve skill fixtures locally (#35429) 2026-07-05 10:40:55 -05:00
Aiden Cline 29e8502bb0 fix(tui): align integration empty state (#35414) 2026-07-05 09:08:09 -05:00
Kit Langton 44b182fe23 fix(core): validate scalar newtypes (#35381)
publish / version (push) Has been cancelled
publish / build-cli (push) Has been cancelled
publish / sign-cli-windows (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Has been cancelled
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Has been cancelled
publish / publish (push) Has been cancelled
typecheck / typecheck (push) Has been cancelled
2026-07-04 21:43:08 -04:00
Kit Langton 905123b9c0 fix(protocol): keep internal events off SSE (#35378) 2026-07-04 21:12:45 -04:00
Kit Langton bb3b6a2f65 fix(protocol): expose MCP tool change events (#35373) 2026-07-04 20:47:33 -04:00
Aiden Cline b8efb33cde feat(codemode): add OpenAPI tool adapter (#35362) 2026-07-04 16:28:11 -05:00
Aiden Cline d65ecd4a90 feat(core): expose deferred tools through execute (#35361) 2026-07-04 16:20:56 -05:00
Simon Klee 57fb3e5cc5 fix(run): align mini with current session contracts (#35354) 2026-07-04 21:14:01 +02:00
Simon Klee ba07481b59 fix(run): restore subprocess output contracts 2026-07-04 21:06:32 +02:00
Aiden Cline b99759c7de fix(core): enforce mcp tool permissions (#35345) 2026-07-04 13:03:40 -05:00
Aiden Cline c590e27639 feat(core): add grouped and deferred tool registration (#35232) 2026-07-04 11:49:24 -05:00
Kit Langton 8f4b62eb49 fix(core): report missing search paths (#35337) 2026-07-04 12:12:56 -04:00
Kit Langton 945d1c8cb2 fix(core): bound recursive file watching (#35323) 2026-07-04 11:55:05 -04:00
Kit Langton 610e618bc5 fix(tui): clear completed background shell status (#35320) 2026-07-04 11:33:45 -04:00
Kit Langton 9daa4d85a4 fix(tui): reconcile session state after reconnect (#35262) 2026-07-04 11:05:41 -04:00
Kit Langton 62af66a74f fix(tui): distinguish variant switch notices (#35315) 2026-07-04 10:58:56 -04:00
Kit Langton 5b44e5bf41 test(core): release shell test locations (#35271) 2026-07-04 10:54:34 -04:00
Dax Raad a15afbe8f2 bye bye orchestrator
publish / version (push) Has been cancelled
publish / build-cli (push) Has been cancelled
publish / sign-cli-windows (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Has been cancelled
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Has been cancelled
publish / publish (push) Has been cancelled
typecheck / typecheck (push) Has been cancelled
2026-07-04 02:04:58 -04:00
Dax Raad 8e0856c43b fix(core): disable external plugin hot reload 2026-07-04 01:58:17 -04:00
Dax Raad f66c829231 Revert "fix(core): bust external plugin import cache"
This reverts commit afe3ebbc35.
2026-07-04 01:54:30 -04:00
161 changed files with 33628 additions and 4755 deletions
-36
View File
@@ -1,36 +0,0 @@
export default {
id: "Orchestrator",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
agents.update("orchestrator", (agent) => {
agent.description = "Coordinates work by delegating implementation tasks to the minion subagent."
agent.mode = "primary"
agent.system = [
"You are Orchestrator, the primary coordinating agent for this repository. You do meta work only: you coordinate, brief, and synthesize — you do not perform the work itself.",
"Delegate ALL actual work to the minion subagent — implementation, exploration, discovery, searching the codebase, reading files to understand a problem, and even trivial one-line edits. Task size is never a reason to do it yourself, and there is no 'final integration' exception.",
"You are not hard-banned from tools, but direct tool use is reserved for coordination overhead: a quick peek to phrase a better brief, a fast read-only check to verify a minion's reported result, or answering a question about coordination state. If a tool call is producing the answer or the artifact the user asked for, that call belongs to a minion, not you.",
"Exploration is work. If the user asks how something works or where something lives, delegate the investigation to a minion rather than exploring yourself.",
"Always start minion subagents in the background. Even if you have nothing else to coordinate right now, the user may assign you new work while a Minion runs, and you must stay free to receive it. Never poll; you will be notified when they finish.",
"Give each minion a clear, self-contained brief: the goal, constraints, expected output, and any files or context already known from the user or previous minion reports.",
"Synthesize minion results, decide next steps, and report back concisely.",
].join("\n")
})
agents.update("minion", (agent) => {
agent.description = "Subagent that executes focused tasks delegated by Orchestrator."
agent.mode = "subagent"
agent.model = { providerID: "opencode", id: "glm-5.2" }
agent.system = [
"You are minion, a focused execution subagent for this repository.",
"Complete the specific task delegated to you by Orchestrator using the available tools.",
"Inspect the codebase before making assumptions, make targeted changes when requested, and verify your work when feasible.",
"Follow the repository's AGENTS.md conventions: respect the style guide, run `bun typecheck` from the affected package directory after code changes, never run tests from the repo root, and do not modify packages/opencode unless the task explicitly says V1 work.",
"If the task is ambiguous or you hit a blocker, stop and report your findings instead of guessing.",
"Keep your final response concise: summarize what you did, list important files changed or findings, and call out blockers or verification gaps.",
"Do not delegate to other subagents; execute the assigned work yourself.",
].join("\n")
agent.permissions.push({ action: "subagent", resource: "*", effect: "deny" })
})
})
},
}
+19
View File
@@ -104,6 +104,25 @@ bun dev api <operationId> --param key=value
- If no compatible background server is registered, `bun dev api` starts one through the daemon service. Use `bun dev service status`, `bun dev service restart`, and `bun dev service stop` when you need explicit lifecycle control.
- Prefer raw method/path calls for quick server debugging and operation IDs when exercising documented OpenAPI routes with path or query parameters.
## Auditing installed `opencode2` sessions
Installed next-channel sessions normally use `~/.local/share/opencode/opencode-next.db` and `~/.local/share/opencode/log/opencode.log`; `OPENCODE_DB` can override the database. Before calling `opencode2 api`, inspect `~/.local/state/opencode/service.json` because the command may start a daemon when none is healthy.
For a supplied `ses_...` ID, compare three sources:
- `opencode2 api get /api/session/active` and the Session/message endpoints for live server state.
- The database's ordered `event` rows for durable history.
- `packages/tui/src/context/data.tsx` and the relevant route for client projection and rendering.
Locate an uncertain database without modifying it:
```bash
SESSION=ses_...
for db in ~/.local/share/opencode/*.db; do
sqlite3 "file:$db?mode=ro" "select 1 from session where id='$SESSION' limit 1" 2>/dev/null | grep -q 1 && printf '%s\n' "$db"
done
```
## Logs
- Log files live under `~/.local/share/opencode/log/`. In a local/dev checkout the active file is `opencode-local.log`; `opencode.log` is used for non-local (released) channel installs. Both are append-only, shared across every CLI and server process on the machine.
+1
View File
@@ -151,6 +151,7 @@ const table = sqliteTable("session", {
## V2 Session Core
- Keep durable events minimal: record irreducible new facts and do not repeat state derivable by folding the ordered aggregate history. Enrich projections and read models with previous or derived state when consumers need self-contained views.
- Keep durable prompt admission separate from model execution. `SessionV2.prompt(...)` admits one durable `session_input` row before scheduling advisory `SessionExecution.wake(sessionID)` unless `resume: false` requests admit-only behavior. The serialized runner promotes admitted inputs into visible user messages at safe boundaries.
- Reusing a Session ID adopts the existing Session. Reusing a prompt message ID reconciles an exact retry only when Session, prompt, and delivery mode match; conflicting reuse fails. Historical projected prompts lazily synthesize promoted inbox records during exact retry.
- Keep `SessionExecution` process-global and Session-ID based. Its local implementation owns the process-local Session coordinator and discovers placement through `SessionStore` plus `LocationServiceMap.get(session.location)` only when a drain starts; no layer should take a Session ID. V2 interruption targets the active process-local ownership chain for that Session; idle or missing interruption is a no-op.
+1
View File
@@ -321,6 +321,7 @@
"@modelcontextprotocol/sdk": "1.29.0",
"@npmcli/arborist": "9.4.0",
"@npmcli/config": "10.8.1",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*",
+3 -1
View File
@@ -6,8 +6,9 @@ const path = require("path")
const os = require("os")
const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"]
const restartExitCode = 75
function run(target) {
function run(target, restarted = false) {
const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
child.on("error", (error) => {
console.error(error.message)
@@ -25,6 +26,7 @@ function run(target) {
child.on("exit", (code, signal) => {
for (const forwardedSignal of forwardedSignals) process.removeListener(forwardedSignal, forwarders[forwardedSignal])
if (signal) return process.kill(process.pid, signal)
if (code === restartExitCode && !restarted) return run(target, true)
process.exit(typeof code === "number" ? code : 0)
})
}
+31 -6
View File
@@ -8,6 +8,8 @@ import { ServiceConfig } from "../../services/service-config"
import { Standalone } from "../../services/standalone"
import { Updater } from "../../services/updater"
const restartExitCode = 75
export default Runtime.handler(Commands, (input) =>
Effect.gen(function* () {
const directory = Option.getOrUndefined(input.directory)
@@ -22,9 +24,7 @@ export default Runtime.handler(Commands, (input) =>
const password = yield* Env.password
const explicit = {
url: server,
headers: password
? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) }
: undefined,
headers: password ? { authorization: "Basic " + btoa("opencode:" + Redacted.value(password)) } : undefined,
} satisfies Service.Transport
// Fail loudly before entering the TUI: an explicit server that is
// unreachable or rejects auth should not present as reconnect churn.
@@ -60,7 +60,7 @@ export default Runtime.handler(Commands, (input) =>
Effect.gen(function* () {
const found = yield* Service.discover(serviceOptions)
return found ?? (yield* Service.start(serviceOptions))
}).pipe(Effect.provide(NodeFileSystem.layer)),
}).pipe(Effect.catchIf(versionMismatch, restart), Effect.provide(NodeFileSystem.layer)),
)
: () => Promise.resolve(transport)
// Restart the managed service in place; start() resolves once the
@@ -76,11 +76,36 @@ export default Runtime.handler(Commands, (input) =>
}).pipe(Effect.provide(NodeFileSystem.layer)),
)
: undefined
yield* runTui(
return yield* runTui(
transport,
{ continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
discover,
reload,
() => {
process.exitCode = restartExitCode
},
)
}),
}).pipe(
Effect.catchIf(versionMismatch, (error) =>
restart(error).pipe(
Effect.tap(() =>
Effect.sync(() => {
process.exitCode = restartExitCode
}),
),
Effect.asVoid,
),
),
),
)
function restart(error: Service.VersionMismatchError) {
return Effect.logInfo("restarting stale client", {
clientVersion: error.clientVersion,
serverVersion: error.serverVersion,
}).pipe(Effect.as({ restart: true as const }))
}
function versionMismatch(error: unknown): error is Service.VersionMismatchError {
return error instanceof Service.VersionMismatchError
}
+1 -1
View File
@@ -56,6 +56,6 @@ Effect.logInfo("cli starting", {
Effect.provide(LoggingLayer),
Effect.provide(NodeServices.layer),
Effect.scoped,
Effect.tap(() => Effect.sync(() => process.exit(0))),
Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))),
NodeRuntime.runMain,
)
@@ -4,6 +4,7 @@ import { Service } from "@opencode-ai/client/effect"
import { Effect, FileSystem, Schema } from "effect"
import { randomBytes } from "crypto"
import path from "path"
import semver from "semver"
// The CLI's service configuration file, plus the Service.Options binding that
// points the client package's service operations at this CLI: which
@@ -45,10 +46,21 @@ export const options = Effect.fnUntraced(function* () {
return {
file,
version: InstallationVersion,
canReplace: (version: string | undefined) => canReplaceVersion(version, InstallationVersion),
command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"],
}
})
export function canReplaceVersion(serverVersion: string | undefined, clientVersion: string) {
if (serverVersion === undefined) return true
// Preview versions end in `<channel>-<build>[.<attempt>]`. Convert the build
// to a numeric semver identifier so next-15000 sorts after next-9999.
const server = serverVersion.replace(/-(\d+)(?=(?:\.\d+)?$)/, ".$1")
const client = clientVersion.replace(/-(\d+)(?=(?:\.\d+)?$)/, ".$1")
if (!semver.valid(server) || !semver.valid(client)) return true
return semver.lt(server, client)
}
export const read = Effect.fn("cli.service-config.read")(function* () {
const { fs, configFile } = yield* env
return yield* fs.readFileString(configFile).pipe(
+4 -1
View File
@@ -12,8 +12,9 @@ import type { Args } from "@opencode-ai/tui/context/args"
export function runTui(
transport: Service.Transport,
args: Args,
discover?: () => Promise<Service.Transport>,
discover?: () => Promise<Service.Transport | { restart: true }>,
reload?: () => Promise<void>,
restart?: () => void,
) {
const config = TuiConfig.resolve({}, { terminalSuspend: false })
let disposeSlots: (() => void) | undefined
@@ -32,12 +33,14 @@ export function runTui(
discover: discover
? async () => {
const next = await discover()
if ("restart" in next) return next
return {
client: createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory }),
api: OpenCode.make({ baseUrl: next.url, headers: next.headers }),
}
}
: undefined,
restart,
reload,
args,
config,
@@ -0,0 +1,6 @@
const stateFile = process.argv[2]
const exit = Number(process.argv[3])
const count = (await Bun.file(stateFile).exists()) ? Number(await Bun.file(stateFile).text()) : 0
await Bun.write(stateFile, String(count + 1))
process.exit(exit || (count === 0 ? 75 : 0))
+43
View File
@@ -0,0 +1,43 @@
import { expect, test } from "bun:test"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
const launcher = path.join(import.meta.dir, "../bin/opencode2.cjs")
const fixture = path.join(import.meta.dir, "fixture/restart-child.ts")
test("restarts the installed binary once with the original arguments", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-launcher-"))
const state = path.join(root, "count")
try {
const child = Bun.spawn([process.execPath, launcher, fixture, state, "0"], {
env: { ...process.env, OPENCODE_BIN_PATH: process.execPath },
stdout: "pipe",
stderr: "pipe",
})
expect(await child.exited).toBe(0)
expect(await Bun.file(state).text()).toBe("2")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
test("bounds automatic restarts", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-launcher-"))
const state = path.join(root, "count")
try {
const child = Bun.spawn([process.execPath, launcher, fixture, state, "75"], {
env: { ...process.env, OPENCODE_BIN_PATH: process.execPath },
stdout: "pipe",
stderr: "pipe",
})
expect(await child.exited).toBe(75)
expect(await Bun.file(state).text()).toBe("2")
} finally {
await fs.rm(root, { recursive: true, force: true })
}
})
+12
View File
@@ -7,6 +7,18 @@ import os from "node:os"
import path from "node:path"
import { ServiceConfig } from "../src/services/service-config"
test("only replaces older semantic versions", () => {
expect(ServiceConfig.canReplaceVersion("1.0.0", "2.0.0")).toBe(true)
expect(ServiceConfig.canReplaceVersion("2.0.0", "1.0.0")).toBe(false)
expect(ServiceConfig.canReplaceVersion("1.0.0", "1.0.0")).toBe(false)
expect(ServiceConfig.canReplaceVersion("0.0.0-next-9999", "0.0.0-next-15000")).toBe(true)
expect(ServiceConfig.canReplaceVersion("0.0.0-next-15000", "0.0.0-next-9999")).toBe(false)
expect(ServiceConfig.canReplaceVersion("0.0.0-next-15000.1", "0.0.0-next-15000.2")).toBe(true)
expect(ServiceConfig.canReplaceVersion("0.0.0-next-15000.10", "0.0.0-next-15000.9")).toBe(false)
expect(ServiceConfig.canReplaceVersion("0.0.0-next-15000.10", "0.0.0-next-15000.10")).toBe(false)
expect(ServiceConfig.canReplaceVersion(undefined, "1.0.0")).toBe(true)
})
test("local channel stores service config with the local service filename", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
try {
+31 -4
View File
@@ -1,4 +1,4 @@
import { Effect, FileSystem, Option, Schedule, Schema } from "effect"
import { Data, Effect, FileSystem, Option, Schedule, Schema } from "effect"
import { spawn } from "node:child_process"
import { homedir } from "node:os"
import { join } from "node:path"
@@ -23,6 +23,9 @@ export type Options = {
// When set, discovery only returns a server reporting this exact version,
// and start() replaces a healthy server whose version differs.
readonly version?: string
// Decides whether start() may terminate a healthy version-mismatched server.
// Defaults to true for callers that do not need directional version handling.
readonly canReplace?: (version: string | undefined) => boolean
// Argv used to spawn the service. Defaults to ["opencode", "serve",
// "--service"] resolved from PATH.
readonly command?: ReadonlyArray<string>
@@ -44,7 +47,11 @@ export const start = Effect.fn("service.start")(function* (options: Options = {}
const compatible = yield* discover(options)
if (compatible !== undefined) return compatible
const mismatched = yield* find(options)
if (mismatched !== undefined) yield* kill(mismatched.info, options).pipe(Effect.ignore)
if (mismatched !== undefined) {
const error = replacementError(mismatched.info, options)
if (error) return yield* Effect.fail(error)
yield* kill(mismatched.info, options).pipe(Effect.ignore)
}
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
if (command === undefined) return yield* Effect.fail(new Error("Missing service command"))
@@ -67,8 +74,12 @@ export const start = Effect.fn("service.start")(function* (options: Options = {}
export const stop = Effect.fn("service.stop")(function* (options: Options = {}) {
const fs = yield* FileSystem.FileSystem
const existing = yield* find(options)
if (existing !== undefined) yield* kill(existing.info, options)
yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore)
if (existing !== undefined) {
const error = replacementError(existing.info, options)
if (error) return yield* Effect.fail(error)
yield* kill(existing.info, options)
}
return yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore)
})
function fallback() {
@@ -89,6 +100,22 @@ export const Info = Schema.Struct({
})
export type Info = typeof Info.Type
export class VersionMismatchError extends Data.TaggedError("ServiceVersionMismatchError")<{
readonly clientVersion: string | undefined
readonly serverVersion: string | undefined
readonly message: string
}> {}
function replacementError(info: Info, options: Options): VersionMismatchError | undefined {
if (options.version === undefined || info.version === options.version || options.canReplace?.(info.version) !== false)
return undefined
return new VersionMismatchError({
clientVersion: options.version,
serverVersion: info.version,
message: `Client version ${options.version} cannot replace server version ${info.version ?? "unknown"}`,
})
}
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
// A missing or corrupt file means no valid info; callers treat both
+125 -560
View File
@@ -928,6 +928,7 @@ export type SessionContextOutput = {
readonly time: { readonly created: number }
readonly type: "model-switched"
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
}
| {
readonly id: string
@@ -1006,20 +1007,23 @@ export type SessionContextOutput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "text"; readonly id: string; readonly text: string }
| {
readonly type: "reasoning"
readonly id: string
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
readonly type: "tool"
readonly id: string
readonly name: string
readonly executed?: boolean
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly provider?: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
}
readonly state:
| { readonly status: "pending"; readonly input: string }
| {
@@ -1057,37 +1061,7 @@ export type SessionContextOutput = {
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly structured: { readonly [x: string]: JsonValue }
readonly error:
| {
readonly type: "provider.rate-limit"
readonly message: string
readonly retryAfterMs?: number
}
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: JsonValue
}
readonly time: {
@@ -1099,7 +1073,7 @@ export type SessionContextOutput = {
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish?: string
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -1107,56 +1081,7 @@ export type SessionContextOutput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
readonly error?: { readonly type: "unknown"; readonly message: string }
}
| {
readonly type: "compaction"
@@ -1287,71 +1212,6 @@ export type SessionLogOutput =
readonly delivery: "steer" | "queue"
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" }
}
| {
readonly id: string
readonly created: number
@@ -1461,7 +1321,7 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish: string
readonly cost: number
readonly tokens: {
readonly input: number
@@ -1483,33 +1343,7 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
}
}
| {
@@ -1519,7 +1353,7 @@ export type SessionLogOutput =
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string }
}
| {
readonly id: string
@@ -1528,7 +1362,12 @@ export type SessionLogOutput =
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly text: string }
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly textID: string
readonly text: string
}
}
| {
readonly id: string
@@ -1540,7 +1379,8 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly state?: { readonly [x: string]: unknown }
readonly reasoningID: string
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
@@ -1553,8 +1393,9 @@ export type SessionLogOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly text: string
readonly state?: { readonly [x: string]: unknown }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
@@ -1596,9 +1437,12 @@ export type SessionLogOutput =
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly tool: string
readonly input: { readonly [x: string]: unknown }
readonly executed: boolean
readonly state?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
@@ -1637,8 +1481,10 @@ export type SessionLogOutput =
>
readonly outputPaths?: ReadonlyArray<string>
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
@@ -1652,77 +1498,32 @@ export type SessionLogOutput =
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.retry.scheduled"
readonly type: "session.retried"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: {
readonly message: string
readonly statusCode?: number
readonly isRetryable: boolean
readonly responseHeaders?: { readonly [x: string]: string }
readonly responseBody?: string
readonly metadata?: { readonly [x: string]: string }
}
}
}
| {
@@ -1788,7 +1589,7 @@ export type SessionLogOutput =
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly to: string }
readonly data: { readonly sessionID: string; readonly messageID: string }
}
)
| { readonly type: "log.synced"; readonly aggregateID: string; readonly seq?: number }
@@ -1821,6 +1622,7 @@ export type SessionMessageOutput = {
readonly time: { readonly created: number }
readonly type: "model-switched"
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
}
| {
readonly id: string
@@ -1899,20 +1701,23 @@ export type SessionMessageOutput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "text"; readonly id: string; readonly text: string }
| {
readonly type: "reasoning"
readonly id: string
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
readonly type: "tool"
readonly id: string
readonly name: string
readonly executed?: boolean
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly provider?: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
}
readonly state:
| { readonly status: "pending"; readonly input: string }
| {
@@ -1950,37 +1755,7 @@ export type SessionMessageOutput = {
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly structured: { readonly [x: string]: JsonValue }
readonly error:
| {
readonly type: "provider.rate-limit"
readonly message: string
readonly retryAfterMs?: number
}
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: JsonValue
}
readonly time: {
@@ -1992,7 +1767,7 @@ export type SessionMessageOutput = {
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish?: string
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -2000,56 +1775,7 @@ export type SessionMessageOutput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
readonly error?: { readonly type: "unknown"; readonly message: string }
}
| {
readonly type: "compaction"
@@ -2096,6 +1822,7 @@ export type MessageListOutput = {
readonly time: { readonly created: number }
readonly type: "model-switched"
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly previous?: { readonly id: string; readonly providerID: string; readonly variant?: string }
}
| {
readonly id: string
@@ -2174,20 +1901,23 @@ export type MessageListOutput = {
readonly agent: string
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
readonly content: ReadonlyArray<
| { readonly type: "text"; readonly text: string }
| { readonly type: "text"; readonly id: string; readonly text: string }
| {
readonly type: "reasoning"
readonly id: string
readonly text: string
readonly state?: { readonly [x: string]: JsonValue }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly time?: { readonly created: number; readonly completed?: number }
}
| {
readonly type: "tool"
readonly id: string
readonly name: string
readonly executed?: boolean
readonly providerState?: { readonly [x: string]: JsonValue }
readonly providerResultState?: { readonly [x: string]: JsonValue }
readonly provider?: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
readonly resultMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
}
readonly state:
| { readonly status: "pending"; readonly input: string }
| {
@@ -2225,37 +1955,7 @@ export type MessageListOutput = {
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
>
readonly structured: { readonly [x: string]: JsonValue }
readonly error:
| {
readonly type: "provider.rate-limit"
readonly message: string
readonly retryAfterMs?: number
}
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| {
readonly type: "aborted"
readonly message: string
readonly reason?: "user" | "shutdown" | "timeout"
}
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: JsonValue
}
readonly time: {
@@ -2267,7 +1967,7 @@ export type MessageListOutput = {
}
>
readonly snapshot?: { readonly start?: string; readonly end?: string; readonly files?: ReadonlyArray<string> }
readonly finish?: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish?: string
readonly cost?: number
readonly tokens?: {
readonly input: number
@@ -2275,56 +1975,7 @@ export type MessageListOutput = {
readonly reasoning: number
readonly cache: { readonly read: number; readonly write: number }
}
readonly error?:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly retry?: {
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
}
readonly error?: { readonly type: "unknown"; readonly message: string }
}
| {
readonly type: "compaction"
@@ -4837,63 +4488,14 @@ export type EventSubscribeOutput =
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.succeeded"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string }
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.failed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly type: "session.execution.settled"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly outcome: "success" | "failure" | "interrupted"
readonly error?: { readonly type: "unknown"; readonly message: string }
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.execution.interrupted"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly reason: "user" | "shutdown" | "superseded" }
}
| {
readonly id: string
readonly created: number
@@ -5003,7 +4605,7 @@ export type EventSubscribeOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly finish: "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown"
readonly finish: string
readonly cost: number
readonly tokens: {
readonly input: number
@@ -5025,29 +4627,7 @@ export type EventSubscribeOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
}
}
| {
@@ -5057,7 +4637,7 @@ export type EventSubscribeOutput =
readonly type: "session.text.started"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly textID: string }
}
| {
readonly id: string
@@ -5065,7 +4645,12 @@ export type EventSubscribeOutput =
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.text.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly delta: string }
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly textID: string
readonly delta: string
}
}
| {
readonly id: string
@@ -5074,7 +4659,12 @@ export type EventSubscribeOutput =
readonly type: "session.text.ended"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly text: string }
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly textID: string
readonly text: string
}
}
| {
readonly id: string
@@ -5086,7 +4676,8 @@ export type EventSubscribeOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly state?: { readonly [x: string]: unknown }
readonly reasoningID: string
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
@@ -5095,7 +4686,12 @@ export type EventSubscribeOutput =
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.reasoning.delta"
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly assistantMessageID: string; readonly delta: string }
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly delta: string
}
}
| {
readonly id: string
@@ -5107,8 +4703,9 @@ export type EventSubscribeOutput =
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly reasoningID: string
readonly text: string
readonly state?: { readonly [x: string]: unknown }
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
| {
@@ -5163,9 +4760,12 @@ export type EventSubscribeOutput =
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly tool: string
readonly input: { readonly [x: string]: unknown }
readonly executed: boolean
readonly state?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
@@ -5204,8 +4804,10 @@ export type EventSubscribeOutput =
>
readonly outputPaths?: ReadonlyArray<string>
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
@@ -5219,69 +4821,32 @@ export type EventSubscribeOutput =
readonly sessionID: string
readonly assistantMessageID: string
readonly callID: string
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: { readonly type: "unknown"; readonly message: string }
readonly result?: unknown
readonly executed: boolean
readonly resultState?: { readonly [x: string]: unknown }
readonly provider: {
readonly executed: boolean
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
}
}
}
| {
readonly id: string
readonly created: number
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "session.retry.scheduled"
readonly type: "session.retried"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly assistantMessageID: string
readonly attempt: number
readonly at: number
readonly error:
| { readonly type: "provider.rate-limit"; readonly message: string; readonly retryAfterMs?: number }
| { readonly type: "provider.auth"; readonly message: string }
| { readonly type: "provider.quota"; readonly message: string }
| { readonly type: "provider.content-filter"; readonly message: string }
| { readonly type: "provider.transport"; readonly message: string }
| { readonly type: "provider.internal"; readonly message: string }
| { readonly type: "provider.invalid-output"; readonly message: string }
| { readonly type: "provider.invalid-request"; readonly message: string }
| { readonly type: "provider.no-route"; readonly message: string }
| { readonly type: "provider.unknown"; readonly message: string }
| {
readonly type: "permission.rejected"
readonly message: string
readonly permission: string
readonly resources: ReadonlyArray<string>
}
| { readonly type: "tool.unknown"; readonly message: string; readonly name: string }
| { readonly type: "tool.stale"; readonly message: string; readonly name?: string }
| { readonly type: "tool.execution"; readonly message: string }
| { readonly type: "tool.result-missing"; readonly message: string; readonly callID?: string }
| { readonly type: "aborted"; readonly message: string; readonly reason?: "user" | "shutdown" | "timeout" }
| { readonly type: "unknown"; readonly message: string; readonly agent?: string }
readonly error: {
readonly message: string
readonly statusCode?: number
readonly isRetryable: boolean
readonly responseHeaders?: { readonly [x: string]: string }
readonly responseBody?: string
readonly metadata?: { readonly [x: string]: string }
}
}
}
| {
@@ -5355,7 +4920,7 @@ export type EventSubscribeOutput =
readonly type: "session.revert.committed"
readonly durable: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly to: string }
readonly data: { readonly sessionID: string; readonly messageID: string }
}
| {
readonly id: string
+49
View File
@@ -0,0 +1,49 @@
import { NodeFileSystem } from "@effect/platform-node"
import { afterEach, expect, test } from "bun:test"
import { Effect } from "effect"
import fs from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { Service } from "../src/effect/index"
const cleanup: Array<() => void | Promise<void>> = []
afterEach(async () => {
await Promise.all(cleanup.splice(0).map(async (run) => await run()))
})
test("does not start or stop a healthy service rejected by the version policy", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-client-service-"))
const child = Bun.spawn([process.execPath, "-e", "await new Promise(() => {})"])
const server = Bun.serve({ port: 0, fetch: () => Response.json({ healthy: true }) })
const file = path.join(root, "service.json")
cleanup.push(
() => child.kill(),
() => server.stop(true),
() => fs.rm(root, { recursive: true, force: true }),
)
await Bun.write(file, JSON.stringify({ id: "newer", version: "2.0.0", url: server.url.toString(), pid: child.pid }))
const options = {
file,
version: "1.0.0",
canReplace: () => false,
command: [process.execPath, "-e", "throw new Error('should not spawn')"],
}
const startError = await Service.start(options).pipe(
Effect.flip,
Effect.provide(NodeFileSystem.layer),
Effect.runPromise,
)
const stopError = await Service.stop(options).pipe(
Effect.flip,
Effect.provide(NodeFileSystem.layer),
Effect.runPromise,
)
expect(startError).toBeInstanceOf(Service.VersionMismatchError)
expect(startError).toMatchObject({ clientVersion: "1.0.0", serverVersion: "2.0.0" })
expect(stopError).toBeInstanceOf(Service.VersionMismatchError)
expect(process.kill(child.pid, 0)).toBe(true)
expect(await Bun.file(file).exists()).toBe(true)
})
+8
View File
@@ -5,6 +5,14 @@
- Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it.
- Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens.
## OpenAPI
- Generate an operation only when its transport semantics are supported; otherwise return a precise `skipped` reason.
- Never guess parameter serialization or malformed security semantics. Unsupported serialization is skipped and malformed security fails closed.
- Render unresolved schema constructs as `unknown`, never as invented TypeScript names.
- Keep network reads bounded and map expected encoding, transport, and decoding failures to model-safe `ToolError` values.
- Test supported behavior directly; do not reproduce adapter algorithms in tests.
## Future Design Notes
- If a captured user-visible output channel returns (an earlier `output.text`/`output.file`/`output.image` API was removed from v1), keep `output` as its name, distinct from the program return value: `return` stays the structured result for the model, while `output.*` describes artifacts the host may render into a conversation or UI after execution. Keep this host-neutral and let applications decide how captured output is delivered. In v1, hosts collect media host-side (outside the sandbox) instead.
+42 -19
View File
@@ -4,7 +4,7 @@ Effect-native confined code execution over explicit, schema-described tools.
CodeMode lets a model write a small JavaScript program that can call only the tools supplied by the host. The program can sequence calls, transform plain data, branch, loop, and run independent calls in parallel without receiving ambient filesystem, process, network, module, or application authority.
The package is currently private to this workspace. Its API is designed around three uses:
The package is currently private to this workspace. Its API is designed around one-shot and reusable execution:
```ts
// One execution
@@ -13,9 +13,6 @@ yield * CodeMode.execute({ tools, code })
// A reusable runtime
const runtime = CodeMode.make({ tools, limits })
yield * runtime.execute(code)
// One agent-facing code tool
const codeTool = runtime.agentTool()
```
## Install
@@ -63,7 +60,7 @@ const result =
`)
```
`result` is always an `ExecuteResult`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well.
@@ -86,6 +83,8 @@ const tool = Tool.make({
The description and schemas are part of the model-visible tool contract. Keep descriptions concrete and put authorization in `run` or in the service it calls.
Public tool types are grouped under the same namespace: `Tool.Definition`, `Tool.Options`, `Tool.SchemaType`, and `Tool.JsonSchema`.
### `CodeMode.execute`
Use `CodeMode.execute` for a single execution:
@@ -116,31 +115,32 @@ const runtime = CodeMode.make({
runtime.catalog() // structured tool descriptions
runtime.instructions() // model-facing syntax and tool guide
runtime.execute(source) // ExecuteResult
runtime.agentTool() // { name, description, input, output, execute }
runtime.execute(source) // CodeMode.Result
```
`catalog`, `instructions`, and `agentTool` are projections of the same configured tool tree. `agentTool().description` is exactly `instructions()`.
`CodeMode.Input`, `CodeMode.Result`, `CodeMode.Success`, `CodeMode.Failure`, `CodeMode.Diagnostic`, and `CodeMode.DiagnosticKind` are both Effect schemas and their inferred TypeScript types. Hosts can combine `CodeMode.Input` and `CodeMode.Result` with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool.
All other CodeMode types use the same namespace: `CodeMode.Options`, `CodeMode.ExecuteOptions`, `CodeMode.Runtime`, `CodeMode.ExecutionLimits`, `CodeMode.DiscoveryOptions`, `CodeMode.DataValue`, `CodeMode.ToolDescription`, and the `CodeMode.ToolCall*` observation types.
### Results
```ts
type ExecuteResult = ExecuteSuccess | ExecuteFailure
type Result = Success | Failure
interface ExecuteSuccess {
interface Success {
readonly ok: true
readonly value: Schema.Json
readonly value: CodeMode.DataValue
readonly logs?: ReadonlyArray<string>
readonly truncated?: boolean
readonly toolCalls: ReadonlyArray<ToolCall>
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
}
interface ExecuteFailure {
interface Failure {
readonly ok: false
readonly error: Diagnostic
readonly error: CodeMode.Diagnostic
readonly logs?: ReadonlyArray<string>
readonly truncated?: boolean
readonly toolCalls: ReadonlyArray<ToolCall>
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
}
```
@@ -152,6 +152,31 @@ interface ExecuteFailure {
`onToolCallEnd` receives `{ index, name, input, durationMs, outcome, message? }` when an admitted call settles. `outcome` is `"success"` or `"failure"`; `message` is the model-safe failure message and is present only on failure. Interrupted calls (for example when the execution timeout fires) do not produce an end event. Both hooks are Effect-returning and must not fail.
### OpenAPI tools
`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation. Dotted `operationId` values form namespaces such as `v2.session.get`. Missing IDs receive a flat method/path fallback such as `getUsersById`; names are sanitized and deduplicated. The host places the subtree under a key in its `tools` tree; that key is the model-visible namespace.
```ts
import { CodeMode, OpenAPI } from "@opencode-ai/codemode"
import { Effect } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
const api = OpenAPI.fromSpec({
spec: await Bun.file("openapi.json").json(), // parsed document (no YAML)
auth: {
resolve: ({ name, scopes, operation }) =>
name === "BearerAuth" ? Effect.succeed({ type: "bearer", token }) : Effect.succeed(undefined),
},
})
const runtime = CodeMode.make({ tools: { opencode: api.tools } })
const result = await Effect.runPromise(runtime.execute(code).pipe(Effect.provide(FetchHttpClient.layer)))
```
`fromSpec` is synchronous and returns `{ tools, skipped }`. The initial adapter supports query `form`/`deepObject`, path/header `simple`, JSON request bodies, JSON responses, and text responses; unsupported parameter encodings, non-JSON request bodies, binary responses, and streaming operations land in `skipped` instead of producing broken tools. Operation and path servers take precedence over document servers unless `baseUrl` explicitly overrides all of them. Tool inputs flatten path, query, header, and closed object-body fields into one model-facing object while retaining their HTTP locations internally. Cross-location name collisions receive a location prefix such as `path_id` and `query_id`; composed, nullable, dictionary, conditionally-required, and non-object JSON bodies remain under `body`. Auth is never model-visible. Responses are limited to 50 MiB, and non-2xx responses become safe tool failures carrying the status and a size-capped body summary. Deferred capabilities are tracked in `src/openapi/TODO.md`.
Supported bearer, basic, header, and query authentication follows OpenAPI `security` semantics and is resolved host-side via `auth.resolve` - credential storage, OAuth flows, and token refresh never enter the compiler. Cookie authentication alternatives are discarded; an operation is skipped when it has no supported alternative. See the option docstrings in `src/openapi/types.ts` for the full semantics. Generated tools require `HttpClient.HttpClient` (from `effect/unstable/http`) in the Effect environment - provide `FetchHttpClient.layer` or a custom/test client layer at execution. The supplied client owns redirect policy; credentialed hosts should reject redirects or strip credentials when the origin changes.
## Discovery
The agent-tool instructions use a budgeted catalog. Every tool namespace is always listed with its tool count regardless of budget, and as many complete tool signatures (each with a one-line description) as fit an estimated-token budget are inlined. Selection is round-robin across namespaces for fairness: in each round (namespaces alphabetical), every namespace still holding un-inlined tools attempts to place its next-cheapest signature line against the shared budget, and a namespace whose next line does not fit drops out while the others keep going - so every namespace gets some representation before any namespace gets everything. The instructions state exactly how comprehensive the list is, both overall (`COMPLETE list` vs `PARTIAL - N of M shown`) and per namespace (`(3 tools)`, `(3 tools, 1 shown)`, `(3 tools, none shown)`).
@@ -279,7 +304,7 @@ import { toolError } from "@opencode-ai/codemode"
run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable")))
```
Only the supplied message is model-visible. The optional cause is never returned in `ExecuteResult`; hosts should perform any required internal logging before crossing this boundary.
Only the supplied message is model-visible. The optional cause is never returned in `CodeMode.Result`; hosts should perform any required internal logging before crossing this boundary.
## Authority Boundary
@@ -308,12 +333,10 @@ A program cannot gain authority through prose or generated code. It can only exe
The public contract is guided by these equivalences:
- `CodeMode.execute({ ...options, code })` is equivalent to `CodeMode.make(options).execute(code)`.
- `CodeMode.make(options).agentTool().execute({ code })` is equivalent to `CodeMode.make(options).execute(code)`.
- `CodeMode.make(options).agentTool().description` equals `CodeMode.make(options).instructions()`.
- A tool implementation is not invoked unless its input has decoded successfully.
- A tool result is not visible to the program unless its output has decoded and crossed the plain-data boundary successfully.
- Unknown host failures do not become model-visible diagnostics; `ToolError` is the explicit safe-message channel.
- Host interruption remains interruption rather than an `ExecuteFailure`.
- Host interruption remains interruption rather than a `CodeMode.Failure`.
## Non-Goals
+24 -17
View File
@@ -220,9 +220,9 @@ wave; both packages typecheck clean.
(render-only - no validation, values pass through; rendering handles `$defs`/`definitions`
- `$ref`). `output` is **optional** -> signature renders `Promise<unknown>` and the host
result is exposed as-is. Discrimination via `Schema.isSchema`. New helpers exported from
`tool.ts`: `inputTypeScript`/`outputTypeScript`/`decodeInput`/`decodeOutput`/
`tool-schema.ts`: `inputTypeScript`/`outputTypeScript`/`decodeInput`/`decodeOutput`/
`jsonSchemaToTypeScript`; `tool-runtime.ts` consumes them (no direct `Schema.*` use there
anymore). Types `JsonSchema`/`ToolSchema` exported from the index. Note: an empty
anymore). Types `Tool.JsonSchema`/`Tool.SchemaType` exported from the index. Note: an empty
`Schema.Struct({})` renders as `{ } | Array<unknown>` (effect's JSON Schema emission) -
cosmetic, fixed in Wave 4.
- **`output.*` API deleted**: `OutputItem`(+Schema), result `output` fields, the `output`
@@ -237,7 +237,7 @@ wave; both packages typecheck clean.
so failures are typed and observable). `message` is the model-safe failure message
(`ToolError`/`ToolRuntimeError` message, else "Tool execution failed"). Interrupted calls
fire no end event (timeout kills the whole execution anyway).
- **Limits collapse**: public `ExecutionLimits` = `{ timeoutMs?, maxToolCalls?,
- **Limits collapse**: public `CodeMode.ExecutionLimits` = `{ timeoutMs?, maxToolCalls?,
maxOutputBytes? }` (defaults 10_000 / 100 / 32_000). This wave kept the other knobs as
internal defaults reachable through an `@internal` `InternalExecutionLimits` type; Fix 5
later deleted that type and the internal limit system entirely.
@@ -246,7 +246,7 @@ maxOutputBytes? }` (defaults 10_000 / 100 / 32_000). This wave kept the other kn
serialized values become truncated text + ` [result truncated: N bytes exceeds the M-byte
output limit; return a smaller value]`; logs keep leading lines within the remaining budget
- `[logs truncated: showing K of N lines]`; result gains `truncated: true` (also added to
`ExecuteResultSchema`). UTF-8-safe truncation (no split code points). (The in-sandbox
`CodeMode.Result`). UTF-8-safe truncation (no split code points). (The in-sandbox
`maxDataBytes` check that used to throw first on oversized raw values died in Fix 5 -
truncation is now the only result-size mechanism.)
- **Search polish**: default limit 12 -> **10** (`defaultSearchLimit`); exact-path lookup - a
@@ -313,7 +313,7 @@ real MCP config. Package still 101 tests / 0 fail; opencode adapter suites still
packages typecheck clean.
- **Budgeted catalog** (`discoveryPlan` in `tool-runtime.ts`): the all-or-nothing
inline/search modes are gone - `DiscoveryMode` deleted, `DiscoveryOptions` is just
inline/search modes are gone - `DiscoveryMode` deleted, `CodeMode.DiscoveryOptions` is just
`{ maxInlineCatalogBytes? }` (default 16,000 UTF-8 bytes; later converted to
`maxInlineCatalogTokens`, default 4,000 estimated tokens - see Post-wave fixes). Port of
the old opencode
@@ -340,7 +340,7 @@ packages typecheck clean.
read-the-description-before-calling guidance. (The flat prose layout this wave produced
was later replaced wholesale by the markdown-section restructure - see Post-wave fixes -
which also deleted this wave's worked example.)
- **Cosmetic renderer fixes** (`renderSchema` in `tool.ts`): an object schema with no
- **Cosmetic renderer fixes** (`renderSchema` in `tool-schema.ts`): an object schema with no
properties renders `{}` (was `{ }`), and the empty `Schema.Struct({})` emission
(`anyOf: [{ type: "object" }, { type: "array" }]`, no properties/items) collapses to `{}`
(was `{ } | Array<unknown>`).
@@ -469,7 +469,7 @@ adapter needed **no changes**.
`rankTools` algorithm in `packages/opencode/src/session/code-mode.ts` at git HEAD),
replacing the word-set ranker in `tool-runtime.ts`. Searchable text per tool = path +
description + input-schema property names + their `description` strings - extracted by
the new `inputProperties` helper in `tool.ts` (Effect Schemas via
the new `inputProperties` helper in `tool-schema.ts` (Effect Schemas via
`Schema.toJsonSchemaDocument`, the same emission signature rendering uses; JSON Schemas
read `properties` directly, resolving a trivial top-level `$ref`; try/catch falls back to
path + description). Queries tokenize on camelCase boundaries + non-alphanumeric
@@ -542,7 +542,7 @@ budget; namespaces must always be present):
- `src/token.ts` added: copy of `@opencode-ai/core/util/token` (`round(chars / 4)`), so
the package stays dependency-free; keep in sync if the core heuristic changes.
- `DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000
- `CodeMode.DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000
estimated tokens ~ the old 16,000 bytes at 4 chars/token - behavior parity, not a size
reduction). `discoveryPlan` charges `estimate(catalogLine(tool))` per line; cheapest-first
- stop-on-first-miss unchanged at the time (stop-on-first-miss replaced by round-robin in
@@ -558,7 +558,7 @@ budget; namespaces must always be present):
**Fix 5 - internal limits removed** (user direction: only the three PUBLIC limits survive as
configurable knobs; the internal limit system dies):
- `ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at
- `CodeMode.ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at
the time; Fix 6 later removed the first two defaults. Same validation: safe integers,
timeoutMs >= 1, others >= 0, RangeError otherwise) is now
the ENTIRE limit surface - exactly the shape section 2's original locked spec named.
@@ -575,7 +575,7 @@ configurable knobs; the internal limit system dies):
`maxCollectionLength` (every array-length/object-field-count check - this knob was
actively harmful: an MCP tool returning 20k rows failed). The `OperationLimitExceeded`
and `AuditLimitExceeded` diagnostic kinds are gone from the `DiagnosticKind` union and
`ExecuteResultSchema` (fine - the package is unreleased).
`CodeMode.Result` (fine - the package is unreleased).
- **Fixed constants, not knobs**: `TOOL_CALL_CONCURRENCY = 8` (codemode.ts; the fork
semaphore) and `MAX_VALUE_DEPTH = 32` (tool-runtime.ts; the `copyIn` depth check - kept
only because it produces a clearer error than a native stack-overflow RangeError; still
@@ -602,7 +602,7 @@ configurable knobs; the internal limit system dies):
enumeration operation-budget, codemode maxDataBytes/maxSourceBytes/maxOperations/
maxConcurrency-RangeError assertions, and the adapter's runaway-loop-via-operation-limit
test - superseded by the package timeout regression test); rewrote the helpers that used
`InternalExecutionLimits` as a convenience to plain `ExecutionLimits`
`InternalExecutionLimits` as a convenience to plain `CodeMode.ExecutionLimits`
(promise/enumeration/stdlib run helpers). Package suite: 154 pass / 0 fail; adapter
suites: 34 + 16.
@@ -633,7 +633,7 @@ Semantics: each described input/output field carries its schema `description` as
express surface as JSDoc tags - `@deprecated`, `@default <json>` (unserializable defaults
skipped), `@format`, `@minItems`/`@maxItems`; `*/` inside text is neutralized to `* /`;
multiline descriptions become `*`-prefixed blocks with blank edges trimmed; undescribed,
untagged fields get no comment. Implementation: `renderSchema` in `tool.ts` grew a
untagged fields get no comment. Implementation: `renderSchema` in `tool-schema.ts` grew a
`RenderContext` (`{ definitions, pretty }`), a `MAX_RENDER_DEPTH = 8` recursion ceiling plus
a `$ref` `seen` guard (the renderer previously had neither - a cyclic `$defs` would have
looped; it now degrades to the ref name/`unknown`), and try/catch totality on the public
@@ -849,7 +849,7 @@ section 4 outer-truncation item the OPPOSITE way from "kill the outer one"):
that relied on the old default now asserts the oversized result reaches the shared
wrapper un-truncated. Suites: 210 + 50, tsgo clean both.
**Docs polish** (post-API-review): stale `DiscoveryOptions` JSDoc fixed (claimed default
**Docs polish** (post-API-review): stale `CodeMode.DiscoveryOptions` JSDoc fixed (claimed default
4,000 and alphabetical cheapest-first - now 2,000 and round-robin, matching Fix 8/9 reality)
and the README's incorrect "`effect` as a peer dependency" line corrected (`effect` is a
regular dependency; hosts depend on it themselves because the API surface is Effect-typed).
@@ -949,16 +949,16 @@ child calls" gap):
**Signature rendering + compound-assignment parity fixes** (externally reported, both
verified real with failing tests before fixing):
- **Non-identifier property names in rendered signatures** (`src/tool.ts`): `renderSchema`
- **Non-identifier property names in rendered signatures** (`src/tool-schema.ts`): `renderSchema`
emitted raw property names, so schema properties like `foo-bar`/`@type`/`x.y`/`123`
rendered invalid TypeScript (`{ foo-bar?: string }`). Fixed with a `renderKey` helper -
bare identifiers stay bare, everything else is `JSON.stringify`-quoted - applied in the
single `field` closure both the compact and pretty renderings share. The
`identifierSegment` regex now lives in `tool.ts` (exported) and `tool-runtime.ts`'s
`identifierSegment` regex now lives in `tool-schema.ts` (internal) and `tool-runtime.ts`'s
bracket-notation `toolExpression` imports it: one source of truth for "is this a bare
identifier" across object keys and tool paths. Tests: `signature.test.ts` +4 (compact,
pretty with JSDoc on a quoted key, JSON Schema input+output, Effect Schema struct).
- **Numeric schema unions keep their real alternatives** (`src/tool.ts`): the old
- **Numeric schema unions keep their real alternatives** (`src/tool-schema.ts`): the old
`anyOf`/`oneOf` renderer collapsed any union containing `{ type: "number" }` to just
`number`, dropping real JSON Schema alternatives (`string | number`, `number | null`,
etc.). The collapse is now restricted to Effect's number-schema artifact
@@ -1132,6 +1132,12 @@ Post-MVP (logged, not blocking an experimental flag):
- [ ] Reviewer observation worth keeping: MCP server instructions (`sys.mcp`,
`session/system.ts:110-126`) still inject prose referencing server-native tool
names that are no longer directly callable under code mode.
- [ ] Tool-tree path segments named `__proto__`, `constructor`, or `prototype` are included
in discovery but rejected by `ToolRuntime` resolution even when supplied as safe own
properties on null-prototype host records. Hosts should preserve registered names rather
than invent incompatible aliases. CodeMode should own a consistent policy: safely admit
these names as own tool-tree members, reject them before catalog generation with a clear
diagnostic, or define one canonical escaping contract.
### Backlog / loose ends (non-blocking, any order)
@@ -1203,7 +1209,8 @@ Post-MVP (logged, not blocking an experimental flag):
the workspace is the implementation source of truth for v4 behavior questions.
- File map (this package): `src/codemode.ts` - types/limits/parser/Interpreter/execute/make;
`src/tool-runtime.ts` - tool tree, `copyIn`/`copyOut`, search/discovery, invoke path;
`src/tool.ts` - `Tool.make` + JSON-Schema->TS rendering; `src/values.ts` - sandbox value
`src/tool.ts` - public `Tool` definitions; `src/tool-schema.ts` - schema rendering and decoding;
`src/values.ts` - sandbox value
types; `src/tool-error.ts` - `ToolError`; tests in `test/{codemode,parity,stdlib}.test.ts`.
- OpenCode file map (integration points): `src/tool/code-mode.ts` (the adapter, now a
registry tool service - `CodeModeTool` + `catalogInstructions`; formerly
+54 -106
View File
@@ -19,8 +19,7 @@ import { ToolError } from "./tool-error.js"
import { isSandboxValue, SandboxDate, SandboxMap, SandboxPromise, SandboxRegExp, SandboxSet } from "./values.js"
/** A tool call admitted during an execution. */
export type { ToolCall, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
export { ToolError, toolError } from "./tool-error.js"
export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
/** Resource budgets enforced independently during each CodeMode program execution. */
export type ExecutionLimits = {
@@ -74,50 +73,20 @@ export type ExecuteOptions<Tools extends Record<string, unknown> = {}> = {
onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect<void, never, Services<Tools>>
}
/** A normalized program diagnostic safe to return across an agent tool boundary. */
export type Diagnostic = {
readonly kind: DiagnosticKind
readonly message: string
readonly location?: { readonly line: number; readonly column: number }
readonly suggestions?: ReadonlyArray<string>
}
/** A JSON value that can cross the confined interpreter boundary. */
export type DataValue = Schema.Json
/** Successful execution after the result has crossed the plain-data boundary. */
export type ExecuteSuccess = {
readonly ok: true
readonly value: DataValue
readonly logs?: ReadonlyArray<string>
/** Present when the value or logs were truncated to fit `maxOutputBytes`. */
readonly truncated?: boolean
readonly toolCalls: ReadonlyArray<ToolCall>
}
/** Failed execution with calls admitted before the diagnostic was produced. */
export type ExecuteFailure = {
readonly ok: false
readonly error: Diagnostic
readonly logs?: ReadonlyArray<string>
/** Present when the logs were truncated to fit `maxOutputBytes`. */
readonly truncated?: boolean
readonly toolCalls: ReadonlyArray<ToolCall>
}
/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
export type ExecuteResult = ExecuteSuccess | ExecuteFailure
/** Reusable CodeMode configuration shared by `execute` and `agentTool`. */
export type CodeModeOptions<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & {
/** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
export type Options<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & {
/** Progressive-disclosure configuration for the agent-facing tool catalog. */
readonly discovery?: DiscoveryOptions
}
/** Input schema for the single agent-facing tool produced by `runtime.agentTool()`. */
export const ExecuteInputSchema = Schema.Struct({ code: Schema.String })
/** Schema for a host tool input containing CodeMode source. */
export const Input = Schema.Struct({ code: Schema.String })
export type Input = typeof Input.Type
const DiagnosticKindSchema = Schema.Literals([
export const DiagnosticKind = Schema.Literals([
"ParseError",
"UnsupportedSyntax",
"UnknownTool",
@@ -129,49 +98,52 @@ const DiagnosticKindSchema = Schema.Literals([
"ToolFailure",
"ExecutionFailure",
])
/** Stable categories produced by program, schema, tool, and limit failures. */
export type DiagnosticKind = typeof DiagnosticKind.Type
/** Structured success or diagnostic result schema returned by CodeMode execution. */
export const ExecuteResultSchema = Schema.Union([
Schema.Struct({
ok: Schema.Literal(true),
value: Schema.Json,
logs: Schema.optionalKey(Schema.Array(Schema.String)),
truncated: Schema.optionalKey(Schema.Boolean),
toolCalls: Schema.Array(Schema.Struct({ name: Schema.String })),
}),
Schema.Struct({
ok: Schema.Literal(false),
error: Schema.Struct({
kind: DiagnosticKindSchema,
message: Schema.String,
location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })),
suggestions: Schema.optionalKey(Schema.Array(Schema.String)),
}),
logs: Schema.optionalKey(Schema.Array(Schema.String)),
truncated: Schema.optionalKey(Schema.Boolean),
toolCalls: Schema.Array(Schema.Struct({ name: Schema.String })),
}),
])
export const Diagnostic = Schema.Struct({
kind: DiagnosticKind,
message: Schema.String,
location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })),
suggestions: Schema.optionalKey(Schema.Array(Schema.String)),
})
/** A normalized program diagnostic safe to return across an agent tool boundary. */
export type Diagnostic = typeof Diagnostic.Type
/** Agent-facing projection of a configured CodeMode runtime. */
export type AgentToolDefinition<R = never> = {
readonly name: "code"
readonly description: string
readonly input: typeof ExecuteInputSchema
readonly output: typeof ExecuteResultSchema
readonly execute: (input: { readonly code: string }) => Effect.Effect<ExecuteResult, never, R>
}
const ToolCallSchema = Schema.Struct({ name: Schema.String })
export const Success = Schema.Struct({
ok: Schema.Literal(true),
value: Schema.Json,
logs: Schema.optionalKey(Schema.Array(Schema.String)),
truncated: Schema.optionalKey(Schema.Boolean),
toolCalls: Schema.Array(ToolCallSchema),
})
/** Successful execution after the result has crossed the plain-data boundary. */
export type Success = typeof Success.Type
export const Failure = Schema.Struct({
ok: Schema.Literal(false),
error: Diagnostic,
logs: Schema.optionalKey(Schema.Array(Schema.String)),
truncated: Schema.optionalKey(Schema.Boolean),
toolCalls: Schema.Array(ToolCallSchema),
})
/** Failed execution with calls admitted before the diagnostic was produced. */
export type Failure = typeof Failure.Type
/** Schema for the structured success or diagnostic returned by CodeMode execution. */
export const Result = Schema.Union([Success, Failure])
/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
export type Result = typeof Result.Type
/** Reusable confined runtime over one explicit tool tree. */
export type CodeModeRuntime<R = never> = {
export type Runtime<R = never> = {
/** Lists schema-described tool paths provided by the host. */
readonly catalog: () => ReadonlyArray<ToolDescription>
/** Builds model-facing syntax guidance and visible tool signatures. */
readonly instructions: () => string
/** Projects the configured runtime as one agent-facing `code` tool. */
readonly agentTool: () => AgentToolDefinition<R>
/** Executes a program using this runtime's configured host tools. */
readonly execute: (code: string) => Effect.Effect<ExecuteResult, never, R>
readonly execute: (code: string) => Effect.Effect<Result, never, R>
}
type SourcePosition = {
@@ -286,19 +258,6 @@ const errorBrandName = (value: unknown): string | undefined =>
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
: undefined
/** Stable categories produced by program, schema, tool, and limit failures. */
export type DiagnosticKind =
| "ParseError"
| "UnsupportedSyntax"
| "UnknownTool"
| "InvalidToolInput"
| "InvalidToolOutput"
| "InvalidDataValue"
| "ToolCallLimitExceeded"
| "TimeoutExceeded"
| "ToolFailure"
| "ExecutionFailure"
const arrayMethods = new Set([
"map",
"filter",
@@ -3954,7 +3913,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
options: ExecuteOptions<Tools>,
limits: ResolvedExecutionLimits,
searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"],
): Effect.Effect<ExecuteResult, never, Services<Tools>> => {
): Effect.Effect<Result, never, Services<Tools>> => {
const hooks = {
...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }),
...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }),
@@ -3986,7 +3945,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
value: result,
...logged(),
toolCalls: tools.calls,
} satisfies ExecuteResult
} satisfies Result
}).pipe((program) => {
const timeoutMs = limits.timeoutMs
if (timeoutMs === undefined) return program
@@ -3999,7 +3958,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` },
...logged(),
toolCalls: tools.calls,
} satisfies ExecuteResult),
} satisfies Result),
}),
)
})
@@ -4013,7 +3972,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
error: normalizeError(Cause.squash(cause)),
...logged(),
toolCalls: tools.calls,
} satisfies ExecuteResult),
} satisfies Result),
),
Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))),
)
@@ -4037,7 +3996,7 @@ const utf8Truncate = (value: string, maxBytes: number): string => {
* fails the execution; `truncated: true` marks affected results. Only runs when the host set
* `maxOutputBytes` - with the limit absent, output passes through unbounded.
*/
const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResult => {
const boundOutput = (result: Result, maxOutputBytes: number): Result => {
let truncated = false
let value: DataValue = null
@@ -4079,7 +4038,7 @@ const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResu
export const execute = <const Tools extends Record<string, unknown>>(
options: ExecuteOptions<Tools>,
): Effect.Effect<ExecuteResult, never, Services<Tools>> => {
): Effect.Effect<Result, never, Services<Tools>> => {
const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
ToolRuntime.assertValidTools(tools)
return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools))
@@ -4088,18 +4047,17 @@ export const execute = <const Tools extends Record<string, unknown>>(
/**
* Creates an Effect-native runtime over explicit, schema-described tools.
*
* Use `execute` for host-driven execution or `agentTool` to expose one confined code tool to an
* agent framework. Tool requirements remain in the returned Effect environment.
* Use `execute` for host-driven execution. Tool requirements remain in the returned Effect environment.
*
* @example
* ```ts
* const runtime = CodeMode.make({ tools: { orders: { lookup } } })
* const code = runtime.agentTool()
* const result = runtime.execute("return await tools.orders.lookup({ id: 'order_42' })")
* ```
*/
export const make = <const Tools extends Record<string, unknown> = {}>(
options: CodeModeOptions<Tools> = {} as CodeModeOptions<Tools>,
): CodeModeRuntime<Services<Tools>> => {
options: Options<Tools> = {} as Options<Tools>,
): Runtime<Services<Tools>> => {
const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
ToolRuntime.assertValidTools(tools)
const limits = resolveExecutionLimits(options.limits)
@@ -4111,16 +4069,6 @@ export const make = <const Tools extends Record<string, unknown> = {}>(
return {
catalog: () => catalog,
instructions: () => instructions,
agentTool: () => ({
name: "code",
description: instructions,
input: ExecuteInputSchema,
output: ExecuteResultSchema,
execute: ({ code }) => executeProgram(code),
}),
execute: executeProgram,
}
}
/** Constructors for one-shot and reusable CodeMode execution. */
export const CodeMode = { make, execute }
+4 -21
View File
@@ -1,21 +1,4 @@
export { ToolError, CodeMode, ExecuteInputSchema, ExecuteResultSchema, toolError } from "./codemode.js"
export { Tool } from "./tool.js"
export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js"
export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js"
export type {
AgentToolDefinition,
CodeModeOptions,
CodeModeRuntime,
DataValue,
Diagnostic,
DiagnosticKind,
DiscoveryOptions,
ExecuteFailure,
ExecuteOptions,
ExecuteResult,
ExecuteSuccess,
ExecutionLimits,
ToolCall,
ToolCallStarted,
ToolDescription,
} from "./codemode.js"
export * as CodeMode from "./codemode.js"
export * as Tool from "./tool.js"
export * as OpenAPI from "./openapi/index.js"
export { ToolError, toolError } from "./tool-error.js"
+19
View File
@@ -0,0 +1,19 @@
# OpenAPI Follow-ups
The initial adapter intentionally skips operations it cannot execute correctly. Future work may add:
- Cookie parameters, authentication, and cookie-header merging.
- Matrix, label, space-delimited, pipe-delimited, `allowReserved`, and parameter `content` serialization.
- External references and complete nested `$defs` support.
- Relative or templated server URLs and server variables.
- Base URLs containing query strings or fragments.
- Runtime response-schema validation and full content negotiation.
- Binary response values and explicit byte-oriented return types.
- Request/response projection for `readOnly` and `writeOnly` properties.
- SSE, WebSocket, and other streaming transports.
- Recovery of responses rejected by a status-filtering `HttpClient`.
- Configurable request and response size limits.
- Adapter-enforced redirect policy independent of the supplied `HttpClient`.
- Strict UTF-8 and empty-body validation for JSON responses.
- Compile-time rejection of parameter schemas with nested values unsupported by their serialization style; runtime rejects them before auth resolution.
- Complete malformed-security-scheme validation and broader auth-combination coverage.
+130
View File
@@ -0,0 +1,130 @@
import { HttpClient } from "effect/unstable/http"
import { make, type Definition } from "../tool.js"
import { invoke } from "./runtime.js"
import {
componentDefinitions,
inputSchema,
isRecord,
methods,
nonEmptyString,
operationInput,
operationOutput,
operationPath,
operationSecurityRequirements,
securityRequirements,
securitySchemes,
specServerUrl,
validateBaseUrl,
} from "./spec.js"
import type { Operation, Options, Result, Skipped, Tools } from "./types.js"
export type {
AuthResolver,
Credential,
Document,
Operation,
Options,
Result,
SecurityScheme,
Skipped,
Tools,
} from "./types.js"
/**
* Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per
* operation. Auth is resolved host-side via `auth.resolve` and never
* model-visible. Tools require `HttpClient.HttpClient`; unrepresentable
* operations land in `skipped`.
*/
export const fromSpec = (options: Options): Result => {
const document = options.spec
const schemes = securitySchemes(document)
const defaultSecurity = securityRequirements(document.security)
const definitions = componentDefinitions(document)
const paths = isRecord(document.paths) ? document.paths : {}
const used = new Set<string>()
const namespaces = new Set<string>()
const skipped: Array<Skipped> = []
const tools = Object.create(null) as Tools
for (const [path, pathValue] of Object.entries(paths)) {
if (!isRecord(pathValue)) continue
for (const [method, operationValue] of Object.entries(pathValue)) {
if (!methods.has(method) || !isRecord(operationValue)) continue
const segments = operationPath(method, path, operationValue, used, namespaces)
const operation: Operation = {
operationId: nonEmptyString(operationValue.operationId),
method: method.toUpperCase(),
path,
summary: nonEmptyString(operationValue.summary),
description: nonEmptyString(operationValue.description),
}
const output = operationOutput(document, operationValue, definitions)
if (!output.ok) {
skipped.push({ method: operation.method, path, reason: output.reason })
continue
}
const resolvedBaseUrl = (() => {
if (options.baseUrl !== undefined) return validateBaseUrl(options.baseUrl)
if (operationValue.servers !== undefined) return specServerUrl(operationValue)
if (pathValue.servers !== undefined) return specServerUrl(pathValue)
return specServerUrl(document)
})()
if (!resolvedBaseUrl.ok) {
skipped.push({ method: operation.method, path, reason: resolvedBaseUrl.reason })
continue
}
const parsedInput = operationInput(document, pathValue, operationValue)
if (!parsedInput.ok) {
skipped.push({ method: operation.method, path, reason: parsedInput.reason })
continue
}
const input = parsedInput.value
const security = operationSecurityRequirements(operationValue.security, defaultSecurity, schemes)
if (!security.ok) {
skipped.push({ method: operation.method, path, reason: security.reason })
continue
}
const plan = {
operation,
url: `${resolvedBaseUrl.value.replace(/\/+$/, "")}${path}`,
fields: input.fields,
body: input.body,
security: security.value,
schemes,
auth: options.auth,
headers: options.headers ?? {},
}
used.add(segments.join("."))
for (const index of segments.slice(0, -1).keys()) namespaces.add(segments.slice(0, index + 1).join("."))
setTool(
tools,
segments,
make({
description: operation.description ?? operation.summary ?? `${operation.method} ${path}`,
input: inputSchema(input.fields, definitions),
output: output.value,
run: (input) => invoke(plan, input),
}),
)
}
}
return { tools, skipped }
}
const setTool = (tools: Tools, path: ReadonlyArray<string>, definition: Definition<HttpClient.HttpClient>): void => {
const [head, ...rest] = path
if (head === undefined) return
if (rest.length === 0) {
tools[head] = definition
return
}
const child = tools[head]
if (child === undefined || !isRecord(child) || child._tag === "CodeModeTool") {
tools[head] = Object.create(null) as Tools
}
setTool(tools[head] as Tools, rest, definition)
}
+324
View File
@@ -0,0 +1,324 @@
import { Effect, Option, Schema, Stream } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse, type HttpMethod } from "effect/unstable/http"
import { ToolError, toolError } from "../tool-error.js"
import { isRecord, own } from "./spec.js"
import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js"
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const maxErrorBodyChars = 1_024
const maxResponseBodyBytes = 50 * 1024 * 1024
export const invoke = (plan: Plan, input: unknown): Effect.Effect<unknown, unknown, HttpClient.HttpClient> =>
Effect.gen(function* () {
const value = isRecord(input) ? input : {}
let request = yield* buildRequest(plan, value)
const auth = yield* resolveAuth(plan)
for (const [name, item] of Object.entries(auth.query)) {
request = HttpClientRequest.setUrlParam(request, name, item)
}
request = HttpClientRequest.setHeaders(request, auth.headers)
const client = yield* HttpClient.HttpClient
const response = yield* client
.execute(request)
.pipe(
Effect.catch((cause) =>
Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} failed: transport error`, cause)),
),
)
const text = yield* readResponseBody(response, plan)
const mediaType = response.headers["content-type"]?.split(";")[0]?.trim().toLowerCase()
const json = mediaType === "application/json" || mediaType?.endsWith("+json") === true
const decoded = text === "" ? Option.some(null) : json ? decodeJson(text) : Option.none()
const parsed = json ? Option.getOrElse(decoded, () => text) : text === "" ? null : text
if (response.status < 200 || response.status >= 300) {
const rendered = typeof parsed === "string" ? parsed : (JSON.stringify(parsed) ?? "")
const summary =
rendered === "" || rendered === "null"
? "no response body"
: rendered.length > maxErrorBodyChars
? `${rendered.slice(0, maxErrorBodyChars)}...`
: rendered
return yield* Effect.fail(
toolError(`${plan.operation.method} ${plan.operation.path} failed with HTTP ${response.status}: ${summary}`),
)
}
if (json && Option.isNone(decoded)) {
return yield* Effect.fail(
toolError(`${plan.operation.method} ${plan.operation.path} returned malformed JSON.`),
)
}
return parsed
})
const buildRequest = (
plan: Plan,
input: Readonly<Record<string, unknown>>,
): Effect.Effect<HttpClientRequest.HttpClientRequest, ToolError> =>
Effect.gen(function* () {
// Validate every model-controlled value before auth resolution, which may refresh tokens.
const url = buildUrl(plan, input)
if (url instanceof ToolError) return yield* Effect.fail(url)
const missing = plan.fields.find(
(field) => field.required && field.location !== "path" && own(input, field.inputName) === undefined,
)
if (missing !== undefined) {
const label = missing.location === "body" ? "body field" : `${missing.location} parameter`
return yield* Effect.fail(toolError(`Missing required ${label} '${missing.inputName}'.`))
}
let request = HttpClientRequest.make(plan.operation.method as HttpMethod.HttpMethod)(url)
for (const field of plan.fields) {
if (field.location !== "query") continue
const item = own(input, field.inputName)
if (item === undefined) continue
const serialized = serializeQuery(request, field, item)
if (serialized instanceof ToolError) return yield* Effect.fail(serialized)
request = serialized
}
// Host headers first, then declared header parameters.
request = HttpClientRequest.setHeaders(request, plan.headers)
for (const field of plan.fields) {
if (field.location !== "header") continue
const item = own(input, field.inputName)
if (item === undefined) continue
const serialized = serializeSimple(field, item, String)
if (serialized instanceof ToolError) return yield* Effect.fail(serialized)
request = HttpClientRequest.setHeader(request, field.name, serialized)
}
const setBody = (value: unknown, mediaType: string) =>
HttpClientRequest.bodyJson(request, value).pipe(
Effect.map((next) => HttpClientRequest.setHeader(next, "content-type", mediaType)),
Effect.mapError((cause) =>
toolError(`Invalid JSON body for ${plan.operation.method} ${plan.operation.path}.`, cause),
),
)
if (plan.body?.mode === "value") {
const field = plan.fields.find((field) => field.location === "body")
const body = field === undefined ? undefined : own(input, field.inputName)
if (body !== undefined) request = yield* setBody(body, plan.body.mediaType)
}
if (plan.body?.mode === "object") {
const entries = plan.fields.flatMap((field) => {
if (field.location !== "body") return []
const item = own(input, field.inputName)
return item === undefined ? [] : [[field.name, item] as const]
})
if (plan.body.required || entries.length > 0) {
request = yield* setBody(Object.fromEntries(entries), plan.body.mediaType)
}
}
return request
})
const resolveAuth = (plan: Plan): Effect.Effect<AppliedAuth, unknown> =>
Effect.gen(function* () {
const none: AppliedAuth = { headers: {}, query: {} }
if (plan.security.length === 0) return none
const unavailable: Array<string> = []
alternatives: for (const requirement of plan.security) {
const names = Object.keys(requirement)
if (names.length === 0) return none
const credentials: Array<readonly [string, SecurityScheme, Credential]> = []
for (const name of names) {
const scheme = own(plan.schemes, name)
if (scheme === undefined || plan.auth === undefined) {
unavailable.push(name)
continue alternatives
}
const credential = yield* plan.auth.resolve({
name,
definition: scheme,
scopes: requirement[name] ?? [],
operation: plan.operation,
})
if (credential === undefined) {
unavailable.push(name)
continue alternatives
}
credentials.push([name, scheme, credential])
}
const applied = applyCredentials(credentials)
return applied instanceof ToolError ? yield* Effect.fail(applied) : applied
}
return yield* Effect.fail(
toolError(
`${plan.operation.method} ${plan.operation.path} requires authentication; no credential available for: ${[...new Set(unavailable)].join(", ")}.`,
),
)
})
const applyCredentials = (
credentials: ReadonlyArray<readonly [string, SecurityScheme, Credential]>,
): AppliedAuth | ToolError => {
const headers = new Map<string, string>()
const query = new Map<string, string>()
const add = (carrier: "header" | "query", name: string, value: string): ToolError | undefined => {
const target = carrier === "header" ? headers : query
if (target.has(name)) return toolError(`Authentication resolves multiple credentials for ${carrier} '${name}'.`)
target.set(name, value)
}
for (const [name, definition, credential] of credentials) {
if (credential.type === "bearer") {
const duplicate = add("header", "authorization", `Bearer ${credential.token}`)
if (duplicate !== undefined) return duplicate
continue
}
if (credential.type === "basic") {
// Buffer instead of btoa: btoa throws on non-Latin-1 credentials.
const duplicate = add(
"header",
"authorization",
`Basic ${Buffer.from(`${credential.username}:${credential.password}`, "utf8").toString("base64")}`,
)
if (duplicate !== undefined) return duplicate
continue
}
if (credential.type === "header") {
const duplicate = add("header", credential.name.toLowerCase(), credential.value)
if (duplicate !== undefined) return duplicate
continue
}
// apiKey: the carrier comes from the scheme declaration.
if (definition.type !== "apiKey") {
return toolError(
`Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`,
)
}
if (definition.in === "cookie") return toolError(`Cookie authentication '${name}' is not supported.`)
const parameter = definition.in === "header" ? definition.name.toLowerCase() : definition.name
const duplicate = add(definition.in, parameter, credential.value)
if (duplicate !== undefined) return duplicate
}
return { headers: Object.fromEntries(headers), query: Object.fromEntries(query) }
}
const buildUrl = (plan: Plan, input: Readonly<Record<string, unknown>>): string | ToolError => {
let url = plan.url
for (const field of plan.fields) {
if (field.location !== "path") continue
const item = own(input, field.inputName)
if (item === undefined) {
return toolError(`Missing required path parameter '${field.inputName}'.`)
}
const fieldValue = serializeSimple(field, item, (value) =>
encodeURIComponent(value).replace(/[!'()*]/g, (character) =>
`%${character.charCodeAt(0).toString(16).toUpperCase()}`,
),
)
if (fieldValue instanceof ToolError) return fieldValue
// '.'/'..' survive encoding and URL normalization collapses them, letting a
// model-supplied value retarget the request to a different endpoint.
if (fieldValue === "" || fieldValue === "." || fieldValue === "..") {
return toolError(`Invalid path parameter '${field.inputName}'.`)
}
url = url.replaceAll(`{${field.name}}`, fieldValue)
}
const unresolved = url.match(/\{[^{}]+\}/)
if (unresolved !== null) return toolError(`Unresolved path parameter ${unresolved[0]}.`)
return url
}
const serializeSimple = (
field: Plan["fields"][number],
value: unknown,
encode: (value: string) => string,
): string | ToolError => {
const scalar = (item: unknown): string | ToolError =>
item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean"
? toolError(`Parameter '${field.inputName}' contains an unsupported nested value.`)
: encode(String(item))
if (Array.isArray(value)) {
const items = value.map(scalar)
const invalid = items.find((item): item is ToolError => item instanceof ToolError)
return invalid ?? items.join(",")
}
if (!isRecord(value)) return scalar(value)
const entries = Object.entries(value).flatMap<string | ToolError>(([name, item]) => {
const rendered = scalar(item)
if (rendered instanceof ToolError) return [rendered]
return field.explode ? [`${encode(name)}=${rendered}`] : [encode(name), rendered]
})
const invalid = entries.find((item): item is ToolError => item instanceof ToolError)
return invalid ?? entries.join(",")
}
const serializeQuery = (
request: HttpClientRequest.HttpClientRequest,
field: Plan["fields"][number],
value: unknown,
): HttpClientRequest.HttpClientRequest | ToolError => {
if (field.style === "deepObject") {
if (!isRecord(value)) return toolError(`Deep-object parameter '${field.inputName}' must be an object.`)
return Object.entries(value).reduce<HttpClientRequest.HttpClientRequest | ToolError>((current, [name, item]) => {
if (current instanceof ToolError) return current
if (item === undefined || (item !== null && typeof item === "object")) {
return toolError(`Deep-object parameter '${field.inputName}' contains an unsupported nested value.`)
}
return HttpClientRequest.appendUrlParam(current, `${field.name}[${name}]`, String(item))
}, request)
}
if (Array.isArray(value)) {
const rendered = serializeSimple(field, value, String)
if (rendered instanceof ToolError) return rendered
if (!field.explode) return HttpClientRequest.appendUrlParam(request, field.name, rendered)
if (value.some((item) => item === undefined || (item !== null && typeof item === "object"))) {
return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`)
}
return value.reduce(
(current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)),
request,
)
}
if (isRecord(value) && field.explode) {
return Object.entries(value).reduce<HttpClientRequest.HttpClientRequest | ToolError>((current, [name, item]) => {
if (current instanceof ToolError) return current
if (item === undefined || (item !== null && typeof item === "object")) {
return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`)
}
return HttpClientRequest.appendUrlParam(current, name, String(item))
}, request)
}
const rendered = serializeSimple(field, value, String)
return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered)
}
const readResponseBody = (response: HttpClientResponse.HttpClientResponse, plan: Plan): Effect.Effect<string, ToolError> =>
Effect.gen(function* () {
const contentLength = response.headers["content-length"]
const parsedSize = contentLength === undefined ? undefined : Number.parseInt(contentLength, 10)
const declaredSize = parsedSize !== undefined && Number.isSafeInteger(parsedSize) && parsedSize >= 0 ? parsedSize : undefined
if (declaredSize !== undefined && declaredSize > maxResponseBodyBytes) {
return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`))
}
let body = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, declaredSize ?? 64 * 1024))
let size = 0
yield* Stream.runForEach(response.stream, (chunk) => {
if (size + chunk.byteLength > maxResponseBodyBytes) {
return Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`))
}
if (size + chunk.byteLength > body.byteLength) {
const grown = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, Math.max(size + chunk.byteLength, body.byteLength * 2)))
body.copy(grown, 0, 0, size)
body = grown
}
body.set(chunk, size)
size += chunk.byteLength
return Effect.void
}).pipe(
Effect.catch((cause) => {
if (cause instanceof ToolError) return Effect.fail(cause)
if (cause.reason._tag === "EmptyBodyError") return Effect.void
return Effect.fail(
toolError(`${plan.operation.method} ${plan.operation.path} failed while reading the response body.`, cause),
)
}),
)
return new TextDecoder().decode(body.subarray(0, size))
})
+507
View File
@@ -0,0 +1,507 @@
import { fromSchemaOpenApi3_0, fromSchemaOpenApi3_1 } from "effect/JsonSchema"
import type { JsonSchema } from "../tool.js"
import { isBlockedMember } from "../tool-runtime.js"
import type {
Body,
Document,
InputField,
OperationInput,
Parsed,
SecurityRequirement,
SecurityScheme,
} from "./types.js"
export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"])
const parameterLocations = ["path", "query", "header"] as const
const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"])
export const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
const asArray = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value) ? value : [])
export const nonEmptyString = (value: unknown): string | undefined =>
typeof value === "string" && value !== "" ? value : undefined
// Guards record lookups keyed by spec- or model-controlled names against
// prototype-inherited values (e.g. a parameter named `toString`).
export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
Object.hasOwn(record, key) ? record[key] : undefined
export const resolve = (document: Document, value: unknown): unknown => {
const next = (current: unknown, seen: ReadonlySet<string>): unknown => {
if (!isRecord(current)) return current
const ref = nonEmptyString(current.$ref)
if (ref === undefined || !ref.startsWith("#/") || seen.has(ref)) return current
const target = ref
.slice(2)
.split("/")
.map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
.reduce<unknown>((item, segment) => (isRecord(item) ? own(item, segment) : undefined), document)
return target === undefined ? current : next(target, new Set([...seen, ref]))
}
return next(value, new Set())
}
const projectSchema = (document: Document, value: unknown): JsonSchema => {
if (!isRecord(value)) return {}
const normalized = nonEmptyString(document.openapi)?.startsWith("3.0")
? fromSchemaOpenApi3_0(value)
: fromSchemaOpenApi3_1(value)
return Object.keys(normalized.definitions).length === 0
? normalized.schema
: { ...normalized.schema, $defs: normalized.definitions }
}
export const componentDefinitions = (document: Document): Readonly<Record<string, JsonSchema>> => {
const components = isRecord(document.components) ? document.components : {}
const schemas = isRecord(components.schemas) ? components.schemas : {}
return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(document, value)]))
}
const withDefinitions = (schema: JsonSchema, definitions: Readonly<Record<string, JsonSchema>>): JsonSchema => {
if (Object.keys(definitions).length === 0) return schema
const local = isRecord(schema.$defs) ? schema.$defs : {}
return { ...schema, $defs: { ...definitions, ...local } }
}
const isJsonMediaType = (mediaType: string): boolean => {
const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
return normalized === "application/json" || normalized.endsWith("+json")
}
const isBinaryMediaType = (document: Document, mediaType: string, value: unknown): boolean => {
const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
if (!isJsonMediaType(normalized) && !normalized.startsWith("text/")) return true
if (!isRecord(value)) return false
const schema = resolve(document, value.schema)
return isRecord(schema) && schema.format === "binary"
}
const jsonContent = (content: Record<string, unknown>): { readonly mediaType: string; readonly schema: unknown } | undefined => {
const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType))
return entry !== undefined && isRecord(entry[1]) ? { mediaType: entry[0], schema: entry[1].schema } : undefined
}
const isFlattenableObjectBody = (
schema: unknown,
requestRequired: boolean,
): schema is Record<string, unknown> & { readonly properties: Record<string, unknown> } =>
isRecord(schema) &&
requestRequired &&
schema.type === "object" &&
isRecord(schema.properties) &&
schema.additionalProperties === false &&
schema.nullable !== true &&
schema.allOf === undefined &&
schema.anyOf === undefined &&
schema.oneOf === undefined
type PlannedField = Omit<InputField, "inputName">
const operationParameters = (
document: Document,
pathItem: Record<string, unknown>,
operation: Record<string, unknown>,
): Parsed<ReadonlyArray<PlannedField>> => {
// Operation-level parameters override path-level ones sharing (location, name).
const declared = new Map<
string,
{ readonly name: string; readonly location: string; readonly parameter: Record<string, unknown> }
>()
for (const raw of [...asArray(pathItem.parameters), ...asArray(operation.parameters)]) {
const resolved = resolve(document, raw)
if (!isRecord(resolved)) return { ok: false, reason: "parameter declaration is invalid or unresolved" }
const name = nonEmptyString(resolved.name)
const location = nonEmptyString(resolved.in)
if (name === undefined || location === undefined)
return { ok: false, reason: "parameter declaration is missing name or location" }
declared.set(`${location}:${name}`, { name, location, parameter: resolved })
}
const unordered: Array<PlannedField> = []
for (const item of declared.values()) {
const name = item.name
const location = item.location
const resolved = item.parameter
if (location === "cookie") return { ok: false, reason: `cookie parameter '${name}' is not supported` }
if (location !== "path" && location !== "query" && location !== "header") {
return { ok: false, reason: `parameter '${name}' uses unsupported location '${location}'` }
}
if (location === "header" && ignoredHeaderParameters.has(name.toLowerCase())) continue
if (resolved.schema === undefined && resolved.content === undefined) {
return { ok: false, reason: `parameter '${name}' declares neither schema nor content` }
}
if (resolved.content !== undefined)
return { ok: false, reason: `parameter '${name}' uses unsupported content encoding` }
if (resolved.style !== undefined && nonEmptyString(resolved.style) === undefined) {
return { ok: false, reason: `parameter '${name}' has an invalid style` }
}
if (resolved.explode !== undefined && typeof resolved.explode !== "boolean") {
return { ok: false, reason: `parameter '${name}' has an invalid explode value` }
}
if (resolved.allowReserved !== undefined && typeof resolved.allowReserved !== "boolean") {
return { ok: false, reason: `parameter '${name}' has an invalid allowReserved value` }
}
if (resolved.allowReserved === true)
return { ok: false, reason: `parameter '${name}' uses unsupported allowReserved encoding` }
const declaredStyle = nonEmptyString(resolved.style) ?? (location === "query" ? "form" : "simple")
if (location === "query" && declaredStyle !== "form" && declaredStyle !== "deepObject") {
return { ok: false, reason: `query parameter '${name}' uses unsupported style '${declaredStyle}'` }
}
if (location !== "query" && declaredStyle !== "simple") {
return { ok: false, reason: `${location} parameter '${name}' uses unsupported style '${declaredStyle}'` }
}
const style = declaredStyle === "deepObject" ? "deepObject" : declaredStyle === "form" ? "form" : "simple"
const explode = typeof resolved.explode === "boolean" ? resolved.explode : style === "form"
if (style === "deepObject" && !explode) {
return { ok: false, reason: `query parameter '${name}' uses deepObject with explode=false` }
}
const base = projectSchema(document, resolved.schema)
const description = nonEmptyString(resolved.description)
unordered.push({
name,
location,
required: resolved.required === true || location === "path",
style,
explode,
schema: {
...base,
...(base.description === undefined && description !== undefined ? { description } : {}),
},
})
}
return {
ok: true,
value: parameterLocations.flatMap((location) => unordered.filter((field) => field.location === location)),
}
}
const operationBody = (
document: Document,
operation: Record<string, unknown>,
): Parsed<{ readonly fields: ReadonlyArray<PlannedField>; readonly body: Body | undefined }> => {
const resolved = resolve(document, operation.requestBody)
if (!isRecord(resolved)) return { ok: true, value: { fields: [], body: undefined } }
const content = isRecord(resolved.content) ? resolved.content : {}
const selected = jsonContent(content)
if (selected === undefined) {
return {
ok: false,
reason: `request body has no JSON content (declared: ${Object.keys(content).join(", ") || "none"})`,
}
}
const schema = resolve(document, selected.schema)
const required = resolved.required === true
if (!isFlattenableObjectBody(schema, required)) {
return {
ok: true,
value: {
fields: [
{
name: "body",
location: "body",
required,
schema: projectSchema(document, selected.schema),
style: undefined,
explode: undefined,
},
],
body: { required, mode: "value", mediaType: selected.mediaType },
},
}
}
const requiredProperties = new Set(
Array.isArray(schema.required) ? schema.required.filter((item): item is string => typeof item === "string") : [],
)
return {
ok: true,
value: {
fields: Object.entries(schema.properties).map(([name, value]) => ({
name,
location: "body" as const,
required: required && requiredProperties.has(name),
schema: projectSchema(document, value),
style: undefined,
explode: undefined,
})),
body: { required, mode: "object", mediaType: selected.mediaType },
},
}
}
export const operationInput = (
document: Document,
pathItem: Record<string, unknown>,
operation: Record<string, unknown>,
): Parsed<OperationInput> => {
const parameters = operationParameters(document, pathItem, operation)
if (!parameters.ok) return parameters
const requestBody = operationBody(document, operation)
if (!requestBody.ok) return requestBody
const fields = [...parameters.value, ...requestBody.value.fields]
const conflicts = new Set(
[...Map.groupBy(fields, (field) => field.name)]
.filter(([, matches]) => new Set(matches.map((field) => field.location)).size > 1)
.map(([name]) => name),
)
const used = new Set<string>()
return {
ok: true,
value: {
fields: fields.map((field) => {
const visibleName = isBlockedMember(field.name) ? `${field.name}_2` : field.name
const base = conflicts.has(field.name) ? `${field.location}_${visibleName}` : visibleName
const next = (index: number): string => {
const candidate = index === 1 ? base : `${base}_${index}`
return used.has(candidate) ? next(index + 1) : candidate
}
const inputName = next(1)
used.add(inputName)
return { ...field, inputName }
}),
body: requestBody.value.body,
},
}
}
export const inputSchema = (
fields: ReadonlyArray<InputField>,
definitions: Readonly<Record<string, JsonSchema>>,
): JsonSchema => {
const required = fields.filter((field) => field.required).map((field) => field.inputName)
return withDefinitions(
{
type: "object",
properties: Object.fromEntries(fields.map((field) => [field.inputName, field.schema])),
...(required.length === 0 ? {} : { required }),
},
definitions,
)
}
const successfulResponses = (
document: Document,
operation: Record<string, unknown>,
): Parsed<ReadonlyArray<Record<string, unknown>>> => {
if (!isRecord(operation.responses)) return { ok: true, value: [] }
const entries = Object.entries(operation.responses)
const selected = [
...entries.filter(([status]) => /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b)),
...entries.filter(([status]) => status.toUpperCase() === "2XX"),
]
const responses: Array<Record<string, unknown>> = []
for (const [, value] of selected) {
const resolved = resolve(document, value)
if (!isRecord(resolved) || nonEmptyString(resolved.$ref) !== undefined) {
return { ok: false, reason: "successful response declaration is invalid or unresolved" }
}
responses.push(resolved)
}
return { ok: true, value: responses }
}
export const operationOutput = (
document: Document,
operation: Record<string, unknown>,
definitions: Readonly<Record<string, JsonSchema>>,
): Parsed<JsonSchema | undefined> => {
if (operation["x-websocket"] === true) return { ok: false, reason: "WebSocket operations are not supported" }
const responses = successfulResponses(document, operation)
if (!responses.ok) return responses
const streams = responses.value.some(
(response) =>
isRecord(response.content) &&
Object.keys(response.content).some(
(mediaType) => mediaType.split(";")[0]?.trim().toLowerCase() === "text/event-stream",
),
)
if (streams) return { ok: false, reason: "SSE operations are not supported" }
const binary = responses.value.some(
(response) =>
isRecord(response.content) &&
Object.entries(response.content).some(([mediaType, value]) => isBinaryMediaType(document, mediaType, value)),
)
if (binary) return { ok: false, reason: "binary responses are not supported" }
const outcomes: Array<JsonSchema> = []
for (const response of responses.value) {
if (response.content !== undefined && !isRecord(response.content)) return { ok: true, value: undefined }
const content = isRecord(response.content) ? response.content : {}
if (Object.keys(content).length === 0) {
outcomes.push({ type: "null" })
continue
}
for (const [mediaType, value] of Object.entries(content)) {
if (!isJsonMediaType(mediaType)) {
outcomes.push({ type: "string" })
continue
}
if (!isRecord(value) || value.schema === undefined) return { ok: true, value: undefined }
outcomes.push(projectSchema(document, value.schema))
}
}
if (outcomes.length === 0) return { ok: true, value: undefined }
return {
ok: true,
value: withDefinitions(outcomes.length === 1 ? outcomes[0] ?? {} : { anyOf: outcomes }, definitions),
}
}
const sanitizeOperationSegment = (raw: string): string => {
const base =
raw
.replaceAll(/[^A-Za-z0-9_$]+/g, "_")
.replace(/^_+|_+$/g, "")
.replace(/^([0-9])/, "_$1") || "operation"
return isBlockedMember(base) ? `${base}_2` : base
}
const fallbackOperationId = (method: string, path: string): string =>
[
method,
...path
.split("/")
.filter((part) => part !== "")
.flatMap((part) => (part.startsWith("{") && part.endsWith("}") ? ["by", part.slice(1, -1)] : [part]))
.flatMap((part) => part.split(/[^A-Za-z0-9]+/).filter((word) => word !== "")),
]
.map((word, index) => {
const lower = word.toLowerCase()
return index === 0 ? lower : `${lower.charAt(0).toUpperCase()}${lower.slice(1)}`
})
.join("")
export const operationPath = (
method: string,
path: string,
operation: Record<string, unknown>,
used: ReadonlySet<string>,
namespaces: ReadonlySet<string>,
): ReadonlyArray<string> => {
const raw = nonEmptyString(operation.operationId)
const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map(sanitizeOperationSegment)
if (isOperationPathAvailable(segments, used, namespaces)) return segments
const conflict = segments.slice(0, -1).findIndex((_, index) => used.has(segments.slice(0, index + 1).join(".")))
if (conflict >= 0 && conflict + 1 < segments.length) {
const collapsed = segments.flatMap((segment, index) => {
if (index === conflict) {
const next = segments[index + 1] ?? ""
return [`${segment}${next.charAt(0).toUpperCase()}${next.slice(1)}`]
}
return index === conflict + 1 ? [] : [segment]
})
if (isOperationPathAvailable(collapsed, used, namespaces)) return collapsed
}
const fallback = segments.join("_")
const next = (index: number): string => {
const candidate = `${fallback}_${index}`
return isOperationPathAvailable([candidate], used, namespaces) ? candidate : next(index + 1)
}
return [next(2)]
}
const isOperationPathAvailable = (
segments: ReadonlyArray<string>,
used: ReadonlySet<string>,
namespaces: ReadonlySet<string>,
): boolean => {
const key = segments.join(".")
if (used.has(key) || namespaces.has(key)) return false
return segments.slice(0, -1).every((_, index) => !used.has(segments.slice(0, index + 1).join(".")))
}
export const specServerUrl = (source: Record<string, unknown>): Parsed<string> => {
const server = asArray(source.servers).find(isRecord)
const url = server === undefined ? undefined : nonEmptyString(server.url)
if (url === undefined) return { ok: false, reason: "spec declares no servers; pass baseUrl" }
if (/\{[^{}]+\}/.test(url)) {
return { ok: false, reason: `server URL '${url}' is not an absolute URL; pass baseUrl` }
}
return validateBaseUrl(url)
}
export const validateBaseUrl = (value: string): Parsed<string> => {
if (!/^https?:\/\//i.test(value)) return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` }
const url = URL.parse(value)
if (url === null || (url.protocol !== "http:" && url.protocol !== "https:")) {
return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` }
}
if (url.search !== "" || url.hash !== "") {
return { ok: false, reason: `server URL '${value}' contains an unsupported query string or fragment` }
}
return { ok: true, value }
}
export const securityRequirements = (value: unknown): Parsed<ReadonlyArray<SecurityRequirement>> => {
if (value === undefined) return { ok: true, value: [] }
if (!Array.isArray(value)) return { ok: false, reason: "security declaration is not an array" }
const requirements: Array<SecurityRequirement> = []
for (const item of value) {
if (!isRecord(item)) return { ok: false, reason: "security requirement is not an object" }
const requirement = Object.create(null) as Record<string, ReadonlyArray<string>>
for (const [name, scopes] of Object.entries(item)) {
if (!Array.isArray(scopes)) return { ok: false, reason: "security requirement scopes are not string arrays" }
const parsed = scopes.filter((scope): scope is string => typeof scope === "string")
if (parsed.length !== scopes.length) {
return { ok: false, reason: "security requirement scopes are not string arrays" }
}
requirement[name] = parsed
}
requirements.push(requirement)
}
return { ok: true, value: requirements }
}
export const operationSecurityRequirements = (
value: unknown,
defaults: Parsed<ReadonlyArray<SecurityRequirement>>,
schemes: Readonly<Record<string, SecurityScheme>>,
): Parsed<ReadonlyArray<SecurityRequirement>> => {
const parsed = value === undefined ? defaults : securityRequirements(value)
if (!parsed.ok) return parsed
const supported = parsed.value.filter((requirement) =>
Object.keys(requirement).every((name) => {
const scheme = own(schemes, name)
return scheme !== undefined && !(scheme.type === "apiKey" && scheme.in === "cookie")
}),
)
if (parsed.value.length === 0 || supported.length > 0) return { ok: true, value: supported }
const names = [...new Set(parsed.value.flatMap((requirement) => Object.keys(requirement)))]
const cookieScheme = names.find((name) => {
const definition = own(schemes, name)
return definition?.type === "apiKey" && definition.in === "cookie"
})
return {
ok: false,
reason:
cookieScheme === undefined
? `security requirement references missing or malformed scheme: ${names.join(", ")}`
: `cookie authentication '${cookieScheme}' is not supported`,
}
}
export const securitySchemes = (document: Document): Readonly<Record<string, SecurityScheme>> => {
const components = isRecord(document.components) ? document.components : {}
const declared = isRecord(components.securitySchemes) ? components.securitySchemes : {}
return Object.fromEntries(
Object.entries(declared).flatMap<readonly [string, SecurityScheme]>(([name, value]) => {
const resolved = resolve(document, value)
if (!isRecord(resolved)) return []
const type = nonEmptyString(resolved.type)
if (type === "apiKey") {
const carrier = nonEmptyString(resolved.in)
const parameter = nonEmptyString(resolved.name)
if (parameter === undefined || (carrier !== "header" && carrier !== "query" && carrier !== "cookie")) return []
return [[name, { type, name: parameter, in: carrier }] as const]
}
if (type === "http") {
const scheme = nonEmptyString(resolved.scheme)?.toLowerCase()
return scheme === undefined ? [] : [[name, { type, scheme }] as const]
}
if (type === "oauth2" || type === "openIdConnect") return [[name, { type }] as const]
return []
}),
)
}
+112
View File
@@ -0,0 +1,112 @@
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"
import type { Definition, JsonSchema } from "../tool.js"
/** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */
export type Document = Record<string, unknown>
/** The operation identity handed to auth resolution and errors. */
export type Operation = {
readonly operationId: string | undefined
readonly method: string
readonly path: string
readonly summary: string | undefined
readonly description: string | undefined
}
/** A resolved OpenAPI security scheme from `components.securitySchemes`. */
export type SecurityScheme =
| { readonly type: "apiKey"; readonly name: string; readonly in: "header" | "query" | "cookie" }
| { readonly type: "http"; readonly scheme: string }
| { readonly type: "oauth2" }
| { readonly type: "openIdConnect" }
/**
* Credential material returned by a host auth resolver. The carrier for `apiKey`
* comes from the scheme definition, not the credential. `header` is the escape
* hatch for nonstandard schemes.
*/
export type Credential =
| { readonly type: "bearer"; readonly token: string }
| { readonly type: "basic"; readonly username: string; readonly password: string }
| { readonly type: "apiKey"; readonly value: string }
| { readonly type: "header"; readonly name: string; readonly value: string }
/**
* Resolves credential material for one named security scheme at call time.
* `undefined` means unavailable, try the next OR alternative; a failure aborts
* the call rather than falling through.
*/
export type AuthResolver = (context: {
readonly name: string
readonly definition: SecurityScheme
readonly scopes: ReadonlyArray<string>
readonly operation: Operation
}) => Effect.Effect<Credential | undefined, unknown>
export type Options = {
readonly spec: Document
/** Overrides all document, path, and operation `servers`. Required when no applicable absolute server URL exists. */
readonly baseUrl?: string | undefined
/** Host credential resolution, keyed by security scheme name. */
readonly auth?: { readonly resolve: AuthResolver } | undefined
/** Static headers on every request. Not model-visible; declared header params may override them, auth always wins. */
readonly headers?: Readonly<Record<string, string>> | undefined
}
/** An operation that could not be represented as a tool, and why. */
export type Skipped = {
readonly method: string
readonly path: string
readonly reason: string
}
export type Tools = { [name: string]: Definition<HttpClient.HttpClient> | Tools }
export type Result = {
/** Tool subtree; the host places it under a key in its `tools` tree. */
readonly tools: Tools
readonly skipped: ReadonlyArray<Skipped>
}
export type Parsed<T> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string }
export type InputLocation = "path" | "query" | "header" | "body"
export type InputField = {
/** Model-visible field name after cross-location collision handling. */
readonly inputName: string
/** Original parameter or body-property name used on the wire. */
readonly name: string
readonly location: InputLocation
readonly required: boolean
readonly schema: JsonSchema
readonly style: "simple" | "form" | "deepObject" | undefined
readonly explode: boolean | undefined
}
export type Body = { readonly required: boolean; readonly mode: "object" | "value"; readonly mediaType: string }
export type OperationInput = {
readonly fields: ReadonlyArray<InputField>
readonly body: Body | undefined
}
/** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */
export type SecurityRequirement = Readonly<Record<string, ReadonlyArray<string>>>
export type Plan = {
readonly operation: Operation
readonly url: string
readonly fields: ReadonlyArray<InputField>
readonly body: Body | undefined
readonly security: ReadonlyArray<SecurityRequirement>
readonly schemes: Readonly<Record<string, SecurityScheme>>
readonly auth: { readonly resolve: AuthResolver } | undefined
readonly headers: Readonly<Record<string, string>>
}
export type AppliedAuth = {
readonly headers: Readonly<Record<string, string>>
readonly query: Readonly<Record<string, string>>
}
-10
View File
@@ -1,10 +0,0 @@
/**
* Token estimation for budgeting model-facing text. Copied from
* `@opencode-ai/core/util/token` (chars / 4) so this package stays
* dependency-free; keep the two in sync if the heuristic ever changes.
*/
export * as Token from "./token.js"
const CHARS_PER_TOKEN = 4
export const estimate = (input: string) => Math.max(0, Math.round(input.length / CHARS_PER_TOKEN))
+43 -42
View File
@@ -6,31 +6,35 @@ import {
identifierSegment,
inputProperties,
inputTypeScript,
isDefinition as isToolDefinition,
outputTypeScript,
type Definition,
} from "./tool.js"
import { estimate } from "./token.js"
} from "./tool-schema.js"
import { isDefinition as isToolDefinition, type Definition } from "./tool.js"
import { SandboxDate, SandboxMap, SandboxPromise, SandboxRegExp, SandboxSet } from "./values.js"
const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4))
export type HostTool<R = never> = (...args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
export type HostTools<R = never> = {
[name: string]: HostTool<R> | Definition<R> | HostTools<R>
}
export type Services<Tools> = Tools extends (...args: Array<unknown>) => Effect.Effect<unknown, unknown, infer R>
? R
: Tools extends {
readonly _tag: "CodeModeTool"
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R>
}
export type Services<Tools> = ServicesOf<Tools, []>
type ServicesOf<Tools, Depth extends ReadonlyArray<unknown>> = Depth["length"] extends 8
? never
: Tools extends (...args: Array<unknown>) => Effect.Effect<unknown, unknown, infer R>
? R
: Tools extends object
? string extends keyof Tools
? never
: Services<Tools[keyof Tools]>
: never
: Tools extends {
readonly _tag: "CodeModeTool"
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R>
}
? R
: Tools extends object
? string extends keyof Tools
? ServicesOf<Tools[string], [...Depth, unknown]>
: ServicesOf<Tools[keyof Tools], [...Depth, unknown]>
: never
/** Minimal audit record retained for each admitted tool call. */
export type ToolCall = {
@@ -290,17 +294,16 @@ const definitions = <R>(
return entries
}
const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDescription => ({
path,
description: definition.description,
signature: `${toolExpression(path)}(input: ${inputTypeScript(definition)}): Promise<${outputTypeScript(definition)}>`,
})
const visibleDefinitions = <R>(tools: HostTools<R>) =>
definitions(tools).flatMap(({ path, definition }) => {
const description = describeDefinition(path, definition)
return [{ path, definition, description }]
})
definitions(tools).map(({ path, definition }) => ({
path,
definition,
description: {
path,
description: definition.description,
signature: `${toolExpression(path)}(input: ${inputTypeScript(definition)}): Promise<${outputTypeScript(definition)}>`,
},
}))
export const catalog = <R>(tools: HostTools<R>): ReadonlyArray<ToolDescription> =>
visibleDefinitions(tools).map(({ description }) => description)
@@ -351,16 +354,10 @@ const termForms = (term: string): Array<string> => {
return forms
}
const firstLine = (text: string) => text.split("\n", 1)[0]!.trim()
/** One-line description used on inline catalog lines; the full text stays in search results. */
const brief = (text: string, max = 120) => {
const line = firstLine(text)
return line.length > max ? line.slice(0, max - 1) + "..." : line
}
const catalogLine = (tool: ToolDescription) => {
const description = brief(tool.description)
// Inline catalog lines use only a compact first line; full text stays in search results.
const line = tool.description.split("\n", 1)[0]!.trim()
const description = line.length > 120 ? line.slice(0, 119) + "..." : line
return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}`
}
@@ -430,7 +427,7 @@ export const discoveryPlan = <R>(
picked: new Set<ToolDescription>(),
queue: [...group].sort(
(left, right) =>
estimate(catalogLine(left)) - estimate(catalogLine(right)) || left.path.localeCompare(right.path),
estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || left.path.localeCompare(right.path),
),
}))
let used = 0
@@ -439,7 +436,7 @@ export const discoveryPlan = <R>(
const stillActive: typeof active = []
for (const selection of active) {
const tool = selection.queue[0]!
const cost = estimate(catalogLine(tool))
const cost = estimateTokens(catalogLine(tool))
if (used + cost > maxInlineCatalogTokens) continue
selection.queue.shift()
selection.picked.add(tool)
@@ -636,9 +633,6 @@ export type ToolRuntime<R = never> = {
readonly keys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
}
const failureMessage = (error: unknown): string =>
error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed"
export const make = <R>(
tools: HostTools<R>,
/** Undefined means unlimited tool calls. */
@@ -657,9 +651,16 @@ export const make = <R>(
const startedAt = Date.now()
return effect.pipe(
Effect.tap(() => onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "success" })),
Effect.tapError((error) =>
onEnd({ ...call, durationMs: Date.now() - startedAt, outcome: "failure", message: failureMessage(error) }),
),
Effect.tapError((error) => {
const message =
error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed"
return onEnd({
...call,
durationMs: Date.now() - startedAt,
outcome: "failure",
message,
})
}),
)
}
+301
View File
@@ -0,0 +1,301 @@
import { JsonPointer, Schema } from "effect"
import type { Definition, JsonSchema, SchemaType } from "./tool.js"
const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder<unknown> & Schema.Top => Schema.isSchema(schema)
const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
/**
* Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime,
* with dot access as a tool-path segment). Anything else must be quoted/bracketed.
*/
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */
const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name))
const effectNumberSentinel = (schema: JsonSchema) =>
schema.type === "string" &&
Array.isArray(schema.enum) &&
schema.enum.length === 1 &&
(schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity")
const intersection = (members: ReadonlyArray<string>): string => {
const concrete = members.filter((member) => member !== "unknown")
if (concrete.length === 0) return "unknown"
if (concrete.length === 1) return concrete[0] ?? "unknown"
return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ")
}
/**
* Recursion ceiling for schema rendering. Object, array, and union recursion all increment
* depth, so this bounds every recursion path - pathological or structurally cyclic schemas
* degrade to `unknown` instead of overflowing the stack (rendering must never throw).
*/
const MAX_RENDER_DEPTH = 8
type RenderContext = {
readonly definitions: Readonly<Record<string, JsonSchema>>
/** Indented, JSDoc-annotated multiline rendering (search results); compact single line otherwise. */
readonly pretty: boolean
}
const hasUnresolvedRef = (
schema: JsonSchema,
definitions: Readonly<Record<string, JsonSchema>>,
seen: ReadonlySet<string> = new Set(),
visited: ReadonlySet<JsonSchema> = new Set(),
): boolean => {
if (visited.has(schema)) return false
const nextVisited = new Set([...visited, schema])
if (schema.$ref !== undefined) {
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
if (name === undefined || definitions[name] === undefined || seen.has(name)) return true
if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited)) return true
}
return [
...(schema.anyOf ?? []),
...(schema.oneOf ?? []),
...(schema.allOf ?? []),
...Object.values(schema.properties ?? {}),
...(schema.items === undefined ? [] : [schema.items]),
...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []),
].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited))
}
/**
* Schema constraints a TypeScript type cannot express natively but a model benefits from,
* surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`).
*/
const docTags = (schema: JsonSchema): Array<string> => {
const tags: Array<string> = []
if (schema.deprecated === true) tags.push("@deprecated")
if (schema.default !== undefined) {
try {
const rendered = JSON.stringify(schema.default)
if (rendered !== undefined) tags.push(`@default ${rendered}`)
} catch {
// unserializable default: skip rather than emit a broken tag
}
}
if (typeof schema.format === "string") tags.push(`@format ${schema.format}`)
if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`)
if (typeof schema.maxItems === "number") tags.push(`@maxItems ${schema.maxItems}`)
return tags
}
/**
* Format a schema `description` plus `tags` as a JSDoc comment at the given indent,
* preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a
* `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and
* blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so
* callers can prepend it directly to the field line.
*/
const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
line.replaceAll("*/", "* /").replace(/\s+$/, ""),
)
while (lines.length > 0 && lines[0]!.trim() === "") lines.shift()
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop()
if (lines.length === 0) return ""
if (lines.length === 1) return `${pad}/** ${lines[0]} */\n`
const body = lines.map((line) => `${pad} *${line === "" ? "" : ` ${line}`}`).join("\n")
return `${pad}/**\n${body}\n${pad} */\n`
}
const renderSchema = (
schema: JsonSchema,
ctx: RenderContext,
depth = 0,
seen: ReadonlySet<string> = new Set(),
): string => {
if (depth > MAX_RENDER_DEPTH) return "unknown"
const nested =
schema.definitions === undefined && schema.$defs === undefined
? ctx
: { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } }
if (schema.$ref) {
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
if (!name || !nested.definitions[name] || seen.has(name)) return "unknown"
return intersection([
renderSchema(nested.definitions[name], nested, depth, new Set([...seen, name])),
renderSchema({ ...schema, $ref: undefined }, nested, depth + 1, seen),
])
}
if (schema.const !== undefined) return renderLiteral(schema.const)
if (schema.enum) return schema.enum.map(renderLiteral).join(" | ")
const alternatives = schema.anyOf ?? schema.oneOf
if (alternatives) {
// Effect's number schema emits `anyOf: [{ type: "number" }, { const: "NaN" },
// { const: "Infinity" }, { const: "-Infinity" }]`. Collapse only that artifact;
// real JSON Schema unions such as `string | number` or `number | null` must keep
// every branch.
if (
alternatives.some((item) => item.type === "number") &&
alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
)
return "number"
// An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]`
// (no properties/items); render the bare shape as {} instead of `{} | Array<unknown>`.
if (
alternatives.length === 2 &&
alternatives[0]?.type === "object" &&
alternatives[0].properties === undefined &&
alternatives[1]?.type === "array" &&
alternatives[1].items === undefined
) {
return "{}"
}
const members = alternatives.map((item) => renderSchema(item, nested, depth + 1, seen))
if (members.some((member) => member === "unknown")) return "unknown"
return intersection([
members.join(" | "),
renderSchema({ ...schema, anyOf: undefined, oneOf: undefined }, nested, depth + 1, seen),
])
}
if (schema.allOf) {
const members = schema.allOf.map((item) => renderSchema(item, nested, depth + 1, seen))
if (schema.allOf.some((item) => hasUnresolvedRef(item, nested.definitions))) return "unknown"
return intersection([renderSchema({ ...schema, allOf: undefined }, nested, depth + 1, seen), ...members])
}
if (Array.isArray(schema.type)) {
return schema.type.map((item) => renderSchema({ ...schema, type: item }, nested, depth + 1, seen)).join(" | ")
}
if (schema.type === "string") return "string"
if (schema.type === "number" || schema.type === "integer") return "number"
if (schema.type === "boolean") return "boolean"
if (schema.type === "null") return "null"
if (schema.type === "array") return `Array<${renderSchema(schema.items ?? {}, nested, depth + 1, seen)}>`
if (schema.type === "object" || schema.properties) {
const required = new Set(schema.required ?? [])
const properties = Object.entries(schema.properties ?? {})
const additional = schema.additionalProperties
const indexType =
additional && typeof additional === "object" ? renderSchema(additional, nested, depth + 1, seen) : undefined
const field = ([name, value]: readonly [string, JsonSchema]) =>
`${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, nested, depth + 1, seen)}`
if (!ctx.pretty) {
const fields = properties.map(field)
if (indexType !== undefined) fields.push(`[key: string]: ${indexType}`)
return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }`
}
// Pretty: an indented block, each described field preceded by its JSDoc comment.
if (properties.length === 0 && indexType === undefined) return "{}"
const pad = " ".repeat(depth + 1)
const lines = properties.map(
(entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)}`,
)
if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType}`)
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`
}
return "unknown"
}
export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => {
try {
const visible = decoded ? Schema.toType(schema) : schema
const document = Schema.toJsonSchemaDocument(visible) as {
readonly schema: JsonSchema
readonly definitions?: Readonly<Record<string, JsonSchema>>
}
return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty })
} catch {
return "unknown"
}
}
/** Renders a raw JSON Schema document as a TypeScript type string. */
export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => {
try {
return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty })
} catch {
return "unknown"
}
}
/** One input property of a tool, extracted best-effort from its input schema. */
export type InputProperty = {
readonly name: string
readonly description: string | undefined
readonly required: boolean
}
/**
* The property names, descriptions, and required flags of a tool's input schema - the raw
* material for search text. Best-effort: Effect Schemas go through their
* JSON Schema document (the same emission signature rendering uses); JSON Schemas are read
* directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present.
* Anything unresolvable yields `[]` (search falls back to path + description).
*/
export const inputProperties = <R>(definition: Definition<R>): Array<InputProperty> => {
try {
const document = isEffectSchema(definition.input)
? (Schema.toJsonSchemaDocument(definition.input) as {
readonly schema: JsonSchema
readonly definitions?: Readonly<Record<string, JsonSchema>>
})
: {
schema: definition.input,
definitions: { ...(definition.input.definitions ?? {}), ...(definition.input.$defs ?? {}) },
}
const definitions = document.definitions ?? {}
let schema = document.schema
if (schema.$ref !== undefined) {
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
const resolved = name === undefined ? undefined : definitions[name]
if (resolved === undefined) return []
schema = resolved
}
const required = new Set(schema.required ?? [])
return Object.entries(schema.properties ?? {}).map(([name, value]) => ({
name,
description: typeof value.description === "string" ? value.description : undefined,
required: required.has(name),
}))
} catch {
return []
}
}
/**
* The model-visible TypeScript type of a tool's input. `pretty` renders an indented
* multiline block with schema descriptions and constraints as JSDoc comments on the
* fields; the default stays the compact single-line form.
*/
export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
isEffectSchema(definition.input)
? toTypeScript(definition.input, false, pretty)
: jsonSchemaToTypeScript(definition.input, pretty)
/**
* The model-visible TypeScript type of a tool's result; tools without an output schema
* return `unknown`. `pretty` renders the JSDoc-annotated multiline form, as for inputs.
*/
export const outputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
definition.output === undefined
? "unknown"
: isEffectSchema(definition.output)
? toTypeScript(definition.output, true, pretty)
: jsonSchemaToTypeScript(definition.output, pretty)
/**
* Decodes tool input before `run` is invoked. Effect Schemas validate (throwing on failure);
* JSON-Schema-described inputs pass through unvalidated (render-only).
*/
export const decodeInput = <R>(definition: Definition<R>, value: unknown): unknown =>
isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value
/**
* Decodes a tool result before it is exposed to the program. Effect Schemas validate and
* transform (throwing on failure); JSON Schema outputs and tools without an output schema pass
* the host value through unchanged.
*/
export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unknown =>
definition.output !== undefined && isEffectSchema(definition.output)
? Schema.decodeUnknownSync(definition.output)(value)
: value
+8 -260
View File
@@ -13,6 +13,7 @@ export type JsonSchema = {
readonly const?: unknown
readonly anyOf?: ReadonlyArray<JsonSchema>
readonly oneOf?: ReadonlyArray<JsonSchema>
readonly allOf?: ReadonlyArray<JsonSchema>
readonly properties?: Readonly<Record<string, JsonSchema>>
readonly required?: ReadonlyArray<string>
readonly items?: JsonSchema
@@ -29,25 +30,25 @@ export type JsonSchema = {
}
/** Either a validating Effect Schema or a render-only JSON Schema document. */
export type ToolSchema = Schema.Decoder<unknown> | JsonSchema
export type SchemaType = Schema.Decoder<unknown> | JsonSchema
/** Schema-backed tool definition consumed by a CodeMode tool tree. */
export type Definition<R = never> = {
readonly _tag: "CodeModeTool"
readonly description: string
readonly input: ToolSchema
readonly output: ToolSchema | undefined
readonly input: SchemaType
readonly output: SchemaType | undefined
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, R>
}
/** The value `run` receives: the decoded type for Effect Schemas, `unknown` for JSON Schemas. */
export type InputType<S> = S extends Schema.Decoder<unknown> ? S["Type"] : unknown
type InputType<S> = S extends Schema.Decoder<unknown> ? S["Type"] : unknown
/** The value `run` returns: the encoded type for Effect Schemas, `unknown` otherwise. */
export type ResultType<S> = S extends Schema.Decoder<unknown> ? S["Encoded"] : unknown
type ResultType<S> = S extends Schema.Decoder<unknown> ? S["Encoded"] : unknown
/** Options for defining one CodeMode tool. */
export type Options<I extends ToolSchema, O extends ToolSchema | undefined, R = never> = {
export type Options<I extends SchemaType, O extends SchemaType | undefined, R = never> = {
readonly description: string
readonly input: I
readonly output?: O
@@ -57,256 +58,6 @@ export type Options<I extends ToolSchema, O extends ToolSchema | undefined, R =
export const isDefinition = <R = never>(value: unknown): value is Definition<R> =>
typeof value === "object" && value !== null && "_tag" in value && value._tag === "CodeModeTool"
const isEffectSchema = (schema: ToolSchema): schema is Schema.Decoder<unknown> & Schema.Top => Schema.isSchema(schema)
const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
/**
* Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime,
* with dot access as a tool-path segment). Anything else must be quoted/bracketed.
*/
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */
const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name))
const effectNumberSentinel = (schema: JsonSchema) =>
schema.type === "string" &&
Array.isArray(schema.enum) &&
schema.enum.length === 1 &&
(schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity")
/**
* Recursion ceiling for schema rendering. Object, array, and union recursion all increment
* depth, so this bounds every recursion path - pathological or structurally cyclic schemas
* degrade to `unknown` instead of overflowing the stack (rendering must never throw).
*/
const MAX_RENDER_DEPTH = 8
type RenderContext = {
readonly definitions: Readonly<Record<string, JsonSchema>>
/** Indented, JSDoc-annotated multiline rendering (search results); compact single line otherwise. */
readonly pretty: boolean
}
/**
* Schema constraints a TypeScript type cannot express natively but a model benefits from,
* surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`).
*/
const docTags = (schema: JsonSchema): Array<string> => {
const tags: Array<string> = []
if (schema.deprecated === true) tags.push("@deprecated")
if (schema.default !== undefined) {
try {
const rendered = JSON.stringify(schema.default)
if (rendered !== undefined) tags.push(`@default ${rendered}`)
} catch {
// unserializable default: skip rather than emit a broken tag
}
}
if (typeof schema.format === "string") tags.push(`@format ${schema.format}`)
if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`)
if (typeof schema.maxItems === "number") tags.push(`@maxItems ${schema.maxItems}`)
return tags
}
/**
* Format a schema `description` plus `tags` as a JSDoc comment at the given indent,
* preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a
* `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and
* blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so
* callers can prepend it directly to the field line.
*/
const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
line.replaceAll("*/", "* /").replace(/\s+$/, ""),
)
while (lines.length > 0 && lines[0]!.trim() === "") lines.shift()
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop()
if (lines.length === 0) return ""
if (lines.length === 1) return `${pad}/** ${lines[0]} */\n`
const body = lines.map((line) => `${pad} *${line === "" ? "" : ` ${line}`}`).join("\n")
return `${pad}/**\n${body}\n${pad} */\n`
}
const renderSchema = (
schema: JsonSchema,
ctx: RenderContext,
depth = 0,
seen: ReadonlySet<string> = new Set(),
): string => {
if (depth > MAX_RENDER_DEPTH) return "unknown"
if (schema.$ref) {
const name = schema.$ref.split("/").pop()
if (!name || !ctx.definitions[name]) return name ?? "unknown"
if (seen.has(name)) return name // recursive type: reference by name rather than loop
return renderSchema(ctx.definitions[name], ctx, depth, new Set([...seen, name]))
}
if (schema.const !== undefined) return renderLiteral(schema.const)
if (schema.enum) return schema.enum.map(renderLiteral).join(" | ")
const alternatives = schema.anyOf ?? schema.oneOf
if (alternatives) {
// Effect's number schema emits `anyOf: [{ type: "number" }, { const: "NaN" },
// { const: "Infinity" }, { const: "-Infinity" }]`. Collapse only that artifact;
// real JSON Schema unions such as `string | number` or `number | null` must keep
// every branch.
if (
alternatives.some((item) => item.type === "number") &&
alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
)
return "number"
// An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]`
// (no properties/items); render the bare shape as {} instead of `{} | Array<unknown>`.
if (
alternatives.length === 2 &&
alternatives[0]?.type === "object" &&
alternatives[0].properties === undefined &&
alternatives[1]?.type === "array" &&
alternatives[1].items === undefined
) {
return "{}"
}
return alternatives.map((item) => renderSchema(item, ctx, depth + 1, seen)).join(" | ")
}
if (Array.isArray(schema.type)) {
return schema.type.map((item) => renderSchema({ type: item }, ctx, depth + 1, seen)).join(" | ")
}
if (schema.type === "string") return "string"
if (schema.type === "number" || schema.type === "integer") return "number"
if (schema.type === "boolean") return "boolean"
if (schema.type === "null") return "null"
if (schema.type === "array") return `Array<${renderSchema(schema.items ?? {}, ctx, depth + 1, seen)}>`
if (schema.type === "object" || schema.properties) {
const required = new Set(schema.required ?? [])
const properties = Object.entries(schema.properties ?? {})
const additional = schema.additionalProperties
const indexType =
additional && typeof additional === "object" ? renderSchema(additional, ctx, depth + 1, seen) : undefined
const field = ([name, value]: readonly [string, JsonSchema]) =>
`${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, ctx, depth + 1, seen)}`
if (!ctx.pretty) {
const fields = properties.map(field)
if (indexType !== undefined) fields.push(`[key: string]: ${indexType}`)
return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }`
}
// Pretty: an indented block, each described field preceded by its JSDoc comment.
if (properties.length === 0 && indexType === undefined) return "{}"
const pad = " ".repeat(depth + 1)
const lines = properties.map(
(entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)}`,
)
if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType}`)
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`
}
return "unknown"
}
export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => {
try {
const visible = decoded ? Schema.toType(schema) : schema
const document = Schema.toJsonSchemaDocument(visible) as {
readonly schema: JsonSchema
readonly definitions?: Readonly<Record<string, JsonSchema>>
}
return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty })
} catch {
return "unknown"
}
}
/** Renders a raw JSON Schema document as a TypeScript type string. */
export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => {
try {
return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty })
} catch {
return "unknown"
}
}
/** One input property of a tool, extracted best-effort from its input schema. */
export type InputProperty = {
readonly name: string
readonly description: string | undefined
readonly required: boolean
}
/**
* The property names, descriptions, and required flags of a tool's input schema - the raw
* material for search text. Best-effort: Effect Schemas go through their
* JSON Schema document (the same emission signature rendering uses); JSON Schemas are read
* directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present.
* Anything unresolvable yields `[]` (search falls back to path + description).
*/
export const inputProperties = <R>(definition: Definition<R>): Array<InputProperty> => {
try {
const document = isEffectSchema(definition.input)
? (Schema.toJsonSchemaDocument(definition.input) as {
readonly schema: JsonSchema
readonly definitions?: Readonly<Record<string, JsonSchema>>
})
: {
schema: definition.input,
definitions: { ...(definition.input.definitions ?? {}), ...(definition.input.$defs ?? {}) },
}
const definitions = document.definitions ?? {}
let schema = document.schema
if (schema.$ref !== undefined) {
const name = schema.$ref.split("/").pop()
const resolved = name === undefined ? undefined : definitions[name]
if (resolved === undefined) return []
schema = resolved
}
const required = new Set(schema.required ?? [])
return Object.entries(schema.properties ?? {}).map(([name, value]) => ({
name,
description: typeof value.description === "string" ? value.description : undefined,
required: required.has(name),
}))
} catch {
return []
}
}
/**
* The model-visible TypeScript type of a tool's input. `pretty` renders an indented
* multiline block with schema descriptions and constraints as JSDoc comments on the
* fields; the default stays the compact single-line form.
*/
export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
isEffectSchema(definition.input)
? toTypeScript(definition.input, false, pretty)
: jsonSchemaToTypeScript(definition.input, pretty)
/**
* The model-visible TypeScript type of a tool's result; tools without an output schema
* return `unknown`. `pretty` renders the JSDoc-annotated multiline form, as for inputs.
*/
export const outputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
definition.output === undefined
? "unknown"
: isEffectSchema(definition.output)
? toTypeScript(definition.output, true, pretty)
: jsonSchemaToTypeScript(definition.output, pretty)
/**
* Decodes tool input before `run` is invoked. Effect Schemas validate (throwing on failure);
* JSON-Schema-described inputs pass through unvalidated (render-only).
*/
export const decodeInput = <R>(definition: Definition<R>, value: unknown): unknown =>
isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value
/**
* Decodes a tool result before it is exposed to the program. Effect Schemas validate and
* transform (throwing on failure); JSON Schema outputs and tools without an output schema pass
* the host value through unchanged.
*/
export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unknown =>
definition.output !== undefined && isEffectSchema(definition.output)
? Schema.decodeUnknownSync(definition.output)(value)
: value
/**
* Defines one schema-described tool available to a CodeMode program through `tools.*`.
*
@@ -334,7 +85,7 @@ export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unkn
* })
* ```
*/
export const make = <I extends ToolSchema, const O extends ToolSchema | undefined = undefined, R = never>(
export const make = <I extends SchemaType, const O extends SchemaType | undefined = undefined, R = never>(
options: Options<I, O, R>,
): Definition<R> => ({
_tag: "CodeModeTool",
@@ -343,6 +94,3 @@ export const make = <I extends ToolSchema, const O extends ToolSchema | undefine
output: options.output,
run: (input) => options.run(input as InputType<I>),
})
/** Constructors for schema-backed tools exposed inside CodeMode programs. */
export const Tool = { make, isDefinition }
+12 -27
View File
@@ -1,16 +1,8 @@
import { describe, expect, test } from "bun:test"
import { Cause, Effect, Schema } from "effect"
import {
CodeMode,
ExecuteInputSchema,
ExecuteResultSchema,
Tool,
toolError,
type ExecutionLimits,
} from "../src/index.js"
import type { Definition } from "../src/tool.js"
import { CodeMode, Tool, toolError } from "../src/index.js"
const run = (tool: Definition<never>) =>
const run = (tool: Tool.Definition<never>) =>
Effect.runPromise(CodeMode.make({ tools: { host: { call: tool } } }).execute("return await tools.host.call({})"))
class UnsafeHostError extends Schema.TaggedErrorClass<UnsafeHostError>()("UnsafeHostError", {
@@ -235,7 +227,7 @@ describe("CodeMode console capture", () => {
logs: ['Thread info: {"name":"Demo","count":2}', "[warn] careful"],
toolCalls: [],
})
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
})
test("keeps logs captured before failures", async () => {
@@ -356,7 +348,7 @@ describe("CodeMode output budget", () => {
})
test("truncates an oversized result value with a marker instead of failing", async () => {
const limits: ExecutionLimits = { maxOutputBytes: 40 }
const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 }
const result = await Effect.runPromise(
CodeMode.execute({
code: `return { data: "${"x".repeat(200)}" }`,
@@ -371,11 +363,11 @@ describe("CodeMode output budget", () => {
expect(result.value).toMatch(
/^\{"data":"x+ \[result truncated: \d+ bytes exceeds the 40-byte output limit; return a smaller value\]$/,
)
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
})
test("keeps leading logs within the remaining budget and marks the cut", async () => {
const limits: ExecutionLimits = { maxOutputBytes: 40 }
const limits: CodeMode.ExecutionLimits = { maxOutputBytes: 40 }
const result = await Effect.runPromise(
CodeMode.execute({
code: `
@@ -501,24 +493,17 @@ describe("CodeMode public contract", () => {
const tools = { orders: { lookup } }
const source = `return await tools.orders.lookup({ id: "order_42" })`
test("keeps one-shot, reusable, and agent-tool execution equivalent", async () => {
test("keeps one-shot and reusable execution equivalent", async () => {
const runtime = CodeMode.make({ tools })
const agentTool = runtime.agentTool()
const [oneShot, reusable, projected] = await Promise.all([
const [oneShot, reusable] = await Promise.all([
Effect.runPromise(CodeMode.execute({ tools, code: source })),
Effect.runPromise(runtime.execute(source)),
Effect.runPromise(agentTool.execute({ code: source })),
])
expect(reusable).toStrictEqual(oneShot)
expect(projected).toStrictEqual(oneShot)
expect(agentTool.name).toBe("code")
expect(agentTool.input).toBe(ExecuteInputSchema)
expect(agentTool.output).toBe(ExecuteResultSchema)
expect(agentTool.description).toBe(runtime.instructions())
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(projected)))).toStrictEqual(
projected,
)
const input: CodeMode.Input = { code: source }
expect(Schema.decodeUnknownSync(CodeMode.Input)(input)).toStrictEqual(input)
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(reusable)))).toStrictEqual(reusable)
})
test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => {
@@ -1035,7 +1020,7 @@ describe("CodeMode public contract", () => {
value: { top: null, nested: [1, null] },
toolCalls: [],
})
expect(Schema.decodeUnknownSync(ExecuteResultSchema)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
expect(Schema.decodeUnknownSync(CodeMode.Result)(JSON.parse(JSON.stringify(result)))).toStrictEqual(result)
})
test("rejects invalid configuration and discovery limits", async () => {
+245
View File
@@ -0,0 +1,245 @@
{
"openapi": "3.1.0",
"info": {
"title": "CodeMode Happy Path",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.example.test/v1"
}
],
"security": [
{
"BearerAuth": []
}
],
"paths": {
"/users/{userId}": {
"parameters": [
{
"$ref": "#/components/parameters/UserId"
}
],
"get": {
"operationId": "users.get",
"summary": "Get a user",
"parameters": [
{
"name": "include",
"in": "query",
"style": "form",
"explode": false,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "verbose",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "X-Trace-ID",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/UserResponse"
}
}
},
"delete": {
"operationId": "users.remove",
"summary": "Remove a user",
"responses": {
"204": {
"description": "Removed"
}
}
}
},
"/users": {
"post": {
"operationId": "users.create",
"summary": "Create a user",
"security": [
{
"ApiKey": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": [
"admin",
"member"
]
}
},
"required": [
"name",
"email"
],
"additionalProperties": false
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/vnd.example+json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
},
"/search": {
"get": {
"operationId": "search.run",
"summary": "Search users",
"security": [],
"parameters": [
{
"name": "filter",
"in": "query",
"style": "deepObject",
"explode": true,
"schema": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"page": {
"type": "integer"
}
},
"required": [
"query"
],
"additionalProperties": false
}
},
{
"name": "tags",
"in": "query",
"style": "form",
"explode": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"responses": {
"200": {
"description": "Summary",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {
"parameters": {
"UserId": {
"name": "userId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
},
"responses": {
"UserResponse": {
"description": "A user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
},
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"role": {
"type": "string",
"enum": [
"admin",
"member"
]
}
},
"required": [
"id",
"name",
"email"
],
"additionalProperties": false
}
},
"securitySchemes": {
"BearerAuth": {
"type": "http",
"scheme": "bearer"
},
"ApiKey": {
"type": "apiKey",
"in": "query",
"name": "api_key"
}
}
}
}
+26962
View File
@@ -0,0 +1,26962 @@
{
"openapi": "3.1.0",
"info": {
"title": "opencode HttpApi",
"version": "0.0.1",
"description": "Experimental HttpApi surface for selected instance routes."
},
"paths": {
"/api/health": {
"get": {
"tags": [
"server.health"
],
"operationId": "v2.health.get",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"healthy": {
"type": "boolean",
"enum": [
true
]
}
},
"required": [
"healthy"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Check whether the API server is ready to accept requests.",
"summary": "Check server health"
}
},
"/api/location": {
"get": {
"tags": [
"server.location"
],
"operationId": "v2.location.get",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Location.Info",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Location.Info"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Resolve the requested location or the server default location.",
"summary": "Get location"
}
},
"/api/agent": {
"get": {
"tags": [
"server.agent"
],
"operationId": "v2.agent.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AgentV2.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve currently registered agents.",
"summary": "List agents"
}
},
"/api/plugin": {
"get": {
"tags": [
"plugins"
],
"operationId": "v2.plugin.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Plugin.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve currently loaded plugins.",
"summary": "List plugins"
}
},
"/api/session": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.list",
"parameters": [
{
"name": "workspace",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^wrk"
}
]
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "limit",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Maximum number of sessions to return. Defaults to the newest 50 sessions."
},
"required": false
},
{
"name": "order",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string",
"enum": [
"asc",
"desc"
]
},
{
"type": "null"
}
],
"description": "Session order for the first page. Use desc for newest first or asc for oldest first."
},
"required": false
},
{
"name": "search",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "parentID",
"in": "query",
"schema": {
"anyOf": [
{
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
{
"type": "string",
"enum": [
"null"
]
}
],
"description": "Filter by parent session. Use null to return only root sessions."
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "directory",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "project",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "subpath",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "cursor",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string",
"description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response."
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "SessionsResponse",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionsResponse"
}
}
}
},
"400": {
"description": "InvalidCursorError | InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidCursorError"
},
{
"$ref": "#/components/schemas/InvalidRequestError1"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
"summary": "List sessions"
},
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.create",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Create a session at the requested location.",
"summary": "Create session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
{
"type": "null"
}
]
},
"agent": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"model": {
"anyOf": [
{
"$ref": "#/components/schemas/Model.Ref"
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"$ref": "#/components/schemas/Location.Ref"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/active": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.active",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "object",
"patternProperties": {
"^ses": {
"$ref": "#/components/schemas/SessionActive"
}
}
},
"watermarks": {
"$ref": "#/components/schemas/SessionWatermarks"
}
},
"required": [
"data",
"watermarks"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive. Watermarks are the durable log positions read alongside the activity snapshot; activity itself is process state, so the pairing is advisory rather than transactional.",
"summary": "List active sessions"
}
},
"/api/session/{sessionID}": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.get",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve a session by ID.",
"summary": "Get session"
}
},
"/api/session/{sessionID}/fork": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.fork",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | MessageNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/MessageNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Create a child session by copying projected history from the parent. When messageID is supplied, copy messages before that boundary.",
"summary": "Fork session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"messageID": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/agent": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.switchAgent",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Switch the agent used by subsequent provider turns.",
"summary": "Switch session agent",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"agent": {
"type": "string"
}
},
"required": [
"agent"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/model": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.switchModel",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Switch the model used by subsequent provider turns.",
"summary": "Switch session model",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"model": {
"$ref": "#/components/schemas/Model.Ref"
}
},
"required": [
"model"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/rename": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.rename",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Update the session title.",
"summary": "Rename session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": [
"title"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/prompt": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.prompt",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/SessionInput.Admitted"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictError"
}
}
}
}
},
"description": "Durably admit one session input and schedule agent-loop execution unless resume is false.",
"summary": "Send message",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
{
"type": "null"
}
]
},
"prompt": {
"$ref": "#/components/schemas/PromptInput"
},
"delivery": {
"anyOf": [
{
"type": "string",
"enum": [
"steer",
"queue"
]
},
{
"type": "null"
}
]
},
"resume": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"prompt"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/command": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.command",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/SessionInput.Admitted"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | CommandNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/CommandNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictError"
}
}
}
},
"500": {
"description": "CommandEvaluationError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CommandEvaluationError"
}
}
}
}
},
"description": "Resolve a slash command into prompt input, admit it durably, and schedule execution unless resume is false.",
"summary": "Run command",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
{
"type": "null"
}
]
},
"command": {
"type": "string"
},
"arguments": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"agent": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"model": {
"anyOf": [
{
"$ref": "#/components/schemas/Model.Ref"
},
{
"type": "null"
}
]
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PromptInput.FileAttachment"
}
},
"agents": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.AgentAttachment"
}
},
"delivery": {
"anyOf": [
{
"type": "string",
"enum": [
"steer",
"queue"
]
},
{
"type": "null"
}
]
},
"resume": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"command"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/skill": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.skill",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | SkillNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SkillNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Activate a skill for a session by appending a skill message and resuming execution.",
"summary": "Activate skill",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
{
"type": "null"
}
]
},
"skill": {
"type": "string"
},
"resume": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"skill"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/synthetic": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.synthetic",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Append a synthetic message to a session and resume execution.",
"summary": "Add synthetic message",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"metadata": {
"type": "object"
}
},
"required": [
"text"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/compact": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.compact",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "SessionBusyError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionBusyError"
}
}
}
},
"500": {
"description": "UnknownError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnknownError"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Compact a session conversation.",
"summary": "Compact session"
}
},
"/api/session/{sessionID}/wait": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.wait",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Wait for a session agent loop to become idle.",
"summary": "Wait for session"
}
},
"/api/session/{sessionID}/revert/stage": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.revert.stage",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "MessageNotFoundError | SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/MessageNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "SessionBusyError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionBusyError"
}
}
}
},
"500": {
"description": "UnknownError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnknownError"
}
}
}
}
},
"description": "Stage or move a reversible session boundary and optionally apply its file changes.",
"summary": "Stage session revert",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"files": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"messageID"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/revert/clear": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.revert.clear",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "SessionBusyError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionBusyError"
}
}
}
},
"500": {
"description": "UnknownError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnknownError"
}
}
}
}
},
"summary": "Clear staged revert"
}
},
"/api/session/{sessionID}/revert/commit": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.revert.commit",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "SessionBusyError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionBusyError"
}
}
}
}
},
"summary": "Commit staged revert"
}
},
"/api/session/{sessionID}/context": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.context",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Message"
}
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"500": {
"description": "UnknownError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnknownError"
}
}
}
}
},
"description": "Retrieve the active context messages for a session (all messages after the last compaction).",
"summary": "Get session context"
}
},
"/api/session/{sessionID}/context-entry": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.context.entry.list",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SessionContextEntry.Info"
}
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "List API-managed context entries attached to the session's system context.",
"summary": "List context entries"
}
},
"/api/session/{sessionID}/context-entry/{key}": {
"put": {
"tags": [
"sessions"
],
"operationId": "v2.session.context.entry.put",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "key",
"in": "path",
"schema": {
"$ref": "#/components/schemas/SessionContextEntry.Key"
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Attach or replace one durable context entry. The value is rendered into the session's system context; changes announce as updates at the next turn boundary.",
"summary": "Put context entry",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"value": {}
},
"required": [
"value"
],
"additionalProperties": false
}
}
},
"required": true
}
},
"delete": {
"tags": [
"sessions"
],
"operationId": "v2.session.context.entry.remove",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "key",
"in": "path",
"schema": {
"$ref": "#/components/schemas/SessionContextEntry.Key"
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Remove one context entry; the removal is announced to the model at the next turn boundary.",
"summary": "Remove context entry"
}
},
"/api/session/{sessionID}/log": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.log",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "after",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
},
{
"name": "follow",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string",
"enum": [
"true",
"false"
]
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"text/event-stream": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"event": {
"type": "string"
},
"data": {
"$ref": "#/components/schemas/SessionLogItemStream"
}
},
"required": [
"id",
"event",
"data"
],
"additionalProperties": false
},
"x-effect-stream": {
"encoding": "sse",
"causeSchema": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Fail"
]
},
"error": {
"not": {}
}
},
"required": [
"_tag",
"error"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Die"
]
},
"defect": {}
},
"required": [
"_tag",
"defect"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Interrupt"
]
},
"fiberId": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"fiberId"
],
"additionalProperties": false
}
]
}
},
"errorSchema": {
"not": {}
},
"failureEvent": "effect/httpapi/stream/failure"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Durable, ordered, gap-free read of public session events after an exclusive aggregate sequence. Emits a synced marker once replay reaches the captured watermark, then completes; with follow=true it continues with live events instead. The only event API that promises reliability: attach after a snapshot watermark to compose fetch and stream without a race window.",
"summary": "Read the session log"
}
},
"/api/session/{sessionID}/interrupt": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.interrupt",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op.",
"summary": "Interrupt session execution"
}
},
"/api/session/{sessionID}/background": {
"post": {
"tags": [
"sessions"
],
"operationId": "v2.session.background",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Move active foreground backgroundable tools for this session into background observation. Idle requests are a no-op.",
"summary": "Background blocking session tools"
}
},
"/api/session/{sessionID}/message/{messageID}": {
"get": {
"tags": [
"sessions"
],
"operationId": "v2.session.message",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "messageID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Session.Message"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | MessageNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/MessageNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve one projected message owned by the Session.",
"summary": "Get session message"
}
},
"/api/session/{sessionID}/message": {
"get": {
"tags": [
"messages"
],
"operationId": "v2.session.messages",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "limit",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Maximum number of messages to return. When omitted, the endpoint returns its default page size."
},
"required": false
},
{
"name": "order",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string",
"enum": [
"asc",
"desc"
]
},
{
"type": "null"
}
],
"description": "Message order for the first page. Use desc for newest first or asc for oldest first."
},
"required": false
},
{
"name": "cursor",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string",
"description": "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order."
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "SessionMessagesResponse",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionMessagesResponse"
}
}
}
},
"400": {
"description": "InvalidCursorError | InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidCursorError"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"500": {
"description": "UnknownError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnknownError"
}
}
}
}
},
"description": "Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
"summary": "Get session messages"
}
},
"/api/model": {
"get": {
"tags": [
"models"
],
"operationId": "v2.model.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ModelV2.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Retrieve available models ordered by release date.",
"summary": "List models"
}
},
"/api/model/default": {
"get": {
"tags": [
"models"
],
"operationId": "v2.model.default",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/ModelV2.Info"
},
{
"type": "null"
}
]
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Retrieve the model used when a session has no explicit model selection.",
"summary": "Get default model"
}
},
"/api/generate": {
"post": {
"tags": [
"generate"
],
"operationId": "v2.generate.text",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "GenerateTextResponse",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenerateTextResponse"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestError1"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Run one stateless model generation at the requested location and return the assistant text. Uses the location's default model when none is specified.",
"summary": "Generate text",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"prompt": {
"type": "string"
},
"model": {
"anyOf": [
{
"$ref": "#/components/schemas/Model.Ref"
},
{
"type": "null"
}
]
}
},
"required": [
"prompt"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/provider": {
"get": {
"tags": [
"providers"
],
"operationId": "v2.provider.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProviderV2.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Retrieve active AI providers so clients can show provider availability and configuration.",
"summary": "List providers"
}
},
"/api/provider/{providerID}": {
"get": {
"tags": [
"providers"
],
"operationId": "v2.provider.get",
"parameters": [
{
"name": "providerID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/ProviderV2.Info"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "ProviderNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderNotFoundError"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableError"
}
}
}
}
},
"description": "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
"summary": "Get provider"
}
},
"/api/integration": {
"get": {
"tags": [
"integrations"
],
"operationId": "v2.integration.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Integration.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve available integrations and their authentication methods.",
"summary": "List integrations"
}
},
"/api/integration/{integrationID}": {
"get": {
"tags": [
"integrations"
],
"operationId": "v2.integration.get",
"parameters": [
{
"name": "integrationID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Integration.Info"
},
{
"type": "null"
}
]
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve one integration and its authentication methods.",
"summary": "Get integration"
}
},
"/api/integration/{integrationID}/connect/key": {
"post": {
"tags": [
"integrations"
],
"operationId": "v2.integration.connect.key",
"parameters": [
{
"name": "integrationID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestError1"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Run a key authentication method and store the resulting credential.",
"summary": "Connect with key",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"key"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/integration/{integrationID}/connect/oauth": {
"post": {
"tags": [
"integrations"
],
"operationId": "v2.integration.connect.oauth",
"parameters": [
{
"name": "integrationID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Integration.Attempt"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestError1"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Start an OAuth attempt and return the authorization details.",
"summary": "Begin OAuth connection",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"methodID": {
"type": "string"
},
"inputs": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"label": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"methodID",
"inputs"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/integration/attempt/{attemptID}": {
"get": {
"tags": [
"integrations"
],
"operationId": "v2.integration.attempt.status",
"parameters": [
{
"name": "attemptID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Integration.AttemptStatus"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Poll the current status of an OAuth attempt.",
"summary": "Get OAuth attempt status"
},
"delete": {
"tags": [
"integrations"
],
"operationId": "v2.integration.attempt.cancel",
"parameters": [
{
"name": "attemptID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Cancel an OAuth attempt and release its resources.",
"summary": "Cancel OAuth connection"
}
},
"/api/integration/attempt/{attemptID}/complete": {
"post": {
"tags": [
"integrations"
],
"operationId": "v2.integration.attempt.complete",
"parameters": [
{
"name": "attemptID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestError1"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
"summary": "Complete OAuth connection",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/mcp": {
"get": {
"tags": [
"mcp"
],
"operationId": "v2.mcp.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Mcp.Server"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve configured MCP servers and their connection status.",
"summary": "List MCP servers"
}
},
"/api/credential/{credentialID}": {
"patch": {
"tags": [
"server.credential"
],
"operationId": "v2.credential.update",
"parameters": [
{
"name": "credentialID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Update a stored credential label.",
"summary": "Update credential",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"label": {
"type": "string"
}
},
"required": [
"label"
],
"additionalProperties": false
}
}
},
"required": true
}
},
"delete": {
"tags": [
"server.credential"
],
"operationId": "v2.credential.remove",
"parameters": [
{
"name": "credentialID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Remove a stored integration credential.",
"summary": "Remove credential"
}
},
"/api/project/current": {
"get": {
"tags": [
"projects"
],
"operationId": "v2.project.current",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Project.Current",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project.Current"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Resolve the project for the requested location.",
"summary": "Get current project"
}
},
"/api/project/{projectID}/directories": {
"get": {
"tags": [
"projects"
],
"operationId": "v2.project.directories",
"parameters": [
{
"name": "projectID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Project.Directories",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project.Directories"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "List known local absolute directories for a project.",
"summary": "List project directories"
}
},
"/api/form/request": {
"get": {
"tags": [
"forms"
],
"operationId": "v2.form.request.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.FormInfo"
},
{
"$ref": "#/components/schemas/Form.UrlInfo"
}
]
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve pending forms for a location.",
"summary": "List pending form requests"
}
},
"/api/session/{sessionID}/form": {
"get": {
"tags": [
"forms"
],
"operationId": "v2.session.form.list",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.FormInfo"
},
{
"$ref": "#/components/schemas/Form.UrlInfo"
}
]
}
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve pending forms for a session.",
"summary": "List session forms"
},
"post": {
"tags": [
"forms"
],
"operationId": "v2.session.form.create",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.FormInfo"
},
{
"$ref": "#/components/schemas/Form.UrlInfo"
}
]
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestError1"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictError"
}
}
}
}
},
"description": "Create a form for a session.",
"summary": "Create session form",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Form.CreatePayload"
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/form/{formID}": {
"get": {
"tags": [
"forms"
],
"operationId": "v2.session.form.get",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "formID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.FormInfo"
},
{
"$ref": "#/components/schemas/Form.UrlInfo"
}
]
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | FormNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/FormNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve a form for a session.",
"summary": "Get session form"
}
},
"/api/session/{sessionID}/form/{formID}/state": {
"get": {
"tags": [
"forms"
],
"operationId": "v2.session.form.state",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "formID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Form.State"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | FormNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/FormNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve the current state for a form.",
"summary": "Get form state"
}
},
"/api/session/{sessionID}/form/{formID}/reply": {
"post": {
"tags": [
"forms"
],
"operationId": "v2.session.form.reply",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "formID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "FormInvalidAnswerError | InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/FormInvalidAnswerError"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | FormNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/FormNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "FormAlreadySettledError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FormAlreadySettledError"
}
}
}
}
},
"description": "Submit an answer to a pending form.",
"summary": "Reply to form",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Form.Reply"
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/form/{formID}/cancel": {
"post": {
"tags": [
"forms"
],
"operationId": "v2.session.form.cancel",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "formID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | FormNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/FormNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
},
"409": {
"description": "FormAlreadySettledError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FormAlreadySettledError"
}
}
}
}
},
"description": "Cancel a pending form.",
"summary": "Cancel form"
}
},
"/api/permission/request": {
"get": {
"tags": [
"permissions"
],
"operationId": "v2.permission.request.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermissionV2.Request"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve pending permission requests for a location.",
"summary": "List pending permission requests"
}
},
"/api/permission/saved": {
"get": {
"tags": [
"permissions"
],
"operationId": "v2.permission.saved.list",
"parameters": [
{
"name": "projectID",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermissionSaved.Info"
}
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve saved permissions, optionally filtered by project.",
"summary": "List saved permissions"
}
},
"/api/permission/saved/{id}": {
"delete": {
"tags": [
"permissions"
],
"operationId": "v2.permission.saved.remove",
"parameters": [
{
"name": "id",
"in": "path",
"schema": {
"type": "string"
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Remove a saved permission by ID.",
"summary": "Remove saved permission"
}
},
"/api/session/{sessionID}/permission": {
"post": {
"tags": [
"permissions"
],
"operationId": "v2.session.permission.create",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"effect": {
"$ref": "#/components/schemas/PermissionV2.Effect"
}
},
"required": [
"id",
"effect"
],
"additionalProperties": false
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Evaluate and, when approval is required, create a permission request for a session.",
"summary": "Create permission request",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
{
"type": "null"
}
]
},
"action": {
"type": "string"
},
"resources": {
"type": "array",
"items": {
"type": "string"
}
},
"save": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object"
},
"source": {
"$ref": "#/components/schemas/PermissionV2.Source"
},
"agent": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"action",
"resources"
],
"additionalProperties": false
}
}
},
"required": true
}
},
"get": {
"tags": [
"permissions"
],
"operationId": "v2.session.permission.list",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermissionV2.Request"
}
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve pending permission requests owned by a session.",
"summary": "List session permission requests"
}
},
"/api/session/{sessionID}/permission/{requestID}": {
"get": {
"tags": [
"permissions"
],
"operationId": "v2.session.permission.get",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "requestID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/PermissionV2.Request"
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | PermissionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/PermissionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve a pending permission request owned by a session.",
"summary": "Get permission request"
}
},
"/api/session/{sessionID}/permission/{requestID}/reply": {
"post": {
"tags": [
"permissions"
],
"operationId": "v2.session.permission.reply",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "requestID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | PermissionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/PermissionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Respond to a pending permission request owned by a session.",
"summary": "Reply to pending permission request",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"reply": {
"$ref": "#/components/schemas/PermissionV2.Reply"
},
"message": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"reply"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/fs/read/*": {
"get": {
"tags": [
"filesystem"
],
"operationId": "v2.fs.read",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Serve one file relative to the requested location.",
"summary": "Read file"
}
},
"/api/fs/list": {
"get": {
"tags": [
"filesystem"
],
"operationId": "v2.fs.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "path",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileSystem.Entry"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "List direct children of one directory relative to the requested location.",
"summary": "List directory"
}
},
"/api/fs/find": {
"get": {
"tags": [
"filesystem"
],
"operationId": "v2.fs.find",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "query",
"in": "query",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "type",
"in": "query",
"schema": {
"type": "string",
"enum": [
"file",
"directory"
]
},
"required": false
},
{
"name": "limit",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/FileSystem.Entry"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Find recursively ranked filesystem entries relative to the requested location.",
"summary": "Find files"
}
},
"/api/command": {
"get": {
"tags": [
"commands"
],
"operationId": "v2.command.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CommandV2.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve currently registered commands.",
"summary": "List commands"
}
},
"/api/skill": {
"get": {
"tags": [
"skills"
],
"operationId": "v2.skill.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SkillV2.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve currently registered skills.",
"summary": "List skills"
}
},
"/api/event": {
"get": {
"tags": [
"events"
],
"operationId": "v2.event.subscribe",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"text/event-stream": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"event": {
"type": "string"
},
"data": {
"$ref": "#/components/schemas/V2EventStream"
}
},
"required": [
"id",
"event",
"data"
],
"additionalProperties": false
},
"x-effect-stream": {
"encoding": "sse",
"causeSchema": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Fail"
]
},
"error": {
"not": {}
}
},
"required": [
"_tag",
"error"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Die"
]
},
"defect": {}
},
"required": [
"_tag",
"defect"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Interrupt"
]
},
"fiberId": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"fiberId"
],
"additionalProperties": false
}
]
}
},
"errorSchema": {
"not": {}
},
"failureEvent": "effect/httpapi/stream/failure"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Subscribe to native event payloads for the server. Volatile by contract: a slow consumer overflows and fails the stream, and events during disconnection are missed. Consumers that need reliability should combine the changes feed with durable session log reads.",
"summary": "Subscribe to events"
}
},
"/api/event/changes": {
"get": {
"tags": [
"events"
],
"operationId": "v2.event.changes",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"text/event-stream": {
"schema": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"event": {
"type": "string"
},
"data": {
"$ref": "#/components/schemas/EventLog.ChangeStream"
}
},
"required": [
"id",
"event",
"data"
],
"additionalProperties": false
},
"x-effect-stream": {
"encoding": "sse",
"causeSchema": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Fail"
]
},
"error": {
"not": {}
}
},
"required": [
"_tag",
"error"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Die"
]
},
"defect": {}
},
"required": [
"_tag",
"defect"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"Interrupt"
]
},
"fiberId": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"fiberId"
],
"additionalProperties": false
}
]
}
},
"errorSchema": {
"not": {}
},
"failureEvent": "effect/httpapi/stream/failure"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Payload-free hint channel: after an event commits, a subscriber eventually receives a hint for that aggregate with seq at or beyond the event, or a sweep-required marker. Hints coalesce to the latest seq per aggregate under backpressure and the stream never fails from overflow. No consumer may derive correctness from receiving a hint; correctness always comes from durable log reads plus the consumer's own checkpoint. A sweep-required marker is emitted first on every (re)subscribe and whenever hint retention is exceeded: treat every aggregate as potentially dirty and recover via bounded sweep plus log reads.",
"summary": "Subscribe to change hints"
}
},
"/api/pty": {
"get": {
"tags": [
"pty"
],
"operationId": "v2.pty.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Pty"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "List PTY sessions for a location, including exited sessions retained until removal.",
"summary": "List PTY sessions"
},
"post": {
"tags": [
"pty"
],
"operationId": "v2.pty.create",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Pty"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Create a pseudo-terminal session for a location.",
"summary": "Create PTY session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"command": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "string"
}
},
"cwd": {
"type": "string"
},
"title": {
"type": "string"
},
"env": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/pty/{ptyID}": {
"get": {
"tags": [
"pty"
],
"operationId": "v2.pty.get",
"parameters": [
{
"name": "ptyID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Pty"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "PtyNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PtyNotFoundError"
}
}
}
}
},
"description": "Get one PTY session, including its exit code once exited.",
"summary": "Get PTY session"
},
"put": {
"tags": [
"pty"
],
"operationId": "v2.pty.update",
"parameters": [
{
"name": "ptyID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Pty"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "PtyNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PtyNotFoundError"
}
}
}
}
},
"description": "Update the title or viewport size of one PTY session.",
"summary": "Update PTY session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"size": {
"type": "object",
"properties": {
"rows": {
"type": "integer",
"allOf": [
{
"exclusiveMinimum": 0
}
]
},
"cols": {
"type": "integer",
"allOf": [
{
"exclusiveMinimum": 0
}
]
}
},
"required": [
"rows",
"cols"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
}
},
"required": true
}
},
"delete": {
"tags": [
"pty"
],
"operationId": "v2.pty.remove",
"parameters": [
{
"name": "ptyID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "PtyNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PtyNotFoundError"
}
}
}
}
},
"description": "Terminate and remove one PTY session.",
"summary": "Remove PTY session"
}
},
"/api/pty/{ptyID}/connect-token": {
"post": {
"tags": [
"pty"
],
"operationId": "v2.pty.connectToken",
"parameters": [
{
"name": "ptyID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/PtyTicket.ConnectToken"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"403": {
"description": "ForbiddenError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ForbiddenError"
}
}
}
},
"404": {
"description": "PtyNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PtyNotFoundError"
}
}
}
}
},
"description": "Create a short-lived single-use ticket for opening a PTY WebSocket connection.",
"summary": "Create PTY WebSocket token"
}
},
"/api/pty/{ptyID}/connect": {
"get": {
"tags": [
"pty"
],
"operationId": "v2.pty.connect",
"x-websocket": true,
"parameters": [
{
"name": "ptyID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"required": true
},
{
"in": "query",
"name": "location[directory]",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "location[workspace]",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "ticket",
"schema": {
"type": "string"
}
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"403": {
"description": "ForbiddenError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ForbiddenError"
}
}
}
},
"404": {
"description": "PtyNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PtyNotFoundError"
}
}
}
}
},
"description": "Establish a WebSocket connection streaming PTY output and accepting terminal input.",
"summary": "Connect to PTY session"
}
},
"/api/shell": {
"get": {
"tags": [
"shell"
],
"operationId": "v2.shell.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Shell1"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "List currently running shell commands for a location. Exited commands are not included.",
"summary": "List running shell commands"
},
"post": {
"tags": [
"shell"
],
"operationId": "v2.shell.create",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Spawn one non-interactive shell command for a location. Combined stdout/stderr is captured to a file pageable via output.",
"summary": "Run shell command",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"timeout": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"metadata": {
"type": "object"
}
},
"required": [
"command"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/api/shell/{id}": {
"get": {
"tags": [
"shell"
],
"operationId": "v2.shell.get",
"parameters": [
{
"name": "id",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"$ref": "#/components/schemas/Shell1"
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "ShellNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShellNotFoundError"
}
}
}
}
},
"description": "Get one shell command, including its status and exit code once exited.",
"summary": "Get shell command"
},
"delete": {
"tags": [
"shell"
],
"operationId": "v2.shell.remove",
"parameters": [
{
"name": "id",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "ShellNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShellNotFoundError"
}
}
}
}
},
"description": "Terminate and remove one shell command and its retained output.",
"summary": "Remove shell command"
}
},
"/api/shell/{id}/output": {
"get": {
"tags": [
"shell"
],
"operationId": "v2.shell.output",
"parameters": [
{
"name": "id",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "cursor",
"in": "query",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$"
}
]
},
"required": false
},
{
"name": "limit",
"in": "query",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "object",
"properties": {
"output": {
"type": "string"
},
"cursor": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"size": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"truncated": {
"type": "boolean"
}
},
"required": [
"output",
"cursor",
"size",
"truncated"
],
"additionalProperties": false
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "ShellNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShellNotFoundError"
}
}
}
}
},
"description": "Page through captured combined output by absolute byte cursor.",
"summary": "Read shell output"
}
},
"/api/question/request": {
"get": {
"tags": [
"session questions"
],
"operationId": "v2.question.request.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Request"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Retrieve pending question requests for a location.",
"summary": "List pending question requests"
}
},
"/api/session/{sessionID}/question": {
"get": {
"tags": [
"session questions"
],
"operationId": "v2.session.question.list",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Request"
}
}
},
"required": [
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Retrieve pending question requests owned by a session.",
"summary": "List session question requests"
}
},
"/api/session/{sessionID}/question/{requestID}/reply": {
"post": {
"tags": [
"session questions"
],
"operationId": "v2.session.question.reply",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "requestID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | QuestionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/QuestionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Answer a pending question request owned by a session.",
"summary": "Reply to pending question request",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QuestionV2.Reply"
}
}
},
"required": true
}
}
},
"/api/session/{sessionID}/question/{requestID}/reject": {
"post": {
"tags": [
"session questions"
],
"operationId": "v2.session.question.reject",
"parameters": [
{
"name": "sessionID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"required": true
},
{
"name": "requestID",
"in": "path",
"schema": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"required": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
},
"404": {
"description": "SessionNotFoundError | QuestionNotFoundError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/QuestionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
},
{
"$ref": "#/components/schemas/SessionNotFoundError"
}
]
}
}
}
}
},
"description": "Reject a pending question request owned by a session.",
"summary": "Reject pending question request"
}
},
"/api/reference": {
"get": {
"tags": [
"reference"
],
"operationId": "v2.reference.list",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Reference.Info"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "List references available in the requested location.",
"summary": "List references"
}
},
"/experimental/project/{projectID}/copy": {
"post": {
"tags": [
"projectCopy"
],
"operationId": "v2.projectCopy.create",
"parameters": [
{
"name": "projectID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "ProjectCopy.Copy",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProjectCopy.Copy"
}
}
}
},
"400": {
"description": "ProjectCopyError | InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/ProjectCopyError"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"strategy": {
"type": "string"
},
"directory": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"strategy",
"directory"
],
"additionalProperties": false
}
}
},
"required": true
}
},
"delete": {
"tags": [
"projectCopy"
],
"operationId": "v2.projectCopy.remove",
"parameters": [
{
"name": "projectID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "ProjectCopyError | InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/ProjectCopyError"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"directory": {
"type": "string"
},
"force": {
"type": "boolean"
}
},
"required": [
"directory",
"force"
],
"additionalProperties": false
}
}
},
"required": true
}
}
},
"/experimental/project/{projectID}/copy/refresh": {
"post": {
"tags": [
"projectCopy"
],
"operationId": "v2.projectCopy.refresh",
"parameters": [
{
"name": "projectID",
"in": "path",
"schema": {
"type": "string"
},
"required": true
},
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "ProjectCopyError | InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/ProjectCopyError"
},
{
"$ref": "#/components/schemas/InvalidRequestError"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
}
}
},
"/api/vcs/status": {
"get": {
"tags": [
"vcs"
],
"operationId": "v2.vcs.status",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Vcs.FileStatus"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "List uncommitted working-copy changes relative to the requested location.",
"summary": "VCS status"
}
},
"/api/vcs/diff": {
"get": {
"tags": [
"vcs"
],
"operationId": "v2.vcs.diff",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workspace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "mode",
"in": "query",
"schema": {
"$ref": "#/components/schemas/Vcs.Mode"
},
"required": true
},
{
"name": "context",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"location": {
"$ref": "#/components/schemas/Location.Info"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
"required": [
"location",
"data"
],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestError"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedError"
}
}
}
}
},
"description": "Diff the working copy against HEAD (mode git) or the default-branch merge base (mode branch) for the requested location.",
"summary": "VCS diff"
}
}
},
"components": {
"schemas": {
"UnauthorizedError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"UnauthorizedError"
]
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"InvalidRequestError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"InvalidRequestError"
]
},
"message": {
"type": "string"
},
"kind": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"field": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"Location.Info": {
"type": "object",
"properties": {
"directory": {
"type": "string"
},
"workspaceID": {
"type": "string",
"allOf": [
{
"pattern": "^wrk"
}
]
},
"project": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"directory": {
"type": "string"
}
},
"required": [
"id",
"directory"
],
"additionalProperties": false
}
},
"required": [
"directory",
"project"
],
"additionalProperties": false
},
"Model.Ref": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"providerID": {
"type": "string"
},
"variant": {
"type": "string"
}
},
"required": [
"id",
"providerID"
],
"additionalProperties": false
},
"Provider.Settings": {
"type": "object"
},
"Provider.Request": {
"type": "object",
"properties": {
"settings": {
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"body": {
"type": "object"
}
},
"required": [
"settings",
"headers",
"body"
],
"additionalProperties": false
},
"Agent.Color": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^#[0-9a-fA-F]{6}$"
}
]
},
{
"type": "string",
"enum": [
"primary",
"secondary",
"accent",
"success",
"warning",
"error",
"info"
]
}
]
},
"PermissionV2.Effect": {
"type": "string",
"enum": [
"allow",
"deny",
"ask"
]
},
"PermissionV2.Rule": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"resource": {
"type": "string"
},
"effect": {
"$ref": "#/components/schemas/PermissionV2.Effect"
}
},
"required": [
"action",
"resource",
"effect"
],
"additionalProperties": false
},
"PermissionV2.Ruleset": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermissionV2.Rule"
}
},
"AgentV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"request": {
"$ref": "#/components/schemas/Provider.Request"
},
"system": {
"type": "string"
},
"description": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"subagent",
"primary",
"all"
]
},
"hidden": {
"type": "boolean"
},
"color": {
"$ref": "#/components/schemas/Agent.Color"
},
"steps": {
"type": "integer",
"allOf": [
{
"exclusiveMinimum": 0
}
]
},
"permissions": {
"$ref": "#/components/schemas/PermissionV2.Ruleset"
}
},
"required": [
"id",
"request",
"mode",
"hidden",
"permissions"
],
"additionalProperties": false
},
"Plugin.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"Location.Ref": {
"type": "object",
"properties": {
"directory": {
"type": "string"
},
"workspaceID": {
"type": "string",
"allOf": [
{
"pattern": "^wrk"
}
]
}
},
"required": [
"directory"
],
"additionalProperties": false
},
"File.Diff": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"added",
"modified",
"deleted"
]
},
"additions": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"deletions": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"patch": {
"type": "string"
}
},
"required": [
"path",
"status",
"additions",
"deletions",
"patch"
],
"additionalProperties": false
},
"Revert.State": {
"type": "object",
"properties": {
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"partID": {
"type": "string"
},
"snapshot": {
"type": "string"
},
"diff": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/File.Diff"
}
}
},
"required": [
"messageID"
],
"additionalProperties": false
},
"SessionV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"parentID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"projectID": {
"type": "string"
},
"agent": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"cost": {
"type": "number"
},
"tokens": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"updated": {
"type": "number"
},
"archived": {
"type": "number"
}
},
"required": [
"created",
"updated"
],
"additionalProperties": false
},
"title": {
"type": "string"
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"subpath": {
"type": "string"
},
"revert": {
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
"id",
"projectID",
"cost",
"tokens",
"time",
"title",
"location"
],
"additionalProperties": false
},
"SessionWatermarks": {
"type": "object",
"patternProperties": {
"^ses": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"description": "Durable log seq each session's snapshot was computed at. Attach a live log read after the watermark to compose fetch and stream gap-free; apply a snapshot only where its watermark is at or beyond already-applied events. Sessions without durable events are absent."
},
"SessionsResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SessionV2.Info"
}
},
"watermarks": {
"$ref": "#/components/schemas/SessionWatermarks"
},
"cursor": {
"type": "object",
"properties": {
"previous": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"next": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
},
"required": [
"data",
"watermarks",
"cursor"
],
"additionalProperties": false
},
"InvalidCursorError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"InvalidCursorError"
]
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"InvalidRequestError1": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"InvalidRequestError"
]
},
"message": {
"type": "string"
},
"kind": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"field": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"SessionActive": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"running"
]
}
},
"required": [
"type"
],
"additionalProperties": false
},
"SessionNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"SessionNotFoundError"
]
},
"sessionID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"sessionID",
"message"
],
"additionalProperties": false
},
"MessageNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"MessageNotFoundError"
]
},
"sessionID": {
"type": "string"
},
"messageID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"sessionID",
"messageID",
"message"
],
"additionalProperties": false
},
"Prompt.Source": {
"type": "object",
"properties": {
"start": {
"type": "number"
},
"end": {
"type": "number"
},
"text": {
"type": "string"
}
},
"required": [
"start",
"end",
"text"
],
"additionalProperties": false
},
"PromptInput.FileAttachment": {
"type": "object",
"properties": {
"uri": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"source": {
"$ref": "#/components/schemas/Prompt.Source"
}
},
"required": [
"uri"
],
"additionalProperties": false
},
"Prompt.AgentAttachment": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"source": {
"$ref": "#/components/schemas/Prompt.Source"
}
},
"required": [
"name"
],
"additionalProperties": false
},
"PromptInput": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PromptInput.FileAttachment"
}
},
"agents": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.AgentAttachment"
}
}
},
"required": [
"text"
],
"additionalProperties": false
},
"Prompt.FileAttachment": {
"type": "object",
"properties": {
"uri": {
"type": "string"
},
"mime": {
"type": "string"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"source": {
"$ref": "#/components/schemas/Prompt.Source"
}
},
"required": [
"uri",
"mime"
],
"additionalProperties": false
},
"Prompt": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.FileAttachment"
}
},
"agents": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.AgentAttachment"
}
}
},
"required": [
"text"
],
"additionalProperties": false
},
"SessionInput.Admitted": {
"type": "object",
"properties": {
"admittedSeq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"prompt": {
"$ref": "#/components/schemas/Prompt"
},
"delivery": {
"type": "string",
"enum": [
"steer",
"queue"
]
},
"timeCreated": {
"type": "number"
},
"promotedSeq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"admittedSeq",
"id",
"sessionID",
"prompt",
"delivery",
"timeCreated"
],
"additionalProperties": false
},
"ConflictError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"ConflictError"
]
},
"message": {
"type": "string"
},
"resource": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"CommandNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"CommandNotFoundError"
]
},
"command": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"command",
"message"
],
"additionalProperties": false
},
"CommandEvaluationError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"CommandEvaluationError"
]
},
"command": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"command",
"message"
],
"additionalProperties": false
},
"SkillNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"SkillNotFoundError"
]
},
"skill": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"skill",
"message"
],
"additionalProperties": false
},
"SessionBusyError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"SessionBusyError"
]
},
"sessionID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"sessionID",
"message"
],
"additionalProperties": false
},
"ServiceUnavailableError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"ServiceUnavailableError"
]
},
"message": {
"type": "string"
},
"service": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"UnknownError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"UnknownError"
]
},
"message": {
"type": "string"
},
"ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"Session.Message.AgentSwitched": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"agent-switched"
]
},
"agent": {
"type": "string"
}
},
"required": [
"id",
"time",
"type",
"agent"
],
"additionalProperties": false
},
"Session.Message.ModelSwitched": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"model-switched"
]
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
}
},
"required": [
"id",
"time",
"type",
"model"
],
"additionalProperties": false
},
"Session.Message.User": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"text": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.FileAttachment"
}
},
"agents": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.AgentAttachment"
}
},
"type": {
"type": "string",
"enum": [
"user"
]
}
},
"required": [
"id",
"time",
"text",
"type"
],
"additionalProperties": false
},
"Session.Message.Synthetic": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"text": {
"type": "string"
},
"description": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"synthetic"
]
}
},
"required": [
"id",
"time",
"sessionID",
"text",
"type"
],
"additionalProperties": false
},
"Session.Message.System": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"system"
]
},
"text": {
"type": "string"
}
},
"required": [
"id",
"time",
"type",
"text"
],
"additionalProperties": false
},
"Session.Message.Skill": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"skill"
]
},
"name": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"id",
"time",
"type",
"name",
"text"
],
"additionalProperties": false
},
"Session.Message.Shell": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"shell"
]
},
"callID": {
"type": "string"
},
"command": {
"type": "string"
},
"output": {
"type": "string"
}
},
"required": [
"id",
"time",
"type",
"callID",
"command",
"output"
],
"additionalProperties": false
},
"Session.Message.Assistant.Text": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"text"
]
},
"id": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"type",
"id",
"text"
],
"additionalProperties": false
},
"LLM.ProviderMetadata": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"Session.Message.Assistant.Reasoning": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"reasoning"
]
},
"id": {
"type": "string"
},
"text": {
"type": "string"
},
"providerMetadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
}
},
"required": [
"type",
"id",
"text"
],
"additionalProperties": false
},
"Session.Message.ToolState.Pending": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending"
]
},
"input": {
"type": "string"
}
},
"required": [
"status",
"input"
],
"additionalProperties": false
},
"Tool.TextContent": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"text"
]
},
"text": {
"type": "string"
}
},
"required": [
"type",
"text"
],
"additionalProperties": false
},
"Tool.FileContent": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"file"
]
},
"uri": {
"type": "string"
},
"mime": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"type",
"uri",
"mime"
],
"additionalProperties": false
},
"LLM.ToolContent": {
"anyOf": [
{
"$ref": "#/components/schemas/Tool.TextContent"
},
{
"$ref": "#/components/schemas/Tool.FileContent"
}
]
},
"Session.Message.ToolState.Running": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"running"
]
},
"input": {
"type": "object"
},
"structured": {
"type": "object"
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
}
},
"required": [
"status",
"input",
"structured",
"content"
],
"additionalProperties": false
},
"Session.Message.ToolState.Completed": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"completed"
]
},
"input": {
"type": "object"
},
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Prompt.FileAttachment"
}
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"outputPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"structured": {
"type": "object"
},
"result": {}
},
"required": [
"status",
"input",
"content",
"structured"
],
"additionalProperties": false
},
"Session.Error.Unknown": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"unknown"
]
},
"message": {
"type": "string"
}
},
"required": [
"type",
"message"
],
"additionalProperties": false
},
"Session.Message.ToolState.Error": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"input": {
"type": "object"
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"structured": {
"type": "object"
},
"error": {
"$ref": "#/components/schemas/Session.Error.Unknown"
},
"result": {}
},
"required": [
"status",
"input",
"content",
"structured",
"error"
],
"additionalProperties": false
},
"Session.Message.Assistant.Tool": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"tool"
]
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"provider": {
"type": "object",
"properties": {
"executed": {
"type": "boolean"
},
"metadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata"
},
"resultMetadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata"
}
},
"required": [
"executed"
],
"additionalProperties": false
},
"state": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.ToolState.Pending"
},
{
"$ref": "#/components/schemas/Session.Message.ToolState.Running"
},
{
"$ref": "#/components/schemas/Session.Message.ToolState.Completed"
},
{
"$ref": "#/components/schemas/Session.Message.ToolState.Error"
}
]
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"ran": {
"type": "number"
},
"completed": {
"type": "number"
},
"pruned": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
}
},
"required": [
"type",
"id",
"name",
"state",
"time"
],
"additionalProperties": false
},
"Session.Message.Assistant": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
},
"type": {
"type": "string",
"enum": [
"assistant"
]
},
"agent": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"content": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.Assistant.Text"
},
{
"$ref": "#/components/schemas/Session.Message.Assistant.Reasoning"
},
{
"$ref": "#/components/schemas/Session.Message.Assistant.Tool"
}
]
}
},
"snapshot": {
"type": "object",
"properties": {
"start": {
"type": "string"
},
"end": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"finish": {
"type": "string"
},
"cost": {
"type": "number"
},
"tokens": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"error": {
"$ref": "#/components/schemas/Session.Error.Unknown"
}
},
"required": [
"id",
"time",
"type",
"agent",
"model",
"content"
],
"additionalProperties": false
},
"Session.Message.Compaction": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"compaction"
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"summary": {
"type": "string"
},
"recent": {
"type": "string"
},
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
}
},
"required": [
"created"
],
"additionalProperties": false
}
},
"required": [
"type",
"reason",
"summary",
"recent",
"id",
"time"
],
"additionalProperties": false
},
"Session.Message": {
"anyOf": [
{
"$ref": "#/components/schemas/Session.Message.AgentSwitched"
},
{
"$ref": "#/components/schemas/Session.Message.ModelSwitched"
},
{
"$ref": "#/components/schemas/Session.Message.User"
},
{
"$ref": "#/components/schemas/Session.Message.Synthetic"
},
{
"$ref": "#/components/schemas/Session.Message.System"
},
{
"$ref": "#/components/schemas/Session.Message.Skill"
},
{
"$ref": "#/components/schemas/Session.Message.Shell"
},
{
"$ref": "#/components/schemas/Session.Message.Assistant"
},
{
"$ref": "#/components/schemas/Session.Message.Compaction"
}
]
},
"SessionContextEntry.Key": {
"type": "string",
"allOf": [
{
"pattern": "^[a-z0-9][a-z0-9._-]*$",
"description": "Context entry key (lowercase alphanumerics plus . _ -)"
}
]
},
"SessionContextEntry.Info": {
"type": "object",
"properties": {
"key": {
"$ref": "#/components/schemas/SessionContextEntry.Key"
},
"value": {}
},
"required": [
"key",
"value"
],
"additionalProperties": false
},
"session.next.agent.switched": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.agent.switched"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"agent": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"agent"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.model.switched": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.model.switched"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"model"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.moved": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.moved"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"subdirectory": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"location"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.renamed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.renamed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"title": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"title"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.forked": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.forked"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"parentID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
}
},
"required": [
"timestamp",
"sessionID",
"parentID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.prompted": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.prompted"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"prompt": {
"$ref": "#/components/schemas/Prompt"
},
"delivery": {
"type": "string",
"enum": [
"steer",
"queue"
]
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"prompt",
"delivery"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.prompt.admitted": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.prompt.admitted"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"prompt": {
"$ref": "#/components/schemas/Prompt"
},
"delivery": {
"type": "string",
"enum": [
"steer",
"queue"
]
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"prompt",
"delivery"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.context.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.context.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"text": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.synthetic": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.synthetic"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"text": {
"type": "string"
},
"description": {
"type": "string"
},
"metadata": {
"type": "object"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.skill.activated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.skill.activated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"name": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"name",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.shell.started": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.shell.started"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"command": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"callID",
"command"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.shell.ended": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.shell.ended"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"callID": {
"type": "string"
},
"output": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"callID",
"output"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.step.started": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.step.started"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"agent": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"snapshot": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"agent",
"model"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.step.ended": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.step.ended"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"finish": {
"type": "string"
},
"cost": {
"type": "number"
},
"tokens": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"snapshot": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"finish",
"cost",
"tokens"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.step.failed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.step.failed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"error": {
"$ref": "#/components/schemas/Session.Error.Unknown"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"error"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.text.started": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.text.started"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"textID": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"textID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.text.ended": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.text.ended"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"textID": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"textID",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.tool.input.started": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.input.started"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"name"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.tool.input.ended": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.input.ended"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"LLM.ProviderMetadata3": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"session.next.tool.called": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.called"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"tool": {
"type": "string"
},
"input": {
"type": "object"
},
"provider": {
"type": "object",
"properties": {
"executed": {
"type": "boolean"
},
"metadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata3"
}
},
"required": [
"executed"
],
"additionalProperties": false
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"tool",
"input",
"provider"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.tool.progress": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.progress"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"structured": {
"type": "object"
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"structured",
"content"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"LLM.ProviderMetadata4": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"session.next.tool.success": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.success"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"structured": {
"type": "object"
},
"content": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LLM.ToolContent"
}
},
"outputPaths": {
"type": "array",
"items": {
"type": "string"
}
},
"result": {},
"provider": {
"type": "object",
"properties": {
"executed": {
"type": "boolean"
},
"metadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata4"
}
},
"required": [
"executed"
],
"additionalProperties": false
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"structured",
"content",
"provider"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"LLM.ProviderMetadata5": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"session.next.tool.failed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.failed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"error": {
"$ref": "#/components/schemas/Session.Error.Unknown"
},
"result": {},
"provider": {
"type": "object",
"properties": {
"executed": {
"type": "boolean"
},
"metadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata5"
}
},
"required": [
"executed"
],
"additionalProperties": false
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"error",
"provider"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"LLM.ProviderMetadata6": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"session.next.reasoning.started": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.reasoning.started"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"reasoningID": {
"type": "string"
},
"providerMetadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata6"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"reasoningID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"LLM.ProviderMetadata7": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"session.next.reasoning.ended": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.reasoning.ended"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"reasoningID": {
"type": "string"
},
"text": {
"type": "string"
},
"providerMetadata": {
"$ref": "#/components/schemas/LLM.ProviderMetadata7"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"reasoningID",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.retry_error": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
},
"isRetryable": {
"type": "boolean"
},
"responseHeaders": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"responseBody": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"message",
"isRetryable"
],
"additionalProperties": false
},
"session.next.retried": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.retried"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"attempt": {
"type": "number"
},
"error": {
"$ref": "#/components/schemas/session.next.retry_error"
}
},
"required": [
"timestamp",
"sessionID",
"attempt",
"error"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.compaction.started": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.compaction.started"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"reason"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.compaction.ended": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.compaction.ended"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"reason": {
"type": "string",
"enum": [
"auto",
"manual"
]
},
"text": {
"type": "string"
},
"recent": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"reason",
"text",
"recent"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.revert.staged": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.revert.staged"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"revert": {
"$ref": "#/components/schemas/Revert.State"
}
},
"required": [
"timestamp",
"sessionID",
"revert"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.revert.cleared": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.revert.cleared"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
}
},
"required": [
"timestamp",
"sessionID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.revert.committed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.revert.committed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
}
},
"required": [
"timestamp",
"sessionID",
"messageID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"SessionDurableEvent": {
"oneOf": [
{
"$ref": "#/components/schemas/session.next.agent.switched"
},
{
"$ref": "#/components/schemas/session.next.model.switched"
},
{
"$ref": "#/components/schemas/session.next.moved"
},
{
"$ref": "#/components/schemas/session.next.renamed"
},
{
"$ref": "#/components/schemas/session.next.forked"
},
{
"$ref": "#/components/schemas/session.next.prompted"
},
{
"$ref": "#/components/schemas/session.next.prompt.admitted"
},
{
"$ref": "#/components/schemas/session.next.context.updated"
},
{
"$ref": "#/components/schemas/session.next.synthetic"
},
{
"$ref": "#/components/schemas/session.next.skill.activated"
},
{
"$ref": "#/components/schemas/session.next.shell.started"
},
{
"$ref": "#/components/schemas/session.next.shell.ended"
},
{
"$ref": "#/components/schemas/session.next.step.started"
},
{
"$ref": "#/components/schemas/session.next.step.ended"
},
{
"$ref": "#/components/schemas/session.next.step.failed"
},
{
"$ref": "#/components/schemas/session.next.text.started"
},
{
"$ref": "#/components/schemas/session.next.text.ended"
},
{
"$ref": "#/components/schemas/session.next.tool.input.started"
},
{
"$ref": "#/components/schemas/session.next.tool.input.ended"
},
{
"$ref": "#/components/schemas/session.next.tool.called"
},
{
"$ref": "#/components/schemas/session.next.tool.progress"
},
{
"$ref": "#/components/schemas/session.next.tool.success"
},
{
"$ref": "#/components/schemas/session.next.tool.failed"
},
{
"$ref": "#/components/schemas/session.next.reasoning.started"
},
{
"$ref": "#/components/schemas/session.next.reasoning.ended"
},
{
"$ref": "#/components/schemas/session.next.retried"
},
{
"$ref": "#/components/schemas/session.next.compaction.started"
},
{
"$ref": "#/components/schemas/session.next.compaction.ended"
},
{
"$ref": "#/components/schemas/session.next.revert.staged"
},
{
"$ref": "#/components/schemas/session.next.revert.cleared"
},
{
"$ref": "#/components/schemas/session.next.revert.committed"
}
]
},
"EventLog.Synced": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"log.synced"
]
},
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"type",
"aggregateID"
],
"additionalProperties": false,
"description": "Marker emitted once when a log read reaches its captured watermark. The reader holds every event committed at or below seq."
},
"SessionLogItem": {
"anyOf": [
{
"$ref": "#/components/schemas/SessionDurableEvent"
},
{
"$ref": "#/components/schemas/EventLog.Synced"
}
]
},
"SessionLogItemStream": {
"type": "string",
"contentSchema": {
"$ref": "#/components/schemas/SessionLogItem"
},
"contentMediaType": "application/json"
},
"SessionMessagesResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Session.Message"
}
},
"watermark": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"cursor": {
"type": "object",
"properties": {
"previous": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"next": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
},
"required": [
"data",
"cursor"
],
"additionalProperties": false
},
"Model.Api": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"aisdk"
]
},
"package": {
"type": "string"
},
"url": {
"type": "string"
},
"settings": {
"type": "object"
}
},
"required": [
"id",
"type",
"package"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"native"
]
},
"url": {
"type": "string"
},
"settings": {
"type": "object"
}
},
"required": [
"id",
"type",
"settings"
],
"additionalProperties": false
}
]
},
"Model.Capabilities": {
"type": "object",
"properties": {
"tools": {
"type": "boolean"
},
"input": {
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"tools",
"input",
"output"
],
"additionalProperties": false
},
"Model.Cost": {
"type": "object",
"properties": {
"tier": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"context"
]
},
"size": {
"type": "integer"
}
},
"required": [
"type",
"size"
],
"additionalProperties": false
},
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"cache"
],
"additionalProperties": false
},
"ModelV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"providerID": {
"type": "string"
},
"family": {
"type": "string"
},
"name": {
"type": "string"
},
"api": {
"$ref": "#/components/schemas/Model.Api"
},
"capabilities": {
"$ref": "#/components/schemas/Model.Capabilities"
},
"request": {
"type": "object",
"properties": {
"settings": {
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"body": {
"type": "object"
},
"variant": {
"type": "string"
}
},
"required": [
"settings",
"headers",
"body"
],
"additionalProperties": false
},
"variants": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"settings": {
"$ref": "#/components/schemas/Provider.Settings"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"body": {
"type": "object"
}
},
"required": [
"id",
"settings",
"headers",
"body"
],
"additionalProperties": false
}
},
"time": {
"type": "object",
"properties": {
"released": {
"type": "number"
}
},
"required": [
"released"
],
"additionalProperties": false
},
"cost": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Model.Cost"
}
},
"status": {
"type": "string",
"enum": [
"alpha",
"beta",
"deprecated",
"active"
]
},
"enabled": {
"type": "boolean"
},
"limit": {
"type": "object",
"properties": {
"context": {
"type": "integer"
},
"input": {
"type": "integer"
},
"output": {
"type": "integer"
}
},
"required": [
"context",
"output"
],
"additionalProperties": false
}
},
"required": [
"id",
"providerID",
"name",
"api",
"capabilities",
"request",
"variants",
"time",
"cost",
"status",
"enabled",
"limit"
],
"additionalProperties": false
},
"GenerateTextResponse": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": [
"text"
],
"additionalProperties": false
}
},
"required": [
"data"
],
"additionalProperties": false
},
"Provider.AISDK": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"aisdk"
]
},
"package": {
"type": "string"
},
"url": {
"type": "string"
},
"settings": {
"type": "object"
}
},
"required": [
"type",
"package"
],
"additionalProperties": false
},
"Provider.Native": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"native"
]
},
"url": {
"type": "string"
},
"settings": {
"type": "object"
}
},
"required": [
"type",
"settings"
],
"additionalProperties": false
},
"Provider.Api": {
"anyOf": [
{
"$ref": "#/components/schemas/Provider.AISDK"
},
{
"$ref": "#/components/schemas/Provider.Native"
}
]
},
"ProviderV2.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"name": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"api": {
"$ref": "#/components/schemas/Provider.Api"
},
"request": {
"$ref": "#/components/schemas/Provider.Request"
}
},
"required": [
"id",
"name",
"api",
"request"
],
"additionalProperties": false
},
"ProviderNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"ProviderNotFoundError"
]
},
"providerID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"providerID",
"message"
],
"additionalProperties": false
},
"Integration.When": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"op": {
"type": "string",
"enum": [
"eq",
"neq"
]
},
"value": {
"type": "string"
}
},
"required": [
"key",
"op",
"value"
],
"additionalProperties": false
},
"Integration.TextPrompt": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"text"
]
},
"key": {
"type": "string"
},
"message": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"when": {
"$ref": "#/components/schemas/Integration.When"
}
},
"required": [
"type",
"key",
"message"
],
"additionalProperties": false
},
"Integration.SelectPrompt": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"select"
]
},
"key": {
"type": "string"
},
"message": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"value": {
"type": "string"
},
"hint": {
"type": "string"
}
},
"required": [
"label",
"value"
],
"additionalProperties": false
}
},
"when": {
"$ref": "#/components/schemas/Integration.When"
}
},
"required": [
"type",
"key",
"message",
"options"
],
"additionalProperties": false
},
"Integration.OAuthMethod": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"oauth"
]
},
"label": {
"type": "string"
},
"prompts": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Integration.TextPrompt"
},
{
"$ref": "#/components/schemas/Integration.SelectPrompt"
}
]
}
}
},
"required": [
"id",
"type",
"label"
],
"additionalProperties": false
},
"Integration.KeyMethod": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"key"
]
},
"label": {
"type": "string"
}
},
"required": [
"type"
],
"additionalProperties": false
},
"Integration.EnvMethod": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"env"
]
},
"names": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"type",
"names"
],
"additionalProperties": false
},
"Integration.Method": {
"anyOf": [
{
"$ref": "#/components/schemas/Integration.OAuthMethod"
},
{
"$ref": "#/components/schemas/Integration.KeyMethod"
},
{
"$ref": "#/components/schemas/Integration.EnvMethod"
}
]
},
"Connection.CredentialInfo": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"credential"
]
},
"id": {
"type": "string"
},
"label": {
"type": "string"
}
},
"required": [
"type",
"id",
"label"
],
"additionalProperties": false
},
"Connection.EnvInfo": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"env"
]
},
"name": {
"type": "string"
}
},
"required": [
"type",
"name"
],
"additionalProperties": false
},
"Connection.Info": {
"anyOf": [
{
"$ref": "#/components/schemas/Connection.CredentialInfo"
},
{
"$ref": "#/components/schemas/Connection.EnvInfo"
}
]
},
"Integration.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"methods": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Integration.Method"
}
},
"connections": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Connection.Info"
}
}
},
"required": [
"id",
"name",
"methods",
"connections"
],
"additionalProperties": false
},
"Integration.Attempt": {
"type": "object",
"properties": {
"attemptID": {
"type": "string"
},
"url": {
"type": "string"
},
"instructions": {
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"auto",
"code"
]
},
"time": {
"type": "object",
"properties": {
"created": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"expires": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"created",
"expires"
],
"additionalProperties": false
}
},
"required": [
"attemptID",
"url",
"instructions",
"mode",
"time"
],
"additionalProperties": false
},
"Integration.AttemptStatus": {
"anyOf": [
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending"
]
},
"time": {
"type": "object",
"properties": {
"created": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"expires": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"created",
"expires"
],
"additionalProperties": false
}
},
"required": [
"status",
"time"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"complete"
]
},
"time": {
"type": "object",
"properties": {
"created": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"expires": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"created",
"expires"
],
"additionalProperties": false
}
},
"required": [
"status",
"time"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"failed"
]
},
"message": {
"type": "string"
},
"time": {
"type": "object",
"properties": {
"created": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"expires": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"created",
"expires"
],
"additionalProperties": false
}
},
"required": [
"status",
"message",
"time"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"expired"
]
},
"time": {
"type": "object",
"properties": {
"created": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"expires": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"created",
"expires"
],
"additionalProperties": false
}
},
"required": [
"status",
"time"
],
"additionalProperties": false
}
]
},
"Mcp.Status.Connected": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"connected"
]
}
},
"required": [
"status"
],
"additionalProperties": false
},
"Mcp.Status.Disconnected": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"disconnected"
]
}
},
"required": [
"status"
],
"additionalProperties": false
},
"Mcp.Status.Disabled": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"disabled"
]
}
},
"required": [
"status"
],
"additionalProperties": false
},
"Mcp.Status.Failed": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"failed"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
],
"additionalProperties": false
},
"Mcp.Status.NeedsAuth": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"needs_auth"
]
}
},
"required": [
"status"
],
"additionalProperties": false
},
"Mcp.Status.NeedsClientRegistration": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"needs_client_registration"
]
},
"error": {
"type": "string"
}
},
"required": [
"status",
"error"
],
"additionalProperties": false
},
"Mcp.Server": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"status": {
"anyOf": [
{
"$ref": "#/components/schemas/Mcp.Status.Connected"
},
{
"$ref": "#/components/schemas/Mcp.Status.Disconnected"
},
{
"$ref": "#/components/schemas/Mcp.Status.Disabled"
},
{
"$ref": "#/components/schemas/Mcp.Status.Failed"
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsAuth"
},
{
"$ref": "#/components/schemas/Mcp.Status.NeedsClientRegistration"
}
]
},
"integrationID": {
"type": "string"
}
},
"required": [
"name",
"status"
],
"additionalProperties": false
},
"Project.Current": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"directory": {
"type": "string"
}
},
"required": [
"id",
"directory"
],
"additionalProperties": false
},
"Project.Directory": {
"type": "object",
"properties": {
"directory": {
"type": "string"
},
"strategy": {
"type": "string"
}
},
"required": [
"directory"
],
"additionalProperties": false
},
"Project.Directories": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Project.Directory"
}
},
"Form.Metadata": {
"type": "object"
},
"Form.When": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"op": {
"type": "string",
"enum": [
"eq",
"neq"
]
},
"value": {
"anyOf": [
{
"type": "string"
},
{
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
{
"type": "boolean"
}
]
}
},
"required": [
"key",
"op",
"value"
],
"additionalProperties": false
},
"Form.Option": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"label": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"value",
"label"
],
"additionalProperties": false
},
"Form.StringField": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When"
}
},
"type": {
"type": "string",
"enum": [
"string"
]
},
"format": {
"type": "string",
"enum": [
"email",
"uri",
"date",
"date-time"
]
},
"minLength": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"maxLength": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"pattern": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"default": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.Option"
}
},
"custom": {
"type": "boolean"
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.NumberField": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When"
}
},
"type": {
"type": "string",
"enum": [
"number"
]
},
"minimum": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"maximum": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"default": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.IntegerField": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When"
}
},
"type": {
"type": "string",
"enum": [
"integer"
]
},
"minimum": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"maximum": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"default": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.BooleanField": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When"
}
},
"type": {
"type": "string",
"enum": [
"boolean"
]
},
"default": {
"type": "boolean"
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.MultiselectField": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When"
}
},
"type": {
"type": "string",
"enum": [
"multiselect"
]
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.Option"
}
},
"minItems": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"maxItems": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"custom": {
"type": "boolean"
},
"default": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"key",
"type",
"options"
],
"additionalProperties": false
},
"Form.FormInfo": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"sessionID": {
"type": "string"
},
"title": {
"type": "string"
},
"metadata": {
"$ref": "#/components/schemas/Form.Metadata"
},
"mode": {
"type": "string",
"enum": [
"form"
]
},
"fields": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.StringField"
},
{
"$ref": "#/components/schemas/Form.NumberField"
},
{
"$ref": "#/components/schemas/Form.IntegerField"
},
{
"$ref": "#/components/schemas/Form.BooleanField"
},
{
"$ref": "#/components/schemas/Form.MultiselectField"
}
]
}
}
},
"required": [
"id",
"sessionID",
"mode",
"fields"
],
"additionalProperties": false
},
"Form.UrlInfo": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"sessionID": {
"type": "string"
},
"title": {
"type": "string"
},
"metadata": {
"$ref": "#/components/schemas/Form.Metadata"
},
"mode": {
"type": "string",
"enum": [
"url"
]
},
"url": {
"type": "string"
}
},
"required": [
"id",
"sessionID",
"mode",
"url"
],
"additionalProperties": false
},
"Form.CreatePayload": {
"type": "object",
"properties": {
"id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
{
"type": "null"
}
]
},
"title": {
"type": "string"
},
"metadata": {
"$ref": "#/components/schemas/Form.Metadata"
},
"mode": {
"type": "string",
"enum": [
"form",
"url"
]
},
"fields": {
"anyOf": [
{
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.StringField"
},
{
"$ref": "#/components/schemas/Form.NumberField"
},
{
"$ref": "#/components/schemas/Form.IntegerField"
},
{
"$ref": "#/components/schemas/Form.BooleanField"
},
{
"$ref": "#/components/schemas/Form.MultiselectField"
}
]
}
},
{
"type": "null"
}
]
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"mode"
],
"additionalProperties": false
},
"FormNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"FormNotFoundError"
]
},
"id": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"id",
"message"
],
"additionalProperties": false
},
"Form.Value": {
"anyOf": [
{
"type": "string"
},
{
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"Form.Answer": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.State": {
"anyOf": [
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending"
]
}
},
"required": [
"status"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"answered"
]
},
"answer": {
"$ref": "#/components/schemas/Form.Answer"
}
},
"required": [
"status",
"answer"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"cancelled"
]
}
},
"required": [
"status"
],
"additionalProperties": false
}
]
},
"Form.Reply": {
"type": "object",
"properties": {
"answer": {
"$ref": "#/components/schemas/Form.Answer"
}
},
"required": [
"answer"
],
"additionalProperties": false
},
"FormAlreadySettledError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"FormAlreadySettledError"
]
},
"id": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"id",
"message"
],
"additionalProperties": false
},
"FormInvalidAnswerError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"FormInvalidAnswerError"
]
},
"id": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"id",
"message"
],
"additionalProperties": false
},
"PermissionV2.Source": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"tool"
]
},
"messageID": {
"type": "string"
},
"callID": {
"type": "string"
}
},
"required": [
"type",
"messageID",
"callID"
],
"additionalProperties": false
}
]
},
"PermissionV2.Request": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"action": {
"type": "string"
},
"resources": {
"type": "array",
"items": {
"type": "string"
}
},
"save": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object"
},
"source": {
"$ref": "#/components/schemas/PermissionV2.Source"
}
},
"required": [
"id",
"sessionID",
"action",
"resources"
],
"additionalProperties": false
},
"PermissionSaved.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectID": {
"type": "string"
},
"action": {
"type": "string"
},
"resource": {
"type": "string"
}
},
"required": [
"id",
"projectID",
"action",
"resource"
],
"additionalProperties": false
},
"PermissionNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"PermissionNotFoundError"
]
},
"requestID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"requestID",
"message"
],
"additionalProperties": false
},
"PermissionV2.Reply": {
"type": "string",
"enum": [
"once",
"always",
"reject"
]
},
"FileSystem.Entry": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"file",
"directory"
]
}
},
"required": [
"path",
"type"
],
"additionalProperties": false
},
"CommandV2.Info": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"template": {
"type": "string"
},
"description": {
"type": "string"
},
"agent": {
"type": "string"
},
"model": {
"$ref": "#/components/schemas/Model.Ref"
},
"subtask": {
"type": "boolean"
}
},
"required": [
"name",
"template"
],
"additionalProperties": false
},
"SkillV2.Info": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"slash": {
"type": "boolean"
},
"autoinvoke": {
"type": "boolean"
},
"location": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"name",
"location",
"content"
],
"additionalProperties": false
},
"models-dev.refreshed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"models-dev.refreshed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"integration.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"integration.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"integration.connection.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"integration.connection.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"integrationID": {
"type": "string"
}
},
"required": [
"integrationID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"catalog.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"catalog.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"agent.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"agent.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"SnapshotFileDiff": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"patch": {
"type": "string"
},
"additions": {
"type": "number"
},
"deletions": {
"type": "number"
},
"status": {
"type": "string",
"enum": [
"added",
"deleted",
"modified"
]
}
},
"required": [
"additions",
"deletions"
],
"additionalProperties": false
},
"PermissionAction": {
"type": "string",
"enum": [
"allow",
"deny",
"ask"
]
},
"PermissionRule": {
"type": "object",
"properties": {
"permission": {
"type": "string"
},
"pattern": {
"type": "string"
},
"action": {
"$ref": "#/components/schemas/PermissionAction"
}
},
"required": [
"permission",
"pattern",
"action"
],
"additionalProperties": false
},
"PermissionRuleset": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PermissionRule"
}
},
"Session": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"slug": {
"type": "string"
},
"projectID": {
"type": "string"
},
"workspaceID": {
"type": "string",
"allOf": [
{
"pattern": "^wrk"
}
]
},
"directory": {
"type": "string"
},
"path": {
"type": "string"
},
"parentID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"summary": {
"type": "object",
"properties": {
"additions": {
"type": "number"
},
"deletions": {
"type": "number"
},
"files": {
"type": "number"
},
"diffs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
"required": [
"additions",
"deletions",
"files"
],
"additionalProperties": false
},
"cost": {
"type": "number"
},
"tokens": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"share": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
],
"additionalProperties": false
},
"title": {
"type": "string"
},
"agent": {
"type": "string"
},
"model": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"providerID": {
"type": "string"
},
"variant": {
"type": "string"
}
},
"required": [
"id",
"providerID"
],
"additionalProperties": false
},
"version": {
"type": "string"
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"updated": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"compacting": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"archived": {
"type": "number"
}
},
"required": [
"created",
"updated"
],
"additionalProperties": false
},
"permission": {
"$ref": "#/components/schemas/PermissionRuleset"
},
"revert": {
"type": "object",
"properties": {
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"partID": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"snapshot": {
"type": "string"
},
"diff": {
"type": "string"
}
},
"required": [
"messageID"
],
"additionalProperties": false
}
},
"required": [
"id",
"slug",
"projectID",
"directory",
"title",
"version",
"time"
],
"additionalProperties": false
},
"session.created": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.created"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"info": {
"$ref": "#/components/schemas/Session"
}
},
"required": [
"sessionID",
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"info": {
"$ref": "#/components/schemas/Session"
}
},
"required": [
"sessionID",
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.deleted": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.deleted"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"info": {
"$ref": "#/components/schemas/Session"
}
},
"required": [
"sessionID",
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"JSONSchema": {
"type": "object"
},
"OutputFormat": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"text"
]
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"json_schema"
]
},
"schema": {
"$ref": "#/components/schemas/JSONSchema"
},
"retryCount": {
"anyOf": [
{
"anyOf": [
{
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
{
"type": "null"
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"type",
"schema"
],
"additionalProperties": false
}
]
},
"UserMessage": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"role": {
"type": "string",
"enum": [
"user"
]
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "number",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"created"
],
"additionalProperties": false
},
"format": {
"anyOf": [
{
"$ref": "#/components/schemas/OutputFormat"
},
{
"type": "null"
}
]
},
"summary": {
"anyOf": [
{
"type": "object",
"properties": {
"title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"body": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"diffs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SnapshotFileDiff"
}
}
},
"required": [
"diffs"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"agent": {
"type": "string"
},
"model": {
"type": "object",
"properties": {
"providerID": {
"type": "string"
},
"modelID": {
"type": "string"
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"providerID",
"modelID"
],
"additionalProperties": false
},
"system": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"tools": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "boolean"
}
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"role",
"time",
"agent",
"model"
],
"additionalProperties": false
},
"ProviderAuthError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"ProviderAuthError"
]
},
"data": {
"type": "object",
"properties": {
"providerID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"providerID",
"message"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"UnknownError1": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"UnknownError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"message"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"MessageOutputLengthError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"MessageOutputLengthError"
]
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"MessageAbortedError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"MessageAbortedError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"StructuredOutputError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"StructuredOutputError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"retries": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"message",
"retries"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"ContextOverflowError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"ContextOverflowError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"responseBody": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"message"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"ContentFilterError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"ContentFilterError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"APIError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"APIError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"statusCode": {
"anyOf": [
{
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
{
"type": "null"
}
]
},
"isRetryable": {
"type": "boolean"
},
"responseHeaders": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
]
},
"responseBody": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"metadata": {
"anyOf": [
{
"type": "object",
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
]
}
},
"required": [
"message",
"isRetryable"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"AssistantMessage": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"role": {
"type": "string",
"enum": [
"assistant"
]
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"completed": {
"anyOf": [
{
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"created"
],
"additionalProperties": false
},
"error": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/components/schemas/ProviderAuthError"
},
{
"$ref": "#/components/schemas/UnknownError1"
},
{
"$ref": "#/components/schemas/MessageOutputLengthError"
},
{
"$ref": "#/components/schemas/MessageAbortedError"
},
{
"$ref": "#/components/schemas/StructuredOutputError"
},
{
"$ref": "#/components/schemas/ContextOverflowError"
},
{
"$ref": "#/components/schemas/ContentFilterError"
},
{
"$ref": "#/components/schemas/APIError"
}
]
},
{
"type": "null"
}
]
},
"parentID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"modelID": {
"type": "string"
},
"providerID": {
"type": "string"
},
"mode": {
"type": "string"
},
"agent": {
"type": "string"
},
"path": {
"type": "object",
"properties": {
"cwd": {
"type": "string"
},
"root": {
"type": "string"
}
},
"required": [
"cwd",
"root"
],
"additionalProperties": false
},
"summary": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"cost": {
"type": "number"
},
"tokens": {
"type": "object",
"properties": {
"total": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
},
"structured": {
"anyOf": [
{},
{
"type": "null"
}
]
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"finish": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"role",
"time",
"parentID",
"modelID",
"providerID",
"mode",
"agent",
"path",
"cost",
"tokens"
],
"additionalProperties": false
},
"Message": {
"anyOf": [
{
"$ref": "#/components/schemas/UserMessage"
},
{
"$ref": "#/components/schemas/AssistantMessage"
}
]
},
"message.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"message.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"info": {
"$ref": "#/components/schemas/Message"
}
},
"required": [
"sessionID",
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"message.removed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"message.removed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
}
},
"required": [
"sessionID",
"messageID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"TextPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"text"
]
},
"text": {
"type": "string"
},
"synthetic": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"ignored": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"time": {
"anyOf": [
{
"type": "object",
"properties": {
"start": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"end": {
"anyOf": [
{
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"start"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"metadata": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"text"
],
"additionalProperties": false
},
"SubtaskPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"subtask"
]
},
"prompt": {
"type": "string"
},
"description": {
"type": "string"
},
"agent": {
"type": "string"
},
"model": {
"anyOf": [
{
"type": "object",
"properties": {
"providerID": {
"type": "string"
},
"modelID": {
"type": "string"
}
},
"required": [
"providerID",
"modelID"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"command": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"prompt",
"description",
"agent"
],
"additionalProperties": false
},
"ReasoningPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"reasoning"
]
},
"text": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
]
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"end": {
"anyOf": [
{
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"start"
],
"additionalProperties": false
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"text",
"time"
],
"additionalProperties": false
},
"FilePartSourceText": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"start": {
"type": "number"
},
"end": {
"type": "number"
}
},
"required": [
"value",
"start",
"end"
],
"additionalProperties": false
},
"FileSource": {
"type": "object",
"properties": {
"text": {
"$ref": "#/components/schemas/FilePartSourceText"
},
"type": {
"type": "string",
"enum": [
"file"
]
},
"path": {
"type": "string"
}
},
"required": [
"text",
"type",
"path"
],
"additionalProperties": false
},
"Range": {
"type": "object",
"properties": {
"start": {
"type": "object",
"properties": {
"line": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"character": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"line",
"character"
],
"additionalProperties": false
},
"end": {
"type": "object",
"properties": {
"line": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"character": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"line",
"character"
],
"additionalProperties": false
}
},
"required": [
"start",
"end"
],
"additionalProperties": false
},
"SymbolSource": {
"type": "object",
"properties": {
"text": {
"$ref": "#/components/schemas/FilePartSourceText"
},
"type": {
"type": "string",
"enum": [
"symbol"
]
},
"path": {
"type": "string"
},
"range": {
"$ref": "#/components/schemas/Range"
},
"name": {
"type": "string"
},
"kind": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"text",
"type",
"path",
"range",
"name",
"kind"
],
"additionalProperties": false
},
"ResourceSource": {
"type": "object",
"properties": {
"text": {
"$ref": "#/components/schemas/FilePartSourceText"
},
"type": {
"type": "string",
"enum": [
"resource"
]
},
"clientName": {
"type": "string"
},
"uri": {
"type": "string"
}
},
"required": [
"text",
"type",
"clientName",
"uri"
],
"additionalProperties": false
},
"FilePartSource": {
"anyOf": [
{
"$ref": "#/components/schemas/FileSource"
},
{
"$ref": "#/components/schemas/SymbolSource"
},
{
"$ref": "#/components/schemas/ResourceSource"
}
]
},
"FilePart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"file"
]
},
"mime": {
"type": "string"
},
"filename": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"url": {
"type": "string"
},
"source": {
"anyOf": [
{
"$ref": "#/components/schemas/FilePartSource"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"mime",
"url"
],
"additionalProperties": false
},
"ToolStatePending": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"pending"
]
},
"input": {
"type": "object"
},
"raw": {
"type": "string"
}
},
"required": [
"status",
"input",
"raw"
],
"additionalProperties": false
},
"ToolStateRunning": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"running"
]
},
"input": {
"type": "object"
},
"title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"metadata": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
]
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"start"
],
"additionalProperties": false
}
},
"required": [
"status",
"input",
"time"
],
"additionalProperties": false
},
"ToolStateCompleted": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"completed"
]
},
"input": {
"type": "object"
},
"output": {
"type": "string"
},
"title": {
"type": "string"
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"end": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"compacted": {
"anyOf": [
{
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"start",
"end"
],
"additionalProperties": false
},
"attachments": {
"anyOf": [
{
"type": "array",
"items": {
"$ref": "#/components/schemas/FilePart"
}
},
{
"type": "null"
}
]
}
},
"required": [
"status",
"input",
"output",
"title",
"metadata",
"time"
],
"additionalProperties": false
},
"ToolStateError": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"error"
]
},
"input": {
"type": "object"
},
"error": {
"type": "string"
},
"metadata": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
]
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"end": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"start",
"end"
],
"additionalProperties": false
}
},
"required": [
"status",
"input",
"error",
"time"
],
"additionalProperties": false
},
"ToolState": {
"anyOf": [
{
"$ref": "#/components/schemas/ToolStatePending"
},
{
"$ref": "#/components/schemas/ToolStateRunning"
},
{
"$ref": "#/components/schemas/ToolStateCompleted"
},
{
"$ref": "#/components/schemas/ToolStateError"
}
]
},
"ToolPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"tool"
]
},
"callID": {
"type": "string"
},
"tool": {
"type": "string"
},
"state": {
"$ref": "#/components/schemas/ToolState"
},
"metadata": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"callID",
"tool",
"state"
],
"additionalProperties": false
},
"StepStartPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"step-start"
]
},
"snapshot": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type"
],
"additionalProperties": false
},
"StepFinishPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"step-finish"
]
},
"reason": {
"type": "string"
},
"snapshot": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"cost": {
"type": "number"
},
"tokens": {
"type": "object",
"properties": {
"total": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
],
"additionalProperties": false
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
],
"additionalProperties": false
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"reason",
"cost",
"tokens"
],
"additionalProperties": false
},
"SnapshotPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"snapshot"
]
},
"snapshot": {
"type": "string"
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"snapshot"
],
"additionalProperties": false
},
"PatchPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"patch"
]
},
"hash": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"hash",
"files"
],
"additionalProperties": false
},
"AgentPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"agent"
]
},
"name": {
"type": "string"
},
"source": {
"anyOf": [
{
"type": "object",
"properties": {
"value": {
"type": "string"
},
"start": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"end": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"value",
"start",
"end"
],
"additionalProperties": false
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"name"
],
"additionalProperties": false
},
"RetryPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"retry"
]
},
"attempt": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"error": {
"$ref": "#/components/schemas/APIError"
},
"time": {
"type": "object",
"properties": {
"created": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"created"
],
"additionalProperties": false
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"attempt",
"error",
"time"
],
"additionalProperties": false
},
"CompactionPart": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"type": {
"type": "string",
"enum": [
"compaction"
]
},
"auto": {
"type": "boolean"
},
"overflow": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
},
"tail_start_id": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"messageID",
"type",
"auto"
],
"additionalProperties": false
},
"Part": {
"anyOf": [
{
"$ref": "#/components/schemas/TextPart"
},
{
"$ref": "#/components/schemas/SubtaskPart"
},
{
"$ref": "#/components/schemas/ReasoningPart"
},
{
"$ref": "#/components/schemas/FilePart"
},
{
"$ref": "#/components/schemas/ToolPart"
},
{
"$ref": "#/components/schemas/StepStartPart"
},
{
"$ref": "#/components/schemas/StepFinishPart"
},
{
"$ref": "#/components/schemas/SnapshotPart"
},
{
"$ref": "#/components/schemas/PatchPart"
},
{
"$ref": "#/components/schemas/AgentPart"
},
{
"$ref": "#/components/schemas/RetryPart"
},
{
"$ref": "#/components/schemas/CompactionPart"
}
]
},
"message.part.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"message.part.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"part": {
"$ref": "#/components/schemas/Part"
},
"time": {
"type": "number"
}
},
"required": [
"sessionID",
"part",
"time"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"message.part.removed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"message.part.removed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"partID": {
"type": "string",
"allOf": [
{
"pattern": "^prt"
}
]
}
},
"required": [
"sessionID",
"messageID",
"partID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.execution.settled": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.execution.settled"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"outcome": {
"type": "string",
"enum": [
"success",
"failure",
"interrupted"
]
},
"error": {
"$ref": "#/components/schemas/Session.Error.Unknown"
}
},
"required": [
"timestamp",
"sessionID",
"outcome"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.text.delta": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.text.delta"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"textID": {
"type": "string"
},
"delta": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"textID",
"delta"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.reasoning.delta": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.reasoning.delta"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"reasoningID": {
"type": "string"
},
"delta": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"reasoningID",
"delta"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.tool.input.delta": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.tool.input.delta"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"assistantMessageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"callID": {
"type": "string"
},
"delta": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"assistantMessageID",
"callID",
"delta"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.next.compaction.delta": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.next.compaction.delta"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"timestamp": {
"type": "number"
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg_"
}
]
},
"text": {
"type": "string"
}
},
"required": [
"timestamp",
"sessionID",
"messageID",
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"file.edited": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"file.edited"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"file": {
"type": "string"
}
},
"required": [
"file"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"reference.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"reference.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"permission.v2.asked": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"permission.v2.asked"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"action": {
"type": "string"
},
"resources": {
"type": "array",
"items": {
"type": "string"
}
},
"save": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object"
},
"source": {
"$ref": "#/components/schemas/PermissionV2.Source"
}
},
"required": [
"id",
"sessionID",
"action",
"resources"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"permission.v2.replied": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"permission.v2.replied"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"requestID": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"reply": {
"$ref": "#/components/schemas/PermissionV2.Reply"
}
},
"required": [
"sessionID",
"requestID",
"reply"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"plugin.added": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"plugin.added"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"project.directories.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"project.directories.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"projectID": {
"type": "string"
}
},
"required": [
"projectID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"command.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"command.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"skill.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"skill.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"file.watcher.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"file.watcher.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"event": {
"type": "string",
"enum": [
"add",
"change",
"unlink"
]
}
},
"required": [
"file",
"event"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"Pty": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"title": {
"type": "string"
},
"command": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "string"
}
},
"cwd": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"running",
"exited"
]
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exitCode": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"id",
"title",
"command",
"args",
"cwd",
"status",
"pid"
],
"additionalProperties": false
},
"pty.created": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"pty.created"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"info": {
"$ref": "#/components/schemas/Pty"
}
},
"required": [
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"pty.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"pty.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"info": {
"$ref": "#/components/schemas/Pty"
}
},
"required": [
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"pty.exited": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"pty.exited"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
},
"exitCode": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"id",
"exitCode"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"pty.deleted": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"pty.deleted"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^pty"
}
]
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"Shell": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"status": {
"type": "string",
"enum": [
"running",
"exited",
"timeout",
"killed"
]
},
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"shell": {
"type": "string"
},
"file": {
"type": "string"
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exit": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"started": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"completed": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
}
},
"required": [
"started"
],
"additionalProperties": false
}
},
"required": [
"id",
"status",
"command",
"cwd",
"shell",
"file",
"metadata",
"time"
],
"additionalProperties": false
},
"shell.created": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"shell.created"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"info": {
"$ref": "#/components/schemas/Shell"
}
},
"required": [
"info"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"shell.exited": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"shell.exited"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"exit": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"status": {
"type": "string",
"enum": [
"running",
"exited",
"timeout",
"killed"
]
}
},
"required": [
"id",
"status"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"shell.deleted": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"shell.deleted"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
}
},
"required": [
"id"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"QuestionV2.Option": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Display text (1-5 words, concise)"
},
"description": {
"type": "string",
"description": "Explanation of choice"
}
},
"required": [
"label",
"description"
],
"additionalProperties": false
},
"QuestionV2.Info": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "Complete question"
},
"header": {
"type": "string",
"description": "Very short label (max 30 chars)"
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Option"
},
"description": "Available choices"
},
"multiple": {
"type": "boolean"
},
"custom": {
"type": "boolean"
}
},
"required": [
"question",
"header",
"options"
],
"additionalProperties": false
},
"QuestionV2.Tool": {
"type": "object",
"properties": {
"messageID": {
"type": "string"
},
"callID": {
"type": "string"
}
},
"required": [
"messageID",
"callID"
],
"additionalProperties": false
},
"question.v2.asked": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"question.v2.asked"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"questions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Info"
},
"description": "Questions to ask"
},
"tool": {
"$ref": "#/components/schemas/QuestionV2.Tool"
}
},
"required": [
"id",
"sessionID",
"questions"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"QuestionV2.Answer": {
"type": "array",
"items": {
"type": "string"
}
},
"question.v2.replied": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"question.v2.replied"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"requestID": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"answers": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Answer"
}
}
},
"required": [
"sessionID",
"requestID",
"answers"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"question.v2.rejected": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"question.v2.rejected"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"requestID": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
}
},
"required": [
"sessionID",
"requestID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"Form.Metadata1": {
"type": "object"
},
"Form.When1": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"op": {
"type": "string",
"enum": [
"eq",
"neq"
]
},
"value": {
"anyOf": [
{
"type": "string"
},
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "boolean"
}
]
}
},
"required": [
"key",
"op",
"value"
],
"additionalProperties": false
},
"Form.StringField1": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When1"
}
},
"type": {
"type": "string",
"enum": [
"string"
]
},
"format": {
"type": "string",
"enum": [
"email",
"uri",
"date",
"date-time"
]
},
"minLength": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"maxLength": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"pattern": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"default": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.Option"
}
},
"custom": {
"type": "boolean"
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.NumberField1": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When1"
}
},
"type": {
"type": "string",
"enum": [
"number"
]
},
"minimum": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"maximum": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"default": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.IntegerField1": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When1"
}
},
"type": {
"type": "string",
"enum": [
"integer"
]
},
"minimum": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"maximum": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
"default": {
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.BooleanField1": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When1"
}
},
"type": {
"type": "string",
"enum": [
"boolean"
]
},
"default": {
"type": "boolean"
}
},
"required": [
"key",
"type"
],
"additionalProperties": false
},
"Form.MultiselectField1": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"required": {
"type": "boolean"
},
"when": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.When1"
}
},
"type": {
"type": "string",
"enum": [
"multiselect"
]
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Form.Option"
}
},
"minItems": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"maxItems": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"custom": {
"type": "boolean"
},
"default": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"key",
"type",
"options"
],
"additionalProperties": false
},
"Form.FormInfo1": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"sessionID": {
"type": "string"
},
"title": {
"type": "string"
},
"metadata": {
"$ref": "#/components/schemas/Form.Metadata1"
},
"mode": {
"type": "string",
"enum": [
"form"
]
},
"fields": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.StringField1"
},
{
"$ref": "#/components/schemas/Form.NumberField1"
},
{
"$ref": "#/components/schemas/Form.IntegerField1"
},
{
"$ref": "#/components/schemas/Form.BooleanField1"
},
{
"$ref": "#/components/schemas/Form.MultiselectField1"
}
]
}
}
},
"required": [
"id",
"sessionID",
"mode",
"fields"
],
"additionalProperties": false
},
"Form.UrlInfo1": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"sessionID": {
"type": "string"
},
"title": {
"type": "string"
},
"metadata": {
"$ref": "#/components/schemas/Form.Metadata1"
},
"mode": {
"type": "string",
"enum": [
"url"
]
},
"url": {
"type": "string"
}
},
"required": [
"id",
"sessionID",
"mode",
"url"
],
"additionalProperties": false
},
"form.created": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"form.created"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"form": {
"anyOf": [
{
"$ref": "#/components/schemas/Form.FormInfo1"
},
{
"$ref": "#/components/schemas/Form.UrlInfo1"
}
]
}
},
"required": [
"form"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"Form.Value1": {
"anyOf": [
{
"type": "string"
},
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "boolean"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"Form.Answer1": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Form.Value1"
}
},
"form.replied": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"form.replied"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"sessionID": {
"type": "string"
},
"answer": {
"$ref": "#/components/schemas/Form.Answer1"
}
},
"required": [
"id",
"sessionID",
"answer"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"form.cancelled": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"form.cancelled"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^frm_"
}
]
},
"sessionID": {
"type": "string"
}
},
"required": [
"id",
"sessionID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"Todo": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Brief description of the task"
},
"status": {
"type": "string",
"description": "Current status of the task: pending, in_progress, completed, cancelled"
},
"priority": {
"type": "string",
"description": "Priority level of the task: high, medium, low"
}
},
"required": [
"content",
"status",
"priority"
],
"additionalProperties": false
},
"todo.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"todo.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"todos": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Todo"
}
}
},
"required": [
"sessionID",
"todos"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"SessionStatus": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"idle"
]
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"retry"
]
},
"attempt": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"message": {
"type": "string"
},
"action": {
"type": "object",
"properties": {
"reason": {
"type": "string"
},
"provider": {
"type": "string"
},
"title": {
"type": "string"
},
"message": {
"type": "string"
},
"label": {
"type": "string"
},
"link": {
"type": "string"
}
},
"required": [
"reason",
"provider",
"title",
"message",
"label"
],
"additionalProperties": false
},
"next": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"type",
"attempt",
"message",
"next"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"busy"
]
}
},
"required": [
"type"
],
"additionalProperties": false
}
]
},
"session.status": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.status"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"status": {
"$ref": "#/components/schemas/SessionStatus"
}
},
"required": [
"sessionID",
"status"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.idle": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.idle"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
}
},
"required": [
"sessionID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"tui.prompt.append": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"tui.prompt.append"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": [
"text"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"tui.command.execute": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"tui.command.execute"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"command": {
"anyOf": [
{
"type": "string",
"enum": [
"session.list",
"session.new",
"session.share",
"session.interrupt",
"session.background",
"session.compact",
"session.page.up",
"session.page.down",
"session.line.up",
"session.line.down",
"session.half.page.up",
"session.half.page.down",
"session.first",
"session.last",
"prompt.clear",
"prompt.submit",
"agent.cycle"
]
},
{
"type": "string"
}
]
}
},
"required": [
"command"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"tui.toast.show": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"tui.toast.show"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"message": {
"type": "string"
},
"variant": {
"type": "string",
"enum": [
"info",
"success",
"warning",
"error"
]
},
"duration": {
"anyOf": [
{
"type": "integer",
"allOf": [
{
"exclusiveMinimum": 0
}
]
},
{
"type": "null"
}
]
}
},
"required": [
"message",
"variant"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"tui.session.select": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"tui.session.select"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses",
"description": "Session ID to navigate to"
}
]
}
},
"required": [
"sessionID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"installation.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"installation.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"version": {
"type": "string"
}
},
"required": [
"version"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"installation.update-available": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"installation.update-available"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"version": {
"type": "string"
}
},
"required": [
"version"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"vcs.branch.updated": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"vcs.branch.updated"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"branch": {
"type": "string"
}
},
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"mcp.status.changed": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"mcp.status.changed"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"server": {
"type": "string"
}
},
"required": [
"server"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"permission.asked": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"permission.asked"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"permission": {
"type": "string"
},
"patterns": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object"
},
"always": {
"type": "array",
"items": {
"type": "string"
}
},
"tool": {
"anyOf": [
{
"type": "object",
"properties": {
"messageID": {
"type": "string"
},
"callID": {
"type": "string"
}
},
"required": [
"messageID",
"callID"
],
"additionalProperties": false
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"permission",
"patterns",
"metadata",
"always"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"permission.replied": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"permission.replied"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"requestID": {
"type": "string",
"allOf": [
{
"pattern": "^per"
}
]
},
"reply": {
"type": "string",
"enum": [
"once",
"always",
"reject"
]
}
},
"required": [
"sessionID",
"requestID",
"reply"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"QuestionOption": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Display text (1-5 words, concise)"
},
"description": {
"type": "string",
"description": "Explanation of choice"
}
},
"required": [
"label",
"description"
],
"additionalProperties": false
},
"QuestionInfo": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "Complete question"
},
"header": {
"type": "string",
"description": "Very short label (max 30 chars)"
},
"options": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionOption"
},
"description": "Available choices"
},
"multiple": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Allow selecting multiple choices"
},
"custom": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Allow typing a custom answer (default: true)"
}
},
"required": [
"question",
"header",
"options"
],
"additionalProperties": false
},
"QuestionTool": {
"type": "object",
"properties": {
"messageID": {
"type": "string",
"allOf": [
{
"pattern": "^msg"
}
]
},
"callID": {
"type": "string"
}
},
"required": [
"messageID",
"callID"
],
"additionalProperties": false
},
"question.asked": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"question.asked"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"questions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionInfo"
},
"description": "Questions to ask"
},
"tool": {
"anyOf": [
{
"$ref": "#/components/schemas/QuestionTool"
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"sessionID",
"questions"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"QuestionAnswer": {
"type": "array",
"items": {
"type": "string"
}
},
"question.replied": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"question.replied"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"requestID": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"answers": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionAnswer"
}
}
},
"required": [
"sessionID",
"requestID",
"answers"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"question.rejected": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"question.rejected"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"requestID": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
}
},
"required": [
"sessionID",
"requestID"
],
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"session.error": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"type": "object"
},
"type": {
"type": "string",
"enum": [
"session.error"
]
},
"durable": {
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
"location": {
"$ref": "#/components/schemas/Location.Ref"
},
"data": {
"type": "object",
"properties": {
"sessionID": {
"anyOf": [
{
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
{
"type": "null"
}
]
},
"error": {
"anyOf": [
{
"anyOf": [
{
"$ref": "#/components/schemas/ProviderAuthError"
},
{
"$ref": "#/components/schemas/UnknownError1"
},
{
"$ref": "#/components/schemas/MessageOutputLengthError"
},
{
"$ref": "#/components/schemas/MessageAbortedError"
},
{
"$ref": "#/components/schemas/StructuredOutputError"
},
{
"$ref": "#/components/schemas/ContextOverflowError"
},
{
"$ref": "#/components/schemas/ContentFilterError"
},
{
"$ref": "#/components/schemas/APIError"
}
]
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"V2Event.server.connected": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^evt_"
}
]
},
"metadata": {
"anyOf": [
{
"type": "object"
},
{
"type": "null"
}
]
},
"durable": {
"anyOf": [
{
"type": "object",
"properties": {
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"version": {
"type": "integer",
"allOf": [
{
"minimum": 1
}
]
}
},
"required": [
"aggregateID",
"seq",
"version"
],
"additionalProperties": false
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"$ref": "#/components/schemas/Location.Ref"
},
{
"type": "null"
}
]
},
"type": {
"type": "string",
"enum": [
"server.connected"
]
},
"data": {
"anyOf": [
{
"type": "object"
},
{
"type": "array"
}
]
}
},
"required": [
"id",
"type",
"data"
],
"additionalProperties": false
},
"V2Event": {
"anyOf": [
{
"$ref": "#/components/schemas/models-dev.refreshed"
},
{
"$ref": "#/components/schemas/integration.updated"
},
{
"$ref": "#/components/schemas/integration.connection.updated"
},
{
"$ref": "#/components/schemas/catalog.updated"
},
{
"$ref": "#/components/schemas/agent.updated"
},
{
"$ref": "#/components/schemas/session.created"
},
{
"$ref": "#/components/schemas/session.updated"
},
{
"$ref": "#/components/schemas/session.deleted"
},
{
"$ref": "#/components/schemas/message.updated"
},
{
"$ref": "#/components/schemas/message.removed"
},
{
"$ref": "#/components/schemas/message.part.updated"
},
{
"$ref": "#/components/schemas/message.part.removed"
},
{
"$ref": "#/components/schemas/session.next.agent.switched"
},
{
"$ref": "#/components/schemas/session.next.model.switched"
},
{
"$ref": "#/components/schemas/session.next.moved"
},
{
"$ref": "#/components/schemas/session.next.renamed"
},
{
"$ref": "#/components/schemas/session.next.forked"
},
{
"$ref": "#/components/schemas/session.next.prompted"
},
{
"$ref": "#/components/schemas/session.next.prompt.admitted"
},
{
"$ref": "#/components/schemas/session.next.execution.settled"
},
{
"$ref": "#/components/schemas/session.next.context.updated"
},
{
"$ref": "#/components/schemas/session.next.synthetic"
},
{
"$ref": "#/components/schemas/session.next.skill.activated"
},
{
"$ref": "#/components/schemas/session.next.shell.started"
},
{
"$ref": "#/components/schemas/session.next.shell.ended"
},
{
"$ref": "#/components/schemas/session.next.step.started"
},
{
"$ref": "#/components/schemas/session.next.step.ended"
},
{
"$ref": "#/components/schemas/session.next.step.failed"
},
{
"$ref": "#/components/schemas/session.next.text.started"
},
{
"$ref": "#/components/schemas/session.next.text.delta"
},
{
"$ref": "#/components/schemas/session.next.text.ended"
},
{
"$ref": "#/components/schemas/session.next.reasoning.started"
},
{
"$ref": "#/components/schemas/session.next.reasoning.delta"
},
{
"$ref": "#/components/schemas/session.next.reasoning.ended"
},
{
"$ref": "#/components/schemas/session.next.tool.input.started"
},
{
"$ref": "#/components/schemas/session.next.tool.input.delta"
},
{
"$ref": "#/components/schemas/session.next.tool.input.ended"
},
{
"$ref": "#/components/schemas/session.next.tool.called"
},
{
"$ref": "#/components/schemas/session.next.tool.progress"
},
{
"$ref": "#/components/schemas/session.next.tool.success"
},
{
"$ref": "#/components/schemas/session.next.tool.failed"
},
{
"$ref": "#/components/schemas/session.next.retried"
},
{
"$ref": "#/components/schemas/session.next.compaction.started"
},
{
"$ref": "#/components/schemas/session.next.compaction.delta"
},
{
"$ref": "#/components/schemas/session.next.compaction.ended"
},
{
"$ref": "#/components/schemas/session.next.revert.staged"
},
{
"$ref": "#/components/schemas/session.next.revert.cleared"
},
{
"$ref": "#/components/schemas/session.next.revert.committed"
},
{
"$ref": "#/components/schemas/file.edited"
},
{
"$ref": "#/components/schemas/reference.updated"
},
{
"$ref": "#/components/schemas/permission.v2.asked"
},
{
"$ref": "#/components/schemas/permission.v2.replied"
},
{
"$ref": "#/components/schemas/plugin.added"
},
{
"$ref": "#/components/schemas/project.directories.updated"
},
{
"$ref": "#/components/schemas/command.updated"
},
{
"$ref": "#/components/schemas/skill.updated"
},
{
"$ref": "#/components/schemas/file.watcher.updated"
},
{
"$ref": "#/components/schemas/pty.created"
},
{
"$ref": "#/components/schemas/pty.updated"
},
{
"$ref": "#/components/schemas/pty.exited"
},
{
"$ref": "#/components/schemas/pty.deleted"
},
{
"$ref": "#/components/schemas/shell.created"
},
{
"$ref": "#/components/schemas/shell.exited"
},
{
"$ref": "#/components/schemas/shell.deleted"
},
{
"$ref": "#/components/schemas/question.v2.asked"
},
{
"$ref": "#/components/schemas/question.v2.replied"
},
{
"$ref": "#/components/schemas/question.v2.rejected"
},
{
"$ref": "#/components/schemas/form.created"
},
{
"$ref": "#/components/schemas/form.replied"
},
{
"$ref": "#/components/schemas/form.cancelled"
},
{
"$ref": "#/components/schemas/todo.updated"
},
{
"$ref": "#/components/schemas/session.status"
},
{
"$ref": "#/components/schemas/session.idle"
},
{
"$ref": "#/components/schemas/tui.prompt.append"
},
{
"$ref": "#/components/schemas/tui.command.execute"
},
{
"$ref": "#/components/schemas/tui.toast.show"
},
{
"$ref": "#/components/schemas/tui.session.select"
},
{
"$ref": "#/components/schemas/installation.updated"
},
{
"$ref": "#/components/schemas/installation.update-available"
},
{
"$ref": "#/components/schemas/vcs.branch.updated"
},
{
"$ref": "#/components/schemas/mcp.status.changed"
},
{
"$ref": "#/components/schemas/permission.asked"
},
{
"$ref": "#/components/schemas/permission.replied"
},
{
"$ref": "#/components/schemas/question.asked"
},
{
"$ref": "#/components/schemas/question.replied"
},
{
"$ref": "#/components/schemas/question.rejected"
},
{
"$ref": "#/components/schemas/session.error"
},
{
"$ref": "#/components/schemas/V2Event.server.connected"
}
]
},
"V2EventStream": {
"type": "string",
"contentSchema": {
"$ref": "#/components/schemas/V2Event"
},
"contentMediaType": "application/json"
},
"EventLog.Hint": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"log.hint"
]
},
"aggregateID": {
"type": "string"
},
"seq": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
}
},
"required": [
"type",
"aggregateID",
"seq"
],
"additionalProperties": false,
"description": "Payload-free change hint: the aggregate's durable log advanced to at least seq. Hints coalesce under backpressure (latest per aggregate) and are never a delivery guarantee."
},
"EventLog.SweepRequired": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"log.sweep_required"
]
}
},
"required": [
"type"
],
"additionalProperties": false,
"description": "Hints may have been lost; treat every aggregate as potentially dirty and recover via bounded sweep plus durable log reads. Emitted first on every (re)subscribe."
},
"EventLog.Change": {
"anyOf": [
{
"$ref": "#/components/schemas/EventLog.Hint"
},
{
"$ref": "#/components/schemas/EventLog.SweepRequired"
}
]
},
"EventLog.ChangeStream": {
"type": "string",
"contentSchema": {
"$ref": "#/components/schemas/EventLog.Change"
},
"contentMediaType": "application/json"
},
"PtyNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"PtyNotFoundError"
]
},
"ptyID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"ptyID",
"message"
],
"additionalProperties": false
},
"PtyTicket.ConnectToken": {
"type": "object",
"properties": {
"ticket": {
"type": "string"
},
"expires_in": {
"type": "integer",
"allOf": [
{
"exclusiveMinimum": 0
}
]
}
},
"required": [
"ticket",
"expires_in"
],
"additionalProperties": false
},
"ForbiddenError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"ForbiddenError"
]
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"message"
],
"additionalProperties": false
},
"Shell1": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^sh_"
}
]
},
"status": {
"type": "string",
"enum": [
"running",
"exited",
"timeout",
"killed"
]
},
"command": {
"type": "string"
},
"cwd": {
"type": "string"
},
"shell": {
"type": "string"
},
"file": {
"type": "string"
},
"pid": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"exit": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"metadata": {
"type": "object"
},
"time": {
"type": "object",
"properties": {
"started": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
},
"completed": {
"anyOf": [
{
"anyOf": [
{
"type": "number"
},
{
"type": "string",
"enum": [
"NaN"
]
},
{
"type": "string",
"enum": [
"Infinity"
]
},
{
"type": "string",
"enum": [
"-Infinity"
]
}
]
},
{
"type": "string",
"enum": [
"Infinity",
"-Infinity",
"NaN"
]
}
]
}
},
"required": [
"started"
],
"additionalProperties": false
}
},
"required": [
"id",
"status",
"command",
"cwd",
"shell",
"file",
"metadata",
"time"
],
"additionalProperties": false
},
"ShellNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"ShellNotFoundError"
]
},
"id": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"id",
"message"
],
"additionalProperties": false
},
"QuestionV2.Request": {
"type": "object",
"properties": {
"id": {
"type": "string",
"allOf": [
{
"pattern": "^que"
}
]
},
"sessionID": {
"type": "string",
"allOf": [
{
"pattern": "^ses"
}
]
},
"questions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Info"
},
"description": "Questions to ask"
},
"tool": {
"$ref": "#/components/schemas/QuestionV2.Tool"
}
},
"required": [
"id",
"sessionID",
"questions"
],
"additionalProperties": false
},
"QuestionV2.Reply": {
"type": "object",
"properties": {
"answers": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuestionV2.Answer"
},
"description": "User answers in order of questions (each answer is an array of selected labels)"
}
},
"required": [
"answers"
],
"additionalProperties": false
},
"QuestionNotFoundError": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": [
"QuestionNotFoundError"
]
},
"requestID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": [
"_tag",
"requestID",
"message"
],
"additionalProperties": false
},
"Reference.LocalSource": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"local"
]
},
"path": {
"type": "string"
},
"description": {
"type": "string"
},
"hidden": {
"type": "boolean"
}
},
"required": [
"type",
"path"
],
"additionalProperties": false
},
"Reference.GitSource": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"git"
]
},
"repository": {
"type": "string"
},
"branch": {
"type": "string"
},
"description": {
"type": "string"
},
"hidden": {
"type": "boolean"
}
},
"required": [
"type",
"repository"
],
"additionalProperties": false
},
"Reference.Source": {
"anyOf": [
{
"$ref": "#/components/schemas/Reference.LocalSource"
},
{
"$ref": "#/components/schemas/Reference.GitSource"
}
]
},
"Reference.Info": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"description": {
"type": "string"
},
"hidden": {
"type": "boolean"
},
"source": {
"$ref": "#/components/schemas/Reference.Source"
}
},
"required": [
"name",
"path",
"source"
],
"additionalProperties": false
},
"ProjectCopy.Copy": {
"type": "object",
"properties": {
"directory": {
"type": "string"
}
},
"required": [
"directory"
],
"additionalProperties": false
},
"ProjectCopyError": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"ProjectCopyError"
]
},
"data": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"forceRequired": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"required": [
"message"
],
"additionalProperties": false
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
},
"Vcs.FileStatus": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"additions": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"deletions": {
"type": "integer",
"allOf": [
{
"minimum": 0
}
]
},
"status": {
"type": "string",
"enum": [
"added",
"deleted",
"modified"
]
}
},
"required": [
"file",
"additions",
"deletions",
"status"
],
"additionalProperties": false
},
"Vcs.Mode": {
"type": "string",
"enum": [
"working",
"branch"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": [
{
"name": "server.health"
},
{
"name": "server.location"
},
{
"name": "server.agent"
},
{
"name": "plugins",
"description": "Experimental plugin routes."
},
{
"name": "sessions",
"description": "Experimental session routes."
},
{
"name": "messages",
"description": "Experimental message routes."
},
{
"name": "models",
"description": "Experimental model routes."
},
{
"name": "generate",
"description": "Experimental one-shot generation routes."
},
{
"name": "providers",
"description": "Experimental provider routes."
},
{
"name": "integrations",
"description": "Integration discovery and authentication routes."
},
{
"name": "mcp",
"description": "MCP server status routes."
},
{
"name": "server.credential"
},
{
"name": "projects",
"description": "Location-scoped project routes."
},
{
"name": "forms",
"description": "Session form routes."
},
{
"name": "permissions",
"description": "Experimental permission routes."
},
{
"name": "filesystem",
"description": "Experimental location-scoped filesystem routes."
},
{
"name": "commands",
"description": "Experimental command routes."
},
{
"name": "skills",
"description": "Experimental skill routes."
},
{
"name": "events",
"description": "Experimental event stream routes."
},
{
"name": "pty",
"description": "Experimental location-scoped PTY routes."
},
{
"name": "shell",
"description": "Experimental location-scoped shell command routes."
},
{
"name": "session questions",
"description": "Experimental session question routes."
},
{
"name": "reference",
"description": "Location-scoped project references."
},
{
"name": "projectCopy",
"description": "Project copy management routes."
},
{
"name": "vcs",
"description": "Location-scoped version control routes."
}
]
}
+958
View File
@@ -0,0 +1,958 @@
import { describe, expect, test } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { CodeMode, OpenAPI, Tool } from "../src/index.js"
import { inputTypeScript, outputTypeScript } from "../src/tool-schema.js"
const baseUrl = "http://localhost:4096"
type Document = OpenAPI.Document
type Recorded = {
readonly method: string
readonly url: string
readonly headers: Record<string, string>
readonly body: unknown
}
const opencodeSpec = async (): Promise<Document> => {
return Bun.file(new URL("./fixtures/opencode-v2-openapi.json", import.meta.url)).json() as Promise<Document>
}
const happyPathSpec = async (): Promise<Document> => {
return Bun.file(new URL("./fixtures/openapi-happy-path.json", import.meta.url)).json() as Promise<Document>
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
const toolAt = (tools: unknown, name: string) =>
name.split(".").reduce<unknown>((current, segment) => (isRecord(current) ? current[segment] : undefined), tools)
const recordingClient = (respond: (request: HttpClientRequest.HttpClientRequest) => Response) => {
const requests: Array<Recorded> = []
const layer = Layer.succeed(HttpClient.HttpClient)(
HttpClient.make((request) =>
Effect.gen(function* () {
const body =
request.body._tag === "Uint8Array" ? JSON.parse(new TextDecoder().decode(request.body.body)) : undefined
const url = Option.map(HttpClientRequest.toUrl(request), (resolved) => resolved.toString())
requests.push({
method: request.method,
url: Option.getOrElse(url, () => request.url),
headers: { ...request.headers },
body,
})
return HttpClientResponse.fromWeb(request, respond(request))
}),
),
)
return { requests, layer }
}
const json = (value: unknown, status = 200) =>
new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } })
const singleOperation = (operation: Record<string, unknown>, method = "get"): Document => ({
openapi: "3.1.0",
paths: { "/test": { [method]: { operationId: "test", responses: { 200: { description: "Success" } }, ...operation } } },
})
describe("OpenAPI.fromSpec", () => {
test("covers a representative API from generation through execution", async () => {
const resolutions: Array<string> = []
const client = recordingClient((request) => {
const url = Option.getOrElse(HttpClientRequest.toUrl(request), () => new URL(request.url))
if (request.method === "POST") {
return new Response(
JSON.stringify({ id: "user-2", name: "Grace", email: "grace@example.test", role: "admin" }),
{ status: 201, headers: { "content-type": "application/vnd.example+json" } },
)
}
if (request.method === "DELETE") return new Response(null, { status: 204 })
if (url.pathname === "/search") {
return new Response("2 matches", { headers: { "content-type": "text/plain" } })
}
return json({ id: "user-1", name: "Ada", email: "ada@example.test", role: "member" })
})
const api = OpenAPI.fromSpec({
spec: await happyPathSpec(),
baseUrl,
auth: {
resolve: ({ name }) => {
resolutions.push(name)
return Effect.succeed(
name === "BearerAuth"
? { type: "bearer", token: "bearer-secret" }
: { type: "apiKey", value: "api-secret" },
)
},
},
})
const get = toolAt(api.tools, "users.get")
const create = toolAt(api.tools, "users.create")
const search = toolAt(api.tools, "search.run")
const remove = toolAt(api.tools, "users.remove")
expect(api.skipped).toEqual([])
if (!Tool.isDefinition(get) || !Tool.isDefinition(create) || !Tool.isDefinition(search) || !Tool.isDefinition(remove)) {
throw new Error("happy-path fixture did not generate every operation")
}
expect(inputTypeScript(get)).toBe(
'{ userId: string; include?: Array<string>; verbose?: boolean; "X-Trace-ID"?: string }',
)
expect(inputTypeScript(create)).toBe('{ name: string; email: string; role?: "admin" | "member" }')
expect(inputTypeScript(search)).toBe("{ filter?: { query: string; page?: number }; tags?: Array<string> }")
expect(inputTypeScript(remove)).toBe("{ userId: string }")
expect(outputTypeScript(get)).toContain("id: string")
expect(outputTypeScript(create)).toContain('role?: "admin" | "member"')
expect(outputTypeScript(search)).toBe("string")
expect(outputTypeScript(remove)).toBe("null")
const result = await Effect.runPromise(
CodeMode.make({ tools: { api: api.tools } })
.execute(`
const user = await tools.api.users.get({
userId: "user-1",
include: ["profile", "permissions"],
verbose: true,
"X-Trace-ID": "trace-1",
})
const created = await tools.api.users.create({
name: "Grace",
email: "grace@example.test",
role: "admin",
})
const summary = await tools.api.search.run({
filter: { query: "effect", page: 2 },
tags: ["typescript", "runtime"],
})
const removed = await tools.api.users.remove({ userId: "user-1" })
return { user, created, summary, removed }
`)
.pipe(Effect.provide(client.layer)),
)
expect(result).toMatchObject({
ok: true,
value: {
user: { id: "user-1", name: "Ada" },
created: { id: "user-2", name: "Grace" },
summary: "2 matches",
removed: null,
},
})
expect(resolutions).toEqual(["BearerAuth", "ApiKey", "BearerAuth"])
expect(client.requests).toHaveLength(4)
const getUrl = new URL(client.requests[0]!.url)
expect(getUrl.pathname).toBe("/users/user-1")
expect(getUrl.searchParams.get("include")).toBe("profile,permissions")
expect(getUrl.searchParams.get("verbose")).toBe("true")
expect(client.requests[0]!.headers["x-trace-id"]).toBe("trace-1")
expect(client.requests[0]!.headers.authorization).toBe("Bearer bearer-secret")
const createUrl = new URL(client.requests[1]!.url)
expect(createUrl.searchParams.get("api_key")).toBe("api-secret")
expect(client.requests[1]!.body).toEqual({ name: "Grace", email: "grace@example.test", role: "admin" })
const searchUrl = new URL(client.requests[2]!.url)
expect(searchUrl.searchParams.get("filter[query]")).toBe("effect")
expect(searchUrl.searchParams.get("filter[page]")).toBe("2")
expect(searchUrl.searchParams.getAll("tags")).toEqual(["typescript", "runtime"])
expect(client.requests[2]!.headers.authorization).toBeUndefined()
expect(new URL(client.requests[3]!.url).pathname).toBe("/users/user-1")
expect(client.requests[3]!.headers.authorization).toBe("Bearer bearer-secret")
})
test("converts representative opencode operations into the expected tool shape", async () => {
const spec = await opencodeSpec()
const result = OpenAPI.fromSpec({ spec, baseUrl })
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(3)
expect(result.skipped).toContainEqual({
method: "GET",
path: "/api/fs/read/*",
reason: "binary responses are not supported",
})
expect(toolAt(result.tools, "v2.health.get")).not.toBeUndefined()
expect(toolAt(result.tools, "v2.session.get")).not.toBeUndefined()
expect(toolAt(result.tools, "v2.session.create")).not.toBeUndefined()
const sessionGet = toolAt(result.tools, "v2.session.get")
expect(Tool.isDefinition(sessionGet)).toBe(true)
if (!Tool.isDefinition(sessionGet)) throw new Error("v2.session.get was not generated")
expect(inputTypeScript(sessionGet)).toBe("{ sessionID: string }")
expect(outputTypeScript(sessionGet)).toContain("id: string")
expect(outputTypeScript(sessionGet)).toContain("additions: number")
const switchAgent = toolAt(result.tools, "v2.session.switchAgent")
expect(Tool.isDefinition(switchAgent)).toBe(true)
if (!Tool.isDefinition(switchAgent)) throw new Error("v2.session.switchAgent was not generated")
expect(inputTypeScript(switchAgent)).toBe("{ sessionID: string; agent: string }")
const contextEntryPut = toolAt(result.tools, "v2.session.contextEntry.put")
expect(Tool.isDefinition(contextEntryPut)).toBe(true)
if (!Tool.isDefinition(contextEntryPut)) throw new Error("v2.session.contextEntry.put was not generated")
expect(inputTypeScript(contextEntryPut)).toBe("{ sessionID: string; key: string; value: unknown }")
expect(toolAt(result.tools, "v2_session_context_entry_put_2")).toBeUndefined()
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.event.changes")).toBeUndefined()
expect(toolAt(result.tools, "v2.fs.read")).toBeUndefined()
expect(toolAt(result.tools, "v2.pty.connectToken")).not.toBeUndefined()
})
test("preserves operation path sanitization and collision handling", () => {
const response = { responses: { 200: { description: "Success" } } }
const result = OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.1.0",
paths: {
"/first": { get: { ...response, operationId: "group.item" } },
"/second": { get: { ...response, operationId: "group.item" } },
"/third": { get: { ...response, operationId: "group..other" } },
},
},
})
expect(Tool.isDefinition(toolAt(result.tools, "group.item"))).toBe(true)
expect(Tool.isDefinition(toolAt(result.tools, "group_item_2"))).toBe(true)
expect(Tool.isDefinition(toolAt(result.tools, "group.operation.other"))).toBe(true)
})
test("synthesizes flat operation IDs from methods and paths", () => {
const response = { responses: { 200: { description: "Success" } } }
const tools = OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.1.0",
paths: {
"/users": { get: response, post: response },
"/users/{id}": { get: response, patch: response, delete: response },
"/organizations/{organizationId}/users/{id}": { get: response },
},
},
}).tools
for (const path of [
"getUsers",
"postUsers",
"getUsersById",
"patchUsersById",
"deleteUsersById",
"getOrganizationsByOrganizationidUsersById",
]) {
expect(Tool.isDefinition(toolAt(tools, path))).toBe(true)
}
})
test("lets operation parameters override matching path parameters", () => {
const tool = toolAt(
OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.1.0",
paths: {
"/test": {
parameters: [{ name: "limit", in: "query", schema: { type: "string" } }],
get: {
operationId: "test",
parameters: [{ name: "limit", in: "query", required: true, schema: { type: "number" } }],
responses: { 200: { description: "Success" } },
},
},
},
},
}).tools,
"test",
)
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
expect(inputTypeScript(tool)).toBe("{ limit: number }")
})
test("normalizes OpenAPI 3.0 schemas with Effect", () => {
const result = OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.0.3",
paths: {
"/search": {
get: {
operationId: "search",
parameters: [
{
in: "query",
name: "value",
schema: { type: "string", nullable: true, minLength: 2 },
},
],
responses: { 200: { description: "Success" } },
},
},
},
},
})
const search = toolAt(result.tools, "search")
expect(Tool.isDefinition(search)).toBe(true)
if (!Tool.isDefinition(search)) throw new Error("search was not generated")
expect(inputTypeScript(search)).toBe("{ value?: string | null }")
const schema: unknown = search.input
const input = isRecord(schema) ? schema : {}
const properties = isRecord(input.properties) ? input.properties : {}
const value = isRecord(properties.value) ? properties.value : {}
expect(value.minLength).toBe(2)
})
test("preserves schema-local definitions alongside component definitions", () => {
const tool = toolAt(
OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.1.0",
paths: {
"/test": {
get: {
operationId: "test",
responses: {
200: {
description: "Success",
content: {
"application/json": {
schema: { $ref: "#/$defs/Local", $defs: { Local: { type: "string" } } },
},
},
},
},
},
},
},
components: { schemas: { Global: { type: "number" } } },
},
}).tools,
"test",
)
if (!Tool.isDefinition(tool) || !isRecord(tool.output)) throw new Error("test output was not generated")
expect(tool.output.$defs).toMatchObject({ Local: { type: "string" }, Global: { type: "number" } })
})
test("documents that the opencode fixture is unauthenticated", async () => {
const spec = await opencodeSpec()
const components = isRecord(spec.components) ? spec.components : {}
const result = OpenAPI.fromSpec({ spec, baseUrl })
expect(spec.security).toStrictEqual([])
expect(isRecord(components.securitySchemes) ? Object.keys(components.securitySchemes) : []).toStrictEqual([])
const health = toolAt(result.tools, "v2.health.get")
const healthInput = isRecord(health) ? health.input : undefined
expect(healthInput).toMatchObject({ type: "object", properties: {} })
const input = isRecord(healthInput) ? healthInput : {}
expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual([])
})
test("exposes real opencode operations through CodeMode discovery", async () => {
const { layer } = recordingClient(() => json({}))
const runtime = CodeMode.make({
tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools },
})
const result = await Effect.runPromise(
runtime
.execute(
`
return await tools.$codemode.search({ query: "global health", namespace: "opencode", limit: 1 })
`,
)
.pipe(Effect.provide(layer)),
)
expect(result).toMatchObject({ ok: true })
if (!result.ok) return
expect(result.value).toMatchObject({
items: [
{
path: "tools.opencode.v2.health.get",
description: "Check whether the API server is ready to accept requests.",
},
],
})
expect(JSON.stringify(result.value)).toContain("healthy: true")
})
test("invokes real opencode path parameters and JSON request bodies", async () => {
const { requests, layer } = recordingClient((request) => {
if (request.method === "GET") return json({ id: "ses_123" })
return json({ id: "ses_456" })
})
const runtime = CodeMode.make({
tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools },
})
const result = await Effect.runPromise(
runtime
.execute(
`
const existing = await tools.opencode.v2.session.get({ sessionID: "ses_123" })
const created = await tools.opencode.v2.session.create({ id: "ses_456" })
return { existing, created }
`,
)
.pipe(Effect.provide(layer)),
)
expect(result).toMatchObject({ ok: true })
expect(requests).toHaveLength(2)
expect(requests[0]).toMatchObject({ method: "GET", body: undefined })
expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_123")
expect(requests[1]).toMatchObject({
method: "POST",
url: "http://localhost:4096/api/session",
body: { id: "ses_456" },
})
})
test("serializes deep-object query parameters from the opencode fixture", async () => {
const client = recordingClient(() => json({ directory: "/tmp" }))
const location = toolAt(OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools, "v2.location.get")
if (!Tool.isDefinition(location)) throw new Error("v2.location.get was not generated")
await Effect.runPromise(
location.run({ location: { directory: "/tmp", workspace: "workspace-1" } }).pipe(Effect.provide(client.layer)),
)
const url = new URL(client.requests[0]!.url)
expect(url.searchParams.get("location[directory]")).toBe("/tmp")
expect(url.searchParams.get("location[workspace]")).toBe("workspace-1")
})
test("serializes supported simple and form parameter shapes", async () => {
const client = recordingClient(() => json({ ok: true }))
const result = OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.1.0",
paths: {
"/items/{keys}": {
get: {
operationId: "items",
parameters: [
{ name: "keys", in: "path", required: true, schema: { type: "array", items: { type: "string" } } },
{ name: "tags", in: "query", style: "form", explode: false, schema: { type: "array" } },
{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } },
{ name: "nullable", in: "query", required: true, schema: { type: ["string", "null"] } },
{ name: "constructor", in: "query", schema: { type: "string" } },
{ name: "meta", in: "header", style: "simple", explode: true, schema: { type: "object" } },
],
responses: { 200: { description: "Success" } },
},
},
},
},
})
const tool = toolAt(result.tools, "items")
if (!Tool.isDefinition(tool)) throw new Error("items was not generated")
await Effect.runPromise(
tool
.run({
keys: ["a!", "b*"],
tags: ["x", "y"],
filter: { state: "open", page: 2 },
nullable: null,
constructor_2: "safe",
meta: { a: "b", c: "d" },
})
.pipe(Effect.provide(client.layer)),
)
const url = new URL(client.requests[0]!.url)
expect(url.pathname).toBe("/items/a%21,b%2A")
expect(url.searchParams.get("tags")).toBe("x,y")
expect(url.searchParams.get("state")).toBe("open")
expect(url.searchParams.get("page")).toBe("2")
expect(url.searchParams.get("nullable")).toBe("null")
expect(url.searchParams.get("constructor")).toBe("safe")
expect(client.requests[0]!.headers.meta).toBe("a=b,c=d")
await expect(
Effect.runPromise(tool.run({ keys: [undefined] }).pipe(Effect.provide(client.layer))),
).rejects.toThrow("unsupported nested value")
})
test("skips unsupported parameter encodings and malformed security", () => {
const result = OpenAPI.fromSpec({
baseUrl,
spec: {
openapi: "3.1.0",
security: [{ bearer: [] }],
paths: {
"/cookie": {
get: {
operationId: "cookie",
parameters: [{ name: "session", in: "cookie", schema: { type: "string" } }],
responses: { 200: { description: "Success" } },
},
},
"/reserved": {
get: {
operationId: "reserved",
parameters: [{ name: "query", in: "query", allowReserved: true, schema: { type: "string" } }],
responses: { 200: { description: "Success" } },
},
},
"/invalid-style": {
get: {
operationId: "invalidStyle",
parameters: [{ name: "query", in: "query", style: 42, schema: { type: "string" } }],
responses: { 200: { description: "Success" } },
},
},
"/security": {
get: { operationId: "security", security: null, responses: { 200: { description: "Success" } } },
},
},
},
})
expect(result.tools).toEqual({})
expect(result.skipped.map((item) => item.reason)).toEqual([
"cookie parameter 'session' is not supported",
"parameter 'query' uses unsupported allowReserved encoding",
"parameter 'query' has an invalid style",
"security declaration is not an array",
])
})
test("fails closed on prototype-named missing security schemes", () => {
const result = OpenAPI.fromSpec({
baseUrl,
spec: singleOperation({ security: [JSON.parse('{"__proto__":[]}')] }),
})
expect(result.tools).toEqual({})
expect(result.skipped[0]?.reason).toBe("security requirement references missing or malformed scheme: __proto__")
})
test("resolves bearer authentication without exposing it as input", async () => {
const contexts: Array<Parameters<OpenAPI.AuthResolver>[0]> = []
const client = recordingClient(() => json({ ok: true }))
const spec = {
...singleOperation({ operationId: undefined }),
security: [{ bearer: [] }],
components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } },
} satisfies Document
const tool = toolAt(
OpenAPI.fromSpec({
baseUrl,
spec,
auth: {
resolve: (context) => {
contexts.push(context)
return Effect.succeed({ type: "bearer", token: "secret" })
},
},
}).tools,
"getTest",
)
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))
expect(inputTypeScript(tool)).toBe("{}")
expect(client.requests[0]!.headers.authorization).toBe("Bearer secret")
expect(contexts).toEqual([
{
name: "bearer",
definition: { type: "http", scheme: "bearer" },
scopes: [],
operation: {
operationId: undefined,
method: "GET",
path: "/test",
summary: undefined,
description: undefined,
},
},
])
})
test("applies authentication carriers without prototype or collision loss", async () => {
const client = recordingClient(() => json({ ok: true }))
const authenticated = (security: ReadonlyArray<Record<string, ReadonlyArray<string>>>, schemes: Record<string, unknown>) =>
OpenAPI.fromSpec({
baseUrl,
spec: { ...singleOperation({}), security, components: { securitySchemes: schemes } },
auth: { resolve: () => Effect.succeed({ type: "apiKey", value: "secret" }) },
})
const prototype = toolAt(
authenticated([{ key: [] }], { key: { type: "apiKey", in: "query", name: "__proto__" } }).tools,
"test",
)
if (!Tool.isDefinition(prototype)) throw new Error("prototype auth tool was not generated")
await Effect.runPromise(prototype.run({}).pipe(Effect.provide(client.layer)))
expect(new URL(client.requests[0]!.url).searchParams.get("__proto__")).toBe("secret")
const duplicate = toolAt(
authenticated(
[{ first: [], second: [] }],
{
first: { type: "apiKey", in: "header", name: "x-key" },
second: { type: "apiKey", in: "header", name: "x-key" },
},
).tools,
"test",
)
if (!Tool.isDefinition(duplicate)) throw new Error("duplicate auth tool was not generated")
await expect(Effect.runPromise(duplicate.run({}).pipe(Effect.provide(client.layer)))).rejects.toThrow(
"multiple credentials",
)
const cookie = authenticated([{ key: [] }], { key: { type: "apiKey", in: "cookie", name: "session" } })
expect(cookie.tools).toEqual({})
expect(cookie.skipped[0]?.reason).toBe("cookie authentication 'key' is not supported")
const alternative = OpenAPI.fromSpec({
baseUrl,
spec: {
...singleOperation({}),
security: [{ cookie: [] }, { bearer: [] }],
components: {
securitySchemes: {
cookie: { type: "apiKey", in: "cookie", name: "session" },
bearer: { type: "http", scheme: "bearer" },
},
},
},
auth: {
resolve: ({ name }) =>
Effect.succeed(name === "bearer" ? { type: "bearer", token: "secret" } : undefined),
},
})
const alternativeTool = toolAt(alternative.tools, "test")
if (!Tool.isDefinition(alternativeTool)) throw new Error("supported auth alternative was not generated")
await Effect.runPromise(alternativeTool.run({}).pipe(Effect.provide(client.layer)))
expect(client.requests.at(-1)?.headers.authorization).toBe("Bearer secret")
})
test("honors server precedence and rejects ambiguous base URLs", async () => {
const client = recordingClient(() => json({ ok: true }))
const spec = {
...singleOperation({ servers: [{ url: "https://operation.example/v1" }] }),
servers: [{ url: "https://document.example" }],
} satisfies Document
const tool = toolAt(OpenAPI.fromSpec({ spec }).tools, "test")
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
await Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))
expect(client.requests[0]?.url).toBe("https://operation.example/v1/test")
const invalid = OpenAPI.fromSpec({ spec, baseUrl: "https://example.com/api?tenant=one" })
expect(invalid.tools).toEqual({})
expect(invalid.skipped[0]?.reason).toContain("unsupported query string or fragment")
const malformed = OpenAPI.fromSpec({ spec, baseUrl: "https:/example.com" })
expect(malformed.tools).toEqual({})
expect(malformed.skipped[0]?.reason).toContain("not an absolute HTTP(S) URL")
})
test("resolves chained response refs before detecting unsupported transports", () => {
const result = OpenAPI.fromSpec({
baseUrl,
spec: {
...singleOperation({ responses: { 200: { $ref: "#/components/responses/First" } } }),
components: {
responses: {
First: { $ref: "#/components/responses/Stream" },
Stream: { content: { "text/event-stream": { schema: { type: "string" } } } },
},
},
},
})
expect(result.tools).toEqual({})
expect(result.skipped[0]?.reason).toBe("SSE operations are not supported")
})
test("resolves response schemas before detecting binary output", () => {
const result = OpenAPI.fromSpec({
baseUrl,
spec: {
...singleOperation({
responses: {
200: {
content: { "text/plain": { schema: { $ref: "#/components/schemas/File" } } },
},
},
}),
components: { schemas: { File: { type: "string", format: "binary" } } },
},
})
expect(result.tools).toEqual({})
expect(result.skipped[0]?.reason).toBe("binary responses are not supported")
})
test("validates composite parameters before resolving auth", async () => {
const resolutions: Array<string> = []
const client = recordingClient(() => json({ ok: true }))
const tool = toolAt(
OpenAPI.fromSpec({
baseUrl,
spec: {
...singleOperation({
parameters: [{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } }],
}),
security: [{ bearer: [] }],
components: { securitySchemes: { bearer: { type: "http", scheme: "bearer" } } },
},
auth: {
resolve: ({ name }) => {
resolutions.push(name)
return Effect.succeed({ type: "bearer", token: "secret" })
},
},
}).tools,
"test",
)
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
await expect(
Effect.runPromise(tool.run({ filter: { value: undefined } }).pipe(Effect.provide(client.layer))),
).rejects.toThrow("unsupported nested value")
expect(resolutions).toEqual([])
expect(client.requests).toEqual([])
})
test("preserves JSON media types and rejects unencodable bodies", async () => {
const client = recordingClient(() => json({ ok: true }))
const tool = toolAt(
OpenAPI.fromSpec({
baseUrl,
spec: singleOperation(
{
requestBody: {
required: true,
content: { "application/merge-patch+json": { schema: { type: "object" } } },
},
},
"post",
),
}).tools,
"test",
)
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
await Effect.runPromise(tool.run({ body: { name: "updated" } }).pipe(Effect.provide(client.layer)))
expect(client.requests[0]!.headers["content-type"]).toBe("application/merge-patch+json")
const cyclic: Record<string, unknown> = {}
cyclic.self = cyclic
await expect(Effect.runPromise(tool.run({ body: cyclic }).pipe(Effect.provide(client.layer)))).rejects.toThrow(
"Invalid JSON body",
)
})
test("rejects oversized and malformed JSON responses", async () => {
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec: singleOperation({}) }).tools, "test")
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
const oversized = recordingClient(
() => new Response(null, { headers: { "content-length": String(50 * 1024 * 1024 + 1) } }),
)
const malformed = recordingClient(
() => new Response("{", { headers: { "content-type": "application/json" } }),
)
const chunked = recordingClient(() => new Response(new Uint8Array(50 * 1024 * 1024 + 1)))
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(oversized.layer)))).rejects.toThrow(
"response exceeds 50 MiB",
)
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(malformed.layer)))).rejects.toThrow(
"returned malformed JSON",
)
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(chunked.layer)))).rejects.toThrow(
"response exceeds 50 MiB",
)
})
test("keeps non-JSON responses raw and unions every success output", async () => {
const spec = singleOperation({
responses: {
200: { description: "Text", content: { "text/plain": { schema: { type: "string" } } } },
204: { description: "Empty" },
},
})
const tool = toolAt(OpenAPI.fromSpec({ baseUrl, spec }).tools, "test")
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
const client = recordingClient(() => new Response("123", { headers: { "content-type": "text/plain" } }))
expect(outputTypeScript(tool)).toBe("string | null")
await expect(Effect.runPromise(tool.run({}).pipe(Effect.provide(client.layer)))).resolves.toBe("123")
})
test("fails missing required parameters before auth and network", async () => {
const { requests, layer } = recordingClient(() => json({}))
const runtime = CodeMode.make({
tools: { opencode: OpenAPI.fromSpec({ spec: await opencodeSpec(), baseUrl }).tools },
})
const result = await Effect.runPromise(
runtime.execute("return await tools.opencode.v2.session.get({})").pipe(Effect.provide(layer)),
)
expect(result).toMatchObject({ ok: false })
expect(JSON.stringify(result)).toContain("Missing required path parameter 'sessionID'")
expect(requests).toHaveLength(0)
})
test("prefixes cross-location collisions and reconstructs the HTTP request", async () => {
const spec = {
openapi: "3.1.0",
info: { title: "collision", version: "1.0.0" },
paths: {
"/echo": {
post: {
operationId: "echo",
requestBody: {
required: true,
content: { "application/json": { schema: { type: "string" } } },
},
responses: { "204": { description: "Echoed" } },
},
},
"/things/{id}": {
post: {
operationId: "things.update",
parameters: [
{ name: "id", in: "path", required: true, schema: { type: "string" } },
{ name: "id", in: "query", required: true, schema: { type: "string" } },
{ name: "path_id", in: "query", schema: { type: "string" } },
{ name: "id", in: "header", required: true, schema: { type: "string" } },
],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
additionalProperties: false,
},
},
},
},
responses: { "204": { description: "Updated" } },
},
},
},
} satisfies Document
const { requests, layer } = recordingClient(() => new Response(null, { status: 204 }))
const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools
const update = toolAt(tools, "things.update")
const echo = toolAt(tools, "echo")
expect(Tool.isDefinition(update)).toBe(true)
if (!Tool.isDefinition(update)) throw new Error("things.update was not generated")
expect(inputTypeScript(update)).toBe(
"{ path_id: string; query_id: string; path_id_2?: string; header_id: string; body_id: string }",
)
expect(Tool.isDefinition(echo)).toBe(true)
if (!Tool.isDefinition(echo)) throw new Error("echo was not generated")
expect(inputTypeScript(echo)).toBe("{ body: string }")
const runtime = CodeMode.make({ tools })
const result = await Effect.runPromise(
runtime
.execute(
`
const updated = await tools.things.update({ path_id: "path", query_id: "query", path_id_2: "literal", header_id: "header", body_id: "body" })
const echoed = await tools.echo({ body: "hello" })
return { updated, echoed }
`,
)
.pipe(Effect.provide(layer)),
)
expect(result).toMatchObject({ ok: true })
expect(requests).toHaveLength(2)
expect(new URL(requests[0]!.url).pathname).toBe("/things/path")
expect(new URL(requests[0]!.url).searchParams.get("id")).toBe("query")
expect(new URL(requests[0]!.url).searchParams.get("path_id")).toBe("literal")
expect(requests[0]!.headers.id).toBe("header")
expect(requests[0]!.body).toStrictEqual({ id: "body" })
expect(requests[1]!.body).toBe("hello")
})
test("keeps bodies nested when flattening would lose schema semantics", () => {
const body = (schema: Record<string, unknown>, required = true) => ({
required,
content: { "application/json": { schema } },
})
const spec = {
openapi: "3.1.0",
info: { title: "bodies", version: "1.0.0" },
paths: Object.fromEntries(
[
[
"optional",
body(
{
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
false,
),
],
["dictionary", body({ type: "object", additionalProperties: { type: "string" } })],
[
"composed",
body({
type: "object",
allOf: [{ type: "object", properties: { name: { type: "string" } }, required: ["name"] }],
additionalProperties: false,
}),
],
[
"nullable",
body({
type: ["object", "null"],
properties: { name: { type: "string" } },
additionalProperties: false,
}),
],
].map(([name, requestBody]) => [
`/body/${name}`,
{
post: {
operationId: `body.${name}`,
requestBody,
responses: { "204": { description: "Accepted" } },
},
},
]),
),
} satisfies Document
const tools = OpenAPI.fromSpec({ spec, baseUrl }).tools
for (const name of ["optional", "dictionary", "composed", "nullable"]) {
const tool = toolAt(tools, `body.${name}`)
expect(Tool.isDefinition(tool)).toBe(true)
if (!Tool.isDefinition(tool)) throw new Error(`body.${name} was not generated`)
const input = isRecord(tool.input) ? tool.input : {}
expect(Object.keys(isRecord(input.properties) ? input.properties : {})).toStrictEqual(["body"])
}
const optional = toolAt(tools, "body.optional")
if (!Tool.isDefinition(optional)) throw new Error("body.optional was not generated")
expect(inputTypeScript(optional)).toBe("{ body?: { name: string } }")
})
})
+1 -1
View File
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
import { ToolRuntime } from "../src/tool-runtime.js"
// Runs a CodeMode program with no host tools and returns the ExecuteResult. These tests pin the
// Runs a CodeMode program with no host tools and returns the CodeMode.Result. These tests pin the
// JS-parity behaviors for the "99% of ordinary defensive JavaScript just works" goal: cases where
// a strict interpreter would throw but idiomatic JS yields undefined / succeeds.
//
+7 -4
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { CodeMode, Tool, toolError, type ExecuteResult, type ExecutionLimits } from "../src/index.js"
import { CodeMode, Tool, toolError } from "../src/index.js"
// Wave 5 acceptance suite: first-class promise values. Un-awaited tool calls start eagerly on
// supervised fibers, `await` settles them, and Promise.all/allSettled/race/resolve/reject are
@@ -48,7 +48,10 @@ const failingTool = Tool.make({
run: () => Effect.fail(toolError("Lookup refused")),
})
const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}): Promise<ExecuteResult> => {
const run = (
code: string,
options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {},
): Promise<CodeMode.Result> => {
const trace = options.trace ?? makeTrace()
return Effect.runPromise(
CodeMode.execute({
@@ -59,13 +62,13 @@ const run = (code: string, options: { trace?: Trace; limits?: ExecutionLimits }
)
}
const value = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => {
const value = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => {
const result = await run(code, options)
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
const error = async (code: string, options: { trace?: Trace; limits?: ExecutionLimits } = {}) => {
const error = async (code: string, options: { trace?: Trace; limits?: CodeMode.ExecutionLimits } = {}) => {
const result = await run(code, options)
if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
return result.error
+67 -4
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { CodeMode } from "../src/index.js"
import { Tool, inputTypeScript, jsonSchemaToTypeScript, outputTypeScript } from "../src/tool.js"
import { CodeMode, Tool } from "../src/index.js"
import { inputTypeScript, jsonSchemaToTypeScript, outputTypeScript } from "../src/tool-schema.js"
// A raw JSON Schema tool in the shape an MCP adapter produces: render-only input schema
// whose property descriptions and constraints must surface as JSDoc in pretty signatures.
@@ -159,8 +159,8 @@ describe("pretty signature rendering", () => {
$ref: "#/$defs/Node",
$defs: { Node: { type: "object", properties: { child: { $ref: "#/$defs/Node" }, name: { type: "string" } } } },
} as const
expect(jsonSchemaToTypeScript(cyclic)).toBe("{ child?: Node; name?: string }")
expect(jsonSchemaToTypeScript(cyclic, true)).toContain("child?: Node")
expect(jsonSchemaToTypeScript(cyclic)).toBe("{ child?: unknown; name?: string }")
expect(jsonSchemaToTypeScript(cyclic, true)).toContain("child?: unknown")
let deep: Record<string, unknown> = { type: "string" }
for (let level = 0; level < 12; level += 1) deep = { type: "object", properties: { next: deep } }
@@ -170,6 +170,43 @@ describe("pretty signature rendering", () => {
expect(rendered).toContain("next?:")
}
})
test("intersects ref and union siblings instead of discarding them", () => {
expect(
jsonSchemaToTypeScript({
$ref: "#/$defs/User",
properties: { active: { type: "boolean" } },
required: ["active"],
$defs: {
User: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
},
}),
).toBe("{ id: string } & { active: boolean }")
expect(
jsonSchemaToTypeScript({
type: "object",
properties: { common: { type: "boolean" } },
required: ["common"],
anyOf: [
{ type: "object", properties: { name: { type: "string" } }, required: ["name"] },
{ type: "object", properties: { count: { type: "number" } }, required: ["count"] },
],
}),
).toBe("({ name: string } | { count: number }) & { common: boolean }")
expect(jsonSchemaToTypeScript({ $ref: "https://example.com/schema.json" })).toBe("unknown")
expect(
jsonSchemaToTypeScript({
$ref: "#/$defs/User/properties/id",
$defs: { User: { type: "object" }, id: { type: "string" } },
}),
).toBe("unknown")
expect(
jsonSchemaToTypeScript({
type: ["object", "null"],
properties: { name: { type: "string" } },
}),
).toBe("{ name?: string } | null")
})
})
describe("non-identifier property names render as quoted keys", () => {
@@ -268,6 +305,32 @@ describe("union schemas render every alternative", () => {
expect(inputTypeScript(tool)).toBe("{ value?: string | number }")
expect(outputTypeScript(tool)).toBe("number | boolean")
})
test("allOf renders intersections with parenthesized union members", () => {
const schema = {
allOf: [
{ type: "object", properties: { id: { type: "string" } } },
{ type: ["string", "null"] },
],
} as const
expect(jsonSchemaToTypeScript(schema)).toBe("{ id?: string } & (string | null)")
})
test("allOf does not discard an unresolved constraint", () => {
expect(jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { $ref: "https://example.com/external.json" }] })).toBe(
"unknown",
)
expect(
jsonSchemaToTypeScript({ allOf: [{ type: "string" }, { allOf: [{ $ref: "https://example.com/external.json" }] }] }),
).toBe("unknown")
expect(
jsonSchemaToTypeScript({
type: "string",
allOf: [{ $ref: "#/$defs/Constraint" }],
$defs: { Constraint: { description: "TypeScript-neutral constraint" } },
}),
).toBe("string")
})
})
describe("pretty signatures in search results", () => {
+1
View File
@@ -90,6 +90,7 @@
"@ff-labs/fff-bun": "0.9.4",
"@npmcli/arborist": "9.4.0",
"@npmcli/config": "10.8.1",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/effect-sqlite-node": "workspace:*",
"@opencode-ai/llm": "workspace:*",
+4 -36
View File
@@ -2,8 +2,7 @@ export * as ConfigExternalPlugin from "./external"
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
import { Effect, Schema, Stream } from "effect"
import { createRequire } from "node:module"
import { Effect, Schema } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Config } from "../../config"
@@ -36,9 +35,6 @@ const PluginPackage = Schema.Struct({
module: Schema.optional(Schema.String),
})
let importGeneration = 0
const moduleCache = createRequire(import.meta.url).cache
export const Plugin = define({
id: "config-plugin",
effect: Effect.fn(function* (ctx) {
@@ -46,7 +42,6 @@ export const Plugin = define({
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const active = new Set<string>()
const load = Effect.fn("ConfigExternalPlugin.load")(function* () {
const configured: { package: string; options?: Record<string, unknown> }[] = []
@@ -110,7 +105,7 @@ export const Plugin = define({
: (yield* npm.add(ref.package)).entrypoint
if (!entrypoint) return
yield* Effect.log({ msg: "loading plugin", id: ref.package, entrypoint })
const mod = yield* Effect.promise(() => import(cacheBust(entrypoint)))
const mod = yield* Effect.promise(() => import(entrypoint))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
return {
@@ -118,40 +113,13 @@ export const Plugin = define({
effect: (host: Parameters<typeof plugin.effect>[0]) =>
plugin.effect({ ...host, options: ref.options ?? {} }),
}
}).pipe(
Effect.catchCause((cause) =>
Effect.logError("failed to load plugin", { package: ref.package, cause }).pipe(Effect.as(undefined)),
),
),
}).pipe(Effect.catchCause(() => Effect.succeed(undefined))),
).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined)))
})
const reconcile = Effect.fn("ConfigExternalPlugin.reconcile")(function* () {
const plugins = yield* load()
const next = new Set(plugins.map((plugin) => plugin.id))
for (const id of active) {
if (!next.has(id)) yield* ctx.plugin.remove(id)
}
for (const plugin of plugins) yield* ctx.plugin.add(plugin)
active.clear()
for (const id of next) active.add(id)
})
yield* reconcile()
yield* ctx.event.subscribe().pipe(
Stream.filter((event) => event.type === "config.updated"),
Stream.runForEach(() => reconcile()),
Effect.forkScoped({ startImmediately: true }),
)
for (const plugin of yield* load()) yield* ctx.plugin.add(plugin)
}),
})
function cacheBust(entrypoint: string) {
const url = path.isAbsolute(entrypoint) ? pathToFileURL(entrypoint) : new URL(entrypoint)
if (url.protocol === "file:") delete moduleCache[fileURLToPath(url)]
url.searchParams.set("opencode-reload", String(++importGeneration))
return url.href
}
const resolvePackageEntrypoint = Effect.fnUntraced(function* (fs: FSUtil.Interface, directory: string) {
const pkg = yield* fs.readJson(path.join(directory, "package.json")).pipe(
Effect.flatMap(Schema.decodeUnknownEffect(PluginPackage)),
-2
View File
@@ -44,7 +44,5 @@ export const migrations = (
import("./migration/20260703090000_reset_v2_event_rename_sweep"),
import("./migration/20260703181610_event_created_column"),
import("./migration/20260703190000_reset_v2_shell_event_payloads"),
import("./migration/20260703200000_reset_v2_event_fragments"),
import("./migration/20260703210000_reset_v2_execution_errors"),
])
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
@@ -1,14 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703200000_reset_v2_event_fragments",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
})
},
} satisfies DatabaseMigration.Migration
@@ -1,14 +0,0 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration"
export default {
id: "20260703210000_reset_v2_execution_errors",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`DELETE FROM \`session_input\`;`)
yield* tx.run(`DELETE FROM \`session_message\`;`)
yield* tx.run(`DELETE FROM \`event\`;`)
yield* tx.run(`DELETE FROM \`event_sequence\`;`)
})
},
} satisfies DatabaseMigration.Migration
+14 -7
View File
@@ -160,15 +160,22 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
export const liveBounded = (events: Interface, capacity: number) =>
export const liveBounded = (
events: Interface,
options: { readonly capacity: number; readonly accept?: (event: Payload) => boolean },
) =>
Effect.gen(function* () {
const queue = yield* Queue.dropping<Payload, SubscriberOverflowError>(capacity)
const queue = yield* Queue.dropping<Payload, SubscriberOverflowError>(options.capacity)
const unsubscribe = yield* events.listen((event) =>
Queue.offer(queue, event).pipe(
Effect.flatMap((accepted) =>
accepted ? Effect.void : Queue.fail(queue, new SubscriberOverflowError({ capacity })).pipe(Effect.asVoid),
),
),
options.accept && !options.accept(event)
? Effect.void
: Queue.offer(queue, event).pipe(
Effect.flatMap((accepted) =>
accepted
? Effect.void
: Queue.fail(queue, new SubscriberOverflowError({ capacity: options.capacity })).pipe(Effect.asVoid),
),
),
)
yield* Effect.addFinalizer(() => unsubscribe.pipe(Effect.andThen(Queue.shutdown(queue)), Effect.asVoid))
return Stream.fromQueue(queue)
+1 -1
View File
@@ -45,7 +45,7 @@ const FILES = [
"**/.nyc_output/**",
]
export const PATTERNS = [...FILES, ...FOLDERS]
export const PATTERNS = [...FILES, ...FOLDERS, `**/{${Array.from(FOLDERS).join(",")}}/**`]
export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) {
for (const pattern of opts?.whitelist || []) {
+3
View File
@@ -52,6 +52,9 @@ export const Flag = {
get OPENCODE_EXPERIMENTAL_REFERENCES() {
return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES")
},
get CODEMODE_ENABLED() {
return process.env["CODEMODE_ENABLED"] === undefined || truthy("CODEMODE_ENABLED")
},
get OPENCODE_TUI_CONFIG() {
return process.env["OPENCODE_TUI_CONFIG"]
},
+10 -2
View File
@@ -1,6 +1,7 @@
export * as McpGuidance from "./guidance"
import { makeLocationNode } from "../effect/app-node"
import { Flag } from "../flag/flag"
import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission"
@@ -17,6 +18,11 @@ type Summary = typeof Summary.Type
const entries = (servers: ReadonlyArray<Summary>) =>
servers.flatMap((server) => [
` <server name="${server.server}">`,
...(Flag.CODEMODE_ENABLED
? [
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.group(server.server))}]\`.`,
]
: []),
...server.instructions.split("\n").map((line) => ` ${line}`),
" </server>",
])
@@ -64,15 +70,17 @@ export const layer = Layer.effect(
load: Effect.fn("McpGuidance.load")(function* (selection) {
const agent = selection.info
if (!agent) return SystemContext.empty
if (Flag.CODEMODE_ENABLED && PermissionV2.evaluate("execute", "*", agent.permissions).effect === "deny")
return SystemContext.empty
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
concurrency: "unbounded",
})
// Hide a server only when every tool it contributes is wholly denied for this agent.
// Instructions are useful only when this agent can reach at least one server tool.
const visible = instructions
.filter((item) => {
const owned = tools.filter((tool) => tool.server === item.server)
return (
owned.length === 0 ||
(!Flag.CODEMODE_ENABLED && owned.length === 0) ||
owned.some(
(tool) =>
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
+7 -38
View File
@@ -59,14 +59,7 @@ export type AskResult = typeof AskResult.Type
export const Event = Permission.Event
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {
permission: Schema.String,
resources: Schema.Array(Schema.String),
}) {
override get message() {
return `Permission rejected: ${this.permission}`
}
}
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", {
feedback: Schema.String,
@@ -74,13 +67,7 @@ export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("P
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
rules: Permission.Ruleset,
permission: Schema.String,
resources: Schema.Array(Schema.String),
}) {
override get message() {
return `Permission denied: ${this.permission}`
}
}
}) {}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("PermissionV2.NotFoundError", {
requestID: ID,
@@ -132,17 +119,9 @@ const layer = Layer.effect(
const pending = new Map<ID, Pending>()
yield* Effect.addFinalizer(() =>
Effect.forEach(
pending.values(),
(item) =>
Deferred.fail(
item.deferred,
new RejectedError({ permission: item.request.action, resources: [...item.request.resources] }),
),
{
discard: true,
},
).pipe(
Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
discard: true,
}).pipe(
Effect.ensuring(
Effect.sync(() => {
pending.clear()
@@ -222,8 +201,6 @@ const layer = Layer.effect(
if (result.effect === "deny") {
return yield* new DeniedError({
rules: relevant(input, result.rules),
permission: input.action,
resources: input.resources,
})
}
if (result.effect === "allow") return
@@ -253,12 +230,7 @@ const layer = Layer.effect(
if (input.reply === "reject") {
yield* Deferred.fail(
existing.deferred,
input.message
? new CorrectedError({ feedback: input.message })
: new RejectedError({
permission: existing.request.action,
resources: [...existing.request.resources],
}),
input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
)
pending.delete(input.requestID)
for (const [id, item] of pending) {
@@ -268,10 +240,7 @@ const layer = Layer.effect(
requestID: item.request.id,
reply: "reject",
})
yield* Deferred.fail(
item.deferred,
new RejectedError({ permission: item.request.action, resources: [...item.request.resources] }),
)
yield* Deferred.fail(item.deferred, new RejectedError())
pending.delete(id)
}
return
+2 -4
View File
@@ -23,8 +23,6 @@ import { ToolHooks } from "../tool/hooks"
import { WorkspaceV2 } from "../workspace"
const mutable = <T>(value: T) => value as DeepMutable<T>
const isEvent = Schema.is(Schema.Union(EventManifest.ServerDefinitions))
export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
const agents = yield* AgentV2.Service
const aisdk = yield* AISDK.Service
@@ -160,7 +158,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
event: {
subscribe: () => events.live().pipe(Stream.filter(isEvent)),
subscribe: () => events.live().pipe(Stream.filter(EventManifest.isServer)),
},
integration: {
list: () => response(integration.list()),
@@ -302,7 +300,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}),
},
tool: {
register: (input) => tools.register(input),
register: (input, options) => tools.register(input, options),
execute: {
before: (callback) =>
toolHooks.hook.before((event) => {
+33 -23
View File
@@ -43,37 +43,47 @@ export type DeepMutable<T> = T extends string | number | boolean | bigint | symb
* Nominal wrapper for scalar types. The class itself is a valid schema
* pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
*
* Overrides `~type.make` on the derived `Schema.Opaque` so `Schema.Schema.Type`
* of a field using this newtype resolves to `Self` rather than the underlying
* branded phantom. Without that override, passing a class instance to code
* typed against `Schema.Schema.Type<FieldSchema>` would require a cast even
* though the values are structurally equivalent at runtime.
* The runtime value remains an unwrapped primitive. `Schema.brand` supplies
* the primitive schema behavior and constructor validation, while the class
* supplies the nominal TypeScript identity.
* Apply checks and annotations to the underlying schema before wrapping it;
* schema rebuild operations intentionally return the underlying schema shape.
*
* @example
* class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
* static make(id: string): QuestionID {
* return this.make(id)
* }
* }
* class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {}
*
* Schema.decodeEffect(QuestionID)(input)
* const id = QuestionID.make("question-1")
* Schema.decodeUnknownEffect(QuestionID)(input)
*/
type NewtypeSchema<Self, Tag extends string, S extends Schema.Top> = (abstract new (_: never) => {
readonly _newtype: Tag
}) &
Schema.Bottom<
Self,
S["Encoded"],
S["DecodingServices"],
S["EncodingServices"],
S["ast"],
S["Rebuild"],
S["~type.make.in"],
Self,
S["~type.parameters"],
Self,
S["~type.mutability"],
S["~type.optionality"],
S["~type.constructor.default"],
S["~encoded.mutability"],
S["~encoded.optionality"]
> &
Omit<S, keyof Schema.Top>
export function Newtype<Self>() {
return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S): NewtypeSchema<Self, Tag, S> => {
abstract class Base {
declare readonly _newtype: Tag
static make(value: Schema.Schema.Type<S>): Self {
return value as unknown as Self
}
}
Object.setPrototypeOf(Base, schema)
return Base as unknown as (abstract new (_: never) => { readonly _newtype: Tag }) & {
readonly make: (value: Schema.Schema.Type<S>) => Self
} & Omit<Schema.Opaque<Self, S, {}>, "make" | "~type.make"> & {
readonly "~type.make": Self
}
Object.setPrototypeOf(Base, schema.pipe(Schema.brand(tag)))
return Base as unknown as NewtypeSchema<Self, Tag, S>
}
}
+12 -8
View File
@@ -540,7 +540,8 @@ const layer = Layer.effect(
const started = yield* Effect.gen(function* () {
const shell = yield* Shell.Service
return yield* shell.create({ command: input.command, cwd: session.location.directory })
}).pipe(Effect.provide(locations.get(session.location)))
})
.pipe(Effect.provide(locations.get(session.location)))
yield* events.publish(
SessionEvent.Shell.Started,
{
@@ -563,8 +564,7 @@ const layer = Layer.effect(
.pipe(Effect.catchTag("Shell.NotFoundError", () => Effect.succeed(missingShellOutput())))
: missingShellOutput()
return { shell: terminal.info, output }
})
.pipe(Effect.provide(locations.get(session.location)))
}).pipe(Effect.provide(locations.get(session.location)))
yield* events.publish(SessionEvent.Shell.Ended, {
sessionID: input.sessionID,
shell: completed.shell,
@@ -585,11 +585,15 @@ const layer = Layer.effect(
const skills = yield* SkillV2.Service.pipe(Effect.provide(locations.get(session.location)))
const skill = (yield* skills.list()).find((item) => item.name === input.skill)
if (!skill) return yield* new SkillNotFoundError({ skill: input.skill })
yield* events.publish(SessionEvent.Skill.Activated, {
sessionID: input.sessionID,
name: skill.name,
text: skill.content,
})
yield* events.publish(
SessionEvent.Skill.Activated,
{
sessionID: input.sessionID,
name: skill.name,
text: skill.content,
},
{ id: input.id ? EventV2.ID.make(input.id.replace(/^msg_/, "evt_")) : undefined },
)
if (input.resume !== false)
yield* execution
.resume(input.sessionID)
+1 -7
View File
@@ -225,13 +225,7 @@ const make = (dependencies: Dependencies) => {
.pipe(
Stream.runForEach((event) => {
if (LLMEvent.is.providerError(event)) failed = true
if (LLMEvent.is.textDelta(event)) {
chunks.push(event.text)
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
sessionID: input.sessionID,
text: event.text,
})
}
if (LLMEvent.is.textDelta(event)) chunks.push(event.text)
return Effect.void
}),
Effect.as(true),
-18
View File
@@ -1,7 +1,6 @@
import { Schema } from "effect"
import { SessionMessage } from "./message"
import { SessionSchema } from "./schema"
import { SessionError } from "@opencode-ai/schema/session-error"
export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeError>()("Session.MessageDecodeError", {
sessionID: SessionSchema.ID,
@@ -11,20 +10,3 @@ export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeErr
return `Failed to decode message ${this.messageID} in session ${this.sessionID}`
}
}
export class StepFailedError extends Schema.TaggedErrorClass<StepFailedError>()("Session.StepFailedError", {
error: SessionError.Error,
}) {
override get message() {
return this.error.message
}
}
export class UserInterruptedError extends Schema.TaggedErrorClass<UserInterruptedError>()(
"Session.UserInterruptedError",
{},
) {
override get message() {
return "Session interrupted by user"
}
}
+19 -48
View File
@@ -1,4 +1,4 @@
import { Cause, Effect, Exit, Layer } from "effect"
import { Cause, DateTime, Effect, Exit, Layer } from "effect"
import { EventV2 } from "../../event"
import { LocationServiceMap } from "../../location-service-map"
import { makeGlobalNode } from "../../effect/app-node"
@@ -8,16 +8,6 @@ import { SessionRunner } from "../runner"
import { SessionSchema } from "../schema"
import { SessionStore } from "../store"
import { SessionExecution } from "../execution"
import { toSessionError } from "../to-session-error"
import { UserInterruptedError } from "../error"
export function terminal(exit: Exit.Exit<void, SessionRunner.RunError>, reason?: "user" | "shutdown" | "superseded") {
if (Exit.isSuccess(exit)) return { type: "succeeded" as const }
if (Cause.hasInterrupts(exit.cause)) return { type: "interrupted" as const, reason: reason ?? "shutdown" }
const failure = Cause.squash(exit.cause)
if (failure instanceof UserInterruptedError) return { type: "interrupted" as const, reason: "user" as const }
return { type: "failed" as const, error: toSessionError(failure) }
}
/** Current-process routing for implicit-local Locations. Future remote placement belongs here. */
const layer = Layer.effect(
@@ -26,23 +16,7 @@ const layer = Layer.effect(
const store = yield* SessionStore.Service
const locations = yield* LocationServiceMap.Service
const events = yield* EventV2.Service
const reportLifecycle = <A>(sessionID: SessionSchema.ID, effect: Effect.Effect<A>) =>
effect.pipe(
Effect.tapCause((cause) =>
Cause.hasInterruptsOnly(cause)
? Effect.void
: Effect.logError("Failed to publish Session execution lifecycle", cause).pipe(
Effect.annotateLogs({ sessionID }),
),
),
Effect.asVoid,
)
const coordinator = yield* SessionRunCoordinator.make<
SessionSchema.ID,
SessionRunner.RunError,
"user" | "shutdown" | "superseded"
>({
started: (sessionID) => reportLifecycle(sessionID, events.publish(SessionEvent.Execution.Started, { sessionID })),
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError>({
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
@@ -55,31 +29,28 @@ const layer = Layer.effect(
),
)
}),
// One terminal observation per busy period, covering every coalesced drain.
settled: (sessionID, exit, reason) =>
reportLifecycle(
sessionID,
Effect.gen(function* () {
const outcome = terminal(exit, reason)
if (outcome.type === "succeeded") {
yield* events.publish(SessionEvent.Execution.Succeeded, { sessionID })
return
}
if (outcome.type === "interrupted") {
yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: outcome.reason })
return
}
yield* events.publish(SessionEvent.Execution.Failed, {
sessionID,
error: outcome.error,
})
}),
// One ExecutionSettled per execution (busy period), covering every coalesced drain.
settled: (sessionID, exit) =>
Effect.gen(function* () {
const failure =
Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause) ? Cause.squash(exit.cause) : undefined
yield* events.publish(SessionEvent.ExecutionSettled, {
sessionID,
outcome: Exit.isSuccess(exit) ? "success" : Cause.hasInterrupts(exit.cause) ? "interrupted" : "failure",
error:
failure !== undefined
? { type: "unknown", message: failure instanceof Error ? failure.message : String(failure) }
: undefined,
})
}).pipe(
Effect.catchCause(() => Effect.void),
Effect.asVoid,
),
})
return SessionExecution.Service.of({
active: coordinator.active,
interrupt: (sessionID) => coordinator.interrupt(sessionID, "user"),
interrupt: coordinator.interrupt,
resume: coordinator.run,
wake: coordinator.wake,
awaitIdle: coordinator.awaitIdle,
+52 -69
View File
@@ -1,5 +1,5 @@
import { castDraft, produce, type WritableDraft } from "immer"
import { DateTime, Effect } from "effect"
import { Effect } from "effect"
import { SessionEvent } from "./event"
import { SessionMessage } from "./message"
@@ -8,6 +8,7 @@ export type MemoryState = {
}
export interface Adapter {
readonly getModel: () => Effect.Effect<SessionMessage.ModelSelected["model"] | undefined, never, never>
readonly getCurrentAssistant: () => Effect.Effect<SessionMessage.Assistant | undefined, never, never>
readonly getAssistant: (
messageID: SessionMessage.ID,
@@ -29,6 +30,15 @@ export function memory(state: MemoryState): Adapter {
const latestAssistantIndex = () => state.messages.findLastIndex((message) => message.type === "assistant")
return {
getModel() {
return Effect.sync(
() =>
state.messages.findLast(
(message): message is SessionMessage.ModelSelected | SessionMessage.Assistant =>
message.type === "model-switched" || message.type === "assistant",
)?.model,
)
},
getCurrentAssistant() {
return Effect.sync(() => {
const index = latestAssistantIndex()
@@ -89,11 +99,11 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
(item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID),
)
const latestText = (assistant: DraftAssistant | undefined) =>
assistant?.content.findLast((item): item is DraftText => item.type === "text")
const latestText = (assistant: DraftAssistant | undefined, textID: string) =>
assistant?.content.findLast((item): item is DraftText => item.type === "text" && item.id === textID)
const latestReasoning = (assistant: DraftAssistant | undefined) =>
assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning" && !item.time?.completed)
const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) =>
assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning" && item.id === reasoningID)
const updateOwnedAssistant = (messageID: SessionMessage.ID, recipe: (draft: DraftAssistant) => void) =>
Effect.gen(function* () {
@@ -101,17 +111,6 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
if (assistant) yield* adapter.updateAssistant(produce(assistant, recipe))
})
const clearCurrentRetry = Effect.gen(function* () {
const assistant = yield* adapter.getCurrentAssistant()
if (assistant?.retry) {
yield* adapter.updateAssistant(
produce(assistant, (draft) => {
draft.retry = undefined
}),
)
}
})
return Effect.gen(function* () {
yield* SessionEvent.All.match(event, {
"session.agent.selected": (event) => {
@@ -126,25 +125,26 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
)
},
"session.model.selected": (event) => {
return adapter.appendMessage(
SessionMessage.ModelSelected.make({
id: SessionMessage.ID.fromEvent(event.id),
type: "model-switched",
metadata: event.metadata,
model: event.data.model,
time: { created: event.created },
}),
)
return Effect.gen(function* () {
const previous = yield* adapter.getModel()
yield* adapter.appendMessage(
SessionMessage.ModelSelected.make({
id: SessionMessage.ID.fromEvent(event.id),
type: "model-switched",
metadata: event.metadata,
model: event.data.model,
previous,
time: { created: event.created },
}),
)
})
},
"session.moved": () => Effect.void,
"session.renamed": () => Effect.void,
"session.forked": () => Effect.void,
"session.prompt.promoted": () => Effect.void,
"session.prompt.admitted": () => Effect.void,
"session.execution.started": () => Effect.void,
"session.execution.succeeded": () => clearCurrentRetry,
"session.execution.failed": () => clearCurrentRetry,
"session.execution.interrupted": () => clearCurrentRetry,
"session.execution.settled": () => Effect.void,
"session.context.updated": (event) =>
adapter.appendMessage(
SessionMessage.System.make({
@@ -205,26 +205,10 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
},
"session.step.started": (event) => {
return Effect.gen(function* () {
const existing = yield* adapter.getAssistant(event.data.assistantMessageID)
if (existing) {
yield* adapter.updateAssistant(
produce(existing, (draft) => {
draft.agent = event.data.agent
draft.model = castDraft(event.data.model)
draft.retry = undefined
draft.error = undefined
draft.finish = undefined
draft.time.completed = undefined
if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, start: event.data.snapshot }
}),
)
return
}
const currentAssistant = yield* adapter.getCurrentAssistant()
if (currentAssistant) {
yield* adapter.updateAssistant(
produce(currentAssistant, (draft) => {
draft.retry = undefined
draft.time.completed = event.created
}),
)
@@ -260,24 +244,25 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.time.completed = event.created
draft.finish = "error"
draft.error = castDraft(event.data.error)
draft.retry = undefined
draft.error = event.data.error
})
},
"session.text.started": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.content.push(castDraft(SessionMessage.AssistantText.make({ type: "text", text: "" })))
draft.content.push(
castDraft(SessionMessage.AssistantText.make({ type: "text", id: event.data.textID, text: "" })),
)
})
},
"session.text.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft)
const match = latestText(draft, event.data.textID)
if (match) match.text += event.data.delta
})
},
"session.text.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestText(draft)
const match = latestText(draft, event.data.textID)
if (match) match.text = event.data.text
})
},
@@ -307,8 +292,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID)
if (match) {
match.executed = event.data.executed
match.providerState = event.data.state
match.provider = event.data.provider
match.time.ran = event.created
match.state = castDraft(
SessionMessage.ToolStateRunning.make({
@@ -334,8 +318,11 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID)
if (match && match.state.status === "running") {
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.provider = {
executed: event.data.provider.executed || match.provider?.executed === true,
metadata: match.provider?.metadata,
resultMetadata: event.data.provider.metadata,
}
match.time.completed = event.created
match.state = castDraft(
SessionMessage.ToolStateCompleted.make({
@@ -354,8 +341,11 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestTool(draft, event.data.callID)
if (match && (match.state.status === "pending" || match.state.status === "running")) {
match.executed = event.data.executed || match.executed === true
match.providerResultState = event.data.resultState
match.provider = {
executed: event.data.provider.executed || match.provider?.executed === true,
metadata: match.provider?.metadata,
resultMetadata: event.data.provider.metadata,
}
match.time.completed = event.created
match.state = castDraft(
SessionMessage.ToolStateError.make({
@@ -376,8 +366,9 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
castDraft(
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: event.data.reasoningID,
text: "",
state: event.data.state,
providerMetadata: event.data.providerMetadata,
time: { created: event.created },
}),
),
@@ -386,29 +377,21 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
},
"session.reasoning.delta": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft)
const match = latestReasoning(draft, event.data.reasoningID)
if (match) match.text += event.data.delta
})
},
"session.reasoning.ended": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
const match = latestReasoning(draft)
const match = latestReasoning(draft, event.data.reasoningID)
if (match) {
match.text = event.data.text
match.time = { created: match.time?.created ?? event.created, completed: event.created }
if (event.data.state !== undefined) match.state = event.data.state
}
})
},
"session.retry.scheduled": (event) => {
return updateOwnedAssistant(event.data.assistantMessageID, (draft) => {
draft.retry = {
attempt: event.data.attempt,
at: DateTime.makeUnsafe(event.data.at),
error: castDraft(event.data.error),
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
}
})
},
"session.retried": () => Effect.void,
"session.compaction.started": () => Effect.void,
"session.compaction.delta": () => Effect.void,
"session.compaction.ended": (event) => {
+19 -7
View File
@@ -5,6 +5,7 @@ import { DateTime, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database"
import { EventV2 } from "../event"
import { makeGlobalNode } from "../effect/app-node"
import { ModelV2 } from "../model"
import { SessionEvent } from "./event"
import { SessionV1 } from "../v1/session"
import { WorkspaceTable } from "../control-plane/workspace.sql"
@@ -356,6 +357,17 @@ function run(db: DatabaseService, event: MessageEvent) {
}
const appendMessage = (message: SessionMessage.Message) => insertMessage(db, event, message)
const adapter: SessionMessageUpdater.Adapter = {
getModel() {
return db
.select({ model: SessionTable.model })
.from(SessionTable)
.where(eq(SessionTable.id, event.data.sessionID))
.get()
.pipe(
Effect.orDie,
Effect.map((row) => (row?.model ? Schema.decodeUnknownSync(ModelV2.Ref)(row.model) : undefined)),
)
},
getCurrentAssistant() {
return Effect.gen(function* () {
// A newer step supersedes stale incomplete rows; never resume an older assistant projection.
@@ -570,13 +582,13 @@ const layer = Layer.effectDiscard(
)
yield* events.project(SessionEvent.ModelSelected, (event) =>
Effect.gen(function* () {
yield* run(db, event)
yield* db
.update(SessionTable)
.set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.created) })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie)
yield* run(db, event)
}),
)
yield* events.project(SessionEvent.Renamed, (event) =>
@@ -622,9 +634,6 @@ const layer = Layer.effectDiscard(
})
}),
)
yield* events.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
yield* events.project(SessionEvent.Execution.Failed, (event) => run(db, event))
yield* events.project(SessionEvent.Execution.Interrupted, (event) => run(db, event))
yield* events.project(SessionEvent.ContextUpdated, (event) => run(db, event))
yield* events.project(SessionEvent.Synthetic, (event) => run(db, event))
yield* events.project(SessionEvent.Skill.Activated, (event) =>
@@ -651,7 +660,7 @@ const layer = Layer.effectDiscard(
yield* events.project(SessionEvent.Tool.Failed, (event) => run(db, event))
yield* events.project(SessionEvent.Reasoning.Started, (event) => run(db, event))
yield* events.project(SessionEvent.Reasoning.Ended, (event) => run(db, event))
yield* events.project(SessionEvent.RetryScheduled, (event) => run(db, event))
// yield* events.project(SessionEvent.Retried, (event) => run(db, event))
yield* events.project(SessionEvent.Compaction.Ended, (event) => run(db, event))
yield* events.project(SessionEvent.RevertEvent.Staged, (event) =>
db
@@ -678,11 +687,14 @@ const layer = Layer.effectDiscard(
.select({ seq: SessionMessageTable.seq })
.from(SessionMessageTable)
.where(
and(eq(SessionMessageTable.session_id, event.data.sessionID), eq(SessionMessageTable.id, event.data.to)),
and(
eq(SessionMessageTable.session_id, event.data.sessionID),
eq(SessionMessageTable.id, event.data.messageID),
),
)
.get()
.pipe(Effect.orDie)
if (!boundary) return yield* Effect.die(new Error(`Revert boundary message not found: ${event.data.to}`))
if (!boundary) return yield* Effect.die(new Error(`Revert boundary message not found: ${event.data.messageID}`))
yield* db
.delete(SessionMessageTable)
.where(
+1 -1
View File
@@ -113,6 +113,6 @@ export const commit = Effect.fn("SessionRevert.commit")(function* (session: Sess
const events = yield* EventV2.Service
yield* events.publish(SessionEvent.RevertEvent.Committed, {
sessionID: session.id,
to: session.revert.messageID,
messageID: session.revert.messageID,
})
})
+13 -28
View File
@@ -3,7 +3,7 @@ export * as SessionRunCoordinator from "./run-coordinator"
import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
/** Serializes execution for each key while allowing different keys to run concurrently. */
export interface Coordinator<Key, E, Reason = never> {
export interface Coordinator<Key, E> {
/** Snapshots keys with an execution owned by this coordinator. */
readonly active: Effect.Effect<ReadonlySet<Key>>
/** Starts an execution while idle, or joins the active execution and returns its exit. */
@@ -11,7 +11,7 @@ export interface Coordinator<Key, E, Reason = never> {
/** Rings the doorbell: an idle key starts an execution; an active one drains again before settling. */
readonly wake: (key: Key) => Effect.Effect<void>
/** Stops the active execution, clears its doorbell, and waits for cleanup. No-op when idle. */
readonly interrupt: (key: Key, reason?: Reason) => Effect.Effect<void>
readonly interrupt: (key: Key) => Effect.Effect<void>
/** Resolves once no execution is active for the key. Returns immediately when already idle and never starts work. */
readonly awaitIdle: (key: Key) => Effect.Effect<void>
}
@@ -23,13 +23,11 @@ export interface Coordinator<Key, E, Reason = never> {
* closes the gap between a drain's last eligibility check and the idle transition, since
* those cannot be one atomic step. `done` resolves joiners with this execution's exit.
*/
type Execution<E, Reason> = {
type Execution<E> = {
readonly done: Deferred.Deferred<void, E>
owner?: Fiber.Fiber<void>
pendingWake: boolean
stopping: boolean
settling: boolean
interruptionReason?: Reason
}
/**
@@ -43,21 +41,19 @@ type Execution<E, Reason> = {
* waiters get this exit
* ```
*/
export const make = <Key, E, Reason = never>(options: {
export const make = <Key, E>(options: {
readonly drain: (key: Key, force: boolean) => Effect.Effect<void, E>
/** Runs once when a process-local busy period begins, before its first drain. */
readonly started?: (key: Key) => Effect.Effect<void>
/**
* Runs in the execution fiber for every exit, including interruption, after the final
* drain and before the execution settles (waiters resolve after it completes).
*/
readonly settled?: (key: Key, exit: Exit.Exit<void, E>, reason?: Reason) => Effect.Effect<void>
}): Effect.Effect<Coordinator<Key, E, Reason>, never, Scope.Scope> =>
readonly settled?: (key: Key, exit: Exit.Exit<void, E>) => Effect.Effect<void>
}): Effect.Effect<Coordinator<Key, E>, never, Scope.Scope> =>
Effect.gen(function* () {
const executions = new Map<Key, Execution<E, Reason>>()
const executions = new Map<Key, Execution<E>>()
const fork = yield* FiberSet.makeRuntime<never, void, never>()
const loop = (key: Key, execution: Execution<E, Reason>, force: boolean): Effect.Effect<void, E> =>
const loop = (key: Key, execution: Execution<E>, force: boolean): Effect.Effect<void, E> =>
Effect.suspend(() => options.drain(key, force)).pipe(
Effect.flatMap(() =>
Effect.suspend(() => {
@@ -70,25 +66,15 @@ export const make = <Key, E, Reason = never>(options: {
)
const start = (key: Key, force: boolean) => {
const execution: Execution<E, Reason> = {
done: Deferred.makeUnsafe<void, E>(),
pendingWake: false,
stopping: false,
settling: false,
}
const execution: Execution<E> = { done: Deferred.makeUnsafe<void, E>(), pendingWake: false, stopping: false }
executions.set(key, execution)
// The leading yield lets `owner` be assigned before the drain can settle, and keeps
// failing self-waking executions from growing the stack across successor starts.
// Drains start one tick after wake; callers observe progress through events or run.
execution.owner = fork(
Effect.yieldNow.pipe(
Effect.andThen(Effect.uninterruptible(options.started?.(key) ?? Effect.void)),
Effect.andThen(loop(key, execution, force)),
Effect.onExit((exit) =>
Effect.sync(() => {
execution.settling = true
}).pipe(Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void)),
),
Effect.onExit((exit) => options.settled?.(key, exit) ?? Effect.void),
Effect.onExit((exit) => Effect.sync(() => settle(key, execution, exit))),
Effect.exit,
Effect.asVoid,
@@ -99,7 +85,7 @@ export const make = <Key, E, Reason = never>(options: {
// A doorbell that survives the execution loop (rung after the loop decided to end, or
// during failure or interruption cleanup) starts a fresh execution for the remaining work.
const settle = (key: Key, execution: Execution<E, Reason>, exit: Exit.Exit<void, E>) => {
const settle = (key: Key, execution: Execution<E>, exit: Exit.Exit<void, E>) => {
if (execution.pendingWake) start(key, false)
else executions.delete(key)
Deferred.doneUnsafe(execution.done, exit)
@@ -126,13 +112,12 @@ export const make = <Key, E, Reason = never>(options: {
start(key, false)
})
const interrupt = (key: Key, reason?: Reason): Effect.Effect<void> =>
const interrupt = (key: Key): Effect.Effect<void> =>
Effect.suspend(() => {
const execution = executions.get(key)
if (execution?.owner === undefined || execution.stopping || execution.settling) return Effect.void
if (execution?.owner === undefined) return Effect.void
execution.stopping = true
execution.pendingWake = false
execution.interruptionReason = reason
return Fiber.interrupt(execution.owner)
})
+2 -8
View File
@@ -3,19 +3,13 @@ export * as SessionRunner from "./index"
import type { LLMError } from "@opencode-ai/llm"
import { Context, Effect } from "effect"
import { SessionSchema } from "../schema"
import type { MessageDecodeError, StepFailedError, UserInterruptedError } from "../error"
import type { MessageDecodeError } from "../error"
import { SessionRunnerModel } from "./model"
import type { SystemContext } from "../../system-context/index"
import type { ToolOutputStore } from "../../tool-output-store"
export type RunError =
| LLMError
| SessionRunnerModel.Error
| MessageDecodeError
| StepFailedError
| UserInterruptedError
| SystemContext.InitializationBlocked
| ToolOutputStore.Error
LLMError | SessionRunnerModel.Error | MessageDecodeError | SystemContext.InitializationBlocked | ToolOutputStore.Error
/** Runs one local continuation from already-recorded Session history. */
export interface Interface {
+33 -103
View File
@@ -10,8 +10,7 @@ import {
isContextOverflowFailure,
type ProviderErrorEvent,
} from "@opencode-ai/llm"
import { SessionError } from "@opencode-ai/schema/session-error"
import { Cause, Effect, Exit, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { Cause, DateTime, Effect, Exit, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
import { AgentV2 } from "../../agent"
import { Config } from "../../config"
import { Database } from "../../database/database"
@@ -32,7 +31,6 @@ import { SessionCompaction } from "../compaction"
import { SessionEvent } from "../event"
import { SessionHistory } from "../history"
import { SessionInput } from "../input"
import { SessionMessage } from "../message"
import { SessionSchema } from "../schema"
import { SessionStore } from "../store"
import { SessionTitle } from "../title"
@@ -45,9 +43,6 @@ import { SessionRunnerSystemPrompt } from "./system-prompt"
import { Snapshot } from "../../snapshot"
import { makeLocationNode } from "../../effect/app-node"
import { llmClient } from "../../effect/app-node-platform"
import { StepFailedError, UserInterruptedError } from "../error"
import { toSessionError } from "../to-session-error"
import { SessionRunnerRetry } from "./retry"
/**
* Runs one durable coding-agent Session until it settles.
@@ -58,10 +53,10 @@ import { SessionRunnerRetry } from "./retry"
* - Session ownership and controls
* - [x] Coordinate one local active drain per Session; explicit resumes join and prompt wakeups coalesce.
* - [ ] Replace local ownership with durable multi-node ownership when clustered.
* - [x] Publish durable historical execution lifecycle and bounded retry observations.
* - [ ] Mark busy, retrying, idle, interrupted, or terminal-failure status durably.
* - [ ] Honor interruption and reject stale work after runtime attachment replacement.
* - [x] Honor optional agent step limits.
* - [ ] Bound repeated identical tool calls (provider retries are bounded).
* - [ ] Bound provider retries and repeated identical tool calls.
*
* - Runtime context assembly
* - Track V1 runtime-context parity canonically in `specs/v2/session.md`.
@@ -70,7 +65,7 @@ import { SessionRunnerRetry } from "./retry"
* - [x] Translate every projected V2 Session message variant into canonical
* `@opencode-ai/llm` messages.
* - [ ] Resolve policy-filtered built-in, MCP, plugin, and structured-output tool definitions.
* - [x] Stream exactly one `llm.stream(request)` call per attempt.
* - [x] Stream exactly one `llm.stream(request)` physical attempt.
* - [x] Persist assistant text and usage events incrementally as they arrive.
* - [ ] Persist snapshots, patches, and retry notices incrementally as they arrive.
* - [x] Persist reasoning, provider errors, and tool-call events incrementally as they arrive.
@@ -91,7 +86,7 @@ import { SessionRunnerRetry } from "./retry"
* - [ ] Coalesce streamed deltas and add covering projected-history indexes.
* - [ ] Update title, summaries, compaction state, and cleanup in bounded background work.
*
* Use `llm.stream(request)` for each attempt. Keep tool execution and continuation here.
* Use `llm.stream(request)` for each physical attempt. Keep tool execution and continuation here.
* Durable continuation recovery remains a separate future slice with an explicit retry policy.
*
* The current slice loads V2 history, translates it, resolves a model through a core service, and persists one
@@ -141,14 +136,17 @@ const layer = Layer.effect(
sessionID,
assistantMessageID: message.id,
callID: tool.id,
error: { type: "tool.stale", message: "Tool execution interrupted", name: tool.name },
executed: tool.executed === true,
error: { type: "unknown", message: "Tool execution interrupted" },
provider: {
executed: tool.provider?.executed === true,
...(tool.provider?.metadata === undefined ? {} : { metadata: tool.provider.metadata }),
},
})
}
}
})
const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error | UserInterruptedError>) =>
const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error>) =>
Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers))
// Match V1: dismissing a question halts the loop instead of becoming model-facing tool output.
@@ -173,7 +171,6 @@ const layer = Layer.effect(
promotion: SessionInput.Delivery | undefined,
step: number,
recoverOverflow?: typeof compaction.compactAfterOverflow,
assistantMessageID?: SessionMessage.ID,
) {
const session = yield* getSession(sessionID)
if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID)
@@ -187,7 +184,7 @@ const layer = Layer.effect(
loadSystemContext(agent, session.id),
session.id,
)
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error | UserInterruptedError>()
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
let needsContinuation = false
let currentStep = step
if (promotion) {
@@ -231,23 +228,21 @@ const layer = Layer.effect(
// The selected catalog identity, not model.id: route-level ids are provider API
// model ids (for example gpt-5.5-fast resolves to api id gpt-5.5).
model: resolved.ref,
provider: model.provider,
snapshot: startSnapshot,
assistantMessageID,
})
const publication = Semaphore.makeUnsafe(1)
// Durable publishes are serialized so tool fibers and step settlement never interleave
// mid-event.
const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = [], error?: SessionError.Error) =>
serialized(publisher.publish(event, outputPaths, error))
const publish = (event: LLMEvent, outputPaths: ReadonlyArray<string> = []) =>
serialized(publisher.publish(event, outputPaths))
let overflowFailure: ProviderErrorEvent | undefined
const providerStream = llm.stream(request).pipe(
Stream.runForEach((event) =>
Effect.gen(function* () {
if (overflowFailure || publisher.hasProviderError()) return
if (LLMEvent.is.providerError(event)) {
if (isContextOverflowFailure(event) && !publisher.hasRetryEvidence()) {
if (isContextOverflowFailure(event) && !publisher.hasAssistantStarted()) {
overflowFailure = event
return
}
@@ -255,12 +250,7 @@ const layer = Layer.effect(
yield* publish(event)
if (event.type !== "tool-call" || event.providerExecuted) return
if (!toolMaterialization) {
yield* serialized(
publisher.failUnsettledTools({
type: "tool.execution",
message: "Tools are disabled after the maximum agent steps",
}),
)
yield* serialized(publisher.failUnsettledTools("Tools are disabled after the maximum agent steps"))
return
}
needsContinuation = true
@@ -283,15 +273,6 @@ const layer = Layer.effect(
output: settlement.output,
}),
settlement.outputPaths ?? [],
settlement.error,
).pipe(
Effect.andThen(
settlement.error?.type === "permission.rejected"
? serialized(publisher.failAssistant(settlement.error)).pipe(
Effect.andThen(Effect.fail(new UserInterruptedError())),
)
: Effect.void,
),
),
),
),
@@ -338,7 +319,7 @@ const layer = Layer.effect(
// restart the step instead of surfacing the provider error.
if (
recoverOverflow &&
!publisher.hasRetryEvidence() &&
!publisher.hasAssistantStarted() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, request })))
)
@@ -350,26 +331,8 @@ const layer = Layer.effect(
if (overflowFailure) yield* publish(overflowFailure)
const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
if (llmFailure && !publisher.hasProviderError()) {
const error = toSessionError(llmFailure)
if (
SessionRunnerRetry.isRetryable(llmFailure) &&
!publisher.hasRetryEvidence() &&
(agent.info?.steps === undefined || currentStep < agent.info.steps)
) {
return yield* new SessionRunnerRetry.RetryableFailure({
cause: llmFailure,
assistantMessageID: yield* publisher.startAssistant(),
error,
step: currentStep,
})
}
yield* serialized(
publisher.failUnsettledTools(
{ type: "tool.result-missing", message: "Provider did not return a tool result" },
true,
),
)
yield* serialized(publisher.failAssistant(error))
yield* serialized(publisher.failUnsettledTools("Provider did not return a tool result", true))
yield* serialized(publisher.failAssistant(llmFailure.reason.message))
}
// Provider error events only arrive from the stream, so the flag is final here.
const providerFailed = publisher.hasProviderError()
@@ -379,30 +342,27 @@ const layer = Layer.effect(
const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit)
const toolsInterrupted = settled._tag === "Failure" && Cause.hasInterrupts(settled.cause)
const questionDismissed = settled._tag === "Failure" && isQuestionRejected(settled.cause)
const settledError =
settled._tag === "Failure" ? Option.getOrUndefined(Cause.findErrorOption(settled.cause)) : undefined
const permissionRejected = settledError instanceof UserInterruptedError
if (questionDismissed || permissionRejected || streamInterrupted || toolsInterrupted) {
if (questionDismissed || streamInterrupted || toolsInterrupted) {
yield* FiberSet.clear(toolFibers)
yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" }))
yield* serialized(publisher.failAssistant({ type: "aborted", message: "Step interrupted" }))
yield* serialized(publisher.failUnsettledTools("Tool execution interrupted"))
yield* serialized(publisher.failAssistant("Step interrupted"))
// Match V1: dismissing a question halts the loop like an interruption.
if (questionDismissed || permissionRejected) return yield* new UserInterruptedError()
if (questionDismissed) return yield* Effect.interrupt
}
// A settled tool fiber failure is one of two things. A defect from a tool
// implementation becomes a failed tool call the model can read, and the step still
// settles so the model may recover. A typed infrastructure failure (tool output
// could not be persisted) also fails the assistant and then fails the drain.
const settledFailure =
settled._tag === "Failure" && !toolsInterrupted && !permissionRejected ? settled.cause : undefined
const settledFailure = settled._tag === "Failure" && !toolsInterrupted ? settled.cause : undefined
const infraError =
settledFailure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(settledFailure))
if (settledFailure !== undefined) {
const failure = infraError ?? Cause.squash(settledFailure)
const error = toSessionError(failure)
yield* serialized(publisher.failUnsettledTools(error))
if (infraError !== undefined) yield* serialized(publisher.failAssistant(error))
const message = failure instanceof Error ? failure.message : String(failure)
yield* serialized(publisher.failUnsettledTools(`Tool execution failed: ${message}`))
if (infraError !== undefined)
yield* serialized(publisher.failAssistant(`Tool execution failed: ${message}`))
}
const stepSettlement = publisher.stepSettlement()
@@ -411,21 +371,13 @@ const layer = Layer.effect(
if (stepSettlement && stepEndedCleanly) yield* publishStepEnd(stepSettlement)
// A provider error orphans recorded local calls; a clean stream can still leave
// hosted calls without results.
if (providerFailed)
yield* serialized(publisher.failUnsettledTools({ type: "aborted", message: "Tool execution interrupted" }))
if (providerFailed) yield* serialized(publisher.failUnsettledTools("Tool execution interrupted"))
if (stream._tag === "Success" && !providerFailed)
yield* serialized(
publisher.failUnsettledTools(
{ type: "tool.result-missing", message: "Provider did not return a tool result" },
true,
),
)
yield* serialized(publisher.failUnsettledTools("Provider did not return a tool result", true))
if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause)
if (settled._tag === "Failure" && (toolsInterrupted || infraError !== undefined))
return yield* Effect.failCause(settled.cause)
const stepFailure = publisher.stepFailure()
if (stepFailure) return yield* new StepFailedError({ error: stepFailure })
return {
_tag: "Completed",
needsContinuation: !providerFailed && needsContinuation,
@@ -446,31 +398,8 @@ const layer = Layer.effect(
let recoverOverflow: typeof compaction.compactAfterOverflow | undefined = compaction.compactAfterOverflow
let currentPromotion = promotion
let currentStep = step
let assistantMessageID: SessionMessage.ID | undefined
while (true) {
const attempt = yield* Effect.suspend(() =>
attemptStep(sessionID, currentPromotion, currentStep, recoverOverflow, assistantMessageID),
).pipe(
Effect.tapError((error) =>
error instanceof SessionRunnerRetry.RetryableFailure
? Effect.sync(() => {
currentStep = error.step + 1
assistantMessageID = error.assistantMessageID
currentPromotion = undefined
})
: Effect.void,
),
Effect.retryOrElse(SessionRunnerRetry.schedule(events, sessionID), (error) => {
if (!(error instanceof SessionRunnerRetry.RetryableFailure)) return Effect.fail(error)
return events
.publish(SessionEvent.Step.Failed, {
sessionID,
assistantMessageID: error.assistantMessageID,
error: error.error,
})
.pipe(Effect.andThen(Effect.fail(error.cause)))
}),
)
const attempt = yield* attemptStep(sessionID, currentPromotion, currentStep, recoverOverflow)
if (attempt._tag === "Completed") return { needsContinuation: attempt.needsContinuation, step: attempt.step }
if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined
yield* Effect.yieldNow
@@ -479,7 +408,8 @@ const layer = Layer.effect(
}
})
// Execution lifecycle is published per busy period by SessionExecution, not per drain here.
// ExecutionSettled is published per execution (busy period) by SessionExecution, not per
// drain here.
const drain = Effect.fn("SessionRunner.drain")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly force: boolean
@@ -1,19 +1,16 @@
import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue, type Usage } from "@opencode-ai/llm"
import { Effect } from "effect"
import { DateTime, Effect } from "effect"
import { EventV2 } from "../../event"
import { ModelV2 } from "../../model"
import { SessionEvent } from "../event"
import { SessionMessage } from "../message"
import { SessionSchema } from "../schema"
import { SessionError } from "@opencode-ai/schema/session-error"
type Input = {
readonly sessionID: SessionSchema.ID
readonly agent: string
readonly model: ModelV2.Ref
readonly provider: string
readonly snapshot?: string
readonly assistantMessageID?: SessionMessage.ID
}
const safe = (value: number | undefined) => Math.max(0, Number.isFinite(value) ? (value ?? 0) : 0)
@@ -44,10 +41,10 @@ const message = (value: unknown) => {
type SettledOutput =
| { readonly structured: Record<string, unknown>; readonly content: ToolOutput["content"] }
| { readonly error: SessionError.Error }
| { readonly error: { readonly type: "unknown"; readonly message: string } }
const settledOutput = (value: ToolOutput | undefined, result: ToolResultValue): SettledOutput => {
if (result.type === "error") return { error: { type: "tool.execution", message: message(result.value) } }
if (result.type === "error") return { error: { type: "unknown", message: message(result.value) } }
const settled = value ?? ToolOutput.fromResultValue(result)
if (!settled) throw new Error(`Unsupported tool result: ${message(result)}`)
return { structured: record(settled.structured), content: settled.content }
@@ -64,25 +61,20 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
called: boolean
settled: boolean
providerExecuted: boolean
providerMetadata?: ProviderMetadata
}
>()
let assistantMessageID = input.assistantMessageID
let stepStarted = false
const timestamp = DateTime.now
let assistantMessageID: SessionMessage.ID | undefined
let assistantActive = false
let assistantFailed = false
let providerFailed = false
let retryEvidence = false
let stepFailure: SessionError.Error | undefined
let stepSettlement:
| {
readonly finish: Extract<LLMEvent, { type: "step-finish" }>["reason"]
readonly tokens: ReturnType<typeof tokens>
}
| undefined
let stepSettlement: { readonly finish: string; readonly tokens: ReturnType<typeof tokens> } | undefined
const startAssistant = Effect.fnUntraced(function* () {
if (stepStarted && assistantMessageID !== undefined) return assistantMessageID
assistantMessageID ??= SessionMessage.ID.create()
stepStarted = true
if (assistantMessageID !== undefined) return assistantMessageID
assistantMessageID = SessionMessage.ID.create()
assistantActive = true
yield* events.publish(SessionEvent.Step.Started, {
...input,
assistantMessageID,
@@ -94,18 +86,15 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
assistantMessageID === undefined
? Effect.die(new Error("Tool event before assistant step start"))
: Effect.succeed(assistantMessageID)
const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.provider]
const fragments = (
name: string,
ended: (id: string, value: string, state?: Record<string, unknown>) => Effect.Effect<void>,
single = false,
ended: (id: string, value: string, providerMetadata?: ProviderMetadata) => Effect.Effect<void>,
) => {
const chunks = new Map<string, string[]>()
const start = (id: string) =>
Effect.suspend(() => {
if (chunks.has(id)) return Effect.die(new Error(`Duplicate ${name} start: ${id}`))
if (single && chunks.size > 0) return Effect.die(new Error(`${name} start before end: ${id}`))
chunks.set(id, [])
return Effect.void
})
@@ -116,10 +105,10 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
current.push(value)
return Effect.void
})
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>) {
const end = Effect.fnUntraced(function* (id: string, providerMetadata?: ProviderMetadata) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
yield* ended(id, current.join(""), state)
yield* ended(id, current.join(""), providerMetadata)
chunks.delete(id)
})
const flush = Effect.fnUntraced(function* () {
@@ -128,30 +117,26 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return { start, append, end, flush }
}
const text = fragments(
"text",
(_textID, value) =>
Effect.gen(function* () {
yield* events.publish(SessionEvent.Text.Ended, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
text: value,
})
}),
true,
const text = fragments("text", (textID, value) =>
Effect.gen(function* () {
yield* events.publish(SessionEvent.Text.Ended, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
textID,
text: value,
})
}),
)
const reasoning = fragments(
"reasoning",
(_reasoningID, value, state) =>
Effect.gen(function* () {
yield* events.publish(SessionEvent.Reasoning.Ended, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
text: value,
state,
})
}),
true,
const reasoning = fragments("reasoning", (reasoningID, value, providerMetadata) =>
Effect.gen(function* () {
yield* events.publish(SessionEvent.Reasoning.Ended, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
reasoningID,
text: value,
providerMetadata,
})
}),
)
const toolInput = fragments("tool input", (callID, value) =>
Effect.gen(function* () {
@@ -206,21 +191,21 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
yield* flushFragments()
})
const failAssistant = Effect.fnUntraced(function* (error: SessionError.Error) {
const failAssistant = Effect.fnUntraced(function* (message: string) {
if (assistantFailed) return
yield* flush()
const assistantMessageID = yield* startAssistant()
assistantActive = false
assistantFailed = true
stepFailure = error
yield* events.publish(SessionEvent.Step.Failed, {
sessionID: input.sessionID,
assistantMessageID,
error,
error: { type: "unknown", message },
})
})
const failUnsettledTools = Effect.fn("SessionRunner.failUnsettledTools")(function* (
error: SessionError.Error,
message: string,
hostedOnly = false,
) {
for (const [callID, tool] of tools) {
@@ -230,8 +215,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
callID,
error,
executed: tool.providerExecuted,
error: { type: "unknown", message },
provider: {
executed: tool.providerExecuted,
...(tool.providerMetadata === undefined ? {} : { metadata: tool.providerMetadata }),
},
})
}
})
@@ -244,18 +232,16 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const publish = Effect.fn("SessionRunner.publishLLMEvent")(function* (
event: LLMEvent,
outputPaths: ReadonlyArray<string> = [],
error?: SessionError.Error,
) {
switch (event.type) {
case "step-start":
yield* startAssistant()
return
case "text-start":
retryEvidence = true
yield* text.start(event.id)
yield* events.publish(SessionEvent.Text.Started, {
sessionID: input.sessionID,
assistantMessageID: yield* startAssistant(),
textID: event.id,
})
return
case "text-delta":
@@ -263,6 +249,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
yield* events.publish(SessionEvent.Text.Delta, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
textID: event.id,
delta: event.text,
})
return
@@ -270,12 +257,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
yield* text.end(event.id)
return
case "reasoning-start":
retryEvidence = true
yield* reasoning.start(event.id)
yield* events.publish(SessionEvent.Reasoning.Started, {
sessionID: input.sessionID,
assistantMessageID: yield* startAssistant(),
state: providerState(event.providerMetadata),
reasoningID: event.id,
providerMetadata: event.providerMetadata,
})
return
case "reasoning-delta":
@@ -283,14 +270,14 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
yield* events.publish(SessionEvent.Reasoning.Delta, {
sessionID: input.sessionID,
assistantMessageID: yield* currentAssistantMessageID(),
reasoningID: event.id,
delta: event.text,
})
return
case "reasoning-end":
yield* reasoning.end(event.id, providerState(event.providerMetadata))
yield* reasoning.end(event.id, event.providerMetadata)
return
case "tool-input-start":
retryEvidence = true
yield* startToolInput(event)
return
case "tool-input-delta": {
@@ -312,7 +299,6 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
yield* endToolInput(event)
return
case "tool-call": {
retryEvidence = true
if (!tools.has(event.id)) yield* startToolInput(event)
const tool = tools.get(event.id)!
if (!tool.inputEnded) yield* endToolInput(event)
@@ -321,19 +307,21 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
tool.called = true
tool.providerExecuted = event.providerExecuted === true
const state = providerState(event.providerMetadata)
tool.providerMetadata = event.providerMetadata
yield* events.publish(SessionEvent.Tool.Called, {
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
callID: event.id,
tool: event.name,
input: record(event.input),
executed: tool.providerExecuted,
state,
provider: {
executed: tool.providerExecuted,
...(event.providerMetadata === undefined ? {} : { metadata: event.providerMetadata }),
},
})
return
}
case "tool-result": {
retryEvidence = true
const tool = tools.get(event.id)
if (!tool?.called) return yield* Effect.die(new Error(`Tool result before call: ${event.id}`))
if (tool.name !== event.name)
@@ -343,9 +331,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return yield* Effect.die(new Error(`Duplicate tool result: ${event.id}`))
}
tool.settled = true
const result = error ? { error } : settledOutput(event.output, event.result)
const executed = event.providerExecuted === true || tool.providerExecuted
const resultState = providerState(event.providerMetadata)
const result = settledOutput(event.output, event.result)
const provider = {
executed: event.providerExecuted === true || tool.providerExecuted,
...(event.providerMetadata === undefined ? {} : { metadata: event.providerMetadata }),
}
if ("error" in result) {
yield* events.publish(SessionEvent.Tool.Failed, {
sessionID: input.sessionID,
@@ -353,8 +343,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
callID: event.id,
error: result.error,
result: event.result,
executed,
resultState,
provider,
})
return
}
@@ -364,14 +353,12 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
callID: event.id,
...result,
outputPaths,
...(executed ? { result: event.result } : {}),
executed,
resultState,
...(provider.executed ? { result: event.result } : {}),
provider,
})
return
}
case "tool-error": {
retryEvidence = true
const tool = tools.get(event.id)
if (!tool?.called) return yield* Effect.die(new Error(`Tool error before call: ${event.id}`))
if (tool.name !== event.name)
@@ -382,30 +369,25 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
sessionID: input.sessionID,
assistantMessageID: tool.assistantMessageID,
callID: event.id,
error:
event.message === `Unknown tool: ${event.name}`
? { type: "tool.unknown", message: event.message, name: event.name }
: { type: "tool.execution", message: event.message },
executed: tool.providerExecuted,
resultState: providerState(event.providerMetadata),
error: { type: "unknown", message: event.message },
provider: {
executed: tool.providerExecuted,
...(event.providerMetadata === undefined ? {} : { metadata: event.providerMetadata }),
},
})
return
}
case "step-finish":
yield* flush()
assistantActive = false
if (stepSettlement) return yield* Effect.die(new Error("Duplicate step finish"))
if (event.reason === "content-filter") {
providerFailed = true
yield* failAssistant({ type: "provider.content-filter", message: "Provider blocked the response" })
return
}
stepSettlement = { finish: event.reason, tokens: tokens(event.usage) }
return
case "finish":
return
case "provider-error":
providerFailed = true
yield* failAssistant({ type: "provider.unknown", message: event.message })
yield* failAssistant(event.message)
return
}
})
@@ -415,9 +397,9 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
flush,
failAssistant,
failUnsettledTools,
hasActiveAssistant: () => assistantActive,
hasAssistantStarted: () => assistantMessageID !== undefined,
hasProviderError: () => providerFailed,
hasRetryEvidence: () => retryEvidence,
stepFailure: () => stepFailure,
stepSettlement: () => stepSettlement,
startAssistant,
assistantMessageID: assistantMessageIDForTool,
-67
View File
@@ -1,67 +0,0 @@
export * as SessionRunnerRetry from "./retry"
import { LLMError } from "@opencode-ai/llm"
import { SessionError } from "@opencode-ai/schema/session-error"
import { Data, Duration, Effect, Schedule } from "effect"
import { EventV2 } from "../../event"
import { SessionEvent } from "../event"
import { SessionMessage } from "../message"
import { SessionSchema } from "../schema"
import type { SessionRunner } from "./index"
export class RetryableFailure extends Data.TaggedError("SessionRunner.RetryableFailure")<{
readonly cause: LLMError
readonly assistantMessageID: SessionMessage.ID
readonly error: SessionError.Error
readonly step: number
}> {}
export function isRetryable(error: LLMError) {
switch (error.reason._tag) {
case "RateLimit":
case "ProviderInternal":
case "Transport":
return true
case "Authentication":
case "QuotaExceeded":
case "ContentPolicy":
case "InvalidProviderOutput":
case "InvalidRequest":
case "NoRoute":
case "UnknownProvider":
return false
default: {
const exhaustive: never = error.reason
return exhaustive
}
}
}
const retryAfter = (failure: RetryableFailure) => {
if (failure.cause.reason._tag === "RateLimit" || failure.cause.reason._tag === "ProviderInternal")
return failure.cause.reason.retryAfterMs
return undefined
}
export const schedule = (events: EventV2.Interface, sessionID: SessionSchema.ID) =>
Schedule.exponential("2 seconds").pipe(
Schedule.take(4),
Schedule.setInputType<RetryableFailure | SessionRunner.RunError>(),
Schedule.passthrough,
Schedule.while(({ input }) => input instanceof RetryableFailure),
Schedule.modifyDelay((failure, delay) => {
const minimum = failure instanceof RetryableFailure ? retryAfter(failure) : undefined
return Effect.succeed(minimum === undefined ? delay : Duration.max(delay, Duration.millis(minimum)))
}),
Schedule.tap((metadata) =>
metadata.input instanceof RetryableFailure
? events.publish(SessionEvent.RetryScheduled, {
sessionID,
assistantMessageID: metadata.input.assistantMessageID,
attempt: metadata.attempt + 1,
at: metadata.now + Duration.toMillis(metadata.duration),
error: metadata.input.error,
})
: Effect.void,
),
)
@@ -21,11 +21,6 @@ const media = (file: FileAttachment): ContentPart => ({
const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const providerMetadata = (
provider: string,
state: Record<string, unknown> | undefined,
): ProviderMetadata | undefined => (state === undefined ? undefined : { [provider]: state })
const toolInput = (tool: SessionMessage.AssistantTool) =>
tool.state.status === "pending"
? Option.getOrElse(decodeToolInput(tool.state.input), () => tool.state.input)
@@ -36,7 +31,7 @@ const toolCall = (tool: SessionMessage.AssistantTool, providerMetadata: Provider
id: tool.id,
name: tool.name,
input: toolInput(tool),
providerExecuted: tool.executed,
providerExecuted: tool.provider?.executed,
providerMetadata,
})
@@ -45,14 +40,14 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
// TODO: Materialize remote and managed URIs before provider-history lowering.
// ToolOutput.toResultValue rejects unresolved URIs rather than treating them as media bytes.
const result =
tool.executed === true && tool.state.result !== undefined
tool.provider?.executed === true && tool.state.result !== undefined
? tool.state.result
: ToolOutput.toResultValue({ structured: tool.state.structured, content: tool.state.content })
return ToolResultPart.make({
id: tool.id,
name: tool.name,
result,
providerExecuted: tool.executed,
providerExecuted: tool.provider?.executed,
providerMetadata,
})
}
@@ -61,11 +56,11 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
id: tool.id,
name: tool.name,
result:
tool.executed === true && tool.state.result !== undefined
tool.provider?.executed === true && tool.state.result !== undefined
? tool.state.result
: { error: tool.state.error, content: tool.state.content, structured: tool.state.structured },
resultType: "error",
providerExecuted: tool.executed,
providerExecuted: tool.provider?.executed,
providerMetadata,
})
}
@@ -83,22 +78,17 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
{
type: "reasoning",
text: item.text,
providerMetadata: reuseProviderMetadata ? providerMetadata(model.provider, item.state) : undefined,
providerMetadata: reuseProviderMetadata ? item.providerMetadata : undefined,
},
]
: item.text.length > 0
? [{ type: "text", text: item.text }]
: []
const call = toolCall(
item,
reuseProviderMetadata ? providerMetadata(model.provider, item.providerState) : undefined,
)
if (item.executed !== true) return [call]
const call = toolCall(item, reuseProviderMetadata ? item.provider?.metadata : undefined)
if (item.provider?.executed !== true) return [call]
const result = toolResult(
item,
reuseProviderMetadata
? providerMetadata(model.provider, item.providerResultState ?? item.providerState)
: undefined,
reuseProviderMetadata ? (item.provider.resultMetadata ?? item.provider.metadata) : undefined,
)
return result ? [call, result] : [call]
})
@@ -108,14 +98,9 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
return part.text !== "" || (part.providerMetadata !== undefined && Object.keys(part.providerMetadata).length > 0)
})
const results = message.content
.filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.executed !== true)
.filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true)
.map((item) =>
toolResult(
item,
reuseProviderMetadata
? providerMetadata(model.provider, item.providerResultState ?? item.providerState)
: undefined,
),
toolResult(item, reuseProviderMetadata ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined),
)
.filter((message) => message !== undefined)
.map(Message.tool)
@@ -1,65 +0,0 @@
import { LLMError, ToolFailure } from "@opencode-ai/llm"
import { SessionError } from "@opencode-ai/schema/session-error"
import { PermissionV2 } from "../permission"
import { QuestionV2 } from "../question"
import { Integration } from "../integration"
import { ToolOutputStore } from "../tool-output-store"
import { StepFailedError, UserInterruptedError } from "./error"
import { SessionRunnerModel } from "./runner/model"
export function toSessionError(cause: unknown): SessionError.Error {
if (cause instanceof LLMError) {
switch (cause.reason._tag) {
case "RateLimit":
return {
type: "provider.rate-limit",
message: cause.reason.message,
retryAfterMs: cause.reason.retryAfterMs,
}
case "Authentication":
return { type: "provider.auth", message: cause.reason.message }
case "QuotaExceeded":
return { type: "provider.quota", message: cause.reason.message }
case "ContentPolicy":
return { type: "provider.content-filter", message: cause.reason.message }
case "Transport":
return { type: "provider.transport", message: cause.reason.message }
case "ProviderInternal":
return { type: "provider.internal", message: cause.reason.message }
case "InvalidProviderOutput":
return { type: "provider.invalid-output", message: cause.reason.message }
case "InvalidRequest":
return { type: "provider.invalid-request", message: cause.reason.message }
case "NoRoute":
return { type: "provider.no-route", message: cause.reason.message }
case "UnknownProvider":
return { type: "provider.unknown", message: cause.reason.message }
default: {
const exhaustive: never = cause.reason
return exhaustive
}
}
}
if (cause instanceof PermissionV2.DeniedError || cause instanceof PermissionV2.RejectedError)
return {
type: "permission.rejected",
message: cause.message,
permission: cause.permission,
resources: [...cause.resources],
}
if (cause instanceof QuestionV2.RejectedError) return { type: "aborted", message: cause.message, reason: "user" }
if (cause instanceof ToolFailure)
return cause.error === undefined ? { type: "tool.execution", message: cause.message } : toSessionError(cause.error)
if (cause instanceof StepFailedError) return cause.error
if (cause instanceof UserInterruptedError) return { type: "aborted", message: cause.message, reason: "user" }
if (
cause instanceof SessionRunnerModel.ModelNotSelectedError ||
cause instanceof SessionRunnerModel.ModelUnavailableError ||
cause instanceof SessionRunnerModel.VariantUnavailableError ||
cause instanceof SessionRunnerModel.UnsupportedApiError
)
return { type: "provider.no-route", message: cause.message }
if (cause instanceof Integration.AuthorizationError) return { type: "provider.auth", message: cause.message }
if (cause instanceof ToolOutputStore.StorageError) return { type: "unknown", message: cause.message }
return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) }
}
+2 -1
View File
@@ -28,7 +28,8 @@ Leaves own resolution, permission, and side-effect ordering. Translate only expe
## Registration
Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`.
Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`. Registrations may provide a
group, which flattens direct model names to `<group>_<tool>`, and may be deferred from direct model exposure.
Registrations are scoped:
+5 -5
View File
@@ -73,12 +73,12 @@ export const Plugin = {
toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
execute: (input, context) => {
const applied: Array<typeof Applied.Type> = []
const fail = (path: string, error?: unknown) => {
const fail = (path: string) => {
const prefix =
applied.length === 0
? `Unable to apply patch at ${path}`
: `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
return new ToolFailure({ message: prefix, error })
return new ToolFailure({ message: prefix })
}
return Effect.gen(function* () {
const source = {
@@ -150,7 +150,7 @@ export const Plugin = {
before,
after: update.content,
})
}).pipe(Effect.mapError((error) => fail(hunk.path, error)))
}).pipe(Effect.mapError(() => fail(hunk.path)))
}
const patchFiles = prepared.map(patchFile)
@@ -180,11 +180,11 @@ export const Plugin = {
content: change.content,
})
applied.push({ type: change.type, resource: result.resource, target: result.target })
}).pipe(Effect.mapError((error) => fail(change.path, error))),
}).pipe(Effect.mapError(() => fail(change.path))),
{ discard: true },
)
return { applied, files: patchFiles }
}).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail("patch", error))))
}).pipe(Effect.mapError((error) => (error instanceof ToolFailure ? error : fail("patch"))))
},
}),
"edit",
+1 -2
View File
@@ -111,9 +111,8 @@ export const Plugin = {
error instanceof FileMutation.StaleContentError
? new ToolFailure({
message: "File changed after permission approval. Read it again before editing.",
error,
})
: new ToolFailure({ message: `Unable to edit ${input.path}`, error }),
: new ToolFailure({ message: `Unable to edit ${input.path}` }),
),
)
+202
View File
@@ -0,0 +1,202 @@
export * as ExecuteTool from "./execute"
import { CodeMode, Tool, toolError } from "@opencode-ai/codemode"
import { ToolOutput } from "@opencode-ai/llm"
import { Effect, Ref, Schema } from "effect"
import { definition, make, settle, type AnyTool } from "./tool"
const ExecuteFile = Schema.Struct({
data: Schema.String,
mime: Schema.String,
name: Schema.optionalKey(Schema.String),
})
const ExecuteCall = Schema.Struct({
tool: Schema.String,
status: Schema.Literals(["running", "completed", "error"]),
input: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)),
})
type ExecuteCall = typeof ExecuteCall.Type
const ExecuteMetadata = Schema.Struct({
toolCalls: Schema.Array(ExecuteCall),
error: Schema.optionalKey(Schema.Literal(true)),
})
const ExecuteOutput = Schema.Struct({
output: Schema.String,
toolCalls: Schema.Array(ExecuteCall),
error: Schema.optionalKey(Schema.Literal(true)),
files: Schema.Array(ExecuteFile),
})
type CollectedFiles = {
readonly index: number
readonly files: Array<typeof ExecuteFile.Type>
}
export interface Registration {
readonly identity: object
readonly tool: AnyTool
readonly name: string
readonly group?: string
}
export const create = (options: {
readonly registrations: ReadonlyMap<string, Registration>
readonly current: (name: string) => Registration | undefined
}) => {
const runtime = (
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
hooks?: CodeMode.ToolCallHooks,
) => {
const tools: Record<string, Tool.Definition<never> | Record<string, Tool.Definition<never>>> = {}
for (const [name, registration] of options.registrations) {
const child = definition(name, registration.tool)
const value = Tool.make({
description: child.description,
input: child.inputSchema,
output: child.outputSchema,
run: (input) => invoke(name, registration, input),
})
if (registration.group === undefined) {
const path = registration.name
if (Object.hasOwn(tools, path)) throw new TypeError(`Deferred tool namespace conflict: ${path}`)
tools[path] = value
continue
}
const path = registration.name
const namespace = registration.group
const group = tools[namespace]
if (group && Tool.isDefinition(group)) throw new TypeError(`Deferred tool namespace conflict: ${namespace}`)
if (group) {
if (Object.hasOwn(group, path)) throw new TypeError(`Deferred tool namespace conflict: ${namespace}.${path}`)
group[path] = value
continue
}
const entries: Record<string, Tool.Definition<never>> = {}
entries[path] = value
tools[namespace] = entries
}
return CodeMode.make<typeof tools>({ tools, ...hooks })
}
const discovery = runtime(() => Effect.fail(toolError("Execute context is unavailable")))
return make({
description: discovery.instructions(),
input: CodeMode.Input,
output: ExecuteOutput,
structured: ExecuteMetadata,
toStructuredOutput: ({ output }) => ({
toolCalls: output.toolCalls,
...(output.error ? { error: true as const } : {}),
}),
toModelOutput: ({ output }) => [
{ type: "text" as const, text: output.output },
...output.files.map((file) => ({
type: "file" as const,
data: file.data,
mime: file.mime,
...(file.name === undefined ? {} : { name: file.name }),
})),
],
execute: ({ code }, context) =>
Effect.gen(function* () {
const callIndex = yield* Ref.make(0)
const files = yield* Ref.make<Array<CollectedFiles>>([])
const calls = yield* Ref.make<Array<ExecuteCall>>([])
// TODO: Publish live call-list updates once V2 has a generic tool progress API.
const finalCalls = Ref.get(calls).pipe(
Effect.map((items) =>
items.map((call) => (call.status === "running" ? { ...call, status: "error" as const } : call)),
),
)
const result = yield* runtime(
(name, registration, input) =>
Effect.gen(function* () {
const index = yield* Ref.getAndUpdate(callIndex, (index) => index + 1)
const current = options.current(name)
if (!current || current.identity !== registration.identity)
return yield* Effect.fail(toolError(`Stale tool call: ${name}`))
const output = yield* settle(
current.tool,
{ type: "tool-call", id: context.toolCallID, name, input },
{
sessionID: context.sessionID,
agent: context.agent,
assistantMessageID: context.assistantMessageID,
toolCallID: context.toolCallID,
},
).pipe(Effect.mapError((failure) => toolError(failure.message, failure)))
const outputFileParts = outputFiles(output)
if (outputFileParts.length > 0)
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
return output.structured
}),
{
onToolCallStart: ({ index, name, input }) =>
Effect.gen(function* () {
const shown = displayInput(input)
yield* Ref.update(calls, (items) => {
const next = [...items]
next[index] = { tool: name, status: "running", ...(shown ? { input: shown } : {}) }
return next
})
}),
onToolCallEnd: ({ index, outcome }) =>
Ref.update(calls, (items) => {
const current = items[index]
if (!current) return items
const next = [...items]
next[index] = { ...current, status: outcome === "success" ? "completed" : "error" }
return next
}),
},
).execute(code)
const toolCalls = yield* finalCalls
const collected = (yield* Ref.get(files))
.toSorted((left, right) => left.index - right.index)
.flatMap((item) => item.files)
const output = formatResult(result)
return { output, toolCalls, files: collected, ...(result.ok ? {} : { error: true as const }) }
}),
})
}
function displayInput(input: unknown): Record<string, unknown> | undefined {
if (input === null || input === undefined) return
if (typeof input !== "object" || Array.isArray(input)) return { input }
if (Object.keys(input).length === 0) return
return input as Record<string, unknown>
}
function formatResult(result: CodeMode.Result) {
const output = result.ok
? formatValue(result.value)
: [result.error.message, ...(result.error.suggestions ?? []).filter((hint) => !result.error.message.includes(hint))]
.join("\n")
.trim()
if (!result.logs || result.logs.length === 0) return output
const logs = `Logs:\n${result.logs.join("\n")}`
return output === "" ? logs : `${output}\n\n${logs}`
}
function formatValue(value: CodeMode.DataValue) {
if (typeof value === "string") return value
return JSON.stringify(value, null, 2) ?? String(value)
}
function outputFiles(output: ToolOutput): Array<typeof ExecuteFile.Type> {
return output.content.flatMap((part) => {
if (part.type !== "file") return []
const prefix = `data:${part.mime};base64,`
if (!part.uri.startsWith(prefix)) return []
return [
{
data: part.uri.slice(prefix.length),
mime: part.mime,
...(part.name === undefined ? {} : { name: part.name }),
},
]
})
}
+13 -2
View File
@@ -5,6 +5,7 @@ import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { Effect, Schema } from "effect"
import path from "path"
import { FileSystem } from "../filesystem"
import { FSUtil } from "../fs-util"
import { Location } from "../location"
import { Ripgrep } from "../ripgrep"
import { RelativePath } from "../schema"
@@ -36,6 +37,7 @@ export const toModelOutput = (output: ModelOutput) => {
export const Plugin = {
id: "core-glob-tool",
effect: Effect.fn("GlobTool.Plugin")(function* (ctx: PluginContext) {
const fs = yield* FSUtil.Service
const ripgrep = yield* Ripgrep.Service
const location = yield* Location.Service
const permission = yield* PermissionV2.Service
@@ -71,6 +73,13 @@ export const Plugin = {
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
})
const cwd = path.resolve(location.directory, input.path ?? ".")
yield* fs
.stat(cwd)
.pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })),
),
)
return yield* ripgrep
.glob({
cwd,
@@ -88,8 +97,10 @@ export const Plugin = {
),
)
}).pipe(
Effect.mapError(
(error) => new ToolFailure({ message: `Unable to find files matching ${input.pattern}`, error }),
Effect.mapError((error) =>
error instanceof ToolFailure
? error
: new ToolFailure({ message: `Unable to find files matching ${input.pattern}` }),
),
),
}),
+12 -2
View File
@@ -91,7 +91,13 @@ export const Plugin = {
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
})
const target = path.resolve(location.directory, input.path ?? ".")
const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined)))
const info = yield* fs
.stat(target)
.pipe(
Effect.catchReason("PlatformError", "NotFound", () =>
Effect.fail(new ToolFailure({ message: `Search path does not exist: ${input.path ?? "."}` })),
),
)
return yield* ripgrep
.grep({
cwd: info?.type === "Directory" ? target : path.dirname(target),
@@ -122,7 +128,11 @@ export const Plugin = {
),
)
}).pipe(
Effect.mapError((error) => new ToolFailure({ message: `Unable to grep for ${input.pattern}`, error })),
Effect.mapError((error) =>
error instanceof ToolFailure
? error
: new ToolFailure({ message: `Unable to grep for ${input.pattern}` }),
),
),
}),
})
+87 -57
View File
@@ -1,101 +1,131 @@
export * as McpTool from "./mcp"
import { createHash } from "node:crypto"
import { ToolFailure } from "@opencode-ai/llm"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
import { makeLocationNode } from "../effect/app-node"
import { EventV2 } from "../event"
import { Flag } from "../flag/flag"
import { MCP } from "../mcp"
import { PermissionV2 } from "../permission"
import { Tool } from "./tool"
import { Tools } from "./tools"
import { ToolRegistry } from "./registry"
const MAX_NAME_LENGTH = 64
const HASH_LENGTH = 8
const sanitize = (value: string) => value.replace(/[^A-Za-z0-9_-]/g, "_")
// Deterministic short suffix used to keep overlong or colliding names unique and stable across restarts.
const hashSuffix = (raw: string) => "_" + createHash("sha1").update(raw).digest("hex").slice(0, HASH_LENGTH)
const fit = (base: string, raw: string) => base.slice(0, MAX_NAME_LENGTH - HASH_LENGTH - 1) + hashSuffix(raw)
/**
* Registry/permission action name for an MCP tool: V1-compatible `<server>_<tool>` so existing deny
* rules keep working. Sanitized to a valid tool name, prefixed when it would not start with a letter,
* and hashed down when it would exceed the 64-char limit.
* Registry group and permission action names for MCP tools.
*/
export const name = (server: string, tool: string) => {
const joined = sanitize(server) + "_" + sanitize(tool)
const base = /^[A-Za-z]/.test(joined) ? joined : "mcp_" + joined
return base.length > MAX_NAME_LENGTH ? fit(base, `${server}\u0000${tool}`) : base
}
const toContent = (part: MCP.ToolResultContent): Tool.Content =>
part.type === "text" ? { type: "text", text: part.text } : { type: "file", data: part.data, mime: part.mimeType }
const errorText = (content: ReadonlyArray<MCP.ToolResultContent>) =>
content
.flatMap((part) => (part.type === "text" ? [part.text] : []))
.join("\n")
.trim()
export const group = (server: string) => server.replace(/[^a-zA-Z0-9_-]/g, "_")
export const name = (server: string, tool: string) => `${group(server)}_${tool.replace(/[^a-zA-Z0-9_-]/g, "_")}`
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const mcp = yield* MCP.Service
const tools = yield* Tools.Service
const events = yield* EventV2.Service
const permission = yield* PermissionV2.Service
const scope = yield* Scope.Scope
const lock = Semaphore.makeUnsafe(1)
let current: Scope.Closeable | undefined
const make = (server: MCP.ServerName, tool: MCP.Tool) =>
Tool.make({
description: tool.description ?? "",
jsonSchema: (tool.inputSchema as JsonSchema.JsonSchema | undefined) ?? { type: "object", properties: {} },
execute: (input) =>
Effect.gen(function* () {
const result = yield* mcp.callTool({ server, name: tool.name, args: (input ?? {}) as Record<string, unknown> }).pipe(
Effect.catchTags({
"MCP.NotFoundError": (error) => new ToolFailure({ message: `MCP server "${error.server}" is not available` }),
"MCP.ToolCallError": (error) => new ToolFailure({ message: error.message }),
}),
)
if (result.isError)
return yield* new ToolFailure({ message: errorText(result.content) || "MCP tool returned an error" })
return { structured: result.structured ?? {}, content: result.content.map(toContent) }
}),
})
// Register the current tool set under a fresh child scope, then close the previous one so the
// registry never has a gap where MCP tools disappear mid-swap.
const reconcile = lock.withPermit(
Effect.gen(function* () {
const used = new Set<string>()
const record: Record<string, Tool.AnyTool> = {}
const groups = new Map<string, Record<string, Tool.AnyTool>>()
for (const tool of yield* mcp.tools()) {
const initial = name(tool.server, tool.name)
const key = used.has(initial) ? fit(initial, `${tool.server}\u0000${tool.name}`) : initial
used.add(key)
record[key] = make(tool.server, tool)
const group = groups.get(tool.server) ?? {}
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
group[tool.name] = Tool.withPermission(
Tool.make({
description: tool.description ?? "",
jsonSchema: {
...schema,
type: "object",
properties: schema.properties ?? {},
additionalProperties: false,
},
execute: (input, context) =>
Effect.gen(function* () {
yield* permission.assert({
action: name(tool.server, tool.name),
resources: ["*"],
save: ["*"],
metadata: {},
sessionID: context.sessionID,
agent: context.agent,
source: {
type: "tool",
messageID: context.assistantMessageID,
callID: context.toolCallID,
},
})
const result = yield* mcp
.callTool({
server: tool.server,
name: tool.name,
args: (input ?? {}) as Record<string, unknown>,
})
.pipe(
Effect.catchTags({
"MCP.NotFoundError": (error) =>
new ToolFailure({ message: `MCP server "${error.server}" is not available` }),
"MCP.ToolCallError": (error) => new ToolFailure({ message: error.message }),
}),
)
if (result.isError)
return yield* new ToolFailure({
message:
result.content
.flatMap((part) => (part.type === "text" ? [part.text] : []))
.join("\n")
.trim() || "MCP tool returned an error",
})
const content = result.content.map((part) =>
part.type === "text"
? { type: "text" as const, text: part.text }
: { type: "file" as const, data: part.data, mime: part.mimeType },
)
const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n")
return {
structured: result.structured ?? (text === "" ? null : text),
content,
}
}).pipe(
Effect.mapError((error) =>
error instanceof ToolFailure
? error
: new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }),
),
),
}),
name(tool.server, tool.name),
)
groups.set(tool.server, group)
}
const next = yield* Scope.fork(scope)
yield* tools.register(record).pipe(Scope.provide(next), Effect.orDie)
yield* Effect.forEach(
groups,
([group, record]) => tools.register(record, { group, deferred: Flag.CODEMODE_ENABLED }),
{
discard: true,
},
).pipe(Scope.provide(next), Effect.orDie)
if (current) yield* Scope.close(current, Exit.void)
current = next
}),
)
yield* reconcile.pipe(Effect.forkScoped)
yield* events
.subscribe(McpEvent.ToolsChanged)
.pipe(Stream.runForEach(() => reconcile), Effect.forkScoped({ startImmediately: true }))
yield* events.subscribe(McpEvent.ToolsChanged).pipe(
Stream.runForEach(() => reconcile),
Effect.forkScoped({ startImmediately: true }),
)
}),
)
export const node = makeLocationNode({
name: "mcp-tools",
layer,
deps: [ToolRegistry.toolsNode, MCP.node, EventV2.node],
deps: [ToolRegistry.toolsNode, MCP.node, EventV2.node, PermissionV2.node],
})
+1 -1
View File
@@ -67,7 +67,7 @@ export const Plugin = {
source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
})
.pipe(
Effect.mapError((error) => new ToolFailure({ message: "Permission denied: question", error })),
Effect.mapError(() => new ToolFailure({ message: "Permission denied: question" })),
Effect.andThen(
question
.ask({
+2 -4
View File
@@ -106,9 +106,7 @@ export const Plugin = {
start: type === "directory" ? resolved : dirname(resolved),
stop: root,
})
const candidates = (yield* Effect.forEach(discovered, fs.resolve)).filter(
(file) => dirname(file) !== root,
)
const candidates = (yield* Effect.forEach(discovered, fs.resolve)).filter((file) => dirname(file) !== root)
if (candidates.length === 0) return
yield* sessionInstructions.load({ sessionID: context.sessionID, paths: candidates })
}).pipe(
@@ -132,7 +130,7 @@ export const Plugin = {
error instanceof Image.SizeError
? error.message
: `Unable to read ${input.path}`
return new ToolFailure({ message, error })
return new ToolFailure({ message })
}),
)
},
+79 -45
View File
@@ -3,17 +3,17 @@ export * as ToolRegistry from "./registry"
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
import { Context, Effect, Layer, Scope } from "effect"
import type { AgentV2 } from "../agent"
import { Flag } from "../flag/flag"
import { PermissionV2 } from "../permission"
import { SessionMessage } from "../session/message"
import { SessionSchema } from "../session/schema"
import { ToolOutputStore } from "../tool-output-store"
import { Wildcard } from "../util/wildcard"
import { definition, permission, registrationEntries, settle, type AnyTool, type RegistrationError } from "./tool"
import { ExecuteTool } from "./execute"
import { definition, permission, registrationEntries, RegistrationError, settle, type AnyTool } from "./tool"
import { Tools } from "./tools"
import { ToolHooks } from "./hooks"
import { makeLocationNode } from "../effect/app-node"
import { SessionError } from "@opencode-ai/schema/session-error"
import { toSessionError } from "../session/to-session-error"
export type ExecuteInput = {
readonly sessionID: SessionSchema.ID
@@ -25,7 +25,10 @@ export type ExecuteInput = {
export interface Interface {
readonly materialize: (input: MaterializeInput) => Effect.Effect<Materialization>
/** Internal registration capability exposed publicly only through Tools.Service. */
readonly register: (tools: Readonly<Record<string, AnyTool>>) => Effect.Effect<void, RegistrationError, Scope.Scope>
readonly register: (
tools: Readonly<Record<string, AnyTool>>,
options?: Tools.RegisterOptions,
) => Effect.Effect<void, RegistrationError, Scope.Scope>
}
export interface MaterializeInput {
@@ -42,7 +45,6 @@ export interface Settlement {
readonly result: ToolResultValue
readonly output?: ToolOutput
readonly outputPaths?: ReadonlyArray<string>
readonly error?: SessionError.Error
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
@@ -52,27 +54,17 @@ const registryLayer = Layer.effect(
Effect.gen(function* () {
const resources = yield* ToolOutputStore.Service
const toolHooks = yield* ToolHooks.Service
type Registration = { readonly identity: object; readonly tool: AnyTool }
type Registration = {
readonly identity: object
readonly tool: AnyTool
readonly name: string
readonly group?: string
readonly deferred: boolean
}
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
const settleWith = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput, advertised?: object) {
const registration = local.get(input.call.name)?.at(-1)?.registration
if (!registration)
return {
result: {
type: "error" as const,
value: advertised ? `Stale tool call: ${input.call.name}` : `Unknown tool: ${input.call.name}`,
},
error: advertised
? ({ type: "tool.stale", message: `Stale tool call: ${input.call.name}`, name: input.call.name } as const)
: ({ type: "tool.unknown", message: `Unknown tool: ${input.call.name}`, name: input.call.name } as const),
}
if (advertised && registration.identity !== advertised)
return {
result: { type: "error" as const, value: `Stale tool call: ${input.call.name}` },
error: { type: "tool.stale" as const, message: `Stale tool call: ${input.call.name}`, name: input.call.name },
}
// Hooks fire only for hosted/local tools; provider-executed calls never reach settleWith.
const settleTool = Effect.fn("ToolRegistry.settleTool")(function* (input: ExecuteInput, tool: AnyTool) {
// Hooks fire only for hosted/local tools; provider-executed calls never reach settleTool.
const beforeEvent: ToolHooks.BeforeEvent = {
tool: input.call.name,
sessionID: input.sessionID,
@@ -83,7 +75,7 @@ const registryLayer = Layer.effect(
}
yield* toolHooks.runBefore(beforeEvent)
const pending = yield* settle(
registration.tool,
tool,
{ ...input.call, input: beforeEvent.input },
{
sessionID: input.sessionID,
@@ -94,10 +86,7 @@ const registryLayer = Layer.effect(
).pipe(
Effect.map((output) => ({ output })),
Effect.catchTag("LLM.ToolFailure", (failure) =>
Effect.succeed({
result: { type: "error" as const, value: failure.message },
error: toSessionError(failure),
}),
Effect.succeed({ result: { type: "error" as const, value: failure.message } }),
),
)
let settlement: Settlement
@@ -135,25 +124,56 @@ const registryLayer = Layer.effect(
result: afterEvent.result,
...(afterEvent.output !== undefined ? { output: afterEvent.output } : {}),
...(afterEvent.outputPaths !== undefined ? { outputPaths: afterEvent.outputPaths } : {}),
...(settlement.error !== undefined ? { error: settlement.error } : {}),
}
})
const settleWith = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput, advertised: object) {
const registration = local.get(input.call.name)?.at(-1)?.registration
if (!registration)
return {
result: {
type: "error" as const,
value: `Stale tool call: ${input.call.name}`,
},
}
if (registration.identity !== advertised)
return { result: { type: "error" as const, value: `Stale tool call: ${input.call.name}` } }
return yield* settleTool(input, registration.tool)
})
return Service.of({
register: Effect.fn("ToolRegistry.register")(function* (tools) {
const entries = registrationEntries(tools)
register: Effect.fn("ToolRegistry.register")(function* (tools, options) {
const entries = registrationEntries(tools, options?.group)
if (entries.length === 0) return
const reserved = options?.deferred ? undefined : entries.find((entry) => entry.key === "execute")
if (reserved)
return yield* Effect.fail(
new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }),
)
yield* Effect.uninterruptible(
Effect.gen(function* () {
const token = {}
for (const [name, tool] of entries)
local.set(name, [...(local.get(name) ?? []), { token, registration: { identity: {}, tool } }])
for (const entry of entries)
local.set(entry.key, [
...(local.get(entry.key) ?? []),
{
token,
registration: {
identity: {},
tool: entry.tool,
name: entry.name,
group: entry.group,
deferred: options?.deferred ?? false,
},
},
])
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
for (const [name] of entries) {
const registrations = local.get(name)?.filter((registration) => registration.token !== token) ?? []
if (registrations.length > 0) local.set(name, registrations)
else local.delete(name)
for (const entry of entries) {
const registrations =
local.get(entry.key)?.filter((registration) => registration.token !== token) ?? []
if (registrations.length > 0) local.set(entry.key, registrations)
else local.delete(entry.key)
}
}),
)
@@ -170,18 +190,32 @@ const registryLayer = Layer.effect(
const usePatch = input.model.provider.toLowerCase() === "openai" || input.model.id.toLowerCase().includes("gpt")
for (const [name, registration] of registrations) {
const wrongEditTool = name === "apply_patch" ? !usePatch : (name === "edit" || name === "write") && usePatch
if (wrongEditTool || whollyDisabled(permission(registration.tool, name), input.permissions ?? []))
if (
wrongEditTool ||
(registration.deferred && !Flag.CODEMODE_ENABLED) ||
whollyDisabled(permission(registration.tool, name), input.permissions ?? [])
)
registrations.delete(name)
}
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred))
const execute =
deferred.size > 0 && !whollyDisabled("execute", input.permissions ?? [])
? ExecuteTool.create({
registrations: deferred,
current: (name) => local.get(name)?.at(-1)?.registration,
})
: undefined
return {
definitions: Array.from(registrations, ([name, registration]) => definition(name, registration.tool)),
definitions: [
...Array.from(direct, ([name, registration]) => definition(name, registration.tool)),
...(execute ? [definition("execute", execute)] : []),
],
settle: (input) => {
const registration = registrations.get(input.call.name)
if (input.call.name === "execute" && execute) return settleTool(input, execute)
const registration = direct.get(input.call.name)
if (registration) return settleWith(input, registration.identity)
return Effect.succeed({
result: { type: "error", value: `Unknown tool: ${input.call.name}` },
error: { type: "tool.unknown", message: `Unknown tool: ${input.call.name}`, name: input.call.name },
})
return Effect.succeed({ result: { type: "error", value: `Unknown tool: ${input.call.name}` } })
},
}
}),
+10 -12
View File
@@ -18,7 +18,9 @@ export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000
export const MAX_TIMEOUT_MS = 10 * 60 * 1_000
export const MAX_CAPTURE_BYTES = 1024 * 1024
const BACKGROUND_STARTED = "The command has not completed; it is now running in the background."
const BACKGROUND_STARTED = "The command was moved to the background."
const BACKGROUND_INSTRUCTION =
"You will be notified automatically when the command finishes. DO NOT sleep, poll, or proactively check on its progress."
export const Input = Schema.Struct({
command: Schema.String.annotate({ description: "Shell command string to execute" }),
@@ -53,10 +55,11 @@ const Output = Schema.Struct({
type Output = typeof Output.Type
const modelOutput = (output: Output): string | undefined => {
if (output.status === "running") return undefined
const warnings = output.warnings?.length
? `\n\nWarnings:\n${output.warnings.map((warning) => `- ${warning}`).join("\n")}`
: ""
if (output.status === "running")
return `${warnings.trimStart()}${warnings ? "\n\n" : ""}${BACKGROUND_INSTRUCTION}`
if (output.timeout) return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command timed out before completion.`
return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command exited with code ${output.exit}.`
}
@@ -244,9 +247,9 @@ export const Plugin = {
}
}
const result = yield* runtime.job
.block({ id: job.id, sessionID: context.sessionID })
.pipe(Effect.onInterrupt(() => runtime.job.cancel(job.id).pipe(Effect.ignore)))
const result = yield* runtime.job.block({ id: job.id, sessionID: context.sessionID }).pipe(
Effect.onInterrupt(() => runtime.job.cancel(job.id).pipe(Effect.ignore)),
)
if (result?.type === "backgrounded") {
yield* notifyWhenDone(context.sessionID, context.toolCallID, input.command)
return {
@@ -257,19 +260,14 @@ export const Plugin = {
...(warnings.length ? { warnings } : {}),
}
}
if (result?.info.status === "error")
return yield* Effect.fail(new Error(result.info.error ?? "Command failed"))
if (result?.info.status === "error") return yield* Effect.fail(new Error(result.info.error ?? "Command failed"))
if (result?.info.status === "cancelled") return yield* Effect.fail(new Error("Command cancelled"))
return {
...(yield* settleShell()),
...(warnings.length ? { warnings } : {}),
}
}).pipe(
Effect.mapError(
(error) => new ToolFailure({ message: `Unable to execute command: ${input.command}`, error }),
),
),
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to execute command: ${input.command}` }))),
}),
})
.pipe(Effect.orDie)
+2 -6
View File
@@ -104,9 +104,7 @@ export const Plugin = {
const parent = yield* runtime.session
.get(context.sessionID)
.pipe(
Effect.mapError(
(error) => new ToolFailure({ message: `Parent session not found: ${context.sessionID}`, error }),
),
Effect.mapError(() => new ToolFailure({ message: `Parent session not found: ${context.sessionID}` })),
)
const agent = yield* agents.resolve(input.agent)
if (agent === undefined) return yield* new ToolFailure({ message: `Unknown agent: ${input.agent}` })
@@ -125,9 +123,7 @@ export const Plugin = {
// session (V1 deriveSubagentSessionPermission). MVP uses the agent's own permissions.
})
.pipe(
Effect.mapError(
(error) => new ToolFailure({ message: `Parent session not found: ${context.sessionID}`, error }),
),
Effect.mapError(() => new ToolFailure({ message: `Parent session not found: ${context.sessionID}` })),
)
const background = input.background === true
+1 -1
View File
@@ -46,7 +46,7 @@ export const Plugin = {
})
yield* todos.update({ sessionID: context.sessionID, todos: input.todos })
return { todos: input.todos }
}).pipe(Effect.mapError((error) => new ToolFailure({ message: "Unable to update todos", error }))),
}).pipe(Effect.mapError(() => new ToolFailure({ message: "Unable to update todos" }))),
}),
})
.pipe(Effect.orDie)
+3
View File
@@ -3,9 +3,12 @@ export * as Tools from "./tools"
import { Context, Effect, Scope } from "effect"
import { Tool } from "./tool"
export type RegisterOptions = Tool.RegisterOptions
export interface Interface {
readonly register: (
tools: Readonly<Record<string, Tool.AnyTool>>,
options?: Tool.RegisterOptions,
) => Effect.Effect<void, Tool.RegistrationError, Scope.Scope>
}
+1 -1
View File
@@ -170,7 +170,7 @@ export const Plugin = {
format: input.format,
output,
}
}).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to fetch ${input.url}`, error }))),
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to fetch ${input.url}` }))),
}),
})
.pipe(Effect.orDie)
+1 -5
View File
@@ -243,11 +243,7 @@ export const Plugin = {
provider,
text: text ?? NO_RESULTS,
}
}).pipe(
Effect.mapError(
(error) => new ToolFailure({ message: `Unable to search the web for ${input.query}`, error }),
),
)
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to search the web for ${input.query}` })))
},
}),
})
+1 -1
View File
@@ -83,7 +83,7 @@ export const Plugin = {
source,
})
return yield* files.writeTextPreservingBom({ target, content: input.content })
}).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to write ${input.path}`, error }))),
}).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to write ${input.path}` }))),
}),
"edit",
),
-73
View File
@@ -1,19 +1,15 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
import { Effect, Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
import { EventV2 } from "@opencode-ai/core/event"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { Npm } from "@opencode-ai/core/npm"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "../plugin/fixture"
@@ -244,49 +240,6 @@ describe("ConfigExternalPlugin", () => {
})
}),
)
it.live("reloads changed plugin source from the same entrypoint", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((tmp) =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const agents = yield* AgentV2.Service
const events = yield* EventV2.Service
const fsUtil = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const host = yield* PluginHost.make(plugins)
const plugin = path.join(tmp.path, "plugin.ts")
const config = Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
info: decode({ plugins: [plugin] }),
}),
]),
})
yield* Effect.promise(() => fs.writeFile(plugin, pluginSource("First source")))
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
Effect.provideService(PluginV2.Service, plugins),
Effect.provideService(FSUtil.Service, fsUtil),
Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm),
Effect.provideService(Config.Service, config),
)
expect((yield* waitForAgent(agents, "hot-reload"))?.description).toBe("First source")
yield* Effect.promise(() => fs.writeFile(plugin, pluginSource("Second source")))
yield* events.publish(ConfigSchema.Event.Updated, {})
expect(yield* waitForAgentDescription(agents, "hot-reload", "Second source")).toBe(true)
}),
),
),
)
})
const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id: string) {
@@ -297,29 +250,3 @@ const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id:
}
return yield* Effect.die(`Timed out waiting for agent ${id}`)
})
const waitForAgentDescription = Effect.fnUntraced(function* (
agents: AgentV2.Interface,
id: string,
description: string,
) {
for (let attempt = 0; attempt < 100; attempt++) {
if ((yield* agents.get(AgentV2.ID.make(id)))?.description === description) return true
yield* Effect.sleep("10 millis")
}
return false
})
function pluginSource(description: string) {
return `export default {
id: "source-hot-reload",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
agents.update("hot-reload", (agent) => {
agent.description = ${JSON.stringify(description)}
agent.mode = "subagent"
})
})
},
}`
}
+3 -7
View File
@@ -27,7 +27,7 @@ const decode = Schema.decodeUnknownSync(Config.Info)
const document = path.join(import.meta.dir, "opencode.json")
describe("config plugin reloads", () => {
it.live("reloads every config-backed domain", () =>
it.live("reloads config-backed domains without reloading external plugins", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const catalog = yield* Catalog.Service
@@ -69,8 +69,7 @@ describe("config plugin reloads", () => {
(yield* commands.get("second"))?.description === "Second command" &&
(yield* references.list()).some((reference) => reference.name === "second") &&
(yield* catalog.provider.get(ProviderV2.ID.make("first"))) === undefined &&
(yield* catalog.provider.get(ProviderV2.ID.make("second"))) !== undefined &&
(yield* agents.get(AgentV2.ID.make("configured")))?.description === "Second plugin"
(yield* catalog.provider.get(ProviderV2.ID.make("second"))) !== undefined
)
}),
)
@@ -81,10 +80,7 @@ describe("config plugin reloads", () => {
expect(
(yield* skills.sources()).some((source) => source.type === "directory" && source.path === "/skills/second"),
).toBe(true)
entries = [config("second")]
yield* events.publish(ConfigSchema.Event.Updated, {})
yield* waitUntil(agents.get(AgentV2.ID.make("configured")).pipe(Effect.map((agent) => agent === undefined)))
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("First plugin")
}).pipe(Effect.provideService(Global.Service, Global.Service.of(Global.make()))),
)
})
@@ -15,7 +15,6 @@ import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migrat
import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent"
import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera"
import simplifySessionInputMigration from "@opencode-ai/core/database/migration/20260622202450_simplify_session_input"
import resetExecutionErrorsMigration from "@opencode-ai/core/database/migration/20260703210000_reset_v2_execution_errors"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
@@ -39,29 +38,6 @@ const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
describe("DatabaseMigration", () => {
test("resets incompatible V2 execution and error history", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE session_input (id text PRIMARY KEY)`)
yield* db.run(sql`CREATE TABLE session_message (id text PRIMARY KEY)`)
yield* db.run(sql`CREATE TABLE event (id text PRIMARY KEY)`)
yield* db.run(sql`CREATE TABLE event_sequence (aggregate_id text PRIMARY KEY, seq integer NOT NULL)`)
yield* db.run(sql`INSERT INTO session_input (id) VALUES ('input')`)
yield* db.run(sql`INSERT INTO session_message (id) VALUES ('message')`)
yield* db.run(sql`INSERT INTO event (id) VALUES ('event')`)
yield* db.run(sql`INSERT INTO event_sequence (aggregate_id, seq) VALUES ('session', 1)`)
yield* DatabaseMigration.applyOnly(db, [resetExecutionErrorsMigration])
expect(yield* db.get(sql`SELECT id FROM session_input`)).toBeUndefined()
expect(yield* db.get(sql`SELECT id FROM session_message`)).toBeUndefined()
expect(yield* db.get(sql`SELECT id FROM event`)).toBeUndefined()
expect(yield* db.get(sql`SELECT aggregate_id FROM event_sequence`)).toBeUndefined()
}),
)
})
test("serializes concurrent embedded initialization for one database path", async () => {
await using tmp = await tmpdir()
const filename = path.join(tmp.path, "embedded.sqlite")
+18 -2
View File
@@ -2,6 +2,8 @@ import { describe, expect } from "bun:test"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Ref, Schema, Stream } from "effect"
import { EventV2 } from "@opencode-ai/core/event"
import { Event } from "@opencode-ai/schema/event"
import { EventManifest } from "@opencode-ai/schema/event-manifest"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { Session } from "@opencode-ai/schema/session"
import { SessionEvent } from "@opencode-ai/schema/session-event"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
@@ -329,8 +331,8 @@ describe("EventV2", () => {
const events = yield* EventV2.Service
const consuming = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const slowStream = yield* EventV2.liveBounded(events, 1)
const fastStream = yield* EventV2.liveBounded(events, 8)
const slowStream = yield* EventV2.liveBounded(events, { capacity: 1 })
const fastStream = yield* EventV2.liveBounded(events, { capacity: 8 })
const slow = yield* slowStream.pipe(
Stream.runForEach(() => Deferred.succeed(consuming, undefined).pipe(Effect.andThen(Deferred.await(release)))),
Effect.forkScoped,
@@ -355,6 +357,20 @@ describe("EventV2", () => {
}),
)
it.effect("filters internal events before they enter a bounded server stream", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
const stream = yield* EventV2.liveBounded(events, { capacity: 1, accept: EventManifest.isServer })
const received = yield* stream.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* events.publish(McpEvent.ToolsChanged, { server: "one" })
yield* events.publish(McpEvent.ToolsChanged, { server: "two" })
const published = yield* events.publish(McpEvent.StatusChanged, { server: "example" })
expect(Array.from(yield* Fiber.join(received))).toEqual([published])
}),
)
it.effect("preserves observer interruption", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
@@ -1,5 +1,7 @@
import { expect, test } from "bun:test"
import { Ignore } from "@opencode-ai/core/filesystem/ignore"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
test("match nested and non-nested", () => {
expect(Ignore.match("node_modules/index.js")).toBe(true)
@@ -8,3 +10,30 @@ test("match nested and non-nested", () => {
expect(Ignore.match("node_modules/bar")).toBe(true)
expect(Ignore.match("node_modules/bar/")).toBe(true)
})
test("parcel patterns ignore built-in folders at any depth", async () => {
let ignoreGlobs: string[] = []
const watcher = createWrapper({
subscribe: async (
_directory: string,
_callback: (...args: unknown[]) => unknown,
options: { ignoreGlobs?: string[] },
) => {
ignoreGlobs = options.ignoreGlobs ?? []
},
})
await watcher.subscribe("/tmp/project", () => {}, { ignore: Ignore.PATTERNS })
const patterns = ignoreGlobs.map((source) => new RegExp(source))
for (const path of [
"nested/node_modules",
"nested/node_modules/package/index.js",
"nested/.git",
"nested/.git/HEAD",
"nested/dist",
"nested/dist/index.js",
]) {
expect(patterns.some((pattern) => pattern.test(path))).toBe(true)
}
expect(patterns.some((pattern) => pattern.test("nested/src/index.ts"))).toBe(false)
})
+25 -5
View File
@@ -2,7 +2,7 @@ import { $ } from "bun"
import { describe, expect } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { Deferred, Duration, Effect, Fiber, Layer, Option, Stream } from "effect"
import { Deferred, Duration, Effect, Fiber, Layer, Option, Schedule, Stream } from "effect"
import { Config } from "@opencode-ai/core/config"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
@@ -149,12 +149,14 @@ describeWatcher("LocationWatcher", () => {
const update = yield* watcher
.subscribe({ path: target, type: "file" })
.pipe(Stream.take(1), Stream.runHead, Effect.forkScoped({ startImmediately: true }))
yield* Effect.yieldNow
yield* fs.writeFileString(sibling, "sibling")
yield* fs.writeFileString(target, "target")
const writes = yield* Effect.suspend(() => fs.writeFileString(target, `target-${Math.random()}`)).pipe(
Effect.repeat(Schedule.spaced("10 millis")),
Effect.forkScoped,
)
const event = yield* Fiber.join(update).pipe(Effect.ensuring(Fiber.interrupt(writes)))
expect((yield* Fiber.join(update)).valueOrUndefined?.path).toBe(target)
expect(event.valueOrUndefined?.path).toBe(target)
}).pipe(Effect.provide(AppNodeBuilder.build(Watcher.node))),
),
)
@@ -197,6 +199,24 @@ describeWatcher("LocationWatcher", () => {
),
)
it.live("ignores dependency, VCS, and build directories at any depth", () =>
withTmp((directory) =>
Effect.gen(function* () {
const afs = yield* FSUtil.Service
yield* ready(directory)
const roots = ["node_modules", ".git", "dist"].map((name) => path.join(directory, "nested", name))
const files = roots.map((root) => path.join(root, "package", "index.js"))
yield* noUpdate(
(event) => roots.some((root) => event.file === root || event.file.startsWith(`${root}${path.sep}`)),
Effect.forEach(files, (file) => afs.writeWithDirs(file, "ignored"), {
concurrency: "unbounded",
discard: true,
}),
)
}),
),
)
it.live("cleanup stops publishing events", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
+126
View File
@@ -1,6 +1,61 @@
import { describe, expect, test } from "bun:test"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { MCP } from "@opencode-ai/core/mcp/index"
import { MCPClient } from "@opencode-ai/core/mcp/client"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { SessionV2 } from "@opencode-ai/core/session"
import { McpTool } from "@opencode-ai/core/tool/mcp"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
import { testEffect } from "./lib/effect"
import { settleTool, toolIdentity, waitForTool } from "./lib/tool"
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
let calls = 0
const mcp = Layer.mock(MCP.Service, {
tools: () =>
Effect.succeed([
new MCP.Tool({
server: MCP.ServerName.make("demo"),
name: "search",
description: "Search",
inputSchema: { type: "object", properties: {} },
}),
]),
callTool: (input) =>
Effect.sync(() => {
calls += 1
return new MCP.ToolResult({
server: MCP.ServerName.make(input.server),
tool: input.name,
isError: false,
structured: { ok: true },
content: [],
})
}),
})
const permissions = Layer.mock(PermissionV2.Service, {
assert: (input) =>
Effect.gen(function* () {
if (!assertion) return yield* Effect.die("Permission test is not initialized")
yield* Deferred.succeed(assertion, input)
yield* decision
}),
})
const events = Layer.mock(EventV2.Service, { subscribe: () => Stream.never })
const it = testEffect(
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, McpTool.node]), [
[MCP.node, mcp],
[PermissionV2.node, permissions],
[EventV2.node, events],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
]),
)
describe("MCP errors", () => {
test("expose useful messages", () => {
@@ -12,3 +67,74 @@ describe("MCP errors", () => {
expect(new MCPClient.ConnectError({ server: "demo", message: "offline" }).message).toBe("offline")
})
})
test("MCP tool names match V1 sanitization", () => {
expect(McpTool.name("context 7", "resolve.library/id")).toBe("context_7_resolve_library_id")
})
it.effect("waits for permission before calling an MCP tool", () =>
Effect.gen(function* () {
calls = 0
assertion = yield* Deferred.make<PermissionV2.AssertInput>()
const permission = yield* Deferred.make<void>()
decision = Deferred.await(permission)
const registry = yield* ToolRegistry.Service
yield* waitForTool(registry, "execute")
const fiber = yield* settleTool(registry, {
sessionID: SessionV2.ID.make("ses_mcp_permission"),
...toolIdentity,
call: {
type: "tool-call",
id: "call_mcp_permission",
name: "execute",
input: { code: "return await tools.demo.search({})" },
},
}).pipe(Effect.forkScoped)
expect(yield* Deferred.await(assertion)).toEqual({
action: "demo_search",
resources: ["*"],
save: ["*"],
metadata: {},
sessionID: SessionV2.ID.make("ses_mcp_permission"),
agent: toolIdentity.agent,
source: {
type: "tool",
messageID: toolIdentity.assistantMessageID,
callID: "call_mcp_permission",
},
})
expect(calls).toBe(0)
yield* Deferred.succeed(permission, undefined)
yield* Fiber.join(fiber)
expect(calls).toBe(1)
}),
)
it.effect("does not call MCP when permission is rejected", () =>
Effect.gen(function* () {
calls = 0
assertion = yield* Deferred.make<PermissionV2.AssertInput>()
decision = Effect.fail(new PermissionV2.RejectedError())
const registry = yield* ToolRegistry.Service
yield* waitForTool(registry, "execute")
const settlement = yield* settleTool(registry, {
sessionID: SessionV2.ID.make("ses_mcp_rejected"),
...toolIdentity,
call: {
type: "tool-call",
id: "call_mcp_rejected",
name: "execute",
input: { code: "return await tools.demo.search({})" },
},
})
expect(settlement.result).toEqual({ type: "text", value: "Unable to execute demo_search" })
expect(settlement.output?.structured).toEqual({
toolCalls: [{ tool: "demo.search", status: "error" }],
error: true,
})
expect(calls).toBe(0)
}),
)
+47
View File
@@ -0,0 +1,47 @@
import { expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { Newtype } from "../src/schema"
class UserID extends Newtype<UserID>()("Test.UserID", Schema.NonEmptyString) {}
class ProjectID extends Newtype<ProjectID>()("Test.ProjectID", Schema.NonEmptyString) {}
class Port extends Newtype<Port>()("Test.Port", Schema.FiniteFromString) {}
const User = Schema.Struct({ id: UserID })
test("constructs nominal values from the underlying type", () => {
const id = UserID.make("user-1")
const acceptUserID = (_id: UserID) => undefined
expect(String(id)).toBe("user-1")
acceptUserID(id)
if (false) {
// @ts-expect-error distinct newtypes are not interchangeable
acceptUserID(ProjectID.make("project-1"))
}
})
test("preserves constructor validation", () => {
expect(() => UserID.make("")).toThrow()
})
test("decodes and encodes as a schema", async () => {
const decoded = await Effect.runPromise(Schema.decodeUnknownEffect(User)({ id: "user-1" }))
const encoded = await Effect.runPromise(Schema.encodeEffect(User)(decoded))
expect(String(decoded.id)).toBe("user-1")
expect(encoded).toEqual({ id: "user-1" })
})
test("preserves the underlying schema validation", async () => {
const result = await Effect.runPromise(Schema.decodeUnknownEffect(UserID)("").pipe(Effect.result))
expect(result._tag).toBe("Failure")
})
test("preserves transformed encoded and decoded representations", async () => {
const decoded = await Effect.runPromise(Schema.decodeUnknownEffect(Port)("8080"))
const encoded = await Effect.runPromise(Schema.encodeEffect(Port)(decoded))
expect(Number(decoded)).toBe(8080)
expect(encoded).toBe("8080")
})
+33
View File
@@ -126,6 +126,39 @@ describe("PluginV2", () => {
}),
)
it.effect("groups tool names and defers registrations from direct exposure", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const registry = yield* ToolRegistry.Service
const tool = (description: string) =>
Tool.make({
description,
input: Schema.Struct({}),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: () => Effect.succeed({ ok: true }),
})
const plugin = define({
id: "grouped-tools",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.tool.register({ plain: tool("Plain") }).pipe(Effect.orDie)
yield* ctx.tool.register({ "look/up": tool("Lookup") }, { group: "context 7" }).pipe(Effect.orDie)
yield* ctx.tool
.register({ search: tool("Search") }, { group: "context 7", deferred: true })
.pipe(Effect.orDie)
}),
})
yield* plugins.add(PluginV2.ID.make(plugin.id), plugin.effect)
expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toEqual([
"plain",
"context_7_look_up",
"execute",
])
}),
)
it.effect("fires before/after tool hooks with mutable events around settlement", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
@@ -19,7 +19,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
import { DateTime, Effect, Layer, Stream } from "effect"
import { asc, eq } from "drizzle-orm"
import { testEffect } from "./lib/effect"
@@ -73,7 +73,6 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
requests = []
const db = (yield* Database.Service).db
const compaction = yield* SessionCompaction.Service
const events = yield* EventV2.Service
const store = yield* SessionStore.Service
const sessionID = SessionV2.ID.make("ses_manual_compaction")
const userMessage = {
@@ -109,12 +108,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
),
)
const delta = yield* events
.subscribe(SessionEvent.Compaction.Delta)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
expect(yield* compaction.compactManual({ session, messages: [userMessage] })).toBe(true)
expect(Array.from(yield* Fiber.join(delta)).map((event) => event.data.text)).toEqual(["manual summary"])
expect(requests).toHaveLength(1)
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
+3 -3
View File
@@ -562,9 +562,9 @@ describe("SessionV2.create", () => {
yield* session.switchModel({ sessionID: created.id, model })
expect(yield* session.get(created.id)).toMatchObject({ model })
expect(
Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect)),
).toMatchObject([{ type: "session.model.selected", data: { model } }])
const events = Array.from(yield* logEvents(session, created.id, true).pipe(Stream.take(1), Stream.runCollect))
expect(events).toMatchObject([{ type: "session.model.selected" }])
expect(events[0]?.data).toEqual({ sessionID: created.id, model })
}),
)
-93
View File
@@ -1,93 +0,0 @@
import { describe, expect, test } from "bun:test"
import {
AuthenticationReason,
ContentPolicyReason,
InvalidProviderOutputReason,
InvalidRequestReason,
LLMError,
NoRouteReason,
ModelID,
ProviderID,
ProviderInternalReason,
QuotaExceededReason,
RateLimitReason,
TransportReason,
UnknownProviderReason,
ToolFailure,
} from "@opencode-ai/llm"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
const llm = (reason: LLMError["reason"]) => new LLMError({ module: "test", method: "stream", reason })
describe("toSessionError", () => {
test("maps every LLM reason to the closed wire type", () => {
expect(toSessionError(llm(new RateLimitReason({ message: "rate", retryAfterMs: 123 })))).toEqual({
type: "provider.rate-limit",
message: "rate",
retryAfterMs: 123,
})
expect(toSessionError(llm(new AuthenticationReason({ message: "auth", kind: "invalid" }))).type).toBe(
"provider.auth",
)
expect(toSessionError(llm(new QuotaExceededReason({ message: "quota" }))).type).toBe("provider.quota")
expect(toSessionError(llm(new ContentPolicyReason({ message: "blocked" }))).type).toBe("provider.content-filter")
expect(toSessionError(llm(new TransportReason({ message: "transport" }))).type).toBe("provider.transport")
expect(toSessionError(llm(new ProviderInternalReason({ message: "internal", status: 500 }))).type).toBe(
"provider.internal",
)
expect(toSessionError(llm(new InvalidProviderOutputReason({ message: "output" }))).type).toBe(
"provider.invalid-output",
)
expect(toSessionError(llm(new InvalidRequestReason({ message: "request" }))).type).toBe("provider.invalid-request")
expect(
toSessionError(
llm(
new NoRouteReason({
route: "route",
provider: ProviderID.make("provider"),
model: ModelID.make("model"),
}),
),
).type,
).toBe("provider.no-route")
expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown")
})
test("preserves structured permission rejection data without inventing resources", () => {
const rejected = new PermissionV2.RejectedError({ permission: "external_directory", resources: [] })
expect(toSessionError(rejected)).toEqual({
type: "permission.rejected",
message: "Permission rejected: external_directory",
permission: "external_directory",
resources: [],
})
expect(toSessionError(new ToolFailure({ message: rejected.message, error: rejected }))).toEqual({
type: "permission.rejected",
message: "Permission rejected: external_directory",
permission: "external_directory",
resources: [],
})
})
test("retries only rate limits, provider-internal failures, and transport failures", () => {
const eligible = [
llm(new RateLimitReason({ message: "rate" })),
llm(new ProviderInternalReason({ message: "internal", status: 500 })),
llm(new TransportReason({ message: "transport" })),
]
const ineligible = [
llm(new AuthenticationReason({ message: "auth", kind: "invalid" })),
llm(new QuotaExceededReason({ message: "quota" })),
llm(new ContentPolicyReason({ message: "blocked" })),
llm(new InvalidProviderOutputReason({ message: "output" })),
llm(new InvalidRequestReason({ message: "request" })),
llm(new NoRouteReason({ route: "route", provider: ProviderID.make("provider"), model: ModelID.make("model") })),
llm(new UnknownProviderReason({ message: "unknown" })),
]
expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true])
expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual([false, false, false, false, false, false, false])
})
})
@@ -1,36 +0,0 @@
import { describe, expect, test } from "bun:test"
import { LLMError, TransportReason } from "@opencode-ai/llm"
import { terminal } from "@opencode-ai/core/session/execution/local"
import { UserInterruptedError } from "@opencode-ai/core/session/error"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { Effect, Exit } from "effect"
describe("SessionExecutionLocal lifecycle", () => {
test("classifies success and typed failure terminals", () => {
expect(terminal(Exit.succeed(undefined))).toEqual({ type: "succeeded" })
expect(
terminal(
Exit.fail(
new LLMError({
module: "test",
method: "stream",
reason: new TransportReason({ message: "Disconnected" }),
}),
),
),
).toEqual({ type: "failed", error: { type: "provider.transport", message: "Disconnected" } })
const storage = new ToolOutputStore.StorageError({ operation: "encode", cause: new Error("invalid output") })
expect(terminal(Exit.fail(storage))).toEqual({
type: "failed",
error: { type: "unknown", message: storage.message },
})
})
test("defaults owner-scope interruption to shutdown and preserves explicit reasons", () => {
const interrupted = Effect.runSyncExit(Effect.interrupt)
expect(terminal(interrupted)).toEqual({ type: "interrupted", reason: "shutdown" })
expect(terminal(interrupted, "user")).toEqual({ type: "interrupted", reason: "user" })
expect(terminal(interrupted, "superseded")).toEqual({ type: "interrupted", reason: "superseded" })
expect(terminal(Exit.fail(new UserInterruptedError()))).toEqual({ type: "interrupted", reason: "user" })
})
})
+6 -69
View File
@@ -34,6 +34,7 @@ const sessionsLayer = AppNodeBuilder.build(SessionV2.node, [[SessionExecution.no
const sessionID = SessionV2.ID.make("ses_projector_test")
const created = DateTime.makeUnsafe(0)
const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
const previousModel = { ...model, variant: ModelV2.VariantID.make("medium") }
const encodeMessage = Schema.encodeSync(SessionMessage.Message)
const assistantRow = (
@@ -95,7 +96,7 @@ describe("SessionProjector", () => {
})
yield* events.publish(SessionEvent.RevertEvent.Committed, {
sessionID,
to: boundary,
messageID: boundary,
})
expect(
(yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all()).map((row) => row.id),
@@ -238,6 +239,7 @@ describe("SessionProjector", () => {
directory: "/project",
title: "test",
version: "test",
model: previousModel,
})
.run()
.pipe(Effect.orDie)
@@ -337,6 +339,7 @@ describe("SessionProjector", () => {
text: "synthetic context",
metadata: { source: "projector-test" },
})
expect(messages.find((message) => message.type === "model-switched")).toMatchObject({ previous: previousModel })
expect(messages.find((message) => message.type === "shell")).toMatchObject({
shell: { command: "pwd", status: "exited", exit: 0 },
output: { output: "/project", truncated: false },
@@ -429,73 +432,6 @@ describe("SessionProjector", () => {
}),
)
it.effect("projects retry state and clears it at the next step or execution terminal", () =>
Effect.gen(function* () {
const { db } = yield* Database.Service
yield* db
.insert(ProjectTable)
.values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
.run()
.pipe(Effect.orDie)
yield* db
.insert(SessionTable)
.values({
id: sessionID,
project_id: Project.ID.global,
slug: "test",
directory: "/project",
title: "test",
version: "test",
})
.run()
.pipe(Effect.orDie)
const events = yield* EventV2.Service
const first = SessionMessage.ID.make("msg_retry_first")
const second = SessionMessage.ID.make("msg_retry_second")
yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: first, agent: "build", model })
yield* events.publish(SessionEvent.RetryScheduled, {
sessionID,
assistantMessageID: first,
attempt: 2,
at: 2_000,
error: { type: "provider.transport", message: "Disconnected" },
})
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
Schema.decodeUnknownSync(SessionMessage.Message)({ ...row.data, id: row.id, type: row.type })
const firstRow = yield* db
.select()
.from(SessionMessageTable)
.where(eq(SessionMessageTable.id, first))
.get()
.pipe(Effect.orDie)
const projected = firstRow ?? (yield* Effect.die(new Error("Missing retry projection")))
expect(decode(projected)).toMatchObject({
retry: { attempt: 2, at: DateTime.makeUnsafe(2_000), error: { type: "provider.transport" } },
})
yield* events.publish(SessionEvent.Step.Started, { sessionID, assistantMessageID: second, agent: "build", model })
yield* events.publish(SessionEvent.RetryScheduled, {
sessionID,
assistantMessageID: second,
attempt: 3,
at: 6_000,
error: { type: "provider.internal", message: "Unavailable" },
})
yield* events.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" })
const rows = yield* db
.select()
.from(SessionMessageTable)
.where(eq(SessionMessageTable.session_id, sessionID))
.orderBy(asc(SessionMessageTable.seq))
.all()
.pipe(Effect.orDie)
expect(decode(rows[0])).not.toHaveProperty("retry")
expect(decode(rows[1])).not.toHaveProperty("retry")
}),
)
it.effect("updates only the newest incomplete assistant projection", () =>
Effect.gen(function* () {
const { db } = yield* Database.Service
@@ -589,6 +525,7 @@ describe("SessionProjector", () => {
yield* service.publish(SessionEvent.Text.Started, {
sessionID,
assistantMessageID: SessionMessage.ID.make("msg_assistant_completed"),
textID: "text-stale",
})
const rows = yield* db
@@ -607,7 +544,7 @@ describe("SessionProjector", () => {
type: "assistant",
agent: "build",
model,
content: [SessionMessage.AssistantText.make({ type: "text", text: "" })],
content: [SessionMessage.AssistantText.make({ type: "text", id: "text-stale", text: "" })],
time: { created: DateTime.makeUnsafe(1), completed: DateTime.makeUnsafe(2) },
}),
SessionMessage.Assistant.make({
@@ -104,10 +104,8 @@ describe("SessionRunCoordinator", () => {
Effect.gen(function* () {
const failure = new Error("failed")
const defect = new Error("defect")
const settled: Exit.Exit<void, Error>[] = []
const coordinator = yield* SessionRunCoordinator.make({
drain: (key: string) => (key === "failure" ? Effect.fail(failure) : Effect.die(defect)),
settled: (_key, exit) => Effect.sync(() => void settled.push(exit)),
})
const failed = yield* coordinator.run("failure").pipe(Effect.exit)
@@ -117,25 +115,6 @@ describe("SessionRunCoordinator", () => {
const died = yield* coordinator.run("defect").pipe(Effect.exit)
expect(Exit.isFailure(died) && Cause.hasDies(died.cause)).toBeTrue()
expect(Array.from(yield* coordinator.active)).toEqual([])
expect(settled).toHaveLength(2)
}),
),
)
it.effect("preserves settlement hook defects while releasing ownership", () =>
Effect.scoped(
Effect.gen(function* () {
const defect = new Error("terminal publication failed")
const coordinator = yield* SessionRunCoordinator.make({
drain: () => Effect.void,
settled: () => Effect.die(defect),
})
const exit = yield* coordinator.run("session").pipe(Effect.exit)
expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true)
if (Exit.isFailure(exit)) expect(Cause.squash(exit.cause)).toBe(defect)
expect(yield* coordinator.active).toEqual(new Set())
}),
),
)
@@ -230,41 +209,8 @@ describe("SessionRunCoordinator", () => {
it.effect("does nothing when interrupted while idle", () =>
Effect.scoped(
Effect.gen(function* () {
const reasons: Array<string | undefined> = []
const coordinator = yield* SessionRunCoordinator.make<string, never, string>({
drain: () => Effect.void,
settled: (_key, _exit, reason) => Effect.sync(() => void reasons.push(reason)),
})
yield* coordinator.interrupt("session", "user")
yield* coordinator.run("session")
expect(reasons).toEqual([undefined])
}),
),
)
it.effect("does not attach a late interrupt reason after terminal settlement starts", () =>
Effect.scoped(
Effect.gen(function* () {
const settling = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const reasons: Array<string | undefined> = []
const coordinator = yield* SessionRunCoordinator.make<string, never, string>({
drain: () => Effect.void,
settled: (_key, _exit, reason) =>
Deferred.succeed(settling, undefined).pipe(
Effect.andThen(Deferred.await(release)),
Effect.andThen(Effect.sync(() => void reasons.push(reason))),
),
})
const run = yield* coordinator.run("session").pipe(Effect.forkChild)
yield* Deferred.await(settling)
yield* coordinator.interrupt("session", "user")
yield* Deferred.succeed(release, undefined)
yield* Fiber.join(run)
yield* coordinator.run("session")
expect(reasons).toEqual([undefined, undefined])
const coordinator = yield* SessionRunCoordinator.make({ drain: () => Effect.void })
yield* coordinator.interrupt("session")
}),
),
)
@@ -275,28 +221,25 @@ describe("SessionRunCoordinator", () => {
const started = yield* Deferred.make<void>()
const interrupted = yield* Deferred.make<void>()
let runs = 0
const reasons: Array<string | undefined> = []
const coordinator = yield* SessionRunCoordinator.make<string, never, string>({
const coordinator = yield* SessionRunCoordinator.make({
drain: () =>
Effect.sync(() => ++runs).pipe(
Effect.andThen(Deferred.succeed(started, undefined)),
Effect.andThen(Effect.never),
Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)),
),
settled: (_key, _exit, reason) => Effect.sync(() => void reasons.push(reason)),
})
const resumed = yield* coordinator.run("session").pipe(Effect.forkChild)
yield* Deferred.await(started)
yield* coordinator.wake("session")
yield* coordinator.interrupt("session", "user")
yield* coordinator.interrupt("session")
yield* Deferred.await(interrupted)
const exit = yield* Fiber.await(resumed)
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBeTrue()
expect(Array.from(yield* coordinator.active)).toEqual([])
expect(runs).toBe(1)
expect(reasons).toEqual(["user"])
}),
),
)
@@ -309,7 +252,6 @@ describe("SessionRunCoordinator", () => {
const cleanupGate = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
let runs = 0
let starts = 0
const coordinator = yield* SessionRunCoordinator.make({
drain: () =>
Effect.sync(() => ++runs).pipe(
@@ -324,7 +266,6 @@ describe("SessionRunCoordinator", () => {
: Deferred.succeed(secondStarted, undefined),
),
),
started: () => Effect.sync(() => starts++).pipe(Effect.asVoid),
})
yield* coordinator.wake("session")
@@ -337,7 +278,6 @@ describe("SessionRunCoordinator", () => {
yield* Deferred.await(secondStarted)
expect(runs).toBe(2)
expect(starts).toBe(2)
}),
),
)
@@ -459,7 +399,6 @@ describe("SessionRunCoordinator", () => {
const gate = yield* Deferred.make<void>()
const idle = yield* Deferred.make<void>()
let drains = 0
let starts = 0
const settled: Exit.Exit<void, never>[] = []
const coordinator = yield* SessionRunCoordinator.make<string, never>({
drain: () =>
@@ -471,7 +410,6 @@ describe("SessionRunCoordinator", () => {
),
Effect.asVoid,
),
started: () => Effect.sync(() => starts++).pipe(Effect.asVoid),
settled: (_key, exit) =>
Effect.sync(() => void settled.push(exit)).pipe(
Effect.andThen(Deferred.succeed(idle, undefined)),
@@ -486,7 +424,6 @@ describe("SessionRunCoordinator", () => {
yield* Deferred.await(idle)
expect(drains).toBe(2)
expect(starts).toBe(1)
expect(settled).toHaveLength(1)
expect(Exit.isSuccess(settled[0]!)).toBe(true)
}),
@@ -28,14 +28,17 @@ describe("toLLMMessages", () => {
const messages = toLLMMessages(
[
assistant("empty", []),
assistant("empty-text", [SessionMessage.AssistantText.make({ type: "text", text: "" })]),
assistant("empty-reasoning", [SessionMessage.AssistantReasoning.make({ type: "reasoning", text: "" })]),
assistant("text", [SessionMessage.AssistantText.make({ type: "text", text: "Partial" })]),
assistant("empty-text", [SessionMessage.AssistantText.make({ type: "text", id: "empty", text: "" })]),
assistant("empty-reasoning", [
SessionMessage.AssistantReasoning.make({ type: "reasoning", id: "empty-reasoning", text: "" }),
]),
assistant("text", [SessionMessage.AssistantText.make({ type: "text", id: "text", text: "Partial" })]),
assistant("reasoning", [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning",
text: "",
state: { signature: "sig_1" },
providerMetadata: { anthropic: { signature: "sig_1" } },
}),
]),
],
@@ -155,11 +158,12 @@ Recent work
agent: "build",
model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
content: [
SessionMessage.AssistantText.make({ type: "text", text: "Checking" }),
SessionMessage.AssistantText.make({ type: "text", id: "text-1", text: "Checking" }),
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-1",
text: "Think",
state: { signature: "sig_1" },
providerMetadata: { anthropic: { signature: "sig_1" } },
}),
SessionMessage.AssistantTool.make({
type: "tool",
@@ -204,9 +208,11 @@ Recent work
type: "tool",
id: "hosted",
name: "web_search",
executed: true,
providerState: { continuation: "hosted-call" },
providerResultState: { continuation: "hosted-result" },
provider: {
executed: true,
metadata: { fake: { continuation: "hosted-call" } },
resultMetadata: { fake: { continuation: "hosted-result" } },
},
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { query: "Effect" },
@@ -219,8 +225,7 @@ Recent work
type: "tool",
id: "hosted-failed",
name: "write",
executed: true,
providerState: { continuation: "failed" },
provider: { executed: true, metadata: { fake: { continuation: "failed" } } },
state: SessionMessage.ToolStateError.make({
status: "error",
input: { path: "README.md" },
@@ -240,7 +245,7 @@ Recent work
expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"])
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Checking" },
{ type: "reasoning", text: "Think", providerMetadata: { provider: { signature: "sig_1" } } },
{ type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "sig_1" } } },
{ type: "tool-call", id: "pending", name: "read", input: { path: "README.md" } },
{ type: "tool-call", id: "running", name: "read", input: { path: "README.md" } },
{
@@ -255,14 +260,14 @@ Recent work
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { provider: { continuation: "hosted-call" } },
providerMetadata: { fake: { continuation: "hosted-call" } },
},
{
type: "tool-result",
id: "hosted",
name: "web_search",
providerExecuted: true,
providerMetadata: { provider: { continuation: "hosted-result" } },
providerMetadata: { fake: { continuation: "hosted-result" } },
result: { type: "text", value: "Found it" },
},
{
@@ -271,14 +276,14 @@ Recent work
name: "write",
input: { path: "README.md" },
providerExecuted: true,
providerMetadata: { provider: { continuation: "failed" } },
providerMetadata: { fake: { continuation: "failed" } },
},
{
type: "tool-result",
id: "hosted-failed",
name: "write",
providerExecuted: true,
providerMetadata: { provider: { continuation: "failed" } },
providerMetadata: { fake: { continuation: "failed" } },
result: {
type: "error",
value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} },
@@ -312,8 +317,9 @@ Recent work
content: [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-openai",
text: "Think",
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
}),
],
time: { created, completed: created },
@@ -326,7 +332,7 @@ Recent work
{
type: "reasoning",
text: "Think",
providerMetadata: { provider: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
})
@@ -342,16 +348,19 @@ Recent work
content: [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-failed",
text: "Partial thought",
state: { itemId: "rs_failed", reasoningEncryptedContent: null },
providerMetadata: { openai: { itemId: "rs_failed", reasoningEncryptedContent: null } },
}),
SessionMessage.AssistantTool.make({
type: "tool",
id: "hosted-failed",
name: "web_search",
executed: true,
providerState: { itemId: "call_failed" },
providerResultState: { itemId: "result_failed" },
provider: {
executed: true,
metadata: { openai: { itemId: "call_failed" } },
resultMetadata: { openai: { itemId: "result_failed" } },
},
state: SessionMessage.ToolStateError.make({
status: "error",
input: { query: "Effect" },
@@ -411,16 +420,19 @@ Recent work
content: [
SessionMessage.AssistantReasoning.make({
type: "reasoning",
id: "reasoning-old-model",
text: "Visible thought",
state: { signature: "sig_old" },
providerMetadata: { anthropic: { signature: "sig_old" } },
}),
SessionMessage.AssistantTool.make({
type: "tool",
id: "hosted-old-model",
name: "web_search",
executed: true,
providerState: { itemId: "hosted-old-model" },
providerResultState: { itemId: "hosted-old-model" },
provider: {
executed: true,
metadata: { openai: { itemId: "hosted-old-model" } },
resultMetadata: { openai: { itemId: "hosted-old-model" } },
},
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { query: "Effect" },
@@ -434,9 +446,11 @@ Recent work
type: "tool",
id: "local-old-model",
name: "read",
executed: false,
providerState: { call: "old" },
providerResultState: { result: "old" },
provider: {
executed: false,
metadata: { fake: { call: "old" } },
resultMetadata: { fake: { result: "old" } },
},
state: SessionMessage.ToolStateCompleted.make({
status: "completed",
input: { path: "README.md" },
@@ -47,7 +47,6 @@ const capture = () => {
id: ModelV2.ID.make("model"),
providerID: ProviderV2.ID.make("provider"),
},
provider: "openai",
}),
}
}
@@ -91,7 +90,7 @@ test("local tool success serializes media base64 once and reconstructs from stru
})
})
test("provider-executed success retains its raw provider result", async () => {
test("provider-executed success retains its compatibility result", async () => {
const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true })))
await Effect.runPromise(publisher.publish(LLMEvent.toolResult({ ...result, providerExecuted: true })))
@@ -99,19 +98,6 @@ test("provider-executed success retains its raw provider result", async () => {
expect(success?.data).toHaveProperty("result")
})
test("provider state uses the route provider instead of the catalog provider", async () => {
const { published, publisher } = capture()
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { openai: { itemId: "reasoning" } } }),
),
)
expect(published.find((event) => event.type === "session.reasoning.started.1")?.data).toMatchObject({
state: { itemId: "reasoning" },
})
})
test("binary failure emits no success event", async () => {
const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(call))
@@ -128,7 +114,7 @@ test("binary failure emits no success event", async () => {
expect(published.some((event) => event.type === "session.tool.failed.1")).toBe(true)
})
test("success event data can carry a provider-executed result", () => {
test("old success event data containing result still decodes", () => {
const decoded = Schema.decodeUnknownSync(SessionEvent.Tool.Success.data)({
sessionID,
assistantMessageID: SessionMessage.ID.create(),
@@ -136,7 +122,7 @@ test("success event data can carry a provider-executed result", () => {
structured: { type: "media", mime: "image/png" },
content: [{ type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }],
result: { type: "content", value: [{ type: "file", uri: `data:image/png;base64,${base64}`, mime: "image/png" }] },
executed: true,
provider: { executed: false },
})
expect(decoded.result).toMatchObject({ type: "content" })
})
@@ -149,37 +135,3 @@ test("step finish records settlement without publishing step ended", async () =>
expect(published.some((event) => event.type === "step.ended.2")).toBe(false)
expect(publisher.stepSettlement()).toMatchObject({ finish: "stop" })
})
test("content-filter finish fails a contentless step", async () => {
const { published, publisher } = capture()
await Effect.runPromise(publisher.publish(LLMEvent.stepStart({ index: 0 })))
await Effect.runPromise(publisher.publish(LLMEvent.stepFinish({ index: 0, reason: "content-filter" })))
expect(published.map((event) => event.type)).toEqual(["session.step.started.1", "session.step.failed.1"])
expect(published.at(-1)?.data).toMatchObject({
error: { type: "provider.content-filter", message: "Provider blocked the response" },
})
expect(publisher.stepSettlement()).toBeUndefined()
})
test("content-filter finish preserves partial streamed text and never ends the step successfully", async () => {
const { published, publisher } = capture()
await Effect.runPromise(
Effect.forEach(
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text" }),
LLMEvent.textDelta({ id: "text", text: "Partial" }),
LLMEvent.stepFinish({ index: 0, reason: "content-filter" }),
],
(event) => publisher.publish(event),
{ discard: true },
),
)
expect(published.some((event) => event.type === "session.step.ended.1")).toBe(false)
expect(published.find((event) => event.type === "session.text.ended.1")?.data).toMatchObject({ text: "Partial" })
expect(published.find((event) => event.type === "session.step.failed.1")?.data).toMatchObject({
error: { type: "provider.content-filter" },
})
})
+54 -335
View File
@@ -4,10 +4,8 @@ import {
LLMError,
LLMEvent,
Model,
ToolFailure,
TransportReason,
InvalidRequestReason,
RateLimitReason,
type LLMClientShape,
type LLMRequest,
} from "@opencode-ai/llm"
@@ -28,7 +26,6 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { Snapshot } from "@opencode-ai/core/snapshot"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { UserInterruptedError } from "@opencode-ai/core/session/error"
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
import { SessionTitle } from "@opencode-ai/core/session/title"
import { SessionInput } from "@opencode-ai/core/session/input"
@@ -64,8 +61,7 @@ import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
import { ModelV2 } from "@opencode-ai/core/model"
import { Location } from "@opencode-ai/core/location"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Schema, Stream } from "effect"
import { TestClock } from "effect/testing"
import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream } from "effect"
import { asc, eq } from "drizzle-orm"
import { testEffect } from "./lib/effect"
@@ -372,20 +368,6 @@ const providerUnavailable = () =>
reason: new TransportReason({ message: "Provider unavailable" }),
})
const invalidRequest = () =>
new LLMError({
module: "test",
method: "stream",
reason: new InvalidRequestReason({ message: "Invalid request" }),
})
const rateLimited = (retryAfterMs?: number) =>
new LLMError({
module: "test",
method: "stream",
reason: new RateLimitReason({ message: "Rate limited", retryAfterMs }),
})
const setupOverflowRecovery = Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
@@ -473,7 +455,7 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
LLMEvent.textStart({ id }),
...chunks.map((text) => LLMEvent.textDelta({ id, text })),
]
const expectedContent = { type: "text", text }
const expectedContent = { type: "text", id, text }
return {
delta: SessionEvent.Text.Delta,
partialEvents,
@@ -493,7 +475,7 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
LLMEvent.reasoningStart({ id }),
...chunks.map((text) => LLMEvent.reasoningDelta({ id, text })),
]
const expectedContent = { type: "reasoning", text }
const expectedContent = { type: "reasoning", id, text }
return {
delta: SessionEvent.Reasoning.Delta,
partialEvents,
@@ -528,7 +510,6 @@ const fragmentFixture = (kind: FragmentKind, id: string, chunks: readonly string
const verifyEphemeralDeltas = (kind: FragmentKind) =>
Effect.gen(function* () {
yield* setup
requests.length = 0
const session = yield* SessionV2.Service
const prompt = `Stream ${kind}`
const chunks = Array.from({ length: 32 }, (_, index) => `${index},`)
@@ -561,7 +542,6 @@ const verifyEphemeralDeltas = (kind: FragmentKind) =>
const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
Effect.gen(function* () {
yield* setup
requests.length = 0
const session = yield* SessionV2.Service
const prompt = `Fail after ${kind}`
const fixture = fragmentFixture(kind, fragmentID(kind, "partial"), ["Partial"])
@@ -575,11 +555,10 @@ const verifyPartialFlushOnFailure = (kind: FragmentKind) =>
{
type: "assistant",
finish: "error",
error: { type: "provider.transport", message: "Provider unavailable" },
error: { type: "unknown", message: "Provider unavailable" },
content: [fixture.expectedContent],
},
])
expect(requests).toHaveLength(1)
})
const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
@@ -604,7 +583,7 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
{
type: "assistant",
finish: "error",
error: { type: "aborted", message: "Step interrupted" },
error: { type: "unknown", message: "Step interrupted" },
content: [
kind === "tool input"
? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
@@ -1390,7 +1369,7 @@ describe("SessionRunnerLLM", () => {
overflow(),
]
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
yield* session.resume(sessionID)
expect(requests).toHaveLength(3)
expect(yield* session.context(sessionID)).toMatchObject([
@@ -1436,7 +1415,7 @@ describe("SessionRunnerLLM", () => {
[LLMEvent.providerError({ message: "summary unavailable" })],
]
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Continue" }), resume: false })
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
yield* session.resume(sessionID)
expect(requests).toHaveLength(2)
const context = yield* session.context(sessionID)
@@ -1573,7 +1552,7 @@ describe("SessionRunnerLLM", () => {
finish: "tool-calls",
tokens: { input: 8, output: 3, reasoning: 1, cache: { read: 2, write: 0 } },
content: [
{ type: "reasoning", text: "Think" },
{ type: "reasoning", id: "reasoning-1", text: "Think" },
{
type: "tool",
id: "call-error",
@@ -1581,16 +1560,14 @@ describe("SessionRunnerLLM", () => {
state: {
status: "error",
input: { path: "README.md" },
error: { type: "tool.execution", message: "Denied" },
error: { type: "unknown", message: "Denied" },
},
},
{
type: "tool",
id: "call-provider",
name: "web_search",
executed: true,
providerState: { source: "provider" },
providerResultState: { source: "provider" },
provider: { executed: true, metadata: { fake: { source: "provider" } } },
state: {
status: "completed",
input: { query: "hello" },
@@ -1660,7 +1637,7 @@ describe("SessionRunnerLLM", () => {
},
],
},
{ type: "assistant", finish: "stop", content: [{ type: "text", text: "Done" }] },
{ type: "assistant", finish: "stop", content: [{ type: "text", id: "text-final", text: "Done" }] },
])
}),
)
@@ -1719,24 +1696,15 @@ describe("SessionRunnerLLM", () => {
LLMEvent.stepStart({ index: 0 }),
LLMEvent.reasoningStart({ id: "reasoning-anthropic" }),
LLMEvent.reasoningDelta({ id: "reasoning-anthropic", text: "Signed thought" }),
LLMEvent.reasoningEnd({
id: "reasoning-anthropic",
providerMetadata: { fake: { signature: "sig_1" }, anthropic: { ignored: true } },
}),
LLMEvent.reasoningEnd({ id: "reasoning-anthropic", providerMetadata: { anthropic: { signature: "sig_1" } } }),
LLMEvent.reasoningStart({
id: "reasoning-openai",
providerMetadata: {
fake: { itemId: "rs_1", reasoningEncryptedContent: null },
openai: { ignored: true },
},
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
}),
LLMEvent.reasoningDelta({ id: "reasoning-openai", text: "Encrypted thought" }),
LLMEvent.reasoningEnd({
id: "reasoning-openai",
providerMetadata: {
fake: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
openai: { ignored: true },
},
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
}),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
@@ -1749,11 +1717,11 @@ describe("SessionRunnerLLM", () => {
{
type: "assistant",
content: [
{ type: "reasoning", text: "Signed thought", state: { signature: "sig_1" } },
{ type: "reasoning", text: "Signed thought", providerMetadata: { anthropic: { signature: "sig_1" } } },
{
type: "reasoning",
text: "Encrypted thought",
state: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
],
},
@@ -1764,11 +1732,11 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
expect(requests[1]?.messages[1]?.content).toEqual([
{ type: "reasoning", text: "Signed thought", providerMetadata: { fake: { signature: "sig_1" } } },
{ type: "reasoning", text: "Signed thought", providerMetadata: { anthropic: { signature: "sig_1" } } },
{
type: "reasoning",
text: "Encrypted thought",
providerMetadata: { fake: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
}),
@@ -1788,14 +1756,14 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { fake: { itemId: "hosted-search" }, openai: { ignored: true } },
providerMetadata: { openai: { itemId: "hosted-search" } },
}),
LLMEvent.toolResult({
id: "hosted-search",
name: "web_search",
result: { type: "json", value: [{ title: "Effect" }] },
providerExecuted: true,
providerMetadata: { fake: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } },
providerMetadata: { anthropic: { blockType: "web_search_tool_result" } },
}),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
@@ -1815,7 +1783,7 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { fake: { itemId: "hosted-search" } },
providerMetadata: { openai: { itemId: "hosted-search" } },
},
{
type: "tool-result",
@@ -1823,7 +1791,7 @@ describe("SessionRunnerLLM", () => {
name: "web_search",
result: { type: "json", value: [{ title: "Effect" }] },
providerExecuted: true,
providerMetadata: { fake: { blockType: "web_search_tool_result" } },
providerMetadata: { anthropic: { blockType: "web_search_tool_result" } },
},
])
}),
@@ -2012,7 +1980,7 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Run once" },
{ type: "assistant", finish: "stop", content: [{ type: "text", text: "Once" }] },
{ type: "assistant", finish: "stop", content: [{ type: "text", id: "text-once", text: "Once" }] },
])
}),
)
@@ -2384,7 +2352,7 @@ describe("SessionRunnerLLM", () => {
requests.length = 0
responses = undefined
response = []
streamFailure = invalidRequest()
streamFailure = providerUnavailable()
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2434,8 +2402,9 @@ describe("SessionRunnerLLM", () => {
sessionID,
assistantMessageID,
callID: "call-interrupted",
tool: "echo",
input: { text: "stale" },
executed: false,
provider: { executed: false },
})
requests.length = 0
response = []
@@ -2451,10 +2420,7 @@ describe("SessionRunnerLLM", () => {
{
type: "tool",
id: "call-interrupted",
state: {
status: "error",
error: { type: "tool.stale", message: "Tool execution interrupted", name: "echo" },
},
state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } },
},
],
},
@@ -2496,9 +2462,9 @@ describe("SessionRunnerLLM", () => {
sessionID,
assistantMessageID,
callID: "call-hosted-interrupted",
tool: "web_search",
input: { query: "stale" },
executed: true,
state: { itemId: "call-hosted-interrupted" },
provider: { executed: true, metadata: { openai: { itemId: "call-hosted-interrupted" } } },
})
requests.length = 0
response = []
@@ -2511,7 +2477,7 @@ describe("SessionRunnerLLM", () => {
type: "tool-call",
id: "call-hosted-interrupted",
providerExecuted: true,
providerMetadata: { fake: { itemId: "call-hosted-interrupted" } },
providerMetadata: { openai: { itemId: "call-hosted-interrupted" } },
},
{ type: "tool-result", id: "call-hosted-interrupted", providerExecuted: true, result: { type: "error" } },
])
@@ -2697,7 +2663,7 @@ describe("SessionRunnerLLM", () => {
requests.length = 0
responses = undefined
response = []
streamFailure = invalidRequest()
streamFailure = providerUnavailable()
streamGate = yield* Deferred.make<void>()
streamStarted = yield* Deferred.make<void>()
@@ -2760,7 +2726,7 @@ describe("SessionRunnerLLM", () => {
},
],
},
{ type: "assistant", finish: "stop", content: [{ type: "text", text: "Recovered" }] },
{ type: "assistant", finish: "stop", content: [{ type: "text", id: "text-after-error", text: "Recovered" }] },
])
}),
)
@@ -2803,7 +2769,7 @@ describe("SessionRunnerLLM", () => {
id: "call-defect",
state: {
status: "error",
error: { type: "unknown", message: "unexpected tool defect" },
error: { type: "unknown", message: "Tool execution failed: unexpected tool defect" },
},
},
],
@@ -2846,80 +2812,13 @@ describe("SessionRunnerLLM", () => {
status: "error",
error: {
type: "unknown",
message: expect.stringContaining("Failed to encode tool output"),
},
},
},
],
finish: "error",
error: { type: "unknown", message: expect.stringContaining("Failed to encode tool output") },
},
])
}),
)
it.effect("preserves permission rejection and stops before continuation", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
yield* registry.register({
permissionfail: Tool.make({
description: "Reject a permission",
input: Schema.Struct({}),
output: Schema.Struct({}),
execute: () =>
new ToolFailure({
message: "Permission rejected: edit",
error: new PermissionV2.RejectedError({ permission: "edit", resources: ["src/index.ts"] }),
}),
}),
})
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Reject permission" }), resume: false })
requests.length = 0
responses = [
[
LLMEvent.stepStart({ index: 0 }),
LLMEvent.toolCall({ id: "call-permission", name: "permissionfail", input: {} }),
LLMEvent.stepFinish({ index: 0, reason: "tool-calls" }),
LLMEvent.finish({ reason: "tool-calls" }),
],
[LLMEvent.stepStart({ index: 0 }), LLMEvent.stepFinish({ index: 0, reason: "stop" })],
]
const exit = yield* session.resume(sessionID).pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user" },
{
type: "assistant",
finish: "error",
error: {
type: "permission.rejected",
message: "Permission rejected: edit",
permission: "edit",
resources: ["src/index.ts"],
},
content: [
{
type: "tool",
id: "call-permission",
state: {
status: "error",
error: {
type: "permission.rejected",
message: "Permission rejected: edit",
permission: "edit",
resources: ["src/index.ts"],
message: expect.stringContaining("Tool execution failed: Failed to encode tool output"),
},
},
},
],
},
])
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1")
}),
)
@@ -2961,8 +2860,7 @@ describe("SessionRunnerLLM", () => {
const exit = yield* Fiber.join(run)
expect(exit._tag).toBe("Failure")
if (exit._tag === "Failure")
expect(Option.getOrUndefined(Cause.findErrorOption(exit.cause))).toBeInstanceOf(UserInterruptedError)
if (exit._tag === "Failure") expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true)
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Ask then stop" },
@@ -2972,7 +2870,7 @@ describe("SessionRunnerLLM", () => {
{
type: "tool",
id: "call-question",
state: { status: "error", error: { type: "aborted", message: "Tool execution interrupted" } },
state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } },
},
],
},
@@ -3044,7 +2942,7 @@ describe("SessionRunnerLLM", () => {
{
type: "tool",
id: "call-before-interrupt",
state: { status: "error", error: { type: "aborted", message: "Tool execution interrupted" } },
state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } },
},
],
},
@@ -3085,7 +2983,7 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Interrupt provider" },
{ type: "assistant", finish: "error", error: { type: "aborted", message: "Step interrupted" } },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Step interrupted" } },
])
expect(yield* recordedEventTypes(sessionID)).toContain("session.step.failed.1")
yield* session.interrupt(sessionID)
@@ -3120,12 +3018,12 @@ describe("SessionRunnerLLM", () => {
{
type: "assistant",
finish: "error",
error: { type: "aborted", message: "Step interrupted" },
error: { type: "unknown", message: "Step interrupted" },
content: [
{
type: "tool",
id: "call-await-interrupt",
state: { status: "error", error: { type: "aborted", message: "Tool execution interrupted" } },
state: { status: "error", error: { type: "unknown", message: "Tool execution interrupted" } },
},
],
},
@@ -3248,12 +3146,12 @@ describe("SessionRunnerLLM", () => {
streamStarted = undefined
response = [LLMEvent.stepStart({ index: 0 }), LLMEvent.providerError({ message: "Provider unavailable" })]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Fail durably" },
{ type: "assistant", finish: "error", error: { type: "provider.unknown", message: "Provider unavailable" } },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } },
])
}),
)
@@ -3267,43 +3165,16 @@ describe("SessionRunnerLLM", () => {
requests.length = 0
response = [LLMEvent.providerError({ message: "Provider unavailable" })]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Fail before step" },
{ type: "assistant", finish: "error", error: { type: "provider.unknown", message: "Provider unavailable" } },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } },
])
}),
)
it.effect("projects content-filter finishes as visible terminal failures", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Blocked response" }), resume: false })
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "partial" }),
LLMEvent.textDelta({ id: "partial", text: "Partial" }),
LLMEvent.stepFinish({ index: 0, reason: "content-filter" }),
LLMEvent.finish({ reason: "content-filter" }),
]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider blocked the response")
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user" },
{
type: "assistant",
finish: "error",
error: { type: "provider.content-filter" },
content: [{ type: "text", text: "Partial" }],
},
])
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.step.ended.1")
}),
)
it.effect("does not recover context overflow after durable assistant output", () =>
Effect.gen(function* () {
yield* setup
@@ -3318,7 +3189,7 @@ describe("SessionRunnerLLM", () => {
LLMEvent.textEnd({ id: "text-partial" }),
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
@@ -3338,146 +3209,18 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Fail raw stream durably" }), resume: false })
const failure = invalidRequest()
const failure = providerUnavailable()
responseStream = Stream.fail(failure)
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
yield* replaySessionProjection(sessionID)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Fail raw stream durably" },
{ type: "assistant", finish: "error", error: { type: "provider.invalid-request", message: "Invalid request" } },
{ type: "assistant", finish: "error", error: { type: "unknown", message: "Provider unavailable" } },
])
}),
)
it.effect("retries eligible pre-output failures after exponential backoff", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Retry transport" }), resume: false })
requests.length = 0
responseStream = Stream.fail(providerUnavailable())
response = fragmentFixture("text", "retry-success", ["Recovered"]).completeEvents
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
yield* TestClock.adjust("1999 millis")
expect(requests).toHaveLength(1)
yield* TestClock.adjust("1 millis")
yield* Fiber.join(run)
expect(requests).toHaveLength(2)
const eventTypes = yield* recordedEventTypes(sessionID)
expect(eventTypes).toContain("session.retry.scheduled.1")
expect(eventTypes.filter((type) => type === "session.step.started.1")).toHaveLength(2)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user" },
{ type: "assistant", finish: "stop", content: [{ type: "text", text: "Recovered" }] },
])
yield* replaySessionProjection(sessionID)
expect((yield* session.context(sessionID)).filter((message) => message.type === "assistant")).toHaveLength(1)
}),
)
it.effect("uses a larger provider retry-after delay", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Retry rate limit" }), resume: false })
requests.length = 0
responseStream = Stream.fail(rateLimited(5_000))
response = fragmentFixture("text", "retry-after-success", ["Recovered"]).completeEvents
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
yield* TestClock.adjust("4999 millis")
expect(requests).toHaveLength(1)
yield* TestClock.adjust("1 millis")
yield* Fiber.join(run)
expect(requests).toHaveLength(2)
}),
)
it.effect("stops after five total retry attempts", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Exhaust retries" }), resume: false })
requests.length = 0
streamFailure = providerUnavailable()
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
for (const [index, delay] of [2_000, 4_000, 8_000, 16_000].entries()) {
yield* TestClock.adjust(delay)
while (requests.length < index + 2) yield* Effect.yieldNow
}
expect(yield* Fiber.join(run).pipe(Effect.flip)).toBe(streamFailure)
expect(requests).toHaveLength(5)
const database = (yield* Database.Service).db
const retries = yield* database
.select({ data: EventTable.data })
.from(EventTable)
.where(eq(EventTable.type, "session.retry.scheduled.1"))
.orderBy(asc(EventTable.seq))
.all()
.pipe(Effect.orDie)
expect(retries.map((event) => event.data)).toMatchObject([
{ attempt: 2, at: 2_000 },
{ attempt: 3, at: 6_000 },
{ attempt: 4, at: 14_000 },
{ attempt: 5, at: 30_000 },
])
expect((yield* recordedEventTypes(sessionID)).filter((type) => type === "session.step.started.1")).toHaveLength(5)
expect((yield* session.context(sessionID)).filter((message) => message.type === "assistant")).toHaveLength(1)
}),
)
it.effect("counts retry attempts against the agent step allowance", () =>
Effect.gen(function* () {
yield* setup
const agents = yield* AgentV2.Service
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.steps = 2
}),
)
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Bound retries by steps" }), resume: false })
requests.length = 0
const failure = providerUnavailable()
responseStream = Stream.fail(failure)
streamFailure = failure
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
while (requests.length < 1) yield* Effect.yieldNow
yield* TestClock.adjust("2 seconds")
expect(yield* Fiber.join(run).pipe(Effect.flip)).toBe(failure)
expect(requests).toHaveLength(2)
const eventTypes = yield* recordedEventTypes(sessionID)
expect(eventTypes.filter((type) => type === "session.step.started.1")).toHaveLength(2)
expect(eventTypes.filter((type) => type === "session.retry.scheduled.1")).toHaveLength(1)
expect((yield* session.context(sessionID)).filter((message) => message.type === "assistant")).toHaveLength(1)
}),
)
it.effect("does not retry non-eligible provider failures", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Do not retry" }), resume: false })
requests.length = 0
const failure = invalidRequest()
streamFailure = failure
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
expect(requests).toHaveLength(1)
expect(yield* recordedEventTypes(sessionID)).not.toContain("session.retry.scheduled.1")
}),
)
it.effect("does not continue automatically after a provider error follows a local tool call", () =>
Effect.gen(function* () {
yield* setup
@@ -3496,7 +3239,7 @@ describe("SessionRunnerLLM", () => {
LLMEvent.providerError({ message: "Provider unavailable" }),
]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(executions.slice(executionCount)).toEqual(["settled"])
@@ -3521,7 +3264,7 @@ describe("SessionRunnerLLM", () => {
LLMEvent.providerError({ message: "Provider unavailable" }),
]
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("Provider unavailable")
yield* session.resume(sessionID)
expect(requests).toHaveLength(1)
expect(yield* session.context(sessionID)).toMatchObject([
@@ -3568,7 +3311,6 @@ describe("SessionRunnerLLM", () => {
prompt: Prompt.make({ text: "Fail hosted tool on raw failure" }),
resume: false,
})
requests.length = 0
const failure = providerUnavailable()
responseStream = Stream.concat(
Stream.fromIterable([
@@ -3584,21 +3326,20 @@ describe("SessionRunnerLLM", () => {
)
expect(yield* session.resume(sessionID).pipe(Effect.flip)).toBe(failure)
expect(requests).toHaveLength(1)
yield* replaySessionProjection(sessionID)
expect(yield* session.context(sessionID)).toMatchObject([
{ type: "user", text: "Fail hosted tool on raw failure" },
{
type: "assistant",
finish: "error",
error: { type: "provider.transport", message: "Provider unavailable" },
error: { type: "unknown", message: "Provider unavailable" },
content: [{ type: "tool", id: "call-hosted-raw-failure", state: { status: "error" } }],
},
])
}),
)
it.effect("rejects a second text start before the open fragment ends", () =>
it.effect("keeps interleaved assistant text blocks separate", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
@@ -3611,31 +3352,9 @@ describe("SessionRunnerLLM", () => {
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-1" }),
LLMEvent.textStart({ id: "text-2" }),
]
const defect = yield* session.resume(sessionID).pipe(Effect.catchDefect(Effect.succeed))
expect(defect).toBeInstanceOf(Error)
if (!(defect instanceof Error)) return
expect(defect.message).toBe("text start before end: text-2")
}),
)
it.effect("projects sequential text fragments as separate content parts", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Two blocks" }), resume: false })
responses = undefined
streamGate = undefined
streamStarted = undefined
response = [
LLMEvent.stepStart({ index: 0 }),
LLMEvent.textStart({ id: "text-1" }),
LLMEvent.textDelta({ id: "text-1", text: "First" }),
LLMEvent.textEnd({ id: "text-1" }),
LLMEvent.textStart({ id: "text-2" }),
LLMEvent.textDelta({ id: "text-2", text: "Second" }),
LLMEvent.textEnd({ id: "text-1" }),
LLMEvent.textEnd({ id: "text-2" }),
LLMEvent.stepFinish({ index: 0, reason: "stop" }),
LLMEvent.finish({ reason: "stop" }),
@@ -3648,8 +3367,8 @@ describe("SessionRunnerLLM", () => {
{
type: "assistant",
content: [
{ type: "text", text: "First" },
{ type: "text", text: "Second" },
{ type: "text", id: "text-1", text: "First" },
{ type: "text", id: "text-2", text: "Second" },
],
},
])
+70
View File
@@ -0,0 +1,70 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer, LayerMap } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import type { LocationServices } from "@opencode-ai/core/location-services"
import { ProjectV2 } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionExecution } from "@opencode-ai/core/session/execution"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { SessionStore } from "@opencode-ai/core/session/store"
import { SkillV2 } from "@opencode-ai/core/skill"
import { testEffect } from "./lib/effect"
const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
const projects = Layer.mock(ProjectV2.Service, {
resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
})
const skills = Layer.mock(SkillV2.Service, {
list: () =>
Effect.succeed([
SkillV2.Info.make({
name: "effect",
description: "Effect guidance",
location: AbsolutePath.make(path.resolve("/skills/effect/SKILL.md")),
content: "Use Effect",
}),
]),
})
const locations = Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
() =>
// The skill endpoint only needs the location-scoped Skill service.
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
skills as unknown as Layer.Layer<LocationServices>,
),
)
const it = testEffect(
AppNodeBuilder.build(
LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]),
[
[LocationServiceMap.node, locations],
[ProjectV2.node, projects],
[SessionExecution.node, SessionExecution.noopLayer],
],
),
)
describe("SessionV2.skill", () => {
it.effect("projects the caller-supplied message ID", () =>
Effect.gen(function* () {
const sessions = yield* SessionV2.Service
const session = yield* sessions.create({ location })
const id = SessionMessage.ID.make("msg_caller_skill")
yield* sessions.skill({ id, sessionID: session.id, skill: "effect", resume: false })
expect(yield* sessions.messages({ sessionID: session.id })).toContainEqual(
expect.objectContaining({ id, type: "skill", name: "effect", text: "Use Effect" }),
)
}),
)
})
@@ -76,8 +76,9 @@ describe("Tool.Progress", () => {
sessionID,
assistantMessageID,
callID,
tool: "bash",
input: { command: "pwd" },
executed: false,
provider: { executed: false },
})
})
@@ -103,7 +104,7 @@ describe("Tool.Progress", () => {
callID: "call-success",
structured: { phase: "done" },
content: content("complete"),
executed: false,
provider: { executed: false },
})
expect((yield* readAssistant).content[0]).toMatchObject({
state: { status: "completed", structured: { phase: "done" }, content: content("complete") },
@@ -122,7 +123,7 @@ describe("Tool.Progress", () => {
assistantMessageID,
callID: "call-failed",
error: { type: "unknown", message: "boom" },
executed: false,
provider: { executed: false },
})
expect((yield* readAssistant).content[1]).toMatchObject({
state: {
+2 -3
View File
@@ -107,7 +107,6 @@ test("Core reuses the canonical shared schemas", async () => {
[coreIntegration.Ref, Integration.Ref],
[coreLocation.Ref, Location.Ref],
[coreLLM.ProviderMetadata, LLM.ProviderMetadata],
[coreLLM.FinishReason, LLM.FinishReason],
[coreLLM.ToolTextContent, LLM.ToolTextContent],
[coreLLM.ToolFileContent, LLM.ToolFileContent],
[coreLLM.ToolContent, LLM.ToolContent],
@@ -148,7 +147,7 @@ test("Core reuses the canonical shared schemas", async () => {
[coreSessionInput.Delivery, SessionInput.Delivery],
[coreSessionInput.Admitted, SessionInput.Admitted],
[coreSessionMessage.ID, SessionMessage.ID],
[coreSessionMessage.AssistantRetry, SessionMessage.AssistantRetry],
[coreSessionMessage.UnknownError, SessionMessage.UnknownError],
[coreSessionMessage.AgentSelected, SessionMessage.AgentSelected],
[coreSessionMessage.ModelSelected, SessionMessage.ModelSelected],
[coreSessionMessage.User, SessionMessage.User],
@@ -198,7 +197,7 @@ test("Core reuses the canonical shared schemas", async () => {
test("shared record schemas construct and decode plain objects", () => {
const made = Prompt.make({ text: "hello" })
const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
const content = Schema.decodeUnknownSync(SessionMessage.AssistantText)({ type: "text", text: "hi" })
const content = Schema.decodeUnknownSync(SessionMessage.AssistantText)({ type: "text", id: "part_1", text: "hi" })
expect(Object.getPrototypeOf(made)).toBe(Object.prototype)
expect(Object.getPrototypeOf(decoded)).toBe(Object.prototype)

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