refactor(simulation): type RPC request payloads (#35734)
This commit is contained in:
parent
93b7ca9b4d
commit
b6a2912bb1
6 changed files with 48 additions and 40 deletions
|
|
@ -25,10 +25,10 @@ import { SimulationNetwork } from "./network"
|
||||||
type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }>
|
type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }>
|
||||||
|
|
||||||
function parseRequest(input: string | Buffer) {
|
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) {
|
switch (request.method) {
|
||||||
case "llm.attach": {
|
case "llm.attach": {
|
||||||
socket.data.unsubscribe?.()
|
socket.data.unsubscribe?.()
|
||||||
|
|
@ -38,23 +38,22 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc
|
||||||
return { attached: true }
|
return { attached: true }
|
||||||
}
|
}
|
||||||
case "llm.chunk": {
|
case "llm.chunk": {
|
||||||
const params = await SimulationProtocol.Backend.decodeChunkParams(request.params)
|
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
SimulationLLMExchange.push(
|
SimulationLLMExchange.push(
|
||||||
params.id,
|
request.params.id,
|
||||||
params.items.map((item) => ({ type: "item", item }) as const),
|
request.params.items.map((item) => ({ type: "item", item }) as const),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
case "llm.finish": {
|
case "llm.finish": {
|
||||||
const params = await SimulationProtocol.Backend.decodeFinishParams(request.params)
|
await Effect.runPromise(
|
||||||
await Effect.runPromise(SimulationLLMExchange.push(params.id, [{ type: "finish", reason: params.reason }]))
|
SimulationLLMExchange.push(request.params.id, [{ type: "finish", reason: request.params.reason }]),
|
||||||
|
)
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
case "llm.disconnect": {
|
case "llm.disconnect": {
|
||||||
const params = await SimulationProtocol.Backend.decodeDisconnectParams(request.params)
|
await Effect.runPromise(SimulationLLMExchange.disconnect(request.params.id))
|
||||||
await Effect.runPromise(SimulationLLMExchange.disconnect(params.id))
|
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
case "llm.pending":
|
case "llm.pending":
|
||||||
|
|
@ -62,7 +61,6 @@ async function handle(socket: ControlSocket, request: SimulationProtocol.JsonRpc
|
||||||
case "network.log":
|
case "network.log":
|
||||||
return { entries: SimulationNetwork.log() }
|
return { entries: SimulationNetwork.log() }
|
||||||
}
|
}
|
||||||
throw new Error(`Unknown simulation control method: ${request.method}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function start(endpoint: string) {
|
export function start(endpoint: string) {
|
||||||
|
|
@ -79,7 +77,7 @@ export function start(endpoint: string) {
|
||||||
socket.data.unsubscribe?.()
|
socket.data.unsubscribe?.()
|
||||||
},
|
},
|
||||||
async message(socket, message) {
|
async message(socket, message) {
|
||||||
let request: SimulationProtocol.JsonRpc.Request | undefined
|
let request: SimulationProtocol.Backend.Request | undefined
|
||||||
try {
|
try {
|
||||||
request = parseRequest(message)
|
request = parseRequest(message)
|
||||||
const result = await handle(socket, request)
|
const result = await handle(socket, request)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function hit(renderer: CliRenderer, renderable: Renderable) {
|
||||||
/**
|
/**
|
||||||
* Builds the harness the simulation server drives.
|
* 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
|
* provides the supported testing APIs. For the visible terminal renderer the
|
||||||
* harness falls back to `requestRender` + `idle` and reading the private
|
* harness falls back to `requestRender` + `idle` and reading the private
|
||||||
* `currentRenderBuffer`.
|
* `currentRenderBuffer`.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testin
|
||||||
const setups = new WeakMap<CliRenderer, TestRendererSetup>()
|
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
|
* in-memory screen buffer instead of a terminal. The TestRendererSetup is
|
||||||
* kept module-side (keyed by renderer) so the harness can use the supported
|
* kept module-side (keyed by renderer) so the harness can use the supported
|
||||||
* testing APIs without app code carrying it around.
|
* testing APIs without app code carrying it around.
|
||||||
|
|
|
||||||
|
|
@ -7,29 +7,20 @@ export interface Server {
|
||||||
readonly stop: () => void
|
readonly stop: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionParam(params: unknown) {
|
|
||||||
return SimulationProtocol.Frontend.decodeActionParams(params).action
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRequest(input: string | Buffer) {
|
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) {
|
switch (request.method) {
|
||||||
case "ui.state": {
|
case "ui.state": {
|
||||||
|
if (headless) await harness.renderOnce()
|
||||||
const result = SimulationActions.state(harness)
|
const result = SimulationActions.state(harness)
|
||||||
SimulationTrace.add("ui.state", { elements: result.elements.length, actions: result.actions.length })
|
SimulationTrace.add("ui.state", { elements: result.elements.length, actions: result.actions.length })
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
case "ui.action":
|
case "ui.action":
|
||||||
return SimulationActions.execute(harness, actionParam(request.params))
|
return SimulationActions.execute(harness, request.params.action)
|
||||||
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
|
|
||||||
}
|
|
||||||
case "trace.list":
|
case "trace.list":
|
||||||
return { records: SimulationTrace.list() }
|
return { records: SimulationTrace.list() }
|
||||||
case "trace.clear":
|
case "trace.clear":
|
||||||
|
|
@ -38,10 +29,9 @@ async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Requ
|
||||||
case "trace.export":
|
case "trace.export":
|
||||||
return SimulationTrace.exportTrace()
|
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 url = new URL(endpoint)
|
||||||
const server = Bun.serve<{ readonly drive: true }>({
|
const server = Bun.serve<{ readonly drive: true }>({
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
|
|
@ -58,10 +48,10 @@ export function start(harness: Harness, endpoint: string): Server {
|
||||||
SimulationTrace.add("control.disconnect")
|
SimulationTrace.add("control.disconnect")
|
||||||
},
|
},
|
||||||
async message(socket, message) {
|
async message(socket, message) {
|
||||||
let request: SimulationProtocol.JsonRpc.Request | undefined
|
let request: SimulationProtocol.Frontend.Request | undefined
|
||||||
try {
|
try {
|
||||||
request = parseRequest(message)
|
request = parseRequest(message)
|
||||||
const result = await handle(harness, request)
|
const result = await handle(harness, request, headless)
|
||||||
const next = SimulationProtocol.JsonRpc.success(request.id, result)
|
const next = SimulationProtocol.JsonRpc.success(request.id, result)
|
||||||
if (next) socket.send(JSON.stringify(next))
|
if (next) socket.send(JSON.stringify(next))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,18 @@ import { SimulationServer } from "./server"
|
||||||
/**
|
/**
|
||||||
* Drive-mode renderer entry point.
|
* 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
|
* visible renderer otherwise) and starts the UI control
|
||||||
* server against it. The server stops when the renderer is destroyed, so the
|
* server against it. The server stops when the renderer is destroyed, so the
|
||||||
* caller only manages the renderer lifecycle.
|
* caller only manages the renderer lifecycle.
|
||||||
*/
|
*/
|
||||||
export async function create(options: CliRendererConfig): Promise<CliRenderer> {
|
export async function create(options: CliRendererConfig): Promise<CliRenderer> {
|
||||||
const renderer =
|
const headless = process.env.OPENCODE_DRIVE_RENDERER === "headless"
|
||||||
process.env.OPENCODE_DRIVE_RENDERER === "fake"
|
const renderer = headless ? await SimulationRenderer.create(options) : await createCliRenderer(options)
|
||||||
? await SimulationRenderer.create(options)
|
|
||||||
: await createCliRenderer(options)
|
|
||||||
const server = SimulationServer.start(
|
const server = SimulationServer.start(
|
||||||
SimulationActions.createHarness(renderer),
|
SimulationActions.createHarness(renderer),
|
||||||
DriveManifest.resolve().endpoints.ui,
|
DriveManifest.resolve().endpoints.ui,
|
||||||
|
headless,
|
||||||
)
|
)
|
||||||
process.stderr.write(`opencode drive ui websocket: ${server.url}\n`)
|
process.stderr.write(`opencode drive ui websocket: ${server.url}\n`)
|
||||||
renderer.once("destroy", () => server.stop())
|
renderer.once("destroy", () => server.stop())
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ const JsonRpcID = Schema.Union([Schema.String, Schema.Number, Schema.Null])
|
||||||
type Json = Schema.Schema.Type<typeof Schema.Json>
|
type Json = Schema.Schema.Type<typeof Schema.Json>
|
||||||
|
|
||||||
export namespace JsonRpc {
|
export namespace JsonRpc {
|
||||||
export const Request = Schema.Struct({
|
export const RequestFields = {
|
||||||
jsonrpc: Schema.Literal("2.0"),
|
jsonrpc: Schema.Literal("2.0"),
|
||||||
id: Schema.optional(JsonRpcID),
|
id: Schema.optional(JsonRpcID),
|
||||||
|
}
|
||||||
|
export const Request = Schema.Struct({
|
||||||
|
...RequestFields,
|
||||||
method: Schema.String,
|
method: Schema.String,
|
||||||
params: Schema.optional(Schema.Json),
|
params: Schema.optional(Schema.Json),
|
||||||
})
|
})
|
||||||
|
|
@ -92,7 +95,16 @@ export namespace Frontend {
|
||||||
|
|
||||||
export const ActionParams = Schema.Struct({ action: Action })
|
export const ActionParams = Schema.Struct({ action: Action })
|
||||||
export interface ActionParams extends Schema.Schema.Type<typeof ActionParams> {}
|
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({
|
export const TraceRecord = Schema.Struct({
|
||||||
id: Schema.Number,
|
id: Schema.Number,
|
||||||
|
|
@ -130,6 +142,18 @@ export namespace Backend {
|
||||||
export const DisconnectParams = Schema.Struct({ id: Schema.String })
|
export const DisconnectParams = Schema.Struct({ id: Schema.String })
|
||||||
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
|
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 const OpenedExchange = Schema.Struct({ id: Schema.String, url: Schema.String, body: Schema.Json })
|
||||||
export interface OpenedExchange extends Schema.Schema.Type<typeof OpenedExchange> {}
|
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 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"
|
export * as SimulationProtocol from "./index"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue