Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1495cc846f | |||
| 3ce5e9800d | |||
| af5eabcb26 | |||
| 0087dd56d9 | |||
| ef2140d121 | |||
| 1de3c6e4a6 | |||
| b6c3a8ea7b | |||
| efcf2c3f5d | |||
| e65477ab1d | |||
| cd0b274856 | |||
| 57e9e9771d | |||
| 1aae92c42a | |||
| 6555df912a | |||
| 7ebd344fa2 | |||
| 460cdc5aec | |||
| b0ca5520a1 | |||
| bc2e270f82 | |||
| 33705e632a | |||
| cff2345c12 | |||
| 0405518180 | |||
| c9604c86ec | |||
| 967c44552e | |||
| ec95694e2f | |||
| c176baee82 | |||
| 016d296f7c | |||
| dbaa53329c | |||
| 7843f8fb38 | |||
| 4045041554 | |||
| 7ec7413fdb | |||
| f016392368 | |||
| 140224b0fc | |||
| cfd35c9354 | |||
| 674d08f9be | |||
| 02f012f5d1 | |||
| bea6e1499d | |||
| 50a1fe49bc | |||
| 6380919fda | |||
| d972aa9d83 | |||
| d544ab4d91 | |||
| ce228bfd7c | |||
| eabf85aea2 | |||
| 2f4a688790 | |||
| 4a90ffedfb | |||
| 95cf5039be |
@@ -2,9 +2,9 @@ name: typecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
branches: [dev, v2]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
branches: [dev, v2]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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" })
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export default {
|
||||
id: "sample-agent-plugin",
|
||||
setup: async (ctx) => {
|
||||
await ctx.agent.transform((agents) => {
|
||||
agents.update("sample-plugin-agent", (agent) => {
|
||||
agent.description = "Example primary agent registered by .opencode/plugins/sample-agent.ts"
|
||||
agent.mode = "primary"
|
||||
agent.system = [
|
||||
"You are the sample plugin agent for this repository.",
|
||||
"Use this agent to verify that local plugin auto-discovery can add agents.",
|
||||
"Keep responses concise and explain which plugin registered you when asked.",
|
||||
].join("\n")
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -159,4 +159,5 @@ const table = sqliteTable("session", {
|
||||
- Keep local Session drains process-local until clustering is implemented. `SessionRunCoordinator` joins explicit same-Session resumes, coalesces prompt wakeups, and allows different Sessions to run concurrently. Advisory wakes drain eligible durable inbox rows only; post-crash continuation recovery requires a separate explicit design before it may retry provider work. A drain has no durable identity or transcript boundary.
|
||||
- Keep delivery vocabulary explicit. Prompts steer by default and promote at the next safe provider-turn boundary while the current drain requires continuation. An explicit `queue` input remains pending until the Session would otherwise become idle; promote one queued input at that boundary, then reevaluate continuation before promoting another. Promoting any new user input resets the selected agent's provider-turn allowance; a batch of steers resets it once.
|
||||
- Keep EventV2 replay owner claims separate from clustered Session execution ownership.
|
||||
- Keep the System Context algebra, registry, and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Epoch persistence Session-owned.
|
||||
- Keep the System Context algebra and built-ins in `src/system-context`; keep Context Source producers with their observed domains, and keep Session History selection plus Context Checkpoint persistence Session-owned. The runner composes all context producers explicitly in `loadSystemContext`; there is no context registry.
|
||||
- The durable Applied record is what the model was last told, per source. Reconcile narrates drift as chronological System updates and never rewrites the baseline; only completed compaction rebaselines, and move or committed revert resets the checkpoint. Unavailable sources keep the model's prior belief, blocking only a session's first baseline.
|
||||
|
||||
@@ -126,7 +126,6 @@
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
|
||||
@@ -14,6 +14,10 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
|
||||
Flag.withDescription("Run with a private server instead of the background service"),
|
||||
Flag.withDefault(false),
|
||||
),
|
||||
server: Flag.string("server").pipe(
|
||||
Flag.withDescription("Connect to a server URL instead of the background service"),
|
||||
Flag.optional,
|
||||
),
|
||||
continue: Flag.boolean("continue").pipe(
|
||||
Flag.withAlias("c"),
|
||||
Flag.withDescription("Continue the last session"),
|
||||
|
||||
@@ -2,7 +2,9 @@ import { EOL } from "node:os"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Daemon } from "../../services/daemon"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import type { Transport } from "@opencode-ai/client/effect"
|
||||
|
||||
const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
|
||||
|
||||
@@ -17,8 +19,9 @@ type OpenApi = {
|
||||
export default Runtime.handler(
|
||||
Commands.commands.api,
|
||||
Effect.fn("cli.api")(function* (input) {
|
||||
const daemon = yield* Daemon.Service
|
||||
const transport = yield* daemon.transport()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const params = Option.getOrElse(input.param, () => ({}))
|
||||
const request = yield* resolveRequest(transport, input.request, params)
|
||||
const headers = new Headers(transport.headers)
|
||||
@@ -58,7 +61,7 @@ export function rawRequest(input: readonly string[]) {
|
||||
}
|
||||
|
||||
function resolveRequest(
|
||||
transport: { url: string; headers: RequestInit["headers"] },
|
||||
transport: Transport,
|
||||
input: readonly string[],
|
||||
params: Record<string, string>,
|
||||
) {
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.debug.commands.agents,
|
||||
Effect.fn("cli.debug.agents")(function* () {
|
||||
const daemon = yield* Daemon.Service
|
||||
const client = yield* daemon.client()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } }))
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Effect, Option } from "effect"
|
||||
import { Daemon } from "../../services/daemon"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import type { Transport } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import { Standalone } from "../../services/standalone"
|
||||
import { Updater } from "../../services/updater"
|
||||
|
||||
@@ -11,18 +14,37 @@ export default Runtime.handler(Commands, (input) =>
|
||||
if (directory !== undefined) process.chdir(directory)
|
||||
const updater = yield* Updater.Service
|
||||
yield* updater.check().pipe(Effect.forkScoped)
|
||||
const daemon = yield* Daemon.Service
|
||||
const transport = yield* (input.standalone ? Standalone.transport() : daemon.transport())
|
||||
const server = Option.getOrUndefined(input.server)
|
||||
if (server !== undefined && input.standalone)
|
||||
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
|
||||
const transport = yield* Effect.gen(function* () {
|
||||
if (server !== undefined) {
|
||||
const password = process.env["OPENCODE_SERVER_PASSWORD"]
|
||||
return {
|
||||
url: server,
|
||||
headers: password ? { authorization: "Basic " + btoa("opencode:" + password) } : undefined,
|
||||
} satisfies Transport
|
||||
}
|
||||
if (input.standalone) return yield* Standalone.transport()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
return found ?? (yield* Service.start(options))
|
||||
})
|
||||
const { runTui } = yield* Effect.promise(() => import("../../tui"))
|
||||
yield* runTui(
|
||||
transport,
|
||||
{ continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
|
||||
input.standalone
|
||||
? undefined
|
||||
: async () => {
|
||||
await Effect.runPromise(daemon.stop())
|
||||
return Effect.runPromise(daemon.transport())
|
||||
},
|
||||
)
|
||||
// The TUI re-runs discover whenever its event stream drops. For an explicit
|
||||
// --server or a standalone child the transport is fixed, so reconnects
|
||||
// retry the same address; for the managed service discovery re-reads the
|
||||
// registration and may start a replacement.
|
||||
const serviceOptions = server === undefined && !input.standalone ? yield* ServiceConfig.options() : undefined
|
||||
const discover = serviceOptions
|
||||
? () =>
|
||||
Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const found = yield* Service.discover(serviceOptions)
|
||||
return found ?? (yield* Service.start(serviceOptions))
|
||||
}).pipe(Effect.provide(NodeFileSystem.layer)),
|
||||
)
|
||||
: () => Promise.resolve(transport)
|
||||
yield* runTui(transport, { continue: input.continue, sessionID: Option.getOrUndefined(input.session) }, discover)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { EOL } from "node:os"
|
||||
import { Effect } from "effect"
|
||||
import type { IntegrationAttemptStatus, IntegrationOAuthMethod, OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import {
|
||||
createOpencodeClient,
|
||||
type IntegrationAttemptStatus,
|
||||
type IntegrationOAuthMethod,
|
||||
type OpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
import { resolveIntegration } from "./resolve"
|
||||
|
||||
const location = { directory: process.cwd() }
|
||||
@@ -11,8 +17,10 @@ const location = { directory: process.cwd() }
|
||||
export default Runtime.handler(
|
||||
Commands.commands.mcp.commands.auth,
|
||||
Effect.fn("cli.mcp.auth")(function* (input) {
|
||||
const daemon = yield* Daemon.Service
|
||||
const client = yield* daemon.client()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
|
||||
const integration = yield* resolveIntegration(client, input.name, location)
|
||||
if (!integration)
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { EOL } from "node:os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import type { McpServer } from "@opencode-ai/sdk/v2/client"
|
||||
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.mcp.commands.list,
|
||||
Effect.fn("cli.mcp.list")(function* () {
|
||||
const daemon = yield* Daemon.Service
|
||||
const client = yield* daemon.client()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
const response = yield* Effect.promise(() => client.v2.mcp.list({ location: { directory: process.cwd() } }))
|
||||
const servers = (response.data?.data ?? []).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
if (servers.length === 0) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { EOL } from "node:os"
|
||||
import { Effect } from "effect"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
import { resolveIntegration } from "./resolve"
|
||||
|
||||
const location = { directory: process.cwd() }
|
||||
@@ -10,8 +12,10 @@ const location = { directory: process.cwd() }
|
||||
export default Runtime.handler(
|
||||
Commands.commands.mcp.commands.logout,
|
||||
Effect.fn("cli.mcp.logout")(function* (input) {
|
||||
const daemon = yield* Daemon.Service
|
||||
const client = yield* daemon.client()
|
||||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const transport = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
||||
|
||||
const integration = yield* resolveIntegration(client, input.name, location)
|
||||
if (!integration) {
|
||||
|
||||
@@ -4,18 +4,20 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Context, Layer, Option, Schedule } from "effect"
|
||||
import { Context, FileSystem, Layer, Option, Schedule, Schema } from "effect"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { createServer } from "node:http"
|
||||
import { createRoutes } from "@opencode-ai/server/routes"
|
||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { Commands } from "../commands"
|
||||
import { Runtime } from "../../framework/runtime"
|
||||
import { Daemon } from "../../services/daemon"
|
||||
import { ServiceConfig } from "../../services/service-config"
|
||||
import { Updater } from "../../services/updater"
|
||||
import { randomBytes } from "crypto"
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import path from "path"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.serve,
|
||||
@@ -23,12 +25,11 @@ export default Runtime.handler(
|
||||
if (input.service) yield* Effect.sync(() => process.chdir(Global.Path.home))
|
||||
return yield* Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const daemon = yield* Daemon.Service
|
||||
const standalonePassword = process.env.OPENCODE_SERVER_PASSWORD
|
||||
if (input.stdio) delete process.env.OPENCODE_SERVER_PASSWORD
|
||||
const config = input.service ? yield* daemon.config() : {}
|
||||
const config = input.service ? yield* ServiceConfig.read() : {}
|
||||
const password = input.service
|
||||
? yield* daemon.password()
|
||||
? yield* ServiceConfig.password()
|
||||
: standalonePassword || randomBytes(32).toString("base64url")
|
||||
if (!password) return yield* Effect.fail(new Error("Missing server password"))
|
||||
const hostname = Option.getOrUndefined(input.hostname) ?? config.hostname ?? "127.0.0.1"
|
||||
@@ -44,7 +45,7 @@ export default Runtime.handler(
|
||||
headers: ServerAuth.headers({ password }),
|
||||
}).v2.health.get({}),
|
||||
)
|
||||
if (input.service) yield* daemon.register(address)
|
||||
if (input.service) yield* register(address)
|
||||
const url = HttpServer.formatAddress(address)
|
||||
console.log(input.stdio ? JSON.stringify({ url }) : `server listening on ${url}`)
|
||||
if (!input.service && !input.stdio && !standalonePassword) console.log(`server password ${password}`)
|
||||
@@ -56,6 +57,56 @@ export default Runtime.handler(
|
||||
}),
|
||||
)
|
||||
|
||||
// Server-side half of the registration protocol. The registration embeds the
|
||||
// password so the file alone is enough for any client to discover and
|
||||
// authenticate. The file arbitrates ownership after concurrent starts; it is
|
||||
// not a startup lock: the atomic rename elects the latest writer, the watcher
|
||||
// self-evicts losers, and the finalizer id-guard keeps an exiting server from
|
||||
// deleting its successor's registration.
|
||||
const RegistrationId = Schema.Struct({ id: Schema.optional(Schema.String) })
|
||||
const decodeRegistrationId = Schema.decodeUnknownEffect(Schema.fromJsonString(RegistrationId))
|
||||
|
||||
const register = Effect.fnUntraced(function* (address: HttpServer.Address) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const { file } = yield* ServiceConfig.options()
|
||||
const id = randomUUID()
|
||||
const secret = yield* ServiceConfig.password()
|
||||
const temp = file + "." + id + ".tmp"
|
||||
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
temp,
|
||||
JSON.stringify({
|
||||
id,
|
||||
version: InstallationVersion,
|
||||
url: HttpServer.formatAddress(address),
|
||||
pid: process.pid,
|
||||
password: secret,
|
||||
}),
|
||||
{ mode: 0o600 },
|
||||
)
|
||||
yield* fs.rename(temp, file)
|
||||
const currentID = fs.readFileString(file).pipe(
|
||||
Effect.flatMap(decodeRegistrationId),
|
||||
Effect.map((info) => info.id),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
yield* currentID.pipe(
|
||||
Effect.flatMap((current) =>
|
||||
current === id
|
||||
? Effect.void
|
||||
: Effect.try({ try: () => process.kill(process.pid, "SIGTERM"), catch: (cause) => cause }).pipe(Effect.ignore),
|
||||
),
|
||||
Effect.repeat(Schedule.spaced("10 seconds")),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
currentID.pipe(
|
||||
Effect.flatMap((current) => (current === id ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
function waitForStdinClose() {
|
||||
return Effect.callback<void>((resume) => {
|
||||
const close = () => resume(Effect.void)
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Option } from "effect"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.get,
|
||||
Effect.fn("cli.service.get")(function* (input) {
|
||||
const daemon = yield* Daemon.Service
|
||||
process.stdout.write((yield* daemon.get(Option.getOrUndefined(input.key))) + EOL)
|
||||
process.stdout.write((yield* ServiceConfig.get(Option.getOrUndefined(input.key))) + EOL)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.restart,
|
||||
Effect.fn("cli.service.restart")(function* () {
|
||||
const daemon = yield* Daemon.Service
|
||||
yield* daemon.stop()
|
||||
process.stdout.write((yield* daemon.start()) + EOL)
|
||||
const options = yield* ServiceConfig.options()
|
||||
yield* Service.stop(options)
|
||||
const transport = yield* Service.start(options)
|
||||
process.stdout.write(transport.url + EOL)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.set,
|
||||
Effect.fn("cli.service.set")(function* (input) {
|
||||
yield* (yield* Daemon.Service).set(input.key, input.value)
|
||||
yield* ServiceConfig.set(input.key, input.value)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.start,
|
||||
Effect.fn("cli.service.start")(function* () {
|
||||
process.stdout.write((yield* (yield* Daemon.Service).start()) + EOL)
|
||||
const transport = yield* Service.start(yield* ServiceConfig.options())
|
||||
process.stdout.write(transport.url + EOL)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { EOL } from "os"
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.status,
|
||||
Effect.fn("cli.service.status")(function* () {
|
||||
const url = yield* (yield* Daemon.Service).status()
|
||||
process.stdout.write((url ? url : "stopped") + EOL)
|
||||
const found = yield* Service.discover(yield* ServiceConfig.options())
|
||||
process.stdout.write((found ? found.url : "stopped") + EOL)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.stop,
|
||||
Effect.fn("cli.service.stop")(function* () {
|
||||
yield* (yield* Daemon.Service).stop()
|
||||
yield* Service.stop(yield* ServiceConfig.options())
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Daemon } from "../../../services/daemon"
|
||||
import { ServiceConfig } from "../../../services/service-config"
|
||||
|
||||
export default Runtime.handler(
|
||||
Commands.commands.service.commands.unset,
|
||||
Effect.fn("cli.service.unset")(function* (input) {
|
||||
yield* (yield* Daemon.Service).unset(input.key)
|
||||
yield* ServiceConfig.unset(input.key)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as Effect from "effect/Effect"
|
||||
import * as Command from "effect/unstable/cli/Command"
|
||||
import { Spec } from "./spec"
|
||||
import { Daemon } from "../services/daemon"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Updater } from "../services/updater"
|
||||
import { Scope } from "effect"
|
||||
import { FileSystem, Scope } from "effect"
|
||||
|
||||
export type Input<Value> =
|
||||
Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
|
||||
@@ -12,11 +12,11 @@ export type Input<Value> =
|
||||
? Input
|
||||
: never
|
||||
|
||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service | Updater.Service | Scope.Scope>
|
||||
type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
type Loader<Node extends Spec.Any> = () => Promise<{
|
||||
default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service | Updater.Service | Scope.Scope>
|
||||
default: (input: Input<Node>) => Effect.Effect<void, any, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
}>
|
||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service | Updater.Service | Scope.Scope>
|
||||
type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, FileSystem.FileSystem | Global.Service | Updater.Service | Scope.Scope>
|
||||
|
||||
export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
|
||||
? Loader<Node>
|
||||
|
||||
@@ -7,7 +7,6 @@ import * as Effect from "effect/Effect"
|
||||
import { Layer, Logger, References } from "effect"
|
||||
import { Commands } from "./commands/commands"
|
||||
import { Runtime } from "./framework/runtime"
|
||||
import { Daemon } from "./services/daemon"
|
||||
import { Logging } from "@opencode-ai/core/observability/logging"
|
||||
import { Updater } from "./services/updater"
|
||||
import { InstallationChannel, InstallationVersion, InstallationLocal } from "@opencode-ai/core/installation/version"
|
||||
@@ -50,7 +49,6 @@ const Handlers = Runtime.handlers(Commands, {
|
||||
Effect.logInfo("cli starting", { version: InstallationVersion, channel: InstallationChannel, local: InstallationLocal }).pipe(
|
||||
Effect.flatMap(() => Runtime.run(Commands, Handlers, { version: InstallationVersion })),
|
||||
Effect.annotateLogs({ role: "cli" }),
|
||||
Effect.provide(Daemon.layer),
|
||||
Effect.provide(Updater.layer),
|
||||
Effect.provide(AppNodeBuilder.build(LayerNode.group([Global.node, AppProcess.node]))),
|
||||
Effect.provide(LoggingLayer),
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||
import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import { spawn } from "node:child_process"
|
||||
import path from "path"
|
||||
|
||||
export interface Interface {
|
||||
readonly client: () => Effect.Effect<ReturnType<typeof createOpencodeClient>, unknown>
|
||||
readonly transport: () => Effect.Effect<{ url: string; headers: RequestInit["headers"] }, unknown>
|
||||
readonly start: () => Effect.Effect<string, Error>
|
||||
readonly status: () => Effect.Effect<string | undefined>
|
||||
readonly stop: () => Effect.Effect<void, unknown>
|
||||
readonly password: (value?: string) => Effect.Effect<string, unknown>
|
||||
readonly config: () => Effect.Effect<ServiceConfig, unknown>
|
||||
readonly get: (key?: string) => Effect.Effect<string, unknown>
|
||||
readonly set: (key: string, value: string) => Effect.Effect<void, unknown>
|
||||
readonly unset: (key: string) => Effect.Effect<void, unknown>
|
||||
readonly register: (address: HttpServer.Address) => Effect.Effect<void, unknown, Scope.Scope>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Daemon") {}
|
||||
|
||||
const Registration = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
version: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
pid: Schema.Int.check(Schema.isGreaterThan(0)),
|
||||
})
|
||||
type Registration = typeof Registration.Type
|
||||
|
||||
const ServiceConfig = Schema.Struct({
|
||||
hostname: Schema.optional(Schema.String),
|
||||
port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))),
|
||||
password: Schema.optional(Schema.String),
|
||||
autostart: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
export type ServiceConfig = typeof ServiceConfig.Type
|
||||
|
||||
const serviceConfigKeys = ["hostname", "port", "password", "autostart"] as const
|
||||
type ServiceConfigKey = (typeof serviceConfigKeys)[number]
|
||||
|
||||
function serviceConfigKey(key: string): ServiceConfigKey {
|
||||
if (serviceConfigKeys.includes(key as ServiceConfigKey)) return key as ServiceConfigKey
|
||||
throw new Error(`Unknown service config key: ${key}`)
|
||||
}
|
||||
|
||||
function sameRegistration(left: Registration, right: Registration) {
|
||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const global = yield* Global.Service
|
||||
const directory = global.state
|
||||
const filename = InstallationChannel === "local" ? "service-local.json" : "service.json"
|
||||
const file = path.join(directory, filename)
|
||||
const configFile = path.join(global.config, filename)
|
||||
const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
||||
const decodeServiceConfig = Schema.decodeUnknownEffect(Schema.fromJsonString(ServiceConfig))
|
||||
|
||||
const config = Effect.fn("cli.daemon.config")(function* () {
|
||||
return yield* fs.readFileString(configFile).pipe(
|
||||
Effect.flatMap(decodeServiceConfig),
|
||||
Effect.catch(() => Effect.succeed({} as ServiceConfig)),
|
||||
)
|
||||
})
|
||||
|
||||
const writeConfig = Effect.fn("cli.daemon.writeConfig")(function* (value: ServiceConfig) {
|
||||
const temp = configFile + ".tmp"
|
||||
yield* fs.makeDirectory(path.dirname(configFile), { recursive: true })
|
||||
yield* fs.writeFileString(temp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 })
|
||||
yield* fs.rename(temp, configFile)
|
||||
})
|
||||
|
||||
const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
|
||||
const existing = yield* config()
|
||||
if (value === undefined && existing.password) return existing.password
|
||||
const next = value ?? randomBytes(32).toString("base64url")
|
||||
|
||||
// Keep one private credential across server restarts so discovered clients
|
||||
// can reconnect without exposing a password flag or environment variable.
|
||||
yield* writeConfig({ ...existing, password: next })
|
||||
return next
|
||||
})
|
||||
|
||||
const get = Effect.fn("cli.daemon.get")(function* (key?: string) {
|
||||
if (key === undefined) {
|
||||
const { password: _password, ...safe } = yield* config()
|
||||
return JSON.stringify(safe, null, 2)
|
||||
}
|
||||
switch (serviceConfigKey(key)) {
|
||||
case "hostname": {
|
||||
return (yield* config()).hostname ?? ""
|
||||
}
|
||||
case "port": {
|
||||
const port = (yield* config()).port
|
||||
return port === undefined ? "" : String(port)
|
||||
}
|
||||
case "password": {
|
||||
return yield* password()
|
||||
}
|
||||
case "autostart": {
|
||||
const autostart = (yield* config()).autostart
|
||||
return autostart === undefined ? "" : String(autostart)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const set = Effect.fn("cli.daemon.set")(function* (key: string, value: string) {
|
||||
switch (serviceConfigKey(key)) {
|
||||
case "hostname": {
|
||||
yield* stop()
|
||||
yield* writeConfig({ ...(yield* config()), hostname: value })
|
||||
return
|
||||
}
|
||||
case "port": {
|
||||
const port = Number(value)
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("Port must be between 1 and 65535")
|
||||
yield* stop()
|
||||
yield* writeConfig({ ...(yield* config()), port })
|
||||
return
|
||||
}
|
||||
case "password": {
|
||||
yield* stop()
|
||||
yield* password(value)
|
||||
return
|
||||
}
|
||||
case "autostart": {
|
||||
if (value !== "true" && value !== "false") throw new Error("Autostart must be true or false")
|
||||
yield* writeConfig({ ...(yield* config()), autostart: value === "true" })
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const unset = Effect.fn("cli.daemon.unset")(function* (key: string) {
|
||||
switch (serviceConfigKey(key)) {
|
||||
case "hostname": {
|
||||
yield* stop()
|
||||
const { hostname: _hostname, ...next } = yield* config()
|
||||
yield* writeConfig(next)
|
||||
return
|
||||
}
|
||||
case "port": {
|
||||
yield* stop()
|
||||
const { port: _port, ...next } = yield* config()
|
||||
yield* writeConfig(next)
|
||||
return
|
||||
}
|
||||
case "password": {
|
||||
yield* stop()
|
||||
const { password: _password, ...next } = yield* config()
|
||||
yield* writeConfig(next)
|
||||
return
|
||||
}
|
||||
case "autostart": {
|
||||
const { autostart: _autostart, ...next } = yield* config()
|
||||
yield* writeConfig(next)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const registration = Effect.fnUntraced(function* () {
|
||||
return yield* fs.readFileString(file).pipe(Effect.flatMap(decodeRegistration))
|
||||
})
|
||||
|
||||
const createClient = Effect.fnUntraced(function* (url: string) {
|
||||
return createOpencodeClient({ baseUrl: url, headers: ServerAuth.headers({ password: yield* password() }) })
|
||||
})
|
||||
|
||||
const healthy = Effect.fnUntraced(function* () {
|
||||
const info = yield* registration()
|
||||
const client = yield* createClient(info.url)
|
||||
const response = yield* Effect.tryPromise(() => client.v2.health.get({ signal: AbortSignal.timeout(2_000) }))
|
||||
if (response.data?.healthy === true) return info
|
||||
return yield* Effect.fail(new Error("Registered server is not healthy"))
|
||||
})
|
||||
|
||||
const remoteTransport = Effect.fn("cli.daemon.remoteTransport")(function* (input: ServiceConfig) {
|
||||
const url = serviceURL(input)
|
||||
const headers = ServerAuth.headers({ password: input.password })
|
||||
const response = yield* Effect.tryPromise(() =>
|
||||
createOpencodeClient({ baseUrl: url, headers }).v2.health.get({ signal: AbortSignal.timeout(2_000) }),
|
||||
)
|
||||
if (response.data?.healthy === true) return { url, headers }
|
||||
return yield* Effect.fail(new Error(`Server is not healthy: ${url}`))
|
||||
})
|
||||
|
||||
const compatible = Effect.fnUntraced(function* () {
|
||||
const info = yield* healthy()
|
||||
if (info.version === InstallationVersion) return info
|
||||
return yield* Effect.fail(new Error("Registered server version does not match the client"))
|
||||
})
|
||||
|
||||
const signal = (pid: number, signal: NodeJS.Signals) =>
|
||||
Effect.try({ try: () => process.kill(pid, signal), catch: (cause) => cause }).pipe(Effect.ignore)
|
||||
|
||||
const awaitStopped = Effect.fnUntraced(function* (pid: number) {
|
||||
const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe(
|
||||
Effect.orElseSucceed(() => false),
|
||||
)
|
||||
if (!running) return true
|
||||
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
|
||||
})
|
||||
|
||||
const stopProcess = Effect.fnUntraced(function* (info: Registration) {
|
||||
const current = yield* healthy().pipe(Effect.option)
|
||||
if (Option.isNone(current) || !sameRegistration(current.value, info)) return
|
||||
|
||||
yield* signal(info.pid, "SIGTERM")
|
||||
const stopped = yield* awaitStopped(info.pid).pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
Effect.option,
|
||||
)
|
||||
if (Option.isSome(stopped)) return
|
||||
|
||||
const latest = yield* healthy().pipe(Effect.option)
|
||||
if (Option.isNone(latest) || !sameRegistration(latest.value, info)) return
|
||||
yield* signal(info.pid, "SIGKILL")
|
||||
yield* awaitStopped(info.pid).pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
)
|
||||
})
|
||||
|
||||
const start = Effect.fn("cli.daemon.start")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
const found = Option.getOrUndefined(existing)
|
||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||
if (found?.version === InstallationVersion) return found.url
|
||||
if (found) yield* stopProcess(found).pipe(Effect.ignore)
|
||||
|
||||
const entrypoint = compiled ? undefined : process.argv[1]
|
||||
if (!compiled && entrypoint === undefined)
|
||||
return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
||||
yield* Effect.try({
|
||||
try: () => {
|
||||
spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--service"], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
}).unref()
|
||||
},
|
||||
catch: (cause) => new Error("Failed to start server", { cause }),
|
||||
})
|
||||
|
||||
return yield* compatible().pipe(
|
||||
Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
|
||||
Effect.map((info) => info.url),
|
||||
Effect.mapError(() => new Error("Failed to start server")),
|
||||
)
|
||||
})
|
||||
|
||||
const transport = Effect.fn("cli.daemon.transport")(function* () {
|
||||
const current = yield* config()
|
||||
if (current.autostart === false) return yield* remoteTransport(current)
|
||||
return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) }
|
||||
})
|
||||
|
||||
const client = Effect.fn("cli.daemon.client")(function* () {
|
||||
const connection = yield* transport()
|
||||
return createOpencodeClient({ baseUrl: connection.url, headers: connection.headers })
|
||||
})
|
||||
|
||||
const status = Effect.fn("cli.daemon.status")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
const found = Option.getOrUndefined(existing)
|
||||
if (found?.version === InstallationVersion) return found.url
|
||||
if (found) return undefined
|
||||
yield* fs.remove(file).pipe(Effect.ignore)
|
||||
return undefined
|
||||
})
|
||||
|
||||
const stop = Effect.fn("cli.daemon.stop")(function* () {
|
||||
const existing = yield* healthy().pipe(Effect.option)
|
||||
// A stale registration may point at a PID that has since been reused by
|
||||
// another process. Only signal the PID after authenticating the server.
|
||||
if (Option.isNone(existing)) return yield* fs.remove(file).pipe(Effect.ignore)
|
||||
yield* stopProcess(existing.value)
|
||||
yield* fs.remove(file).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
const register = Effect.fn("cli.daemon.register")(function* (address: HttpServer.Address) {
|
||||
const id = randomUUID()
|
||||
const temp = file + "." + id + ".tmp"
|
||||
yield* fs.makeDirectory(directory, { recursive: true })
|
||||
yield* fs.writeFileString(
|
||||
temp,
|
||||
JSON.stringify({ id, version: InstallationVersion, url: HttpServer.formatAddress(address), pid: process.pid }),
|
||||
{ mode: 0o600 },
|
||||
)
|
||||
yield* fs.rename(temp, file)
|
||||
yield* registration().pipe(
|
||||
Effect.flatMap((info) => (info.id === id ? Effect.void : signal(process.pid, "SIGTERM"))),
|
||||
Effect.catch(() => signal(process.pid, "SIGTERM")),
|
||||
Effect.repeat(Schedule.spaced("10 seconds")),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* Effect.addFinalizer(() =>
|
||||
registration().pipe(
|
||||
Effect.flatMap((info) => (info.id === id ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({ client, transport, start, status, stop, password, config, get, set, unset, register })
|
||||
}),
|
||||
)
|
||||
|
||||
function serviceURL(config: ServiceConfig) {
|
||||
const hostname = config.hostname ?? "127.0.0.1"
|
||||
const result = new URL(`http://${hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname}`)
|
||||
result.port = String(config.port ?? 4096)
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
export * as Daemon from "./daemon"
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
import { Effect, FileSystem, Schema } from "effect"
|
||||
import { randomBytes } from "crypto"
|
||||
import path from "path"
|
||||
|
||||
// The CLI's service configuration file, plus the ServiceOptions binding that
|
||||
// points the client package's service operations at this CLI: which
|
||||
// registration file (by channel), which version, and how to spawn opencode.
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
hostname: Schema.optional(Schema.String),
|
||||
port: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535))),
|
||||
password: Schema.optional(Schema.String),
|
||||
})
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
const keys = ["hostname", "port", "password"] as const
|
||||
type Key = (typeof keys)[number]
|
||||
|
||||
const decodeInfo = Schema.decodeUnknownEffect(Schema.fromJsonString(Info))
|
||||
|
||||
function configKey(key: string): Key {
|
||||
if (keys.includes(key as Key)) return key as Key
|
||||
throw new Error(`Unknown service config key: ${key}`)
|
||||
}
|
||||
|
||||
const env = Effect.gen(function* () {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const global = yield* Global.Service
|
||||
const filename = InstallationChannel === "local" ? "service-local.json" : "service.json"
|
||||
return {
|
||||
fs,
|
||||
file: path.join(global.state, filename),
|
||||
configFile: path.join(global.config, filename),
|
||||
}
|
||||
})
|
||||
|
||||
export const options = Effect.fnUntraced(function* () {
|
||||
const { file } = yield* env
|
||||
const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
|
||||
const entrypoint = compiled ? undefined : process.argv[1]
|
||||
if (!compiled && entrypoint === undefined) return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
|
||||
return {
|
||||
file,
|
||||
version: InstallationVersion,
|
||||
command: [process.execPath, ...(entrypoint ? [entrypoint] : []), "serve", "--service"],
|
||||
}
|
||||
})
|
||||
|
||||
export const read = Effect.fn("cli.service-config.read")(function* () {
|
||||
const { fs, configFile } = yield* env
|
||||
return yield* fs.readFileString(configFile).pipe(
|
||||
Effect.flatMap(decodeInfo),
|
||||
Effect.catch(() => Effect.succeed({} as Info)),
|
||||
)
|
||||
})
|
||||
|
||||
const write = Effect.fn("cli.service-config.write")(function* (value: Info) {
|
||||
const { fs, configFile } = yield* env
|
||||
const temp = configFile + ".tmp"
|
||||
yield* fs.makeDirectory(path.dirname(configFile), { recursive: true })
|
||||
yield* fs.writeFileString(temp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 })
|
||||
yield* fs.rename(temp, configFile)
|
||||
})
|
||||
|
||||
export const password = Effect.fn("cli.service-config.password")(function* (value?: string) {
|
||||
const existing = yield* read()
|
||||
if (value === undefined && existing.password) return existing.password
|
||||
const next = value ?? randomBytes(32).toString("base64url")
|
||||
|
||||
// Keep one private credential across server restarts so discovered clients
|
||||
// can reconnect without exposing a password flag or environment variable.
|
||||
yield* write({ ...existing, password: next })
|
||||
return next
|
||||
})
|
||||
|
||||
export const get = Effect.fn("cli.service-config.get")(function* (key?: string) {
|
||||
if (key === undefined) {
|
||||
const { password: _password, ...safe } = yield* read()
|
||||
return JSON.stringify(safe, null, 2)
|
||||
}
|
||||
switch (configKey(key)) {
|
||||
case "hostname": {
|
||||
return (yield* read()).hostname ?? ""
|
||||
}
|
||||
case "port": {
|
||||
const port = (yield* read()).port
|
||||
return port === undefined ? "" : String(port)
|
||||
}
|
||||
case "password": {
|
||||
return yield* password()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const set = Effect.fn("cli.service-config.set")(function* (key: string, value: string) {
|
||||
switch (configKey(key)) {
|
||||
case "hostname": {
|
||||
yield* Service.stop(yield* options())
|
||||
yield* write({ ...(yield* read()), hostname: value })
|
||||
return
|
||||
}
|
||||
case "port": {
|
||||
const port = Number(value)
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error("Port must be between 1 and 65535")
|
||||
yield* Service.stop(yield* options())
|
||||
yield* write({ ...(yield* read()), port })
|
||||
return
|
||||
}
|
||||
case "password": {
|
||||
yield* Service.stop(yield* options())
|
||||
yield* password(value)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const unset = Effect.fn("cli.service-config.unset")(function* (key: string) {
|
||||
switch (configKey(key)) {
|
||||
case "hostname": {
|
||||
yield* Service.stop(yield* options())
|
||||
const { hostname: _hostname, ...next } = yield* read()
|
||||
yield* write(next)
|
||||
return
|
||||
}
|
||||
case "port": {
|
||||
yield* Service.stop(yield* options())
|
||||
const { port: _port, ...next } = yield* read()
|
||||
yield* write(next)
|
||||
return
|
||||
}
|
||||
case "password": {
|
||||
yield* Service.stop(yield* options())
|
||||
const { password: _password, ...next } = yield* read()
|
||||
yield* write(next)
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export * as ServiceConfig from "./service-config"
|
||||
@@ -4,13 +4,12 @@ import { Effect } from "effect"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import type { Transport } from "@opencode-ai/client/effect"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import type { Args } from "@opencode-ai/tui/context/args"
|
||||
|
||||
type Transport = { url: string; headers: RequestInit["headers"] }
|
||||
|
||||
export function runTui(transport: Transport, args: Args, reload?: () => Promise<Transport>) {
|
||||
export function runTui(transport: Transport, args: Args, discover?: () => Promise<Transport>) {
|
||||
const config = TuiConfig.resolve({}, { terminalSuspend: false })
|
||||
let disposeSlots: (() => void) | undefined
|
||||
return Effect.gen(function* () {
|
||||
@@ -25,9 +24,9 @@ export function runTui(transport: Transport, args: Args, reload?: () => Promise<
|
||||
return yield* run({
|
||||
client: createOpencodeClient({ ...options, directory }),
|
||||
api,
|
||||
reload: reload
|
||||
discover: discover
|
||||
? async () => {
|
||||
const next = await reload()
|
||||
const next = await discover()
|
||||
return {
|
||||
client: createOpencodeClient({ baseUrl: next.url, headers: next.headers, directory }),
|
||||
api: OpenCode.make({ baseUrl: next.url, headers: next.headers }),
|
||||
|
||||
@@ -5,23 +5,19 @@ import { Effect } from "effect"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { Daemon } from "../src/services/daemon"
|
||||
import { ServiceConfig } from "../src/services/service-config"
|
||||
|
||||
test("local channel stores service config with the local service filename", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-daemon-"))
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
|
||||
try {
|
||||
await Effect.runPromise(
|
||||
Effect.gen(function* () {
|
||||
const daemon = yield* Daemon.Service
|
||||
yield* daemon.set("autostart", "false")
|
||||
}).pipe(
|
||||
Effect.provide(Daemon.layer),
|
||||
ServiceConfig.set("hostname", "127.0.0.2").pipe(
|
||||
Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })),
|
||||
Effect.provide(NodeFileSystem.layer),
|
||||
),
|
||||
)
|
||||
expect(await Bun.file(path.join(root, "config", "service-local.json")).json()).toEqual({
|
||||
autostart: false,
|
||||
hostname: "127.0.0.2",
|
||||
})
|
||||
expect(await Bun.file(path.join(root, "config", "service.json")).exists()).toBe(false)
|
||||
} finally {
|
||||
@@ -5,12 +5,12 @@
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effect": "./src/effect.ts"
|
||||
"./promise": "./src/promise/index.ts",
|
||||
"./effect": "./src/effect/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "bun run script/build.ts",
|
||||
"check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect",
|
||||
"check:generated": "bun run generate && git diff --exit-code -- src/promise/generated src/effect/generated",
|
||||
"test": "bun test --timeout 5000",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
|
||||
@@ -25,11 +25,11 @@ await Effect.runPromise(
|
||||
},
|
||||
},
|
||||
}),
|
||||
fileURLToPath(new URL("../src/generated", import.meta.url)),
|
||||
fileURLToPath(new URL("../src/promise/generated", import.meta.url)),
|
||||
),
|
||||
write(
|
||||
emitEffectImported(effectContract, { module: "../contract", api: "ClientApi" }),
|
||||
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
|
||||
emitEffectImported(effectContract, { module: "../../contract", api: "ClientApi" }),
|
||||
fileURLToPath(new URL("../src/effect/generated", import.meta.url)),
|
||||
),
|
||||
write(
|
||||
emitEffectShape(effectContract, { module: "@opencode-ai/protocol/client", api: "ClientApi" }),
|
||||
|
||||
+440
-263
@@ -3,7 +3,7 @@ import { Effect, Stream, Schema } from "effect"
|
||||
import { Sse } from "effect/unstable/encoding"
|
||||
import { HttpClientError } from "effect/unstable/http"
|
||||
import { HttpApiClient } from "effect/unstable/httpapi"
|
||||
import { ClientApi } from "../contract"
|
||||
import { ClientApi } from "../../contract"
|
||||
import { ClientError } from "./client-error"
|
||||
|
||||
type RawClient = HttpApiClient.ForApi<typeof ClientApi>
|
||||
@@ -45,6 +45,7 @@ type Endpoint4_0Input = {
|
||||
readonly limit?: Endpoint4_0Request["query"]["limit"]
|
||||
readonly order?: Endpoint4_0Request["query"]["order"]
|
||||
readonly search?: Endpoint4_0Request["query"]["search"]
|
||||
readonly parentID?: Endpoint4_0Request["query"]["parentID"]
|
||||
readonly directory?: Endpoint4_0Request["query"]["directory"]
|
||||
readonly project?: Endpoint4_0Request["query"]["project"]
|
||||
readonly subpath?: Endpoint4_0Request["query"]["subpath"]
|
||||
@@ -57,6 +58,7 @@ const Endpoint4_0 = (raw: RawClient["server.session"]) => (input?: Endpoint4_0In
|
||||
limit: input?.["limit"],
|
||||
order: input?.["order"],
|
||||
search: input?.["search"],
|
||||
parentID: input?.["parentID"],
|
||||
directory: input?.["directory"],
|
||||
project: input?.["project"],
|
||||
subpath: input?.["subpath"],
|
||||
@@ -80,10 +82,7 @@ const Endpoint4_1 = (raw: RawClient["server.session"]) => (input?: Endpoint4_1In
|
||||
)
|
||||
|
||||
const Endpoint4_2 = (raw: RawClient["server.session"]) => () =>
|
||||
raw["session.active"]({}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
raw["session.active"]({}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
|
||||
type Endpoint4_3Input = { readonly sessionID: Endpoint4_3Request["params"]["sessionID"] }
|
||||
@@ -151,36 +150,81 @@ const Endpoint4_8 = (raw: RawClient["server.session"]) => (input: Endpoint4_8Inp
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||
type Endpoint4_9Request = Parameters<RawClient["server.session"]["session.command"]>[0]
|
||||
type Endpoint4_9Input = {
|
||||
readonly sessionID: Endpoint4_9Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_9Request["payload"]["id"]
|
||||
readonly skill: Endpoint4_9Request["payload"]["skill"]
|
||||
readonly command: Endpoint4_9Request["payload"]["command"]
|
||||
readonly arguments?: Endpoint4_9Request["payload"]["arguments"]
|
||||
readonly agent?: Endpoint4_9Request["payload"]["agent"]
|
||||
readonly model?: Endpoint4_9Request["payload"]["model"]
|
||||
readonly files?: Endpoint4_9Request["payload"]["files"]
|
||||
readonly agents?: Endpoint4_9Request["payload"]["agents"]
|
||||
readonly delivery?: Endpoint4_9Request["payload"]["delivery"]
|
||||
readonly resume?: Endpoint4_9Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint4_9 = (raw: RawClient["server.session"]) => (input: Endpoint4_9Input) =>
|
||||
raw["session.command"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
id: input["id"],
|
||||
command: input["command"],
|
||||
arguments: input["arguments"],
|
||||
agent: input["agent"],
|
||||
model: input["model"],
|
||||
files: input["files"],
|
||||
agents: input["agents"],
|
||||
delivery: input["delivery"],
|
||||
resume: input["resume"],
|
||||
},
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.skill"]>[0]
|
||||
type Endpoint4_10Input = {
|
||||
readonly sessionID: Endpoint4_10Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint4_10Request["payload"]["id"]
|
||||
readonly skill: Endpoint4_10Request["payload"]["skill"]
|
||||
readonly resume?: Endpoint4_10Request["payload"]["resume"]
|
||||
}
|
||||
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
||||
raw["session.skill"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { id: input["id"], skill: input["skill"], resume: input["resume"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_10Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint4_10Input = { readonly sessionID: Endpoint4_10Request["params"]["sessionID"] }
|
||||
const Endpoint4_10 = (raw: RawClient["server.session"]) => (input: Endpoint4_10Input) =>
|
||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.synthetic"]>[0]
|
||||
type Endpoint4_11Input = {
|
||||
readonly sessionID: Endpoint4_11Request["params"]["sessionID"]
|
||||
readonly text: Endpoint4_11Request["payload"]["text"]
|
||||
readonly description?: Endpoint4_11Request["payload"]["description"]
|
||||
readonly metadata?: Endpoint4_11Request["payload"]["metadata"]
|
||||
}
|
||||
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
||||
raw["session.synthetic"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
|
||||
type Endpoint4_12Input = { readonly sessionID: Endpoint4_12Request["params"]["sessionID"] }
|
||||
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||
raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_11Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint4_11Input = { readonly sessionID: Endpoint4_11Request["params"]["sessionID"] }
|
||||
const Endpoint4_11 = (raw: RawClient["server.session"]) => (input: Endpoint4_11Input) =>
|
||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
|
||||
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||
raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_12Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint4_12Input = {
|
||||
readonly sessionID: Endpoint4_12Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_12Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint4_12Request["payload"]["files"]
|
||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
|
||||
type Endpoint4_14Input = {
|
||||
readonly sessionID: Endpoint4_14Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_14Request["payload"]["messageID"]
|
||||
readonly files?: Endpoint4_14Request["payload"]["files"]
|
||||
}
|
||||
const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12Input) =>
|
||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||
raw["session.revert.stage"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: { messageID: input["messageID"], files: input["files"] },
|
||||
@@ -189,65 +233,87 @@ const Endpoint4_12 = (raw: RawClient["server.session"]) => (input: Endpoint4_12I
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_13Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint4_13Input = { readonly sessionID: Endpoint4_13Request["params"]["sessionID"] }
|
||||
const Endpoint4_13 = (raw: RawClient["server.session"]) => (input: Endpoint4_13Input) =>
|
||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_14Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint4_14Input = { readonly sessionID: Endpoint4_14Request["params"]["sessionID"] }
|
||||
const Endpoint4_14 = (raw: RawClient["server.session"]) => (input: Endpoint4_14Input) =>
|
||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint4_15Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
|
||||
type Endpoint4_15Input = { readonly sessionID: Endpoint4_15Request["params"]["sessionID"] }
|
||||
const Endpoint4_15 = (raw: RawClient["server.session"]) => (input: Endpoint4_15Input) =>
|
||||
raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
|
||||
type Endpoint4_16Input = { readonly sessionID: Endpoint4_16Request["params"]["sessionID"] }
|
||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||
raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.context"]>[0]
|
||||
type Endpoint4_17Input = { readonly sessionID: Endpoint4_17Request["params"]["sessionID"] }
|
||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||
raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_16Request = Parameters<RawClient["server.session"]["session.history"]>[0]
|
||||
type Endpoint4_16Input = {
|
||||
readonly sessionID: Endpoint4_16Request["params"]["sessionID"]
|
||||
readonly limit?: Endpoint4_16Request["query"]["limit"]
|
||||
readonly after?: Endpoint4_16Request["query"]["after"]
|
||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.context.entry.list"]>[0]
|
||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||
raw["session.context.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.context.entry.put"]>[0]
|
||||
type Endpoint4_19Input = {
|
||||
readonly sessionID: Endpoint4_19Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_19Request["params"]["key"]
|
||||
readonly value: Endpoint4_19Request["payload"]["value"]
|
||||
}
|
||||
const Endpoint4_16 = (raw: RawClient["server.session"]) => (input: Endpoint4_16Input) =>
|
||||
raw["session.history"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
query: { limit: input["limit"], after: input["after"] },
|
||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||
raw["session.context.entry.put"]({
|
||||
params: { sessionID: input["sessionID"], key: input["key"] },
|
||||
payload: { value: input["value"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_17Request = Parameters<RawClient["server.session"]["session.events"]>[0]
|
||||
type Endpoint4_17Input = {
|
||||
readonly sessionID: Endpoint4_17Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint4_17Request["query"]["after"]
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.context.entry.remove"]>[0]
|
||||
type Endpoint4_20Input = {
|
||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||
readonly key: Endpoint4_20Request["params"]["key"]
|
||||
}
|
||||
const Endpoint4_17 = (raw: RawClient["server.session"]) => (input: Endpoint4_17Input) =>
|
||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||
raw["session.context.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint4_21Request = Parameters<RawClient["server.session"]["session.log"]>[0]
|
||||
type Endpoint4_21Input = {
|
||||
readonly sessionID: Endpoint4_21Request["params"]["sessionID"]
|
||||
readonly after?: Endpoint4_21Request["query"]["after"]
|
||||
readonly follow?: Endpoint4_21Request["query"]["follow"]
|
||||
}
|
||||
const Endpoint4_21 = (raw: RawClient["server.session"]) => (input: Endpoint4_21Input) =>
|
||||
Stream.unwrap(
|
||||
raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
|
||||
raw["session.log"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
query: { after: input["after"], follow: input["follow"] },
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
|
||||
),
|
||||
)
|
||||
|
||||
type Endpoint4_18Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint4_18Input = { readonly sessionID: Endpoint4_18Request["params"]["sessionID"] }
|
||||
const Endpoint4_18 = (raw: RawClient["server.session"]) => (input: Endpoint4_18Input) =>
|
||||
type Endpoint4_22Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
|
||||
type Endpoint4_22Input = { readonly sessionID: Endpoint4_22Request["params"]["sessionID"] }
|
||||
const Endpoint4_22 = (raw: RawClient["server.session"]) => (input: Endpoint4_22Input) =>
|
||||
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_19Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
type Endpoint4_19Input = { readonly sessionID: Endpoint4_19Request["params"]["sessionID"] }
|
||||
const Endpoint4_19 = (raw: RawClient["server.session"]) => (input: Endpoint4_19Input) =>
|
||||
type Endpoint4_23Request = Parameters<RawClient["server.session"]["session.background"]>[0]
|
||||
type Endpoint4_23Input = { readonly sessionID: Endpoint4_23Request["params"]["sessionID"] }
|
||||
const Endpoint4_23 = (raw: RawClient["server.session"]) => (input: Endpoint4_23Input) =>
|
||||
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint4_20Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint4_20Input = {
|
||||
readonly sessionID: Endpoint4_20Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_20Request["params"]["messageID"]
|
||||
type Endpoint4_24Request = Parameters<RawClient["server.session"]["session.message"]>[0]
|
||||
type Endpoint4_24Input = {
|
||||
readonly sessionID: Endpoint4_24Request["params"]["sessionID"]
|
||||
readonly messageID: Endpoint4_24Request["params"]["messageID"]
|
||||
}
|
||||
const Endpoint4_20 = (raw: RawClient["server.session"]) => (input: Endpoint4_20Input) =>
|
||||
const Endpoint4_24 = (raw: RawClient["server.session"]) => (input: Endpoint4_24Input) =>
|
||||
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
@@ -263,18 +329,22 @@ const adaptGroup4 = (raw: RawClient["server.session"]) => ({
|
||||
switchModel: Endpoint4_6(raw),
|
||||
rename: Endpoint4_7(raw),
|
||||
prompt: Endpoint4_8(raw),
|
||||
skill: Endpoint4_9(raw),
|
||||
compact: Endpoint4_10(raw),
|
||||
wait: Endpoint4_11(raw),
|
||||
revertStage: Endpoint4_12(raw),
|
||||
revertClear: Endpoint4_13(raw),
|
||||
revertCommit: Endpoint4_14(raw),
|
||||
context: Endpoint4_15(raw),
|
||||
history: Endpoint4_16(raw),
|
||||
events: Endpoint4_17(raw),
|
||||
interrupt: Endpoint4_18(raw),
|
||||
background: Endpoint4_19(raw),
|
||||
message: Endpoint4_20(raw),
|
||||
command: Endpoint4_9(raw),
|
||||
skill: Endpoint4_10(raw),
|
||||
synthetic: Endpoint4_11(raw),
|
||||
compact: Endpoint4_12(raw),
|
||||
wait: Endpoint4_13(raw),
|
||||
revertStage: Endpoint4_14(raw),
|
||||
revertClear: Endpoint4_15(raw),
|
||||
revertCommit: Endpoint4_16(raw),
|
||||
context: Endpoint4_17(raw),
|
||||
listContextEntries: Endpoint4_18(raw),
|
||||
putContextEntry: Endpoint4_19(raw),
|
||||
removeContextEntry: Endpoint4_20(raw),
|
||||
log: Endpoint4_21(raw),
|
||||
interrupt: Endpoint4_22(raw),
|
||||
background: Endpoint4_23(raw),
|
||||
message: Endpoint4_24(raw),
|
||||
})
|
||||
|
||||
type Endpoint5_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
|
||||
@@ -297,7 +367,12 @@ type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["locat
|
||||
const Endpoint6_0 = (raw: RawClient["server.model"]) => (input?: Endpoint6_0Input) =>
|
||||
raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup6 = (raw: RawClient["server.model"]) => ({ list: Endpoint6_0(raw) })
|
||||
type Endpoint6_1Request = Parameters<RawClient["server.model"]["model.default"]>[0]
|
||||
type Endpoint6_1Input = { readonly location?: Endpoint6_1Request["query"]["location"] }
|
||||
const Endpoint6_1 = (raw: RawClient["server.model"]) => (input?: Endpoint6_1Input) =>
|
||||
raw["model.default"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup6 = (raw: RawClient["server.model"]) => ({ list: Endpoint6_0(raw), default: Endpoint6_1(raw) })
|
||||
|
||||
type Endpoint7_0Request = Parameters<RawClient["server.generate"]["generate.text"]>[0]
|
||||
type Endpoint7_0Input = {
|
||||
@@ -477,36 +552,129 @@ const adaptGroup12 = (raw: RawClient["server.project"]) => ({
|
||||
directories: Endpoint12_1(raw),
|
||||
})
|
||||
|
||||
type Endpoint13_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
|
||||
type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
|
||||
const Endpoint13_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint13_0Input) =>
|
||||
const Endpoint13_0 = (raw: RawClient["server.form"]) => (input?: Endpoint13_0Input) =>
|
||||
raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
|
||||
type Endpoint13_1Input = { readonly sessionID: Endpoint13_1Request["params"]["sessionID"] }
|
||||
const Endpoint13_1 = (raw: RawClient["server.form"]) => (input: Endpoint13_1Input) =>
|
||||
raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.create"]>[0]
|
||||
type Endpoint13_2Input = {
|
||||
readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint13_2Request["payload"]["id"]
|
||||
readonly title?: Endpoint13_2Request["payload"]["title"]
|
||||
readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
|
||||
readonly mode: Endpoint13_2Request["payload"]["mode"]
|
||||
readonly fields?: Endpoint13_2Request["payload"]["fields"]
|
||||
readonly url?: Endpoint13_2Request["payload"]["url"]
|
||||
}
|
||||
const Endpoint13_2 = (raw: RawClient["server.form"]) => (input: Endpoint13_2Input) =>
|
||||
raw["session.form.create"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
id: input["id"],
|
||||
title: input["title"],
|
||||
metadata: input["metadata"],
|
||||
mode: input["mode"],
|
||||
fields: input["fields"],
|
||||
url: input["url"],
|
||||
},
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_3Request = Parameters<RawClient["server.form"]["session.form.get"]>[0]
|
||||
type Endpoint13_3Input = {
|
||||
readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
|
||||
readonly formID: Endpoint13_3Request["params"]["formID"]
|
||||
}
|
||||
const Endpoint13_3 = (raw: RawClient["server.form"]) => (input: Endpoint13_3Input) =>
|
||||
raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_4Request = Parameters<RawClient["server.form"]["session.form.state"]>[0]
|
||||
type Endpoint13_4Input = {
|
||||
readonly sessionID: Endpoint13_4Request["params"]["sessionID"]
|
||||
readonly formID: Endpoint13_4Request["params"]["formID"]
|
||||
}
|
||||
const Endpoint13_4 = (raw: RawClient["server.form"]) => (input: Endpoint13_4Input) =>
|
||||
raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_5Request = Parameters<RawClient["server.form"]["session.form.reply"]>[0]
|
||||
type Endpoint13_5Input = {
|
||||
readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
|
||||
readonly formID: Endpoint13_5Request["params"]["formID"]
|
||||
readonly answer: Endpoint13_5Request["payload"]["answer"]
|
||||
}
|
||||
const Endpoint13_5 = (raw: RawClient["server.form"]) => (input: Endpoint13_5Input) =>
|
||||
raw["session.form.reply"]({
|
||||
params: { sessionID: input["sessionID"], formID: input["formID"] },
|
||||
payload: { answer: input["answer"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint13_6Request = Parameters<RawClient["server.form"]["session.form.cancel"]>[0]
|
||||
type Endpoint13_6Input = {
|
||||
readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
|
||||
readonly formID: Endpoint13_6Request["params"]["formID"]
|
||||
}
|
||||
const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Input) =>
|
||||
raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
|
||||
listRequests: Endpoint13_0(raw),
|
||||
list: Endpoint13_1(raw),
|
||||
create: Endpoint13_2(raw),
|
||||
get: Endpoint13_3(raw),
|
||||
state: Endpoint13_4(raw),
|
||||
reply: Endpoint13_5(raw),
|
||||
cancel: Endpoint13_6(raw),
|
||||
})
|
||||
|
||||
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
|
||||
type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
|
||||
const Endpoint14_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_0Input) =>
|
||||
raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint13_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
||||
type Endpoint13_1Input = { readonly projectID?: Endpoint13_1Request["query"]["projectID"] }
|
||||
const Endpoint13_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint13_1Input) =>
|
||||
type Endpoint14_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
|
||||
type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["query"]["projectID"] }
|
||||
const Endpoint14_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_1Input) =>
|
||||
raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||
type Endpoint13_2Input = { readonly id: Endpoint13_2Request["params"]["id"] }
|
||||
const Endpoint13_2 = (raw: RawClient["server.permission"]) => (input: Endpoint13_2Input) =>
|
||||
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
|
||||
type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
|
||||
const Endpoint14_2 = (raw: RawClient["server.permission"]) => (input: Endpoint14_2Input) =>
|
||||
raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint13_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
||||
type Endpoint13_3Input = {
|
||||
readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint13_3Request["payload"]["id"]
|
||||
readonly action: Endpoint13_3Request["payload"]["action"]
|
||||
readonly resources: Endpoint13_3Request["payload"]["resources"]
|
||||
readonly save?: Endpoint13_3Request["payload"]["save"]
|
||||
readonly metadata?: Endpoint13_3Request["payload"]["metadata"]
|
||||
readonly source?: Endpoint13_3Request["payload"]["source"]
|
||||
readonly agent?: Endpoint13_3Request["payload"]["agent"]
|
||||
type Endpoint14_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
|
||||
type Endpoint14_3Input = {
|
||||
readonly sessionID: Endpoint14_3Request["params"]["sessionID"]
|
||||
readonly id?: Endpoint14_3Request["payload"]["id"]
|
||||
readonly action: Endpoint14_3Request["payload"]["action"]
|
||||
readonly resources: Endpoint14_3Request["payload"]["resources"]
|
||||
readonly save?: Endpoint14_3Request["payload"]["save"]
|
||||
readonly metadata?: Endpoint14_3Request["payload"]["metadata"]
|
||||
readonly source?: Endpoint14_3Request["payload"]["source"]
|
||||
readonly agent?: Endpoint14_3Request["payload"]["agent"]
|
||||
}
|
||||
const Endpoint13_3 = (raw: RawClient["server.permission"]) => (input: Endpoint13_3Input) =>
|
||||
const Endpoint14_3 = (raw: RawClient["server.permission"]) => (input: Endpoint14_3Input) =>
|
||||
raw["session.permission.create"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
payload: {
|
||||
@@ -523,87 +691,87 @@ const Endpoint13_3 = (raw: RawClient["server.permission"]) => (input: Endpoint13
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
||||
type Endpoint13_4Input = { readonly sessionID: Endpoint13_4Request["params"]["sessionID"] }
|
||||
const Endpoint13_4 = (raw: RawClient["server.permission"]) => (input: Endpoint13_4Input) =>
|
||||
type Endpoint14_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
|
||||
type Endpoint14_4Input = { readonly sessionID: Endpoint14_4Request["params"]["sessionID"] }
|
||||
const Endpoint14_4 = (raw: RawClient["server.permission"]) => (input: Endpoint14_4Input) =>
|
||||
raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
||||
type Endpoint13_5Input = {
|
||||
readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint13_5Request["params"]["requestID"]
|
||||
type Endpoint14_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
|
||||
type Endpoint14_5Input = {
|
||||
readonly sessionID: Endpoint14_5Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint14_5Request["params"]["requestID"]
|
||||
}
|
||||
const Endpoint13_5 = (raw: RawClient["server.permission"]) => (input: Endpoint13_5Input) =>
|
||||
const Endpoint14_5 = (raw: RawClient["server.permission"]) => (input: Endpoint14_5Input) =>
|
||||
raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint13_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
||||
type Endpoint13_6Input = {
|
||||
readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint13_6Request["params"]["requestID"]
|
||||
readonly reply: Endpoint13_6Request["payload"]["reply"]
|
||||
readonly message?: Endpoint13_6Request["payload"]["message"]
|
||||
type Endpoint14_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
|
||||
type Endpoint14_6Input = {
|
||||
readonly sessionID: Endpoint14_6Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint14_6Request["params"]["requestID"]
|
||||
readonly reply: Endpoint14_6Request["payload"]["reply"]
|
||||
readonly message?: Endpoint14_6Request["payload"]["message"]
|
||||
}
|
||||
const Endpoint13_6 = (raw: RawClient["server.permission"]) => (input: Endpoint13_6Input) =>
|
||||
const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14_6Input) =>
|
||||
raw["session.permission.reply"]({
|
||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
||||
payload: { reply: input["reply"], message: input["message"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup13 = (raw: RawClient["server.permission"]) => ({
|
||||
listRequests: Endpoint13_0(raw),
|
||||
listSaved: Endpoint13_1(raw),
|
||||
removeSaved: Endpoint13_2(raw),
|
||||
create: Endpoint13_3(raw),
|
||||
list: Endpoint13_4(raw),
|
||||
get: Endpoint13_5(raw),
|
||||
reply: Endpoint13_6(raw),
|
||||
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
|
||||
listRequests: Endpoint14_0(raw),
|
||||
listSaved: Endpoint14_1(raw),
|
||||
removeSaved: Endpoint14_2(raw),
|
||||
create: Endpoint14_3(raw),
|
||||
list: Endpoint14_4(raw),
|
||||
get: Endpoint14_5(raw),
|
||||
reply: Endpoint14_6(raw),
|
||||
})
|
||||
|
||||
type Endpoint14_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
||||
type Endpoint14_0Input = {
|
||||
readonly location?: Endpoint14_0Request["query"]["location"]
|
||||
readonly path?: Endpoint14_0Request["query"]["path"]
|
||||
type Endpoint15_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
|
||||
type Endpoint15_0Input = {
|
||||
readonly location?: Endpoint15_0Request["query"]["location"]
|
||||
readonly path?: Endpoint15_0Request["query"]["path"]
|
||||
}
|
||||
const Endpoint14_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint14_0Input) =>
|
||||
const Endpoint15_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint15_0Input) =>
|
||||
raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint14_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
||||
type Endpoint14_1Input = {
|
||||
readonly location?: Endpoint14_1Request["query"]["location"]
|
||||
readonly query: Endpoint14_1Request["query"]["query"]
|
||||
readonly type?: Endpoint14_1Request["query"]["type"]
|
||||
readonly limit?: Endpoint14_1Request["query"]["limit"]
|
||||
type Endpoint15_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
|
||||
type Endpoint15_1Input = {
|
||||
readonly location?: Endpoint15_1Request["query"]["location"]
|
||||
readonly query: Endpoint15_1Request["query"]["query"]
|
||||
readonly type?: Endpoint15_1Request["query"]["type"]
|
||||
readonly limit?: Endpoint15_1Request["query"]["limit"]
|
||||
}
|
||||
const Endpoint14_1 = (raw: RawClient["server.fs"]) => (input: Endpoint14_1Input) =>
|
||||
const Endpoint15_1 = (raw: RawClient["server.fs"]) => (input: Endpoint15_1Input) =>
|
||||
raw["fs.find"]({
|
||||
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup14 = (raw: RawClient["server.fs"]) => ({ list: Endpoint14_0(raw), find: Endpoint14_1(raw) })
|
||||
const adaptGroup15 = (raw: RawClient["server.fs"]) => ({ list: Endpoint15_0(raw), find: Endpoint15_1(raw) })
|
||||
|
||||
type Endpoint15_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
||||
type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
|
||||
const Endpoint15_0 = (raw: RawClient["server.command"]) => (input?: Endpoint15_0Input) =>
|
||||
type Endpoint16_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
|
||||
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
||||
const Endpoint16_0 = (raw: RawClient["server.command"]) => (input?: Endpoint16_0Input) =>
|
||||
raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup15 = (raw: RawClient["server.command"]) => ({ list: Endpoint15_0(raw) })
|
||||
const adaptGroup16 = (raw: RawClient["server.command"]) => ({ list: Endpoint16_0(raw) })
|
||||
|
||||
type Endpoint16_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
||||
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
|
||||
const Endpoint16_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint16_0Input) =>
|
||||
type Endpoint17_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
|
||||
type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
|
||||
const Endpoint17_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint17_0Input) =>
|
||||
raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup16 = (raw: RawClient["server.skill"]) => ({ list: Endpoint16_0(raw) })
|
||||
const adaptGroup17 = (raw: RawClient["server.skill"]) => ({ list: Endpoint17_0(raw) })
|
||||
|
||||
const Endpoint17_0 = (raw: RawClient["server.event"]) => () =>
|
||||
const Endpoint18_0 = (raw: RawClient["server.event"]) => () =>
|
||||
Stream.unwrap(
|
||||
raw["event.subscribe"]({}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
@@ -611,23 +779,31 @@ const Endpoint17_0 = (raw: RawClient["server.event"]) => () =>
|
||||
),
|
||||
)
|
||||
|
||||
const adaptGroup17 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint17_0(raw) })
|
||||
const Endpoint18_1 = (raw: RawClient["server.event"]) => () =>
|
||||
Stream.unwrap(
|
||||
raw["event.changes"]({}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
|
||||
),
|
||||
)
|
||||
|
||||
type Endpoint18_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
||||
type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
|
||||
const Endpoint18_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_0Input) =>
|
||||
const adaptGroup18 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint18_0(raw), changes: Endpoint18_1(raw) })
|
||||
|
||||
type Endpoint19_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
|
||||
type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
|
||||
const Endpoint19_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_0Input) =>
|
||||
raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint18_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
||||
type Endpoint18_1Input = {
|
||||
readonly location?: Endpoint18_1Request["query"]["location"]
|
||||
readonly command?: Endpoint18_1Request["payload"]["command"]
|
||||
readonly args?: Endpoint18_1Request["payload"]["args"]
|
||||
readonly cwd?: Endpoint18_1Request["payload"]["cwd"]
|
||||
readonly title?: Endpoint18_1Request["payload"]["title"]
|
||||
readonly env?: Endpoint18_1Request["payload"]["env"]
|
||||
type Endpoint19_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
|
||||
type Endpoint19_1Input = {
|
||||
readonly location?: Endpoint19_1Request["query"]["location"]
|
||||
readonly command?: Endpoint19_1Request["payload"]["command"]
|
||||
readonly args?: Endpoint19_1Request["payload"]["args"]
|
||||
readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
|
||||
readonly title?: Endpoint19_1Request["payload"]["title"]
|
||||
readonly env?: Endpoint19_1Request["payload"]["env"]
|
||||
}
|
||||
const Endpoint18_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_1Input) =>
|
||||
const Endpoint19_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_1Input) =>
|
||||
raw["pty.create"]({
|
||||
query: { location: input?.["location"] },
|
||||
payload: {
|
||||
@@ -639,201 +815,201 @@ const Endpoint18_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_1Inpu
|
||||
},
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint18_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
||||
type Endpoint18_2Input = {
|
||||
readonly ptyID: Endpoint18_2Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint18_2Request["query"]["location"]
|
||||
type Endpoint19_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
|
||||
type Endpoint19_2Input = {
|
||||
readonly ptyID: Endpoint19_2Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint19_2Request["query"]["location"]
|
||||
}
|
||||
const Endpoint18_2 = (raw: RawClient["server.pty"]) => (input: Endpoint18_2Input) =>
|
||||
const Endpoint19_2 = (raw: RawClient["server.pty"]) => (input: Endpoint19_2Input) =>
|
||||
raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint18_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
||||
type Endpoint18_3Input = {
|
||||
readonly ptyID: Endpoint18_3Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint18_3Request["query"]["location"]
|
||||
readonly title?: Endpoint18_3Request["payload"]["title"]
|
||||
readonly size?: Endpoint18_3Request["payload"]["size"]
|
||||
type Endpoint19_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
|
||||
type Endpoint19_3Input = {
|
||||
readonly ptyID: Endpoint19_3Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint19_3Request["query"]["location"]
|
||||
readonly title?: Endpoint19_3Request["payload"]["title"]
|
||||
readonly size?: Endpoint19_3Request["payload"]["size"]
|
||||
}
|
||||
const Endpoint18_3 = (raw: RawClient["server.pty"]) => (input: Endpoint18_3Input) =>
|
||||
const Endpoint19_3 = (raw: RawClient["server.pty"]) => (input: Endpoint19_3Input) =>
|
||||
raw["pty.update"]({
|
||||
params: { ptyID: input["ptyID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { title: input["title"], size: input["size"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint18_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
||||
type Endpoint18_4Input = {
|
||||
readonly ptyID: Endpoint18_4Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint18_4Request["query"]["location"]
|
||||
type Endpoint19_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
|
||||
type Endpoint19_4Input = {
|
||||
readonly ptyID: Endpoint19_4Request["params"]["ptyID"]
|
||||
readonly location?: Endpoint19_4Request["query"]["location"]
|
||||
}
|
||||
const Endpoint18_4 = (raw: RawClient["server.pty"]) => (input: Endpoint18_4Input) =>
|
||||
const Endpoint19_4 = (raw: RawClient["server.pty"]) => (input: Endpoint19_4Input) =>
|
||||
raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup18 = (raw: RawClient["server.pty"]) => ({
|
||||
list: Endpoint18_0(raw),
|
||||
create: Endpoint18_1(raw),
|
||||
get: Endpoint18_2(raw),
|
||||
update: Endpoint18_3(raw),
|
||||
remove: Endpoint18_4(raw),
|
||||
const adaptGroup19 = (raw: RawClient["server.pty"]) => ({
|
||||
list: Endpoint19_0(raw),
|
||||
create: Endpoint19_1(raw),
|
||||
get: Endpoint19_2(raw),
|
||||
update: Endpoint19_3(raw),
|
||||
remove: Endpoint19_4(raw),
|
||||
})
|
||||
|
||||
type Endpoint19_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
|
||||
type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
|
||||
const Endpoint19_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint19_0Input) =>
|
||||
type Endpoint20_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
|
||||
type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
|
||||
const Endpoint20_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint20_0Input) =>
|
||||
raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint19_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
|
||||
type Endpoint19_1Input = {
|
||||
readonly location?: Endpoint19_1Request["query"]["location"]
|
||||
readonly command: Endpoint19_1Request["payload"]["command"]
|
||||
readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
|
||||
readonly timeout?: Endpoint19_1Request["payload"]["timeout"]
|
||||
readonly metadata?: Endpoint19_1Request["payload"]["metadata"]
|
||||
type Endpoint20_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
|
||||
type Endpoint20_1Input = {
|
||||
readonly location?: Endpoint20_1Request["query"]["location"]
|
||||
readonly command: Endpoint20_1Request["payload"]["command"]
|
||||
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
|
||||
readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
|
||||
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
|
||||
}
|
||||
const Endpoint19_1 = (raw: RawClient["server.shell"]) => (input: Endpoint19_1Input) =>
|
||||
const Endpoint20_1 = (raw: RawClient["server.shell"]) => (input: Endpoint20_1Input) =>
|
||||
raw["shell.create"]({
|
||||
query: { location: input["location"] },
|
||||
payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint19_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
|
||||
type Endpoint19_2Input = {
|
||||
readonly id: Endpoint19_2Request["params"]["id"]
|
||||
readonly location?: Endpoint19_2Request["query"]["location"]
|
||||
type Endpoint20_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
|
||||
type Endpoint20_2Input = {
|
||||
readonly id: Endpoint20_2Request["params"]["id"]
|
||||
readonly location?: Endpoint20_2Request["query"]["location"]
|
||||
}
|
||||
const Endpoint19_2 = (raw: RawClient["server.shell"]) => (input: Endpoint19_2Input) =>
|
||||
const Endpoint20_2 = (raw: RawClient["server.shell"]) => (input: Endpoint20_2Input) =>
|
||||
raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
type Endpoint19_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
||||
type Endpoint19_3Input = {
|
||||
readonly id: Endpoint19_3Request["params"]["id"]
|
||||
readonly location?: Endpoint19_3Request["query"]["location"]
|
||||
readonly cursor?: Endpoint19_3Request["query"]["cursor"]
|
||||
readonly limit?: Endpoint19_3Request["query"]["limit"]
|
||||
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
|
||||
type Endpoint20_3Input = {
|
||||
readonly id: Endpoint20_3Request["params"]["id"]
|
||||
readonly location?: Endpoint20_3Request["query"]["location"]
|
||||
readonly cursor?: Endpoint20_3Request["query"]["cursor"]
|
||||
readonly limit?: Endpoint20_3Request["query"]["limit"]
|
||||
}
|
||||
const Endpoint19_3 = (raw: RawClient["server.shell"]) => (input: Endpoint19_3Input) =>
|
||||
const Endpoint20_3 = (raw: RawClient["server.shell"]) => (input: Endpoint20_3Input) =>
|
||||
raw["shell.output"]({
|
||||
params: { id: input["id"] },
|
||||
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint19_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
||||
type Endpoint19_4Input = {
|
||||
readonly id: Endpoint19_4Request["params"]["id"]
|
||||
readonly location?: Endpoint19_4Request["query"]["location"]
|
||||
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
|
||||
type Endpoint20_4Input = {
|
||||
readonly id: Endpoint20_4Request["params"]["id"]
|
||||
readonly location?: Endpoint20_4Request["query"]["location"]
|
||||
}
|
||||
const Endpoint19_4 = (raw: RawClient["server.shell"]) => (input: Endpoint19_4Input) =>
|
||||
const Endpoint20_4 = (raw: RawClient["server.shell"]) => (input: Endpoint20_4Input) =>
|
||||
raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup19 = (raw: RawClient["server.shell"]) => ({
|
||||
list: Endpoint19_0(raw),
|
||||
create: Endpoint19_1(raw),
|
||||
get: Endpoint19_2(raw),
|
||||
output: Endpoint19_3(raw),
|
||||
remove: Endpoint19_4(raw),
|
||||
const adaptGroup20 = (raw: RawClient["server.shell"]) => ({
|
||||
list: Endpoint20_0(raw),
|
||||
create: Endpoint20_1(raw),
|
||||
get: Endpoint20_2(raw),
|
||||
output: Endpoint20_3(raw),
|
||||
remove: Endpoint20_4(raw),
|
||||
})
|
||||
|
||||
type Endpoint20_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||
type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
|
||||
const Endpoint20_0 = (raw: RawClient["server.question"]) => (input?: Endpoint20_0Input) =>
|
||||
type Endpoint21_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
|
||||
type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
|
||||
const Endpoint21_0 = (raw: RawClient["server.question"]) => (input?: Endpoint21_0Input) =>
|
||||
raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint20_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
||||
type Endpoint20_1Input = { readonly sessionID: Endpoint20_1Request["params"]["sessionID"] }
|
||||
const Endpoint20_1 = (raw: RawClient["server.question"]) => (input: Endpoint20_1Input) =>
|
||||
type Endpoint21_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
|
||||
type Endpoint21_1Input = { readonly sessionID: Endpoint21_1Request["params"]["sessionID"] }
|
||||
const Endpoint21_1 = (raw: RawClient["server.question"]) => (input: Endpoint21_1Input) =>
|
||||
raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
)
|
||||
|
||||
type Endpoint20_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
||||
type Endpoint20_2Input = {
|
||||
readonly sessionID: Endpoint20_2Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint20_2Request["params"]["requestID"]
|
||||
readonly answers: Endpoint20_2Request["payload"]["answers"]
|
||||
type Endpoint21_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
|
||||
type Endpoint21_2Input = {
|
||||
readonly sessionID: Endpoint21_2Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint21_2Request["params"]["requestID"]
|
||||
readonly answers: Endpoint21_2Request["payload"]["answers"]
|
||||
}
|
||||
const Endpoint20_2 = (raw: RawClient["server.question"]) => (input: Endpoint20_2Input) =>
|
||||
const Endpoint21_2 = (raw: RawClient["server.question"]) => (input: Endpoint21_2Input) =>
|
||||
raw["session.question.reply"]({
|
||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
||||
payload: { answers: input["answers"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint20_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
||||
type Endpoint20_3Input = {
|
||||
readonly sessionID: Endpoint20_3Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint20_3Request["params"]["requestID"]
|
||||
type Endpoint21_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
|
||||
type Endpoint21_3Input = {
|
||||
readonly sessionID: Endpoint21_3Request["params"]["sessionID"]
|
||||
readonly requestID: Endpoint21_3Request["params"]["requestID"]
|
||||
}
|
||||
const Endpoint20_3 = (raw: RawClient["server.question"]) => (input: Endpoint20_3Input) =>
|
||||
const Endpoint21_3 = (raw: RawClient["server.question"]) => (input: Endpoint21_3Input) =>
|
||||
raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
)
|
||||
|
||||
const adaptGroup20 = (raw: RawClient["server.question"]) => ({
|
||||
listRequests: Endpoint20_0(raw),
|
||||
list: Endpoint20_1(raw),
|
||||
reply: Endpoint20_2(raw),
|
||||
reject: Endpoint20_3(raw),
|
||||
const adaptGroup21 = (raw: RawClient["server.question"]) => ({
|
||||
listRequests: Endpoint21_0(raw),
|
||||
list: Endpoint21_1(raw),
|
||||
reply: Endpoint21_2(raw),
|
||||
reject: Endpoint21_3(raw),
|
||||
})
|
||||
|
||||
type Endpoint21_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
||||
type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
|
||||
const Endpoint21_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint21_0Input) =>
|
||||
type Endpoint22_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
|
||||
type Endpoint22_0Input = { readonly location?: Endpoint22_0Request["query"]["location"] }
|
||||
const Endpoint22_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint22_0Input) =>
|
||||
raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup21 = (raw: RawClient["server.reference"]) => ({ list: Endpoint21_0(raw) })
|
||||
const adaptGroup22 = (raw: RawClient["server.reference"]) => ({ list: Endpoint22_0(raw) })
|
||||
|
||||
type Endpoint22_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
||||
type Endpoint22_0Input = {
|
||||
readonly projectID: Endpoint22_0Request["params"]["projectID"]
|
||||
readonly location?: Endpoint22_0Request["query"]["location"]
|
||||
readonly strategy: Endpoint22_0Request["payload"]["strategy"]
|
||||
readonly directory: Endpoint22_0Request["payload"]["directory"]
|
||||
readonly name?: Endpoint22_0Request["payload"]["name"]
|
||||
type Endpoint23_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
|
||||
type Endpoint23_0Input = {
|
||||
readonly projectID: Endpoint23_0Request["params"]["projectID"]
|
||||
readonly location?: Endpoint23_0Request["query"]["location"]
|
||||
readonly strategy: Endpoint23_0Request["payload"]["strategy"]
|
||||
readonly directory: Endpoint23_0Request["payload"]["directory"]
|
||||
readonly name?: Endpoint23_0Request["payload"]["name"]
|
||||
}
|
||||
const Endpoint22_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint22_0Input) =>
|
||||
const Endpoint23_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_0Input) =>
|
||||
raw["projectCopy.create"]({
|
||||
params: { projectID: input["projectID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint22_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
||||
type Endpoint22_1Input = {
|
||||
readonly projectID: Endpoint22_1Request["params"]["projectID"]
|
||||
readonly location?: Endpoint22_1Request["query"]["location"]
|
||||
readonly directory: Endpoint22_1Request["payload"]["directory"]
|
||||
readonly force: Endpoint22_1Request["payload"]["force"]
|
||||
type Endpoint23_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
|
||||
type Endpoint23_1Input = {
|
||||
readonly projectID: Endpoint23_1Request["params"]["projectID"]
|
||||
readonly location?: Endpoint23_1Request["query"]["location"]
|
||||
readonly directory: Endpoint23_1Request["payload"]["directory"]
|
||||
readonly force: Endpoint23_1Request["payload"]["force"]
|
||||
}
|
||||
const Endpoint22_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint22_1Input) =>
|
||||
const Endpoint23_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_1Input) =>
|
||||
raw["projectCopy.remove"]({
|
||||
params: { projectID: input["projectID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { directory: input["directory"], force: input["force"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
type Endpoint22_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
||||
type Endpoint22_2Input = {
|
||||
readonly projectID: Endpoint22_2Request["params"]["projectID"]
|
||||
readonly location?: Endpoint22_2Request["query"]["location"]
|
||||
type Endpoint23_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
|
||||
type Endpoint23_2Input = {
|
||||
readonly projectID: Endpoint23_2Request["params"]["projectID"]
|
||||
readonly location?: Endpoint23_2Request["query"]["location"]
|
||||
}
|
||||
const Endpoint22_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint22_2Input) =>
|
||||
const Endpoint23_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint23_2Input) =>
|
||||
raw["projectCopy.refresh"]({
|
||||
params: { projectID: input["projectID"] },
|
||||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError))
|
||||
|
||||
const adaptGroup22 = (raw: RawClient["server.projectCopy"]) => ({
|
||||
create: Endpoint22_0(raw),
|
||||
remove: Endpoint22_1(raw),
|
||||
refresh: Endpoint22_2(raw),
|
||||
const adaptGroup23 = (raw: RawClient["server.projectCopy"]) => ({
|
||||
create: Endpoint23_0(raw),
|
||||
remove: Endpoint23_1(raw),
|
||||
refresh: Endpoint23_2(raw),
|
||||
})
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
@@ -850,16 +1026,17 @@ const adaptClient = (raw: RawClient) => ({
|
||||
"server.mcp": adaptGroup10(raw["server.mcp"]),
|
||||
credential: adaptGroup11(raw["server.credential"]),
|
||||
project: adaptGroup12(raw["server.project"]),
|
||||
permission: adaptGroup13(raw["server.permission"]),
|
||||
file: adaptGroup14(raw["server.fs"]),
|
||||
command: adaptGroup15(raw["server.command"]),
|
||||
skill: adaptGroup16(raw["server.skill"]),
|
||||
event: adaptGroup17(raw["server.event"]),
|
||||
pty: adaptGroup18(raw["server.pty"]),
|
||||
shell: adaptGroup19(raw["server.shell"]),
|
||||
question: adaptGroup20(raw["server.question"]),
|
||||
reference: adaptGroup21(raw["server.reference"]),
|
||||
projectCopy: adaptGroup22(raw["server.projectCopy"]),
|
||||
form: adaptGroup13(raw["server.form"]),
|
||||
permission: adaptGroup14(raw["server.permission"]),
|
||||
file: adaptGroup15(raw["server.fs"]),
|
||||
command: adaptGroup16(raw["server.command"]),
|
||||
skill: adaptGroup17(raw["server.skill"]),
|
||||
event: adaptGroup18(raw["server.event"]),
|
||||
pty: adaptGroup19(raw["server.pty"]),
|
||||
shell: adaptGroup20(raw["server.shell"]),
|
||||
question: adaptGroup21(raw["server.question"]),
|
||||
reference: adaptGroup22(raw["server.reference"]),
|
||||
projectCopy: adaptGroup23(raw["server.projectCopy"]),
|
||||
})
|
||||
|
||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||
@@ -1,10 +1,15 @@
|
||||
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
|
||||
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
|
||||
export * from "./generated-effect/index"
|
||||
export * from "./generated/index"
|
||||
export { Service } from "./service.js"
|
||||
export type { Transport, ServiceOptions } from "./service.js"
|
||||
export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { Event } from "@opencode-ai/schema/event"
|
||||
export { EventLog } from "@opencode-ai/schema/event-log"
|
||||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
export { Form } from "@opencode-ai/schema/form"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Location } from "@opencode-ai/schema/location"
|
||||
export { Model } from "@opencode-ai/schema/model"
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Effect, FileSystem, Option, Schedule, Schema } from "effect"
|
||||
import { spawn } from "node:child_process"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
// Find, start, and stop the local opencode background service.
|
||||
//
|
||||
// The service daemon advertises itself through a registration file in the
|
||||
// user's state directory: url, pid, version, and the private password, with
|
||||
// 0600 permissions. That file is the complete discovery contract — reading it
|
||||
// is all a client needs to connect. The daemon's own configuration (port,
|
||||
// persisted password) is CLI-owned and never read here.
|
||||
|
||||
export type Transport = {
|
||||
readonly url: string
|
||||
readonly headers?: RequestInit["headers"]
|
||||
}
|
||||
|
||||
export type ServiceOptions = {
|
||||
// Absolute path to the service registration file. Defaults to
|
||||
// opencode/service.json in the XDG state directory.
|
||||
readonly file?: string
|
||||
// When set, discovery only returns a server reporting this exact version,
|
||||
// and start() replaces a healthy server whose version differs.
|
||||
readonly version?: string
|
||||
// Argv used to spawn the service. Defaults to ["opencode", "serve",
|
||||
// "--service"] resolved from PATH.
|
||||
readonly command?: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
// Read-only lookup: registration file plus health check and version gate.
|
||||
// Never spawns; escalation to start() is the caller's policy.
|
||||
export const discover = Effect.fn("service.discover")(function* (options: ServiceOptions = {}) {
|
||||
const registration = yield* read(options.file)
|
||||
if (registration === undefined) return undefined
|
||||
if (options.version !== undefined && registration.version !== options.version) return undefined
|
||||
const found = yield* probe(registration)
|
||||
return found?.transport
|
||||
})
|
||||
|
||||
// Idempotent ensure-running: reuses a healthy compatible server, replaces a
|
||||
// version-mismatched one, and otherwise spawns the service command detached.
|
||||
export const start = Effect.fn("service.start")(function* (options: ServiceOptions = {}) {
|
||||
const compatible = yield* discover(options)
|
||||
if (compatible !== undefined) return compatible
|
||||
const mismatched = yield* find(options)
|
||||
if (mismatched !== undefined) yield* kill(mismatched.registration, options).pipe(Effect.ignore)
|
||||
|
||||
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"]
|
||||
if (command === undefined) return yield* Effect.fail(new Error("Missing service command"))
|
||||
yield* Effect.try({
|
||||
try: () => {
|
||||
spawn(command, args, { detached: true, stdio: "ignore" }).unref()
|
||||
},
|
||||
catch: (cause) => new Error("Failed to start server", { cause }),
|
||||
})
|
||||
|
||||
return yield* discover(options).pipe(
|
||||
Effect.flatMap((found) =>
|
||||
found === undefined ? Effect.fail(new Error("Server is not ready")) : Effect.succeed(found),
|
||||
),
|
||||
Effect.retry(poll),
|
||||
Effect.mapError(() => new Error("Failed to start server")),
|
||||
)
|
||||
})
|
||||
|
||||
export const stop = Effect.fn("service.stop")(function* (options: ServiceOptions = {}) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const existing = yield* find(options)
|
||||
if (existing !== undefined) yield* kill(existing.registration, options)
|
||||
yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore)
|
||||
})
|
||||
|
||||
function fallback() {
|
||||
const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state")
|
||||
return join(state, "opencode", "service.json")
|
||||
}
|
||||
|
||||
function auth(password: string): RequestInit["headers"] {
|
||||
return { authorization: "Basic " + btoa("opencode:" + password) }
|
||||
}
|
||||
|
||||
const Registration = Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
version: Schema.optional(Schema.String),
|
||||
url: Schema.String,
|
||||
pid: Schema.Int.check(Schema.isGreaterThan(0)),
|
||||
password: Schema.optional(Schema.String),
|
||||
})
|
||||
type Registration = typeof Registration.Type
|
||||
|
||||
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
|
||||
|
||||
// A missing or corrupt file means no valid registration; callers treat both
|
||||
// the same (the registering server self-evicts, clients rediscover).
|
||||
const read = Effect.fnUntraced(function* (file?: string) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const text = yield* fs.readFileString(file ?? fallback()).pipe(Effect.option)
|
||||
if (Option.isNone(text)) return undefined
|
||||
return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined))
|
||||
})
|
||||
|
||||
type LocalService = {
|
||||
readonly registration: Registration
|
||||
readonly transport: Transport
|
||||
}
|
||||
|
||||
const probe = Effect.fnUntraced(function* (registration: Registration) {
|
||||
const headers = registration.password === undefined ? undefined : auth(registration.password)
|
||||
const healthy = yield* Effect.tryPromise(() =>
|
||||
fetch(new URL("/api/health", registration.url), {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(2_000),
|
||||
}),
|
||||
).pipe(
|
||||
Effect.map((response) => response.ok),
|
||||
Effect.orElseSucceed(() => false),
|
||||
)
|
||||
if (!healthy) return undefined
|
||||
return { registration, transport: { url: registration.url, headers } } satisfies LocalService
|
||||
})
|
||||
|
||||
// Health-checked lookup without the version gate: lifecycle operations must be
|
||||
// able to see (and replace or stop) a server from a different version.
|
||||
const find = Effect.fnUntraced(function* (options: ServiceOptions) {
|
||||
const registration = yield* read(options.file)
|
||||
if (registration === undefined) return undefined
|
||||
return yield* probe(registration)
|
||||
})
|
||||
|
||||
// 50ms cadence bounded at ~5s, shared by stop escalation and start readiness.
|
||||
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))
|
||||
|
||||
const signal = (pid: number, name: NodeJS.Signals) =>
|
||||
Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore)
|
||||
|
||||
const stopped = Effect.fnUntraced(function* (pid: number) {
|
||||
const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe(
|
||||
Effect.orElseSucceed(() => false),
|
||||
)
|
||||
if (!running) return true
|
||||
return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
|
||||
})
|
||||
|
||||
function same(left: Registration, right: Registration) {
|
||||
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
|
||||
}
|
||||
|
||||
const kill = Effect.fnUntraced(function* (info: Registration, options: ServiceOptions) {
|
||||
// A stale registration may point at a PID that has since been reused by
|
||||
// another process. Only signal the PID after authenticating the server.
|
||||
const current = yield* find(options)
|
||||
if (current === undefined || !same(current.registration, info)) return
|
||||
|
||||
yield* signal(info.pid, "SIGTERM")
|
||||
const done = yield* stopped(info.pid).pipe(Effect.retry(poll), Effect.option)
|
||||
if (Option.isSome(done)) return
|
||||
|
||||
const latest = yield* find(options)
|
||||
if (latest === undefined || !same(latest.registration, info)) return
|
||||
yield* signal(info.pid, "SIGKILL")
|
||||
yield* stopped(info.pid).pipe(Effect.retry(poll))
|
||||
})
|
||||
|
||||
export * as Service from "./service.js"
|
||||
+207
-15
@@ -23,8 +23,12 @@ import type {
|
||||
SessionRenameOutput,
|
||||
SessionPromptInput,
|
||||
SessionPromptOutput,
|
||||
SessionCommandInput,
|
||||
SessionCommandOutput,
|
||||
SessionSkillInput,
|
||||
SessionSkillOutput,
|
||||
SessionSyntheticInput,
|
||||
SessionSyntheticOutput,
|
||||
SessionCompactInput,
|
||||
SessionCompactOutput,
|
||||
SessionWaitInput,
|
||||
@@ -37,10 +41,14 @@ import type {
|
||||
SessionRevertCommitOutput,
|
||||
SessionContextInput,
|
||||
SessionContextOutput,
|
||||
SessionHistoryInput,
|
||||
SessionHistoryOutput,
|
||||
SessionEventsInput,
|
||||
SessionEventsOutput,
|
||||
SessionListContextEntriesInput,
|
||||
SessionListContextEntriesOutput,
|
||||
SessionPutContextEntryInput,
|
||||
SessionPutContextEntryOutput,
|
||||
SessionRemoveContextEntryInput,
|
||||
SessionRemoveContextEntryOutput,
|
||||
SessionLogInput,
|
||||
SessionLogOutput,
|
||||
SessionInterruptInput,
|
||||
SessionInterruptOutput,
|
||||
SessionBackgroundInput,
|
||||
@@ -51,6 +59,8 @@ import type {
|
||||
MessageListOutput,
|
||||
ModelListInput,
|
||||
ModelListOutput,
|
||||
ModelDefaultInput,
|
||||
ModelDefaultOutput,
|
||||
GenerateTextInput,
|
||||
GenerateTextOutput,
|
||||
ProviderListInput,
|
||||
@@ -81,6 +91,20 @@ import type {
|
||||
ProjectCurrentOutput,
|
||||
ProjectDirectoriesInput,
|
||||
ProjectDirectoriesOutput,
|
||||
FormListRequestsInput,
|
||||
FormListRequestsOutput,
|
||||
FormListInput,
|
||||
FormListOutput,
|
||||
FormCreateInput,
|
||||
FormCreateOutput,
|
||||
FormGetInput,
|
||||
FormGetOutput,
|
||||
FormStateInput,
|
||||
FormStateOutput,
|
||||
FormReplyInput,
|
||||
FormReplyOutput,
|
||||
FormCancelInput,
|
||||
FormCancelOutput,
|
||||
PermissionListRequestsInput,
|
||||
PermissionListRequestsOutput,
|
||||
PermissionListSavedInput,
|
||||
@@ -106,6 +130,7 @@ import type {
|
||||
SkillListInput,
|
||||
SkillListOutput,
|
||||
EventSubscribeOutput,
|
||||
EventChangesOutput,
|
||||
PtyListInput,
|
||||
PtyListOutput,
|
||||
PtyCreateInput,
|
||||
@@ -339,6 +364,7 @@ export function make(options: ClientOptions) {
|
||||
limit: input?.["limit"],
|
||||
order: input?.["order"],
|
||||
search: input?.["search"],
|
||||
parentID: input?.["parentID"],
|
||||
directory: input?.["directory"],
|
||||
project: input?.["project"],
|
||||
subpath: input?.["subpath"],
|
||||
@@ -368,7 +394,7 @@ export function make(options: ClientOptions) {
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
active: (requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionActiveOutput }>(
|
||||
request<SessionActiveOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/active`,
|
||||
@@ -377,7 +403,7 @@ export function make(options: ClientOptions) {
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
),
|
||||
get: (input: SessionGetInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionGetOutput }>(
|
||||
{
|
||||
@@ -449,6 +475,28 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
command: (input: SessionCommandInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionCommandOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/command`,
|
||||
body: {
|
||||
id: input["id"],
|
||||
command: input["command"],
|
||||
arguments: input["arguments"],
|
||||
agent: input["agent"],
|
||||
model: input["model"],
|
||||
files: input["files"],
|
||||
agents: input["agents"],
|
||||
delivery: input["delivery"],
|
||||
resume: input["resume"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [409, 404, 500, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
skill: (input: SessionSkillInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionSkillOutput>(
|
||||
{
|
||||
@@ -461,6 +509,18 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
synthetic: (input: SessionSyntheticInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionSyntheticOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/synthetic`,
|
||||
body: { text: input["text"], description: input["description"], metadata: input["metadata"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
compact: (input: SessionCompactInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionCompactOutput>(
|
||||
{
|
||||
@@ -528,24 +588,46 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
history: (input: SessionHistoryInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionHistoryOutput>(
|
||||
listContextEntries: (input: SessionListContextEntriesInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionListContextEntriesOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/history`,
|
||||
query: { limit: input["limit"], after: input["after"] },
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context-entry`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
putContextEntry: (input: SessionPutContextEntryInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionPutContextEntryOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context-entry/${encodeURIComponent(input.key)}`,
|
||||
body: { value: input["value"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
events: (input: SessionEventsInput, requestOptions?: RequestOptions): AsyncIterable<SessionEventsOutput> =>
|
||||
sse<SessionEventsOutput>(
|
||||
removeContextEntry: (input: SessionRemoveContextEntryInput, requestOptions?: RequestOptions) =>
|
||||
request<SessionRemoveContextEntryOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/context-entry/${encodeURIComponent(input.key)}`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable<SessionLogOutput> =>
|
||||
sse<SessionLogOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
|
||||
query: { after: input["after"] },
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/log`,
|
||||
query: { after: input["after"], follow: input["follow"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
@@ -613,6 +695,18 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
default: (input?: ModelDefaultInput, requestOptions?: RequestOptions) =>
|
||||
request<ModelDefaultOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/model/default`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [503, 401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
generate: {
|
||||
text: (input: GenerateTextInput, requestOptions?: RequestOptions) =>
|
||||
@@ -811,6 +905,95 @@ export function make(options: ClientOptions) {
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
form: {
|
||||
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<FormListRequestsOutput>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/form/request`,
|
||||
query: { location: input?.["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [401, 400],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
list: (input: FormListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: FormListOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
create: (input: FormCreateInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: FormCreateOutput }>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
|
||||
body: {
|
||||
id: input["id"],
|
||||
title: input["title"],
|
||||
metadata: input["metadata"],
|
||||
mode: input["mode"],
|
||||
fields: input["fields"],
|
||||
url: input["url"],
|
||||
},
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 409, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
get: (input: FormGetInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: FormGetOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
state: (input: FormStateInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: FormStateOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/state`,
|
||||
successStatus: 200,
|
||||
declaredStatuses: [404, 400, 401],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
reply: (input: FormReplyInput, requestOptions?: RequestOptions) =>
|
||||
request<FormReplyOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`,
|
||||
body: { answer: input["answer"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 409, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
cancel: (input: FormCancelInput, requestOptions?: RequestOptions) =>
|
||||
request<FormCancelOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/cancel`,
|
||||
successStatus: 204,
|
||||
declaredStatuses: [404, 409, 400, 401],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
permission: {
|
||||
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
|
||||
request<PermissionListRequestsOutput>(
|
||||
@@ -975,6 +1158,11 @@ export function make(options: ClientOptions) {
|
||||
{ method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
|
||||
requestOptions,
|
||||
),
|
||||
changes: (requestOptions?: RequestOptions): AsyncIterable<EventChangesOutput> =>
|
||||
sse<EventChangesOutput>(
|
||||
{ method: "GET", path: `/api/event/changes`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
pty: {
|
||||
list: (input?: PtyListInput, requestOptions?: RequestOptions) =>
|
||||
@@ -1224,7 +1412,11 @@ function encodePath(value: string): string {
|
||||
}
|
||||
|
||||
function appendQuery(params: URLSearchParams, key: string, value: unknown): void {
|
||||
if (value === undefined || value === null) return
|
||||
if (value === undefined) return
|
||||
if (value === null) {
|
||||
params.append(key, "null")
|
||||
return
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) appendQuery(params, key, item)
|
||||
return
|
||||
+2096
-971
@@ -50,6 +50,22 @@ export type ConflictError = {
|
||||
export const isConflictError = (value: unknown): value is ConflictError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError"
|
||||
|
||||
export type CommandNotFoundError = {
|
||||
readonly _tag: "CommandNotFoundError"
|
||||
readonly command: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isCommandNotFoundError = (value: unknown): value is CommandNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandNotFoundError"
|
||||
|
||||
export type CommandEvaluationError = {
|
||||
readonly _tag: "CommandEvaluationError"
|
||||
readonly command: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isCommandEvaluationError = (value: unknown): value is CommandEvaluationError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandEvaluationError"
|
||||
|
||||
export type SkillNotFoundError = {
|
||||
readonly _tag: "SkillNotFoundError"
|
||||
readonly skill: string
|
||||
@@ -90,6 +106,26 @@ export type ProviderNotFoundError = {
|
||||
export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError"
|
||||
|
||||
export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string }
|
||||
export const isFormNotFoundError = (value: unknown): value is FormNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError"
|
||||
|
||||
export type FormAlreadySettledError = {
|
||||
readonly _tag: "FormAlreadySettledError"
|
||||
readonly id: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError"
|
||||
|
||||
export type FormInvalidAnswerError = {
|
||||
readonly _tag: "FormInvalidAnswerError"
|
||||
readonly id: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isFormInvalidAnswerError = (value: unknown): value is FormInvalidAnswerError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormInvalidAnswerError"
|
||||
|
||||
export type PermissionNotFoundError = {
|
||||
readonly _tag: "PermissionNotFoundError"
|
||||
readonly requestID: string
|
||||
@@ -151,6 +187,7 @@ export type AgentListOutput = {
|
||||
readonly id: string
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly request: {
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
}
|
||||
@@ -189,6 +226,7 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -199,6 +237,7 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -209,6 +248,7 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -219,16 +259,29 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
readonly cursor?: string | undefined
|
||||
}["search"]
|
||||
readonly parentID?: {
|
||||
readonly workspace?: string | undefined
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
readonly cursor?: string | undefined
|
||||
}["parentID"]
|
||||
readonly directory?: {
|
||||
readonly workspace?: string | undefined
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -239,6 +292,7 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -249,6 +303,7 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -259,6 +314,7 @@ export type SessionListInput = {
|
||||
readonly limit?: number | undefined
|
||||
readonly order?: "asc" | "desc" | undefined
|
||||
readonly search?: string | undefined
|
||||
readonly parentID?: string | null | undefined
|
||||
readonly directory?: string | undefined
|
||||
readonly project?: string | undefined
|
||||
readonly subpath?: string | undefined
|
||||
@@ -298,6 +354,7 @@ export type SessionListOutput = {
|
||||
}>
|
||||
}
|
||||
}>
|
||||
readonly watermarks: { readonly [x: string]: number }
|
||||
readonly cursor: { readonly previous?: string | null; readonly next?: string | null }
|
||||
}
|
||||
|
||||
@@ -362,7 +419,10 @@ export type SessionCreateOutput = {
|
||||
}
|
||||
}["data"]
|
||||
|
||||
export type SessionActiveOutput = { readonly data: { readonly [x: string]: { readonly type: "running" } } }["data"]
|
||||
export type SessionActiveOutput = {
|
||||
readonly data: { readonly [x: string]: { readonly type: "running" } }
|
||||
readonly watermarks: { readonly [x: string]: number }
|
||||
}
|
||||
|
||||
export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
@@ -563,6 +623,206 @@ export type SessionPromptOutput = {
|
||||
}
|
||||
}["data"]
|
||||
|
||||
export type SessionCommandInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["id"]
|
||||
readonly command: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["command"]
|
||||
readonly arguments?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["arguments"]
|
||||
readonly agent?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["agent"]
|
||||
readonly model?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["model"]
|
||||
readonly files?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["files"]
|
||||
readonly agents?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["agents"]
|
||||
readonly delivery?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["delivery"]
|
||||
readonly resume?: {
|
||||
readonly id?: string | null
|
||||
readonly command: string
|
||||
readonly arguments?: string | null
|
||||
readonly agent?: string | null
|
||||
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly delivery?: "steer" | "queue" | null
|
||||
readonly resume?: boolean | null
|
||||
}["resume"]
|
||||
}
|
||||
|
||||
export type SessionCommandOutput = {
|
||||
readonly data: {
|
||||
readonly admittedSeq: number
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
readonly timeCreated: number
|
||||
readonly promotedSeq?: number
|
||||
}
|
||||
}["data"]
|
||||
|
||||
export type SessionSkillInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: {
|
||||
@@ -584,6 +844,27 @@ export type SessionSkillInput = {
|
||||
|
||||
export type SessionSkillOutput = void
|
||||
|
||||
export type SessionSyntheticInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly text: {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
}["text"]
|
||||
readonly description?: {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
}["description"]
|
||||
readonly metadata?: {
|
||||
readonly text: string
|
||||
readonly description?: string | null
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
}["metadata"]
|
||||
}
|
||||
|
||||
export type SessionSyntheticOutput = void
|
||||
|
||||
export type SessionCompactInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionCompactOutput = void
|
||||
@@ -787,992 +1068,522 @@ export type SessionContextOutput = {
|
||||
>
|
||||
}["data"]
|
||||
|
||||
export type SessionHistoryInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly limit?: { readonly limit?: number | undefined; readonly after?: number | undefined }["limit"]
|
||||
readonly after?: { readonly limit?: number | undefined; readonly after?: number | undefined }["after"]
|
||||
export type SessionListContextEntriesInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionListContextEntriesOutput = {
|
||||
readonly data: ReadonlyArray<{ readonly key: string; readonly value: JsonValue }>
|
||||
}["data"]
|
||||
|
||||
export type SessionPutContextEntryInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"]
|
||||
readonly key: { readonly sessionID: string; readonly key: string }["key"]
|
||||
readonly value: { readonly value: JsonValue }["value"]
|
||||
}
|
||||
|
||||
export type SessionHistoryOutput = {
|
||||
readonly data: ReadonlyArray<
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.agent.switched"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly agent: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.model.switched"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.moved"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly subdirectory?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.renamed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.forked"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly parentID: string
|
||||
readonly messageID?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.prompted"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.prompt.admitted"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.context.updated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.synthetic"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
readonly description?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.skill.activated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly name: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.shell.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly callID: string
|
||||
readonly command: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.shell.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly callID: string
|
||||
readonly output: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.step.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly snapshot?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.step.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly finish: string
|
||||
readonly cost: number
|
||||
readonly tokens: {
|
||||
readonly input: number
|
||||
readonly output: number
|
||||
readonly reasoning: number
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
}
|
||||
readonly snapshot?: string
|
||||
readonly files?: ReadonlyArray<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.step.failed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly error: { readonly type: "unknown"; readonly message: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.text.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly textID: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.text.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly textID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.tool.input.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly name: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.tool.input.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.tool.called"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly tool: string
|
||||
readonly input: { readonly [x: string]: JsonValue }
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.tool.progress"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly structured: { readonly [x: string]: JsonValue }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
|
||||
>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.tool.success"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly structured: { readonly [x: string]: JsonValue }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
|
||||
>
|
||||
readonly outputPaths?: ReadonlyArray<string>
|
||||
readonly result?: JsonValue
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.tool.failed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly error: { readonly type: "unknown"; readonly message: string }
|
||||
readonly result?: JsonValue
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.reasoning.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly reasoningID: string
|
||||
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.reasoning.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly reasoningID: string
|
||||
readonly text: string
|
||||
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: JsonValue } }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.retried"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly attempt: number
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.compaction.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly reason: "auto" | "manual"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.compaction.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly text: string
|
||||
readonly recent: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.revert.staged"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly revert: {
|
||||
export type SessionPutContextEntryOutput = void
|
||||
|
||||
export type SessionRemoveContextEntryInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"]
|
||||
readonly key: { readonly sessionID: string; readonly key: string }["key"]
|
||||
}
|
||||
|
||||
export type SessionRemoveContextEntryOutput = void
|
||||
|
||||
export type SessionLogInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"]
|
||||
readonly follow?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["follow"]
|
||||
}
|
||||
|
||||
export type SessionLogOutput =
|
||||
| (
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.agent.switched"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly partID?: string
|
||||
readonly snapshot?: string
|
||||
readonly diff?: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly path: string
|
||||
readonly status: "added" | "modified" | "deleted"
|
||||
readonly additions: number
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
readonly agent: string
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.revert.cleared"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly type: "session.next.revert.committed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string }
|
||||
}
|
||||
>
|
||||
readonly hasMore: boolean
|
||||
}
|
||||
|
||||
export type SessionEventsInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly after?: { readonly after?: number | undefined }["after"]
|
||||
}
|
||||
|
||||
export type SessionEventsOutput =
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.agent.switched"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly agent: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.model.switched"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.moved"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly subdirectory?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.renamed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.forked"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly parentID: string
|
||||
readonly messageID?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.prompted"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.model.switched"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.moved"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly location: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly subdirectory?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.renamed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly title: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.forked"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly parentID: string
|
||||
readonly messageID?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.prompted"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.prompt.admitted"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.context.updated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.synthetic"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.skill.activated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.prompt.admitted"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly prompt: {
|
||||
readonly text: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly uri: string
|
||||
readonly mime: string
|
||||
readonly name?: string
|
||||
readonly description?: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
readonly agents?: ReadonlyArray<{
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.shell.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly callID: string
|
||||
readonly command: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.shell.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly callID: string
|
||||
readonly output: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.step.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly snapshot?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.step.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly finish: string
|
||||
readonly cost: number
|
||||
readonly tokens: {
|
||||
readonly input: number
|
||||
readonly output: number
|
||||
readonly reasoning: number
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
}
|
||||
readonly snapshot?: string
|
||||
readonly files?: ReadonlyArray<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.step.failed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly error: { readonly type: "unknown"; readonly message: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.text.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly textID: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.text.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly textID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.input.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly name: string
|
||||
readonly source?: { readonly start: number; readonly end: number; readonly text: string }
|
||||
}>
|
||||
}
|
||||
}
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.context.updated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.synthetic"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
readonly description?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.skill.activated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly name: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.shell.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly callID: string
|
||||
readonly command: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.shell.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly callID: string
|
||||
readonly output: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.step.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly agent: string
|
||||
readonly model: { readonly id: string; readonly providerID: string; readonly variant?: string }
|
||||
readonly snapshot?: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.step.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly finish: string
|
||||
readonly cost: number
|
||||
readonly tokens: {
|
||||
readonly input: number
|
||||
readonly output: number
|
||||
readonly reasoning: number
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.input.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
readonly snapshot?: string
|
||||
readonly files?: ReadonlyArray<string>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.step.failed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly error: { readonly type: "unknown"; readonly message: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.text.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly textID: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.text.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly textID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.input.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly name: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.input.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly text: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.called"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly tool: string
|
||||
readonly input: { readonly [x: string]: unknown }
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.called"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly tool: string
|
||||
readonly input: { readonly [x: string]: unknown }
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.progress"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly structured: { readonly [x: string]: unknown }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
|
||||
>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.success"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly structured: { readonly [x: string]: unknown }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
|
||||
>
|
||||
readonly outputPaths?: ReadonlyArray<string>
|
||||
readonly result?: unknown
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.progress"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly structured: { readonly [x: string]: unknown }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
|
||||
>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.failed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly error: { readonly type: "unknown"; readonly message: string }
|
||||
readonly result?: unknown
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.success"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly structured: { readonly [x: string]: unknown }
|
||||
readonly content: ReadonlyArray<
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "file"; readonly uri: string; readonly mime: string; readonly name?: string }
|
||||
>
|
||||
readonly outputPaths?: ReadonlyArray<string>
|
||||
readonly result?: unknown
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.reasoning.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly reasoningID: string
|
||||
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.reasoning.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly reasoningID: string
|
||||
readonly text: string
|
||||
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.retried"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly attempt: number
|
||||
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 }
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.tool.failed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly callID: string
|
||||
readonly error: { readonly type: "unknown"; readonly message: string }
|
||||
readonly result?: unknown
|
||||
readonly provider: {
|
||||
readonly executed: boolean
|
||||
readonly metadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.compaction.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly reason: "auto" | "manual"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.compaction.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly text: string
|
||||
readonly recent: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.revert.staged"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly revert: {
|
||||
readonly messageID: string
|
||||
readonly partID?: string
|
||||
readonly snapshot?: string
|
||||
readonly diff?: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly path: string
|
||||
readonly status: "added" | "modified" | "deleted"
|
||||
readonly additions: number
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.reasoning.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly reasoningID: string
|
||||
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.revert.cleared"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.revert.committed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.reasoning.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly assistantMessageID: string
|
||||
readonly reasoningID: string
|
||||
readonly text: string
|
||||
readonly providerMetadata?: { readonly [x: string]: { readonly [x: string]: unknown } }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.retried"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly attempt: number
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.compaction.started"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly reason: "auto" | "manual"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.compaction.ended"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly messageID: string
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly text: string
|
||||
readonly recent: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.revert.staged"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly revert: {
|
||||
readonly messageID: string
|
||||
readonly partID?: string
|
||||
readonly snapshot?: string
|
||||
readonly diff?: string
|
||||
readonly files?: ReadonlyArray<{
|
||||
readonly path: string
|
||||
readonly status: "added" | "modified" | "deleted"
|
||||
readonly additions: number
|
||||
readonly deletions: number
|
||||
readonly patch: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.revert.cleared"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.revert.committed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly timestamp: number; readonly sessionID: string; readonly messageID: string }
|
||||
}
|
||||
)
|
||||
| { readonly type: "log.synced"; readonly aggregateID: string; readonly seq?: number }
|
||||
|
||||
export type SessionInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
@@ -2129,6 +1940,7 @@ export type MessageListOutput = {
|
||||
readonly time: { readonly created: number }
|
||||
}
|
||||
>
|
||||
readonly watermark?: number
|
||||
readonly cursor: { readonly previous?: string | null; readonly next?: string | null }
|
||||
}
|
||||
|
||||
@@ -2169,12 +1981,14 @@ export type ModelListOutput = {
|
||||
readonly output: ReadonlyArray<string>
|
||||
}
|
||||
readonly request: {
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
readonly variant?: string
|
||||
}
|
||||
readonly variants: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
}>
|
||||
@@ -2191,6 +2005,67 @@ export type ModelListOutput = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type ModelDefaultInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type ModelDefaultOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly project: { readonly id: string; readonly directory: string }
|
||||
}
|
||||
readonly data: {
|
||||
readonly id: string
|
||||
readonly providerID: string
|
||||
readonly family?: string
|
||||
readonly name: string
|
||||
readonly api:
|
||||
| {
|
||||
readonly id: string
|
||||
readonly type: "aisdk"
|
||||
readonly package: string
|
||||
readonly url?: string
|
||||
readonly settings?: { readonly [x: string]: JsonValue }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly type: "native"
|
||||
readonly url?: string
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
}
|
||||
readonly capabilities: {
|
||||
readonly tools: boolean
|
||||
readonly input: ReadonlyArray<string>
|
||||
readonly output: ReadonlyArray<string>
|
||||
}
|
||||
readonly request: {
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
readonly variant?: string
|
||||
}
|
||||
readonly variants: ReadonlyArray<{
|
||||
readonly id: string
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
}>
|
||||
readonly time: { readonly released: number }
|
||||
readonly cost: ReadonlyArray<{
|
||||
readonly tier?: { readonly type: "context"; readonly size: number }
|
||||
readonly input: number
|
||||
readonly output: number
|
||||
readonly cache: { readonly read: number; readonly write: number }
|
||||
}>
|
||||
readonly status: "alpha" | "beta" | "deprecated" | "active"
|
||||
readonly enabled: boolean
|
||||
readonly limit: { readonly context: number; readonly input?: number; readonly output: number }
|
||||
} | null
|
||||
}
|
||||
|
||||
export type GenerateTextInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
@@ -2233,6 +2108,7 @@ export type ProviderListOutput = {
|
||||
}
|
||||
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
|
||||
readonly request: {
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
}
|
||||
@@ -2266,6 +2142,7 @@ export type ProviderGetOutput = {
|
||||
}
|
||||
| { readonly type: "native"; readonly url?: string; readonly settings: { readonly [x: string]: JsonValue } }
|
||||
readonly request: {
|
||||
readonly settings: { readonly [x: string]: JsonValue }
|
||||
readonly headers: { readonly [x: string]: string }
|
||||
readonly body: { readonly [x: string]: JsonValue }
|
||||
}
|
||||
@@ -2551,6 +2428,880 @@ export type ProjectDirectoriesInput = {
|
||||
|
||||
export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }>
|
||||
|
||||
export type FormListRequestsInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type FormListRequestsOutput = {
|
||||
readonly location: {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
readonly project: { readonly id: string; readonly directory: string }
|
||||
}
|
||||
readonly data: ReadonlyArray<
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form"
|
||||
readonly fields: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "url"
|
||||
readonly url: string
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export type FormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type FormListOutput = {
|
||||
readonly data: ReadonlyArray<
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form"
|
||||
readonly fields: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "url"
|
||||
readonly url: string
|
||||
}
|
||||
>
|
||||
}["data"]
|
||||
|
||||
export type FormCreateInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly id?: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form" | "url"
|
||||
readonly fields?: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
> | null
|
||||
readonly url?: string | null
|
||||
}["id"]
|
||||
readonly title?: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form" | "url"
|
||||
readonly fields?: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
> | null
|
||||
readonly url?: string | null
|
||||
}["title"]
|
||||
readonly metadata?: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form" | "url"
|
||||
readonly fields?: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
> | null
|
||||
readonly url?: string | null
|
||||
}["metadata"]
|
||||
readonly mode: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form" | "url"
|
||||
readonly fields?: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
> | null
|
||||
readonly url?: string | null
|
||||
}["mode"]
|
||||
readonly fields?: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form" | "url"
|
||||
readonly fields?: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
> | null
|
||||
readonly url?: string | null
|
||||
}["fields"]
|
||||
readonly url?: {
|
||||
readonly id?: string | null
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form" | "url"
|
||||
readonly fields?: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
> | null
|
||||
readonly url?: string | null
|
||||
}["url"]
|
||||
}
|
||||
|
||||
export type FormCreateOutput = {
|
||||
readonly data:
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form"
|
||||
readonly fields: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "url"
|
||||
readonly url: string
|
||||
}
|
||||
}["data"]
|
||||
|
||||
export type FormGetInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
|
||||
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
|
||||
}
|
||||
|
||||
export type FormGetOutput = {
|
||||
readonly data:
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "form"
|
||||
readonly fields: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly mode: "url"
|
||||
readonly url: string
|
||||
}
|
||||
}["data"]
|
||||
|
||||
export type FormStateInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
|
||||
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
|
||||
}
|
||||
|
||||
export type FormStateOutput = {
|
||||
readonly data:
|
||||
| { readonly status: "pending" }
|
||||
| {
|
||||
readonly status: "answered"
|
||||
readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
}
|
||||
| { readonly status: "cancelled" }
|
||||
}["data"]
|
||||
|
||||
export type FormReplyInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
|
||||
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
|
||||
readonly answer: {
|
||||
readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
}["answer"]
|
||||
}
|
||||
|
||||
export type FormReplyOutput = void
|
||||
|
||||
export type FormCancelInput = {
|
||||
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
|
||||
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
|
||||
}
|
||||
|
||||
export type FormCancelOutput = void
|
||||
|
||||
export type PermissionListRequestsInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
@@ -3505,6 +4256,19 @@ export type EventSubscribeOutput =
|
||||
readonly delivery: "steer" | "queue"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.next.execution.settled"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly timestamp: number
|
||||
readonly sessionID: string
|
||||
readonly outcome: "success" | "failure" | "interrupted"
|
||||
readonly error?: { readonly type: "unknown"; readonly message: string }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
@@ -3530,6 +4294,7 @@ export type EventSubscribeOutput =
|
||||
readonly messageID: string
|
||||
readonly text: string
|
||||
readonly description?: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
}
|
||||
}
|
||||
| {
|
||||
@@ -3990,6 +4755,14 @@ export type EventSubscribeOutput =
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly projectID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "command.updated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
@@ -4140,6 +4913,121 @@ export type EventSubscribeOutput =
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly sessionID: string; readonly requestID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "form.created"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly form:
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly mode: "form"
|
||||
readonly fields: ReadonlyArray<
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "string"
|
||||
readonly format?: "email" | "uri" | "date" | "date-time"
|
||||
readonly minLength?: number
|
||||
readonly maxLength?: number
|
||||
readonly pattern?: string
|
||||
readonly placeholder?: string
|
||||
readonly default?: string
|
||||
readonly options?: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly custom?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "number"
|
||||
readonly minimum?: number
|
||||
readonly maximum?: number
|
||||
readonly default?: number
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "integer"
|
||||
readonly minimum?: number
|
||||
readonly maximum?: number
|
||||
readonly default?: number
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "boolean"
|
||||
readonly default?: boolean
|
||||
}
|
||||
| {
|
||||
readonly key: string
|
||||
readonly title?: string
|
||||
readonly description?: string
|
||||
readonly required?: boolean
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
readonly type: "multiselect"
|
||||
readonly options: ReadonlyArray<{
|
||||
readonly value: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}>
|
||||
readonly minItems?: number
|
||||
readonly maxItems?: number
|
||||
readonly custom?: boolean
|
||||
readonly default?: ReadonlyArray<string>
|
||||
}
|
||||
>
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title?: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly mode: "url"
|
||||
readonly url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "form.replied"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "form.cancelled"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly id: string; readonly sessionID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
@@ -4151,6 +5039,239 @@ export type EventSubscribeOutput =
|
||||
readonly todos: ReadonlyArray<{ readonly content: string; readonly status: string; readonly priority: string }>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.status"
|
||||
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 status:
|
||||
| { readonly type: "idle" }
|
||||
| {
|
||||
readonly type: "retry"
|
||||
readonly attempt: number
|
||||
readonly message: string
|
||||
readonly action?: {
|
||||
readonly reason: string
|
||||
readonly provider: string
|
||||
readonly title: string
|
||||
readonly message: string
|
||||
readonly label: string
|
||||
readonly link?: string
|
||||
}
|
||||
readonly next: number
|
||||
}
|
||||
| { readonly type: "busy" }
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.idle"
|
||||
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 metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "tui.prompt.append"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly text: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "tui.command.execute"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly command:
|
||||
| "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"
|
||||
| string
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "tui.toast.show"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly title?: string
|
||||
readonly message: string
|
||||
readonly variant: "info" | "success" | "warning" | "error"
|
||||
readonly duration?: number | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "tui.session.select"
|
||||
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 metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "installation.updated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly version: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "installation.update-available"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly version: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "vcs.branch.updated"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly branch?: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "mcp.status.changed"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: { readonly server: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "permission.asked"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly permission: string
|
||||
readonly patterns: ReadonlyArray<string>
|
||||
readonly metadata: { readonly [x: string]: unknown }
|
||||
readonly always: ReadonlyArray<string>
|
||||
readonly tool?: { readonly messageID: string; readonly callID: string } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "permission.replied"
|
||||
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 requestID: string
|
||||
readonly reply: "once" | "always" | "reject"
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "question.asked"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly questions: ReadonlyArray<{
|
||||
readonly question: string
|
||||
readonly header: string
|
||||
readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }>
|
||||
readonly multiple?: boolean | undefined
|
||||
readonly custom?: boolean | undefined
|
||||
}>
|
||||
readonly tool?: { readonly messageID: string; readonly callID: string } | undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "question.replied"
|
||||
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 requestID: string
|
||||
readonly answers: ReadonlyArray<ReadonlyArray<string>>
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "question.rejected"
|
||||
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 requestID: string }
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown }
|
||||
readonly type: "session.error"
|
||||
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string }
|
||||
readonly data: {
|
||||
readonly sessionID?: string | undefined
|
||||
readonly error?:
|
||||
| {
|
||||
readonly name: "ProviderAuthError"
|
||||
readonly data: { readonly providerID: string; readonly message: string }
|
||||
}
|
||||
| {
|
||||
readonly name: "UnknownError"
|
||||
readonly data: { readonly message: string; readonly ref?: string | undefined }
|
||||
}
|
||||
| { readonly name: "MessageOutputLengthError"; readonly data: {} }
|
||||
| { readonly name: "MessageAbortedError"; readonly data: { readonly message: string } }
|
||||
| {
|
||||
readonly name: "StructuredOutputError"
|
||||
readonly data: { readonly message: string; readonly retries: number }
|
||||
}
|
||||
| {
|
||||
readonly name: "ContextOverflowError"
|
||||
readonly data: { readonly message: string; readonly responseBody?: string | undefined }
|
||||
}
|
||||
| { readonly name: "ContentFilterError"; readonly data: { readonly message: string } }
|
||||
| {
|
||||
readonly name: "APIError"
|
||||
readonly data: {
|
||||
readonly message: string
|
||||
readonly statusCode?: number | undefined
|
||||
readonly isRetryable: boolean
|
||||
readonly responseHeaders?: { readonly [x: string]: string } | undefined
|
||||
readonly responseBody?: string | undefined
|
||||
readonly metadata?: { readonly [x: string]: string } | undefined
|
||||
}
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: unknown } | undefined
|
||||
@@ -4160,6 +5281,10 @@ export type EventSubscribeOutput =
|
||||
readonly data: {}
|
||||
}
|
||||
|
||||
export type EventChangesOutput =
|
||||
| { readonly type: "log.hint"; readonly aggregateID: string; readonly seq: number }
|
||||
| { readonly type: "log.sweep_required" }
|
||||
|
||||
export type PtyListInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./generated/index"
|
||||
export type { Transport } from "../effect/service.js"
|
||||
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types"
|
||||
export type OpenCodeClient = ReturnType<typeof import("./generated/client").make>
|
||||
@@ -1,7 +1,9 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { DateTime, Effect, Stream } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
|
||||
import { AbsolutePath, Agent, Event, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect/index"
|
||||
|
||||
const synced = { type: "log.synced" as const, aggregateID: "ses_test", seq: Event.Seq.make(1) }
|
||||
|
||||
test("session.get returns the decoded Effect projection", async () => {
|
||||
const httpClient = HttpClient.make((request) =>
|
||||
@@ -60,32 +62,20 @@ test("event.subscribe terminates on Effect protocol decode failures", async () =
|
||||
})
|
||||
|
||||
test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
const historyQueries: Array<Record<string, string>> = []
|
||||
let historyPage = 0
|
||||
const logQueries: Array<Record<string, string>> = []
|
||||
const httpClient = HttpClient.make((request) => {
|
||||
const url = request.url
|
||||
if (url.includes("/event")) {
|
||||
if (url.includes("/log")) {
|
||||
logQueries.push(Object.fromEntries(request.urlParams.params))
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
|
||||
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (url.includes("/history")) {
|
||||
historyPage++
|
||||
historyQueries.push(Object.fromEntries(request.urlParams.params))
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json(
|
||||
historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (url.includes("/prompt")) {
|
||||
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
|
||||
}
|
||||
@@ -97,7 +87,10 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
}
|
||||
if (url.endsWith("/api/session/active")) {
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(request, Response.json({ data: { ses_test: { type: "running" } } })),
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } }),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (request.method === "POST" && url.endsWith("/api/session")) {
|
||||
@@ -107,7 +100,10 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
|
||||
}
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(request, Response.json({ data: [session.data], cursor: { next: "next" } })),
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json({ data: [session.data], watermarks: { ses_test: 3 }, cursor: { next: "next" } }),
|
||||
),
|
||||
)
|
||||
})
|
||||
const result = await Effect.gen(function* () {
|
||||
@@ -130,31 +126,20 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
yield* client.session.compact({ sessionID: Session.ID.make("ses_test") })
|
||||
yield* client.session.wait({ sessionID: Session.ID.make("ses_test") })
|
||||
const context = yield* client.session.context({ sessionID: Session.ID.make("ses_test") })
|
||||
const history = yield* client.session.history({
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
after: 0,
|
||||
limit: 1,
|
||||
})
|
||||
const historyNext = history.hasMore
|
||||
? yield* client.session.history({
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
after: history.data.at(-1)?.durable?.seq,
|
||||
limit: 2,
|
||||
})
|
||||
: undefined
|
||||
const events = yield* client.session
|
||||
.events({ sessionID: Session.ID.make("ses_test"), after: 0 })
|
||||
const log = yield* client.session
|
||||
.log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) })
|
||||
.pipe(Stream.runCollect)
|
||||
yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") })
|
||||
const message = yield* client.session.message({
|
||||
sessionID: Session.ID.make("ses_test"),
|
||||
messageID: SessionMessage.ID.make("msg_model"),
|
||||
})
|
||||
return { page, active, created, admitted, context, history, historyNext, events, message }
|
||||
return { page, active, created, admitted, context, log, message }
|
||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||
|
||||
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
|
||||
expect(result.active).toEqual({ ses_test: { type: "running" } })
|
||||
expect(result.active).toEqual({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } })
|
||||
expect(result.page.watermarks).toEqual({ ses_test: 3 })
|
||||
expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
|
||||
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
|
||||
expect(result.created.id).toBe("ses_test")
|
||||
@@ -162,16 +147,17 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
|
||||
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
|
||||
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
|
||||
expect(result.context).toEqual([])
|
||||
expect(DateTime.toEpochMillis(result.history.data[0].data.timestamp)).toBe(1_717_171_717_000)
|
||||
expect(result.history).toEqual(expect.objectContaining({ hasMore: true }))
|
||||
expect(result.historyNext).toEqual({ data: [], hasMore: false })
|
||||
expect(historyQueries[0]).toEqual({ limit: "1", after: "0" })
|
||||
expect(historyQueries[1]).toEqual({ limit: "2", after: "1" })
|
||||
expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
|
||||
expect(logQueries[0]).toEqual({ after: "0" })
|
||||
const logged = Array.from(result.log)
|
||||
expect(logged.map((item) => item.type)).toEqual(["session.next.model.switched", "log.synced"])
|
||||
expect(logged[0]?.type === "session.next.model.switched" && DateTime.toEpochMillis(logged[0].data.timestamp)).toBe(
|
||||
1_717_171_717_000,
|
||||
)
|
||||
expect(logged.at(-1)).toEqual(synced)
|
||||
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
|
||||
})
|
||||
|
||||
test("session.history retains the typed SessionNotFoundError", async () => {
|
||||
test("session.log retains the typed SessionNotFoundError", async () => {
|
||||
const httpClient = HttpClient.make((request) =>
|
||||
Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
@@ -185,11 +171,7 @@ test("session.history retains the typed SessionNotFoundError", async () => {
|
||||
)
|
||||
const error = await Effect.gen(function* () {
|
||||
const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||
return yield* client.session
|
||||
.history({
|
||||
sessionID: Session.ID.make("ses_missing"),
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
return yield* client.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip)
|
||||
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
|
||||
|
||||
expect(error._tag).toBe("SessionNotFoundError")
|
||||
|
||||
@@ -12,7 +12,7 @@ const server = resolve(import.meta.dir, "../../server")
|
||||
|
||||
describe("public import boundaries", () => {
|
||||
test("isolates each public entrypoint", async () => {
|
||||
const root = await bundleInputs("@opencode-ai/client", "browser")
|
||||
const root = await bundleInputs("@opencode-ai/client/promise", "browser")
|
||||
|
||||
expect(within(root, effect)).toEqual([])
|
||||
expect(within(root, schema)).toEqual([])
|
||||
@@ -20,7 +20,9 @@ describe("public import boundaries", () => {
|
||||
expect(within(root, core)).toEqual([])
|
||||
expect(within(root, server)).toEqual([])
|
||||
|
||||
const network = await bundleInputs("@opencode-ai/client/effect", "browser")
|
||||
// The effect entry includes local service lifecycle (node spawn/fs), so it
|
||||
// bundles for bun; the boundary assertions below are what matter.
|
||||
const network = await bundleInputs("@opencode-ai/client/effect", "bun")
|
||||
|
||||
expect(within(network, effect).length).toBeGreaterThan(0)
|
||||
expect(within(network, schema).length).toBeGreaterThan(0)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src"
|
||||
import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src/promise/index"
|
||||
|
||||
test("exposes every standard HTTP API group", () => {
|
||||
const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
|
||||
@@ -8,12 +8,14 @@ test("exposes every standard HTTP API group", () => {
|
||||
"health",
|
||||
"location",
|
||||
"agent",
|
||||
"plugin",
|
||||
"session",
|
||||
"message",
|
||||
"model",
|
||||
"generate",
|
||||
"provider",
|
||||
"integration",
|
||||
"server.mcp",
|
||||
"credential",
|
||||
"project",
|
||||
"permission",
|
||||
@@ -172,7 +174,6 @@ test("event.subscribe terminates on malformed Promise SSE data", async () => {
|
||||
|
||||
test("session methods use the public HTTP contract", async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||
let historyPage = 0
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async (input, init) => {
|
||||
@@ -183,23 +184,23 @@ test("session methods use the public HTTP contract", async () => {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
})
|
||||
}
|
||||
if (url.includes("/history")) {
|
||||
historyPage++
|
||||
return Response.json(
|
||||
historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
|
||||
)
|
||||
if (url.includes("/log")) {
|
||||
return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\ndata: ${JSON.stringify(synced)}\n\n`, {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
})
|
||||
}
|
||||
if (url.includes("/prompt")) return Response.json(admission)
|
||||
if (url.includes("/context")) return Response.json({ data: [] })
|
||||
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
|
||||
if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } })
|
||||
if (url.endsWith("/api/session/active"))
|
||||
return Response.json({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } })
|
||||
if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
|
||||
if (init?.method === "POST") return new Response(null, { status: 204 })
|
||||
return Response.json({ data: [session.data], cursor: { next: "next" } })
|
||||
},
|
||||
})
|
||||
|
||||
const page = await client.session.list({ limit: 10, order: "desc" })
|
||||
const page = await client.session.list({ limit: 10, order: "desc", parentID: null })
|
||||
const active = await client.session.active()
|
||||
const created = await client.session.create({ location: { directory: "/tmp/project" } })
|
||||
await client.session.switchAgent({ sessionID: "ses_test", agent: "build" })
|
||||
@@ -215,27 +216,20 @@ test("session methods use the public HTTP contract", async () => {
|
||||
await client.session.compact({ sessionID: "ses_test" })
|
||||
await client.session.wait({ sessionID: "ses_test" })
|
||||
const context = await client.session.context({ sessionID: "ses_test" })
|
||||
const history = await client.session.history({ sessionID: "ses_test", after: 0, limit: 1 })
|
||||
const historyAfter = history.data.at(-1)?.durable?.seq
|
||||
const historyNext = history.hasMore
|
||||
? await client.session.history({ sessionID: "ses_test", after: historyAfter, limit: 2 })
|
||||
: undefined
|
||||
const events = []
|
||||
for await (const event of client.session.events({ sessionID: "ses_test", after: 0 })) events.push(event)
|
||||
const log = []
|
||||
for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item)
|
||||
await client.session.interrupt({ sessionID: "ses_test" })
|
||||
const message = await client.session.message({ sessionID: "ses_test", messageID: "msg_model" })
|
||||
|
||||
expect(page.cursor.next).toBe("next")
|
||||
expect(active).toEqual({ ses_test: { type: "running" } })
|
||||
expect(active).toEqual({ data: { ses_test: { type: "running" } }, watermarks: { ses_test: 3 } })
|
||||
expect(created.id).toBe("ses_test")
|
||||
expect(admitted.id).toBe("msg_test")
|
||||
expect(context).toEqual([])
|
||||
expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true })
|
||||
expect(historyNext).toEqual({ data: [], hasMore: false })
|
||||
expect(events).toEqual([modelSwitchedEvent])
|
||||
expect(log).toEqual([modelSwitchedEvent, synced])
|
||||
expect(message).toEqual(modelSwitchedMessage)
|
||||
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
|
||||
["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
|
||||
["GET", "http://localhost:3000/api/session?limit=10&order=desc&parentID=null"],
|
||||
["GET", "http://localhost:3000/api/session/active"],
|
||||
["POST", "http://localhost:3000/api/session"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/agent"],
|
||||
@@ -244,9 +238,7 @@ test("session methods use the public HTTP contract", async () => {
|
||||
["POST", "http://localhost:3000/api/session/ses_test/compact"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/wait"],
|
||||
["GET", "http://localhost:3000/api/session/ses_test/context"],
|
||||
["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"],
|
||||
["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"],
|
||||
["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
|
||||
["GET", "http://localhost:3000/api/session/ses_test/log?after=0"],
|
||||
["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
|
||||
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
|
||||
])
|
||||
@@ -273,7 +265,7 @@ test("middleware errors remain declared client errors", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("session.history decodes SessionNotFoundError", async () => {
|
||||
test("session.log decodes SessionNotFoundError", async () => {
|
||||
const client = OpenCode.make({
|
||||
baseUrl: "http://localhost:3000",
|
||||
fetch: async () =>
|
||||
@@ -284,7 +276,7 @@ test("session.history decodes SessionNotFoundError", async () => {
|
||||
})
|
||||
|
||||
try {
|
||||
await client.session.history({ sessionID: "ses_missing" })
|
||||
await client.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next()
|
||||
throw new Error("Expected request to fail")
|
||||
} catch (error) {
|
||||
expect(isSessionNotFoundError(error)).toBe(true)
|
||||
@@ -329,6 +321,8 @@ const modelSwitchedMessage = {
|
||||
model: { id: "claude", providerID: "anthropic" },
|
||||
}
|
||||
|
||||
const synced = { type: "log.synced", aggregateID: "ses_test", seq: 1 }
|
||||
|
||||
const modelSwitchedEvent = {
|
||||
id: "evt_model",
|
||||
type: "session.next.model.switched",
|
||||
|
||||
+222
-48
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "f14a9b18-8207-487e-a3d3-227e629ba9ad",
|
||||
"prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"],
|
||||
"id": "22e57fed-b9b8-4e94-a3b4-f94bece680a8",
|
||||
"prevIds": [
|
||||
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
@@ -60,6 +62,10 @@
|
||||
"name": "session_context_epoch",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_context_entry",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_input",
|
||||
"entityType": "tables"
|
||||
@@ -920,6 +926,56 @@
|
||||
"entityType": "columns",
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "key",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "value",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
@@ -1481,9 +1537,13 @@
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1492,9 +1552,13 @@
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["active_account_id"],
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
@@ -1503,9 +1567,13 @@
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"columns": ["aggregate_id"],
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"tableTo": "event_sequence",
|
||||
"columnsTo": ["aggregate_id"],
|
||||
"columnsTo": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1514,9 +1582,13 @@
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1525,9 +1597,13 @@
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1536,9 +1612,13 @@
|
||||
"table": "project_directory"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1547,9 +1627,13 @@
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"columns": [
|
||||
"message_id"
|
||||
],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1558,9 +1642,13 @@
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1569,9 +1657,28 @@
|
||||
"table": "session_context_epoch"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_context_entry_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1580,9 +1687,13 @@
|
||||
"table": "session_input"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1591,9 +1702,13 @@
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1602,9 +1717,13 @@
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1613,9 +1732,13 @@
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1624,133 +1747,184 @@
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"columns": [
|
||||
"email",
|
||||
"url"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id", "directory"],
|
||||
"columns": [
|
||||
"project_id",
|
||||
"directory"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_directory_pk",
|
||||
"entityType": "pks",
|
||||
"table": "project_directory"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"columns": [
|
||||
"session_id",
|
||||
"key"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_context_entry_pk",
|
||||
"entityType": "pks",
|
||||
"table": "session_context_entry"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id",
|
||||
"position"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["name"],
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "data_migration_pk",
|
||||
"table": "data_migration",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_state_pk",
|
||||
"table": "account_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_pk",
|
||||
"table": "account",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "credential_pk",
|
||||
"table": "credential",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["aggregate_id"],
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_sequence_pk",
|
||||
"table": "event_sequence",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_pk",
|
||||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_context_epoch_pk",
|
||||
"table": "session_context_epoch",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_input_pk",
|
||||
"table": "session_input",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_message_pk",
|
||||
"table": "session_message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
@@ -2068,4 +2242,4 @@
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ const layer = Layer.effect(
|
||||
? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } }
|
||||
: model.api
|
||||
const request = {
|
||||
settings: { ...provider.request.settings, ...model.request.settings },
|
||||
headers: { ...provider.request.headers, ...model.request.headers },
|
||||
body: { ...provider.request.body, ...model.request.body },
|
||||
variant: model.request.variant,
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
export * as CommandV2 from "./command"
|
||||
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { Context, Effect, Layer, Types } from "effect"
|
||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
import { State } from "./state"
|
||||
import { MCP } from "./mcp/index"
|
||||
import { EventV2 } from "./event"
|
||||
import { AppProcess } from "./process"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { Config } from "./config"
|
||||
import { Location } from "./location"
|
||||
import { ShellSelect } from "./shell/select"
|
||||
|
||||
export const Info = Command.Info
|
||||
export type Info = Command.Info
|
||||
export const Event = Command.Event
|
||||
|
||||
export type Evaluation = {
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
export type Data = {
|
||||
commands: Map<string, Types.DeepMutable<Info>>
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Command.NotFoundError", {
|
||||
command: Schema.String,
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class EvaluationError extends Schema.TaggedErrorClass<EvaluationError>()("Command.EvaluationError", {
|
||||
command: Schema.String,
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export type Draft = {
|
||||
list: () => readonly Info[]
|
||||
get: (name: string) => Info | undefined
|
||||
@@ -22,13 +44,22 @@ export type Draft = {
|
||||
export interface Interface extends State.Transformable<Draft> {
|
||||
readonly get: (name: string) => Effect.Effect<Info | undefined>
|
||||
readonly list: () => Effect.Effect<Info[]>
|
||||
readonly evaluate: (input: {
|
||||
readonly name: string
|
||||
readonly arguments?: string
|
||||
}) => Effect.Effect<Evaluation, NotFoundError | EvaluationError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Command") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.sync(() => {
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
const events = yield* EventV2.Service
|
||||
const processes = yield* AppProcess.Service
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const state = State.create<Data, Draft>({
|
||||
initial: () => ({ commands: new Map() }),
|
||||
draft: (draft) => ({
|
||||
@@ -44,19 +75,172 @@ const layer = Layer.effect(
|
||||
draft.commands.delete(name)
|
||||
},
|
||||
}),
|
||||
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
|
||||
})
|
||||
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
|
||||
const mcpCommands = Effect.fnUntraced(function* () {
|
||||
return (yield* mcp.prompts()).map((prompt) =>
|
||||
Info.make({
|
||||
name: mcpCommandName(prompt.server, prompt.name),
|
||||
template: "",
|
||||
description: prompt.description,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
reload: state.reload,
|
||||
transform: state.transform,
|
||||
get: Effect.fn("CommandV2.get")(function* (name) {
|
||||
return state.get().commands.get(name)
|
||||
const command = staticCommand(name)
|
||||
if (command) return command
|
||||
return (yield* mcpCommands()).find((command) => command.name === name)
|
||||
}),
|
||||
list: Effect.fn("CommandV2.list")(function* () {
|
||||
return Array.from(state.get().commands.values())
|
||||
const commands = Array.from(state.get().commands.values()) as Info[]
|
||||
const names = new Set(commands.map((command) => command.name))
|
||||
return [
|
||||
...commands,
|
||||
...(yield* mcpCommands()).filter((command) => !names.has(command.name)),
|
||||
]
|
||||
}),
|
||||
evaluate: Effect.fn("CommandV2.evaluate")(function* (input) {
|
||||
const command = staticCommand(input.name)
|
||||
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
|
||||
config,
|
||||
location,
|
||||
processes,
|
||||
})
|
||||
|
||||
const prompt = (yield* mcp.prompts()).find((prompt) => mcpCommandName(prompt.server, prompt.name) === input.name)
|
||||
if (!prompt) return yield* new NotFoundError({ command: input.name, message: `Command not found: ${input.name}` })
|
||||
const result = yield* mcp
|
||||
.prompt({
|
||||
server: prompt.server,
|
||||
name: prompt.name,
|
||||
args: Object.fromEntries(
|
||||
(prompt.arguments ?? []).map((argument, index) => [
|
||||
argument.name,
|
||||
parseArguments(input.arguments ?? "")[index] ?? "",
|
||||
]),
|
||||
),
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag(
|
||||
"MCP.NotFoundError",
|
||||
() =>
|
||||
Effect.fail(
|
||||
new EvaluationError({
|
||||
command: input.name,
|
||||
message: `MCP server could not be found while evaluating prompt: ${prompt.server}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (!result)
|
||||
return yield* new EvaluationError({
|
||||
command: input.name,
|
||||
message: `MCP prompt could not be evaluated: ${prompt.server}:${prompt.name}`,
|
||||
})
|
||||
return { text: result.messages.map((message) => promptMessageText(message.content)).join("\n").trim() }
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||
function evaluateTemplate(
|
||||
command: string,
|
||||
template: string,
|
||||
input: string,
|
||||
services: {
|
||||
readonly config: Config.Interface
|
||||
readonly location: Location.Info
|
||||
readonly processes: AppProcess.Interface
|
||||
},
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const expanded = evaluateArguments(template, input)
|
||||
return { text: yield* evaluateShell(command, expanded, services) }
|
||||
})
|
||||
}
|
||||
|
||||
function evaluateArguments(template: string, input: string) {
|
||||
const args = parseArguments(input)
|
||||
const placeholders = template.match(placeholderRegex) ?? []
|
||||
const last = Math.max(0, ...placeholders.map((item) => Number(item.slice(1))))
|
||||
const expanded = template.replaceAll(placeholderRegex, (_, index) => {
|
||||
const position = Number(index)
|
||||
const argIndex = position - 1
|
||||
if (argIndex >= args.length) return ""
|
||||
if (position === last) return args.slice(argIndex).join(" ")
|
||||
return args[argIndex]
|
||||
})
|
||||
const withArguments = expanded.replaceAll("$ARGUMENTS", input)
|
||||
if (placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim()) return `${withArguments}\n\n${input}`.trim()
|
||||
return withArguments.trim()
|
||||
}
|
||||
|
||||
const evaluateShell = Effect.fnUntraced(function* (
|
||||
command: string,
|
||||
text: string,
|
||||
services: {
|
||||
readonly config: Config.Interface
|
||||
readonly location: Location.Info
|
||||
readonly processes: AppProcess.Interface
|
||||
},
|
||||
) {
|
||||
const matches = Array.from(text.matchAll(shellRegex))
|
||||
if (matches.length === 0) return text
|
||||
const shell = ShellSelect.preferred(Config.latest(yield* services.config.entries(), "shell"))
|
||||
const outputs = yield* Effect.forEach(
|
||||
matches,
|
||||
(match) => {
|
||||
const source = match[1] ?? ""
|
||||
return services.processes
|
||||
.run(ChildProcess.make(shell, ShellSelect.args(shell, source), { cwd: services.location.directory, stdin: "ignore" }), {
|
||||
combineOutput: true,
|
||||
})
|
||||
.pipe(
|
||||
Effect.map((result) => (result.output ?? Buffer.concat([result.stdout, result.stderr])).toString("utf8")),
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new EvaluationError({ command, message: `Shell interpolation failed for ${JSON.stringify(source)}: ${error.message}` }),
|
||||
),
|
||||
)
|
||||
},
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const iterator = outputs[Symbol.iterator]()
|
||||
return text.replace(shellRegex, () => iterator.next().value ?? "")
|
||||
})
|
||||
|
||||
function parseArguments(input: string) {
|
||||
return (input.match(argsRegex) ?? []).map((arg) => arg.replace(quoteTrimRegex, ""))
|
||||
}
|
||||
|
||||
function promptMessageText(content: unknown) {
|
||||
if (typeof content === "string") return content
|
||||
if (!content || typeof content !== "object") return ""
|
||||
if (!("type" in content) || content.type !== "text") return ""
|
||||
if (!("text" in content) || typeof content.text !== "string") return ""
|
||||
return content.text
|
||||
}
|
||||
|
||||
function mcpCommandName(server: string, prompt: string) {
|
||||
return `${sanitize(server)}:${sanitize(prompt)}`
|
||||
}
|
||||
|
||||
function sanitize(value: string) {
|
||||
return value.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
}
|
||||
|
||||
const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
|
||||
const placeholderRegex = /\$(\d+)/g
|
||||
const quoteTrimRegex = /^["']|["']$/g
|
||||
const shellRegex = /!`([^`]+)`/g
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [MCP.node, EventV2.node, AppProcess.node, Config.node, Location.node],
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ export class Timeout extends Schema.Class<Timeout>("ConfigV2.MCP.Timeout")({
|
||||
description: "Maximum time in milliseconds to establish and initialize the MCP server.",
|
||||
}),
|
||||
request: PositiveInt.pipe(Schema.optional).annotate({
|
||||
description: "Maximum time in milliseconds to wait for each MCP request after initialization.",
|
||||
description: "Maximum time in milliseconds to wait for MCP catalog/list requests after initialization.",
|
||||
}),
|
||||
}) {}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { define } from "../../plugin/internal"
|
||||
import { Effect } from "effect"
|
||||
import { Config } from "../../config"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
export const Plugin = define({
|
||||
id: "config-provider",
|
||||
@@ -54,6 +53,7 @@ export const Plugin = define({
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
if (item.api !== undefined) provider.api = { ...item.api }
|
||||
if (item.request !== undefined) {
|
||||
Object.assign(provider.request.settings, item.request.settings)
|
||||
Object.assign(provider.request.headers, item.request.headers)
|
||||
Object.assign(provider.request.body, item.request.body)
|
||||
}
|
||||
@@ -71,6 +71,7 @@ export const Plugin = define({
|
||||
}
|
||||
}
|
||||
if (config.request !== undefined) {
|
||||
Object.assign(model.request.settings, config.request.settings)
|
||||
Object.assign(model.request.headers, config.request.headers)
|
||||
Object.assign(model.request.body, config.request.body)
|
||||
if (config.request.variant !== undefined) model.request.variant = config.request.variant
|
||||
@@ -81,11 +82,13 @@ export const Plugin = define({
|
||||
if (!existing) {
|
||||
existing = {
|
||||
id: variant.id,
|
||||
settings: {},
|
||||
headers: {},
|
||||
body: {},
|
||||
}
|
||||
model.variants.push(existing)
|
||||
}
|
||||
Object.assign(existing.settings, variant.settings)
|
||||
Object.assign(existing.headers, variant.headers)
|
||||
Object.assign(existing.body, variant.body)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ProviderV2 } from "../provider"
|
||||
import { ModelV2 } from "../model"
|
||||
|
||||
export class Request extends Schema.Class<Request>("ConfigV2.Provider.Request")({
|
||||
settings: ProviderV2.Settings.pipe(Schema.optional),
|
||||
headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
+1
@@ -40,5 +40,6 @@ export const migrations = (
|
||||
import("./migration/20260622142730_simplify_session_context_epoch"),
|
||||
import("./migration/20260622170816_reset_v2_session_state"),
|
||||
import("./migration/20260622202450_simplify_session_input"),
|
||||
import("./migration/20260702134641_add_session_context_entry"),
|
||||
])
|
||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Effect } from "effect"
|
||||
import type { DatabaseMigration } from "../migration"
|
||||
|
||||
export default {
|
||||
id: "20260702134641_add_session_context_entry",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_context_entry\` (
|
||||
\`session_id\` text NOT NULL,
|
||||
\`key\` text NOT NULL,
|
||||
\`value\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`session_context_entry_pk\` PRIMARY KEY(\`session_id\`, \`key\`),
|
||||
CONSTRAINT \`fk_session_context_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
})
|
||||
},
|
||||
} satisfies DatabaseMigration.Migration
|
||||
@@ -154,6 +154,17 @@ export default {
|
||||
CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_context_entry\` (
|
||||
\`session_id\` text NOT NULL,
|
||||
\`key\` text NOT NULL,
|
||||
\`value\` text NOT NULL,
|
||||
\`time_created\` integer NOT NULL,
|
||||
\`time_updated\` integer NOT NULL,
|
||||
CONSTRAINT \`session_context_entry_pk\` PRIMARY KEY(\`session_id\`, \`key\`),
|
||||
CONSTRAINT \`fk_session_context_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
yield* tx.run(`
|
||||
CREATE TABLE \`session_input\` (
|
||||
\`id\` text PRIMARY KEY,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/
|
||||
+224
-99
@@ -3,7 +3,8 @@ export * as EventV2 from "./event"
|
||||
import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||
import { and, asc, eq, gt, inArray, sql } from "drizzle-orm"
|
||||
import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||
import { and, asc, eq, gt, inArray, lte, sql } from "drizzle-orm"
|
||||
import { Database } from "./database/database"
|
||||
import { EventSequenceTable, EventTable } from "./event/sql"
|
||||
import { Location } from "./location"
|
||||
@@ -13,6 +14,10 @@ import { Durable } from "@opencode-ai/schema/durable-event-manifest"
|
||||
|
||||
export const ID = Event.ID
|
||||
export type ID = import("@opencode-ai/schema/event").ID
|
||||
export const Seq = Event.Seq
|
||||
export type Seq = import("@opencode-ai/schema/event").Seq
|
||||
export const Version = Event.Version
|
||||
export type Version = import("@opencode-ai/schema/event").Version
|
||||
export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
|
||||
|
||||
export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
|
||||
@@ -63,6 +68,12 @@ export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDur
|
||||
},
|
||||
) {}
|
||||
|
||||
const envelope = (aggregateID: string, seq: number, version: number) => ({
|
||||
aggregateID,
|
||||
seq: Seq.make(seq),
|
||||
version: Version.make(version),
|
||||
})
|
||||
|
||||
const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
||||
const definition = Durable.get(event.type)
|
||||
if (!definition?.durable) {
|
||||
@@ -71,58 +82,11 @@ const decodeSerializedEvent = (event: SerializedEvent): Payload => {
|
||||
return {
|
||||
id: event.id,
|
||||
type: definition.type,
|
||||
durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
|
||||
durable: envelope(event.aggregateID, event.seq, definition.durable.version),
|
||||
data: Schema.decodeUnknownSync(definition.data)(event.data),
|
||||
}
|
||||
}
|
||||
|
||||
export const readAggregate = Effect.fn("EventV2.readAggregate")(function* <A>(
|
||||
db: Database.Interface["db"],
|
||||
input: {
|
||||
readonly aggregateID: string
|
||||
readonly after?: number
|
||||
readonly limit: number
|
||||
readonly manifest: {
|
||||
readonly definitions: ReadonlyMap<string, Definition>
|
||||
readonly schema: Schema.Decoder<A, never>
|
||||
}
|
||||
},
|
||||
) {
|
||||
const after = input.after ?? -1
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(
|
||||
and(
|
||||
eq(EventTable.aggregate_id, input.aggregateID),
|
||||
gt(EventTable.seq, after),
|
||||
inArray(EventTable.type, Array.from(input.manifest.definitions.keys())),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(EventTable.seq))
|
||||
.limit(input.limit + 1)
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
const page = rows.slice(0, input.limit)
|
||||
const decode = Schema.decodeUnknownSync(input.manifest.schema)
|
||||
const events = page.map((event) =>
|
||||
decode({
|
||||
id: event.id,
|
||||
type: input.manifest.definitions.get(event.type)?.type ?? event.type,
|
||||
durable: {
|
||||
aggregateID: event.aggregate_id,
|
||||
seq: event.seq,
|
||||
version: input.manifest.definitions.get(event.type)?.durable?.version,
|
||||
},
|
||||
data: event.data,
|
||||
}),
|
||||
)
|
||||
return {
|
||||
events,
|
||||
hasMore: rows.length > input.limit,
|
||||
}
|
||||
})
|
||||
|
||||
export class SubscriberOverflowError extends Schema.TaggedErrorClass<SubscriberOverflowError>()(
|
||||
"EventV2.SubscriberOverflow",
|
||||
{ capacity: Schema.Int },
|
||||
@@ -139,6 +103,11 @@ export interface PublishOptions {
|
||||
readonly commit?: (seq: number) => Effect.Effect<void>
|
||||
}
|
||||
|
||||
/** Marker/event union emitted by `log`. */
|
||||
export type LogItem = Payload | EventLog.Synced
|
||||
|
||||
export const isSynced = (item: LogItem): item is EventLog.Synced => item.type === "log.synced"
|
||||
|
||||
export interface Interface {
|
||||
readonly publish: <D extends Definition>(
|
||||
definition: D,
|
||||
@@ -146,8 +115,31 @@ export interface Interface {
|
||||
options?: PublishOptions,
|
||||
) => Effect.Effect<Payload<D>>
|
||||
readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
|
||||
readonly all: () => Stream.Stream<Payload>
|
||||
readonly durable: (input: { readonly aggregateID: string; readonly after?: number }) => Stream.Stream<Payload>
|
||||
/**
|
||||
* Volatile live channel: every event published from now on, nothing before,
|
||||
* nothing across a disconnect. The only channel that carries non-durable
|
||||
* events; consumers that need reliability combine `changes` with `log`.
|
||||
*/
|
||||
readonly live: () => Stream.Stream<Payload>
|
||||
/**
|
||||
* Durable, ordered, gap-free per-aggregate log read. `follow: false`
|
||||
* completes at the end of the log; `follow: true` replays then transitions
|
||||
* to live. Both modes emit one `Synced` marker at the captured replay
|
||||
* watermark.
|
||||
*/
|
||||
readonly log: (input: {
|
||||
readonly aggregateID: string
|
||||
readonly after?: number
|
||||
readonly follow?: boolean
|
||||
}) => Stream.Stream<LogItem>
|
||||
/**
|
||||
* Coalescing hint channel: latest committed seq per aggregate, never a
|
||||
* delivery guarantee. Emits `SweepRequired` first on every subscribe and
|
||||
* whenever per-key retention is exceeded. Never fails under backpressure.
|
||||
*/
|
||||
readonly changes: () => Stream.Stream<EventLog.Change>
|
||||
/** Latest committed seq per aggregate. Aggregates without events are absent. */
|
||||
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Seq>>
|
||||
/** @deprecated Use `all()` and consume the returned stream. */
|
||||
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
|
||||
readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
|
||||
@@ -165,7 +157,7 @@ export interface Interface {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
|
||||
|
||||
export const allBounded = (events: Interface, capacity: number) =>
|
||||
export const liveBounded = (events: Interface, capacity: number) =>
|
||||
Effect.gen(function* () {
|
||||
const queue = yield* Queue.dropping<Payload, SubscriberOverflowError>(capacity)
|
||||
const unsubscribe = yield* events.listen((event) =>
|
||||
@@ -181,6 +173,13 @@ export const allBounded = (events: Interface, capacity: number) =>
|
||||
|
||||
export interface LayerOptions {
|
||||
readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
|
||||
/**
|
||||
* Maximum distinct aggregates buffered per changes subscriber before the
|
||||
* buffer is abandoned and the subscriber is told to sweep.
|
||||
*/
|
||||
readonly changesKeyCapacity?: number
|
||||
/** Maximum durable rows read per page while replaying or tailing an aggregate log. */
|
||||
readonly logReadPageSize?: number
|
||||
}
|
||||
|
||||
export const layerWith = (options?: LayerOptions) =>
|
||||
@@ -188,14 +187,21 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const pubsub = {
|
||||
all: yield* PubSub.unbounded<Payload>(),
|
||||
live: yield* PubSub.unbounded<Payload>(),
|
||||
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
|
||||
typed: new Map<string, PubSub.PubSub<Payload>>(),
|
||||
}
|
||||
const projectors = new Map<string, Subscriber[]>()
|
||||
// TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
|
||||
const listeners = new Array<Subscriber>()
|
||||
const changesKeyCapacity = options?.changesKeyCapacity ?? 4096
|
||||
const changesSubscribers = new Set<{
|
||||
readonly hints: Map<string, number>
|
||||
sweepRequired: boolean
|
||||
readonly wake: PubSub.PubSub<void>
|
||||
}>()
|
||||
const { db } = yield* Database.Service
|
||||
const logReadPageSize = options?.logReadPageSize ?? 512
|
||||
|
||||
const getOrCreate = (definition: Definition) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -208,13 +214,16 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.gen(function* () {
|
||||
yield* PubSub.shutdown(pubsub.all)
|
||||
yield* PubSub.shutdown(pubsub.live)
|
||||
yield* Effect.forEach(
|
||||
pubsub.durable.values(),
|
||||
(pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
|
||||
{ discard: true },
|
||||
)
|
||||
yield* Effect.forEach(pubsub.typed.values(), PubSub.shutdown, { discard: true })
|
||||
yield* Effect.forEach(changesSubscribers, (subscriber) => PubSub.shutdown(subscriber.wake), {
|
||||
discard: true,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -373,6 +382,27 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
(wake) => PubSub.publish(wake, undefined),
|
||||
{ discard: true },
|
||||
)
|
||||
yield* Effect.forEach(
|
||||
changesSubscribers,
|
||||
(subscriber) =>
|
||||
Effect.sync(() => {
|
||||
// Coalesce to the latest seq per aggregate. Overflowing key
|
||||
// cardinality abandons the buffer instead of dropping hints silently.
|
||||
if (
|
||||
subscriber.hints.size >= changesKeyCapacity &&
|
||||
!subscriber.hints.has(committed.aggregateID)
|
||||
) {
|
||||
subscriber.hints.clear()
|
||||
subscriber.sweepRequired = true
|
||||
} else if (!subscriber.sweepRequired) {
|
||||
subscriber.hints.set(
|
||||
committed.aggregateID,
|
||||
Math.max(subscriber.hints.get(committed.aggregateID) ?? -1, committed.seq),
|
||||
)
|
||||
}
|
||||
}).pipe(Effect.andThen(PubSub.publish(subscriber.wake, undefined)), Effect.asVoid),
|
||||
{ discard: true },
|
||||
)
|
||||
}
|
||||
return committed
|
||||
}),
|
||||
@@ -396,11 +426,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
if (committed) {
|
||||
event = {
|
||||
...event,
|
||||
durable: {
|
||||
aggregateID: committed.aggregateID,
|
||||
seq: committed.seq,
|
||||
version: definition.durable.version,
|
||||
},
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
}
|
||||
yield* notify(event as Payload, true)
|
||||
return event
|
||||
@@ -428,7 +454,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
)
|
||||
const typed = pubsub.typed.get(event.type)
|
||||
if (typed) yield* PubSub.publish(typed, event)
|
||||
yield* PubSub.publish(pubsub.all, event)
|
||||
yield* PubSub.publish(pubsub.live, event)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -480,11 +506,7 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
yield* notify(
|
||||
{
|
||||
...payload,
|
||||
durable: {
|
||||
aggregateID: committed.aggregateID,
|
||||
seq: committed.seq,
|
||||
version: definition.durable.version,
|
||||
},
|
||||
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
|
||||
},
|
||||
true,
|
||||
)
|
||||
@@ -552,30 +574,49 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
Stream.map((event) => event as Payload<D>),
|
||||
)
|
||||
|
||||
const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
|
||||
const streamLive = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.live)
|
||||
|
||||
const readAfter = (aggregateID: string, after: number) =>
|
||||
const readAfter = (
|
||||
aggregateID: string,
|
||||
after: number,
|
||||
input: { readonly through: number; readonly limit: number },
|
||||
) =>
|
||||
(options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe(
|
||||
Effect.andThen(
|
||||
db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after)))
|
||||
.orderBy(asc(EventTable.seq))
|
||||
.all(),
|
||||
Effect.suspend(() => {
|
||||
const query = db
|
||||
.select()
|
||||
.from(EventTable)
|
||||
.where(
|
||||
and(
|
||||
eq(EventTable.aggregate_id, aggregateID),
|
||||
gt(EventTable.seq, after),
|
||||
lte(EventTable.seq, input.through),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(EventTable.seq))
|
||||
return query.limit(input.limit).all()
|
||||
}),
|
||||
),
|
||||
Effect.orDie,
|
||||
Effect.map((rows) =>
|
||||
rows.map((event) =>
|
||||
decodeSerializedEvent({
|
||||
id: event.id,
|
||||
aggregateID: event.aggregate_id,
|
||||
seq: event.seq,
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
}),
|
||||
),
|
||||
),
|
||||
// Skip types missing from the durable manifest instead of failing the
|
||||
// read: the aggregate may hold events this process cannot decode. The
|
||||
// raw tail seq keeps cursors advancing across the resulting gaps.
|
||||
Effect.map((rows) => ({
|
||||
seq: rows.at(-1)?.seq,
|
||||
events: rows.flatMap((event) => {
|
||||
if (!Durable.get(event.type)?.durable) return []
|
||||
return [
|
||||
decodeSerializedEvent({
|
||||
id: event.id,
|
||||
aggregateID: event.aggregate_id,
|
||||
seq: event.seq,
|
||||
type: event.type,
|
||||
data: event.data,
|
||||
}),
|
||||
]
|
||||
}),
|
||||
})),
|
||||
)
|
||||
|
||||
const subscribeDurable = (aggregateID: string) =>
|
||||
@@ -598,27 +639,109 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
return subscription
|
||||
})
|
||||
|
||||
const durable = (input: { readonly aggregateID: string; readonly after?: number }): Stream.Stream<Payload> =>
|
||||
const log = (input: {
|
||||
readonly aggregateID: string
|
||||
readonly after?: number
|
||||
readonly follow?: boolean
|
||||
}): Stream.Stream<LogItem> =>
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const wakes = yield* subscribeDurable(input.aggregateID)
|
||||
let sequence = input.after ?? -1
|
||||
const read = Effect.suspend(() => readAfter(input.aggregateID, sequence)).pipe(
|
||||
Effect.tap((events) =>
|
||||
Effect.sync(() => {
|
||||
sequence = events.at(-1)?.durable?.seq ?? sequence
|
||||
}),
|
||||
),
|
||||
const readThrough = (through: number): Stream.Stream<Payload> =>
|
||||
Stream.paginate(sequence, (cursor) =>
|
||||
readAfter(input.aggregateID, cursor, { through, limit: logReadPageSize }).pipe(
|
||||
Effect.tap((page) =>
|
||||
Effect.sync(() => {
|
||||
sequence = page.seq ?? sequence
|
||||
}),
|
||||
),
|
||||
Effect.map(
|
||||
(page) =>
|
||||
[
|
||||
page.events,
|
||||
page.seq !== undefined && page.seq < through ? Option.some(page.seq) : Option.none<number>(),
|
||||
] as const,
|
||||
),
|
||||
),
|
||||
)
|
||||
// Subscribing before the historical read means events committed during
|
||||
// replay either appear in the read or arrive through a post-marker wake.
|
||||
const wakes = input.follow ? yield* subscribeDurable(input.aggregateID) : undefined
|
||||
const target = yield* latestSequence(db, input.aggregateID)
|
||||
const marker: EventLog.Synced = {
|
||||
type: "log.synced",
|
||||
aggregateID: input.aggregateID,
|
||||
...(target >= 0 ? { seq: Seq.make(target) } : {}),
|
||||
}
|
||||
const replay: Stream.Stream<LogItem> = readThrough(target).pipe(
|
||||
Stream.map((event): LogItem => event),
|
||||
Stream.concat(Stream.make(marker)),
|
||||
)
|
||||
const historical = yield* read
|
||||
const live = Stream.fromSubscription(wakes).pipe(
|
||||
Stream.mapEffect(() => read),
|
||||
Stream.flattenIterable,
|
||||
if (!wakes) return replay
|
||||
const live: Stream.Stream<LogItem> = Stream.fromSubscription(wakes).pipe(
|
||||
Stream.mapEffect(() => latestSequence(db, input.aggregateID)),
|
||||
Stream.filter((target) => target > sequence),
|
||||
Stream.flatMap((target) => readThrough(target)),
|
||||
Stream.map((event): LogItem => event),
|
||||
)
|
||||
return Stream.concat(Stream.fromIterable(historical), live)
|
||||
return Stream.concat(replay, live)
|
||||
}),
|
||||
)
|
||||
|
||||
const changes = (): Stream.Stream<EventLog.Change> =>
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const wake = yield* PubSub.sliding<void>(1)
|
||||
const subscription = yield* PubSub.subscribe(wake)
|
||||
const subscriber = { hints: new Map<string, number>(), sweepRequired: false, wake }
|
||||
yield* Effect.acquireRelease(
|
||||
Effect.sync(() => changesSubscribers.add(subscriber)),
|
||||
() =>
|
||||
Effect.sync(() => changesSubscribers.delete(subscriber)).pipe(
|
||||
Effect.andThen(PubSub.shutdown(wake)),
|
||||
Effect.asVoid,
|
||||
),
|
||||
)
|
||||
const drain = Effect.sync((): ReadonlyArray<EventLog.Change> => {
|
||||
if (subscriber.sweepRequired) {
|
||||
subscriber.sweepRequired = false
|
||||
subscriber.hints.clear()
|
||||
return [{ type: "log.sweep_required" }]
|
||||
}
|
||||
const hints = Array.from(
|
||||
subscriber.hints,
|
||||
([aggregateID, seq]): EventLog.Change => ({ type: "log.hint", aggregateID, seq: Seq.make(seq) }),
|
||||
)
|
||||
subscriber.hints.clear()
|
||||
return hints
|
||||
})
|
||||
// Hints missed while unsubscribed were never buffered, so every
|
||||
// (re)subscribe starts from the sweep contract.
|
||||
const initial: EventLog.Change = { type: "log.sweep_required" }
|
||||
return Stream.make(initial).pipe(
|
||||
Stream.concat(
|
||||
Stream.fromSubscription(subscription).pipe(
|
||||
Stream.mapEffect(() => drain),
|
||||
Stream.flattenIterable,
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const sequences = (aggregateIDs: ReadonlyArray<string>): Effect.Effect<ReadonlyMap<string, Seq>> => {
|
||||
if (aggregateIDs.length === 0) return Effect.succeed(new Map())
|
||||
return db
|
||||
.select({ aggregateID: EventSequenceTable.aggregate_id, seq: EventSequenceTable.seq })
|
||||
.from(EventSequenceTable)
|
||||
.where(inArray(EventSequenceTable.aggregate_id, Array.from(aggregateIDs)))
|
||||
.all()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((rows) => new Map(rows.map((row) => [row.aggregateID, Seq.make(row.seq)]))),
|
||||
)
|
||||
}
|
||||
|
||||
const listen = (listener: Subscriber): Effect.Effect<Unsubscribe> =>
|
||||
Effect.sync(() => {
|
||||
listeners.push(listener)
|
||||
@@ -638,8 +761,10 @@ export const layerWith = (options?: LayerOptions) =>
|
||||
return Service.of({
|
||||
publish,
|
||||
subscribe,
|
||||
all: streamAll,
|
||||
durable,
|
||||
live: streamLive,
|
||||
log,
|
||||
changes,
|
||||
sequences,
|
||||
listen,
|
||||
project,
|
||||
replay,
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
export * as Form from "./form"
|
||||
|
||||
import { Form } from "@opencode-ai/schema/form"
|
||||
import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect"
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
import { EventV2 } from "./event"
|
||||
|
||||
const RETENTION = Duration.minutes(10)
|
||||
|
||||
export const ID = Form.ID
|
||||
export type ID = typeof ID.Type
|
||||
|
||||
export const Info = Form.Info
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
export const Field = Form.Field
|
||||
export type Field = Form.Field
|
||||
|
||||
export const State = Form.State
|
||||
export type State = typeof State.Type
|
||||
|
||||
export const Answer = Form.Answer
|
||||
export type Answer = typeof Answer.Type
|
||||
|
||||
export const Reply = Form.Reply
|
||||
export type Reply = typeof Reply.Type
|
||||
|
||||
export const Event = Form.Event
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Form.NotFoundError", {
|
||||
id: ID,
|
||||
}) {
|
||||
override get message() {
|
||||
return `Form not found: ${this.id}`
|
||||
}
|
||||
}
|
||||
|
||||
export class AlreadySettledError extends Schema.TaggedErrorClass<AlreadySettledError>()("Form.AlreadySettledError", {
|
||||
id: ID,
|
||||
}) {
|
||||
override get message() {
|
||||
return `Form already settled: ${this.id}`
|
||||
}
|
||||
}
|
||||
|
||||
export class AlreadyExistsError extends Schema.TaggedErrorClass<AlreadyExistsError>()("Form.AlreadyExistsError", {
|
||||
id: ID,
|
||||
}) {
|
||||
override get message() {
|
||||
return `Form already exists: ${this.id}`
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidAnswerError extends Schema.TaggedErrorClass<InvalidAnswerError>()("Form.InvalidAnswerError", {
|
||||
id: ID,
|
||||
message: Schema.String,
|
||||
}) {}
|
||||
|
||||
export type CreateInput =
|
||||
| (Omit<Form.FormInfo, "id"> & { readonly id?: ID })
|
||||
| (Omit<Form.UrlInfo, "id"> & { readonly id?: ID })
|
||||
|
||||
export interface ReplyInput {
|
||||
readonly id: ID
|
||||
readonly answer: Answer
|
||||
}
|
||||
|
||||
export interface ListInput {
|
||||
readonly sessionID?: Form.FormInfo["sessionID"]
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly create: (input: CreateInput) => Effect.Effect<Info, AlreadyExistsError>
|
||||
readonly ask: (input: CreateInput) => Effect.Effect<State, AlreadyExistsError>
|
||||
readonly get: (id: ID) => Effect.Effect<Info, NotFoundError>
|
||||
readonly list: (input?: ListInput) => Effect.Effect<ReadonlyArray<Info>>
|
||||
readonly state: (id: ID) => Effect.Effect<State, NotFoundError>
|
||||
readonly reply: (input: ReplyInput) => Effect.Effect<void, AlreadySettledError | InvalidAnswerError | NotFoundError>
|
||||
readonly cancel: (id: ID) => Effect.Effect<void, AlreadySettledError | NotFoundError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Form") {}
|
||||
|
||||
interface Entry {
|
||||
readonly form: Info
|
||||
readonly state: State
|
||||
readonly deferred: Deferred.Deferred<State>
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const forms = yield* Cache.makeWith<ID, Entry>(() => Effect.die("Form cache must be used via set/getSuccess, never get"), {
|
||||
capacity: Number.MAX_SAFE_INTEGER,
|
||||
timeToLive: (exit) => (Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION),
|
||||
})
|
||||
|
||||
const find = Effect.fn("Form.find")(function* (id: ID) {
|
||||
return yield* Cache.getSuccess(forms, id).pipe(
|
||||
Effect.flatMap((entry) =>
|
||||
Option.match(entry, {
|
||||
onNone: () => Effect.fail(new NotFoundError({ id })),
|
||||
onSome: Effect.succeed,
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const create = Effect.fn("Form.create")((input: CreateInput) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const id = input.id ?? ID.create()
|
||||
const existing = yield* Cache.getSuccess(forms, id)
|
||||
if (Option.isSome(existing)) return yield* new AlreadyExistsError({ id })
|
||||
const base = {
|
||||
id,
|
||||
sessionID: input.sessionID,
|
||||
title: input.title,
|
||||
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
||||
}
|
||||
const form: Info =
|
||||
input.mode === "form" ? { ...base, mode: "form", fields: input.fields } : { ...base, mode: "url", url: input.url }
|
||||
const entry: Entry = {
|
||||
form,
|
||||
state: { status: "pending" },
|
||||
deferred: yield* Deferred.make<State>(),
|
||||
}
|
||||
yield* Cache.set(forms, id, entry)
|
||||
yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
|
||||
return form
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const ask = Effect.fn("Form.ask")((input: CreateInput) =>
|
||||
Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
const form = yield* create(input)
|
||||
const entry = yield* find(form.id).pipe(Effect.orDie)
|
||||
return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.onInterrupt(() => Effect.ignore(cancel(form.id))))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const get = Effect.fn("Form.get")(function* (id: ID) {
|
||||
return (yield* find(id)).form
|
||||
})
|
||||
|
||||
const list = Effect.fn("Form.list")(function* (input?: ListInput) {
|
||||
const entries = yield* Cache.values(forms)
|
||||
return Array.from(entries)
|
||||
.filter((entry) => entry.state.status === "pending")
|
||||
.filter((entry) => input?.sessionID === undefined || entry.form.sessionID === input.sessionID)
|
||||
.map((entry) => entry.form)
|
||||
})
|
||||
|
||||
const state = Effect.fn("Form.state")(function* (id: ID) {
|
||||
return (yield* find(id)).state
|
||||
})
|
||||
|
||||
const reply = Effect.fn("Form.reply")((input: ReplyInput) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const entry = yield* find(input.id)
|
||||
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id: input.id })
|
||||
const invalid = validateAnswer(entry.form, input.answer)
|
||||
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
|
||||
const next: State = { status: "answered", answer: input.answer }
|
||||
yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
|
||||
yield* Cache.set(forms, input.id, { ...entry, state: next })
|
||||
yield* Deferred.succeed(entry.deferred, next)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const cancel = Effect.fn("Form.cancel")((id: ID) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const entry = yield* find(id)
|
||||
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id })
|
||||
const next: State = { status: "cancelled" }
|
||||
yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID })
|
||||
yield* Cache.set(forms, id, { ...entry, state: next })
|
||||
yield* Deferred.succeed(entry.deferred, next)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Cache.values(forms).pipe(
|
||||
Effect.flatMap((entries) =>
|
||||
Effect.forEach(
|
||||
Array.from(entries).filter((entry) => entry.state.status === "pending"),
|
||||
(entry) => cancel(entry.form.id).pipe(Effect.ignore),
|
||||
{ discard: true },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return Service.of({ create, ask, get, list, state, reply, cancel })
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
|
||||
|
||||
function validateAnswer(form: Info, answer: Answer) {
|
||||
if (form.mode === "url") {
|
||||
if (Object.keys(answer).length === 0) return
|
||||
return "URL forms must be answered with an empty answer"
|
||||
}
|
||||
const fields = new Map(form.fields.map((field) => [field.key, field]))
|
||||
for (const key of Object.keys(answer)) {
|
||||
if (!fields.has(key)) return `Unknown form field: ${key}`
|
||||
}
|
||||
for (const field of form.fields) {
|
||||
const value = answer[field.key]
|
||||
if (value === undefined) {
|
||||
if (field.required && isActive(field, answer)) return `Missing required form field: ${field.key}`
|
||||
continue
|
||||
}
|
||||
const invalid = validateField(field, value)
|
||||
if (invalid) return invalid
|
||||
}
|
||||
}
|
||||
|
||||
function isActive(field: Form.Field, answer: Answer) {
|
||||
if (!field.when) return true
|
||||
const value = answer[field.when.key]
|
||||
if (field.when.op === "eq") return value === field.when.value
|
||||
return value !== field.when.value
|
||||
}
|
||||
|
||||
function validateField(field: Form.Field, value: Form.Value): string | undefined {
|
||||
if (field.type === "string") {
|
||||
if (typeof value !== "string") return `Expected string for form field: ${field.key}`
|
||||
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
|
||||
if (field.minLength !== undefined && value.length < field.minLength) return `Form field is too short: ${field.key}`
|
||||
if (field.maxLength !== undefined && value.length > field.maxLength) return `Form field is too long: ${field.key}`
|
||||
if (field.pattern !== undefined) {
|
||||
try {
|
||||
if (!new RegExp(field.pattern).test(value)) return `Form field does not match pattern: ${field.key}`
|
||||
} catch {
|
||||
return `Form field has invalid pattern: ${field.key}`
|
||||
}
|
||||
}
|
||||
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `Expected email for form field: ${field.key}`
|
||||
if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}`
|
||||
if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}`
|
||||
if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}`
|
||||
if (field.options && !field.custom && !field.options.some((option) => option.value === value)) {
|
||||
return `Invalid option for form field: ${field.key}`
|
||||
}
|
||||
return
|
||||
}
|
||||
if (field.type === "number" || field.type === "integer") {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return `Expected number for form field: ${field.key}`
|
||||
if (field.type === "integer" && !Number.isInteger(value)) return `Expected integer for form field: ${field.key}`
|
||||
if (field.minimum !== undefined && value < field.minimum) return `Form field is too small: ${field.key}`
|
||||
if (field.maximum !== undefined && value > field.maximum) return `Form field is too large: ${field.key}`
|
||||
return
|
||||
}
|
||||
if (field.type === "boolean") {
|
||||
if (typeof value !== "boolean") return `Expected boolean for form field: ${field.key}`
|
||||
return
|
||||
}
|
||||
if (field.type === "multiselect") {
|
||||
if (!isStringArray(value)) return `Expected string array for form field: ${field.key}`
|
||||
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
|
||||
if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}`
|
||||
if (field.maxItems !== undefined && value.length > field.maxItems) return `Too many selections for form field: ${field.key}`
|
||||
if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) {
|
||||
return `Invalid option for form field: ${field.key}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isStringArray(value: Form.Value): value is ReadonlyArray<string> {
|
||||
return Array.isArray(value) && value.every((item): item is string => typeof item === "string")
|
||||
}
|
||||
|
||||
function isUri(value: string) {
|
||||
try {
|
||||
new URL(value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isDate(value: string) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
|
||||
const date = new Date(`${value}T00:00:00.000Z`)
|
||||
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
|
||||
}
|
||||
|
||||
function isDateTime(value: string) {
|
||||
return !Number.isNaN(new Date(value).getTime())
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as InstructionContext from "./instruction-context"
|
||||
|
||||
import { Array, Effect, Layer, Schema } from "effect"
|
||||
import { Array, Context, Effect, Layer, Schema } from "effect"
|
||||
import { isAbsolute, join, relative, sep } from "path"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { Flag } from "./flag/flag"
|
||||
@@ -8,7 +8,6 @@ import { Global } from "./global"
|
||||
import { Location } from "./location"
|
||||
import { AbsolutePath } from "./schema"
|
||||
import { SystemContext } from "./system-context/index"
|
||||
import { SystemContextRegistry } from "./system-context/registry"
|
||||
import { makeLocationNode } from "./effect/app-node"
|
||||
|
||||
class File extends Schema.Class<File>("InstructionContext.File")({
|
||||
@@ -19,12 +18,18 @@ class File extends Schema.Class<File>("InstructionContext.File")({
|
||||
const Files = Schema.Array(File)
|
||||
const key = SystemContext.Key.make("core/instructions")
|
||||
|
||||
const layer = Layer.effectDiscard(
|
||||
export interface Interface {
|
||||
readonly load: () => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/InstructionContext") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
const location = yield* Location.Service
|
||||
const registry = yield* SystemContextRegistry.Service
|
||||
|
||||
const source = (value: ReadonlyArray<File> | SystemContext.Unavailable) =>
|
||||
SystemContext.make({
|
||||
@@ -71,28 +76,24 @@ const layer = Layer.effectDiscard(
|
||||
return files.filter((file): file is File => file !== undefined)
|
||||
})
|
||||
|
||||
yield* registry.register({
|
||||
key,
|
||||
load: observe().pipe(
|
||||
Effect.map((files) =>
|
||||
files === SystemContext.unavailable
|
||||
? source(files)
|
||||
: files.length === 0
|
||||
? SystemContext.empty
|
||||
: source(files),
|
||||
return Service.of({
|
||||
load: () =>
|
||||
observe().pipe(
|
||||
Effect.map((files) =>
|
||||
files === SystemContext.unavailable
|
||||
? source(files)
|
||||
: files.length === 0
|
||||
? SystemContext.empty
|
||||
: source(files),
|
||||
),
|
||||
Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))),
|
||||
Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))),
|
||||
),
|
||||
Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))),
|
||||
Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))),
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
name: "instruction-context",
|
||||
layer,
|
||||
deps: [FSUtil.node, Global.node, Location.node, SystemContextRegistry.node],
|
||||
})
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Global.node, Location.node] })
|
||||
|
||||
function render(files: ReadonlyArray<File>) {
|
||||
return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n")
|
||||
|
||||
@@ -10,6 +10,7 @@ import { FileMutation } from "./file-mutation"
|
||||
import { FileSystem } from "./filesystem"
|
||||
import { FileSystemSearch } from "./filesystem/search"
|
||||
import { Generate } from "./generate"
|
||||
import { Form } from "./form"
|
||||
import { Watcher } from "./filesystem/watcher"
|
||||
import { Image } from "./image"
|
||||
import { Integration } from "./integration"
|
||||
@@ -36,8 +37,10 @@ import { SessionTodo } from "./session/todo"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { SkillGuidance } from "./skill/guidance"
|
||||
import { Snapshot } from "./snapshot"
|
||||
import { InstructionContext } from "./instruction-context"
|
||||
import { SystemContextBuiltIns } from "./system-context/builtins"
|
||||
import { SystemContextRegistry } from "./system-context/registry"
|
||||
import { SessionContextEntry } from "./session/context-entry"
|
||||
import { SessionInstructions } from "./session/instructions"
|
||||
import { BuiltInTools } from "./tool/builtins"
|
||||
import { McpTool } from "./tool/mcp"
|
||||
import { ReadToolFileSystem } from "./tool/read-filesystem"
|
||||
@@ -67,8 +70,8 @@ export const locationServices = LayerNode.group([
|
||||
Pty.node,
|
||||
Shell.node,
|
||||
SkillV2.node,
|
||||
SystemContextRegistry.node,
|
||||
SystemContextBuiltIns.node,
|
||||
InstructionContext.node,
|
||||
LocationMutation.node,
|
||||
FileMutation.node,
|
||||
MCP.node,
|
||||
@@ -80,11 +83,14 @@ export const locationServices = LayerNode.group([
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
SessionTodo.node,
|
||||
SessionContextEntry.node,
|
||||
Form.node,
|
||||
QuestionV2.node,
|
||||
Generate.node,
|
||||
ReadToolFileSystem.node,
|
||||
BuiltInTools.node,
|
||||
McpTool.node,
|
||||
SessionInstructions.node,
|
||||
SessionRunnerModel.node,
|
||||
SessionCompaction.node,
|
||||
SessionTitle.node,
|
||||
|
||||
@@ -9,8 +9,12 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
||||
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import {
|
||||
CallToolResultSchema,
|
||||
GetPromptResultSchema,
|
||||
ListPromptsResultSchema,
|
||||
ListRootsRequestSchema,
|
||||
ListToolsResultSchema,
|
||||
PromptListChangedNotificationSchema,
|
||||
PromptSchema,
|
||||
type LoggingMessageNotification,
|
||||
LoggingMessageNotificationSchema,
|
||||
ToolListChangedNotificationSchema,
|
||||
@@ -30,6 +34,9 @@ type Transport = StdioClientTransport | StreamableHTTPClientTransport
|
||||
const TolerantListToolsResult = ListToolsResultSchema.extend({
|
||||
tools: ToolSchema.omit({ outputSchema: true }).array(),
|
||||
})
|
||||
const TolerantListPromptsResult = ListPromptsResultSchema.extend({
|
||||
prompts: PromptSchema.array(),
|
||||
})
|
||||
|
||||
export class NeedsAuthError extends Schema.TaggedErrorClass<NeedsAuthError>()("MCP.NeedsAuthError", {
|
||||
server: Schema.String,
|
||||
@@ -46,6 +53,25 @@ export interface ToolDefinition {
|
||||
readonly inputSchema: unknown
|
||||
}
|
||||
|
||||
export interface PromptDefinition {
|
||||
readonly name: string
|
||||
readonly description: string | undefined
|
||||
readonly arguments: ReadonlyArray<{
|
||||
readonly name: string
|
||||
readonly description: string | undefined
|
||||
readonly required: boolean | undefined
|
||||
}> | undefined
|
||||
}
|
||||
|
||||
export interface PromptMessage {
|
||||
readonly role: string
|
||||
readonly content: unknown
|
||||
}
|
||||
|
||||
export interface PromptResult {
|
||||
readonly messages: ReadonlyArray<PromptMessage>
|
||||
}
|
||||
|
||||
export type CallToolContent =
|
||||
| { readonly type: "text"; readonly text: string }
|
||||
| { readonly type: "media"; readonly data: string; readonly mimeType: string }
|
||||
@@ -68,6 +94,13 @@ export interface Connection {
|
||||
readonly instructions: string | undefined
|
||||
/** Lists the server's tools; returns [] when the server doesn't advertise tool support, fails on a transport error. */
|
||||
readonly tools: () => Effect.Effect<ToolDefinition[], Error>
|
||||
/** Lists the server's prompts; returns [] when the server doesn't advertise prompt support, fails on a transport error. */
|
||||
readonly prompts: () => Effect.Effect<PromptDefinition[], Error>
|
||||
/** Invokes a prompt on the server. Interruption aborts the in-flight request. */
|
||||
readonly prompt: (input: {
|
||||
readonly name: string
|
||||
readonly args?: Record<string, string>
|
||||
}) => Effect.Effect<PromptResult, Error>
|
||||
/** Invokes a tool on the server. Interruption aborts the in-flight request. */
|
||||
readonly callTool: (input: {
|
||||
readonly name: string
|
||||
@@ -78,6 +111,8 @@ export interface Connection {
|
||||
readonly onLog: (callback: (message: LogMessage) => void) => void
|
||||
/** Registers a callback fired when the server announces its tool list changed; no-op if unsupported. */
|
||||
readonly onToolsChanged: (callback: () => void) => void
|
||||
/** Registers a callback fired when the server announces its prompt list changed; no-op if unsupported. */
|
||||
readonly onPromptsChanged: (callback: () => void) => void
|
||||
}
|
||||
|
||||
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
|
||||
@@ -166,14 +201,56 @@ export const connect = Effect.fnUntraced(function* (
|
||||
inputSchema: tool.inputSchema,
|
||||
}))
|
||||
}),
|
||||
prompts: () =>
|
||||
Effect.gen(function* () {
|
||||
if (!client.getServerCapabilities()?.prompts) return []
|
||||
const prompts = yield* Effect.tryPromise({
|
||||
try: () =>
|
||||
paginate(
|
||||
async (cursor) => {
|
||||
const params = cursor === undefined ? undefined : { cursor }
|
||||
return client.request({ method: "prompts/list", params }, TolerantListPromptsResult, {
|
||||
timeout: requestTimeout,
|
||||
})
|
||||
},
|
||||
(result) => result.prompts,
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
Effect.tapError((error) => Effect.logWarning("failed to list MCP prompts", { server, error: error.message })),
|
||||
)
|
||||
return prompts.map((prompt) => ({
|
||||
name: prompt.name,
|
||||
description: prompt.description,
|
||||
arguments: prompt.arguments?.map((argument) => ({
|
||||
name: argument.name,
|
||||
description: argument.description,
|
||||
required: argument.required,
|
||||
})),
|
||||
}))
|
||||
}),
|
||||
prompt: (input) =>
|
||||
Effect.tryPromise({
|
||||
try: (signal) =>
|
||||
client.request(
|
||||
{ method: "prompts/get", params: { name: input.name, arguments: input.args ?? {} } },
|
||||
GetPromptResultSchema,
|
||||
{ signal },
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
Effect.map((result) => ({
|
||||
messages: result.messages.map((message) => ({ role: message.role, content: message.content })),
|
||||
})),
|
||||
),
|
||||
callTool: (input) =>
|
||||
Effect.tryPromise({
|
||||
try: (signal) =>
|
||||
client.callTool(
|
||||
{ name: input.name, arguments: input.args ?? {} },
|
||||
CallToolResultSchema,
|
||||
// The SDK only sends a progress token when onprogress is present, which enables timeout resets.
|
||||
{ signal, timeout: requestTimeout, resetTimeoutOnProgress: true, onprogress: () => {} },
|
||||
// Keep progress tokens available without imposing a client timeout on tool execution.
|
||||
{ signal, resetTimeoutOnProgress: true, onprogress: () => {} },
|
||||
),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
}).pipe(
|
||||
@@ -207,6 +284,10 @@ export const connect = Effect.fnUntraced(function* (
|
||||
if (!client.getServerCapabilities()?.tools?.listChanged) return
|
||||
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => callback())
|
||||
},
|
||||
onPromptsChanged: (callback) => {
|
||||
if (!client.getServerCapabilities()?.prompts?.listChanged) return
|
||||
client.setNotificationHandler(PromptListChangedNotificationSchema, async () => callback())
|
||||
},
|
||||
} satisfies Connection
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,40 @@ const Summary = Schema.Struct({
|
||||
})
|
||||
type Summary = typeof Summary.Type
|
||||
|
||||
const entries = (servers: ReadonlyArray<Summary>) =>
|
||||
servers.flatMap((server) => [
|
||||
` <server name="${server.server}">`,
|
||||
...server.instructions.split("\n").map((line) => ` ${line}`),
|
||||
" </server>",
|
||||
])
|
||||
|
||||
const render = (servers: ReadonlyArray<Summary>) =>
|
||||
[
|
||||
"<mcp_instructions>",
|
||||
...servers.flatMap((server) => [
|
||||
` <server name="${server.server}">`,
|
||||
...server.instructions.split("\n").map((line) => ` ${line}`),
|
||||
" </server>",
|
||||
]),
|
||||
"</mcp_instructions>",
|
||||
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
|
||||
|
||||
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
||||
const diff = SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(server) => server.server,
|
||||
(before, after) => before.instructions !== after.instructions,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
return [
|
||||
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
||||
render(current),
|
||||
].join("\n")
|
||||
return [
|
||||
...(diff.added.length === 0
|
||||
? []
|
||||
: ["New MCP server instructions are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
`Instructions for the following MCP servers are no longer available: ${diff.removed.map((server) => server.server).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: (agent: AgentV2.Selection) => Effect.Effect<SystemContext.SystemContext>
|
||||
@@ -50,7 +74,8 @@ export const layer = Layer.effect(
|
||||
return (
|
||||
owned.length === 0 ||
|
||||
owned.some(
|
||||
(tool) => PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
|
||||
(tool) =>
|
||||
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
|
||||
)
|
||||
)
|
||||
})
|
||||
@@ -61,11 +86,7 @@ export const layer = Layer.effect(
|
||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||
load: Effect.succeed(visible),
|
||||
baseline: render,
|
||||
update: (_previous, current) =>
|
||||
[
|
||||
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
||||
render(current),
|
||||
].join("\n"),
|
||||
update,
|
||||
removed: () => "MCP server instructions are no longer available.",
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as MCP from "./index"
|
||||
|
||||
import { Mcp } from "@opencode-ai/schema/mcp"
|
||||
import { McpEvent } from "@opencode-ai/schema/mcp-event"
|
||||
import { Command } from "@opencode-ai/schema/command"
|
||||
import { createHash } from "node:crypto"
|
||||
import { Cause, Context, Deferred, Effect, Exit, FiberSet, Layer, Schema, Scope, Stream } from "effect"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
@@ -139,6 +140,7 @@ type ServerEntry = {
|
||||
scope?: Scope.Closeable
|
||||
client?: MCPClient.Connection
|
||||
tools?: ReadonlyArray<Tool>
|
||||
prompts?: ReadonlyArray<Prompt>
|
||||
// Set when a remote server is registered as an OAuth integration; the credential lives in the global store.
|
||||
integrationID?: Integration.ID
|
||||
}
|
||||
@@ -309,6 +311,21 @@ export const layer = Layer.effect(
|
||||
const toTool = (server: ServerName, def: MCPClient.ToolDefinition) =>
|
||||
new Tool({ server, name: def.name, description: def.description, inputSchema: def.inputSchema })
|
||||
|
||||
const toPrompt = (server: ServerName, def: MCPClient.PromptDefinition) =>
|
||||
new Prompt({
|
||||
server,
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
arguments: def.arguments?.map(
|
||||
(argument) =>
|
||||
new PromptArgument({
|
||||
name: argument.name,
|
||||
description: argument.description,
|
||||
required: argument.required,
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
|
||||
connection.tools().pipe(
|
||||
Effect.map((defs) => {
|
||||
@@ -316,6 +333,17 @@ export const layer = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
const refreshPrompts = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
|
||||
connection.prompts().pipe(
|
||||
Effect.map((defs) => {
|
||||
entry.prompts = defs.map((def) => toPrompt(name, def))
|
||||
}),
|
||||
Effect.andThen(events.publish(Command.Event.Updated, {})),
|
||||
Effect.catch(() =>
|
||||
Effect.sync(() => (entry.prompts = [])).pipe(Effect.andThen(events.publish(Command.Event.Updated, {}))),
|
||||
),
|
||||
)
|
||||
|
||||
const watch = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) => {
|
||||
connection.onClose(() => {
|
||||
// A reconnect closes the previous scope, but the SDK may fire this onclose after the new
|
||||
@@ -323,8 +351,10 @@ export const layer = Layer.effect(
|
||||
if (entry.client !== connection) return
|
||||
entry.client = undefined
|
||||
entry.tools = undefined
|
||||
entry.prompts = undefined
|
||||
entry.status = { status: "failed", error: "Connection closed" }
|
||||
fork(events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore))
|
||||
fork(events.publish(Command.Event.Updated, {}).pipe(Effect.ignore))
|
||||
fork(events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore))
|
||||
})
|
||||
connection.onLog((message) => fork(serverLog(name, message).pipe(Effect.ignore)))
|
||||
@@ -336,6 +366,9 @@ export const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
})
|
||||
connection.onPromptsChanged(() => {
|
||||
fork(refreshPrompts(name, entry, connection).pipe(Effect.ignore))
|
||||
})
|
||||
}
|
||||
|
||||
const serverLog = (server: ServerName, message: MCPClient.LogMessage) => {
|
||||
@@ -364,13 +397,14 @@ export const layer = Layer.effect(
|
||||
// List tools as part of connect so a failure here marks the server failed rather than
|
||||
// leaving it connected with a silently empty tool list and no path to recover.
|
||||
const result = yield* MCPClient.connect(name, entry.config, location.directory, authProvider).pipe(
|
||||
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((defs) => ({ connection, defs })))),
|
||||
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((tools) => ({ connection, tools })))),
|
||||
Scope.provide(scope),
|
||||
Effect.exit,
|
||||
)
|
||||
if (Exit.isSuccess(result)) {
|
||||
entry.client = result.value.connection
|
||||
entry.tools = result.value.defs.map((def) => toTool(name, def))
|
||||
entry.tools = result.value.tools.map((def) => toTool(name, def))
|
||||
entry.prompts = []
|
||||
entry.status = { status: "connected" }
|
||||
watch(name, entry, result.value.connection)
|
||||
yield* Effect.logInfo("mcp connected", { server: name, tools: entry.tools.length })
|
||||
@@ -379,6 +413,7 @@ export const layer = Layer.effect(
|
||||
// stay invisible to the model.
|
||||
yield* events.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
|
||||
yield* events.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
|
||||
fork(refreshPrompts(name, entry, result.value.connection).pipe(Effect.ignore))
|
||||
return
|
||||
}
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
@@ -416,6 +451,8 @@ export const layer = Layer.effect(
|
||||
entry.scope = undefined
|
||||
entry.client = undefined
|
||||
entry.tools = undefined
|
||||
entry.prompts = undefined
|
||||
yield* events.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
|
||||
}
|
||||
yield* startServer(name, entry)
|
||||
})
|
||||
@@ -489,12 +526,25 @@ export const layer = Layer.effect(
|
||||
.toSorted((a, b) => a.server.localeCompare(b.server))
|
||||
}),
|
||||
prompts: Effect.fn("MCP.prompts")(function* () {
|
||||
yield* whenAllReady
|
||||
return []
|
||||
return Array.from(runtime.values())
|
||||
.flatMap((entry) => entry.prompts ?? [])
|
||||
.toSorted((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name))
|
||||
}),
|
||||
prompt: Effect.fn("MCP.prompt")(function* (input) {
|
||||
yield* gate(input.server)
|
||||
return undefined
|
||||
const target = yield* requireServer(input.server)
|
||||
yield* Deferred.await(target.entry.startup)
|
||||
if (!target.entry.client) return undefined
|
||||
const result = yield* target.entry.client
|
||||
.prompt({ name: input.name, args: input.args })
|
||||
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!result) return undefined
|
||||
return new PromptResult({
|
||||
server: target.name,
|
||||
name: input.name,
|
||||
messages: result.messages.map(
|
||||
(message) => new PromptMessage({ role: message.role, content: message.content }),
|
||||
),
|
||||
})
|
||||
}),
|
||||
resourceCatalog: Effect.fn("MCP.resourceCatalog")(function* () {
|
||||
yield* whenAllReady
|
||||
|
||||
@@ -26,8 +26,13 @@ export type Api = Model.Api
|
||||
export const Info = Model.Info
|
||||
export type Info = Model.Info
|
||||
|
||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
|
||||
export type MutableRequest = ProviderV2.MutableRequest & { variant?: string }
|
||||
export type MutableVariant = ProviderV2.MutableRequest & { id: VariantID }
|
||||
|
||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request" | "variants"> & {
|
||||
api: ProviderV2.MutableApi<Api>
|
||||
request: MutableRequest
|
||||
variants: MutableVariant[]
|
||||
}
|
||||
|
||||
export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Reference } from "./reference"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { State } from "./state"
|
||||
import { ToolRegistry } from "./tool/registry"
|
||||
import { ToolHooks } from "./tool/hooks"
|
||||
|
||||
export const ID = Plugin.ID
|
||||
export type ID = typeof ID.Type
|
||||
@@ -165,6 +166,7 @@ export const node = makeLocationNode({
|
||||
Reference.node,
|
||||
SkillV2.node,
|
||||
ToolRegistry.toolsNode,
|
||||
ToolHooks.node,
|
||||
PluginRuntime.node,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -18,7 +18,6 @@ export const Plugin = define({
|
||||
draft.update("review", (command) => {
|
||||
command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
|
||||
command.description = "review changes [commit|branch|pr], defaults to uncommitted"
|
||||
command.subtask = true
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Reference } from "../reference"
|
||||
import { AbsolutePath, type DeepMutable } from "../schema"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { Tools } from "../tool/tools"
|
||||
import { ToolHooks } from "../tool/hooks"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
|
||||
const mutable = <T>(value: T) => value as DeepMutable<T>
|
||||
@@ -31,6 +32,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
const reference = yield* Reference.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
const tools = yield* Tools.Service
|
||||
const toolHooks = yield* ToolHooks.Service
|
||||
const runtime = yield* PluginRuntime.Service
|
||||
const locationInfo = () =>
|
||||
new Location.Info({
|
||||
@@ -247,6 +249,47 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
},
|
||||
tool: {
|
||||
register: (input) => tools.register(input),
|
||||
execute: {
|
||||
before: (callback) =>
|
||||
toolHooks.hook.before((event) => {
|
||||
const output = {
|
||||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
agent: event.agent,
|
||||
assistantMessageID: event.assistantMessageID,
|
||||
toolCallID: event.toolCallID,
|
||||
input: event.input,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() => Effect.sync(() => (event.input = output.input))),
|
||||
)
|
||||
}),
|
||||
after: (callback) =>
|
||||
toolHooks.hook.after((event) => {
|
||||
const output = {
|
||||
tool: event.tool,
|
||||
sessionID: event.sessionID,
|
||||
agent: event.agent,
|
||||
assistantMessageID: event.assistantMessageID,
|
||||
toolCallID: event.toolCallID,
|
||||
input: event.input,
|
||||
result: event.result,
|
||||
output: event.output,
|
||||
outputPaths: event.outputPaths,
|
||||
}
|
||||
const result = callback(output)
|
||||
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() => {
|
||||
event.result = output.result
|
||||
event.output = output.output
|
||||
event.outputPaths = output.outputPaths
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
},
|
||||
},
|
||||
session: {
|
||||
create: (input) =>
|
||||
@@ -259,6 +302,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
|
||||
}),
|
||||
get: (input) => runtime.session.get(input.sessionID),
|
||||
prompt: runtime.session.prompt,
|
||||
command: runtime.session.command,
|
||||
interrupt: (input) => runtime.session.interrupt(input.sessionID),
|
||||
},
|
||||
} satisfies Interface
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as PluginInternal from "./internal"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { httpClient } from "../effect/app-node-platform"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Layer, Scope } from "effect"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { Catalog } from "../catalog"
|
||||
import { CommandV2 } from "../command"
|
||||
@@ -27,6 +27,7 @@ import { PluginV2 } from "../plugin"
|
||||
import { PluginRuntime } from "../plugin/runtime"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { Reference } from "../reference"
|
||||
import { Ripgrep } from "../ripgrep"
|
||||
import { Shell } from "../shell"
|
||||
import { SkillV2 } from "../skill"
|
||||
import { State } from "../state"
|
||||
@@ -40,6 +41,7 @@ import { ProviderPlugins } from "./provider"
|
||||
import { SdkPlugins } from "./sdk"
|
||||
import { SkillPlugin } from "./skill"
|
||||
import { VariantPlugin } from "./variant"
|
||||
import { GlobTool } from "../tool/glob"
|
||||
import { ShellTool } from "../tool/shell"
|
||||
import { SubagentTool } from "../tool/subagent"
|
||||
|
||||
@@ -61,6 +63,7 @@ export type Requirements =
|
||||
| PermissionV2.Service
|
||||
| PluginRuntime.Service
|
||||
| Reference.Service
|
||||
| Ripgrep.Service
|
||||
| Shell.Service
|
||||
| SkillV2.Service
|
||||
| Tools.Service
|
||||
@@ -76,59 +79,35 @@ export function define<R>(plugin: Plugin<R>) {
|
||||
|
||||
const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const commands = yield* CommandV2.Service
|
||||
const plugin = yield* PluginV2.Service
|
||||
const sdkPlugins = yield* SdkPlugins.Service
|
||||
const integration = yield* Integration.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const modelsDev = yield* ModelsDev.Service
|
||||
const npm = yield* Npm.Service
|
||||
const events = yield* EventV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const filesystem = yield* FileSystem.Service
|
||||
const global = yield* Global.Service
|
||||
const http = yield* HttpClient.HttpClient
|
||||
const mutation = yield* LocationMutation.Service
|
||||
const permission = yield* PermissionV2.Service
|
||||
const skill = yield* SkillV2.Service
|
||||
const reference = yield* Reference.Service
|
||||
const shell = yield* Shell.Service
|
||||
const tools = yield* Tools.Service
|
||||
const runtime = yield* PluginRuntime.Service
|
||||
const add = <R>(input: Plugin<R>) => {
|
||||
const loaded = {
|
||||
id: input.id,
|
||||
effect: (context: PluginContext) =>
|
||||
input
|
||||
.effect(context)
|
||||
.pipe(
|
||||
Effect.provideService(Catalog.Service, catalog),
|
||||
Effect.provideService(CommandV2.Service, commands),
|
||||
Effect.provideService(Integration.Service, integration),
|
||||
Effect.provideService(AgentV2.Service, agents),
|
||||
Effect.provideService(Config.Service, config),
|
||||
Effect.provideService(Location.Service, location),
|
||||
Effect.provideService(ModelsDev.Service, modelsDev),
|
||||
Effect.provideService(Npm.Service, npm),
|
||||
Effect.provideService(EventV2.Service, events),
|
||||
Effect.provideService(FSUtil.Service, fs),
|
||||
Effect.provideService(FileSystem.Service, filesystem),
|
||||
Effect.provideService(Global.Service, global),
|
||||
Effect.provideService(HttpClient.HttpClient, http),
|
||||
Effect.provideService(LocationMutation.Service, mutation),
|
||||
Effect.provideService(PermissionV2.Service, permission),
|
||||
Effect.provideService(SkillV2.Service, skill),
|
||||
Effect.provideService(Reference.Service, reference),
|
||||
Effect.provideService(Shell.Service, shell),
|
||||
Effect.provideService(Tools.Service, tools),
|
||||
Effect.provideService(PluginRuntime.Service, runtime),
|
||||
),
|
||||
}
|
||||
return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect)
|
||||
}
|
||||
const services = Context.mergeAll(
|
||||
Context.make(Catalog.Service, yield* Catalog.Service),
|
||||
Context.make(CommandV2.Service, yield* CommandV2.Service),
|
||||
Context.make(Integration.Service, yield* Integration.Service),
|
||||
Context.make(AgentV2.Service, yield* AgentV2.Service),
|
||||
Context.make(Config.Service, yield* Config.Service),
|
||||
Context.make(Location.Service, yield* Location.Service),
|
||||
Context.make(ModelsDev.Service, yield* ModelsDev.Service),
|
||||
Context.make(Npm.Service, yield* Npm.Service),
|
||||
Context.make(EventV2.Service, yield* EventV2.Service),
|
||||
Context.make(FSUtil.Service, yield* FSUtil.Service),
|
||||
Context.make(FileSystem.Service, yield* FileSystem.Service),
|
||||
Context.make(Global.Service, yield* Global.Service),
|
||||
Context.make(HttpClient.HttpClient, yield* HttpClient.HttpClient),
|
||||
Context.make(LocationMutation.Service, yield* LocationMutation.Service),
|
||||
Context.make(PermissionV2.Service, yield* PermissionV2.Service),
|
||||
Context.make(SkillV2.Service, yield* SkillV2.Service),
|
||||
Context.make(Reference.Service, yield* Reference.Service),
|
||||
Context.make(Ripgrep.Service, yield* Ripgrep.Service),
|
||||
Context.make(Shell.Service, yield* Shell.Service),
|
||||
Context.make(Tools.Service, yield* Tools.Service),
|
||||
Context.make(PluginRuntime.Service, yield* PluginRuntime.Service),
|
||||
)
|
||||
const add = (input: Plugin<Requirements | Scope.Scope>) =>
|
||||
plugin.add(PluginV2.ID.make(input.id), (context: PluginContext) =>
|
||||
input.effect(context).pipe(Effect.provide(services)),
|
||||
)
|
||||
|
||||
yield* State.batch(
|
||||
Effect.gen(function* () {
|
||||
@@ -138,6 +117,7 @@ const layer = Layer.effectDiscard(
|
||||
yield* add(SkillPlugin.Plugin)
|
||||
yield* add(ModelsDevPlugin)
|
||||
yield* add(ConfigExternalPlugin.Plugin)
|
||||
yield* add(GlobTool.Plugin)
|
||||
yield* add(ShellTool.Plugin)
|
||||
yield* add(SubagentTool.Plugin)
|
||||
yield* add(ConfigAgentPlugin.Plugin)
|
||||
@@ -175,6 +155,7 @@ export const node = makeLocationNode({
|
||||
PermissionV2.node,
|
||||
SkillV2.node,
|
||||
Reference.node,
|
||||
Ripgrep.node,
|
||||
Shell.node,
|
||||
ToolRegistry.toolsNode,
|
||||
PluginRuntime.node,
|
||||
|
||||
@@ -70,25 +70,73 @@ function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"]
|
||||
return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()]
|
||||
}
|
||||
|
||||
function reasoningVariants(model: ModelsDev.Model, packageName: string | undefined): ModelV2Info["variants"] {
|
||||
const result = new Map<ModelV2.VariantID, ModelV2Info["variants"][number]>()
|
||||
if (packageName === "@ai-sdk/openai" || packageName === "@ai-sdk/openai-compatible") {
|
||||
const option = model.reasoning_options?.find((option) => option.type === "effort")
|
||||
for (const value of option?.values ?? []) {
|
||||
const id = value === null ? "none" : value
|
||||
if (typeof id !== "string") continue
|
||||
const variantID = ModelV2.VariantID.make(id)
|
||||
result.set(variantID, {
|
||||
id: variantID,
|
||||
headers: {},
|
||||
body:
|
||||
packageName === "@ai-sdk/openai"
|
||||
? { include: ["reasoning.encrypted_content"], reasoning: { effort: id, summary: "auto" } }
|
||||
: { reasoning_effort: id },
|
||||
})
|
||||
const OPENAI_INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"]
|
||||
|
||||
function reasoningVariants(provider: ModelsDev.Provider, model: ModelsDev.Model): ModelV2Info["variants"] {
|
||||
const npm = model.provider?.npm ?? provider.npm
|
||||
const options = model.reasoning_options ?? []
|
||||
const effort = options.find((option) => option.type === "effort")
|
||||
if (effort?.type === "effort") {
|
||||
return effort.values.flatMap((value) => {
|
||||
const raw: unknown = value
|
||||
const id = raw === null ? "none" : typeof raw === "string" ? raw : undefined
|
||||
if (id === undefined) return []
|
||||
const settings = settingsForEffort(npm, id)
|
||||
return settings ? [{ id, settings, headers: {}, body: {} }] : []
|
||||
})
|
||||
}
|
||||
|
||||
const budget = options.find((option) => option.type === "budget_tokens")
|
||||
if (budget?.type === "budget_tokens") return budgetVariants(npm, budget)
|
||||
|
||||
// Toggle-only reasoning is intentionally left for a follow-up because V1 has
|
||||
// provider/model-specific behavior like MiniMax M3 adaptive thinking and
|
||||
// Qwen/GLM enable_thinking request shapes in packages/opencode.
|
||||
return []
|
||||
}
|
||||
|
||||
function settingsForEffort(npm: string | undefined, effort: string): ProviderV2.Settings | undefined {
|
||||
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { effort } }
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
|
||||
return { thinking: { type: "adaptive", display: "summarized" }, effort }
|
||||
}
|
||||
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") {
|
||||
return { thinkingConfig: { includeThoughts: true, thinkingLevel: effort } }
|
||||
}
|
||||
if (npm === "@ai-sdk/azure") return { reasoningEffort: effort }
|
||||
if (npm === "@ai-sdk/openai") {
|
||||
return {
|
||||
reasoningEffort: effort,
|
||||
reasoningSummary: "auto",
|
||||
include: OPENAI_INCLUDE_ENCRYPTED_REASONING,
|
||||
}
|
||||
}
|
||||
return [...result.values()]
|
||||
if (npm === "@ai-sdk/openai-compatible") return { reasoningEffort: effort }
|
||||
}
|
||||
|
||||
function budgetVariants(
|
||||
npm: string | undefined,
|
||||
option: Extract<NonNullable<ModelsDev.Model["reasoning_options"]>[number], { type: "budget_tokens" }>,
|
||||
): ModelV2Info["variants"] {
|
||||
const max = option.max
|
||||
const high = option.max === undefined ? Math.max(option.min ?? 0, 16_000) : Math.min(Math.max(option.min ?? 0, 16_000), option.max)
|
||||
return [
|
||||
{ id: "high", budget: high },
|
||||
...(max === undefined || max === high ? [] : [{ id: "max", budget: max }]),
|
||||
].flatMap((item) => {
|
||||
const settings = settingsForBudget(npm, item.budget)
|
||||
return settings ? [{ id: item.id, settings, headers: {}, body: {} }] : []
|
||||
})
|
||||
}
|
||||
|
||||
function settingsForBudget(npm: string | undefined, budget: number): ProviderV2.Settings | undefined {
|
||||
if (npm === "@openrouter/ai-sdk-provider") return { reasoning: { max_tokens: budget } }
|
||||
if (npm === "@ai-sdk/anthropic" || npm === "@ai-sdk/google-vertex/anthropic") {
|
||||
return { thinking: { type: "enabled", budgetTokens: budget } }
|
||||
}
|
||||
if (npm === "@ai-sdk/google" || npm === "@ai-sdk/google-vertex") {
|
||||
return { thinkingConfig: { includeThoughts: true, thinkingBudget: budget } }
|
||||
}
|
||||
}
|
||||
|
||||
function modeName(model: ModelsDev.Model, mode: string) {
|
||||
@@ -193,7 +241,7 @@ export const ModelsDevPlugin = define({
|
||||
|
||||
for (const model of Object.values(item.models)) {
|
||||
const baseCost = cost(model.cost)
|
||||
const variants = reasoningVariants(model, model.provider?.npm ?? item.npm)
|
||||
const variants = reasoningVariants(item, model)
|
||||
catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost, variants }))
|
||||
for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) {
|
||||
catalog.model.update(providerID, `${model.id}-${mode}`, (draft) =>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export * as OpenAICodex from "./openai-codex"
|
||||
|
||||
// TEMPORARY SEAM (#34765): plugins have no hook into LLM route construction, so
|
||||
// codex routing lives in SessionRunnerModel.fromCatalogModel and catalog filtering
|
||||
// in OpenAIPlugin, sharing this module. Once the native provider packages land
|
||||
// (#33689/#33925/#34462) this should collapse into the native OpenAI provider.
|
||||
// The eligibility rules mirror V1's CodexAuthPlugin allowlist; models.dev has no
|
||||
// plan-eligibility data for OpenAI today, but models other vendors' subscriptions
|
||||
// as dedicated providers (e.g. zai-coding-plan) - a future openai-chatgpt-plan
|
||||
// provider entry could replace the hardcoded rules with catalog data.
|
||||
|
||||
/** ChatGPT-plan requests must target the codex backend instead of the public API. */
|
||||
export const baseURL = "https://chatgpt.com/backend-api/codex"
|
||||
|
||||
const methodIDs: readonly string[] = ["chatgpt-browser", "chatgpt-headless"]
|
||||
|
||||
/** Structural credential shape so both core and plugin-facing credential types fit. */
|
||||
type CredentialLike = {
|
||||
readonly type: string
|
||||
readonly methodID?: string
|
||||
readonly metadata?: Record<string, unknown> | undefined
|
||||
}
|
||||
|
||||
export const isChatGPT = (credential: CredentialLike | undefined) =>
|
||||
credential?.type === "oauth" && credential.methodID !== undefined && methodIDs.includes(credential.methodID)
|
||||
|
||||
export const accountID = (credential: CredentialLike | undefined) => {
|
||||
if (!isChatGPT(credential)) return undefined
|
||||
const value = credential?.metadata?.accountID
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
const allowed = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
||||
const disallowed = new Set(["gpt-5.5-pro"])
|
||||
|
||||
/** Which API model ids a ChatGPT subscription may call through the codex backend. */
|
||||
export const eligible = (apiID: string) => {
|
||||
if (allowed.has(apiID)) return true
|
||||
if (disallowed.has(apiID)) return false
|
||||
const match = apiID.match(/^gpt-(\d+\.\d+)/)
|
||||
return match ? Number.parseFloat(match[1]) > 5.4 : false
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import { createServer } from "node:http"
|
||||
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration"
|
||||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { Deferred, Effect } from "effect"
|
||||
import { Deferred, Effect, Semaphore, Stream } from "effect"
|
||||
import type { Scope } from "effect"
|
||||
import { Credential } from "../../credential"
|
||||
import { EventV2 } from "../../event"
|
||||
import { InstallationVersion } from "../../installation/version"
|
||||
import { Integration } from "../../integration"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { OauthCallbackPage } from "../../oauth/page"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import type { PluginInternal } from "../internal"
|
||||
import { OpenAICodex } from "./openai-codex"
|
||||
|
||||
const clientID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||
const issuer = "https://auth.openai.com"
|
||||
@@ -154,6 +156,18 @@ const headless = {
|
||||
export const OpenAIPlugin = define({
|
||||
id: "openai",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const events = yield* EventV2.Service
|
||||
const loading = Semaphore.makeUnsafe(1)
|
||||
let chatgpt = false
|
||||
|
||||
const load = Effect.fn("OpenAIPlugin.load")(function* () {
|
||||
const connection = yield* ctx.integration.connection.active("openai")
|
||||
const credential = connection
|
||||
? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
: undefined
|
||||
chatgpt = OpenAICodex.isChatGPT(credential)
|
||||
})
|
||||
|
||||
yield* ctx.integration.transform((draft) => {
|
||||
draft.method.update(browser)
|
||||
draft.method.update(headless)
|
||||
@@ -170,8 +184,30 @@ export const OpenAIPlugin = define({
|
||||
model.enabled = false
|
||||
})
|
||||
}
|
||||
if (!chatgpt) return
|
||||
const item = evt.provider.get(ProviderV2.ID.openai)
|
||||
if (!item) return
|
||||
for (const model of item.models.values()) {
|
||||
// ChatGPT-plan tokens only authorize codex-eligible models, and the
|
||||
// subscription covers usage, so hide the rest and zero the cost.
|
||||
evt.model.update(item.provider.id, model.id, (draft) => {
|
||||
if (!OpenAICodex.eligible(draft.api.id)) {
|
||||
draft.enabled = false
|
||||
return
|
||||
}
|
||||
draft.cost = []
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload())))
|
||||
yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
||||
Stream.filter((event) => event.data.integrationID === Integration.ID.make("openai")),
|
||||
Stream.runForEach(refresh),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* refresh().pipe(Effect.forkScoped)
|
||||
yield* ctx.aisdk.sdk(
|
||||
Effect.fn(function* (evt) {
|
||||
if (evt.package !== "@ai-sdk/openai") return
|
||||
|
||||
@@ -146,7 +146,7 @@ export const OpencodePlugin = define<HttpClient.HttpClient | EventV2.Service | S
|
||||
const variantID = ModelV2.VariantID.make(id)
|
||||
let existing = model.variants.find((item) => item.id === variantID)
|
||||
if (!existing) {
|
||||
existing = { id: variantID, headers: {}, body: {} }
|
||||
existing = { id: variantID, settings: {}, headers: {}, body: {} }
|
||||
model.variants.push(existing)
|
||||
}
|
||||
Object.assign(existing.headers, options.headers)
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SessionV2 } from "../session"
|
||||
export interface Interface {
|
||||
readonly session: Pick<
|
||||
SessionV2.Interface,
|
||||
"get" | "create" | "messages" | "prompt" | "resume" | "interrupt" | "synthetic"
|
||||
"get" | "create" | "messages" | "prompt" | "command" | "resume" | "interrupt" | "synthetic"
|
||||
>
|
||||
readonly job: Pick<Job.Interface, "start" | "wait" | "block" | "background" | "cancel">
|
||||
readonly location: {
|
||||
@@ -50,6 +50,7 @@ export const layerWithCell = (cell: Cell) =>
|
||||
create: (input) => require(cell, (runtime) => runtime.session.create(input)),
|
||||
messages: (input) => require(cell, (runtime) => runtime.session.messages(input)),
|
||||
prompt: (input) => require(cell, (runtime) => runtime.session.prompt(input)),
|
||||
command: (input) => require(cell, (runtime) => runtime.session.command(input)),
|
||||
resume: (sessionID) => require(cell, (runtime) => runtime.session.resume(sessionID)),
|
||||
interrupt: (sessionID) => require(cell, (runtime) => runtime.session.interrupt(sessionID)),
|
||||
synthetic: (input) => require(cell, (runtime) => runtime.session.synthetic(input)),
|
||||
@@ -110,8 +111,13 @@ export const providerLayer = providerLayerWithCell(defaultCell)
|
||||
|
||||
export const node = makeGlobalNode({ service: Service, layer, deps: [] })
|
||||
|
||||
export const providerNode = makeGlobalNode({
|
||||
name: "plugin-runtime-provider",
|
||||
layer: providerLayer,
|
||||
deps: [node, SessionV2.node, Job.node, LocationServiceMap.node],
|
||||
})
|
||||
// Raw layer replacements are compiled without dependencies, so cell-scoped
|
||||
// provider replacements must go through this node to keep their deps wired.
|
||||
export const providerNodeWithCell = (cell: Cell) =>
|
||||
makeGlobalNode({
|
||||
name: "plugin-runtime-provider",
|
||||
layer: providerLayerWithCell(cell),
|
||||
deps: [node, SessionV2.node, Job.node, LocationServiceMap.node],
|
||||
})
|
||||
|
||||
export const providerNode = providerNodeWithCell(defaultCell)
|
||||
|
||||
@@ -33,7 +33,8 @@ export function generate(model: ModelV2Info): ModelV2Info["variants"] {
|
||||
if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return []
|
||||
return ["high", "max"].map((id) => ({
|
||||
id,
|
||||
settings: { reasoningEffort: id },
|
||||
headers: {},
|
||||
body: { reasoning_effort: id },
|
||||
body: {},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -19,7 +19,15 @@ export type MutableApi<T extends Api = Api> = T extends Api
|
||||
export const Request = Provider.Request
|
||||
export type Request = Provider.Request
|
||||
|
||||
export const Settings = Provider.Settings
|
||||
export type Settings = Provider.Settings
|
||||
|
||||
export const Info = Provider.Info
|
||||
export type Info = Provider.Info
|
||||
|
||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }
|
||||
export type MutableRequest = Types.DeepMutable<Request>
|
||||
|
||||
export type MutableInfo = Omit<Types.DeepMutable<Info>, "api" | "request"> & {
|
||||
api: MutableApi
|
||||
request: MutableRequest
|
||||
}
|
||||
|
||||
@@ -11,20 +11,48 @@ const Summary = Schema.Struct({
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const entries = (references: ReadonlyArray<typeof Summary.Type>) =>
|
||||
references.flatMap((reference) => [
|
||||
" <reference>",
|
||||
` <name>${reference.name}</name>`,
|
||||
` <path>${reference.path}</path>`,
|
||||
...(reference.description === undefined ? [] : [` <description>${reference.description}</description>`]),
|
||||
" </reference>",
|
||||
])
|
||||
|
||||
const render = (references: ReadonlyArray<typeof Summary.Type>) =>
|
||||
[
|
||||
"Project references provide additional directories that can be accessed when relevant.",
|
||||
"<available_references>",
|
||||
...references.flatMap((reference) => [
|
||||
" <reference>",
|
||||
` <name>${reference.name}</name>`,
|
||||
` <path>${reference.path}</path>`,
|
||||
...(reference.description === undefined ? [] : [` <description>${reference.description}</description>`]),
|
||||
" </reference>",
|
||||
]),
|
||||
...entries(references),
|
||||
"</available_references>",
|
||||
].join("\n")
|
||||
|
||||
const update = (previous: ReadonlyArray<typeof Summary.Type>, current: ReadonlyArray<typeof Summary.Type>) => {
|
||||
const diff = SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(reference) => reference.name,
|
||||
(before, after) => before.path !== after.path || before.description !== after.description,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
return [
|
||||
"The available project references have changed. This list supersedes the previous reference list.",
|
||||
render(current),
|
||||
].join("\n")
|
||||
return [
|
||||
...(diff.added.length === 0
|
||||
? []
|
||||
: ["New project references are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
`The following project references are no longer available and must not be used: ${diff.removed.map((reference) => reference.name).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: () => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
@@ -52,11 +80,7 @@ const layer = Layer.effect(
|
||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||
load: Effect.succeed(available),
|
||||
baseline: render,
|
||||
update: (_previous, current) =>
|
||||
[
|
||||
"The available project references have changed. This list supersedes the previous reference list.",
|
||||
render(current),
|
||||
].join("\n"),
|
||||
update,
|
||||
removed: () => "Project reference guidance is no longer available. Do not use previously listed references.",
|
||||
})
|
||||
}),
|
||||
|
||||
+109
-28
@@ -3,7 +3,7 @@ export * from "./session/schema"
|
||||
|
||||
import { DateTime, Effect, Layer, Schema, Context, Stream, Scope } from "effect"
|
||||
import { ListAnchor } from "@opencode-ai/schema/session"
|
||||
import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm"
|
||||
import { and, asc, desc, eq, gt, isNull, like, lt, or, type SQL } from "drizzle-orm"
|
||||
import { ProjectV2 } from "./project"
|
||||
import { WorkspaceV2 } from "./workspace"
|
||||
import { ModelV2 } from "./model"
|
||||
@@ -37,9 +37,10 @@ import { SessionCompaction } from "./session/compaction"
|
||||
import { SessionRevert } from "./session/revert"
|
||||
import { Revert } from "@opencode-ai/schema/revert"
|
||||
import { FSUtil } from "./fs-util"
|
||||
import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest"
|
||||
import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||
import { SkillV2 } from "./skill"
|
||||
import { Job } from "./job"
|
||||
import { CommandV2 } from "./command"
|
||||
|
||||
export const RevertState = Revert.State
|
||||
export type RevertState = Revert.State
|
||||
@@ -60,6 +61,7 @@ const ListInputBase = {
|
||||
search: Schema.String.pipe(Schema.optional),
|
||||
limit: PositiveInt.pipe(Schema.optional),
|
||||
order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional),
|
||||
parentID: Schema.NullOr(SessionSchema.ID).pipe(Schema.optional),
|
||||
anchor: ListAnchor.pipe(Schema.optional),
|
||||
}
|
||||
|
||||
@@ -108,7 +110,7 @@ export class OperationUnavailableError extends Schema.TaggedErrorClass<Operation
|
||||
},
|
||||
) {}
|
||||
|
||||
export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error"
|
||||
export { MessageDecodeError } from "./session/error"
|
||||
|
||||
export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictError>()("Session.PromptConflictError", {
|
||||
sessionID: SessionSchema.ID,
|
||||
@@ -130,10 +132,16 @@ export type Error =
|
||||
| PromptConflictError
|
||||
| BusyError
|
||||
| SkillNotFoundError
|
||||
| CommandV2.NotFoundError
|
||||
| CommandV2.EvaluationError
|
||||
| MessageNotFoundError
|
||||
|
||||
export interface Interface {
|
||||
readonly list: (input?: ListInput) => Effect.Effect<SessionSchema.Info[]>
|
||||
readonly list: (input?: ListInput) => Effect.Effect<{
|
||||
readonly data: SessionSchema.Info[]
|
||||
/** Per-session durable log watermark, read in the same transaction as the snapshot. Sessions without events are absent. */
|
||||
readonly watermarks: ReadonlyMap<string, EventV2.Seq>
|
||||
}>
|
||||
readonly create: (input: CreateInput) => Effect.Effect<SessionSchema.Info, NotFoundError>
|
||||
readonly fork: (input: ForkInput) => Effect.Effect<SessionSchema.Info, NotFoundError | MessageNotFoundError>
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info, NotFoundError>
|
||||
@@ -153,15 +161,21 @@ export interface Interface {
|
||||
readonly context: (
|
||||
sessionID: SessionSchema.ID,
|
||||
) => Effect.Effect<SessionMessage.Message[], NotFoundError | MessageDecodeError>
|
||||
readonly events: (input: {
|
||||
/**
|
||||
* Durable, ordered, gap-free session log read. Replays public durable
|
||||
* session events after the exclusive `after` cursor, emits a `Synced`
|
||||
* marker at the captured replay watermark, then continues live when `follow`
|
||||
* is set.
|
||||
* The marker's seq may exceed the last emitted event because non-public
|
||||
* durable events share the aggregate's sequence space.
|
||||
*/
|
||||
readonly log: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
after?: number
|
||||
}) => Stream.Stream<SessionEvent.DurableEvent, NotFoundError>
|
||||
readonly history: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
after?: number
|
||||
limit: number
|
||||
}) => Effect.Effect<{ events: ReadonlyArray<SessionEvent.DurableEvent>; hasMore: boolean }, NotFoundError>
|
||||
follow?: boolean
|
||||
}) => Stream.Stream<SessionEvent.DurableEvent | EventLog.Synced, NotFoundError>
|
||||
/** Latest durable log seq per session. Sessions without events are absent. */
|
||||
readonly watermarks: (sessionIDs: ReadonlyArray<SessionSchema.ID>) => Effect.Effect<ReadonlyMap<string, EventV2.Seq>>
|
||||
readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchModel: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
@@ -175,6 +189,21 @@ export interface Interface {
|
||||
delivery?: SessionInput.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<SessionInput.Admitted, NotFoundError | PromptConflictError>
|
||||
readonly command: (input: {
|
||||
id?: SessionMessage.ID
|
||||
sessionID: SessionSchema.ID
|
||||
command: string
|
||||
arguments?: string
|
||||
agent?: string
|
||||
model?: ModelV2.Ref
|
||||
files?: PromptInput.Prompt["files"]
|
||||
agents?: PromptInput.Prompt["agents"]
|
||||
delivery?: SessionInput.Delivery
|
||||
resume?: boolean
|
||||
}) => Effect.Effect<
|
||||
SessionInput.Admitted,
|
||||
NotFoundError | PromptConflictError | CommandV2.NotFoundError | CommandV2.EvaluationError
|
||||
>
|
||||
readonly shell: (input: {
|
||||
id?: EventV2.ID
|
||||
sessionID: SessionSchema.ID
|
||||
@@ -199,6 +228,7 @@ export interface Interface {
|
||||
sessionID: SessionSchema.ID
|
||||
text: string
|
||||
description?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly revert: {
|
||||
readonly stage: (input: {
|
||||
@@ -330,12 +360,16 @@ const layer = Layer.effect(
|
||||
const direction = input.anchor?.direction ?? "next"
|
||||
const requestedOrder = input.order ?? "desc"
|
||||
const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder
|
||||
const sortColumn = SessionTable.time_created
|
||||
const sortColumn = SessionTable.time_updated
|
||||
const conditions: SQL[] = []
|
||||
if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory))
|
||||
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
||||
if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project))
|
||||
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||
if (input.parentID !== undefined)
|
||||
conditions.push(
|
||||
input.parentID === null ? isNull(SessionTable.parent_id) : eq(SessionTable.parent_id, input.parentID),
|
||||
)
|
||||
if (input.anchor) {
|
||||
conditions.push(
|
||||
order === "asc"
|
||||
@@ -357,10 +391,21 @@ const layer = Layer.effect(
|
||||
order === "asc" ? asc(sortColumn) : desc(sortColumn),
|
||||
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
||||
)
|
||||
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
|
||||
// Watermarks must pair with the snapshot exactly, so both reads share a transaction:
|
||||
// a higher watermark would let an attached tail skip events missing from the snapshot.
|
||||
const snapshot = yield* db
|
||||
.transaction(() =>
|
||||
Effect.gen(function* () {
|
||||
const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
const watermarks = yield* events.sequences(rows.map((row) => row.id))
|
||||
return { rows, watermarks }
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
const rows = direction === "previous" ? snapshot.rows.toReversed() : snapshot.rows
|
||||
return { data: rows.map((row) => fromRow(row)), watermarks: snapshot.watermarks }
|
||||
}),
|
||||
messages: Effect.fn("V2Session.messages")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
@@ -404,19 +449,19 @@ const layer = Layer.effect(
|
||||
yield* result.get(sessionID)
|
||||
return yield* store.context(sessionID)
|
||||
}),
|
||||
events: (input) =>
|
||||
log: (input) =>
|
||||
Stream.unwrap(
|
||||
result
|
||||
.get(input.sessionID)
|
||||
.pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))),
|
||||
).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))),
|
||||
history: Effect.fn("V2Session.history")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
return yield* EventV2.readAggregate(db, {
|
||||
...input,
|
||||
aggregateID: input.sessionID,
|
||||
manifest: SessionDurable,
|
||||
})
|
||||
.pipe(Effect.as(events.log({ aggregateID: input.sessionID, after: input.after, follow: input.follow }))),
|
||||
).pipe(
|
||||
Stream.filter(
|
||||
(item): item is SessionEvent.DurableEvent | EventLog.Synced =>
|
||||
EventV2.isSynced(item) || isDurableSessionEvent(item),
|
||||
),
|
||||
),
|
||||
watermarks: Effect.fn("V2Session.watermarks")(function* (sessionIDs) {
|
||||
return yield* events.sequences(sessionIDs)
|
||||
}),
|
||||
prompt: Effect.fn("V2Session.prompt")((input) =>
|
||||
Effect.uninterruptible(
|
||||
@@ -449,6 +494,37 @@ const layer = Layer.effect(
|
||||
}),
|
||||
),
|
||||
),
|
||||
command: Effect.fn("V2Session.command")(function* (input) {
|
||||
const session = yield* result.get(input.sessionID)
|
||||
const commands = yield* CommandV2.Service.pipe(Effect.provide(locations.get(session.location)))
|
||||
const command = yield* commands.get(input.command)
|
||||
if (!command)
|
||||
return yield* new CommandV2.NotFoundError({
|
||||
command: input.command,
|
||||
message: `Command not found: ${input.command}`,
|
||||
})
|
||||
const evaluated = yield* commands.evaluate({ name: input.command, arguments: input.arguments })
|
||||
|
||||
// TODO(v2 commands): decide whether command-level subtask/background execution belongs in v2 commands.
|
||||
const agent = command.agent ?? input.agent
|
||||
const commandAgent = yield* Effect.gen(function* () {
|
||||
if (!command.agent) return undefined
|
||||
const agents = yield* AgentV2.Service.pipe(Effect.provide(locations.get(session.location)))
|
||||
return yield* agents.get(AgentV2.ID.make(command.agent))
|
||||
})
|
||||
const model = command.model ?? commandAgent?.model ?? input.model
|
||||
if (agent !== undefined && session.agent !== AgentV2.ID.make(agent))
|
||||
yield* result.switchAgent({ sessionID: input.sessionID, agent })
|
||||
if (model !== undefined) yield* result.switchModel({ sessionID: input.sessionID, model })
|
||||
|
||||
return yield* result.prompt({
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
prompt: { text: evaluated.text, files: input.files, agents: input.agents },
|
||||
delivery: input.delivery,
|
||||
resume: input.resume,
|
||||
})
|
||||
}),
|
||||
shell: Effect.fn("V2Session.shell")(function* () {
|
||||
return yield* new OperationUnavailableError({ operation: "shell" })
|
||||
}),
|
||||
@@ -465,7 +541,9 @@ const layer = Layer.effect(
|
||||
text: skill.content,
|
||||
})
|
||||
if (input.resume !== false)
|
||||
yield* execution.resume(input.sessionID).pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||
yield* execution
|
||||
.resume(input.sessionID)
|
||||
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||
}),
|
||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
@@ -547,8 +625,11 @@ const layer = Layer.effect(
|
||||
timestamp: yield* DateTime.now,
|
||||
text: input.text,
|
||||
description: input.description,
|
||||
metadata: input.metadata,
|
||||
})
|
||||
yield* execution.resume(input.sessionID).pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||
yield* execution
|
||||
.resume(input.sessionID)
|
||||
.pipe(Effect.ignore, Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||
}),
|
||||
interrupt: Effect.fn("V2Session.interrupt")((sessionID) =>
|
||||
Effect.uninterruptible(execution.interrupt(sessionID)),
|
||||
|
||||
@@ -321,12 +321,12 @@ export const layer = Layer.effect(
|
||||
compactIfNeeded: compaction.compactIfNeeded,
|
||||
compactAfterOverflow: compaction.compactAfterOverflow,
|
||||
compactManual: Effect.fn("SessionCompaction.compactManual")(function* (input) {
|
||||
const model = yield* models.resolve(input.session).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!model) return false
|
||||
const resolved = yield* models.resolve(input.session).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!resolved) return false
|
||||
return yield* compaction.compactManual({
|
||||
sessionID: input.session.id,
|
||||
messages: input.messages,
|
||||
model,
|
||||
model: resolved.model,
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
export * as SessionContextCheckpoint from "./context-checkpoint"
|
||||
|
||||
import { eq } from "drizzle-orm"
|
||||
import { DateTime, Effect, Option, Schema } from "effect"
|
||||
import type { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionHistory } from "./history"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionContextCheckpointTable } from "./sql"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
const decodeApplied = Schema.decodeUnknownOption(SystemContext.Applied)
|
||||
|
||||
/**
|
||||
* Loads or creates the session's durable context checkpoint, narrating any
|
||||
* drift since the model was last told as a chronological update. Completed
|
||||
* compaction rebaselines; nothing else rewrites the baseline. Runs before
|
||||
* input promotion so a blocked first turn leaves pending inputs untouched.
|
||||
*/
|
||||
export const prepare = Effect.fn("SessionContextCheckpoint.prepare")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
context: Effect.Effect<SystemContext.SystemContext>,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const [value, stored, compaction] = yield* Effect.all(
|
||||
[context, find(db, sessionID), SessionHistory.latestCompaction(db, sessionID)],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
if (!stored) {
|
||||
const baseline = yield* SystemContext.initialize(value)
|
||||
const baselineSeq = yield* insert(db, sessionID, baseline)
|
||||
return { baseline: baseline.text, baselineSeq }
|
||||
}
|
||||
|
||||
// The applied record is comparison state only; an undecodable one heals by
|
||||
// treating every source as new, re-announcing baselines as updates.
|
||||
const applied = Option.getOrElse(decodeApplied(stored.snapshot), () => ({}))
|
||||
if (compaction !== undefined && compaction.seq > stored.baseline_seq) {
|
||||
const baseline = yield* SystemContext.rebaseline(value, applied)
|
||||
yield* rewrite(db, sessionID, compaction.seq, baseline)
|
||||
return { baseline: baseline.text, baselineSeq: compaction.seq }
|
||||
}
|
||||
const result = yield* SystemContext.reconcile(value, applied)
|
||||
if (result._tag === "Unchanged") return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
|
||||
yield* events.publish(
|
||||
SessionEvent.ContextUpdated,
|
||||
{ sessionID, messageID: SessionMessage.ID.create(), timestamp: yield* DateTime.now, text: result.text },
|
||||
{ commit: () => advance(db, sessionID, result.applied).pipe(Effect.orDie) },
|
||||
)
|
||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
})
|
||||
|
||||
export const reset = Effect.fn("SessionContextCheckpoint.reset")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
yield* db
|
||||
.delete(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const find = Effect.fnUntraced(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
return yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const insert = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
baseline: SystemContext.Baseline,
|
||||
) {
|
||||
const baselineSeq = yield* EventV2.latestSequence(db, sessionID)
|
||||
yield* db
|
||||
.insert(SessionContextCheckpointTable)
|
||||
.values({
|
||||
session_id: sessionID,
|
||||
baseline: baseline.text,
|
||||
snapshot: baseline.applied,
|
||||
baseline_seq: baselineSeq,
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
return baselineSeq
|
||||
})
|
||||
|
||||
const rewrite = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
baselineSeq: number,
|
||||
baseline: SystemContext.Baseline,
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionContextCheckpointTable)
|
||||
.set({
|
||||
baseline: baseline.text,
|
||||
snapshot: baseline.applied,
|
||||
baseline_seq: baselineSeq,
|
||||
})
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.returning({ sessionID: SessionContextCheckpointTable.session_id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die("Context checkpoint not found")
|
||||
})
|
||||
|
||||
const advance = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
applied: SystemContext.Applied,
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionContextCheckpointTable)
|
||||
.set({ snapshot: applied })
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.returning({ sessionID: SessionContextCheckpointTable.session_id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die("Context checkpoint not found")
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
export * as SessionContextEntry from "./context-entry"
|
||||
|
||||
import { and, asc, eq } from "drizzle-orm"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { SessionContextEntry } from "@opencode-ai/schema/session-context-entry"
|
||||
import { Database } from "../database/database"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionContextEntryTable } from "./sql"
|
||||
|
||||
export const Key = SessionContextEntry.Key
|
||||
export type Key = typeof Key.Type
|
||||
export const Info = SessionContextEntry.Info
|
||||
export type Info = typeof Info.Type
|
||||
|
||||
export interface Interface {
|
||||
readonly list: (sessionID: SessionSchema.ID) => Effect.Effect<ReadonlyArray<Info>>
|
||||
readonly put: (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly key: Key
|
||||
readonly value: Schema.Json
|
||||
}) => Effect.Effect<void>
|
||||
readonly remove: (input: { readonly sessionID: SessionSchema.ID; readonly key: Key }) => Effect.Effect<void>
|
||||
/** Produces one SystemContext source per stored entry, keyed `api/<key>`. */
|
||||
readonly load: (sessionID: SessionSchema.ID) => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContextEntry") {}
|
||||
|
||||
const renderValue = (value: Schema.Json) => (typeof value === "string" ? value : JSON.stringify(value, null, 2))
|
||||
|
||||
const renderBlock = (key: Key, value: Schema.Json) =>
|
||||
[`<context key="${key}">`, renderValue(value), "</context>"].join("\n")
|
||||
|
||||
// Rendering stays mechanism-neutral: the model sees session context, not how
|
||||
// it was attached. Only chronological updates and removals carry narration.
|
||||
const source = (entry: Info) =>
|
||||
SystemContext.make({
|
||||
key: SystemContext.Key.make(`api/${entry.key}`),
|
||||
codec: Schema.toCodecJson(Schema.Json),
|
||||
load: Effect.succeed(entry.value),
|
||||
baseline: (value) => renderBlock(entry.key, value),
|
||||
update: (_previous, value) =>
|
||||
[
|
||||
`The context under "${entry.key}" changed and supersedes the previous value:`,
|
||||
renderBlock(entry.key, value),
|
||||
].join("\n"),
|
||||
removed: () => `The context under "${entry.key}" no longer applies. Disregard it.`,
|
||||
})
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
|
||||
const list = Effect.fn("SessionContextEntry.list")(function* (sessionID: SessionSchema.ID) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionContextEntryTable)
|
||||
.where(eq(SessionContextEntryTable.session_id, sessionID))
|
||||
.orderBy(asc(SessionContextEntryTable.key))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.map((row) => ({ key: row.key, value: row.value }))
|
||||
})
|
||||
|
||||
const put = Effect.fn("SessionContextEntry.put")(function* (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly key: Key
|
||||
readonly value: Schema.Json
|
||||
}) {
|
||||
yield* db
|
||||
.insert(SessionContextEntryTable)
|
||||
.values({ session_id: input.sessionID, key: input.key, value: input.value })
|
||||
.onConflictDoUpdate({
|
||||
target: [SessionContextEntryTable.session_id, SessionContextEntryTable.key],
|
||||
set: { value: input.value, time_updated: Date.now() },
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const remove = Effect.fn("SessionContextEntry.remove")(function* (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly key: Key
|
||||
}) {
|
||||
yield* db
|
||||
.delete(SessionContextEntryTable)
|
||||
.where(
|
||||
and(eq(SessionContextEntryTable.session_id, input.sessionID), eq(SessionContextEntryTable.key, input.key)),
|
||||
)
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const load = Effect.fn("SessionContextEntry.load")(function* (sessionID: SessionSchema.ID) {
|
||||
const entries = yield* list(sessionID)
|
||||
return SystemContext.combine(entries.map(source))
|
||||
})
|
||||
|
||||
return Service.of({ list, put, remove, load })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Database.node] })
|
||||
@@ -1,174 +0,0 @@
|
||||
export * as SessionContextEpoch from "./context-epoch"
|
||||
|
||||
import { eq } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import type { Database } from "../database/database"
|
||||
import { EventV2 } from "../event"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
import { ContextSnapshotDecodeError } from "./error"
|
||||
import { SessionEvent } from "./event"
|
||||
import { SessionHistory } from "./history"
|
||||
import { SessionInput } from "./input"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionContextEpochTable } from "./sql"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
interface Prepared {
|
||||
readonly baseline: string
|
||||
readonly baselineSeq: number
|
||||
}
|
||||
|
||||
export function initialize(
|
||||
db: DatabaseService,
|
||||
context: Effect.Effect<SystemContext.SystemContext>,
|
||||
sessionID: SessionSchema.ID,
|
||||
): Effect.Effect<Prepared | undefined, SystemContext.InitializationBlocked> {
|
||||
return initializeOnce(db, context, sessionID).pipe(Effect.withSpan("SessionContextEpoch.initialize"))
|
||||
}
|
||||
|
||||
export function prepare(
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
context: Effect.Effect<SystemContext.SystemContext>,
|
||||
sessionID: SessionSchema.ID,
|
||||
): Effect.Effect<Prepared, SystemContext.InitializationBlocked | ContextSnapshotDecodeError> {
|
||||
return prepareOnce(db, events, context, sessionID).pipe(Effect.withSpan("SessionContextEpoch.prepare"))
|
||||
}
|
||||
|
||||
const prepareOnce = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
context: Effect.Effect<SystemContext.SystemContext>,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
const [value, stored, compaction] = yield* Effect.all(
|
||||
[context, find(db, sessionID), SessionHistory.latestCompaction(db, sessionID)],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
if (!stored) {
|
||||
const generation = yield* SystemContext.initialize(value)
|
||||
const baselineSeq = yield* insert(db, sessionID, generation)
|
||||
return { baseline: generation.baseline, baselineSeq }
|
||||
}
|
||||
|
||||
const snapshot = yield* Schema.decodeUnknownEffect(SystemContext.Snapshot)(stored.snapshot).pipe(
|
||||
Effect.mapError((error) => new ContextSnapshotDecodeError({ sessionID, details: String(error) })),
|
||||
)
|
||||
const replacementSeq = compaction !== undefined && compaction.seq > stored.baseline_seq ? compaction.seq : undefined
|
||||
const result = replacementSeq
|
||||
? yield* SystemContext.replace(value, snapshot)
|
||||
: yield* SystemContext.reconcile(value, snapshot)
|
||||
if (result._tag === "Unchanged" || result._tag === "ReplacementBlocked") {
|
||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
}
|
||||
if (result._tag === "ReplacementReady") {
|
||||
const baselineSeq = replacementSeq ?? (yield* EventV2.latestSequence(db, sessionID))
|
||||
yield* replace(db, sessionID, baselineSeq, result.generation)
|
||||
return { baseline: result.generation.baseline, baselineSeq }
|
||||
}
|
||||
|
||||
yield* events.publish(
|
||||
SessionEvent.ContextUpdated,
|
||||
{ sessionID, messageID: SessionMessage.ID.create(), timestamp: yield* DateTime.now, text: result.text },
|
||||
{ commit: () => advance(db, sessionID, result.snapshot).pipe(Effect.orDie) },
|
||||
)
|
||||
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
||||
})
|
||||
|
||||
const initializeOnce = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
context: Effect.Effect<SystemContext.SystemContext>,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
if (yield* exists(db, sessionID)) return
|
||||
const generation = yield* context.pipe(Effect.flatMap(SystemContext.initialize))
|
||||
const baselineSeq = yield* insert(db, sessionID, generation)
|
||||
return { baseline: generation.baseline, baselineSeq }
|
||||
})
|
||||
|
||||
const exists = Effect.fn("SessionContextEpoch.exists")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
return (
|
||||
(yield* db
|
||||
.select({ sessionID: SessionContextEpochTable.session_id })
|
||||
.from(SessionContextEpochTable)
|
||||
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)) !== undefined
|
||||
)
|
||||
})
|
||||
|
||||
const find = Effect.fn("SessionContextEpoch.find")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
||||
return yield* db
|
||||
.select()
|
||||
.from(SessionContextEpochTable)
|
||||
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
export const reset = Effect.fn("SessionContextEpoch.reset")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
yield* db
|
||||
.delete(SessionContextEpochTable)
|
||||
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const insert = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
generation: SystemContext.Generation,
|
||||
) {
|
||||
const baselineSeq = yield* EventV2.latestSequence(db, sessionID)
|
||||
yield* db
|
||||
.insert(SessionContextEpochTable)
|
||||
.values({
|
||||
session_id: sessionID,
|
||||
baseline: generation.baseline,
|
||||
snapshot: generation.snapshot,
|
||||
baseline_seq: baselineSeq,
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
return baselineSeq
|
||||
})
|
||||
|
||||
const replace = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
baselineSeq: number,
|
||||
generation: SystemContext.Generation,
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionContextEpochTable)
|
||||
.set({
|
||||
baseline: generation.baseline,
|
||||
snapshot: generation.snapshot,
|
||||
baseline_seq: baselineSeq,
|
||||
})
|
||||
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
||||
.returning({ sessionID: SessionContextEpochTable.session_id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die("Context Epoch not found")
|
||||
})
|
||||
|
||||
const advance = Effect.fnUntraced(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
snapshot: SystemContext.Snapshot,
|
||||
) {
|
||||
const updated = yield* db
|
||||
.update(SessionContextEpochTable)
|
||||
.set({ snapshot })
|
||||
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
||||
.returning({ sessionID: SessionContextEpochTable.session_id })
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!updated) return yield* Effect.die("Context Epoch not found")
|
||||
})
|
||||
@@ -10,15 +10,3 @@ export class MessageDecodeError extends Schema.TaggedErrorClass<MessageDecodeErr
|
||||
return `Failed to decode message ${this.messageID} in session ${this.sessionID}`
|
||||
}
|
||||
}
|
||||
|
||||
export class ContextSnapshotDecodeError extends Schema.TaggedErrorClass<ContextSnapshotDecodeError>()(
|
||||
"Session.ContextSnapshotDecodeError",
|
||||
{
|
||||
sessionID: SessionSchema.ID,
|
||||
details: Schema.String,
|
||||
},
|
||||
) {
|
||||
override get message() {
|
||||
return `Failed to decode context snapshot for session ${this.sessionID}: ${this.details}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Cause, Effect, 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"
|
||||
import { SessionEvent } from "../event"
|
||||
import { SessionRunCoordinator } from "../run-coordinator"
|
||||
import { SessionRunner } from "../runner"
|
||||
import { SessionSchema } from "../schema"
|
||||
@@ -13,6 +15,7 @@ const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const store = yield* SessionStore.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
const events = yield* EventV2.Service
|
||||
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError>({
|
||||
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
|
||||
const session = yield* store.get(sessionID)
|
||||
@@ -26,6 +29,24 @@ const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
}),
|
||||
// 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,
|
||||
timestamp: yield* DateTime.now,
|
||||
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({
|
||||
@@ -41,7 +62,7 @@ const layer = Layer.effect(
|
||||
export const node = makeGlobalNode({
|
||||
service: SessionExecution.Service,
|
||||
layer,
|
||||
deps: [SessionStore.node, LocationServiceMap.node],
|
||||
deps: [SessionStore.node, LocationServiceMap.node, EventV2.node],
|
||||
})
|
||||
|
||||
export * as SessionExecutionLocal from "./local"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Database } from "../database/database"
|
||||
import { MessageDecodeError } from "./error"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionContextEpochTable, SessionMessageTable } from "./sql"
|
||||
import { SessionContextCheckpointTable, SessionMessageTable } from "./sql"
|
||||
|
||||
type DatabaseService = Database.Interface["db"]
|
||||
|
||||
@@ -33,6 +33,9 @@ const messageRows = Effect.fnUntraced(function* (
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
// Keep system updates visible in the gap between a completed compaction
|
||||
// and the next prepared turn's rebaseline, when their content is not yet
|
||||
// folded into a new baseline.
|
||||
compaction
|
||||
? or(
|
||||
gte(SessionMessageTable.seq, compaction.seq),
|
||||
@@ -67,9 +70,9 @@ export const load = Effect.fn("SessionHistory.load")(function* (db: DatabaseServ
|
||||
const [epoch, compaction] = yield* Effect.all(
|
||||
[
|
||||
db
|
||||
.select({ baselineSeq: SessionContextEpochTable.baseline_seq })
|
||||
.from(SessionContextEpochTable)
|
||||
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
||||
.select({ baselineSeq: SessionContextCheckpointTable.baseline_seq })
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie),
|
||||
latestCompaction(db, sessionID),
|
||||
@@ -79,14 +82,6 @@ export const load = Effect.fn("SessionHistory.load")(function* (db: DatabaseServ
|
||||
return yield* Effect.forEach(yield* messageRows(db, sessionID, compaction, epoch?.baselineSeq), decodeMessageRow)
|
||||
})
|
||||
|
||||
export const loadForRunner = Effect.fn("SessionHistory.loadForRunner")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
baselineSeq: number,
|
||||
) {
|
||||
return (yield* entriesForRunner(db, sessionID, baselineSeq)).map((entry) => entry.message)
|
||||
})
|
||||
|
||||
export const entriesForRunner = Effect.fn("SessionHistory.entriesForRunner")(function* (
|
||||
db: DatabaseService,
|
||||
sessionID: SessionSchema.ID,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as SessionInput from "./input"
|
||||
|
||||
import { and, asc, eq, isNull, lte } from "drizzle-orm"
|
||||
import { and, asc, eq, isNull } from "drizzle-orm"
|
||||
import { DateTime, Effect, Schema } from "effect"
|
||||
import { Admitted, Delivery } from "@opencode-ai/schema/session-input"
|
||||
import type { Database } from "../database/database"
|
||||
@@ -145,26 +145,11 @@ export const projectPrompted = Effect.fn("SessionInput.projectPrompted")(functio
|
||||
return
|
||||
}
|
||||
|
||||
// Every Prompted event is published from an admitted inbox row, so a missing or
|
||||
// divergent row on replay is an invariant violation.
|
||||
const stored = yield* find(db, input.id)
|
||||
if (stored) {
|
||||
if (!matchesProjection(stored, input) || stored.promotedSeq !== input.promotedSeq)
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
return
|
||||
}
|
||||
|
||||
yield* db
|
||||
.insert(SessionInputTable)
|
||||
.values({
|
||||
id: input.id,
|
||||
session_id: input.sessionID,
|
||||
prompt: encodePrompt(input.prompt),
|
||||
delivery: input.delivery,
|
||||
admitted_seq: input.promotedSeq,
|
||||
promoted_seq: input.promotedSeq,
|
||||
time_created: DateTime.toEpochMillis(input.timeCreated),
|
||||
})
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
if (!stored || !matchesProjection(stored, input) || stored.promotedSeq !== input.promotedSeq)
|
||||
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
|
||||
})
|
||||
|
||||
export const hasPending = Effect.fn("SessionInput.hasPending")(function* (
|
||||
@@ -195,9 +180,8 @@ export const equivalent = (
|
||||
readonly prompt: Prompt
|
||||
readonly delivery: Delivery
|
||||
},
|
||||
) => input.delivery === expected.delivery && matchesPrompt(input, expected)
|
||||
|
||||
const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionSchema.ID; readonly prompt: Prompt }) =>
|
||||
) =>
|
||||
input.delivery === expected.delivery &&
|
||||
input.sessionID === expected.sessionID &&
|
||||
JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
|
||||
|
||||
@@ -246,7 +230,6 @@ export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
|
||||
db: DatabaseService,
|
||||
events: EventV2.Interface,
|
||||
sessionID: SessionSchema.ID,
|
||||
cutoff: number,
|
||||
) {
|
||||
const rows = yield* db
|
||||
.select()
|
||||
@@ -256,7 +239,6 @@ export const promoteSteers = Effect.fn("SessionInput.promoteSteers")(function* (
|
||||
eq(SessionInputTable.session_id, sessionID),
|
||||
isNull(SessionInputTable.promoted_seq),
|
||||
eq(SessionInputTable.delivery, "steer"),
|
||||
lte(SessionInputTable.admitted_seq, cutoff),
|
||||
),
|
||||
)
|
||||
.orderBy(asc(SessionInputTable.admitted_seq))
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
export * as SessionInstructions from "./instructions"
|
||||
|
||||
import { relative } from "path"
|
||||
import { Context, DateTime, Effect, Layer, Option, Ref, Schema } from "effect"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { EventV2 } from "../event"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { Location } from "../location"
|
||||
import { SessionEvent } from "./event"
|
||||
import { MessageDecodeError } from "./error"
|
||||
import { SessionMessage } from "./message"
|
||||
import { SessionSchema } from "./schema"
|
||||
import { SessionStore } from "./store"
|
||||
|
||||
const InjectedMetadata = Schema.Struct({
|
||||
instruction: Schema.Struct({ paths: Schema.Array(Schema.String) }),
|
||||
})
|
||||
|
||||
export interface Interface {
|
||||
readonly load: (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly paths: ReadonlyArray<string>
|
||||
}) => Effect.Effect<void, MessageDecodeError | FSUtil.Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionInstructions") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
const fs = yield* FSUtil.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const location = yield* Location.Service
|
||||
// Resolved once for the Location layer; the synthetic text and dedup ledger keep
|
||||
// absolute paths, but the human-facing description shows paths relative to the project
|
||||
// root so opening a subdirectory still describes paths from the project root.
|
||||
const root = FSUtil.resolve(location.project.directory)
|
||||
// Same-turn parallel reads settle concurrently, so an in-memory claim guards each
|
||||
// Session/path pair before any filesystem work. The durable history check below covers
|
||||
// paths injected in earlier turns after this Location layer was reopened.
|
||||
const injected = yield* Ref.make<Map<SessionSchema.ID, Set<string>>>(new Map())
|
||||
|
||||
const load = Effect.fn("SessionInstructions.load")(function* (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly paths: ReadonlyArray<string>
|
||||
}) {
|
||||
const claimed = yield* Ref.modify(injected, (map) => {
|
||||
const existing = map.get(input.sessionID) ?? new Set<string>()
|
||||
const newlyClaimed = input.paths.filter((path) => !existing.has(path))
|
||||
if (newlyClaimed.length === 0) return [newlyClaimed, map]
|
||||
const next = new Map(map)
|
||||
next.set(input.sessionID, new Set([...existing, ...newlyClaimed]))
|
||||
return [newlyClaimed, next]
|
||||
})
|
||||
if (claimed.length === 0) return
|
||||
const alreadyInjected = yield* previouslyInjected(store, input.sessionID)
|
||||
const toInject = claimed.filter((path) => !alreadyInjected.has(path))
|
||||
if (toInject.length === 0) return
|
||||
const files = yield* Effect.forEach(
|
||||
toInject,
|
||||
(path) =>
|
||||
fs.readFileStringSafe(path).pipe(
|
||||
Effect.map((content) => (content === undefined ? undefined : { path, content })),
|
||||
),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
const readable = files.filter((file): file is { path: string; content: string } => file !== undefined)
|
||||
if (readable.length === 0) return
|
||||
// Publish directly rather than through SessionV2.synthetic: a Location-scoped layer
|
||||
// cannot depend on SessionV2 (it routes through LocationServiceMap, forming a type
|
||||
// cycle with this node). The durable publish is what makes the synthetic visible on
|
||||
// the next projected history reload. The dedup ledger lives on the synthetic message
|
||||
// metadata so it survives across Location layer restarts.
|
||||
yield* events.publish(SessionEvent.Synthetic, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
text: readable.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n"),
|
||||
description: `Loaded ${readable.map((file) => describePath(root, file.path)).join(", ")}`,
|
||||
metadata: { instruction: { paths: readable.map((file) => file.path) } },
|
||||
})
|
||||
})
|
||||
|
||||
return Service.of({ load })
|
||||
}),
|
||||
)
|
||||
|
||||
function previouslyInjected(store: SessionStore.Interface, sessionID: SessionSchema.ID) {
|
||||
return Effect.gen(function* () {
|
||||
const history = yield* store.context(sessionID)
|
||||
return new Set(
|
||||
history
|
||||
.filter((message): message is SessionMessage.Synthetic => message.type === "synthetic")
|
||||
.flatMap(
|
||||
(message) =>
|
||||
Option.getOrUndefined(Schema.decodeUnknownOption(InjectedMetadata)(message.metadata))?.instruction.paths ??
|
||||
[],
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Paths are normally discovered under the project root, so the description shows them
|
||||
// relative to it. A directly-loaded path outside the root falls back to its absolute form
|
||||
// rather than emitting `../..` chains.
|
||||
function describePath(root: string, path: string) {
|
||||
return FSUtil.contains(root, path) ? relative(root, path) : path
|
||||
}
|
||||
|
||||
export const node = makeLocationNode({
|
||||
name: "session-instructions",
|
||||
layer,
|
||||
deps: [EventV2.node, FSUtil.node, Location.node, SessionStore.node],
|
||||
})
|
||||
@@ -139,6 +139,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
)
|
||||
},
|
||||
"session.next.prompt.admitted": () => Effect.void,
|
||||
"session.next.execution.settled": () => Effect.void,
|
||||
"session.next.context.updated": (event) =>
|
||||
adapter.appendMessage(
|
||||
SessionMessage.System.make({
|
||||
@@ -154,6 +155,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
|
||||
sessionID: event.data.sessionID,
|
||||
text: event.data.text,
|
||||
description: event.data.description,
|
||||
metadata: event.data.metadata,
|
||||
id: event.data.messageID,
|
||||
type: "synthetic",
|
||||
time: { created: event.data.timestamp },
|
||||
|
||||
@@ -12,8 +12,15 @@ import { SessionMessage } from "./message"
|
||||
import { SessionMessageUpdater } from "./message-updater"
|
||||
import { SessionInput } from "./input"
|
||||
import { WorkspaceV2 } from "../workspace"
|
||||
import { SessionContextEpoch } from "./context-epoch"
|
||||
import { MessageTable, PartTable, SessionInputTable, SessionMessageTable, SessionTable } from "./sql"
|
||||
import { SessionContextCheckpoint } from "./context-checkpoint"
|
||||
import {
|
||||
MessageTable,
|
||||
PartTable,
|
||||
SessionContextCheckpointTable,
|
||||
SessionInputTable,
|
||||
SessionMessageTable,
|
||||
SessionTable,
|
||||
} from "./sql"
|
||||
import type { DeepMutable } from "../schema"
|
||||
import { Slug } from "../util/slug"
|
||||
|
||||
@@ -156,12 +163,16 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
.where(
|
||||
and(eq(SessionMessageTable.session_id, event.data.parentID), eq(SessionMessageTable.id, event.data.messageID)),
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, event.data.parentID),
|
||||
eq(SessionMessageTable.id, event.data.messageID),
|
||||
),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
: undefined
|
||||
if (event.data.messageID && !boundary) return yield* Effect.die(`Fork boundary message not found: ${event.data.messageID}`)
|
||||
if (event.data.messageID && !boundary)
|
||||
return yield* Effect.die(`Fork boundary message not found: ${event.data.messageID}`)
|
||||
const copied = yield* db
|
||||
.select({ seq: SessionMessageTable.seq })
|
||||
.from(SessionMessageTable)
|
||||
@@ -206,6 +217,23 @@ const projectFork = Effect.fn("SessionProjector.projectFork")(function* (
|
||||
.pipe(Effect.orDie)
|
||||
if (!stored) return yield* Effect.die(new SessionAlreadyProjected())
|
||||
|
||||
// The fork inherits the parent's transcript, so it inherits the context
|
||||
// checkpoint that transcript was built against: copied message seqs keep
|
||||
// folding at the same baseline horizon.
|
||||
const checkpoint = yield* db
|
||||
.select()
|
||||
.from(SessionContextCheckpointTable)
|
||||
.where(eq(SessionContextCheckpointTable.session_id, event.data.parentID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (checkpoint) {
|
||||
yield* db
|
||||
.insert(SessionContextCheckpointTable)
|
||||
.values({ ...checkpoint, session_id: event.data.sessionID })
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
}
|
||||
|
||||
const usage = emptyUsage()
|
||||
let cursor = -1
|
||||
while (true) {
|
||||
@@ -452,7 +480,7 @@ const layer = Layer.effectDiscard(
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* SessionContextEpoch.reset(db, event.data.sessionID)
|
||||
yield* SessionContextCheckpoint.reset(db, event.data.sessionID)
|
||||
}),
|
||||
)
|
||||
yield* events.project(SessionV1.Event.Deleted, (event) =>
|
||||
@@ -666,7 +694,7 @@ const layer = Layer.effectDiscard(
|
||||
.where(eq(SessionTable.id, event.data.sessionID))
|
||||
.run()
|
||||
.pipe(Effect.orDie)
|
||||
yield* SessionContextEpoch.reset(db, event.data.sessionID)
|
||||
yield* SessionContextCheckpoint.reset(db, event.data.sessionID)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -6,111 +6,129 @@ import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
|
||||
export interface Coordinator<Key, E> {
|
||||
/** Snapshots keys with an execution owned by this coordinator. */
|
||||
readonly active: Effect.Effect<ReadonlySet<Key>>
|
||||
/** Starts execution while idle or joins the active execution. */
|
||||
/** Starts an execution while idle, or joins the active execution and returns its exit. */
|
||||
readonly run: (key: Key) => Effect.Effect<void, E>
|
||||
/** Registers one coalesced follow-up after newly recorded work. */
|
||||
/** Rings the doorbell: an idle key starts an execution; an active one drains again before settling. */
|
||||
readonly wake: (key: Key) => Effect.Effect<void>
|
||||
/** Stops active execution and waits for its cleanup. */
|
||||
/** Stops the active execution, clears its doorbell, and waits for cleanup. No-op when idle. */
|
||||
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>
|
||||
}
|
||||
|
||||
type Entry<E> = {
|
||||
/**
|
||||
* One execution is a busy period for one key: one fiber that drains from the first wake
|
||||
* until the key would stay idle. `pendingWake` is the doorbell: work recorded during the
|
||||
* execution rings it, and the execution loop drains again instead of ending. The doorbell
|
||||
* 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> = {
|
||||
readonly done: Deferred.Deferred<void, E>
|
||||
owner?: Fiber.Fiber<void, never>
|
||||
owner?: Fiber.Fiber<void>
|
||||
pendingWake: boolean
|
||||
stopping: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* ```text
|
||||
* wake | run
|
||||
* idle ──────────────▶ execution (one fiber)
|
||||
* drain ⟲ doorbell rung mid-drain
|
||||
* │ exit (settled hook runs)
|
||||
* doorbell quiet ◀───────┴───────▶ doorbell rung
|
||||
* idle, waiters get exit successor execution,
|
||||
* waiters get this exit
|
||||
* ```
|
||||
*/
|
||||
export const make = <Key, E>(options: {
|
||||
readonly drain: (key: Key, force: boolean) => Effect.Effect<void, E>
|
||||
/**
|
||||
* 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>) => Effect.Effect<void>
|
||||
}): Effect.Effect<Coordinator<Key, E>, never, Scope.Scope> =>
|
||||
Effect.gen(function* () {
|
||||
const active = new Map<Key, Entry<E>>()
|
||||
const executions = new Map<Key, Execution<E>>()
|
||||
const fork = yield* FiberSet.makeRuntime<never, void, never>()
|
||||
|
||||
const makeEntry = (): Entry<E> => ({
|
||||
done: Deferred.makeUnsafe<void, E>(),
|
||||
pendingWake: false,
|
||||
stopping: false,
|
||||
})
|
||||
const loop = (key: Key, execution: Execution<E>, force: boolean): Effect.Effect<void, E> =>
|
||||
Effect.suspend(() => options.drain(key, force)).pipe(
|
||||
Effect.flatMap(() =>
|
||||
Effect.suspend(() => {
|
||||
if (execution.stopping || !execution.pendingWake) return Effect.void
|
||||
execution.pendingWake = false
|
||||
// Trampoline so drains that complete synchronously cannot grow the stack.
|
||||
return Effect.yieldNow.pipe(Effect.andThen(loop(key, execution, false)))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const start = (key: Key, entry: Entry<E>, force: boolean, successor = false) => {
|
||||
const ready = Deferred.makeUnsafe<void>()
|
||||
const owner = fork(
|
||||
(successor ? Effect.yieldNow : Deferred.await(ready)).pipe(
|
||||
Effect.andThen(Effect.suspend(() => options.drain(key, force))),
|
||||
Effect.onExit((exit) => Effect.sync(() => settle(key, entry, exit))),
|
||||
const start = (key: Key, force: boolean) => {
|
||||
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(loop(key, execution, force)),
|
||||
Effect.onExit((exit) => options.settled?.(key, exit) ?? Effect.void),
|
||||
Effect.onExit((exit) => Effect.sync(() => settle(key, execution, exit))),
|
||||
Effect.exit,
|
||||
Effect.asVoid,
|
||||
),
|
||||
)
|
||||
entry.owner = owner
|
||||
if (!successor) Deferred.doneUnsafe(ready, Effect.void)
|
||||
return execution
|
||||
}
|
||||
|
||||
const settle = (key: Key, entry: Entry<E>, exit: Exit.Exit<void, E>) => {
|
||||
if (Exit.isSuccess(exit) && !entry.stopping && entry.pendingWake) {
|
||||
entry.pendingWake = false
|
||||
start(key, entry, false, true)
|
||||
return
|
||||
}
|
||||
|
||||
const successor = entry.pendingWake ? makeEntry() : undefined
|
||||
if (successor === undefined) active.delete(key)
|
||||
else {
|
||||
active.set(key, successor)
|
||||
start(key, successor, false, true)
|
||||
}
|
||||
Deferred.doneUnsafe(entry.done, exit)
|
||||
// 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>, exit: Exit.Exit<void, E>) => {
|
||||
if (execution.pendingWake) start(key, false)
|
||||
else executions.delete(key)
|
||||
Deferred.doneUnsafe(execution.done, exit)
|
||||
}
|
||||
|
||||
const run = (key: Key): Effect.Effect<void, E> =>
|
||||
Effect.uninterruptibleMask((restore) => {
|
||||
const entry = active.get(key)
|
||||
if (entry !== undefined) {
|
||||
if (entry.stopping) return restore(Deferred.await(entry.done).pipe(Effect.andThen(run(key))))
|
||||
return restore(Deferred.await(entry.done))
|
||||
const execution = executions.get(key)
|
||||
if (execution !== undefined) {
|
||||
// A stopping execution refuses joiners: wait out its cleanup, then run fresh.
|
||||
if (execution.stopping) return restore(Deferred.await(execution.done).pipe(Effect.andThen(run(key))))
|
||||
return restore(Deferred.await(execution.done))
|
||||
}
|
||||
|
||||
const next = makeEntry()
|
||||
active.set(key, next)
|
||||
start(key, next, true)
|
||||
return restore(Deferred.await(next.done))
|
||||
return restore(Deferred.await(start(key, true).done))
|
||||
})
|
||||
|
||||
const wake = (key: Key) =>
|
||||
Effect.sync(() => {
|
||||
const entry = active.get(key)
|
||||
if (entry !== undefined) {
|
||||
entry.pendingWake = true
|
||||
const execution = executions.get(key)
|
||||
if (execution !== undefined) {
|
||||
execution.pendingWake = true
|
||||
return
|
||||
}
|
||||
|
||||
const next = makeEntry()
|
||||
active.set(key, next)
|
||||
start(key, next, false)
|
||||
start(key, false)
|
||||
})
|
||||
|
||||
const interrupt = (key: Key): Effect.Effect<void> =>
|
||||
Effect.suspend(() => {
|
||||
const entry = active.get(key)
|
||||
if (entry?.owner === undefined) return Effect.void
|
||||
entry.stopping = true
|
||||
entry.pendingWake = false
|
||||
return Fiber.interrupt(entry.owner)
|
||||
const execution = executions.get(key)
|
||||
if (execution?.owner === undefined) return Effect.void
|
||||
execution.stopping = true
|
||||
execution.pendingWake = false
|
||||
return Fiber.interrupt(execution.owner)
|
||||
})
|
||||
|
||||
// Each successful drain reuses its entry.done across coalesced wakes, so one await
|
||||
// already spans steered and queued continuation. Re-check after it settles to cover a
|
||||
// fresh wake (or a failure/stopping successor) that installs a new entry.
|
||||
// One execution's `done` already spans coalesced continuations; re-check after it
|
||||
// settles to cover a successor execution started by a late doorbell.
|
||||
const awaitIdle = (key: Key): Effect.Effect<void> =>
|
||||
Effect.suspend(() => {
|
||||
const entry = active.get(key)
|
||||
if (entry === undefined) return Effect.void
|
||||
return Deferred.await(entry.done).pipe(Effect.exit, Effect.andThen(awaitIdle(key)))
|
||||
const execution = executions.get(key)
|
||||
if (execution === undefined) return Effect.void
|
||||
return Deferred.await(execution.done).pipe(Effect.exit, Effect.andThen(awaitIdle(key)))
|
||||
})
|
||||
|
||||
return { active: Effect.sync(() => new Set(active.keys())), run, wake, interrupt, awaitIdle }
|
||||
return { active: Effect.sync(() => new Set(executions.keys())), run, wake, interrupt, awaitIdle }
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ export * as SessionRunner from "./index"
|
||||
import type { LLMError } from "@opencode-ai/llm"
|
||||
import { Context, Effect } from "effect"
|
||||
import { SessionSchema } from "../schema"
|
||||
import type { ContextSnapshotDecodeError, MessageDecodeError } 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"
|
||||
@@ -12,7 +12,6 @@ export type RunError =
|
||||
| LLMError
|
||||
| SessionRunnerModel.Error
|
||||
| MessageDecodeError
|
||||
| ContextSnapshotDecodeError
|
||||
| SystemContext.InitializationBlocked
|
||||
| ToolOutputStore.Error
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@ import {
|
||||
isContextOverflowFailure,
|
||||
type ProviderErrorEvent,
|
||||
} from "@opencode-ai/llm"
|
||||
import { Cause, DateTime, Effect, 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"
|
||||
import { EventV2 } from "../../event"
|
||||
import { Location } from "../../location"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { QuestionV2 } from "../../question"
|
||||
import { SystemContext } from "../../system-context/index"
|
||||
import { SystemContextRegistry } from "../../system-context/registry"
|
||||
import { SystemContextBuiltIns } from "../../system-context/builtins"
|
||||
import { InstructionContext } from "../../instruction-context"
|
||||
import { SkillGuidance } from "../../skill/guidance"
|
||||
import { ReferenceGuidance } from "../../reference/guidance"
|
||||
import { McpGuidance } from "../../mcp/guidance"
|
||||
import { SessionContextEntry } from "../context-entry"
|
||||
import { ToolRegistry } from "../../tool/registry"
|
||||
import { ToolOutputStore } from "../../tool-output-store"
|
||||
import { SessionContextEpoch } from "../context-epoch"
|
||||
import { SessionContextCheckpoint } from "../context-checkpoint"
|
||||
import { SessionCompaction } from "../compaction"
|
||||
import { SessionEvent } from "../event"
|
||||
import { SessionHistory } from "../history"
|
||||
@@ -32,7 +32,7 @@ import { SessionInput } from "../input"
|
||||
import { SessionSchema } from "../schema"
|
||||
import { SessionStore } from "../store"
|
||||
import { SessionTitle } from "../title"
|
||||
import { type RunError, Service } from "./index"
|
||||
import { Service } from "./index"
|
||||
import { SessionRunnerModel } from "./model"
|
||||
import { createLLMEventPublisher } from "./publish-llm-event"
|
||||
import { toLLMMessages } from "./to-llm-message"
|
||||
@@ -102,10 +102,12 @@ const layer = Layer.effect(
|
||||
const models = yield* SessionRunnerModel.Service
|
||||
const store = yield* SessionStore.Service
|
||||
const location = yield* Location.Service
|
||||
const systemContext = yield* SystemContextRegistry.Service
|
||||
const builtins = yield* SystemContextBuiltIns.Service
|
||||
const instructions = yield* InstructionContext.Service
|
||||
const skillGuidance = yield* SkillGuidance.Service
|
||||
const referenceGuidance = yield* ReferenceGuidance.Service
|
||||
const mcpGuidance = yield* McpGuidance.Service
|
||||
const contextEntries = yield* SessionContextEntry.Service
|
||||
const snapshots = yield* Snapshot.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
@@ -121,13 +123,10 @@ const layer = Layer.effect(
|
||||
return session
|
||||
})
|
||||
|
||||
const getContext = Effect.fn("SessionRunner.getContext")(function* (sessionID: SessionSchema.ID) {
|
||||
return yield* store.context(sessionID)
|
||||
})
|
||||
const failInterruptedTools = Effect.fn("SessionRunner.failInterruptedTools")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
) {
|
||||
for (const message of yield* getContext(sessionID)) {
|
||||
for (const message of yield* store.context(sessionID)) {
|
||||
if (message.type !== "assistant") continue
|
||||
for (const tool of message.content) {
|
||||
if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue
|
||||
@@ -153,26 +152,18 @@ const layer = Layer.effect(
|
||||
const isQuestionRejected = (cause: Cause.Cause<unknown>) =>
|
||||
cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionV2.RejectedError)
|
||||
|
||||
type TurnTransition =
|
||||
// Automatic compaction completed; rebuild the request from compacted history.
|
||||
| { readonly _tag: "ContinueAfterCompaction"; readonly step: number }
|
||||
// Overflow compaction completed; rebuild once through the path without overflow recovery.
|
||||
| { readonly _tag: "ContinueAfterOverflowCompaction"; readonly step: number }
|
||||
|
||||
class TurnTransitionError extends Error {
|
||||
constructor(readonly transition: TurnTransition) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
const continueAfterCompaction = (step: number) => new TurnTransitionError({ _tag: "ContinueAfterCompaction", step })
|
||||
const continueAfterOverflowCompaction = (step: number) =>
|
||||
new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
|
||||
|
||||
const loadSystemContext = (agent: AgentV2.Selection) =>
|
||||
Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load(), mcpGuidance.load(agent)], {
|
||||
concurrency: "unbounded",
|
||||
}).pipe(Effect.map(SystemContext.combine))
|
||||
const loadSystemContext = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
|
||||
Effect.all(
|
||||
[
|
||||
builtins.load(),
|
||||
instructions.load(),
|
||||
skillGuidance.load(agent),
|
||||
referenceGuidance.load(),
|
||||
mcpGuidance.load(agent),
|
||||
contextEntries.load(sessionID),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
).pipe(Effect.map(SystemContext.combine))
|
||||
|
||||
const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
@@ -184,24 +175,29 @@ const layer = Layer.effect(
|
||||
if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID)
|
||||
return yield* Effect.interrupt
|
||||
const agent = yield* agents.select(session.agent)
|
||||
const initialized = yield* SessionContextEpoch.initialize(db, loadSystemContext(agent), session.id)
|
||||
// Establish what the model knows before admitting what the user said, so
|
||||
// a blocked first turn leaves pending inputs untouched.
|
||||
const checkpoint = yield* SessionContextCheckpoint.prepare(
|
||||
db,
|
||||
events,
|
||||
loadSystemContext(agent, session.id),
|
||||
session.id,
|
||||
)
|
||||
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
||||
let needsContinuation = false
|
||||
let currentStep = step
|
||||
if (promotion) {
|
||||
const cutoff = yield* EventV2.latestSequence(db, session.id)
|
||||
let promoted = 0
|
||||
if (promotion === "steer") promoted = yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
||||
if (promotion === "steer") promoted = yield* SessionInput.promoteSteers(db, events, session.id)
|
||||
if (promotion === "queue") {
|
||||
promoted += Number(yield* SessionInput.promoteNextQueued(db, events, session.id))
|
||||
promoted += yield* SessionInput.promoteSteers(db, events, session.id, cutoff)
|
||||
promoted += yield* SessionInput.promoteSteers(db, events, session.id)
|
||||
}
|
||||
if (promoted > 0) currentStep = 1
|
||||
}
|
||||
const system =
|
||||
initialized ?? (yield* SessionContextEpoch.prepare(db, events, loadSystemContext(agent), session.id))
|
||||
const model = yield* models.resolve(session)
|
||||
const entries = yield* SessionHistory.entriesForRunner(db, session.id, system.baselineSeq)
|
||||
const resolved = yield* models.resolve(session)
|
||||
const model = resolved.model
|
||||
const entries = yield* SessionHistory.entriesForRunner(db, session.id, checkpoint.baselineSeq)
|
||||
const context = entries.map((entry) => entry.message)
|
||||
const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps
|
||||
const toolMaterialization = isLastStep
|
||||
@@ -211,29 +207,34 @@ const layer = Layer.effect(
|
||||
const request = LLM.request({
|
||||
model,
|
||||
providerOptions: { openai: { promptCacheKey } },
|
||||
system: [agent.info?.system ? agent.info.system : SessionRunnerSystemPrompt.provider(model), system.baseline]
|
||||
system: [
|
||||
agent.info?.system ? agent.info.system : SessionRunnerSystemPrompt.provider(model),
|
||||
checkpoint.baseline,
|
||||
]
|
||||
.filter((part): part is string => part !== undefined && part.length > 0)
|
||||
.map(SystemPart.make),
|
||||
messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])],
|
||||
tools: toolMaterialization?.definitions ?? [],
|
||||
toolChoice: isLastStep ? "none" : undefined,
|
||||
})
|
||||
// Automatic compaction completed; rebuild the request from compacted history.
|
||||
if (yield* compaction.compactIfNeeded({ sessionID: session.id, messages: context, request }))
|
||||
return yield* Effect.die(continueAfterCompaction(currentStep))
|
||||
return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||
const startSnapshot = yield* snapshots.capture()
|
||||
const publisher = createLLMEventPublisher(events, {
|
||||
sessionID: session.id,
|
||||
agent: agent.id,
|
||||
model: {
|
||||
id: ModelV2.ID.make(model.id),
|
||||
providerID: ProviderV2.ID.make(model.provider),
|
||||
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
||||
},
|
||||
// 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,
|
||||
snapshot: startSnapshot,
|
||||
})
|
||||
const publication = Semaphore.makeUnsafe(1)
|
||||
// Durable publishes are serialized so tool fibers and turn 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> = []) =>
|
||||
publication.withPermit(publisher.publish(event, outputPaths))
|
||||
serialized(publisher.publish(event, outputPaths))
|
||||
let overflowFailure: ProviderErrorEvent | undefined
|
||||
const providerStream = llm.stream(request).pipe(
|
||||
Stream.runForEach((event) =>
|
||||
@@ -248,9 +249,7 @@ const layer = Layer.effect(
|
||||
yield* publish(event)
|
||||
if (event.type !== "tool-call" || event.providerExecuted) return
|
||||
if (!toolMaterialization) {
|
||||
yield* publication.withPermit(
|
||||
publisher.failUnsettledTools("Tools are disabled after the maximum agent steps"),
|
||||
)
|
||||
yield* serialized(publisher.failUnsettledTools("Tools are disabled after the maximum agent steps"))
|
||||
return
|
||||
}
|
||||
needsContinuation = true
|
||||
@@ -279,114 +278,138 @@ const layer = Layer.effect(
|
||||
).pipe(FiberSet.run(toolFibers))
|
||||
}),
|
||||
),
|
||||
Effect.ensuring(publication.withPermit(publisher.flush())),
|
||||
Effect.ensuring(serialized(publisher.flush())),
|
||||
)
|
||||
|
||||
// Captures the end snapshot, diffs it against the turn's start, and durably ends the
|
||||
// assistant step.
|
||||
const publishStepEnd = (settlement: NonNullable<ReturnType<typeof publisher.stepSettlement>>) =>
|
||||
Effect.gen(function* () {
|
||||
const endSnapshot = yield* snapshots.capture()
|
||||
const files =
|
||||
startSnapshot && endSnapshot
|
||||
? yield* snapshots
|
||||
.files({ from: startSnapshot, to: endSnapshot })
|
||||
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
: undefined
|
||||
yield* serialized(
|
||||
events.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: yield* publisher.startAssistant(),
|
||||
finish: settlement.finish,
|
||||
cost: 0,
|
||||
tokens: settlement.tokens,
|
||||
snapshot: endSnapshot,
|
||||
files,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return yield* Effect.uninterruptibleMask((restore) =>
|
||||
Effect.gen(function* () {
|
||||
// Gather the evidence: how did the provider stream end?
|
||||
const stream = yield* restore(providerStream).pipe(Effect.exit)
|
||||
const failure =
|
||||
stream._tag === "Failure" ? Option.getOrUndefined(Cause.findErrorOption(stream.cause)) : undefined
|
||||
const streamFailure = Option.getOrUndefined(Exit.findErrorOption(stream))
|
||||
// Note: Exit.hasInterrupts is a type guard whose false branch unsoundly narrows
|
||||
// away non-interrupt failures, so both interrupt checks stay Cause-based.
|
||||
const streamInterrupted = stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)
|
||||
|
||||
// A context overflow before any assistant output is recoverable: compact and
|
||||
// restart the turn instead of surfacing the provider error.
|
||||
if (
|
||||
recoverOverflow &&
|
||||
!publisher.hasAssistantStarted() &&
|
||||
isContextOverflowFailure(overflowFailure ?? failure) &&
|
||||
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
|
||||
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, request })))
|
||||
)
|
||||
return yield* Effect.die(continueAfterOverflowCompaction(currentStep))
|
||||
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
|
||||
|
||||
// An unrecovered held-back overflow becomes the turn's durable provider error. A
|
||||
// thrown LLM failure fails hosted tool calls and the assistant unless a provider
|
||||
// error was already recorded from the stream.
|
||||
if (overflowFailure) yield* publish(overflowFailure)
|
||||
const llmFailure = failure instanceof LLMError ? failure : undefined
|
||||
const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
|
||||
if (llmFailure && !publisher.hasProviderError()) {
|
||||
yield* publication.withPermit(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
||||
yield* publication.withPermit(publisher.failAssistant(llmFailure.reason.message))
|
||||
yield* serialized(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
||||
yield* serialized(publisher.failAssistant(llmFailure.reason.message))
|
||||
}
|
||||
if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers)
|
||||
// Provider error events only arrive from the stream, so the flag is final here.
|
||||
const providerFailed = publisher.hasProviderError()
|
||||
|
||||
// Settle tool fibers: an interrupted stream abandons unstarted tool work first.
|
||||
if (streamInterrupted) yield* FiberSet.clear(toolFibers)
|
||||
const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit)
|
||||
const streamInterrupted = stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)
|
||||
const toolsInterrupted = settled._tag === "Failure" && Cause.hasInterrupts(settled.cause)
|
||||
if (settled._tag === "Failure" && isQuestionRejected(settled.cause)) {
|
||||
const questionDismissed = settled._tag === "Failure" && isQuestionRejected(settled.cause)
|
||||
|
||||
if (questionDismissed || streamInterrupted || toolsInterrupted) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* publication.withPermit(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
yield* publication.withPermit(publisher.failAssistant("Provider turn interrupted"))
|
||||
return yield* Effect.interrupt
|
||||
yield* serialized(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
yield* serialized(publisher.failAssistant("Provider turn interrupted"))
|
||||
// Match V1: dismissing a question halts the loop like an interruption.
|
||||
if (questionDismissed) return yield* Effect.interrupt
|
||||
}
|
||||
if (streamInterrupted || toolsInterrupted) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* publication.withPermit(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
yield* publication.withPermit(publisher.failAssistant("Provider turn interrupted"))
|
||||
}
|
||||
if (settled._tag === "Failure" && !Cause.hasInterrupts(settled.cause)) {
|
||||
const failure = Cause.squash(settled.cause)
|
||||
// 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 turn 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 ? settled.cause : undefined
|
||||
const infraError =
|
||||
settledFailure === undefined ? undefined : Option.getOrUndefined(Cause.findErrorOption(settledFailure))
|
||||
if (settledFailure !== undefined) {
|
||||
const failure = infraError ?? Cause.squash(settledFailure)
|
||||
const message = failure instanceof Error ? failure.message : String(failure)
|
||||
yield* publication.withPermit(publisher.failUnsettledTools(`Tool execution failed: ${message}`))
|
||||
yield* serialized(publisher.failUnsettledTools(`Tool execution failed: ${message}`))
|
||||
if (infraError !== undefined)
|
||||
yield* serialized(publisher.failAssistant(`Tool execution failed: ${message}`))
|
||||
}
|
||||
|
||||
const stepSettlement = publisher.stepSettlement()
|
||||
if (stepSettlement && !streamInterrupted && !toolsInterrupted && !publisher.hasProviderError()) {
|
||||
const endSnapshot = yield* snapshots.capture()
|
||||
const files =
|
||||
startSnapshot && endSnapshot
|
||||
? yield* snapshots
|
||||
.files({ from: startSnapshot, to: endSnapshot })
|
||||
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
: undefined
|
||||
yield* publication.withPermit(
|
||||
events.publish(SessionEvent.Step.Ended, {
|
||||
sessionID: session.id,
|
||||
timestamp: yield* DateTime.now,
|
||||
assistantMessageID: yield* publisher.startAssistant(),
|
||||
finish: stepSettlement.finish,
|
||||
cost: 0,
|
||||
tokens: stepSettlement.tokens,
|
||||
snapshot: endSnapshot,
|
||||
files,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (publisher.hasProviderError())
|
||||
yield* publication.withPermit(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
if (stream._tag === "Success" && !publisher.hasProviderError())
|
||||
yield* publication.withPermit(publisher.failUnsettledTools("Provider did not return a tool result", true))
|
||||
const stepEndedCleanly =
|
||||
!streamInterrupted && !toolsInterrupted && infraError === undefined && !providerFailed
|
||||
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("Tool execution interrupted"))
|
||||
if (stream._tag === "Success" && !providerFailed)
|
||||
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" && Cause.hasInterrupts(settled.cause))
|
||||
if (settled._tag === "Failure" && (toolsInterrupted || infraError !== undefined))
|
||||
return yield* Effect.failCause(settled.cause)
|
||||
return { needsContinuation: !publisher.hasProviderError() && needsContinuation, step: currentStep }
|
||||
return {
|
||||
_tag: "Completed",
|
||||
needsContinuation: !providerFailed && needsContinuation,
|
||||
step: currentStep,
|
||||
} as const
|
||||
}),
|
||||
)
|
||||
}, Effect.scoped)
|
||||
type RunTurn = (
|
||||
|
||||
const runTurn = Effect.fnUntraced(function* (
|
||||
sessionID: SessionSchema.ID,
|
||||
promotion: SessionInput.Delivery | undefined,
|
||||
step: number,
|
||||
) => Effect.Effect<{ readonly needsContinuation: boolean; readonly step: number }, RunError>
|
||||
|
||||
const runAfterOverflowCompaction: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {
|
||||
return yield* runTurnAttempt(sessionID, promotion, step).pipe(
|
||||
Effect.catchDefect(
|
||||
Effect.fnUntraced(function* (defect) {
|
||||
if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect)
|
||||
if (defect.transition._tag === "ContinueAfterOverflowCompaction")
|
||||
return yield* Effect.die("Post-compaction provider attempt cannot recover another overflow")
|
||||
yield* Effect.yieldNow
|
||||
return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const runTurn: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) {
|
||||
return yield* runTurnAttempt(sessionID, promotion, step, compaction.compactAfterOverflow).pipe(
|
||||
Effect.catchDefect(
|
||||
Effect.fnUntraced(function* (defect) {
|
||||
if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect)
|
||||
yield* Effect.yieldNow
|
||||
if (defect.transition._tag === "ContinueAfterOverflowCompaction")
|
||||
return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step)
|
||||
return yield* runTurn(sessionID, undefined, defect.transition.step)
|
||||
}),
|
||||
),
|
||||
)
|
||||
) {
|
||||
// Compaction restarts rebuild the request from compacted history without re-promoting.
|
||||
// Overflow recovery is one-shot: a post-compaction attempt must not recover another
|
||||
// overflow, so the recovery hook is dropped after it fires.
|
||||
let recoverOverflow: typeof compaction.compactAfterOverflow | undefined = compaction.compactAfterOverflow
|
||||
let currentPromotion = promotion
|
||||
let currentStep = step
|
||||
while (true) {
|
||||
const attempt = yield* runTurnAttempt(sessionID, currentPromotion, currentStep, recoverOverflow)
|
||||
if (attempt._tag === "Completed") return { needsContinuation: attempt.needsContinuation, step: attempt.step }
|
||||
if (attempt._tag === "RestartAfterOverflowCompaction") recoverOverflow = undefined
|
||||
yield* Effect.yieldNow
|
||||
currentPromotion = undefined
|
||||
currentStep = attempt.step
|
||||
}
|
||||
})
|
||||
|
||||
// ExecutionSettled is published per execution (busy period) by SessionExecution, not per
|
||||
// drain here.
|
||||
const run = Effect.fn("SessionRunner.run")(function* (input: {
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly force: boolean
|
||||
@@ -418,9 +441,7 @@ const layer = Layer.effect(
|
||||
}
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
run,
|
||||
})
|
||||
return Service.of({ run })
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -435,10 +456,12 @@ export const node = makeLocationNode({
|
||||
SessionRunnerModel.node,
|
||||
SessionStore.node,
|
||||
Location.node,
|
||||
SystemContextRegistry.node,
|
||||
SystemContextBuiltIns.node,
|
||||
InstructionContext.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
McpGuidance.node,
|
||||
SessionContextEntry.node,
|
||||
SessionCompaction.node,
|
||||
SessionTitle.node,
|
||||
Config.node,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Catalog } from "../../catalog"
|
||||
import { Credential } from "../../credential"
|
||||
import { Integration } from "../../integration"
|
||||
import { ModelV2 } from "../../model"
|
||||
import { OpenAICodex } from "../../plugin/provider/openai-codex"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
import { SessionSchema } from "../schema"
|
||||
|
||||
@@ -71,8 +72,15 @@ export type Error =
|
||||
| UnsupportedApiError
|
||||
| Integration.AuthorizationError
|
||||
|
||||
export interface Resolved {
|
||||
/** Route-level model for provider requests; its id is the provider API model id, which may differ from the catalog id. */
|
||||
readonly model: Model
|
||||
/** Selected catalog identity. Durable records and displays must use this, never the API model id. */
|
||||
readonly ref: ModelV2.Ref
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
|
||||
readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Resolved, Error>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionRunnerModel") {}
|
||||
@@ -80,6 +88,16 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
||||
/** Test or embedding seam for supplying a model resolver directly. */
|
||||
export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
|
||||
|
||||
/** Builds a Resolved whose catalog identity mirrors the route model. Test or embedding seam. */
|
||||
export const resolved = (model: Model, variant?: ModelV2.VariantID): Resolved => ({
|
||||
model,
|
||||
ref: ModelV2.Ref.make({
|
||||
id: ModelV2.ID.make(model.id),
|
||||
providerID: ProviderV2.ID.make(model.provider),
|
||||
...(variant === undefined ? {} : { variant }),
|
||||
}),
|
||||
})
|
||||
|
||||
const apiKey = (model: ModelV2.Info, credential?: Credential.Value) => {
|
||||
if (credential?.type === "key") return Auth.value(credential.key)
|
||||
if (credential?.type === "oauth") return Auth.value(credential.access)
|
||||
@@ -96,11 +114,22 @@ const withDefaults = (model: ModelV2.Info, route: AnyRoute) => {
|
||||
provider: model.providerID,
|
||||
endpoint: model.api.url === undefined ? undefined : { baseURL: model.api.url },
|
||||
headers: model.request.headers,
|
||||
providerOptions: providerOptions(model),
|
||||
http: { body: httpBody },
|
||||
limits: { context: model.limit.context, output: model.limit.output },
|
||||
})
|
||||
}
|
||||
|
||||
const providerOptions = (
|
||||
model: ModelV2.Info,
|
||||
): { readonly [key: string]: { readonly [key: string]: unknown } } | undefined => {
|
||||
if (Object.keys(model.request.settings).length === 0) return undefined
|
||||
if (model.api.type !== "aisdk") return undefined
|
||||
if (model.api.package === "@ai-sdk/openai") return { openai: model.request.settings }
|
||||
if (model.api.package === "@ai-sdk/anthropic") return { anthropic: model.request.settings }
|
||||
if (model.api.package === "@ai-sdk/openai-compatible") return { openai: model.request.settings }
|
||||
}
|
||||
|
||||
export const withVariant = (
|
||||
model: ModelV2.Info,
|
||||
variantID: ModelV2.VariantID | undefined,
|
||||
@@ -118,6 +147,7 @@ export const withVariant = (
|
||||
return Effect.succeed(
|
||||
variant
|
||||
? produce(model, (draft) => {
|
||||
Object.assign(draft.request.settings, variant.settings)
|
||||
Object.assign(draft.request.headers, variant.headers)
|
||||
Object.assign(draft.request.body, variant.body)
|
||||
})
|
||||
@@ -140,6 +170,21 @@ export const fromCatalogModel = (
|
||||
})
|
||||
const key = apiKey(resolved, credential)
|
||||
if (resolved.api.type === "aisdk" && resolved.api.package === "@ai-sdk/openai") {
|
||||
// ChatGPT-plan OAuth tokens are not API-key credentials: the public API rejects
|
||||
// them, so requests must target the codex backend with the account header.
|
||||
if (OpenAICodex.isChatGPT(credential)) {
|
||||
const account = OpenAICodex.accountID(credential)
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAIResponses.route)
|
||||
.with({
|
||||
endpoint: { baseURL: OpenAICodex.baseURL },
|
||||
auth: (key === undefined ? Auth.none : Auth.bearer(key)).andThen(
|
||||
account === undefined ? Auth.none : Auth.headers({ "chatgpt-account-id": account }),
|
||||
),
|
||||
})
|
||||
.model({ id: resolved.api.id }),
|
||||
)
|
||||
}
|
||||
return Effect.succeed(
|
||||
withDefaults(resolved, OpenAIResponses.route)
|
||||
.with({ auth: key === undefined ? Auth.none : Auth.bearer(key) })
|
||||
@@ -205,11 +250,19 @@ const layer = Layer.effect(
|
||||
const connection = yield* integrations.connection.active(
|
||||
provider?.integrationID ?? Integration.ID.make(selected.providerID),
|
||||
)
|
||||
return yield* resolve(
|
||||
const model = yield* resolve(
|
||||
session,
|
||||
selected,
|
||||
connection ? yield* integrations.connection.resolve(connection) : undefined,
|
||||
)
|
||||
return {
|
||||
model,
|
||||
ref: ModelV2.Ref.make({
|
||||
id: selected.id,
|
||||
providerID: selected.providerID,
|
||||
...(session.model?.variant === undefined ? {} : { variant: session.model.variant }),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -130,7 +130,7 @@ function toLLMMessage(message: SessionMessage.Message, model: Model): Message[]
|
||||
}),
|
||||
]
|
||||
case "synthetic":
|
||||
return [Message.make({ id: message.id, role: "user", content: message.text, metadata: message.metadata })]
|
||||
return [Message.make({ id: message.id, role: "user", content: message.text })]
|
||||
case "skill":
|
||||
return [Message.make({ id: message.id, role: "user", content: message.text, metadata: message.metadata })]
|
||||
case "system":
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Timestamps } from "../database/schema.sql"
|
||||
import type { SystemContext } from "../system-context/index"
|
||||
import { AgentV2 } from "../agent"
|
||||
import type { Revert } from "@opencode-ai/schema/revert"
|
||||
import type { Schema } from "effect"
|
||||
|
||||
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
||||
type V1MessageData = Omit<SessionV1.Info, "id" | "sessionID">
|
||||
@@ -165,12 +166,26 @@ export const SessionInputTable = sqliteTable(
|
||||
],
|
||||
)
|
||||
|
||||
export const SessionContextEpochTable = sqliteTable("session_context_epoch", {
|
||||
export const SessionContextEntryTable = sqliteTable(
|
||||
"session_context_entry",
|
||||
{
|
||||
session_id: text()
|
||||
.$type<SessionSchema.ID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
key: text().notNull(),
|
||||
value: text({ mode: "json" }).notNull().$type<Schema.Json>(),
|
||||
...Timestamps,
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.session_id, table.key] })],
|
||||
)
|
||||
|
||||
export const SessionContextCheckpointTable = sqliteTable("session_context_epoch", {
|
||||
session_id: text()
|
||||
.$type<SessionSchema.ID>()
|
||||
.primaryKey()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
baseline: text().notNull(),
|
||||
snapshot: text({ mode: "json" }).notNull().$type<SystemContext.Snapshot>(),
|
||||
snapshot: text({ mode: "json" }).notNull().$type<SystemContext.Applied>(),
|
||||
baseline_seq: integer().notNull(),
|
||||
})
|
||||
|
||||
@@ -14,10 +14,6 @@ import { fromRow } from "./info"
|
||||
export interface Interface {
|
||||
readonly get: (sessionID: SessionSchema.ID) => Effect.Effect<SessionSchema.Info | undefined>
|
||||
readonly context: (sessionID: SessionSchema.ID) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||
readonly runnerContext: (
|
||||
sessionID: SessionSchema.ID,
|
||||
baselineSeq: number,
|
||||
) => Effect.Effect<SessionMessage.Message[], MessageDecodeError>
|
||||
readonly message: (
|
||||
messageID: SessionMessage.ID,
|
||||
) => Effect.Effect<{ readonly sessionID: SessionSchema.ID; readonly message: SessionMessage.Message } | undefined>
|
||||
@@ -39,9 +35,6 @@ const layer = Layer.effect(
|
||||
context: Effect.fn("SessionStore.context")(function* (sessionID) {
|
||||
return yield* SessionHistory.load(db, sessionID)
|
||||
}),
|
||||
runnerContext: Effect.fn("SessionStore.runnerContext")(function* (sessionID, baselineSeq) {
|
||||
return yield* SessionHistory.loadForRunner(db, sessionID, baselineSeq)
|
||||
}),
|
||||
message: Effect.fn("SessionStore.message")(function* (messageID) {
|
||||
const row = yield* db
|
||||
.select()
|
||||
|
||||
@@ -42,17 +42,17 @@ const make = (dependencies: Dependencies) => {
|
||||
if (!firstUser) return
|
||||
const agent = yield* dependencies.agents.get(AgentV2.ID.make("title"))
|
||||
if (!agent) return
|
||||
const model = yield* (agent.model
|
||||
const resolved = yield* (agent.model
|
||||
? dependencies.models.resolve({ ...session, model: agent.model })
|
||||
: dependencies.models.resolve(session)
|
||||
).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||
if (!model) return
|
||||
if (!resolved) return
|
||||
const chunks: string[] = []
|
||||
let failed = false
|
||||
const streamed = yield* dependencies.llm
|
||||
.stream(
|
||||
LLM.request({
|
||||
model,
|
||||
model: resolved.model,
|
||||
system: agent.system,
|
||||
messages: [Message.user(firstUser.text)],
|
||||
tools: [],
|
||||
|
||||
@@ -13,23 +13,47 @@ const Summary = Schema.Struct({
|
||||
})
|
||||
type Summary = typeof Summary.Type
|
||||
|
||||
const entries = (skills: ReadonlyArray<Summary>) =>
|
||||
skills.flatMap((skill) => [
|
||||
" <skill>",
|
||||
` <name>${skill.name}</name>`,
|
||||
` <description>${skill.description}</description>`,
|
||||
" </skill>",
|
||||
])
|
||||
|
||||
const render = (skills: ReadonlyArray<Summary>) =>
|
||||
[
|
||||
"Skills provide specialized instructions and workflows for specific tasks.",
|
||||
"Use the skill tool to load a skill when a task matches its description.",
|
||||
...(skills.length === 0
|
||||
? ["No skills are currently available."]
|
||||
: ["<available_skills>", ...entries(skills), "</available_skills>"]),
|
||||
].join("\n")
|
||||
|
||||
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
||||
const diff = SystemContext.diffByKey(
|
||||
previous,
|
||||
current,
|
||||
(skill) => skill.name,
|
||||
(before, after) => before.description !== after.description,
|
||||
)
|
||||
// Additions and removals render as small deltas; anything else restates the full list.
|
||||
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
||||
return [
|
||||
"The available skills have changed. This list supersedes the previous available skills list.",
|
||||
render(current),
|
||||
].join("\n")
|
||||
return [
|
||||
...(diff.added.length === 0
|
||||
? []
|
||||
: ["New skills are available in addition to those previously listed:", ...entries(diff.added)]),
|
||||
...(diff.removed.length === 0
|
||||
? []
|
||||
: [
|
||||
"<available_skills>",
|
||||
...skills.flatMap((skill) => [
|
||||
" <skill>",
|
||||
` <name>${skill.name}</name>`,
|
||||
` <description>${skill.description}</description>`,
|
||||
" </skill>",
|
||||
]),
|
||||
"</available_skills>",
|
||||
`The following skills are no longer available and must not be used: ${diff.removed.map((skill) => skill.name).join(", ")}.`,
|
||||
]),
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly load: (agent: AgentV2.Selection) => Effect.Effect<SystemContext.SystemContext>
|
||||
@@ -61,11 +85,7 @@ const layer = Layer.effect(
|
||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||
load: Effect.succeed(available),
|
||||
baseline: render,
|
||||
update: (_previous, current) =>
|
||||
[
|
||||
"The available skills have changed. This list supersedes the previous available skills list.",
|
||||
render(current),
|
||||
].join("\n"),
|
||||
update,
|
||||
removed: () => "Skill guidance is no longer available. Do not use any previously listed skill.",
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
export * as SystemContextBuiltIns from "./builtins"
|
||||
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { Location } from "../location"
|
||||
import { SystemContext } from "./index"
|
||||
import { InstructionContext } from "../instruction-context"
|
||||
import { SystemContextRegistry } from "./registry"
|
||||
import { FSUtil } from "../fs-util"
|
||||
import { Global } from "../global"
|
||||
|
||||
const builtIns = Layer.effectDiscard(
|
||||
export interface Interface {
|
||||
readonly load: () => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SystemContextBuiltIns") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const registry = yield* SystemContextRegistry.Service
|
||||
const environment = [
|
||||
"<env>",
|
||||
` Working directory: ${location.directory}`,
|
||||
@@ -39,12 +41,8 @@ const builtIns = Layer.effectDiscard(
|
||||
}),
|
||||
])
|
||||
|
||||
yield* registry.register({ key: SystemContext.Key.make("core/builtins"), load: Effect.succeed(context) })
|
||||
return Service.of({ load: () => Effect.succeed(context) })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
name: "system-context-builtins",
|
||||
layer: builtIns,
|
||||
deps: [Location.node, SystemContextRegistry.node, InstructionContext.node, FSUtil.node, Global.node],
|
||||
})
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] })
|
||||
|
||||
@@ -7,13 +7,18 @@ import { Effect, Option, Schema } from "effect"
|
||||
*
|
||||
* `Source<A>` describes how to observe, compare, and render one value. `make`
|
||||
* closes over `A`, producing an opaque `SystemContext` that composes uniformly
|
||||
* with contexts built from other value types. Interpreters observe the composed
|
||||
* context once, then produce a durable structured
|
||||
* `Snapshot` alongside the exact model-visible baseline or update text.
|
||||
* with contexts built from other value types.
|
||||
*
|
||||
* The durable `Applied` record tracks what the model was last told, per source:
|
||||
* it is the model's current belief. Interpreters uphold one invariant —
|
||||
* `reconcile` never rewrites the baseline; it only narrates drift as update
|
||||
* text. Only `rebaseline` (compaction) and `initialize` (first turn) produce
|
||||
* baseline text.
|
||||
*
|
||||
* Returning `unavailable` means observation failed temporarily. It differs from
|
||||
* removing a source from the context: refresh preserves the admitted snapshot,
|
||||
* and replacement waits rather than silently constructing an incomplete baseline.
|
||||
* removing a source from the context: the model's prior belief stands.
|
||||
* `reconcile` retains the applied value silently, and `rebaseline` restates the
|
||||
* belief by rendering the last-applied value instead of a live observation.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -45,39 +50,30 @@ export interface SystemContext {
|
||||
readonly [ContextTypeId]: ReadonlyArray<PackedSource>
|
||||
}
|
||||
|
||||
/** Durable comparison state for one admitted source. */
|
||||
export const SourceSnapshot = Schema.Struct({
|
||||
/** The value last applied to the model for one admitted source. */
|
||||
export const AppliedSource = Schema.Struct({
|
||||
value: Schema.Json,
|
||||
removed: Schema.optional(Schema.NonEmptyString),
|
||||
})
|
||||
export type SourceSnapshot = typeof SourceSnapshot.Type
|
||||
export type AppliedSource = typeof AppliedSource.Type
|
||||
|
||||
/** Durable structured comparison state for one active context generation. */
|
||||
export const Snapshot = Schema.Record(Key, SourceSnapshot)
|
||||
export type Snapshot = Readonly<Record<string, SourceSnapshot>>
|
||||
/** Durable record of what the model currently believes, per source. */
|
||||
export const Applied = Schema.Record(Key, AppliedSource)
|
||||
export type Applied = Readonly<Record<string, AppliedSource>>
|
||||
|
||||
export interface Generation {
|
||||
readonly baseline: string
|
||||
readonly snapshot: Snapshot
|
||||
/** A rendered baseline together with the applied values it was rendered from. */
|
||||
export interface Baseline {
|
||||
readonly text: string
|
||||
readonly applied: Applied
|
||||
}
|
||||
|
||||
export interface Updated {
|
||||
readonly _tag: "Updated"
|
||||
readonly text: string
|
||||
readonly snapshot: Snapshot
|
||||
readonly applied: Applied
|
||||
}
|
||||
|
||||
export interface ReplacementReady {
|
||||
readonly _tag: "ReplacementReady"
|
||||
readonly generation: Generation
|
||||
}
|
||||
|
||||
export interface ReplacementBlocked {
|
||||
readonly _tag: "ReplacementBlocked"
|
||||
}
|
||||
|
||||
export type ReplacementResult = ReplacementReady | ReplacementBlocked
|
||||
export type ReconcileResult = { readonly _tag: "Unchanged" } | Updated | ReplacementResult
|
||||
export type ReconcileResult = { readonly _tag: "Unchanged" } | Updated
|
||||
|
||||
export class InitializationBlocked extends Schema.TaggedErrorClass<InitializationBlocked>()(
|
||||
"SystemContext.InitializationBlocked",
|
||||
@@ -98,36 +94,24 @@ export class DuplicateKeyError extends Schema.TaggedErrorClass<DuplicateKeyError
|
||||
|
||||
interface PackedSource {
|
||||
readonly key: Key
|
||||
readonly load: Effect.Effect<Loaded | Unavailable>
|
||||
readonly load: Effect.Effect<Observed | Unavailable>
|
||||
/** Restates the model's belief from a last-applied value when the source cannot be observed. */
|
||||
readonly recall: (stored: AppliedSource) => string | undefined
|
||||
}
|
||||
|
||||
interface Loaded {
|
||||
readonly baseline: () => Rendered
|
||||
readonly compare: (previous: Schema.Json) => Compared
|
||||
interface Observed {
|
||||
readonly applied: AppliedSource
|
||||
readonly baseline: () => string
|
||||
/** `undefined` means unchanged. An undecodable previous value re-renders the baseline (treat-as-new). */
|
||||
readonly update: (previous: AppliedSource) => string | undefined
|
||||
}
|
||||
|
||||
interface Rendered {
|
||||
readonly text: string
|
||||
readonly snapshot: SourceSnapshot
|
||||
}
|
||||
|
||||
type Compared =
|
||||
| { readonly _tag: "Incompatible" }
|
||||
| { readonly _tag: "Unchanged" }
|
||||
| { readonly _tag: "Updated"; readonly render: () => Rendered }
|
||||
|
||||
interface AvailableEntry extends Loaded {
|
||||
readonly _tag: "Available"
|
||||
interface Entry {
|
||||
readonly key: Key
|
||||
readonly recall: PackedSource["recall"]
|
||||
readonly observed: Observed | Unavailable
|
||||
}
|
||||
|
||||
interface UnavailableEntry {
|
||||
readonly _tag: "Unavailable"
|
||||
readonly key: Key
|
||||
}
|
||||
|
||||
type Entry = AvailableEntry | UnavailableEntry
|
||||
|
||||
/** The identity context. */
|
||||
export const empty = context([])
|
||||
|
||||
@@ -136,42 +120,65 @@ export function make<A>(source: Source<A>): SystemContext {
|
||||
const decode = Schema.decodeUnknownOption(source.codec)
|
||||
const encode = Schema.encodeSync(source.codec)
|
||||
const equivalent = Schema.toEquivalence(source.codec)
|
||||
const baseline = (value: A) => requireText(source.key, "baseline", source.baseline(value))
|
||||
return context([
|
||||
{
|
||||
key: source.key,
|
||||
recall: (stored) =>
|
||||
Option.match(decode(stored.value), {
|
||||
onNone: () => undefined,
|
||||
onSome: baseline,
|
||||
}),
|
||||
load: source.load.pipe(
|
||||
Effect.map((value) => {
|
||||
if (isUnavailable(value)) return value
|
||||
const snapshot = (): SourceSnapshot => ({
|
||||
value: encode(value),
|
||||
...(source.removed ? { removed: requireText(source.key, "removal", source.removed(value)) } : {}),
|
||||
})
|
||||
return {
|
||||
baseline: (): Rendered => ({
|
||||
text: requireText(source.key, "baseline", source.baseline(value)),
|
||||
snapshot: snapshot(),
|
||||
}),
|
||||
compare: (previous): Compared =>
|
||||
Option.match(decode(previous), {
|
||||
onNone: (): Compared => ({ _tag: "Incompatible" }),
|
||||
onSome: (decoded): Compared =>
|
||||
applied: {
|
||||
value: encode(value),
|
||||
...(source.removed ? { removed: requireText(source.key, "removal", source.removed(value)) } : {}),
|
||||
},
|
||||
baseline: () => baseline(value),
|
||||
update: (previous) =>
|
||||
Option.match(decode(previous.value), {
|
||||
onNone: () => baseline(value),
|
||||
onSome: (decoded) =>
|
||||
equivalent(decoded, value)
|
||||
? { _tag: "Unchanged" }
|
||||
: {
|
||||
_tag: "Updated",
|
||||
render: () => ({
|
||||
text: requireText(source.key, "update", source.update(decoded, value)),
|
||||
snapshot: snapshot(),
|
||||
}),
|
||||
},
|
||||
? undefined
|
||||
: requireText(source.key, "update", source.update(decoded, value)),
|
||||
}),
|
||||
}
|
||||
} satisfies Observed
|
||||
}),
|
||||
),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyed three-way diff for list-shaped sources rendering delta updates.
|
||||
* `changed` compares two values sharing a key; entries equal under it are dropped.
|
||||
*/
|
||||
export function diffByKey<A>(
|
||||
previous: ReadonlyArray<A>,
|
||||
current: ReadonlyArray<A>,
|
||||
key: (value: A) => string,
|
||||
changed: (previous: A, current: A) => boolean,
|
||||
): {
|
||||
readonly added: ReadonlyArray<A>
|
||||
readonly removed: ReadonlyArray<A>
|
||||
readonly changed: ReadonlyArray<{ readonly previous: A; readonly current: A }>
|
||||
} {
|
||||
const currentKeys = new Set(current.map(key))
|
||||
const previousByKey = new Map(previous.map((value) => [key(value), value] as const))
|
||||
return {
|
||||
added: current.filter((value) => !previousByKey.has(key(value))),
|
||||
removed: previous.filter((value) => !currentKeys.has(key(value))),
|
||||
changed: current.flatMap((value) => {
|
||||
const before = previousByKey.get(key(value))
|
||||
return before === undefined || !changed(before, value) ? [] : [{ previous: before, current: value }]
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/** Combines contexts in order and rejects duplicate source keys immediately. */
|
||||
export function combine(values: ReadonlyArray<SystemContext>): SystemContext {
|
||||
const sources = values.flatMap((value) => value[ContextTypeId])
|
||||
@@ -183,111 +190,91 @@ const observe = (value: SystemContext) =>
|
||||
Effect.forEach(
|
||||
value[ContextTypeId],
|
||||
(source) =>
|
||||
source.load.pipe(
|
||||
Effect.map(
|
||||
(result): Entry =>
|
||||
result === unavailable
|
||||
? { _tag: "Unavailable", key: source.key }
|
||||
: { _tag: "Available", key: source.key, ...result },
|
||||
),
|
||||
),
|
||||
source.load.pipe(Effect.map((observed): Entry => ({ key: source.key, recall: source.recall, observed }))),
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
|
||||
/** Creates the immutable baseline and durable snapshot for a new generation. */
|
||||
export function initialize(value: SystemContext): Effect.Effect<Generation, InitializationBlocked> {
|
||||
/** Creates the first baseline. Blocks rather than admit a baseline missing an unobservable source. */
|
||||
export function initialize(value: SystemContext): Effect.Effect<Baseline, InitializationBlocked> {
|
||||
return observe(value).pipe(
|
||||
Effect.flatMap((entries) => {
|
||||
const unavailable = entries.flatMap((entry) => (entry._tag === "Unavailable" ? [entry.key] : []))
|
||||
if (unavailable.length > 0) return new InitializationBlocked({ keys: unavailable })
|
||||
return Effect.succeed(initializeObservation(entries))
|
||||
const blocked = entries.flatMap((entry) => (entry.observed === unavailable ? [entry.key] : []))
|
||||
if (blocked.length > 0) return new InitializationBlocked({ keys: blocked })
|
||||
const parts: string[] = []
|
||||
const applied: Record<string, AppliedSource> = {}
|
||||
for (const entry of entries) {
|
||||
if (entry.observed === unavailable) continue
|
||||
parts.push(entry.observed.baseline())
|
||||
applied[entry.key] = entry.observed.applied
|
||||
}
|
||||
return Effect.succeed({ text: render(parts), applied })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function initializeObservation(entries: ReadonlyArray<Entry>): Generation {
|
||||
const available = entries.filter((entry): entry is AvailableEntry => entry._tag === "Available")
|
||||
const rendered = available.map((entry) => [entry.key, entry.baseline()] as const)
|
||||
return {
|
||||
baseline: render(rendered.map(([, result]) => result.text)),
|
||||
snapshot: Object.fromEntries(rendered.map(([key, result]) => [key, result.snapshot])),
|
||||
}
|
||||
}
|
||||
|
||||
/** Reconciles current source values with one active generation. */
|
||||
export function reconcile(value: SystemContext, previous: Snapshot): Effect.Effect<ReconcileResult> {
|
||||
/** Narrates drift between current source values and the model's beliefs. Never rewrites the baseline. */
|
||||
export function reconcile(value: SystemContext, previous: Applied): Effect.Effect<ReconcileResult> {
|
||||
return observe(value).pipe(
|
||||
Effect.map((entries): ReconcileResult => {
|
||||
const result = reconcileObservation(entries, previous)
|
||||
if (result._tag === "Unchanged" || result._tag === "Updated") return result
|
||||
return replaceObservation(entries, previous)
|
||||
const updates: string[] = []
|
||||
const applied: Record<string, AppliedSource> = {}
|
||||
for (const entry of entries) {
|
||||
const stored = get(previous, entry.key)
|
||||
if (entry.observed === unavailable) {
|
||||
// The prior belief stands while the source cannot be observed.
|
||||
if (stored) applied[entry.key] = stored
|
||||
continue
|
||||
}
|
||||
if (!stored) {
|
||||
updates.push(entry.observed.baseline())
|
||||
applied[entry.key] = entry.observed.applied
|
||||
continue
|
||||
}
|
||||
const text = entry.observed.update(stored)
|
||||
if (text === undefined) {
|
||||
applied[entry.key] = stored
|
||||
continue
|
||||
}
|
||||
updates.push(text)
|
||||
applied[entry.key] = entry.observed.applied
|
||||
}
|
||||
const keys = new Set<string>(entries.map((entry) => entry.key))
|
||||
for (const key of Object.keys(previous).sort()) {
|
||||
if (keys.has(key)) continue
|
||||
const removed = previous[key].removed
|
||||
// An unannounced removal retains the belief; it clears at the next rebaseline.
|
||||
if (removed === undefined) applied[key] = previous[key]
|
||||
else updates.push(removed)
|
||||
}
|
||||
if (updates.length === 0) return { _tag: "Unchanged" }
|
||||
return { _tag: "Updated", text: render(updates), applied }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function reconcileObservation(
|
||||
entries: ReadonlyArray<Entry>,
|
||||
previous: Snapshot,
|
||||
): { readonly _tag: "Unchanged" } | Updated | { readonly _tag: "Replace" } {
|
||||
const keys = new Set(entries.map((entry) => entry.key))
|
||||
const comparisons = new Map<Key, Compared>()
|
||||
for (const entry of entries) {
|
||||
if (entry._tag === "Unavailable") continue
|
||||
const stored = getSnapshot(previous, entry.key)
|
||||
if (!stored) continue
|
||||
const compared = entry.compare(stored.value)
|
||||
if (compared._tag === "Incompatible") return { _tag: "Replace" }
|
||||
comparisons.set(entry.key, compared)
|
||||
}
|
||||
for (const key of Object.keys(previous).sort()) {
|
||||
if (keys.has(Key.make(key))) continue
|
||||
if (previous[key].removed === undefined) return { _tag: "Replace" }
|
||||
}
|
||||
|
||||
const snapshot: Record<string, SourceSnapshot> = {}
|
||||
const updates: string[] = []
|
||||
for (const entry of entries) {
|
||||
const stored = getSnapshot(previous, entry.key)
|
||||
if (entry._tag === "Unavailable") {
|
||||
if (stored) snapshot[entry.key] = stored
|
||||
continue
|
||||
}
|
||||
if (!stored) {
|
||||
const rendered = entry.baseline()
|
||||
updates.push(rendered.text)
|
||||
snapshot[entry.key] = rendered.snapshot
|
||||
continue
|
||||
}
|
||||
const compared = comparisons.get(entry.key)
|
||||
if (!compared || compared._tag === "Incompatible")
|
||||
throw new Error(`Missing comparison for system context source ${entry.key}`)
|
||||
if (compared._tag === "Unchanged") {
|
||||
snapshot[entry.key] = stored
|
||||
continue
|
||||
}
|
||||
const rendered = compared.render()
|
||||
updates.push(rendered.text)
|
||||
snapshot[entry.key] = rendered.snapshot
|
||||
}
|
||||
for (const key of Object.keys(previous).sort()) {
|
||||
if (keys.has(Key.make(key))) continue
|
||||
const removed = previous[key].removed
|
||||
if (removed === undefined) throw new Error(`Missing removal rendering for system context source ${key}`)
|
||||
updates.push(removed)
|
||||
}
|
||||
if (updates.length === 0) return { _tag: "Unchanged" }
|
||||
return { _tag: "Updated", text: render(updates), snapshot }
|
||||
}
|
||||
|
||||
/** Creates a complete replacement generation or blocks while admitted context is unavailable. */
|
||||
export function replace(value: SystemContext, previous: Snapshot): Effect.Effect<ReplacementResult> {
|
||||
return observe(value).pipe(Effect.map((entries) => replaceObservation(entries, previous)))
|
||||
}
|
||||
|
||||
function replaceObservation(entries: ReadonlyArray<Entry>, previous: Snapshot): ReplacementResult {
|
||||
if (entries.some((entry) => entry._tag === "Unavailable" && getSnapshot(previous, entry.key) !== undefined))
|
||||
return { _tag: "ReplacementBlocked" }
|
||||
return { _tag: "ReplacementReady", generation: initializeObservation(entries) }
|
||||
/** Rebuilds the baseline, restating unobservable sources from the model's last-applied beliefs. */
|
||||
export function rebaseline(value: SystemContext, previous: Applied): Effect.Effect<Baseline> {
|
||||
return observe(value).pipe(
|
||||
Effect.map((entries): Baseline => {
|
||||
const parts: string[] = []
|
||||
const applied: Record<string, AppliedSource> = {}
|
||||
for (const entry of entries) {
|
||||
if (entry.observed !== unavailable) {
|
||||
parts.push(entry.observed.baseline())
|
||||
applied[entry.key] = entry.observed.applied
|
||||
continue
|
||||
}
|
||||
const stored = get(previous, entry.key)
|
||||
if (!stored) continue
|
||||
const text = entry.recall(stored)
|
||||
// An undecodable belief cannot be restated; the source re-announces when observable again.
|
||||
if (text === undefined) continue
|
||||
parts.push(text)
|
||||
applied[entry.key] = stored
|
||||
}
|
||||
return { text: render(parts), applied }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function context(sources: ReadonlyArray<PackedSource>): SystemContext {
|
||||
@@ -298,8 +285,8 @@ function render(parts: ReadonlyArray<string>) {
|
||||
return parts.join("\n\n")
|
||||
}
|
||||
|
||||
function getSnapshot(snapshot: Snapshot, key: Key) {
|
||||
return Object.hasOwn(snapshot, key) ? snapshot[key] : undefined
|
||||
function get(applied: Applied, key: Key) {
|
||||
return Object.hasOwn(applied, key) ? applied[key] : undefined
|
||||
}
|
||||
|
||||
function isUnavailable(value: unknown): value is Unavailable {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
export * as SystemContextRegistry from "./registry"
|
||||
|
||||
import { Context, Effect, Layer, Ref, Scope } from "effect"
|
||||
import { SystemContext } from "./index"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
|
||||
export interface Entry {
|
||||
readonly key: SystemContext.Key
|
||||
readonly load: Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly register: (entry: Entry) => Effect.Effect<void, never, Scope.Scope>
|
||||
readonly load: () => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SystemContextRegistry") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const entries = yield* Ref.make<ReadonlyArray<Entry>>([])
|
||||
|
||||
return Service.of({
|
||||
register: Effect.fn("SystemContextRegistry.register")(function* (entry) {
|
||||
yield* Effect.acquireRelease(
|
||||
Ref.modify(entries, (current) => {
|
||||
if (current.some((item) => item.key === entry.key)) return [false, current]
|
||||
return [true, [...current, entry]]
|
||||
}).pipe(
|
||||
Effect.flatMap((added) =>
|
||||
added ? Effect.void : Effect.die(`Duplicate system context entry key: ${entry.key}`),
|
||||
),
|
||||
Effect.as(entry),
|
||||
),
|
||||
(entry) => Ref.update(entries, (current) => current.filter((item) => item !== entry)),
|
||||
)
|
||||
}),
|
||||
load: Effect.fn("SystemContextRegistry.load")(function* () {
|
||||
const current = (yield* Ref.get(entries)).toSorted((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
|
||||
return SystemContext.combine(
|
||||
yield* Effect.forEach(current, (entry) => entry.load, { concurrency: "unbounded" }),
|
||||
)
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||
@@ -4,7 +4,6 @@ import { makeLocationNode } from "../effect/app-node"
|
||||
import { Context, Layer } from "effect"
|
||||
import { ApplyPatchTool } from "./apply-patch"
|
||||
import { EditTool } from "./edit"
|
||||
import { GlobTool } from "./glob"
|
||||
import { GrepTool } from "./grep"
|
||||
import { QuestionTool } from "./question"
|
||||
import { ReadTool } from "./read"
|
||||
@@ -38,7 +37,6 @@ export const node = makeLocationNode({
|
||||
deps: [
|
||||
ApplyPatchTool.node,
|
||||
EditTool.node,
|
||||
GlobTool.node,
|
||||
GrepTool.node,
|
||||
QuestionTool.node,
|
||||
ReadTool.node,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
export * as GlobTool from "./glob"
|
||||
|
||||
import { ToolFailure } from "@opencode-ai/llm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { FileSystem } from "../filesystem"
|
||||
import { Location } from "../location"
|
||||
import { Ripgrep } from "../ripgrep"
|
||||
import { RelativePath } from "../schema"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { ToolRegistry } from "./registry"
|
||||
import { Tool } from "./tool"
|
||||
import { Tools } from "./tools"
|
||||
|
||||
export const name = "glob"
|
||||
|
||||
@@ -35,14 +33,14 @@ export const toModelOutput = (output: ModelOutput) => {
|
||||
}
|
||||
|
||||
/** Glob leaf that defaults its filesystem root to the active Location. */
|
||||
const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
const tools = yield* Tools.Service
|
||||
export const Plugin = {
|
||||
id: "core-glob-tool",
|
||||
effect: Effect.fn("GlobTool.Plugin")(function* (ctx: PluginContext) {
|
||||
const ripgrep = yield* Ripgrep.Service
|
||||
const location = yield* Location.Service
|
||||
const permission = yield* PermissionV2.Service
|
||||
|
||||
yield* tools
|
||||
yield* ctx.tool
|
||||
.register({
|
||||
[name]: Tool.make({
|
||||
description:
|
||||
@@ -96,10 +94,4 @@ const layer = Layer.effectDiscard(
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
name: "tool/glob",
|
||||
layer,
|
||||
deps: [ToolRegistry.node, Ripgrep.node, Location.node, PermissionV2.node],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
export * as ToolHooks from "./hooks"
|
||||
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { SessionMessage } from "../session/message"
|
||||
import { SessionSchema } from "../session/schema"
|
||||
import { State } from "../state"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import type { ToolOutput, ToolResultValue } from "@opencode-ai/llm"
|
||||
|
||||
export interface BeforeEvent {
|
||||
readonly tool: string
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly agent: AgentV2.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly toolCallID: string
|
||||
input: unknown
|
||||
}
|
||||
|
||||
export interface AfterEvent {
|
||||
readonly tool: string
|
||||
readonly sessionID: SessionSchema.ID
|
||||
readonly agent: AgentV2.ID
|
||||
readonly assistantMessageID: SessionMessage.ID
|
||||
readonly toolCallID: string
|
||||
readonly input: unknown
|
||||
result: ToolResultValue
|
||||
output?: ToolOutput
|
||||
outputPaths?: ReadonlyArray<string>
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly hook: {
|
||||
readonly before: (
|
||||
callback: (event: BeforeEvent) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
readonly after: (
|
||||
callback: (event: AfterEvent) => Effect.Effect<void> | void,
|
||||
) => Effect.Effect<State.Registration, never, Scope.Scope>
|
||||
}
|
||||
readonly runBefore: (event: BeforeEvent) => Effect.Effect<BeforeEvent>
|
||||
readonly runAfter: (event: AfterEvent) => Effect.Effect<AfterEvent>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolHooks") {}
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
let beforeHooks: ((event: BeforeEvent) => Effect.Effect<void> | void)[] = []
|
||||
let afterHooks: ((event: AfterEvent) => Effect.Effect<void> | void)[] = []
|
||||
|
||||
const register = <Event>(
|
||||
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
|
||||
update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
|
||||
) =>
|
||||
Effect.fn("ToolHooks.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
|
||||
const scope = yield* Scope.Scope
|
||||
let active = true
|
||||
update([...hooks(), callback])
|
||||
const dispose = Effect.sync(() => {
|
||||
if (!active) return
|
||||
active = false
|
||||
update(hooks().filter((item) => item !== callback))
|
||||
})
|
||||
yield* Scope.addFinalizer(scope, dispose)
|
||||
return { dispose }
|
||||
})
|
||||
|
||||
const run = Effect.fnUntraced(function* <Event>(
|
||||
hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
|
||||
event: Event,
|
||||
) {
|
||||
for (const hook of hooks) {
|
||||
const result = hook(event)
|
||||
if (Effect.isEffect(result)) yield* result
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
hook: {
|
||||
before: register(() => beforeHooks, (next) => (beforeHooks = next)),
|
||||
after: register(() => afterHooks, (next) => (afterHooks = next)),
|
||||
},
|
||||
runBefore: (event) => run(beforeHooks, event),
|
||||
runAfter: (event) => run(afterHooks, event),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user