This commit is contained in:
James Long
2026-05-17 14:50:41 -04:00
parent 02b7367b4e
commit 13ea36f575
13 changed files with 455 additions and 80 deletions
+8 -1
View File
@@ -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()
+20 -56
View File
@@ -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()),
+5 -1
View File
@@ -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) =>
+13 -1
View File
@@ -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)),
+10 -1
View File
@@ -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) }
},
}