Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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.

221 changes: 49 additions & 172 deletions src/local-agent-opencode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,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 +164,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 +189,13 @@ 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 } }; } },
},
} 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: {},
},
};
},
},
},
} 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",
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.ok(
longSessionRequests.some((request) => request.cursor === "long-session-next"),
"OpenCode should follow the continuation cursor",
);
await longSessionPool.close();

assert.equal(opencodeAgentFor("read_only"), "devspace_read_only");
assert.equal(opencodeAgentFor("full_access"), "devspace_full_access");
Expand All @@ -356,21 +219,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 +263,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