Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/local-agent-control.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/local-agent-control.test.ts && tsx src/local-agent-runtime-pool.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
63 changes: 54 additions & 9 deletions src/local-agent-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type LocalAgentRunInput,
type LocalAgentRunResult,
} from "./local-agent-runtime.js";
import type { HarnessDriver, HarnessRuntime } from "./local-agent-runtime-pool.js";

export interface LocalAgentAdapter {
readonly provider: LocalAgentProvider;
Expand Down Expand Up @@ -142,27 +143,71 @@ class OpencodeLocalAgentAdapter implements LocalAgentAdapter {
readonly provider = "opencode" as const;

async run(input: LocalAgentRunInput): Promise<LocalAgentRunResult> {
const { createOpencode } = await import("@opencode-ai/sdk/v2");
const { client, server } = await createOpencode();
const runtime = await createOpencodeHarnessDriver().createRuntime(input);
try {
const sessionId = input.providerSessionId ?? await createOpencodeSession(client, input);
const promptResult = await promptOpencodeSession(client, sessionId, input);
await waitForOpencodeSession(client, sessionId);
const messages = await readOpencodeMessages(client, sessionId);
return await runtime.run(input);
} finally {
await runtime.close();
}
}
}

class OpencodeHarnessRuntime implements HarnessRuntime {
private closed = false;
private failed = false;

constructor(
private readonly client: unknown,
private readonly closeServer: () => void,
) {}

async run(input: LocalAgentRunInput): Promise<LocalAgentRunResult> {
if (this.closed) throw new Error("OpenCode runtime is closed.");
try {
const sessionId = input.providerSessionId ?? await createOpencodeSession(this.client, input);
const promptResult = await promptOpencodeSession(this.client, sessionId, input);
await waitForOpencodeSession(this.client, sessionId);
const messages = await readOpencodeMessages(this.client, sessionId);
const finalResponse = requireFinalResponse(
"OpenCode",
extractOpenCodeFinalResponse(messages) || extractOpenCodeFinalResponse(promptResult),
);
return {
provider: this.provider,
provider: "opencode",
providerSessionId: sessionId,
finalResponse,
items: [promptResult, messages],
};
} finally {
server.close();
} catch (error) {
// The SDK does not expose the child server's exit state. Treat a failed
// turn as poisoning this pooled runtime so the next turn gets a fresh
// server instead of repeatedly reusing a dead transport.
this.failed = true;
throw error;
}
}

isUsable(): boolean {
return !this.closed && !this.failed;
}
Comment thread
Waishnav marked this conversation as resolved.

async close(): Promise<void> {
if (this.closed) return;
this.closed = true;
this.closeServer();
}
}

export function createOpencodeHarnessDriver(): HarnessDriver {
return {
provider: "opencode",
runtimeKey: () => "default",
createRuntime: async () => {
const { createOpencode } = await import("@opencode-ai/sdk/v2");
const { client, server } = await createOpencode();
return new OpencodeHarnessRuntime(client, () => server.close());
},
};
}

class AcpLocalAgentAdapter implements LocalAgentAdapter {
Expand Down
11 changes: 5 additions & 6 deletions src/local-agent-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ServerConfig } from "./config.js";
import { runLocalAgentProvider } from "./local-agent-adapters.js";
import { assertLocalAgentProviderAvailable } from "./local-agent-availability.js";
import {
isLocalAgentProvider,
Expand All @@ -15,7 +14,7 @@ import {
type LocalAgentRecord,
type LocalAgentStore,
} from "./local-agent-store.js";
import type { LocalAgentRunResult } from "./local-agent-runtime.js";
import type { LocalAgentRunInput, LocalAgentRunResult } from "./local-agent-runtime.js";
import { assertAllowedPath } from "./roots.js";

export interface LocalAgentRunCommand {
Expand All @@ -29,12 +28,12 @@ export interface LocalAgentRunCommand {

type RunProvider = (
provider: LocalAgentProfile["provider"],
input: Parameters<typeof runLocalAgentProvider>[1],
input: LocalAgentRunInput,
) => Promise<LocalAgentRunResult>;

interface LocalAgentManagerOptions {
store?: LocalAgentStore;
runProvider?: RunProvider;
runProvider: RunProvider;
assertProviderAvailable?: typeof assertLocalAgentProviderAvailable;
}

Expand Down Expand Up @@ -64,10 +63,10 @@ export class LocalAgentManager {

constructor(
private readonly config: ServerConfig,
options: LocalAgentManagerOptions = {},
options: LocalAgentManagerOptions,
) {
this.store = options.store ?? createLocalAgentStore(config);
this.runProvider = options.runProvider ?? runLocalAgentProvider;
this.runProvider = options.runProvider;
this.assertProviderAvailable = options.assertProviderAvailable ?? assertLocalAgentProviderAvailable;
}

Expand Down
126 changes: 126 additions & 0 deletions src/local-agent-runtime-pool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import assert from "node:assert/strict";
import { HarnessRuntimePool, type HarnessDriver, type HarnessRuntime } from "./local-agent-runtime-pool.js";
import { LocalAgentRuntimeRegistry } from "./local-agent-runtime-registry.js";
import type { LocalAgentRunInput, LocalAgentRunResult } from "./local-agent-runtime.js";

class FakeRuntime implements HarnessRuntime {
readonly prompts: string[] = [];
closed = false;
private sessionCount = 0;

constructor(private readonly name: string) {}

async run(input: LocalAgentRunInput): Promise<LocalAgentRunResult> {
assert.equal(this.closed, false);
this.prompts.push(input.prompt);
const providerSessionId = input.providerSessionId ?? `${this.name}-session-${++this.sessionCount}`;
return {
provider: "opencode",
providerSessionId,
finalResponse: `${this.name}:${input.prompt}`,
items: [],
};
}

isUsable(): boolean {
return !this.closed;
}

async close(): Promise<void> {
this.closed = true;
}
}

class FailingRuntime extends FakeRuntime {
failed = false;

override async run(input: LocalAgentRunInput): Promise<LocalAgentRunResult> {
if (input.prompt === "fail") {
this.failed = true;
throw new Error("runtime transport failed");
}
return super.run(input);
}

override isUsable(): boolean {
return !this.closed && !this.failed;
}
}

let now = 0;
let created = 0;
const runtimes: FakeRuntime[] = [];
const driver: HarnessDriver = {
provider: "opencode",
runtimeKey: () => "shared",
async createRuntime() {
created += 1;
const runtime = new FakeRuntime(`runtime-${created}`);
runtimes.push(runtime);
return runtime;
},
};
const pool = new HarnessRuntimePool({ idleMs: 10, reapIntervalMs: 0, now: () => now });
const registry = new LocalAgentRuntimeRegistry({ pool, opencodeDriver: driver });

try {
const first = await registry.run("opencode", input("/tmp/a", "first"));
const second = await registry.run("opencode", input("/tmp/b", "second", first.providerSessionId ?? undefined));

assert.equal(created, 1, "compatible OpenCode runs should share one live runtime");
assert.equal(first.providerSessionId, "runtime-1-session-1");
assert.equal(second.providerSessionId, "runtime-1-session-1");
assert.deepEqual(runtimes[0]?.prompts, ["first", "second"]);

now = 9;
await pool.reapIdle();
assert.equal(runtimes[0]?.closed, false);

now = 10;
await pool.reapIdle();
assert.equal(runtimes[0]?.closed, true, "idle runtimes should be reclaimable without touching durable session ids");

await registry.run("opencode", input("/tmp/a", "third", second.providerSessionId ?? undefined));
assert.equal(created, 2, "a later run should recreate an evicted runtime");
assert.equal(runtimes[1]?.prompts[0], "third");
} finally {
await registry.shutdown();
}

{
let failureCreates = 0;
const failedRuntimes: FailingRuntime[] = [];
const failureDriver: HarnessDriver = {
provider: "opencode",
runtimeKey: () => "failure-recovery",
async createRuntime() {
const runtime = new FailingRuntime(`failure-runtime-${++failureCreates}`);
failedRuntimes.push(runtime);
return runtime;
},
};
const failurePool = new HarnessRuntimePool({ reapIntervalMs: 0 });

try {
await assert.rejects(
failurePool.run(failureDriver, input("/tmp/a", "fail")),
/runtime transport failed/,
);
assert.equal(failedRuntimes[0]?.closed, true, "an unusable failed runtime should be evicted immediately");

const recovered = await failurePool.run(failureDriver, input("/tmp/a", "recover"));
assert.equal(failureCreates, 2, "the next turn should create a fresh runtime after a transport failure");
assert.equal(recovered.finalResponse, "failure-runtime-2:recover");
} finally {
await failurePool.shutdown();
}
}

function input(workspace: string, prompt: string, providerSessionId?: string): LocalAgentRunInput {
return {
workspace,
prompt,
providerSessionId,
writeMode: "allowed",
};
}
Loading
Loading