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.
Files changed (45) hide show
  1. package/README.md +90 -0
  2. package/bin/opencode-drive +2 -0
  3. package/package.json +45 -0
  4. package/src/cli/commands.ts +175 -0
  5. package/src/cli/describe.ts +14 -0
  6. package/src/cli/driver-runner.ts +39 -0
  7. package/src/cli/driver.ts +28 -0
  8. package/src/cli/index.ts +158 -0
  9. package/src/cli/instance.ts +218 -0
  10. package/src/cli/parse.ts +24 -0
  11. package/src/cli/registry.ts +120 -0
  12. package/src/cli/send.ts +48 -0
  13. package/src/cli/start.ts +56 -0
  14. package/src/cli/types.ts +46 -0
  15. package/src/client/backend.ts +184 -0
  16. package/src/client/client.ts +252 -0
  17. package/src/client/index.ts +17 -0
  18. package/src/client/protocol.ts +186 -0
  19. package/src/experimental/campaign-api.ts +34 -0
  20. package/src/experimental/campaign.ts +144 -0
  21. package/src/experimental/cli-campaign.ts +179 -0
  22. package/src/experimental/drive.ts +37 -0
  23. package/src/experimental/driver.ts +41 -0
  24. package/src/experimental/flow-driver.ts +189 -0
  25. package/src/experimental/flows/generate.ts +278 -0
  26. package/src/experimental/flows/index.ts +6 -0
  27. package/src/experimental/flows/properties.ts +51 -0
  28. package/src/experimental/flows/types.ts +47 -0
  29. package/src/experimental/flows/weights.ts +198 -0
  30. package/src/experimental/generators/config.ts +240 -0
  31. package/src/experimental/generators/filesystem.ts +95 -0
  32. package/src/experimental/generators/generate.ts +32 -0
  33. package/src/experimental/generators/index.ts +10 -0
  34. package/src/experimental/generators/initial-state.ts +37 -0
  35. package/src/experimental/generators/random.ts +35 -0
  36. package/src/experimental/hello-driver.ts +42 -0
  37. package/src/experimental/index.ts +3 -0
  38. package/src/experimental/model/derive.ts +119 -0
  39. package/src/experimental/model/index.ts +2 -0
  40. package/src/experimental/model/model.ts +42 -0
  41. package/src/experimental/opencode-simulation.ts +1 -0
  42. package/src/experimental/reproduce-stale-running-visible.ts +60 -0
  43. package/src/experimental/reproduce-stale-running.ts +26 -0
  44. package/src/experimental/stale-running-driver.ts +74 -0
  45. 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
+ }