Files
anomalyco_opencode/packages/simulation/src/backend/control.ts
T

98 lines
3.4 KiB
TypeScript

import { Effect } from "effect"
import { SimulationProtocol } from "../protocol"
import { SimulationLLMExchange } from "./llm-exchange"
/**
* Backend-hosted simulation control WebSocket.
*
* JSON-RPC 2.0 over a loopback WebSocket, mirroring the protocol of the TUI
* simulation server. Drivers connect directly (standalone topology; no
* frontend proxy) to answer LLM exchanges and inspect the simulated network.
* This is also the headless-simulation interface: it works with no TUI at
* all.
*
* Methods:
* - `llm.attach` -> subscribe; pending and future exchanges arrive
* as `llm.request` notifications
* - `llm.chunk` { id, items } append response items to an exchange
* - `llm.finish` { id, reason? } finish an exchange
* - `llm.disconnect` { id } abruptly terminate an exchange without a finish
* - `llm.pending` list open exchanges
*/
type ControlSocket = Bun.ServerWebSocket<{ unsubscribe?: () => void }>
function parseRequest(input: string | Buffer) {
return SimulationProtocol.Backend.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
}
async function handle(socket: ControlSocket, request: SimulationProtocol.Backend.Request): Promise<unknown> {
switch (request.method) {
case "llm.attach": {
socket.data.unsubscribe?.()
socket.data.unsubscribe = SimulationLLMExchange.subscribe((exchange) => {
socket.send(JSON.stringify({ jsonrpc: "2.0", method: "llm.request", params: exchange }))
})
return { attached: true }
}
case "llm.chunk": {
await Effect.runPromise(
SimulationLLMExchange.push(
request.params.id,
request.params.items.map((item) => ({ type: "item", item }) as const),
),
)
return { ok: true }
}
case "llm.finish": {
await Effect.runPromise(
SimulationLLMExchange.push(request.params.id, [{ type: "finish", reason: request.params.reason }]),
)
return { ok: true }
}
case "llm.disconnect": {
await Effect.runPromise(SimulationLLMExchange.disconnect(request.params.id))
return { ok: true }
}
case "llm.pending":
return { exchanges: SimulationLLMExchange.pending() }
}
}
export function start(endpoint: string) {
const url = new URL(endpoint)
const server = Bun.serve<{ unsubscribe?: () => void }>({
hostname: url.hostname,
port: Number(url.port),
fetch(request, server) {
if (server.upgrade(request, { data: {} })) return undefined
return new Response("opencode drive backend websocket", { status: 426 })
},
websocket: {
close(socket) {
socket.data.unsubscribe?.()
},
async message(socket, message) {
let request: SimulationProtocol.Backend.Request | undefined
try {
request = parseRequest(message)
const result = await handle(socket, request)
const response = SimulationProtocol.JsonRpc.success(request.id, result)
if (response) socket.send(JSON.stringify(response))
} catch (error) {
socket.send(JSON.stringify(SimulationProtocol.JsonRpc.failure(request?.id, error)))
}
},
},
})
process.stderr.write(`opencode drive backend websocket: ${endpoint}\n`)
return {
url: endpoint,
stop: () => {
server.stop(true)
},
}
}
export * as SimulationControl from "./control"