diff --git a/src/runtime/opencode.ts b/src/runtime/opencode.ts index e8f7dc1..067cdb2 100644 --- a/src/runtime/opencode.ts +++ b/src/runtime/opencode.ts @@ -64,6 +64,38 @@ export class OpencodeRuntimeAdapter { return requireData(response.data, "global.health"); } + async warmup(): Promise { + const client = await this.ensureClient(); + const healthResponse = await client.global.health(); + requireData(healthResponse.data, "global.health"); + + const sessionResponse = await client.session.create({ + title: "tjwater-agent-warmup", + }); + const session = requireData(sessionResponse.data, "session.create"); + + try { + const [provider, model] = config.OPENCODE_MODEL.split("/"); + if (!provider || !model) { + throw new Error( + `invalid OPENCODE_MODEL; expected provider/model, received ${config.OPENCODE_MODEL}`, + ); + } + const toolsResponse = await client.tool.list({ provider, model }); + requireData(toolsResponse.data, "tool.list"); + } finally { + await client.session.delete( + { sessionID: session.id }, + { throwOnError: true }, + ).catch((error) => { + logger.warn( + { err: error, sessionId: session.id }, + "failed to remove opencode warmup session", + ); + }); + } + } + async createSession(title?: string) { const client = await this.ensureClient(); const response = await client.session.create({ diff --git a/src/server.ts b/src/server.ts index a7a8bd6..b59cb0f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -488,23 +488,12 @@ const bootstrap = async () => { resultReferenceStore.initialize(), sessionTranscriptStore.initialize(), ]); - resultReferenceStore.startCleanupLoop(); }; -await bootstrap(); - -const server = app.listen(config.PORT, config.HOST, () => { - logger.info( - { host: config.HOST, port: config.PORT }, - "TJWaterAgent listening", - ); - void warmupOpencodeRuntime(); -}); - const warmupOpencodeRuntime = async () => { const startedAt = Date.now(); try { - await opencodeRuntime.ensureClient(); + await opencodeRuntime.warmup(); logger.info( { elapsedMs: Math.max(0, Date.now() - startedAt), @@ -521,9 +510,21 @@ const warmupOpencodeRuntime = async () => { }, "failed to warm up opencode runtime", ); + throw error; } }; +await bootstrap(); +await warmupOpencodeRuntime(); +resultReferenceStore.startCleanupLoop(); + +const server = app.listen(config.PORT, config.HOST, () => { + logger.info( + { host: config.HOST, port: config.PORT }, + "TJWaterAgent listening", + ); +}); + const shutdown = async () => { logger.info("shutting down TJWaterAgent"); server.close(); diff --git a/tests/runtime/opencode.test.ts b/tests/runtime/opencode.test.ts index 60e75a3..7fb4548 100644 --- a/tests/runtime/opencode.test.ts +++ b/tests/runtime/opencode.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { type OpencodeClient } from "@opencode-ai/sdk/v2"; +import { config } from "../../src/config.js"; import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; const createRuntimeAdapter = ( @@ -85,3 +86,47 @@ describe("OpencodeRuntimeAdapter.ensureClient", () => { expect(attempts).toBe(2); }); }); + +describe("OpencodeRuntimeAdapter.warmup", () => { + it("initializes the project session and model tools before reporting ready", async () => { + const calls: string[] = []; + const client = { + global: { + health: async () => { + calls.push("health"); + return { data: { healthy: true, version: "test" } }; + }, + }, + session: { + create: async () => { + calls.push("session.create"); + return { data: { id: "warmup-session" } }; + }, + delete: async ({ sessionID }: { sessionID: string }) => { + calls.push(`session.delete:${sessionID}`); + return { data: true }; + }, + }, + tool: { + list: async (model: { provider: string; model: string }) => { + calls.push(`tool.list:${model.provider}/${model.model}`); + return { data: [] }; + }, + }, + } as unknown as OpencodeClient; + const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { + clientPromise: null, + closeServer: null, + ensureClient: async () => client, + }) as OpencodeRuntimeAdapter; + + await runtime.warmup(); + + expect(calls).toEqual([ + "health", + "session.create", + `tool.list:${config.OPENCODE_MODEL}`, + "session.delete:warmup-session", + ]); + }); +});