Quellcode durchsuchen

refactor(simulation): type RPC request payloads (#35734)

James Long vor 1 Monat
Ursprung
Commit
b6a2912bb1

+ 9 - 11
packages/simulation/src/backend/control.ts

@@ -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
packages/simulation/src/frontend/actions.ts

@@ -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
packages/simulation/src/frontend/renderer.ts

@@ -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
packages/simulation/src/frontend/server.ts

@@ -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) {

+ 4 - 5
packages/simulation/src/frontend/simulation.ts

@@ -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
packages/simulation/src/protocol/index.ts

@@ -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"