wip
This commit is contained in:
@@ -168,6 +168,7 @@ export function tui(input: {
|
||||
renderer?: CliRenderer
|
||||
mode?: "dark" | "light"
|
||||
onReady?: (ctx: { renderer: CliRenderer }) => void | Promise<void | { simulationMcpUrl?: string }>
|
||||
onStop?: (stop: () => Promise<void>) => void
|
||||
}) {
|
||||
// promise to prevent immediate exit
|
||||
// oxlint-disable-next-line no-async-promise-executor -- intentional: async executor used for sequential setup before resolve
|
||||
@@ -236,7 +237,7 @@ export function tui(input: {
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
<AppLifecycle onSnapshot={input.onSnapshot} onStop={input.onStop} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
@@ -265,6 +266,12 @@ export function tui(input: {
|
||||
})
|
||||
}
|
||||
|
||||
function AppLifecycle(props: { onSnapshot?: () => Promise<string[]>; onStop?: (stop: () => Promise<void>) => void }) {
|
||||
const exit = useExit()
|
||||
props.onStop?.(() => exit())
|
||||
return <App onSnapshot={props.onSnapshot} />
|
||||
}
|
||||
|
||||
function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
const tuiConfig = useTuiConfig()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { cmd } from "@/cli/cmd/cmd"
|
||||
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Rpc } from "@/util/rpc"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { withTimeout } from "@/util/timeout"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import {
|
||||
OPENCODE_PROCESS_ROLE,
|
||||
OPENCODE_RUN_ID,
|
||||
ensureRunID,
|
||||
sanitizedProcessEnv,
|
||||
} from "@opencode-ai/core/util/opencode-process"
|
||||
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
|
||||
import type { EventSource } from "./context/sdk"
|
||||
import type { SimulationMcpRuntimeState } from "./simulation-mcp"
|
||||
import type { rpc } from "./worker"
|
||||
import { SimulationDebugLog } from "../../../testing/simulation/debug-log"
|
||||
import { fileURLToPath } from "url"
|
||||
import { writeHeapSnapshot } from "v8"
|
||||
|
||||
type RpcClient = ReturnType<typeof Rpc.client<typeof rpc>>
|
||||
const simulatedDirectory = "/"
|
||||
const simulatedCwdEnv = "OPENCODE_SIMULATION_CWD"
|
||||
|
||||
function fakeCwd(directory: string) {
|
||||
process.env.PWD = directory
|
||||
Object.defineProperty(process, "cwd", {
|
||||
value: () => directory,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
interface Transport {
|
||||
readonly url: string
|
||||
readonly fetch: typeof fetch
|
||||
readonly events: EventSource
|
||||
}
|
||||
|
||||
interface RunningInstance {
|
||||
readonly client: RpcClient
|
||||
readonly done: Promise<void>
|
||||
readonly stopTui: () => Promise<void>
|
||||
readonly stopWorker: () => Promise<void>
|
||||
}
|
||||
|
||||
function createWorkerFetch(client: RpcClient): typeof fetch {
|
||||
const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const request = new Request(input, init)
|
||||
const body = request.body ? await request.text() : undefined
|
||||
SimulationDebugLog.write("simulate.fetch.start", { method: request.method, url: request.url })
|
||||
const result = await client.call("fetch", {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
body,
|
||||
})
|
||||
SimulationDebugLog.write("simulate.fetch.end", { method: request.method, url: request.url, status: result.status })
|
||||
return new Response(result.body, {
|
||||
status: result.status,
|
||||
headers: result.headers,
|
||||
})
|
||||
}
|
||||
return fn as typeof fetch
|
||||
}
|
||||
|
||||
function createEventSource(client: RpcClient, onSubscribe?: () => void): EventSource {
|
||||
return {
|
||||
subscribe: async (handler) => {
|
||||
// SimulationDebugLog.write("simulate.events.subscribe")
|
||||
onSubscribe?.()
|
||||
return client.on<GlobalEvent>("global.event", (e) => {
|
||||
// SimulationDebugLog.write("simulate.events.received", {
|
||||
// directory: e.directory,
|
||||
// workspace: e.workspace,
|
||||
// type: e.payload?.type,
|
||||
// sync: e.payload?.type === "sync",
|
||||
// })
|
||||
handler(e)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function target() {
|
||||
const workerPath = Reflect.get(globalThis, "OPENCODE_WORKER_PATH")
|
||||
if (typeof workerPath === "string") return workerPath
|
||||
const dist = new URL("./cli/cmd/tui/worker.js", import.meta.url)
|
||||
if (await Filesystem.exists(fileURLToPath(dist))) return dist
|
||||
return new URL("./worker.ts", import.meta.url)
|
||||
}
|
||||
|
||||
export const SimulateCommand = cmd({
|
||||
command: "simulate",
|
||||
describe: "start restartable simulated opencode tui",
|
||||
handler: async () => {
|
||||
SimulationDebugLog.reset()
|
||||
fakeCwd(simulatedDirectory)
|
||||
const file = await target()
|
||||
const cwd = simulatedDirectory
|
||||
const config = await TuiConfig.get()
|
||||
const simulationMcpMode = Flag.OPENCODE_SIMULATION ? "stdio" : "remote"
|
||||
|
||||
let currentInstance: RunningInstance | undefined
|
||||
let currentRuntime: SimulationMcpRuntimeState | undefined
|
||||
let restartRequested = false
|
||||
let restartWaiter:
|
||||
| {
|
||||
readonly promise: Promise<{ restarted: true }>
|
||||
readonly resolve: (value: { restarted: true }) => void
|
||||
readonly reject: (error: unknown) => void
|
||||
}
|
||||
| undefined
|
||||
|
||||
const error = (e: unknown) => {
|
||||
Log.Default.error("process error", { error: errorMessage(e) })
|
||||
}
|
||||
const reload = () => {
|
||||
currentInstance?.client.call("reload", undefined).catch((err) => {
|
||||
Log.Default.warn("worker reload failed", {
|
||||
error: errorMessage(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
process.on("uncaughtException", error)
|
||||
process.on("unhandledRejection", error)
|
||||
process.on("SIGUSR2", reload)
|
||||
|
||||
const simulationMcpModule = await import("./simulation-mcp")
|
||||
const simulationMcp = await simulationMcpModule.TuiSimulationMcp.createSimulationMcpServer({
|
||||
mode: simulationMcpMode,
|
||||
runtime: {
|
||||
current: () => currentRuntime,
|
||||
restart: async () => {
|
||||
if (restartWaiter) return restartWaiter.promise
|
||||
if (!currentInstance) throw new Error("Simulation TUI is not ready")
|
||||
restartRequested = true
|
||||
let resolve!: (value: { restarted: true }) => void
|
||||
let reject!: (error: unknown) => void
|
||||
const promise = new Promise<{ restarted: true }>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
restartWaiter = { promise, resolve, reject }
|
||||
currentInstance.stopTui().catch((err) => restartWaiter?.reject(err))
|
||||
return promise
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const stopWorker = async (client: RpcClient, worker: Worker) => {
|
||||
await withTimeout(client.call("shutdown", undefined), 5000).catch((err) => {
|
||||
Log.Default.warn("worker shutdown failed", {
|
||||
error: errorMessage(err),
|
||||
})
|
||||
})
|
||||
worker.terminate()
|
||||
}
|
||||
|
||||
const start = async (): Promise<RunningInstance> => {
|
||||
const worker = new Worker(file, {
|
||||
env: sanitizedProcessEnv({
|
||||
[OPENCODE_PROCESS_ROLE]: "worker",
|
||||
[OPENCODE_RUN_ID]: ensureRunID(),
|
||||
PWD: simulatedDirectory,
|
||||
[simulatedCwdEnv]: simulatedDirectory,
|
||||
}),
|
||||
})
|
||||
worker.onerror = (e) => {
|
||||
Log.Default.error("thread error", {
|
||||
message: e.message,
|
||||
filename: e.filename,
|
||||
lineno: e.lineno,
|
||||
colno: e.colno,
|
||||
error: e.error,
|
||||
})
|
||||
}
|
||||
|
||||
const client = Rpc.client<typeof rpc>(worker)
|
||||
let eventSubscribedResolve!: () => void
|
||||
const eventSubscribed = new Promise<void>((resolve) => {
|
||||
eventSubscribedResolve = resolve
|
||||
})
|
||||
const transport: Transport = {
|
||||
url: "http://opencode.internal",
|
||||
fetch: createWorkerFetch(client),
|
||||
events: createEventSource(client, eventSubscribedResolve),
|
||||
}
|
||||
|
||||
const { tui } = await import("./app")
|
||||
const simulationRenderer = Flag.OPENCODE_SIMULATION
|
||||
? await (await import("./simulation")).TuiSimulation.createSimulationRenderer()
|
||||
: undefined
|
||||
|
||||
let stopTui = async () => {}
|
||||
let stopReadyResolve!: () => void
|
||||
const stopReady = new Promise<void>((resolve) => {
|
||||
stopReadyResolve = resolve
|
||||
})
|
||||
let readyResolve!: () => void
|
||||
let readyReject!: (error: unknown) => void
|
||||
const ready = new Promise<void>((resolve, reject) => {
|
||||
readyResolve = resolve
|
||||
readyReject = reject
|
||||
})
|
||||
const done = tui({
|
||||
url: transport.url,
|
||||
async onSnapshot() {
|
||||
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
||||
const server = await client.call("snapshot", undefined)
|
||||
return [tui, server]
|
||||
},
|
||||
config,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch,
|
||||
events: transport.events,
|
||||
renderer: simulationRenderer?.renderer,
|
||||
mode: simulationRenderer ? "dark" : undefined,
|
||||
onStop: (stop) => {
|
||||
stopTui = stop
|
||||
stopReadyResolve()
|
||||
},
|
||||
onReady: async (ctx) => {
|
||||
try {
|
||||
currentRuntime = {
|
||||
harness: simulationRenderer
|
||||
? simulationMcpModule.TuiSimulationMcp.harnessFromSimulationRenderer(simulationRenderer)
|
||||
: simulationMcpModule.TuiSimulationMcp.harnessFromRenderer(ctx.renderer),
|
||||
controlUrl: transport.url,
|
||||
controlFetch: transport.fetch,
|
||||
}
|
||||
if (simulationRenderer) await simulationRenderer.renderOnce()
|
||||
readyResolve()
|
||||
return { simulationMcpUrl: simulationMcp.url }
|
||||
} catch (err) {
|
||||
readyReject(err)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
args: {},
|
||||
})
|
||||
|
||||
await Promise.all([ready, stopReady, eventSubscribed])
|
||||
return {
|
||||
client,
|
||||
done,
|
||||
stopTui: () => stopTui(),
|
||||
stopWorker: async () => {
|
||||
simulationRenderer?.destroy()
|
||||
await stopWorker(client, worker)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
currentInstance = await start()
|
||||
restartWaiter?.resolve({ restarted: true })
|
||||
restartWaiter = undefined
|
||||
restartRequested = false
|
||||
} catch (err) {
|
||||
restartWaiter?.reject(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
await currentInstance.done
|
||||
currentRuntime = undefined
|
||||
await currentInstance.stopWorker()
|
||||
currentInstance = undefined
|
||||
if (!restartRequested) break
|
||||
}
|
||||
} finally {
|
||||
currentRuntime = undefined
|
||||
await currentInstance?.stopTui().catch(() => {})
|
||||
await currentInstance?.stopWorker()
|
||||
await simulationMcp.stop()
|
||||
process.off("uncaughtException", error)
|
||||
process.off("unhandledRejection", error)
|
||||
process.off("SIGUSR2", reload)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export * as TuiSimulate from "./simulate"
|
||||
@@ -26,6 +26,22 @@ export interface SimulationMcpOptions {
|
||||
readonly controlFetch?: typeof fetch
|
||||
}
|
||||
|
||||
export interface SimulationMcpRuntimeState {
|
||||
readonly harness: SimulationMcpHarness
|
||||
readonly controlUrl: string
|
||||
readonly controlFetch?: typeof fetch
|
||||
}
|
||||
|
||||
export interface RestartableSimulationMcpOptions {
|
||||
readonly mode: SimulationMcpMode
|
||||
readonly runtime: {
|
||||
readonly current: () => SimulationMcpRuntimeState | undefined
|
||||
readonly restart: () => Promise<unknown>
|
||||
}
|
||||
}
|
||||
|
||||
type Options = SimulationMcpOptions | RestartableSimulationMcpOptions
|
||||
|
||||
export interface SimulationMcpServer {
|
||||
readonly mode: SimulationMcpMode
|
||||
readonly url?: string
|
||||
@@ -167,27 +183,39 @@ function toolResult(value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function state(options: SimulationMcpOptions) {
|
||||
function current(options: Options) {
|
||||
if ("runtime" in options) {
|
||||
const value = options.runtime.current()
|
||||
if (value) return value
|
||||
throw new Error("Simulation TUI is not ready")
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function state(options: Options) {
|
||||
const running = current(options)
|
||||
return {
|
||||
focused: {
|
||||
renderable: options.harness.renderer.currentFocusedRenderable?.num,
|
||||
editor: Boolean(options.harness.renderer.currentFocusedEditor),
|
||||
renderable: running.harness.renderer.currentFocusedRenderable?.num,
|
||||
editor: Boolean(running.harness.renderer.currentFocusedEditor),
|
||||
},
|
||||
elements: SimulationActions.elements(options.harness.renderer),
|
||||
actions: SimulationActions.actions(options.harness.renderer),
|
||||
elements: SimulationActions.elements(running.harness.renderer),
|
||||
actions: SimulationActions.actions(running.harness.renderer),
|
||||
}
|
||||
}
|
||||
|
||||
function snapshot(options: SimulationMcpOptions) {
|
||||
function snapshot(options: Options) {
|
||||
const running = current(options)
|
||||
return {
|
||||
screen: options.harness.screen(),
|
||||
spans: options.harness.spans(),
|
||||
screen: running.harness.screen(),
|
||||
spans: running.harness.spans(),
|
||||
ui: state(options),
|
||||
}
|
||||
}
|
||||
|
||||
async function control(options: SimulationMcpOptions, method: string, pathname: string, body?: unknown) {
|
||||
const response = await (options.controlFetch ?? fetch)(new URL(pathname, options.controlUrl), {
|
||||
async function control(options: Options, method: string, pathname: string, body?: unknown) {
|
||||
const running = current(options)
|
||||
const response = await (running.controlFetch ?? fetch)(new URL(pathname, running.controlUrl), {
|
||||
method,
|
||||
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
@@ -198,7 +226,7 @@ async function control(options: SimulationMcpOptions, method: string, pathname:
|
||||
throw new Error(typeof data?.error === "string" ? data.error : `Simulation control request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
function createServer(options: SimulationMcpOptions) {
|
||||
function createServer(options: Options) {
|
||||
const server = new McpServer(
|
||||
{ name: "opencode-simulation", version: InstallationVersion },
|
||||
{
|
||||
@@ -208,10 +236,10 @@ function createServer(options: SimulationMcpOptions) {
|
||||
)
|
||||
|
||||
server.registerResource("screen", "simulation://screen", { mimeType: "text/plain" }, () => ({
|
||||
contents: [{ uri: "simulation://screen", mimeType: "text/plain", text: options.harness.screen() }],
|
||||
contents: [{ uri: "simulation://screen", mimeType: "text/plain", text: current(options).harness.screen() }],
|
||||
}))
|
||||
server.registerResource("spans", "simulation://spans", { mimeType: "application/json" }, () => ({
|
||||
contents: [{ uri: "simulation://spans", mimeType: "application/json", text: JSON.stringify(options.harness.spans()) }],
|
||||
contents: [{ uri: "simulation://spans", mimeType: "application/json", text: JSON.stringify(current(options).harness.spans()) }],
|
||||
}))
|
||||
server.registerResource("ui-state", "simulation://ui-state", { mimeType: "application/json" }, () => ({
|
||||
contents: [{ uri: "simulation://ui-state", mimeType: "application/json", text: JSON.stringify(state(options)) }],
|
||||
@@ -239,16 +267,16 @@ function createServer(options: SimulationMcpOptions) {
|
||||
}))
|
||||
|
||||
server.registerTool("simulation_screen_get", { description: "Get the current TUI screen buffer." }, () =>
|
||||
toolResult({ screen: options.harness.screen() }),
|
||||
toolResult({ screen: current(options).harness.screen() }),
|
||||
)
|
||||
server.registerTool("simulation_spans_get", { description: "Get the current structured TUI spans." }, () =>
|
||||
toolResult(options.harness.spans()),
|
||||
toolResult(current(options).harness.spans()),
|
||||
)
|
||||
server.registerTool("simulation_ui_state_get", { description: "Get elements, focus state, and generated actions." }, () =>
|
||||
toolResult(state(options)),
|
||||
)
|
||||
server.registerTool("simulation_render_once", { description: "Force one render and return current state." }, async () => {
|
||||
await options.harness.renderOnce()
|
||||
await current(options).harness.renderOnce()
|
||||
return toolResult(snapshot(options))
|
||||
})
|
||||
server.registerTool(
|
||||
@@ -258,7 +286,7 @@ function createServer(options: SimulationMcpOptions) {
|
||||
inputSchema: z.object({ action: ActionSchema }),
|
||||
},
|
||||
async (input) => {
|
||||
await SimulationActions.execute(options.harness, input.action)
|
||||
await SimulationActions.execute(current(options).harness, input.action)
|
||||
return toolResult(snapshot(options))
|
||||
},
|
||||
)
|
||||
@@ -269,11 +297,17 @@ function createServer(options: SimulationMcpOptions) {
|
||||
inputSchema: z.object({ actions: z.array(ActionSchema).max(50) }),
|
||||
},
|
||||
async (input) => {
|
||||
for (const action of input.actions) await SimulationActions.execute(options.harness, action)
|
||||
for (const action of input.actions) await SimulationActions.execute(current(options).harness, action)
|
||||
return toolResult(snapshot(options))
|
||||
},
|
||||
)
|
||||
|
||||
if ("runtime" in options) {
|
||||
server.registerTool("simulation_restart", { description: "Restart the simulated TUI and backend while keeping MCP alive." }, async () =>
|
||||
toolResult(await options.runtime.restart()),
|
||||
)
|
||||
}
|
||||
|
||||
server.registerTool("simulation_control_reset", { description: "Reset backend simulation state." }, async () =>
|
||||
toolResult(await control(options, "POST", "/experimental/simulation/reset")),
|
||||
)
|
||||
@@ -308,7 +342,7 @@ function createServer(options: SimulationMcpOptions) {
|
||||
return server
|
||||
}
|
||||
|
||||
export async function createSimulationMcpServer(options: SimulationMcpOptions): Promise<SimulationMcpServer> {
|
||||
export async function createSimulationMcpServer(options: Options): Promise<SimulationMcpServer> {
|
||||
if (options.mode === "stdio") {
|
||||
const server = createServer(options)
|
||||
const transport = new StdioServerTransport()
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
sanitizedProcessEnv,
|
||||
} from "@opencode-ai/core/util/opencode-process"
|
||||
import { validateSession } from "./validate-session"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { TuiSimulation } from "./simulation"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_WORKER_PATH: string
|
||||
@@ -231,60 +229,26 @@ export const TuiThreadCommand = cmd({
|
||||
|
||||
try {
|
||||
const { tui } = await import("./app")
|
||||
const simulationRenderer = Flag.OPENCODE_SIMULATION ? await TuiSimulation.createSimulationRenderer() : undefined
|
||||
const simulationMcpMode = Flag.OPENCODE_SIMULATION
|
||||
? "stdio"
|
||||
: Flag.OPENCODE_SIMULATION_BACKEND
|
||||
? "remote"
|
||||
: undefined
|
||||
let simulationMcp: Awaited<ReturnType<typeof import("./simulation-mcp").TuiSimulationMcp.createSimulationMcpServer>> | undefined
|
||||
try {
|
||||
await tui({
|
||||
url: transport.url,
|
||||
async onSnapshot() {
|
||||
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
||||
const server = await client.call("snapshot", undefined)
|
||||
return [tui, server]
|
||||
},
|
||||
config,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch,
|
||||
events: transport.events,
|
||||
renderer: simulationRenderer?.renderer,
|
||||
mode: simulationRenderer ? "dark" : undefined,
|
||||
onReady: simulationMcpMode
|
||||
? async (ctx) => {
|
||||
const module = await import("./simulation-mcp")
|
||||
const harness = simulationRenderer
|
||||
? module.TuiSimulationMcp.harnessFromSimulationRenderer(simulationRenderer)
|
||||
: module.TuiSimulationMcp.harnessFromRenderer(ctx.renderer)
|
||||
if (simulationRenderer) await simulationRenderer.renderOnce()
|
||||
simulationMcp = await module.TuiSimulationMcp.createSimulationMcpServer({
|
||||
mode: simulationMcpMode,
|
||||
harness,
|
||||
controlUrl: transport.url,
|
||||
controlFetch: transport.fetch,
|
||||
})
|
||||
return { simulationMcpUrl: simulationMcp.url }
|
||||
}
|
||||
: simulationRenderer
|
||||
? async () => {
|
||||
await simulationRenderer.renderOnce()
|
||||
}
|
||||
: undefined,
|
||||
args: {
|
||||
continue: args.continue,
|
||||
sessionID: args.session,
|
||||
agent: args.agent,
|
||||
model: args.model,
|
||||
prompt,
|
||||
fork: args.fork,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
await simulationMcp?.stop()
|
||||
simulationRenderer?.destroy()
|
||||
}
|
||||
await tui({
|
||||
url: transport.url,
|
||||
async onSnapshot() {
|
||||
const tui = writeHeapSnapshot("tui.heapsnapshot")
|
||||
const server = await client.call("snapshot", undefined)
|
||||
return [tui, server]
|
||||
},
|
||||
config,
|
||||
directory: cwd,
|
||||
fetch: transport.fetch,
|
||||
events: transport.events,
|
||||
args: {
|
||||
continue: args.continue,
|
||||
sessionID: args.session,
|
||||
agent: args.agent,
|
||||
model: args.model,
|
||||
prompt,
|
||||
fork: args.fork,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
await stop()
|
||||
}
|
||||
|
||||
@@ -13,8 +13,18 @@ import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
|
||||
import { Effect } from "effect"
|
||||
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
|
||||
import { SimulationDebugLog } from "../../../testing/simulation/debug-log"
|
||||
import fs from "fs/promises"
|
||||
|
||||
ensureProcessMetadata("worker")
|
||||
if (process.env.OPENCODE_SIMULATION_CWD) {
|
||||
process.env.PWD = process.env.OPENCODE_SIMULATION_CWD
|
||||
Object.defineProperty(process, "cwd", {
|
||||
value: () => process.env.OPENCODE_SIMULATION_CWD!,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
void fs.writeFile("/tmp/opencode-http-errors.log", "")
|
||||
|
||||
await Log.init({
|
||||
print: process.argv.includes("--print-logs"),
|
||||
@@ -41,6 +51,12 @@ process.on("uncaughtException", (e) => {
|
||||
|
||||
// Subscribe to global events and forward them via RPC
|
||||
GlobalBus.on("event", (event) => {
|
||||
SimulationDebugLog.write("worker.global.event", {
|
||||
directory: event.directory,
|
||||
workspace: event.workspace,
|
||||
type: event.payload?.type,
|
||||
syncType: event.payload?.syncEvent?.type,
|
||||
})
|
||||
Rpc.emit("global.event", event)
|
||||
})
|
||||
|
||||
@@ -48,6 +64,7 @@ let server: Awaited<ReturnType<typeof Server.listen>> | undefined
|
||||
|
||||
export const rpc = {
|
||||
async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
|
||||
SimulationDebugLog.write("worker.fetch.start", { method: input.method, url: input.url })
|
||||
const headers = { ...input.headers }
|
||||
const auth = ServerAuth.header()
|
||||
if (auth && !headers["authorization"] && !headers["Authorization"]) {
|
||||
@@ -60,6 +77,7 @@ export const rpc = {
|
||||
})
|
||||
const response = await Server.Default().app.fetch(request)
|
||||
const body = await response.text()
|
||||
SimulationDebugLog.write("worker.fetch.end", { method: input.method, url: input.url, status: response.status })
|
||||
return {
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ExportCommand } from "./cli/cmd/export"
|
||||
import { ImportCommand } from "./cli/cmd/import"
|
||||
import { AttachCommand } from "./cli/cmd/tui/attach"
|
||||
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
|
||||
import { SimulateCommand } from "./cli/cmd/tui/simulate"
|
||||
import { AcpCommand } from "./cli/cmd/acp"
|
||||
import { EOL } from "os"
|
||||
import { WebCommand } from "./cli/cmd/web"
|
||||
@@ -40,6 +41,7 @@ import { Heap } from "./cli/heap"
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
|
||||
const processMetadata = ensureProcessMetadata("main")
|
||||
|
||||
@@ -67,7 +69,7 @@ function show(out: string) {
|
||||
process.stderr.write(out)
|
||||
}
|
||||
|
||||
const cli = yargs(args)
|
||||
const baseCli = yargs(args)
|
||||
.parserConfiguration({ "populate--": true })
|
||||
.scriptName("opencode")
|
||||
.wrap(100)
|
||||
@@ -178,6 +180,8 @@ const cli = yargs(args)
|
||||
.command(SessionCommand)
|
||||
.command(PluginCommand)
|
||||
.command(DbCommand)
|
||||
|
||||
const cli = (Flag.OPENCODE_SIMULATION || Flag.OPENCODE_SIMULATION_BACKEND ? baseCli.command(SimulateCommand) : baseCli)
|
||||
.fail((msg, err) => {
|
||||
if (
|
||||
msg?.startsWith("Unknown argument") ||
|
||||
|
||||
@@ -3,8 +3,22 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||
import { ConfigError } from "@/config/error"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
|
||||
import fs from "fs/promises"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
const errorLogPath = "/tmp/opencode-http-errors.log"
|
||||
|
||||
function writeHttpError(cause: Cause.Cause<unknown>, error: unknown) {
|
||||
void fs.appendFile(
|
||||
errorLogPath,
|
||||
JSON.stringify({
|
||||
time: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
cause: Cause.pretty(cause),
|
||||
}) + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
// Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s.
|
||||
export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { EffectBridge } from "@/effect/bridge"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import * as Option from "effect/Option"
|
||||
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
||||
import { SimulationDebugLog } from "../testing/simulation/debug-log"
|
||||
|
||||
const log = Log.create({ service: "llm" })
|
||||
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
|
||||
@@ -408,14 +409,25 @@ const live: Layer.Layer<
|
||||
Stream.scoped(
|
||||
Stream.unwrap(
|
||||
Effect.gen(function* () {
|
||||
SimulationDebugLog.write("llm.stream.start", {
|
||||
providerID: input.model.providerID,
|
||||
modelID: input.model.id,
|
||||
small: input.small,
|
||||
messages: input.messages.length,
|
||||
tools: Object.keys(input.tools).length,
|
||||
stack: new Error().stack,
|
||||
})
|
||||
const ctrl = yield* Effect.acquireRelease(
|
||||
Effect.sync(() => new AbortController()),
|
||||
(ctrl) => Effect.sync(() => ctrl.abort()),
|
||||
)
|
||||
|
||||
const result = yield* run({ ...input, abort: ctrl.signal })
|
||||
SimulationDebugLog.write("llm.stream.result")
|
||||
|
||||
return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e))))
|
||||
return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e)))).pipe(
|
||||
Stream.tap((event) => Effect.sync(() => SimulationDebugLog.write("llm.stream.event", { type: event }))),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { SimulationDebugLog } from "../testing/simulation/debug-log"
|
||||
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
const log = Log.create({ service: "session.processor" })
|
||||
@@ -212,6 +213,7 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) {
|
||||
SimulationDebugLog.write("processor.handleEvent", { type: value.type })
|
||||
switch (value.type) {
|
||||
case "start":
|
||||
yield* status.set(ctx.sessionID, { type: "busy" })
|
||||
@@ -719,15 +721,23 @@ export const layer = Layer.effect(
|
||||
})
|
||||
|
||||
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
|
||||
SimulationDebugLog.write("processor.process.start", {
|
||||
providerID: streamInput.model.providerID,
|
||||
modelID: streamInput.model.id,
|
||||
})
|
||||
slog.info("process")
|
||||
ctx.needsCompaction = false
|
||||
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
|
||||
|
||||
SimulationDebugLog.write("JWL processing")
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
yield* Effect.gen(function* () {
|
||||
SimulationDebugLog.write("JWL inside")
|
||||
ctx.currentText = undefined
|
||||
ctx.reasoningMap = {}
|
||||
const stream = llm.stream(streamInput)
|
||||
SimulationDebugLog.write("processor.stream.created")
|
||||
|
||||
yield* stream.pipe(
|
||||
Stream.tap((event) => handleEvent(event)),
|
||||
|
||||
@@ -38,6 +38,7 @@ import { Tool } from "@/tool/tool"
|
||||
import { Permission } from "@/permission"
|
||||
import { SessionStatus } from "./status"
|
||||
import { LLM } from "./llm"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import { Shell } from "@/shell/shell"
|
||||
import { ShellID } from "@/tool/shell/id"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
@@ -1812,7 +1813,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
|
||||
sys.skills(agent),
|
||||
sys.environment(model),
|
||||
instruction.system().pipe(Effect.orDie),
|
||||
instruction.system().pipe(
|
||||
Effect.tapError((error) =>
|
||||
bus.publish(Session.Event.Error, {
|
||||
sessionID,
|
||||
error: new NamedError.Unknown({ message: errorMessage(error) }, { cause: error }).toObject(),
|
||||
}),
|
||||
),
|
||||
Effect.orDie,
|
||||
),
|
||||
MessageV2.toModelMessagesEffect(msgs, model),
|
||||
])
|
||||
const system = [...env, ...instructions, ...(skills ? [skills] : [])]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
james@james-6.local.82438:1777694013
|
||||
@@ -0,0 +1,14 @@
|
||||
import fs from "fs/promises"
|
||||
|
||||
const file = "/tmp/opencode-simulation-stream.log"
|
||||
|
||||
export function reset() {
|
||||
void fs.writeFile(file, "")
|
||||
}
|
||||
|
||||
export function write(event: string, data?: unknown) {
|
||||
const line = JSON.stringify({ time: new Date().toISOString(), event, data }) + "\n"
|
||||
void fs.appendFile(file, line)
|
||||
}
|
||||
|
||||
export * as SimulationDebugLog from "./debug-log"
|
||||
@@ -3,6 +3,7 @@ import { simulateReadableStream } from "ai"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { SimulationDebugLog } from "./debug-log"
|
||||
import { Simulation, type LLMScript } from "./service"
|
||||
|
||||
const providerID = ProviderID.make("simulation")
|
||||
@@ -81,6 +82,7 @@ function stream(script: LLMScript) {
|
||||
)
|
||||
}
|
||||
chunks.push({ type: "finish", finishReason: finishReason(script), usage: usage(script) })
|
||||
SimulationDebugLog.write("provider.stream.chunks", { chunks: chunks.map((chunk) => chunk.type) })
|
||||
|
||||
return simulateReadableStream({
|
||||
chunks,
|
||||
@@ -128,7 +130,9 @@ function language(simulation: Simulation.Interface): LanguageModelV3 {
|
||||
}
|
||||
},
|
||||
async doStream(_options: LanguageModelV3CallOptions) {
|
||||
SimulationDebugLog.write("provider.doStream.start")
|
||||
const script = await nextScript(simulation)
|
||||
SimulationDebugLog.write("provider.doStream.script", { steps: script.steps.map((step) => step.map((item) => item.type)) })
|
||||
return { stream: stream(script) }
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user