Compare commits

...

5 Commits

Author SHA1 Message Date
Aiden Cline c1faad5d62 feat(codemode): support JSON callbacks 2026-07-07 11:55:52 -05:00
Kit Langton 8deb0e5780 fix(core): preserve SDK plugins across location eviction (#35725) 2026-07-07 11:43:40 -04:00
Aiden Cline f488089179 fix: preserve compaction files and classify Z.AI overflow (#35747)
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
2026-07-07 09:54:31 -05:00
Kit Langton e3a39b214f fix(core): lower failed reasoning to text (#35735) 2026-07-07 09:49:17 -04:00
James Long b6a2912bb1 refactor(simulation): type RPC request payloads (#35734) 2026-07-07 09:21:19 -04:00
31 changed files with 601 additions and 178 deletions
+6
View File
@@ -890,8 +890,14 @@ export interface VcsApi<E = never> {
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
export type DebugEvictLocationOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
export interface DebugApi<E = never> {
readonly location: DebugLocationOperation<E>
readonly evictLocation: DebugEvictLocationOperation<E>
}
export interface AppApi<E = never> {
@@ -1069,7 +1069,15 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
location: Endpoint25_0(raw),
evictLocation: Endpoint25_1(raw),
})
const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]),
@@ -178,6 +178,8 @@ import type {
VcsDiffInput,
VcsDiffOutput,
DebugLocationOutput,
DebugEvictLocationInput,
DebugEvictLocationOutput,
} from "./types"
import { ClientError } from "./client-error"
@@ -1494,6 +1496,18 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
evictLocation: (input?: DebugEvictLocationInput, requestOptions?: RequestOptions) =>
request<DebugEvictLocationOutput>(
{
method: "DELETE",
path: `/api/debug/location`,
query: { location: input?.["location"] },
successStatus: 204,
declaredStatuses: [401, 400],
empty: true,
},
requestOptions,
),
},
}
}
@@ -6130,3 +6130,11 @@ export type VcsDiffOutput = {
}
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
export type DebugEvictLocationInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type DebugEvictLocationOutput = void
+1 -2
View File
@@ -153,8 +153,7 @@ current omissions to implement, not intentional product boundaries.
shims. Consider `Promise.any` in the same pass.
- [ ] Support async iteration and `for await...of`. Define behavior first for the runtime's supported promise and
collection values, then extend it to bounded host streams when a stream boundary exists.
- [ ] Support callback-bearing standard-library variants that models commonly generate: the mapper argument to
`Array.from(...)` and replacers for `JSON.stringify(...)`, including Effect-aware callbacks where needed.
- [ ] Support the mapper argument to `Array.from(...)`, including Effect-aware callbacks.
- [ ] Add `Object.is` after runtime method and tool references have stable identity semantics.
- [ ] Add deterministic modern collection conveniences where they improve orchestration: `Object.groupBy`, Set
composition methods, and `Array.prototype.toSpliced`.
+143 -3
View File
@@ -5,6 +5,7 @@ import {
copyIn,
copyOut,
isBlockedMember,
MAX_VALUE_DEPTH,
ToolReference,
ToolRuntime,
ToolRuntimeError,
@@ -2027,6 +2028,7 @@ class Interpreter<R> {
}
if (callable instanceof GlobalMethodReference) {
if (callable.namespace === "console") return self.invokeConsole(callable.name, args, node)
if (callable.namespace === "JSON") return yield* self.invokeJsonMethod(callable.name, args, node)
if (callable.namespace === "Object" && args[0] instanceof ToolReference) {
return self.invokeObjectMethodOnTools(callable.name, args[0], node)
}
@@ -2492,6 +2494,129 @@ class Interpreter<R> {
})
}
private invokeJsonMethod(
name: string,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> {
const callback = args[1]
const callable =
callback instanceof CodeModeFunction ||
callback instanceof CoercionFunction ||
callback instanceof GlobalMethodReference ||
callback instanceof UriFunction
if (name === "parse" && callable) return this.parseJsonWithReviver(args, node)
if (name === "stringify" && (callable || Array.isArray(callback))) {
return this.stringifyJsonWithReplacer(args, node)
}
return Effect.sync(() => invokeJsonMethod(name, args, node))
}
private parseJsonWithReviver(args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> {
const apply = this.applySettledCollectionCallback(args[1], "JSON.parse", node)
const visit = (key: string, item: unknown): Effect.Effect<unknown, unknown, R> =>
Effect.gen(function* () {
if (Array.isArray(item)) {
for (let index = 0; index < item.length; index += 1) {
if (!(index in item)) continue
const revived = yield* visit(String(index), item[index])
if (revived === undefined) {
delete item[index]
continue
}
item[index] = revived
}
return yield* apply([key, item])
}
if (item !== null && typeof item === "object" && !isSandboxValue(item)) {
const object = item as SafeObject
for (const childKey of Object.keys(object)) {
const revived = yield* visit(childKey, object[childKey])
if (revived === undefined) {
delete object[childKey]
continue
}
object[childKey] = revived
}
}
return yield* apply([key, item])
})
return visit("", invokeJsonMethod("parse", [args[0]], node))
}
private stringifyJsonWithReplacer(args: Array<unknown>, node: AstNode): Effect.Effect<unknown, unknown, R> {
const callback = args[1]
const apply = Array.isArray(callback)
? undefined
: this.applySettledCollectionCallback(callback, "JSON.stringify", node)
const ancestors = new Set<object>()
const propertyList = Array.isArray(callback)
? Array.from(
new Set(
callback
.filter((item): item is string | number => typeof item === "string" || typeof item === "number")
.map(String),
),
)
: undefined
const visit = (key: string, item: unknown, depth: number): Effect.Effect<unknown, unknown, R> =>
Effect.gen(function* () {
const resolved = yield* (() => {
if (apply === undefined) return Effect.succeed(item)
return apply([
key,
item instanceof SandboxDate || item instanceof SandboxURL
? copyIn(item, "JSON.stringify value")
: item,
])
})()
if (typeofValue(resolved) === "function") return undefined
if (resolved === null || typeof resolved !== "object") return resolved
if (isSandboxValue(resolved)) {
return apply === undefined
? copyIn(resolved, "JSON.stringify replacer result")
: (Object.create(null) as SafeObject)
}
if (!Array.isArray(resolved)) {
const prototype = Object.getPrototypeOf(resolved)
if (prototype !== Object.prototype && prototype !== null) {
return copyIn(resolved, "JSON.stringify replacer result")
}
}
if (depth > MAX_VALUE_DEPTH) {
throw new ToolRuntimeError(
"InvalidDataValue",
`JSON.stringify replacer result exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`,
)
}
if (ancestors.has(resolved)) {
throw new ToolRuntimeError("InvalidDataValue", "JSON.stringify replacer result contains a circular value.")
}
ancestors.add(resolved)
return yield* Effect.gen(function* () {
if (Array.isArray(resolved)) {
const output: Array<unknown> = []
const length = resolved.length
for (let index = 0; index < length; index += 1) {
output[index] = yield* visit(String(index), resolved[index], depth + 1)
}
return output
}
const output: SafeObject = Object.create(null) as SafeObject
for (const childKey of propertyList ?? Object.keys(resolved)) {
if (isBlockedMember(childKey)) continue
if (apply === undefined && !Object.hasOwn(resolved, childKey)) continue
const child = yield* visit(childKey, (resolved as SafeObject)[childKey], depth + 1)
if (child !== undefined) output[childKey] = child
}
return output
}).pipe(Effect.ensuring(Effect.sync(() => ancestors.delete(resolved))))
})
return Effect.map(visit("", args[0], 0), (value) =>
invokeJsonMethod("stringify", [value, propertyList, args[2]], node),
)
}
// Runs a collection callback accepting a user function or supported builtin callable,
// mirroring the array-method callback contract.
private applyCollectionCallback(
@@ -2502,6 +2627,7 @@ class Interpreter<R> {
if (
!(callback instanceof CodeModeFunction) &&
!(callback instanceof CoercionFunction) &&
!(callback instanceof GlobalMethodReference) &&
!(callback instanceof UriFunction)
) {
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
@@ -2509,9 +2635,23 @@ class Interpreter<R> {
return (callbackArgs) =>
callback instanceof CoercionFunction
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
: callback instanceof UriFunction
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
: this.invokeFunction(callback, callbackArgs)
: callback instanceof GlobalMethodReference
? Effect.succeed(invokeGlobalMethod(callback, callbackArgs, node))
: callback instanceof UriFunction
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
: this.invokeFunction(callback, callbackArgs)
}
private applySettledCollectionCallback(
callback: unknown,
name: string,
node: AstNode,
): (args: Array<unknown>) => Effect.Effect<unknown, unknown, R> {
const apply = this.applyCollectionCallback(callback, name, node)
return (args) =>
Effect.flatMap(apply(args), (value) =>
value instanceof SandboxPromise ? this.settlePromise(value, node) : Effect.succeed(value),
)
}
private invokeMapMethod(
+6 -16
View File
@@ -1,9 +1,4 @@
import {
type AstNode,
CodeModeFunction,
InterpreterRuntimeError,
supportedSyntaxMessage,
} from "../interpreter/model.js"
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
import { copyIn, copyOut } from "../tool-runtime.js"
export const jsonStatics = new Set(["stringify", "parse"])
@@ -12,18 +7,13 @@ export const invokeJsonMethod = (name: string, args: Array<unknown>, node: AstNo
if (!jsonStatics.has(name)) throw new InterpreterRuntimeError(`JSON.${name} is not available in CodeMode.`, node)
switch (name) {
case "stringify": {
const replacer = args[1]
if (Array.isArray(replacer) || replacer instanceof CodeModeFunction) {
throw new InterpreterRuntimeError(
"JSON.stringify replacers are not supported in CodeMode.",
node,
"UnsupportedSyntax",
[supportedSyntaxMessage],
)
}
const space = args[2]
const indent = typeof space === "number" || typeof space === "string" ? space : undefined
return JSON.stringify(copyOut(copyIn(args[0], "JSON.stringify value")), null, indent)
return JSON.stringify(
copyOut(copyIn(args[0], "JSON.stringify value")),
Array.isArray(args[1]) ? args[1] : null,
indent,
)
}
case "parse": {
const text = args[0]
+1 -1
View File
@@ -119,7 +119,7 @@ export class ToolReference {
* limit) purely because it produces a clearer diagnostic than a native stack-overflow
* RangeError would.
*/
const MAX_VALUE_DEPTH = 32
export const MAX_VALUE_DEPTH = 32
export class ToolRuntimeError extends Error {
constructor(
+155
View File
@@ -19,6 +19,161 @@ const error = async (code: string) => {
return result.error
}
describe("JSON", () => {
test("parse revivers run post-order including the root", async () => {
expect(
await value(`
const order = []
const parsed = JSON.parse('{"item":{"count":2}}', (key, item) => {
order.push(key)
if (key === "count") return item + 1
if (key === "") return { parsed: item }
return item
})
return { parsed, order }
`),
).toEqual({ parsed: { parsed: { item: { count: 3 } } }, order: ["count", "item", ""] })
expect(await value(`return JSON.parse("1", (key, value) => undefined) === undefined`)).toBe(true)
})
test("parse reviver deletion removes properties and creates array holes", async () => {
expect(
await value(`
const parsed = JSON.parse('{"keep":1,"drop":2,"items":[1,2,3]}', (key, item) =>
key === "drop" || key === "1" ? undefined : item
)
return { json: JSON.stringify(parsed), hasSecond: 1 in parsed.items, length: parsed.items.length }
`),
).toEqual({ json: '{"keep":1,"items":[1,null,3]}', hasSecond: false, length: 3 })
})
test("stringify function replacers run root-first and apply JSON deletion rules", async () => {
expect(
await value(`
const order = []
const json = JSON.stringify({ keep: 1, drop: 2, items: [1, 2] }, (key, item) => {
order.push(key)
if (key === "drop" || key === "1") return undefined
return item
})
return { json, order }
`),
).toEqual({ json: '{"keep":1,"items":[1,null]}', order: ["", "keep", "drop", "items", "0", "1"] })
expect(
await value(`
return [
JSON.stringify(1, (key, item) => key === "" ? new Date(0) : item),
JSON.stringify(1, (key, item) => key === "" ? new URL("https://example.test") : item),
]
`),
).toEqual(["{}", "{}"])
})
test("stringify function replacers observe live mutations and sandbox values", async () => {
expect(
await value(`
const item = { a: 1, b: 2 }
const visited = []
const mutated = JSON.stringify(item, (key, value) => {
visited.push(key)
if (key === "a") item.b = 3
return value
})
const mapped = JSON.stringify(new Map([["x", 1]]), (key, value) =>
value instanceof Map ? Object.fromEntries(value) : value
)
const removed = { a: 1, b: 2 }
const deleted = JSON.stringify(removed, (key, value) => {
if (key === "a") removed.b = undefined
if (key === "b") visited.push(value === undefined ? "missing" : "present")
return value
})
return { mutated, mapped, deleted, visited }
`),
).toEqual({
mutated: '{"a":1,"b":3}',
mapped: '{"x":1}',
deleted: '{"a":1}',
visited: ["", "a", "b", "missing"],
})
})
test("stringify replacer arrays coerce, dedupe, and recursively filter object keys", async () => {
expect(
await value(`
return JSON.stringify(
{ 1: "one", keep: { keep: 2, drop: 3 }, drop: 4, list: [{ keep: 5, drop: 6 }] },
["keep", 1, "keep", null, {}, "list"],
)
`),
).toBe('{"keep":{"keep":2},"1":"one","list":[{"keep":5}]}')
})
test("JSON callbacks settle async tool calls sequentially", async () => {
const calls: Array<number> = []
const transform = Tool.make({
description: "Transform a number",
input: Schema.Number,
output: Schema.Number,
run: (input) =>
Effect.sync(() => {
calls.push(input)
return input * 10
}),
})
const parse = await Effect.runPromise(
CodeMode.execute({
tools: { host: { transform } },
code: `return JSON.parse("[1,2]", async (key, item) => key === "" ? item : await tools.host.transform(item))`,
}),
)
expect(parse.ok && parse.value).toEqual([10, 20])
const stringify = await Effect.runPromise(
CodeMode.execute({
tools: { host: { transform } },
code: `return JSON.stringify([1,2], async (key, item) => key === "" ? item : await tools.host.transform(item))`,
}),
)
expect(stringify.ok && stringify.value).toBe("[10,20]")
expect(calls).toEqual([1, 2, 1, 2])
})
test("non-callable JSON callback arguments are ignored", async () => {
expect(await value(`return [JSON.parse("{\\"a\\":1}", 42), JSON.stringify({ a: 1 }, { nope: true })]`)).toEqual([
{ a: 1 },
'{"a":1}',
])
expect(await value(`return [JSON.parse("1", Boolean), JSON.stringify({ a: 1 }, String)]`)).toEqual([false, '""'])
})
test("supported global methods work as JSON callbacks", async () => {
expect(
await value(`return [JSON.parse("[1,2]", Number.isFinite), JSON.stringify({ a: 1 }, Number.isFinite)]`),
).toEqual([false, "false"])
})
test("stringify replacers can prune values before the depth limit", async () => {
expect(
await value(`
let item = 1
for (let index = 0; index < 33; index++) item = { x: item }
let calls = 0
return JSON.stringify(item, (key, value) => ++calls === 34 ? undefined : value)
`),
).toBe(`${'{"x":'.repeat(32)}{}${"}".repeat(32)}`)
})
test("stringify omits callable replacer results", async () => {
expect(
await value(`
return JSON.stringify({ object: 1, array: [1] }, (key, value) =>
key === "object" || key === "0" ? Number.isFinite : value
)
`),
).toBe('{"array":[null]}')
})
})
describe("Date", () => {
test("Date.now() returns a number", async () => {
expect(await value(`return typeof Date.now()`)).toBe("number")
+16 -32
View File
@@ -7,14 +7,6 @@ import { EventV2 } from "../event"
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
export interface Store {
readonly plugins: Map<string, Plugin>
}
export const makeStore = (): Store => ({ plugins: new Map() })
const defaultStore = makeStore()
/**
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
* so `PluginSupervisor` can add them on every Location boot through the ordinary
@@ -22,10 +14,9 @@ const defaultStore = makeStore()
* config. Registration publishes an unlocated update so every booted Location
* reloads its plugin generation from the shared store.
*
* The store is shared explicitly between the SDK construction graph and the
* embedded route graph because `LocationServiceMap` builds Location layers lazily
* in a nested graph. Each embedded SDK creates its own store, so instances do not
* see each other's contributions.
* Each host-global layer owns one private store. Location graphs reuse that
* layer through Effect's memoization, so separate hosts remain isolated while
* every Location in one host sees the same registrations.
*/
export interface Interface {
readonly register: (plugin: Plugin) => Effect.Effect<void>
@@ -34,26 +25,19 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
export const layerWithStore = (store: Store) =>
Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
yield* Effect.addFinalizer(() =>
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const plugins = new Map<string, Plugin>()
return Service.of({
register: (plugin) =>
Effect.sync(() => {
store.plugins.clear()
}),
)
return Service.of({
register: (plugin) =>
Effect.sync(() => {
store.plugins.set(plugin.id, plugin)
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
all: () => [...store.plugins.values()],
})
}),
)
export const layer = layerWithStore(defaultStore)
plugins.set(plugin.id, plugin)
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
all: () => [...plugins.values()],
})
}),
)
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
+11 -4
View File
@@ -25,20 +25,27 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
## Work State
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"]
- Active: [current work, partial changes, or investigation state; otherwise "(none)"]
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
### Completed
- [finished work, verified facts, or changes made; otherwise "(none)"]
### Active
- [current work, partial changes, or investigation state; otherwise "(none)"]
### Blocked
- [blockers, failing commands, or unknowns; otherwise "(none)"]
## Next Move
1. [immediate concrete action, or "(none)"]
2. [next action if known, or "(none)"]
## Relevant Files
- [file or directory path: why it matters, or "(none)"]
</template>
Rules:
- Keep every section, even when empty.
- Use terse bullets, not prose paragraphs.
- Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known.
- Put relevant files and symbols inside the section where they matter; do not add extra sections.
- Do not mention the summary process or that context was compacted.`
type Settings = {
@@ -120,12 +120,12 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => {
const content = message.content.flatMap((item): ContentPart[] => {
if (item.type === "text") return [{ type: "text", text: item.text }]
if (item.type === "reasoning")
return sameModel
return reuseProviderMetadata
? [
{
type: "reasoning",
text: item.text,
providerMetadata: reuseProviderMetadata ? providerMetadata(model.providerID, item.state) : undefined,
providerMetadata: providerMetadata(model.providerID, item.state),
},
]
: item.text.length > 0
+36
View File
@@ -12,6 +12,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProjectV2 } from "@opencode-ai/core/project"
@@ -28,8 +29,43 @@ import { Reference } from "../src/reference"
import { ToolRegistry } from "../src/tool/registry"
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
const itWithSdk = testEffect(
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])),
)
describe("LocationServiceMap", () => {
itWithSdk.live("preserves embedded SDK plugins after Location eviction", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const sdk = yield* SdkPlugins.Service
const locations = yield* LocationServiceMap.Service
const id = AgentV2.ID.make("persistent-sdk-agent")
const plugin = define({
id: "persistent-sdk-plugin",
effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
})
yield* sdk.register(plugin)
const ref = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
const read = Effect.gen(function* () {
const supervisor = yield* PluginSupervisor.Service
yield* supervisor.ready
const agents = yield* AgentV2.Service
return yield* agents.get(id)
})
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
yield* locations.invalidate(ref)
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
}),
),
),
)
it.live("applies ordered plugin config operations during boot", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
+13 -3
View File
@@ -52,6 +52,15 @@ const it = testEffect(
),
)
test("compaction prompt preserves detailed work state and relevant files", () => {
const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] })
expect(prompt).toContain("## Work State\n### Completed")
expect(prompt).toContain("### Active")
expect(prompt).toContain("### Blocked")
expect(prompt).toContain("## Relevant Files")
})
test("compaction describes tool media without embedding base64", () => {
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
const serialized = SessionCompaction.serializeToolContent([
@@ -74,13 +83,14 @@ test("compaction prompt requires the checkpoint headings in order", () => {
"## Objective",
"## Important Details",
"## Work State",
"### Completed",
"### Active",
"### Blocked",
"## Next Move",
"## Relevant Files",
])
expect(prompt).toContain("one or two brief sentences")
expect(prompt).toContain("constraints/preferences, decisions and why")
expect(prompt).toContain("Completed:")
expect(prompt).toContain("Active:")
expect(prompt).toContain("Blocked:")
expect(prompt).toContain("immediate concrete action")
expect(prompt).toContain("next action if known")
expect(prompt).toContain("Keep every section, even when empty.")
@@ -461,7 +461,7 @@ Recent work
])
})
test("drops provider-native continuation metadata from failed assistant turns", () => {
test("lowers failed assistant reasoning to text", () => {
const messages = toLLMMessages(
[
SessionMessage.Assistant.make({
@@ -501,7 +501,7 @@ Recent work
)
expect(messages[0]?.content).toEqual([
{ type: "reasoning", text: "Partial thought", providerMetadata: undefined },
{ type: "text", text: "Partial thought" },
{
type: "tool-call",
id: "hosted-failed",
+1
View File
@@ -6,6 +6,7 @@ const patterns = [
/input is too long for requested model/i,
/exceeds the context window/i,
/input token count.*exceeds the maximum/i,
/tokens in request more than max tokens allowed/i,
/maximum prompt length is \d+/i,
/reduce the length of the messages/i,
/maximum context length is \d+ tokens/i,
+8
View File
@@ -0,0 +1,8 @@
import { describe, expect, test } from "bun:test"
import { isContextOverflow } from "../src"
describe("provider error classification", () => {
test("classifies Z.AI GLM token limit messages as context overflow", () => {
expect(isContextOverflow("tokens in request more than max tokens allowed")).toBe(true)
})
})
@@ -1448,6 +1448,7 @@ describe("session.message-v2.fromError", () => {
"prompt is too long: 213462 tokens > 200000 maximum",
"Your input exceeds the context window of this model",
"The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)",
"tokens in request more than max tokens allowed",
"Please reduce the length of the messages or completion",
"400 status code (no body)",
"413 status code (no body)",
+1
View File
@@ -61,6 +61,7 @@ export const groupNames = {
} as const
export const endpointNames = {
"debug.location.evict": "evictLocation",
"session.messages": "list",
"integration.connect.key": "connectKey",
"integration.connect.oauth": "connectOauth",
+16 -1
View File
@@ -1,6 +1,7 @@
import { Location } from "@opencode-ai/schema/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const DebugGroup = HttpApiGroup.make("server.debug")
.add(
@@ -14,4 +15,18 @@ export const DebugGroup = HttpApiGroup.make("server.debug")
}),
),
)
.add(
HttpApiEndpoint.delete("debug.location.evict", "/api/debug/location", {
query: LocationQuery,
success: HttpApiSchema.NoContent,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.debug.location.evict",
summary: "Evict a loaded location",
description: "Dispose the requested location's cached services so its next use boots them fresh.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "debug" }))
+10 -34
View File
@@ -1,47 +1,23 @@
import { OpenCode } from "@opencode-ai/client/effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { Project } from "@opencode-ai/core/project"
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
import { Context, Effect, Layer, Scope } from "effect"
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
import { Context, Effect, Layer, ManagedRuntime } from "effect"
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
export const create = Effect.fn("OpenCode.create")(function* () {
const scope = yield* Scope.Scope
const memoMap = yield* Layer.makeMemoMap
const sdkPlugins = SdkPlugins.makeStore()
const context = yield* Layer.buildWithMemoMap(
AppNodeBuilder.build(LayerNode.group([EventV2.node, PermissionSaved.node, Project.node, SdkPlugins.node]), [
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
]),
memoMap,
scope,
const runtime = yield* Effect.acquireRelease(
Effect.sync(() => ManagedRuntime.make(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices)))),
(runtime) => runtime.disposeEffect,
)
const context = yield* runtime.contextEffect
const plugins = Context.get(context, SdkPlugins.Service)
const permissions = Context.get(context, PermissionSaved.Service)
const project = Context.get(context, Project.Service)
const web = yield* Effect.acquireRelease(
Effect.sync(() =>
HttpRouter.toWebHandler(
createEmbeddedRoutes(sdkPlugins).pipe(
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
HttpRouter.provideRequest(Layer.succeed(Project.Service, project)),
Layer.provide(HttpServer.layerServices),
),
{ disableLogger: true, memoMap },
),
),
(web) => Effect.promise(web.dispose),
)
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => web.handler(new Request(input, init)), {
const router = Context.get(context, HttpRouter.HttpRouter)
const handler = HttpEffect.toWebHandler(router.asHttpEffect())
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => handler(new Request(input, init)), {
preconnect: () => undefined,
}) satisfies typeof globalThis.fetch
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
Effect.provide(FetchHttpClient.layer),
Effect.provideService(FetchHttpClient.Fetch, fetch),
Effect.provide(FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetch)), Layer.fresh)),
)
return {
...client,
+44
View File
@@ -105,6 +105,50 @@ it.live(
25_000,
)
it.live(
"preserves SDK plugins across Location eviction",
() =>
withEmbedded("opencode-embedded-plugin-eviction-", (fixture) =>
Effect.gen(function* () {
const opencode = yield* fixture.sdk.OpenCode.create()
const ref = location(fixture)
const connected = yield* Latch.make(false)
const booted = yield* Deferred.make<void>()
// The rebooted Location commits its second plugin generation.
const recommitted = yield* Deferred.make<void>()
const generations = yield* Ref.make(0)
const id = `evicted-sdk-${crypto.randomUUID()}`
yield* opencode.events.subscribe().pipe(
Stream.runForEach((event) => {
if (event.type === "server.connected") return connected.open
if (event.type !== "plugin.updated" || event.location?.directory !== fixture.directory) return Effect.void
return Ref.updateAndGet(generations, (total) => total + 1).pipe(
Effect.flatMap((total) => {
if (total === 1) return Deferred.succeed(booted, undefined)
if (total === 2) return Deferred.succeed(recommitted, undefined)
return Effect.void
}),
Effect.asVoid,
)
}),
Effect.forkScoped,
)
yield* connected.await
yield* opencode.plugin({ id, effect: () => Effect.void })
yield* opencode.plugin.list({ location: ref })
yield* Deferred.await(booted).pipe(Effect.timeout("5 seconds"))
yield* opencode.debug.evictLocation({ location: ref })
yield* opencode.plugin.list({ location: ref })
yield* Deferred.await(recommitted).pipe(Effect.timeout("5 seconds"))
expect((yield* opencode.plugin.list({ location: ref })).data.map((plugin) => String(plugin.id))).toContain(id)
}),
),
15_000,
)
it.live(
"keeps SDK plugin registration isolated between embedded hosts",
() =>
+18 -7
View File
@@ -2,13 +2,24 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import { Effect, Option, RcMap } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { requestRef } from "../location"
export const DebugHandler = HttpApiBuilder.group(Api, "server.debug", (handlers) =>
handlers.handle(
"debug.location",
Effect.fn(function* () {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
return Array.from(yield* RcMap.keys(locations.rcMap))
}),
),
handlers
.handle(
"debug.location",
Effect.fn(function* () {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
return Array.from(yield* RcMap.keys(locations.rcMap))
}),
)
.handle(
"debug.location.evict",
Effect.fn(function* (ctx) {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
// Resolve through requestRef so the key matches the shape the location
// middleware cached the services under.
yield* locations.invalidate(requestRef(ctx.request))
}),
),
)
+5 -8
View File
@@ -1,14 +1,9 @@
export * as ServerProcess from "./process"
import { NodeHttpClient, NodeHttpServer } from "@effect/platform-node"
import { Credential } from "@opencode-ai/core/credential"
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 { Project } from "@opencode-ai/core/project"
import { HealthGroup } from "@opencode-ai/protocol/groups/health"
import { Context, Effect, Layer, Option } from "effect"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpClient, HttpClientRequest, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
import { createServer } from "node:http"
import { ServerAuth } from "./auth"
@@ -53,9 +48,11 @@ function listen(options: Options) {
function bind(hostname: string, port: number, password: string) {
const server = createServer()
return Layer.build(
HttpRouter.serve(createRoutes(password), { disableListenLog: true }).pipe(
createRoutes(password).pipe(
Layer.flatMap((context) =>
HttpServer.serve(Context.get(context, HttpRouter.HttpRouter).asHttpEffect(), HttpMiddleware.logger),
),
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
),
).pipe(
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
+26 -22
View File
@@ -19,7 +19,7 @@ import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Effect, Layer, Option } from "effect"
import { Context, Effect, Layer, Option } from "effect"
import { Api } from "./api"
import { ServerAuth } from "./auth"
import { handlers } from "./handlers"
@@ -40,6 +40,7 @@ const applicationServices = LayerNode.group([
Project.node,
SessionV2.node,
PluginRuntime.providerNode,
SdkPlugins.node,
PermissionSaved.node,
PtyTicket.node,
Credential.node,
@@ -55,26 +56,22 @@ export function createRoutes(password?: string) {
)
}
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) {
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), sdkPlugins)
export function createEmbeddedRoutes() {
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }))
}
function makeRoutes<AuthError, AuthServices>(
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
sdkPlugins?: SdkPlugins.Store,
) {
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
const pluginRuntimeCell = PluginRuntime.makeCell()
const replacements: LayerNode.Replacements = [
[SessionExecution.node, SessionExecutionLocal.node],
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
]
const serviceLayer = simulateEnabled()
? Layer.unwrap(
Effect.gen(function* () {
const { simulationReplacements, startDriveServer } = yield* Effect.promise(() =>
import("@opencode-ai/simulation/backend"),
const { simulationReplacements, startDriveServer } = yield* Effect.promise(
() => import("@opencode-ai/simulation/backend"),
)
if (driveEnabled()) startDriveServer()
return AppNodeBuilder.build(applicationServices, [
@@ -85,16 +82,24 @@ function makeRoutes<AuthError, AuthServices>(
)
: AppNodeBuilder.build(applicationServices, replacements)
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers.pipe(Layer.provide(serviceLayer))),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(layer),
Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer),
Layer.provide(auth),
Layer.provide(serviceLayer),
Layer.provide(Observability.layer),
return serviceLayer.pipe(
Layer.flatMap((context) => {
const services = Layer.succeedContext(context)
const requestServices = Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context))
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers.pipe(Layer.provide(services))),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(layer),
Layer.provide(authorizationLayer),
Layer.provide(schemaErrorLayer),
Layer.provide(auth),
Layer.provide(Observability.layer),
HttpRouter.provideRequest(requestServices),
Layer.provideMerge(services),
Layer.provideMerge(HttpRouter.layer),
)
}),
)
}
@@ -108,5 +113,4 @@ function driveEnabled() {
export const routes = createRoutes()
export const webHandler = () =>
HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
export const webHandler = () => HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
+9 -11
View File
@@ -25,10 +25,10 @@ import { SimulationNetwork } from "./network"
type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }>
function parseRequest(input: string | Buffer) {
return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
return SimulationProtocol.Backend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
}
async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc.Request): Promise<unknown> {
async function handle(socket: ControlSocket, request: SimulationProtocol.Backend.Request): Promise<unknown> {
switch (request.method) {
case "llm.attach": {
socket.data.unsubscribe?.()
@@ -38,23 +38,22 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc
return { attached: true }
}
case "llm.chunk": {
const params = await SimulationProtocol.Backend.decodeChunkParams(request.params)
await Effect.runPromise(
SimulationLLMExchange.push(
params.id,
params.items.map((item) => ({ type: "item", item }) as const),
request.params.id,
request.params.items.map((item) => ({ type: "item", item }) as const),
),
)
return { ok: true }
}
case "llm.finish": {
const params = await SimulationProtocol.Backend.decodeFinishParams(request.params)
await Effect.runPromise(SimulationLLMExchange.push(params.id, [{ type: "finish", reason: params.reason }]))
await Effect.runPromise(
SimulationLLMExchange.push(request.params.id, [{ type: "finish", reason: request.params.reason }]),
)
return { ok: true }
}
case "llm.disconnect": {
const params = await SimulationProtocol.Backend.decodeDisconnectParams(request.params)
await Effect.runPromise(SimulationLLMExchange.disconnect(params.id))
await Effect.runPromise(SimulationLLMExchange.disconnect(request.params.id))
return { ok: true }
}
case "llm.pending":
@@ -62,7 +61,6 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc
case "network.log":
return { entries: SimulationNetwork.log() }
}
throw new Error(`Unknown simulation control method: ${request.method}`)
}
export function start(endpoint: string) {
@@ -79,7 +77,7 @@ export function start(endpoint: string) {
socket.data.unsubscribe?.()
},
async message(socket, message) {
let request: SimulationProtocol.JsonRpc.Request | undefined
let request: SimulationProtocol.Backend.Request | undefined
try {
request = parseRequest(message)
const result = await handle(socket, request)
+1 -1
View File
@@ -47,7 +47,7 @@ function hit(renderer: CliRenderer, renderable: Renderable) {
/**
* Builds the harness the simulation server drives.
*
* When the renderer is the fake simulation renderer, its TestRendererSetup
* When the renderer is the headless simulation renderer, its TestRendererSetup
* provides the supported testing APIs. For the visible terminal renderer the
* harness falls back to `requestRender` + `idle` and reading the private
* `currentRenderBuffer`.
+1 -1
View File
@@ -4,7 +4,7 @@ import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testin
const setups = new WeakMap<CliRenderer, TestRendererSetup>()
/**
* Creates the fake simulation renderer: a real CliRenderer backed by an
* Creates the headless simulation renderer: a real CliRenderer backed by an
* in-memory screen buffer instead of a terminal. The TestRendererSetup is
* kept module-side (keyed by renderer) so the harness can use the supported
* testing APIs without app code carrying it around.
+7 -17
View File
@@ -7,29 +7,20 @@ export interface Server {
readonly stop: () => void
}
function actionParam(params: unknown) {
return SimulationProtocol.Frontend.decodeActionParams(params).action
}
function parseRequest(input: string | Buffer) {
return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
return SimulationProtocol.Frontend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
}
async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Request) {
async function handle(harness: Harness, request: SimulationProtocol.Frontend.Request, headless: boolean) {
switch (request.method) {
case "ui.state": {
if (headless) await harness.renderOnce()
const result = SimulationActions.state(harness)
SimulationTrace.add("ui.state", { elements: result.elements.length, actions: result.actions.length })
return result
}
case "ui.action":
return SimulationActions.execute(harness, actionParam(request.params))
case "ui.render": {
await harness.renderOnce()
const result = SimulationActions.state(harness)
SimulationTrace.add("ui.render", { elements: result.elements.length, actions: result.actions.length })
return result
}
return SimulationActions.execute(harness, request.params.action)
case "trace.list":
return { records: SimulationTrace.list() }
case "trace.clear":
@@ -38,10 +29,9 @@ async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Requ
case "trace.export":
return SimulationTrace.exportTrace()
}
throw new Error(`Unknown simulation method: ${request.method}`)
}
export function start(harness: Harness, endpoint: string): Server {
export function start(harness: Harness, endpoint: string, headless: boolean): Server {
const url = new URL(endpoint)
const server = Bun.serve<{ readonly drive: true }>({
hostname: url.hostname,
@@ -58,10 +48,10 @@ export function start(harness: Harness, endpoint: string): Server {
SimulationTrace.add("control.disconnect")
},
async message(socket, message) {
let request: SimulationProtocol.JsonRpc.Request | undefined
let request: SimulationProtocol.Frontend.Request | undefined
try {
request = parseRequest(message)
const result = await handle(harness, request)
const result = await handle(harness, request, headless)
const next = SimulationProtocol.JsonRpc.success(request.id, result)
if (next) socket.send(JSON.stringify(next))
} catch (error) {
@@ -7,19 +7,18 @@ import { SimulationServer } from "./server"
/**
* Drive-mode renderer entry point.
*
* Creates the renderer (fake when OPENCODE_DRIVE_RENDERER=fake, the normal
* Creates the renderer (headless when OPENCODE_DRIVE_RENDERER=headless, the normal
* visible renderer otherwise) and starts the UI control
* server against it. The server stops when the renderer is destroyed, so the
* caller only manages the renderer lifecycle.
*/
export async function create(options: CliRendererConfig): Promise<CliRenderer> {
const renderer =
process.env.OPENCODE_DRIVE_RENDERER === "fake"
? await SimulationRenderer.create(options)
: await createCliRenderer(options)
const headless = process.env.OPENCODE_DRIVE_RENDERER === "headless"
const renderer = headless ? await SimulationRenderer.create(options) : await createCliRenderer(options)
const server = SimulationServer.start(
SimulationActions.createHarness(renderer),
DriveManifest.resolve().endpoints.ui,
headless,
)
process.stderr.write(`opencode drive ui websocket: ${server.url}\n`)
renderer.once("destroy", () => server.stop())
+26 -5
View File
@@ -4,9 +4,12 @@ const JsonRpcID = Schema.Union([Schema.String, Schema.Number, Schema.Null])
type Json = Schema.Schema.Type<typeof Schema.Json>
export namespace JsonRpc {
export const Request = Schema.Struct({
export const RequestFields = {
jsonrpc: Schema.Literal("2.0"),
id: Schema.optional(JsonRpcID),
}
export const Request = Schema.Struct({
...RequestFields,
method: Schema.String,
params: Schema.optional(Schema.Json),
})
@@ -92,7 +95,16 @@ export namespace Frontend {
export const ActionParams = Schema.Struct({ action: Action })
export interface ActionParams extends Schema.Schema.Type<typeof ActionParams> {}
export const decodeActionParams = Schema.decodeUnknownSync(ActionParams)
export const Request = Schema.Union([
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.action"), params: ActionParams }),
Schema.Struct({
...JsonRpc.RequestFields,
method: Schema.Literals(["ui.state", "trace.list", "trace.clear", "trace.export"]),
}),
])
export type Request = Schema.Schema.Type<typeof Request>
export const decodeRequest = Schema.decodeUnknownSync(Request)
export const TraceRecord = Schema.Struct({
id: Schema.Number,
@@ -130,6 +142,18 @@ export namespace Backend {
export const DisconnectParams = Schema.Struct({ id: Schema.String })
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
export const Request = Schema.Union([
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.chunk"), params: ChunkParams }),
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.finish"), params: FinishParams }),
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.disconnect"), params: DisconnectParams }),
Schema.Struct({
...JsonRpc.RequestFields,
method: Schema.Literals(["llm.attach", "llm.pending", "network.log"]),
}),
])
export type Request = Schema.Schema.Type<typeof Request>
export const decodeRequest = Schema.decodeUnknownSync(Request)
export const OpenedExchange = Schema.Struct({ id: Schema.String, url: Schema.String, body: Schema.Json })
export interface OpenedExchange extends Schema.Schema.Type<typeof OpenedExchange> {}
@@ -141,9 +165,6 @@ export namespace Backend {
})
export interface NetworkLogEntry extends Schema.Schema.Type<typeof NetworkLogEntry> {}
export const decodeChunkParams = Schema.decodeUnknownPromise(ChunkParams)
export const decodeFinishParams = Schema.decodeUnknownPromise(FinishParams)
export const decodeDisconnectParams = Schema.decodeUnknownPromise(DisconnectParams)
}
export * as SimulationProtocol from "./index"