opencode-drive 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -0
- package/bin/opencode-drive +2 -0
- package/package.json +45 -0
- package/src/cli/commands.ts +175 -0
- package/src/cli/describe.ts +14 -0
- package/src/cli/driver-runner.ts +39 -0
- package/src/cli/driver.ts +28 -0
- package/src/cli/index.ts +158 -0
- package/src/cli/instance.ts +218 -0
- package/src/cli/parse.ts +24 -0
- package/src/cli/registry.ts +120 -0
- package/src/cli/send.ts +48 -0
- package/src/cli/start.ts +56 -0
- package/src/cli/types.ts +46 -0
- package/src/client/backend.ts +184 -0
- package/src/client/client.ts +252 -0
- package/src/client/index.ts +17 -0
- package/src/client/protocol.ts +186 -0
- package/src/experimental/campaign-api.ts +34 -0
- package/src/experimental/campaign.ts +144 -0
- package/src/experimental/cli-campaign.ts +179 -0
- package/src/experimental/drive.ts +37 -0
- package/src/experimental/driver.ts +41 -0
- package/src/experimental/flow-driver.ts +189 -0
- package/src/experimental/flows/generate.ts +278 -0
- package/src/experimental/flows/index.ts +6 -0
- package/src/experimental/flows/properties.ts +51 -0
- package/src/experimental/flows/types.ts +47 -0
- package/src/experimental/flows/weights.ts +198 -0
- package/src/experimental/generators/config.ts +240 -0
- package/src/experimental/generators/filesystem.ts +95 -0
- package/src/experimental/generators/generate.ts +32 -0
- package/src/experimental/generators/index.ts +10 -0
- package/src/experimental/generators/initial-state.ts +37 -0
- package/src/experimental/generators/random.ts +35 -0
- package/src/experimental/hello-driver.ts +42 -0
- package/src/experimental/index.ts +3 -0
- package/src/experimental/model/derive.ts +119 -0
- package/src/experimental/model/index.ts +2 -0
- package/src/experimental/model/model.ts +42 -0
- package/src/experimental/opencode-simulation.ts +1 -0
- package/src/experimental/reproduce-stale-running-visible.ts +60 -0
- package/src/experimental/reproduce-stale-running.ts +26 -0
- package/src/experimental/stale-running-driver.ts +74 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Backend,
|
|
3
|
+
type JsonRpc,
|
|
4
|
+
} from "./protocol.js"
|
|
5
|
+
|
|
6
|
+
const defaultBackendPort = 40950
|
|
7
|
+
|
|
8
|
+
type BackendMethods = {
|
|
9
|
+
readonly "llm.attach": { readonly params: undefined; readonly result: { readonly attached: true } }
|
|
10
|
+
readonly "llm.chunk": { readonly params: Backend.ChunkParams; readonly result: { readonly ok: true } }
|
|
11
|
+
readonly "llm.finish": { readonly params: Partial<Backend.FinishParams> & Pick<Backend.FinishParams, "id">; readonly result: { readonly ok: true } }
|
|
12
|
+
readonly "llm.disconnect": { readonly params: Backend.DisconnectParams; readonly result: { readonly ok: true } }
|
|
13
|
+
readonly "llm.pending": { readonly params: undefined; readonly result: { readonly exchanges: ReadonlyArray<Backend.OpenedExchange> } }
|
|
14
|
+
readonly "network.log": { readonly params: undefined; readonly result: { readonly entries: ReadonlyArray<Backend.NetworkLogEntry> } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type BackendMethodName = keyof BackendMethods
|
|
18
|
+
|
|
19
|
+
export interface BackendSimulationClientOptions {
|
|
20
|
+
readonly url?: string
|
|
21
|
+
readonly port?: number
|
|
22
|
+
readonly portAttempts?: number
|
|
23
|
+
readonly timeout?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BackendSimulationError extends Error {
|
|
27
|
+
constructor(
|
|
28
|
+
message: string,
|
|
29
|
+
readonly method?: string,
|
|
30
|
+
) {
|
|
31
|
+
super(message)
|
|
32
|
+
this.name = "BackendSimulationError"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Waiter {
|
|
37
|
+
readonly method: string
|
|
38
|
+
readonly resolve: (value: unknown) => void
|
|
39
|
+
readonly reject: (error: Error) => void
|
|
40
|
+
readonly timer: ReturnType<typeof setTimeout>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class BackendSimulationClient {
|
|
44
|
+
readonly url: string
|
|
45
|
+
|
|
46
|
+
private readonly socket: WebSocket
|
|
47
|
+
private readonly timeout: number
|
|
48
|
+
private nextId = 1
|
|
49
|
+
private readonly pending = new Map<number, Waiter>()
|
|
50
|
+
private readonly llmRequests = new Set<(request: Backend.OpenedExchange) => void>()
|
|
51
|
+
|
|
52
|
+
private constructor(socket: WebSocket, url: string, timeout: number) {
|
|
53
|
+
this.socket = socket
|
|
54
|
+
this.url = url
|
|
55
|
+
this.timeout = timeout
|
|
56
|
+
socket.addEventListener("message", (event) => this.onMessage(String(event.data)))
|
|
57
|
+
socket.addEventListener("close", () => this.rejectAll(new BackendSimulationError("connection closed")))
|
|
58
|
+
socket.addEventListener("error", () => this.rejectAll(new BackendSimulationError("connection error")))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static async connect(options?: BackendSimulationClientOptions): Promise<BackendSimulationClient> {
|
|
62
|
+
const timeout = options?.timeout ?? 30_000
|
|
63
|
+
if (options?.url !== undefined) return new BackendSimulationClient(await open(options.url), options.url, timeout)
|
|
64
|
+
const first = options?.port ?? defaultBackendPort
|
|
65
|
+
const attempts = options?.portAttempts ?? 10
|
|
66
|
+
for (let offset = 0; offset < attempts; offset++) {
|
|
67
|
+
const url = `ws://127.0.0.1:${first + offset}`
|
|
68
|
+
try {
|
|
69
|
+
return new BackendSimulationClient(await open(url), url, timeout)
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
throw new BackendSimulationError(`no backend simulation server found on ports ${first}-${first + attempts - 1}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async call<M extends BackendMethodName>(
|
|
76
|
+
method: M,
|
|
77
|
+
params?: BackendMethods[M]["params"],
|
|
78
|
+
): Promise<BackendMethods[M]["result"]> {
|
|
79
|
+
if (this.socket.readyState !== WebSocket.OPEN) throw new BackendSimulationError("connection is not open", method)
|
|
80
|
+
const id = this.nextId++
|
|
81
|
+
const promise = new Promise<unknown>((resolve, reject) => {
|
|
82
|
+
const timer = setTimeout(() => {
|
|
83
|
+
this.pending.delete(id)
|
|
84
|
+
reject(new BackendSimulationError(`timed out after ${this.timeout}ms`, method))
|
|
85
|
+
}, this.timeout)
|
|
86
|
+
this.pending.set(id, { method, resolve, reject, timer })
|
|
87
|
+
})
|
|
88
|
+
this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) }))
|
|
89
|
+
return (await promise) as BackendMethods[M]["result"]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async attach(onRequest: (request: Backend.OpenedExchange) => void | Promise<void>) {
|
|
93
|
+
this.llmRequests.add((request) => void onRequest(request))
|
|
94
|
+
return await this.call("llm.attach")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
chunk(id: string, items: ReadonlyArray<Backend.Item>) {
|
|
98
|
+
return this.call("llm.chunk", { id, items: [...items] })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
finish(id: string, reason?: Backend.FinishReason) {
|
|
102
|
+
return this.call("llm.finish", { id, ...(reason === undefined ? {} : { reason }) })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
disconnect(id: string) {
|
|
106
|
+
return this.call("llm.disconnect", { id })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pendingExchanges() {
|
|
110
|
+
return this.call("llm.pending")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
networkLog(): Promise<{ readonly entries: ReadonlyArray<Backend.NetworkLogEntry> }> {
|
|
114
|
+
return this.call("network.log")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
close() {
|
|
118
|
+
this.socket.close()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private onMessage(data: string) {
|
|
122
|
+
const message = parseResponse(data)
|
|
123
|
+
if (message === undefined) return
|
|
124
|
+
if ("method" in message) {
|
|
125
|
+
if (message.method === "llm.request") {
|
|
126
|
+
for (const listener of this.llmRequests) listener(message.params as Backend.OpenedExchange)
|
|
127
|
+
}
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (typeof message.id !== "number") return
|
|
131
|
+
const waiter = this.pending.get(message.id)
|
|
132
|
+
if (waiter === undefined) return
|
|
133
|
+
this.pending.delete(message.id)
|
|
134
|
+
clearTimeout(waiter.timer)
|
|
135
|
+
if (message.error) waiter.reject(new BackendSimulationError(message.error.message, waiter.method))
|
|
136
|
+
else waiter.resolve(message.result)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private rejectAll(error: BackendSimulationError) {
|
|
140
|
+
for (const waiter of this.pending.values()) {
|
|
141
|
+
clearTimeout(waiter.timer)
|
|
142
|
+
waiter.reject(error)
|
|
143
|
+
}
|
|
144
|
+
this.pending.clear()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseResponse(data: string): JsonRpc.Response | { readonly method: string; readonly params: unknown } | undefined {
|
|
149
|
+
try {
|
|
150
|
+
const value = JSON.parse(data) as unknown
|
|
151
|
+
if (typeof value !== "object" || value === null) return undefined
|
|
152
|
+
if (!("jsonrpc" in value) || value.jsonrpc !== "2.0") return undefined
|
|
153
|
+
if ("method" in value && typeof value.method === "string") {
|
|
154
|
+
return { method: value.method, params: "params" in value ? value.params : undefined }
|
|
155
|
+
}
|
|
156
|
+
if (!("id" in value)) return undefined
|
|
157
|
+
return value as JsonRpc.Response
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function open(url: string): Promise<WebSocket> {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const socket = new WebSocket(url)
|
|
166
|
+
const onOpen = () => {
|
|
167
|
+
cleanup()
|
|
168
|
+
resolve(socket)
|
|
169
|
+
}
|
|
170
|
+
const onError = () => {
|
|
171
|
+
cleanup()
|
|
172
|
+
reject(new BackendSimulationError(`cannot connect to ${url}`))
|
|
173
|
+
}
|
|
174
|
+
const cleanup = () => {
|
|
175
|
+
socket.removeEventListener("open", onOpen)
|
|
176
|
+
socket.removeEventListener("error", onError)
|
|
177
|
+
}
|
|
178
|
+
socket.addEventListener("open", onOpen)
|
|
179
|
+
socket.addEventListener("error", onError)
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const connectBackendSimulation = (options?: BackendSimulationClientOptions): Promise<BackendSimulationClient> =>
|
|
184
|
+
BackendSimulationClient.connect(options)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Frontend,
|
|
3
|
+
type JsonRpc,
|
|
4
|
+
} from "./protocol.js"
|
|
5
|
+
|
|
6
|
+
const defaultPort = 40900
|
|
7
|
+
|
|
8
|
+
type Methods = {
|
|
9
|
+
readonly "ui.render": { readonly params: undefined; readonly result: Frontend.Render }
|
|
10
|
+
readonly "ui.state": { readonly params: undefined; readonly result: Frontend.State }
|
|
11
|
+
readonly "ui.start-record": { readonly params: undefined; readonly result: Frontend.StartRecord }
|
|
12
|
+
readonly "ui.end-record": { readonly params: undefined; readonly result: Frontend.EndRecord }
|
|
13
|
+
readonly "ui.action": { readonly params: Frontend.ActionParams; readonly result: Frontend.State }
|
|
14
|
+
readonly "trace.list": { readonly params: undefined; readonly result: Frontend.TraceList }
|
|
15
|
+
readonly "trace.clear": { readonly params: undefined; readonly result: { readonly cleared: true } }
|
|
16
|
+
readonly "trace.export": { readonly params: undefined; readonly result: Frontend.TraceList }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type MethodName = keyof Methods
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* WebSocket client for the OpenCode simulation control server.
|
|
23
|
+
*
|
|
24
|
+
* Start OpenCode with `OPENCODE_SIMULATION=1` (optionally
|
|
25
|
+
* `OPENCODE_SIMULATION_RENDERER=headless`), then connect from a probe run:
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const client = await connectSimulation()
|
|
29
|
+
* const state = await client.state()
|
|
30
|
+
* await client.typeText("hello")
|
|
31
|
+
* client.close()
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface SimulationClientOptions {
|
|
35
|
+
/** Explicit server URL; skips port scanning. */
|
|
36
|
+
readonly url?: string
|
|
37
|
+
/** First port to try when no URL is given. Defaults to 40900. */
|
|
38
|
+
readonly port?: number
|
|
39
|
+
/** Ports to scan upward from `port`. Defaults to 10. */
|
|
40
|
+
readonly portAttempts?: number
|
|
41
|
+
/** Per-call timeout in milliseconds. Defaults to 30_000. */
|
|
42
|
+
readonly timeout?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class SimulationError extends Error {
|
|
46
|
+
constructor(
|
|
47
|
+
message: string,
|
|
48
|
+
readonly method?: string,
|
|
49
|
+
) {
|
|
50
|
+
super(message)
|
|
51
|
+
this.name = "SimulationError"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Waiter {
|
|
56
|
+
readonly method: string
|
|
57
|
+
readonly resolve: (value: unknown) => void
|
|
58
|
+
readonly reject: (error: Error) => void
|
|
59
|
+
readonly timer: ReturnType<typeof setTimeout>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class SimulationClient {
|
|
63
|
+
readonly url: string
|
|
64
|
+
|
|
65
|
+
private readonly socket: WebSocket
|
|
66
|
+
private readonly timeout: number
|
|
67
|
+
private nextId = 1
|
|
68
|
+
private readonly pending = new Map<number, Waiter>()
|
|
69
|
+
|
|
70
|
+
private constructor(socket: WebSocket, url: string, timeout: number) {
|
|
71
|
+
this.socket = socket
|
|
72
|
+
this.url = url
|
|
73
|
+
this.timeout = timeout
|
|
74
|
+
socket.addEventListener("message", (event) => this.onMessage(String(event.data)))
|
|
75
|
+
socket.addEventListener("close", () => this.rejectAll(new SimulationError("connection closed")))
|
|
76
|
+
socket.addEventListener("error", () => this.rejectAll(new SimulationError("connection error")))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static async connect(options?: SimulationClientOptions): Promise<SimulationClient> {
|
|
80
|
+
const timeout = options?.timeout ?? 30_000
|
|
81
|
+
if (options?.url !== undefined) {
|
|
82
|
+
return new SimulationClient(await open(options.url), options.url, timeout)
|
|
83
|
+
}
|
|
84
|
+
const first = options?.port ?? defaultPort
|
|
85
|
+
const attempts = options?.portAttempts ?? 10
|
|
86
|
+
for (let offset = 0; offset < attempts; offset++) {
|
|
87
|
+
const url = `ws://127.0.0.1:${first + offset}`
|
|
88
|
+
try {
|
|
89
|
+
return new SimulationClient(await open(url), url, timeout)
|
|
90
|
+
} catch {
|
|
91
|
+
// occupied by something else or nothing listening; try the next port
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new SimulationError(
|
|
95
|
+
`no simulation server found on ports ${first}-${first + attempts - 1}; ` +
|
|
96
|
+
"is OpenCode running with OPENCODE_SIMULATION=1?",
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Raw JSON-RPC call. Prefer the typed wrappers below. */
|
|
101
|
+
async call<M extends MethodName>(method: M, params?: Methods[M]["params"]): Promise<Methods[M]["result"]> {
|
|
102
|
+
if (this.socket.readyState !== WebSocket.OPEN) {
|
|
103
|
+
throw new SimulationError("connection is not open", method)
|
|
104
|
+
}
|
|
105
|
+
const id = this.nextId++
|
|
106
|
+
const promise = new Promise<unknown>((resolve, reject) => {
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
this.pending.delete(id)
|
|
109
|
+
reject(new SimulationError(`timed out after ${this.timeout}ms`, method))
|
|
110
|
+
}, this.timeout)
|
|
111
|
+
this.pending.set(id, { method, resolve, reject, timer })
|
|
112
|
+
})
|
|
113
|
+
this.socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) }))
|
|
114
|
+
// The server contract types each method's result; the cast happens once
|
|
115
|
+
// here rather than at every call site.
|
|
116
|
+
return (await promise) as Methods[M]["result"]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── ui ────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/** Current screen, focus, elements, and generated actions. */
|
|
122
|
+
state(): Promise<Frontend.State> {
|
|
123
|
+
return this.call("ui.state")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render(): Promise<Frontend.Render> {
|
|
127
|
+
return this.call("ui.render")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
startRecord(): Promise<Frontend.StartRecord> {
|
|
131
|
+
return this.call("ui.start-record")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
endRecord(): Promise<Frontend.EndRecord> {
|
|
135
|
+
return this.call("ui.end-record")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Executes one user-level action and returns the post-action state. */
|
|
139
|
+
action(action: Frontend.Action): Promise<Frontend.State> {
|
|
140
|
+
return this.call("ui.action", { action })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
typeText(text: string): Promise<Frontend.State> {
|
|
144
|
+
return this.action({ type: "typeText", text })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pressKey(key: string, modifiers?: Frontend.KeyModifiers): Promise<Frontend.State> {
|
|
148
|
+
return this.action({ type: "pressKey", key: key === "escape" ? "\u001b" : key, ...(modifiers === undefined ? {} : { modifiers }) })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pressEnter(): Promise<Frontend.State> {
|
|
152
|
+
return this.action({ type: "pressEnter" })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pressArrow(direction: "up" | "down" | "left" | "right"): Promise<Frontend.State> {
|
|
156
|
+
return this.action({ type: "pressArrow", direction })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
focus(target: number): Promise<Frontend.State> {
|
|
160
|
+
return this.action({ type: "focus", target })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
click(target: number, x: number, y: number): Promise<Frontend.State> {
|
|
164
|
+
return this.action({ type: "click", target, x, y })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── trace ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
traceList(): Promise<Frontend.TraceList> {
|
|
170
|
+
return this.call("trace.list")
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
traceClear(): Promise<{ readonly cleared: true }> {
|
|
174
|
+
return this.call("trace.clear")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
traceExport(): Promise<Frontend.TraceList> {
|
|
178
|
+
return this.call("trace.export")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── lifecycle ─────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
close(): void {
|
|
184
|
+
this.socket.close()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private onMessage(data: string) {
|
|
188
|
+
const message = parseResponse(data)
|
|
189
|
+
if (message === undefined || typeof message.id !== "number") return
|
|
190
|
+
const waiter = this.pending.get(message.id)
|
|
191
|
+
if (waiter === undefined) return
|
|
192
|
+
this.pending.delete(message.id)
|
|
193
|
+
clearTimeout(waiter.timer)
|
|
194
|
+
if (message.error) waiter.reject(new SimulationError(message.error.message, waiter.method))
|
|
195
|
+
else waiter.resolve(message.result)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private rejectAll(error: SimulationError) {
|
|
199
|
+
for (const waiter of this.pending.values()) {
|
|
200
|
+
clearTimeout(waiter.timer)
|
|
201
|
+
waiter.reject(error)
|
|
202
|
+
}
|
|
203
|
+
this.pending.clear()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parseResponse(data: string): JsonRpc.Response | undefined {
|
|
208
|
+
let value: unknown
|
|
209
|
+
try {
|
|
210
|
+
value = JSON.parse(data)
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
if (typeof value !== "object" || value === null) return undefined
|
|
215
|
+
if (!("jsonrpc" in value) || value.jsonrpc !== "2.0") return undefined
|
|
216
|
+
if (!("id" in value)) return undefined
|
|
217
|
+
const id = value.id
|
|
218
|
+
if (typeof id !== "number" && typeof id !== "string" && id !== null) return undefined
|
|
219
|
+
const result = "result" in value ? value.result : undefined
|
|
220
|
+
const error = "error" in value ? value.error : undefined
|
|
221
|
+
if (error !== undefined) {
|
|
222
|
+
if (typeof error !== "object" || error === null) return undefined
|
|
223
|
+
const code = "code" in error ? error.code : undefined
|
|
224
|
+
const message = "message" in error ? error.message : undefined
|
|
225
|
+
if (typeof code !== "number" || typeof message !== "string") return undefined
|
|
226
|
+
return { jsonrpc: "2.0", id, error: { code, message } }
|
|
227
|
+
}
|
|
228
|
+
return { jsonrpc: "2.0", id, result: result as JsonRpc.Response["result"] }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function open(url: string): Promise<WebSocket> {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const socket = new WebSocket(url)
|
|
234
|
+
const onOpen = () => {
|
|
235
|
+
cleanup()
|
|
236
|
+
resolve(socket)
|
|
237
|
+
}
|
|
238
|
+
const onError = () => {
|
|
239
|
+
cleanup()
|
|
240
|
+
reject(new SimulationError(`cannot connect to ${url}`))
|
|
241
|
+
}
|
|
242
|
+
const cleanup = () => {
|
|
243
|
+
socket.removeEventListener("open", onOpen)
|
|
244
|
+
socket.removeEventListener("error", onError)
|
|
245
|
+
}
|
|
246
|
+
socket.addEventListener("open", onOpen)
|
|
247
|
+
socket.addEventListener("error", onError)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const connectSimulation = (options?: SimulationClientOptions): Promise<SimulationClient> =>
|
|
252
|
+
SimulationClient.connect(options)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { SimulationClient, SimulationError, connectSimulation } from "./client.js"
|
|
2
|
+
export { BackendSimulationClient, BackendSimulationError, connectBackendSimulation } from "./backend.js"
|
|
3
|
+
export type { BackendSimulationClientOptions } from "./backend.js"
|
|
4
|
+
export type { SimulationClientOptions } from "./client.js"
|
|
5
|
+
export const defaultPort = 40900
|
|
6
|
+
export const defaultBackendPort = 40950
|
|
7
|
+
export { Backend, Frontend, JsonRpc, SimulationProtocol } from "./protocol.js"
|
|
8
|
+
export type BackendFinishReason = import("./protocol.js").Backend.FinishReason
|
|
9
|
+
export type BackendItem = import("./protocol.js").Backend.Item
|
|
10
|
+
export type NetworkLogEntry = import("./protocol.js").Backend.NetworkLogEntry
|
|
11
|
+
export type OpenedExchange = import("./protocol.js").Backend.OpenedExchange
|
|
12
|
+
export type KeyModifiers = import("./protocol.js").Frontend.KeyModifiers
|
|
13
|
+
export type TraceList = import("./protocol.js").Frontend.TraceList
|
|
14
|
+
export type TraceRecord = import("./protocol.js").Frontend.TraceRecord
|
|
15
|
+
export type UiAction = import("./protocol.js").Frontend.Action
|
|
16
|
+
export type UiElement = import("./protocol.js").Frontend.Element
|
|
17
|
+
export type UiState = import("./protocol.js").Frontend.State
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
const JsonRpcID = Schema.Union([Schema.String, Schema.Number, Schema.Null])
|
|
4
|
+
type Json = Schema.Schema.Type<typeof Schema.Json>
|
|
5
|
+
|
|
6
|
+
export namespace JsonRpc {
|
|
7
|
+
export const RequestFields = {
|
|
8
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
9
|
+
id: Schema.optional(JsonRpcID),
|
|
10
|
+
}
|
|
11
|
+
export const Request = Schema.Struct({
|
|
12
|
+
...RequestFields,
|
|
13
|
+
method: Schema.String,
|
|
14
|
+
params: Schema.optional(Schema.Json),
|
|
15
|
+
})
|
|
16
|
+
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
|
17
|
+
|
|
18
|
+
export const ErrorObject = Schema.Struct({
|
|
19
|
+
code: Schema.Number,
|
|
20
|
+
message: Schema.String,
|
|
21
|
+
data: Schema.optional(Schema.Json),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const Response = Schema.Struct({
|
|
25
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
26
|
+
id: JsonRpcID,
|
|
27
|
+
result: Schema.optional(Schema.Json),
|
|
28
|
+
error: Schema.optional(ErrorObject),
|
|
29
|
+
})
|
|
30
|
+
export interface Response extends Schema.Schema.Type<typeof Response> {}
|
|
31
|
+
|
|
32
|
+
export const decodeRequest = Schema.decodeUnknownSync(Request)
|
|
33
|
+
|
|
34
|
+
export function success(id: Request["id"], result: unknown): Response | undefined {
|
|
35
|
+
if (id === undefined) return undefined
|
|
36
|
+
return { jsonrpc: "2.0", id, result: result as Json }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function failure(id: Request["id"], error: unknown): Response {
|
|
40
|
+
return {
|
|
41
|
+
jsonrpc: "2.0",
|
|
42
|
+
id: id ?? null,
|
|
43
|
+
error: {
|
|
44
|
+
code: -32000,
|
|
45
|
+
message: error instanceof Error ? error.message : String(error),
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export namespace Frontend {
|
|
52
|
+
export const KeyModifiers = Schema.Struct({
|
|
53
|
+
ctrl: Schema.optional(Schema.Boolean),
|
|
54
|
+
shift: Schema.optional(Schema.Boolean),
|
|
55
|
+
meta: Schema.optional(Schema.Boolean),
|
|
56
|
+
super: Schema.optional(Schema.Boolean),
|
|
57
|
+
hyper: Schema.optional(Schema.Boolean),
|
|
58
|
+
})
|
|
59
|
+
export interface KeyModifiers extends Schema.Schema.Type<typeof KeyModifiers> {}
|
|
60
|
+
|
|
61
|
+
export const Action = Schema.Union([
|
|
62
|
+
Schema.Struct({ type: Schema.Literal("typeText"), text: Schema.String }),
|
|
63
|
+
Schema.Struct({ type: Schema.Literal("pressKey"), key: Schema.String, modifiers: Schema.optional(KeyModifiers) }),
|
|
64
|
+
Schema.Struct({ type: Schema.Literal("pressEnter") }),
|
|
65
|
+
Schema.Struct({ type: Schema.Literal("pressArrow"), direction: Schema.Literals(["up", "down", "left", "right"]) }),
|
|
66
|
+
Schema.Struct({ type: Schema.Literal("focus"), target: Schema.Number }),
|
|
67
|
+
Schema.Struct({ type: Schema.Literal("click"), target: Schema.Number, x: Schema.Number, y: Schema.Number }),
|
|
68
|
+
])
|
|
69
|
+
export type Action = Schema.Schema.Type<typeof Action>
|
|
70
|
+
|
|
71
|
+
export const Element = Schema.Struct({
|
|
72
|
+
id: Schema.String,
|
|
73
|
+
num: Schema.Number,
|
|
74
|
+
x: Schema.Number,
|
|
75
|
+
y: Schema.Number,
|
|
76
|
+
width: Schema.Number,
|
|
77
|
+
height: Schema.Number,
|
|
78
|
+
focusable: Schema.Boolean,
|
|
79
|
+
focused: Schema.Boolean,
|
|
80
|
+
clickable: Schema.Boolean,
|
|
81
|
+
editor: Schema.Boolean,
|
|
82
|
+
})
|
|
83
|
+
export interface Element extends Schema.Schema.Type<typeof Element> {}
|
|
84
|
+
|
|
85
|
+
export const State = Schema.Struct({
|
|
86
|
+
focused: Schema.Struct({
|
|
87
|
+
renderable: Schema.optional(Schema.Number),
|
|
88
|
+
editor: Schema.Boolean,
|
|
89
|
+
}),
|
|
90
|
+
elements: Schema.Array(Element),
|
|
91
|
+
actions: Schema.Array(Action),
|
|
92
|
+
})
|
|
93
|
+
export interface State extends Schema.Schema.Type<typeof State> {}
|
|
94
|
+
|
|
95
|
+
export const Render = Schema.String
|
|
96
|
+
export type Render = Schema.Schema.Type<typeof Render>
|
|
97
|
+
|
|
98
|
+
export const StartRecord = Schema.Struct({ recording: Schema.Literal(true) })
|
|
99
|
+
export interface StartRecord extends Schema.Schema.Type<typeof StartRecord> {}
|
|
100
|
+
|
|
101
|
+
export const EndRecord = Schema.String
|
|
102
|
+
export type EndRecord = Schema.Schema.Type<typeof EndRecord>
|
|
103
|
+
|
|
104
|
+
export const ActionParams = Schema.Struct({ action: Action })
|
|
105
|
+
export interface ActionParams extends Schema.Schema.Type<typeof ActionParams> {}
|
|
106
|
+
|
|
107
|
+
export const Request = Schema.Union([
|
|
108
|
+
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.action"), params: ActionParams }),
|
|
109
|
+
Schema.Struct({
|
|
110
|
+
...JsonRpc.RequestFields,
|
|
111
|
+
method: Schema.Literals([
|
|
112
|
+
"ui.render",
|
|
113
|
+
"ui.state",
|
|
114
|
+
"ui.start-record",
|
|
115
|
+
"ui.end-record",
|
|
116
|
+
"trace.list",
|
|
117
|
+
"trace.clear",
|
|
118
|
+
"trace.export",
|
|
119
|
+
]),
|
|
120
|
+
}),
|
|
121
|
+
])
|
|
122
|
+
export type Request = Schema.Schema.Type<typeof Request>
|
|
123
|
+
export const decodeRequest = Schema.decodeUnknownSync(Request)
|
|
124
|
+
|
|
125
|
+
export const TraceRecord = Schema.Struct({
|
|
126
|
+
id: Schema.Number,
|
|
127
|
+
time: Schema.String,
|
|
128
|
+
type: Schema.String,
|
|
129
|
+
data: Schema.optional(Schema.Json),
|
|
130
|
+
})
|
|
131
|
+
export interface TraceRecord extends Schema.Schema.Type<typeof TraceRecord> {}
|
|
132
|
+
|
|
133
|
+
export const TraceList = Schema.Struct({ records: Schema.Array(TraceRecord) })
|
|
134
|
+
export interface TraceList extends Schema.Schema.Type<typeof TraceList> {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export namespace Backend {
|
|
138
|
+
export const Item = Schema.Union([
|
|
139
|
+
Schema.Struct({ type: Schema.Literal("textDelta"), text: Schema.String }),
|
|
140
|
+
Schema.Struct({ type: Schema.Literal("reasoningDelta"), text: Schema.String }),
|
|
141
|
+
Schema.Struct({ type: Schema.Literal("toolCall"), id: Schema.String, name: Schema.String, input: Schema.Json }),
|
|
142
|
+
Schema.Struct({ type: Schema.Literal("raw"), chunk: Schema.Json }),
|
|
143
|
+
])
|
|
144
|
+
export type Item = Schema.Schema.Type<typeof Item>
|
|
145
|
+
|
|
146
|
+
export const FinishReason = Schema.Literals(["stop", "tool-calls", "length", "content-filter"])
|
|
147
|
+
export type FinishReason = Schema.Schema.Type<typeof FinishReason>
|
|
148
|
+
|
|
149
|
+
export const ChunkParams = Schema.Struct({ id: Schema.String, items: Schema.Array(Item) })
|
|
150
|
+
export interface ChunkParams extends Schema.Schema.Type<typeof ChunkParams> {}
|
|
151
|
+
|
|
152
|
+
export const FinishParams = Schema.Struct({
|
|
153
|
+
id: Schema.String,
|
|
154
|
+
reason: FinishReason.pipe(Schema.withDecodingDefault(Effect.succeed("stop" as const))),
|
|
155
|
+
})
|
|
156
|
+
export interface FinishParams extends Schema.Schema.Type<typeof FinishParams> {}
|
|
157
|
+
|
|
158
|
+
export const DisconnectParams = Schema.Struct({ id: Schema.String })
|
|
159
|
+
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
|
|
160
|
+
|
|
161
|
+
export const Request = Schema.Union([
|
|
162
|
+
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.chunk"), params: ChunkParams }),
|
|
163
|
+
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.finish"), params: FinishParams }),
|
|
164
|
+
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.disconnect"), params: DisconnectParams }),
|
|
165
|
+
Schema.Struct({
|
|
166
|
+
...JsonRpc.RequestFields,
|
|
167
|
+
method: Schema.Literals(["llm.attach", "llm.pending", "network.log"]),
|
|
168
|
+
}),
|
|
169
|
+
])
|
|
170
|
+
export type Request = Schema.Schema.Type<typeof Request>
|
|
171
|
+
export const decodeRequest = Schema.decodeUnknownSync(Request)
|
|
172
|
+
|
|
173
|
+
export const OpenedExchange = Schema.Struct({ id: Schema.String, url: Schema.String, body: Schema.Json })
|
|
174
|
+
export interface OpenedExchange extends Schema.Schema.Type<typeof OpenedExchange> {}
|
|
175
|
+
|
|
176
|
+
export const NetworkLogEntry = Schema.Struct({
|
|
177
|
+
time: Schema.Number,
|
|
178
|
+
method: Schema.String,
|
|
179
|
+
url: Schema.String,
|
|
180
|
+
matched: Schema.Boolean,
|
|
181
|
+
})
|
|
182
|
+
export interface NetworkLogEntry extends Schema.Schema.Type<typeof NetworkLogEntry> {}
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export * as SimulationProtocol from "./index"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DriveContext } from "./drive.js"
|
|
2
|
+
|
|
3
|
+
export interface CampaignGenerateContext {
|
|
4
|
+
readonly index: number
|
|
5
|
+
readonly seed: number
|
|
6
|
+
readonly artifacts: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CampaignPrepareContext<Flow> extends CampaignGenerateContext {
|
|
10
|
+
readonly flow: Flow
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CampaignRunContext<Flow> extends CampaignPrepareContext<Flow>, DriveContext {}
|
|
14
|
+
|
|
15
|
+
export interface CampaignInstanceOptions {
|
|
16
|
+
readonly state?: string
|
|
17
|
+
readonly env?: Readonly<Record<string, string>>
|
|
18
|
+
readonly command?: ReadonlyArray<string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CampaignDefinition<Flow = unknown, Result = unknown> {
|
|
22
|
+
readonly count?: number
|
|
23
|
+
readonly generate: (context: CampaignGenerateContext) => Flow | Promise<Flow>
|
|
24
|
+
readonly prepare?: (context: CampaignPrepareContext<Flow>) => CampaignInstanceOptions | Promise<CampaignInstanceOptions>
|
|
25
|
+
readonly run: (context: CampaignRunContext<Flow>) => Result | Promise<Result>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DefinedCampaign<Flow = unknown, Result = unknown> extends CampaignDefinition<Flow, Result> {
|
|
29
|
+
readonly kind: "opencode-drive/campaign"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defineCampaign<Flow, Result>(campaign: CampaignDefinition<Flow, Result>): DefinedCampaign<Flow, Result> {
|
|
33
|
+
return { kind: "opencode-drive/campaign", ...campaign }
|
|
34
|
+
}
|