feat(simulation): add endpoint handshake protocol (#37157)
This commit is contained in:
parent
f92d84746b
commit
5e2e0d6965
8 changed files with 350 additions and 2 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { Cause, Context, Effect, Fiber, FiberSet, Layer, PubSub, Queue, Ref, Schema, Semaphore, Stream } from "effect"
|
||||
import { SimulationControlServer } from "../control-server"
|
||||
import { SimulationProtocol } from "../protocol"
|
||||
|
|
@ -211,6 +212,15 @@ function handle(
|
|||
request: SimulationProtocol.Backend.Request,
|
||||
) {
|
||||
switch (request.method) {
|
||||
case "simulation.handshake":
|
||||
return SimulationProtocol.Handshake.dispatch(
|
||||
{
|
||||
role: "backend",
|
||||
server: { name: "opencode", version: InstallationVersion },
|
||||
capabilities: SimulationProtocol.Backend.Capabilities,
|
||||
},
|
||||
request.params,
|
||||
)
|
||||
case "llm.attach":
|
||||
return controllerLock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { SimulationControlServer } from "../control-server"
|
||||
import { SimulationProtocol } from "../protocol"
|
||||
|
|
@ -6,6 +7,15 @@ import { SimulationRenderer } from "./renderer"
|
|||
|
||||
function handle(harness: Harness, request: SimulationProtocol.Frontend.Request) {
|
||||
switch (request.method) {
|
||||
case "simulation.handshake":
|
||||
return SimulationProtocol.Handshake.dispatch(
|
||||
{
|
||||
role: "ui",
|
||||
server: { name: "opencode", version: InstallationVersion },
|
||||
capabilities: SimulationProtocol.Frontend.Capabilities,
|
||||
},
|
||||
request.params,
|
||||
)
|
||||
case "ui.capture":
|
||||
return SimulationActions.capture(harness)
|
||||
case "ui.screenshot":
|
||||
|
|
|
|||
|
|
@ -48,7 +48,136 @@ export namespace JsonRpc {
|
|||
}
|
||||
}
|
||||
|
||||
export namespace Handshake {
|
||||
export const ProtocolVersion = Schema.Literal(1)
|
||||
export type ProtocolVersion = Schema.Schema.Type<typeof ProtocolVersion>
|
||||
|
||||
export const Capability = Schema.NonEmptyString
|
||||
export type Capability = Schema.Schema.Type<typeof Capability>
|
||||
|
||||
export const EndpointRole = Schema.Literals(["ui", "backend"])
|
||||
export type EndpointRole = Schema.Schema.Type<typeof EndpointRole>
|
||||
|
||||
export const Identity = Schema.Struct({
|
||||
name: Schema.NonEmptyString,
|
||||
version: Schema.NonEmptyString,
|
||||
})
|
||||
export interface Identity extends Schema.Schema.Type<typeof Identity> {}
|
||||
|
||||
export const Params = Schema.Struct({
|
||||
client: Identity,
|
||||
expectedRole: EndpointRole,
|
||||
offeredVersions: Schema.Array(
|
||||
Schema.Int.check(Schema.isGreaterThan(0)),
|
||||
).check(Schema.isMinLength(1), Schema.isUnique()),
|
||||
requiredCapabilities: Schema.Array(Capability).check(Schema.isUnique()),
|
||||
optionalCapabilities: Schema.Array(Capability).check(Schema.isUnique()),
|
||||
})
|
||||
export interface Params extends Schema.Schema.Type<typeof Params> {}
|
||||
|
||||
export const Response = Schema.Struct({
|
||||
protocolVersion: ProtocolVersion,
|
||||
role: EndpointRole,
|
||||
server: Identity,
|
||||
capabilities: Schema.Array(Capability),
|
||||
})
|
||||
export interface Response extends Schema.Schema.Type<typeof Response> {}
|
||||
|
||||
export const Request = Schema.Struct({
|
||||
...JsonRpc.RequestFields,
|
||||
method: Schema.Literal("simulation.handshake"),
|
||||
params: Params,
|
||||
})
|
||||
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
||||
|
||||
export interface DispatchAction {
|
||||
readonly role: EndpointRole
|
||||
readonly server: Identity
|
||||
readonly capabilities: ReadonlyArray<Capability>
|
||||
}
|
||||
|
||||
export class RoleMismatchError extends Schema.TaggedErrorClass<RoleMismatchError>()(
|
||||
"SimulationHandshake.RoleMismatchError",
|
||||
{
|
||||
expected: EndpointRole,
|
||||
actual: EndpointRole,
|
||||
message: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class UnsupportedProtocolError extends Schema.TaggedErrorClass<UnsupportedProtocolError>()(
|
||||
"SimulationHandshake.UnsupportedProtocolError",
|
||||
{
|
||||
offered: Schema.Array(Schema.Number),
|
||||
supported: Schema.Array(ProtocolVersion),
|
||||
message: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export class MissingCapabilityError extends Schema.TaggedErrorClass<MissingCapabilityError>()(
|
||||
"SimulationHandshake.MissingCapabilityError",
|
||||
{
|
||||
missing: Schema.Array(Capability),
|
||||
message: Schema.String,
|
||||
},
|
||||
) {}
|
||||
|
||||
export function dispatch(action: DispatchAction, params: Params) {
|
||||
return Effect.gen(function* () {
|
||||
if (params.expectedRole !== action.role) {
|
||||
return yield* Effect.fail(
|
||||
new RoleMismatchError({
|
||||
expected: params.expectedRole,
|
||||
actual: action.role,
|
||||
message: `Expected simulation endpoint role ${params.expectedRole}, received ${action.role}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (!params.offeredVersions.includes(1)) {
|
||||
return yield* Effect.fail(
|
||||
new UnsupportedProtocolError({
|
||||
offered: params.offeredVersions,
|
||||
supported: [1],
|
||||
message: "No mutually supported simulation protocol version",
|
||||
}),
|
||||
)
|
||||
}
|
||||
const installed = new Set(action.capabilities)
|
||||
const missing = params.requiredCapabilities.filter((capability) => !installed.has(capability))
|
||||
if (missing.length > 0) {
|
||||
return yield* Effect.fail(
|
||||
new MissingCapabilityError({
|
||||
missing,
|
||||
message: `Simulation endpoint is missing required capabilities: ${missing.join(", ")}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return {
|
||||
protocolVersion: 1,
|
||||
role: action.role,
|
||||
server: action.server,
|
||||
capabilities: Array.from(installed),
|
||||
} satisfies Response
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Frontend {
|
||||
export const Capabilities = [
|
||||
"ui.type",
|
||||
"ui.press",
|
||||
"ui.enter",
|
||||
"ui.arrow",
|
||||
"ui.focus",
|
||||
"ui.click",
|
||||
"ui.resize",
|
||||
"ui.matches",
|
||||
"ui.screenshot",
|
||||
"ui.state",
|
||||
"ui.capture",
|
||||
"ui.recording.finish",
|
||||
] as const satisfies ReadonlyArray<Handshake.Capability>
|
||||
|
||||
export const KeyModifiers = Schema.Struct({
|
||||
ctrl: Schema.optional(Schema.Boolean),
|
||||
shift: Schema.optional(Schema.Boolean),
|
||||
|
|
@ -149,6 +278,7 @@ export namespace Frontend {
|
|||
export interface ResizeParams extends Schema.Schema.Type<typeof ResizeParams> {}
|
||||
|
||||
export const Request = Schema.Union([
|
||||
Handshake.Request,
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.type"), params: TypeParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.press"), params: PressParams }),
|
||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.arrow"), params: ArrowParams }),
|
||||
|
|
@ -173,6 +303,15 @@ export namespace Frontend {
|
|||
}
|
||||
|
||||
export namespace Backend {
|
||||
export const Capabilities = [
|
||||
"llm.attach",
|
||||
"llm.chunk",
|
||||
"llm.finish",
|
||||
"llm.disconnect",
|
||||
"llm.pending",
|
||||
"llm.request",
|
||||
] as const satisfies ReadonlyArray<Handshake.Capability>
|
||||
|
||||
export const Item = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("textDelta"), text: Schema.String }),
|
||||
Schema.Struct({ type: Schema.Literal("reasoningDelta"), text: Schema.String }),
|
||||
|
|
@ -203,6 +342,7 @@ export namespace Backend {
|
|||
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
|
||||
|
||||
export const Request = Schema.Union([
|
||||
Handshake.Request,
|
||||
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 }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue