Skip to content
Merged
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 @@ -57,7 +57,7 @@
"@modelcontextprotocol/node": "^2.0.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/server": "^2.0.0",
"@opencode-ai/sdk": "^1.17.13",
"@opencode-ai/sdk": "1.17.13",
"@pierre/diffs": "^1.3.6",
"better-result": "^2.10.0",
"better-sqlite3": "^12.10.0",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

234 changes: 69 additions & 165 deletions src/local-agent-opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join } from "node:path";
import {
opencodeAgentConfig,
OpencodeLocalAgentDriver,
OpencodeRuntime,
opencodeAgentFor,
opencodePermissionFor,
type OpencodeClientLike,
Expand All @@ -15,41 +16,30 @@ import { LocalAgentRuntimePool } from "./local-agent-runtime-pool.js";
let sessionNumber = 0;
const createInputs: unknown[] = [];
const promptInputs: unknown[] = [];
const switchInputs: unknown[] = [];
const agentInputs: unknown[] = [];
let healthAvailable = true;
const client = {
v2: {
session: {
global: {
async health() {
if (!healthAvailable) throw new Error("server unavailable");
return { data: { healthy: true } };
},
},
session: {
async create(input: unknown) {
createInputs.push(input);
sessionNumber += 1;
return { data: { data: { id: `session_${sessionNumber}` } } };
return { data: { id: `session_${sessionNumber}` } };
},
async prompt(input: unknown) {
promptInputs.push(input);
return input;
},
async wait() {},
async messages(input: unknown) {
const sessionId = (input as { sessionID: string }).sessionID;
return {
data: { data: [{
data: {
info: { role: "assistant" },
parts: [{ type: "text", text: `response:${sessionId}` }],
}] },
},
};
},
async get() {
return { data: { data: { model: { providerID: "anthropic", id: "sonnet" } } } };
},
async switchAgent(input: unknown) { agentInputs.push(input); },
async switchModel(input: unknown) { switchInputs.push(input); },
},
health: { async get() {
if (!healthAvailable) throw new Error("server unavailable");
return { data: { healthy: true } };
} },
},
} as unknown as OpencodeClientLike;
let factoryCalls = 0;
Expand Down Expand Up @@ -175,13 +165,15 @@ assert.equal(firstRecord.providerSessionId, "session_1");
assert.equal(secondRecord.providerSessionId, "session_2");
assert.equal(secondRecord.finalResponse, "response:session_2");
assert.deepEqual(createInputs[0], {
location: { directory: "/tmp/project" },
agent: "devspace_allowed",
model: { providerID: "anthropic", id: "sonnet", variant: "high" },
directory: "/tmp/project",
});
assert.deepEqual(promptInputs[0], {
sessionID: "session_1",
prompt: { text: "first" },
directory: "/tmp/project",
parts: [{ type: "text", text: "first" }],
agent: "devspace_allowed",
model: { providerID: "anthropic", modelID: "sonnet" },
variant: "high",
});

let callbackSessionId: string | undefined;
Expand All @@ -198,141 +190,39 @@ await pool.run(driver, {
onSessionId: (id) => { callbackSessionId = id; },
});
assert.equal(callbackSessionId, firstRecord.providerSessionId);
assert.deepEqual(switchInputs[0], {
assert.deepEqual(promptInputs[2], {
sessionID: "session_1",
model: { providerID: "anthropic", id: "sonnet", variant: "low" },
directory: "/tmp/project",
parts: [{ type: "text", text: "effort override" }],
agent: "devspace_allowed",
variant: "low",
});
assert.deepEqual(agentInputs[0], { sessionID: "session_1", agent: "devspace_allowed" });

let readinessActiveCalls = 0;
let readinessWaitCalls = 0;
const readinessRaceClient = {
v2: {
session: {
async create() {
return { data: { data: { id: "session_readiness" } } };
},
async switchAgent() {},
async prompt() {
return { data: { data: { id: "prompt_readiness" } } };
},
async wait() {
readinessWaitCalls += 1;
throw new Error("Session wait is not available yet");
},
async active() {
readinessActiveCalls += 1;
return {
data: {
data: readinessActiveCalls < 3
? { session_readiness: { type: "running" } }
: {},
},
};
},
async messages() {
const data = readinessActiveCalls >= 3
? [
{ type: "user", id: "prompt_readiness" },
{
type: "assistant",
id: "assistant_readiness",
time: { created: 1, completed: 2 },
finish: "stop",
content: [{ type: "text", id: "part_readiness", text: "ready response" }],
},
]
: [{ type: "user", id: "prompt_readiness" }];
return { data: { data } };
},
},
health: { async get() { return { data: { healthy: true } }; } },
const timeoutClient = {
global: {
async health() { return { data: { healthy: true } }; },
},
} as unknown as OpencodeClientLike;
const readinessPool = new LocalAgentRuntimePool();
const readinessDriver = new OpencodeLocalAgentDriver(async () => ({
client: readinessRaceClient,
server: { close: () => undefined },
}));
const readinessResult = await readinessPool.run(readinessDriver, {
agentId: "agt_readiness",
provider: "opencode",
workspaceRoot: "/tmp/project",
}, { prompt: "readiness", workspaceRoot: "/tmp/project" });
assert.equal(readinessResult.isOk(), true, "OpenCode should wait for the active session to finish");
if (readinessResult.isOk()) {
assert.equal(readinessResult.value.finalResponse, "ready response");
}
assert.equal(readinessWaitCalls, 0, "OpenCode should not rely on the unavailable wait endpoint");
await readinessPool.close();

const longSessionRequests: Array<{ cursor?: string; order?: string }> = [];
const longSessionClient = {
v2: {
session: {
async create() {
return { data: { data: { id: "session_long" } } };
},
async switchAgent() {},
async prompt() {
return { data: { data: { id: "prompt_long" } } };
},
async active() {
return { data: { data: {} } };
},
async messages(input: unknown) {
const request = input as { cursor?: string; order?: string };
longSessionRequests.push({ cursor: request.cursor, order: request.order });
if (!request.cursor) {
return {
data: {
data: Array.from({ length: 100 }, (_, index) => ({
type: "assistant",
id: `old-assistant-${index}`,
finish: "stop",
content: [{ type: "text", text: `old response ${index}` }],
})),
cursor: { next: "long-session-next" },
},
};
}
return {
data: {
data: [
{ type: "user", id: "prompt_long" },
{
type: "assistant",
id: "assistant_long",
finish: "stop",
content: [{ type: "text", text: "long response" }],
},
],
cursor: {},
},
};
},
session: {
async create() { return { data: { id: "session_timeout" } }; },
async prompt(_input: unknown, options?: { signal?: AbortSignal }) {
return new Promise<never>((_resolve, reject) => {
options?.signal?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
});
},
},
} as unknown as OpencodeClientLike;
const longSessionPool = new LocalAgentRuntimePool();
const longSessionDriver = new OpencodeLocalAgentDriver(async () => ({
client: longSessionClient,
server: { close: () => undefined },
}));
const longSessionResult = await longSessionPool.run(longSessionDriver, {
agentId: "agt_long_session",
provider: "opencode",
const timeoutRuntime = new OpencodeRuntime(timeoutClient, { close: () => undefined }, 5);
const timedOutPrompt = await timeoutRuntime.run({
prompt: "never finishes",
workspaceRoot: "/tmp/project",
}, { prompt: "long session", workspaceRoot: "/tmp/project" });
assert.equal(longSessionResult.isOk(), true, "OpenCode should find completions past the first message page");
if (longSessionResult.isOk()) {
assert.equal(longSessionResult.value.finalResponse, "long response");
});
assert.equal(timedOutPrompt.isErr(), true);
if (timedOutPrompt.isErr()) {
assert.equal(timedOutPrompt.error.code, "PROVIDER_PROTOCOL_ERROR");
assert.equal(timedOutPrompt.error.retryable, true);
assert.match(timedOutPrompt.error.message, /provider timeout/);
}
assert.ok(
longSessionRequests.some((request) => request.cursor === "long-session-next"),
"OpenCode should follow the continuation cursor",
);
await longSessionPool.close();
await timeoutRuntime.close();

assert.equal(opencodeAgentFor("read_only"), "devspace_read_only");
assert.equal(opencodeAgentFor("full_access"), "devspace_full_access");
Expand All @@ -356,21 +246,34 @@ for (const writeMode of ["read_only", "allowed", "full_access"] as const) {

let promptFailureCount = 0;
const applicationErrorClient = {
v2: {
session: {
async create() { return { data: { data: { id: "session_app_error" } } }; },
async switchAgent() {},
async prompt() {
promptFailureCount += 1;
if (promptFailureCount === 1) throw new Error("server rejected invalid input");
return {};
},
async wait() {},
async messages() {
return { data: { data: [{ info: { role: "assistant" }, parts: [{ type: "text", text: "ok" }] }] } };
},
global: {
async health() { return { data: { healthy: true } }; },
},
session: {
async create() { return { data: { id: "session_app_error" } }; },
async prompt() {
promptFailureCount += 1;
if (promptFailureCount === 1) {
return {
data: {
info: {
role: "assistant",
error: {
name: "ProviderAuthError",
data: { providerID: "example", message: "Provider credentials are unavailable." },
},
},
parts: [],
},
};
}
return {
data: {
info: { role: "assistant" },
parts: [{ type: "text", text: "ok" }],
},
};
},
health: { async get() { return { data: { healthy: true } }; } },
},
} as unknown as OpencodeClientLike;
const applicationErrorPool = new LocalAgentRuntimePool();
Expand All @@ -387,6 +290,7 @@ assert.equal(applicationFailure.isErr(), true);
if (applicationFailure.isErr()) {
assert.equal(applicationFailure.error.code, "PROVIDER_EXECUTION_ERROR");
assert.equal(applicationFailure.error.retryable, false);
assert.equal(applicationFailure.error.message, "Provider credentials are unavailable.");
}
assert.equal(applicationErrorPool.size, 1, "ordinary provider errors must not evict a healthy server runtime");
const recoveredApplicationTurn = await applicationErrorPool.run(applicationErrorDriver, {
Expand Down
Loading
Loading