Official
opencode
OpenCode agent provider: runs opencode serve and talks to it over HTTP and server-sent events.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/opencode@0.2.0
Permissions in 0.2.0
Files
agent.test.ts51.4 KB
// Scripted scenarios for the opencode conversion: the agent against a
// scripted server behind `api.net.fetch`, with the same assertions as the
// JavaScript suite.
import { test } from "node:test";
import assert from "node:assert/strict";
import { fieldAnswer } from "./agent.ts";
import { defaultInstance } from "./instances.ts";
import { questionId } from "./map.ts";
import { permissionMode } from "../sdk/agent.ts";
import { fakeApi, settle } from "../sdk/testing.ts";
import type { FakeOptions, HostScript } from "../sdk/testing.ts";
import { BASE, FakeOpenCode, apiFor, collect, sse, testAgent } from "./testing.test.ts";
import type { Event } from "./types.ts";
import { obj } from "./map.ts";
type Keys<T> = T extends unknown ? keyof T : never;
type Field<T, K> = T extends unknown ? (K extends keyof T ? T[K] : never) : never;
type ObservedEvent = Event & { [K in Keys<Event>]?: Field<Event, K> };
/// An agent on the fake server, as a server the user runs (so nothing is
/// started), with its events kept in `emitted`.
function agentOn(
server: FakeOpenCode,
{
onHost = () => ({}),
instance = {},
onFetch,
}: {
onHost?: HostScript;
instance?: Partial<import("./instances.ts").Instance>;
onFetch?: FakeOptions["onFetch"];
} = {},
) {
const api = onFetch ? fakeApi({ onHost, onFetch }) : apiFor(server, { onHost });
const emitted: ObservedEvent[] = [];
const agent = testAgent(api, {}, { ...defaultInstance(), serverUrl: BASE, ...instance });
agent.emit = (event) => {
emitted.push(event);
};
return { agent, api, emitted };
}
const tasks = (events: ObservedEvent[]) => events.filter((event) => event.event === "task");
function present<T>(value: T | null | undefined): T {
assert.ok(value != null);
return value;
}
test("a dropped event stream reconnects and withdraws what the server forgot", async () => {
const asked = {
type: "permission.asked",
properties: {
id: "per_1",
sessionID: "ses_1",
permission: "bash",
patterns: ["echo hi"],
metadata: { command: "echo hi" },
always: [],
},
};
// First connection: one approval, then the stream drops.
const server = new FakeOpenCode({ events: [sse(asked), ""] });
const { agent, emitted } = agentOn(server);
// The run is there before the stream opens, or nothing would fail.
agent.runs.set("ses_1", { id: "run_1", error: null });
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const seen = await collect(emitted, (event) => event.event === "approval_resolved", 5000);
assert.equal(seen[0]?.event, "approval", `the approval first: ${JSON.stringify(seen)}`);
assert.equal(seen[0]?.id, "per_1");
assert.equal(seen[0]?.runId, "run_1");
assert.ok(
seen.some((event) => event.event === "approval_resolved" && event.id === "per_1"),
"the approval the server forgot was withdrawn",
);
assert.ok(!seen.some((event) => event.event === "run_finished"), "a reconnect must not end the run");
assert.ok(server.connections >= 2, "the stream reconnected");
assert.ok(agent.runs.has("ses_1"));
await agent.shutdown();
});
test("a run whose idle event was lost while the stream was down ends after the reconnect", async () => {
// The stream drops; meanwhile ses_1's turn ended, ses_2 still works.
const server = new FakeOpenCode({ events: ["", ""], status: { ses_2: { type: "busy" } } });
const { agent, emitted } = agentOn(server);
agent.runs.set("ses_1", { id: "run_1", error: null, startedAt: 0 });
agent.runs.set("ses_2", { id: "run_2", error: null, startedAt: 0 });
await agent.resumeSession({ sessionId: "ses_2", workspace: "/w", options: {} });
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const seen = await collect(emitted, (event) => event.event === "run_finished", 5000);
const finished = seen.find((event) => event.event === "run_finished");
assert.deepEqual(
[finished?.sessionId, finished?.runId, finished?.outcome],
["ses_1", "run_1", { status: "completed" }],
);
assert.ok(agent.runs.has("ses_2"), "a busy session keeps its run");
const call = server.calls.find((call) => call.path === "/session/status");
assert.equal(call?.query.directory, "/w");
await agent.shutdown();
});
test("subagent events are reported against the chat that started them", async () => {
const events = [
{
type: "session.created",
properties: {
sessionID: "ses_child",
info: { id: "ses_child", parentID: "ses_1", title: "Search the tests", agent: "explore" },
},
},
{
type: "message.part.delta",
properties: { sessionID: "ses_child", messageID: "msg_c", partID: "prt_c", field: "text", delta: "found it" },
},
{ type: "session.idle", properties: { sessionID: "ses_child" } },
];
const server = new FakeOpenCode({ events: [events.map(sse).join("")] });
const { agent, emitted } = agentOn(server);
agent.runs.set("ses_1", { id: "run_1", error: null });
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const seen = await collect(emitted, (event) => event.event === "task" && event.status === "completed");
const started = tasks(seen).find((task) => task.status === "running");
assert.equal(present(started).sessionId, "ses_1", "the task belongs to the chat");
assert.equal(present(started).id, "ses_child");
assert.equal(present(started).title, "Search the tests");
assert.equal(present(started).name, "explore");
const text = seen.find((event) => event.event === "text_delta");
assert.equal(present(text).sessionId, "ses_1");
assert.equal(present(text).taskId, "ses_child", "the text is the subagent's");
assert.equal(present(text).runId, "run_1");
// The subagent finishing is not the chat finishing.
assert.ok(!seen.some((event) => event.event === "run_finished"));
assert.ok(agent.runs.has("ses_1"));
await agent.shutdown();
});
test("the context window follows the model that ran", async () => {
const ran = {
type: "message.updated",
properties: {
info: { id: "msg_a", role: "assistant", sessionID: "ses_1", providerID: "opencode", modelID: "big-pickle" },
},
};
const step = {
type: "message.part.updated",
properties: {
sessionID: "ses_1",
part: {
type: "step-finish",
id: "prt_s",
messageID: "msg_a",
sessionID: "ses_1",
tokens: { total: 4000, input: 3900, output: 100 },
},
},
};
const server = new FakeOpenCode({ events: [sse(ran) + sse(step)] });
const { agent, emitted } = agentOn(server);
agent.catalogs.set("/w", {
providers: [
{
id: "opencode",
name: "OpenCode",
models: { "big-pickle": { id: "big-pickle", name: "Big Pickle", limit: { context: 200000 } } },
},
],
});
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const seen = await collect(emitted, (event) => event.event === "usage");
const usage = present(seen.at(-1));
assert.equal(usage.event, "usage");
assert.equal(usage.usedTokens, 4000);
assert.equal(usage.contextWindow, 200000);
await agent.shutdown();
});
test("a subagent is linked, followed and settled live", async () => {
const events = [
{
type: "session.created",
properties: {
sessionID: "ses_child",
info: { id: "ses_child", parentID: "ses_1", title: "Find the parser (@explore subagent)", agent: "explore" },
},
},
{
type: "message.part.updated",
properties: {
sessionID: "ses_1",
part: {
type: "tool",
id: "prt_task",
messageID: "msg_a",
sessionID: "ses_1",
callID: "call_t",
tool: "task",
state: {
status: "running",
title: "Find the parser",
input: { description: "Find the parser", prompt: "Look for the parser.", subagent_type: "explore" },
metadata: { sessionId: "ses_child", model: { providerID: "opencode", modelID: "big-pickle" } },
time: { start: 1700000000000 },
},
},
},
},
{
type: "message.part.updated",
properties: {
sessionID: "ses_child",
part: {
type: "tool",
id: "prt_grep",
messageID: "msg_c",
sessionID: "ses_child",
callID: "c1",
tool: "grep",
state: { status: "running", input: { pattern: "parse" }, title: "parse" },
},
},
},
{
type: "message.part.updated",
properties: {
sessionID: "ses_child",
part: {
type: "step-finish",
id: "prt_step",
messageID: "msg_c",
sessionID: "ses_child",
tokens: { total: 4000, input: 3900, output: 100 },
},
},
},
{
type: "session.created",
properties: {
sessionID: "ses_grand",
info: { id: "ses_grand", parentID: "ses_child", title: "Read it (@general subagent)", agent: "general" },
},
},
{
type: "message.part.delta",
properties: {
sessionID: "ses_child",
messageID: "msg_c",
partID: "prt_txt",
field: "text",
delta: "It is in src/parse.rs.",
},
},
{ type: "session.idle", properties: { sessionID: "ses_child" } },
];
const server = new FakeOpenCode({ events: [events.map(sse).join("")] });
const { agent, emitted } = agentOn(server);
agent.runs.set("ses_1", { id: "run_1", error: null });
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const seen = await collect(
emitted,
(event) => event.event === "task" && event.id === "ses_child" && event.status === "completed",
);
const all = tasks(seen);
assert.ok(
all.every((task) => task.title !== "task"),
"a tool name is never a title",
);
const linked = all.find((task) => task.id === "ses_child" && task.toolCallId);
assert.equal(present(linked).toolCallId, "prt_task");
assert.equal(present(linked).title, "Find the parser");
assert.equal(present(linked).prompt, "Look for the parser.");
assert.equal(present(linked).model, "big-pickle");
assert.equal(present(linked).name, "explore");
assert.ok(
all.some((task) => task.id === "ses_child" && task.activity === "parse"),
"the running tool showed as activity",
);
assert.ok(!seen.some((event) => event.event === "usage"), "a subagent's usage never reaches the chat's meter");
const grand = all.find((task) => task.id === "ses_grand");
assert.equal(present(grand).parentTaskId, "ses_child");
assert.equal(present(grand).title, "Read it");
const done = present(all.at(-1));
assert.equal(done.status, "completed");
assert.equal(done.summary, "It is in src/parse.rs.", "the answer is its last text");
assert.equal(done.usage?.usedTokens, 4000);
assert.equal(done.toolUses, 1);
assert.ok(done.endedAt);
assert.equal(done.activity, undefined);
const tool = seen.find((event) => event.event === "tool_call_started" && event.id === "prt_grep");
assert.equal(present(tool).taskId, "ses_child");
assert.equal(present(tool).sessionId, "ses_1");
await agent.shutdown();
});
test("one subagent can be stopped while the chat's run goes on", async () => {
const created = {
type: "session.created",
properties: { sessionID: "ses_child", info: { id: "ses_child", parentID: "ses_1", agent: "explore" } },
};
const server = new FakeOpenCode({ events: [sse(created)] });
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
agent.runs.set("ses_1", { id: "run_1", error: null });
await collect(emitted, (event) => event.event === "task");
await assert.rejects(agent.cancelTask({ sessionId: "ses_1", taskId: "ses_other" }), /no opencode subagent/);
await agent.cancelTask({ sessionId: "ses_1", taskId: "ses_child" });
assert.ok(tasks(emitted).some((task) => task.status === "cancelled"));
const aborted = server.calls.filter((call) => call.path.endsWith("/abort")).map((call) => call.path);
assert.deepEqual(aborted, ["/session/ses_child/abort"]);
assert.ok(agent.runs.has("ses_1"), "the chat's run goes on");
// The abort error it reports afterwards does not turn it into a failure.
await agent.apply({ type: "failed", session: "ses_child", message: "Aborted" }, "/w");
assert.equal(agent.children.get("ses_child")?.status, "cancelled");
assert.ok(!emitted.some((event) => event.event === "notice"), "a stop is not news");
await agent.shutdown();
});
test("cancelling a chat stops its subagents, one the plugin never saw start too", async () => {
const created = {
type: "session.created",
properties: { sessionID: "ses_child", info: { id: "ses_child", parentID: "ses_1", agent: "explore" } },
};
const server = new FakeOpenCode({
events: [sse(created)],
routes: (call) =>
call.path === "/session/ses_1/children" ? { body: [{ id: "ses_unseen", parentID: "ses_1" }] } : undefined,
});
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
agent.runs.set("ses_1", { id: "run_1", error: null });
await collect(emitted, (event) => event.event === "task");
await agent.cancel({ sessionId: "ses_1" });
const aborted = server.calls.filter((call) => call.path.endsWith("/abort")).map((call) => call.path);
assert.ok(aborted.includes("/session/ses_1/abort"), "the chat was aborted");
assert.ok(aborted.includes("/session/ses_child/abort"), "the subagent was stopped");
assert.ok(aborted.includes("/session/ses_unseen/abort"), "the server was asked for the children too");
const finished = emitted.filter((event) => event.event === "run_finished");
assert.deepEqual(
finished.map((event) => event.outcome?.status),
["cancelled"],
);
await agent.shutdown();
});
test("the permission ruleset is pushed on resume and on a mode change, never twice", async () => {
const server = new FakeOpenCode();
const { agent } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: { permission_mode: "auto_edits" } });
const pushed = await server.waitFor((call) => call.method === "PATCH" && call.path === "/session/ses_1");
const body = obj(pushed.body);
const permission = Array.isArray(body["permission"]) ? body["permission"] : [];
const edit = permission.find((rule) => obj(rule)["permission"] === "edit");
assert.equal(obj(edit)["action"], "allow", "auto-accept edits reaches the session");
assert.ok(permission.some((rule) => obj(rule)["permission"] === "read" && obj(rule)["pattern"] === "*.env"));
server.calls.length = 0;
// Rules a session already runs are left alone.
const { ruleset } = await import("./permission.ts");
const stored = ruleset("supervised");
await agent.pushRules("ses_1", "/w", "supervised", stored);
assert.ok(!server.calls.some((call) => call.method === "PATCH"));
await agent.pushRules("ses_1", "/w", "full", stored);
assert.ok(
server.calls.some((call) => call.method === "PATCH"),
"a mode change is pushed",
);
await agent.shutdown();
});
test("full access answers permission requests without asking the user, once", async () => {
const asked = {
type: "permission.asked",
properties: {
id: "per_1",
sessionID: "ses_1",
permission: "bash",
patterns: ["echo hi"],
metadata: { command: "echo hi" },
always: ["echo *"],
},
};
const server = new FakeOpenCode({ events: [sse(asked)] });
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: { permission_mode: permissionMode.FULL } });
const reply = await server.waitFor((call) => call.path === "/permission/per_1/reply");
assert.deepEqual(reply.body, { reply: "once" }, "an `always` grant would widen other chats on this folder");
await settle();
assert.ok(!emitted.some((event) => event.event === "approval"), "full access showed no card");
await agent.shutdown();
});
test("question answers follow their id, not their position", () => {
const fields = [
{ id: questionId(0, "Database"), header: "Database", question: "Which database?", kind: "select" },
{ id: questionId(1, "Migrate now?"), header: "Migrate now?", question: "Run the migration now?", kind: "boolean" },
];
assert.deepEqual(
fields.map((field) => field.id),
["question-0-database", "question-1-migrate-now"],
);
const answer = (values: Record<string, unknown>) => fields.map((field) => fieldAnswer(field, values));
assert.deepEqual(answer({ "question-1-migrate-now": true, "question-0-database": "Postgres" }), [
["Postgres"],
["true"],
]);
// An answer keyed the way OpenCode names the question still lands.
assert.deepEqual(answer({ Database: ["SQLite"], "Run the migration now?": false }), [["SQLite"], ["false"]]);
assert.deepEqual(answer({}), [[], []]);
});
test("a question round trip: asked, answered in the server's order, resolved", async () => {
const asked = {
type: "question.asked",
properties: {
id: "que_1",
sessionID: "ses_1",
questions: [
{ question: "Which database?", header: "Database", options: [{ label: "Postgres" }, { label: "SQLite" }] },
{ question: "Why?", header: "", options: [], custom: true },
],
},
};
const server = new FakeOpenCode({ events: [sse(asked)] });
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const [question] = (await collect(emitted, (event) => event.event === "question")).filter(
(event) => event.event === "question",
);
assert.deepEqual(
present(question).fields?.map((field) => field.id),
["question-0-database", "question-1"],
);
await agent.respondToQuestion({
questionId: "que_1",
answer: { values: { "question-1": "speed", "question-0-database": "SQLite" }, cancelled: false },
});
const reply = await server.waitFor((call) => call.path === "/question/que_1/reply");
assert.deepEqual(reply.body, { answers: [["SQLite"], ["speed"]] });
await assert.rejects(agent.respondToQuestion({ questionId: "que_1", answer: {} }), /no pending opencode question/);
await agent.shutdown();
});
test("a prompt carries the host's message id, the chosen model, mode, variant and the plugin instructions", async () => {
const server = new FakeOpenCode({
routes: (call) => (call.path === "/command" ? { body: [{ name: "review" }] } : undefined),
});
const { agent, emitted } = agentOn(server);
const { sessionId } = await agent.createSession({
workspace: "/w",
options: { model: "opencode/big-pickle", agent: "plan", variant: "high", permission_mode: "supervised" },
instructions: "## Rules from the x plugin\n\nBe brief.",
});
assert.equal(server.connections, 1, "the folder's event stream is open before the session is handed out");
const created = server.calls.find((call) => call.method === "POST" && call.path === "/session");
assert.deepEqual(present(created).query, { directory: "/w" });
const createdBody = obj(present(created).body);
assert.equal(createdBody["agent"], "plan");
assert.deepEqual(createdBody["model"], { providerID: "opencode", id: "big-pickle" });
const createdPermission = Array.isArray(createdBody["permission"]) ? createdBody["permission"] : [];
assert.equal(obj(createdPermission[0])["action"], "ask");
const { runId } = await agent.prompt({
sessionId,
input: { blocks: [{ type: "text", text: "hello" }], itemId: "0f1e-22" },
});
assert.match(runId, /^opencode-run-/);
const sent = server.calls.find((call) => call.path === `/session/${sessionId}/prompt_async`);
assert.deepEqual(present(sent).body, {
parts: [{ type: "text", text: "hello" }],
messageID: "msg_0f1e-22",
agent: "plan",
model: { providerID: "opencode", modelID: "big-pickle" },
variant: "high",
system: "## Rules from the x plugin\n\nBe brief.",
});
// A known command goes to the command endpoint instead.
await agent.finish(sessionId, { status: "completed" });
await agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "/review the parser" }] } });
const command = await server.waitFor((call) => call.path === `/session/${sessionId}/command`);
assert.deepEqual(command.body, {
command: "review",
arguments: "the parser",
agent: "plan",
model: "opencode/big-pickle",
variant: "high",
});
// A model change drops the variant.
const { options } = await agent
.setOption({ sessionId, optionId: "model", value: "opencode/other" })
.catch(() => ({ options: null }));
assert.equal(agent.sessions.get(sessionId)?.options.variant, undefined);
assert.equal(options, null, "the fake server has no catalog, which is an error, never an invented list");
assert.ok(emitted.every((event) => event.event !== "run_finished" || event.outcome?.status === "completed"));
await agent.shutdown();
});
test("a failed prompt leaves no run behind", async () => {
const server = new FakeOpenCode({
routes: (call) => (call.path.endsWith("/prompt_async") ? { status: 400, body: "bad model" } : undefined),
});
const { agent } = agentOn(server);
const { sessionId } = await agent.createSession({ workspace: "/w", options: {} });
await assert.rejects(agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "x" }] } }), /400: bad model/);
assert.equal(agent.runs.size, 0);
await assert.rejects(
agent.prompt({ sessionId, input: { blocks: [{ type: "image", mimeType: "application/zip", data: "AA" }] } }),
/cannot attach/,
);
assert.equal(agent.runs.size, 0);
await agent.shutdown();
});
const delivery = { inputId: "input_1", attemptId: "attempt_1", intent: "send" as const };
const userMessage = (sessionID: string, id: string) => ({
type: "message.updated",
properties: { info: { id, sessionID, role: "user" } },
});
const assistantMessage = (sessionID: string, parentID: string) => ({
type: "message.updated",
properties: { info: { id: "msg_answer", sessionID, role: "assistant", parentID } },
});
test("durable input is consumed once by its native assistant parent, not its write or user echo", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const reply = await agent.prompt({
sessionId: "ses_1",
input: { itemId: "host_1", delivery, blocks: [{ type: "text", text: "hello" }] },
});
assert.deepEqual(obj(reply)["receipt"], { evidence: "local_write", nativeInputId: "msg_host_1" });
assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
server.push(
userMessage("ses_1", "msg_host_1"),
assistantMessage("ses_other", "msg_host_1"),
assistantMessage("ses_1", "msg_unrelated"),
{ type: "session.updated", properties: { sessionID: "ses_1", info: { id: "ses_1", title: "Echo received" } } },
);
await collect(emitted, (event) => event.event === "session_info");
assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
server.push(assistantMessage("ses_1", "msg_host_1"), assistantMessage("ses_1", "msg_host_1"), {
type: "session.idle",
properties: { sessionID: "ses_1" },
});
await collect(emitted, (event) => event.event === "run_finished");
assert.deepEqual(
emitted.filter((event) => event.event === "input_consumed"),
[
{
sessionId: "ses_1",
runId: reply.runId,
event: "input_consumed",
inputId: "input_1",
nativeInputId: "msg_host_1",
},
],
);
assert.equal(emitted.at(-1)?.event, "run_finished", "pickup is visible before completion releases FIFO");
await agent.shutdown();
});
test("idle success and cancellation without parent evidence never fabricate consumption", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
await agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } });
server.push(userMessage("ses_1", "msg_first"), { type: "session.idle", properties: { sessionID: "ses_1" } });
await collect(emitted, (event) => event.event === "run_finished");
assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
await agent.prompt({
sessionId: "ses_1",
input: { itemId: "second", delivery: { ...delivery, inputId: "input_2", attemptId: "attempt_2" }, blocks: [] },
});
await agent.cancel({ sessionId: "ses_1" });
server.push(assistantMessage("ses_1", "msg_second"));
await settle(10);
assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
assert.deepEqual(
emitted.filter((event) => event.event === "run_finished").map((event) => event.outcome?.status),
["completed", "cancelled"],
);
assert.equal(agent.inputRuns.size, 1, "the completed but unproven input remains reconcilable");
await agent.shutdown();
assert.equal(agent.inputRuns.size, 0);
});
test("a synchronous completed command resolves its durable input and ends once without an idle event", async () => {
const server = new FakeOpenCode({
routes: (call) => {
if (call.path === "/command") return { body: [{ name: "review" }] };
if (call.path.endsWith("/command"))
return {
body: {
info: {
id: "msg_answer",
sessionID: "ses_1",
role: "assistant",
parentID: "msg_host_1",
time: { completed: 1 },
},
parts: [],
},
};
return undefined;
},
});
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const { runId } = await agent.prompt({
sessionId: "ses_1",
input: { itemId: "host_1", delivery, blocks: [{ type: "text", text: "/review code" }] },
});
await collect(emitted, (event) => event.event === "run_finished");
assert.equal(
obj(present(server.calls.find((call) => call.path === "/session/ses_1/command")).body)["messageID"],
"msg_host_1",
);
assert.deepEqual(
emitted.filter((event) => event.event === "input_consumed" || event.event === "run_finished"),
[
{ sessionId: "ses_1", runId, event: "input_consumed", inputId: "input_1", nativeInputId: "msg_host_1" },
{ sessionId: "ses_1", runId, event: "run_finished", outcome: { status: "completed" } },
],
);
assert.equal(agent.runs.size, 0);
await agent.shutdown();
});
test("a completed command racing Stop cannot consume input or finish a successor", async () => {
const commandEntered = Promise.withResolvers<void>();
const commandRelease = Promise.withResolvers<void>();
const abortEntered = Promise.withResolvers<void>();
const abortRelease = Promise.withResolvers<void>();
const server = new FakeOpenCode({
routes: (call) => {
if (call.path === "/command") return { body: [{ name: "review" }] };
if (call.path === "/session/ses_1/command")
return {
body: {
info: {
id: "msg_answer",
role: "assistant",
sessionID: "ses_1",
parentID: "msg_first",
time: { completed: 1 },
},
parts: [],
},
};
return undefined;
},
});
const { agent, emitted } = agentOn(server, {
onFetch: async (url, init) => {
const response = server.handle(url, init);
const path = new URL(url).pathname;
if (path === "/session/ses_1/command") {
commandEntered.resolve();
await commandRelease.promise;
}
if (path === "/session/ses_1/abort") {
abortEntered.resolve();
await abortRelease.promise;
}
return response;
},
});
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const first = await agent.prompt({
sessionId: "ses_1",
input: { itemId: "first", delivery, blocks: [{ type: "text", text: "/review" }] },
});
await commandEntered.promise;
const stopping = agent.cancel({ sessionId: "ses_1" });
await abortEntered.promise;
commandRelease.resolve();
await collect(emitted, (event) => event.event === "run_finished");
assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
assert.deepEqual(
emitted.filter((event) => event.event === "run_finished").map((event) => [event.runId, event.outcome?.status]),
[[first.runId, "cancelled"]],
);
const next = await agent.prompt({ sessionId: "ses_1", input: { itemId: "next", blocks: [] } });
abortRelease.resolve();
await stopping;
assert.equal(agent.runs.get("ses_1")?.id, next.runId);
assert.equal(emitted.filter((event) => event.event === "run_finished").length, 1);
await agent.shutdown();
});
test("durable input without a user-row id names its delivery attempt", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const { runId, receipt } = await agent.prompt({ sessionId: "ses_1", input: { delivery, blocks: [] } });
assert.deepEqual(receipt, { evidence: "local_write", nativeInputId: "msg_attempt_1" });
assert.equal(
obj(present(server.calls.find((call) => call.path.endsWith("/prompt_async"))).body)["messageID"],
"msg_attempt_1",
);
server.push(assistantMessage("ses_1", "msg_attempt_1"));
await collect(emitted, (event) => event.event === "input_consumed");
assert.equal(emitted.at(-1)?.runId, runId);
assert.equal(emitted.at(-1)?.inputId, "input_1");
await agent.shutdown();
});
test("late native parent evidence reconciles a completed input without finishing its successor", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const first = await agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } });
await agent.finish("ses_1", { status: "completed" });
const next = await agent.prompt({
sessionId: "ses_1",
input: { itemId: "next", delivery: { ...delivery, inputId: "input_2", attemptId: "attempt_2" }, blocks: [] },
});
server.push(assistantMessage("ses_1", "msg_first"));
await collect(emitted, (event) => event.event === "input_consumed");
assert.equal(emitted.at(-1)?.runId, first.runId);
assert.equal(emitted.at(-1)?.inputId, "input_1");
assert.equal(agent.runs.get("ses_1")?.id, next.runId);
assert.equal(emitted.filter((event) => event.event === "run_finished").length, 1);
await agent.shutdown();
});
for (const failed of [false, true])
test(`native pickup before an async ${failed ? "failed" : "successful"} reply stays correlated`, async () => {
const entered = Promise.withResolvers<void>();
const release = Promise.withResolvers<void>();
const server = new FakeOpenCode();
let delayed = true;
const { agent, emitted } = agentOn(server, {
onFetch: async (url, init) => {
const response = server.handle(url, init);
if (url.includes("/prompt_async") && delayed) {
delayed = false;
entered.resolve();
await release.promise;
if (failed) throw new Error("connection lost after sending");
}
return response;
},
});
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const sending = agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } });
await entered.promise;
const firstRun = present(agent.runs.get("ses_1")).id;
server.push(assistantMessage("ses_1", "msg_first"), { type: "session.idle", properties: { sessionID: "ses_1" } });
await collect(emitted, (event) => event.event === "run_finished");
const next = await agent.prompt({ sessionId: "ses_1", input: { itemId: "next", blocks: [] } });
release.resolve();
if (failed) await assert.rejects(sending, /connection lost after sending/);
else assert.equal((await sending).runId, firstRun);
assert.equal(agent.runs.get("ses_1")?.id, next.runId, "late cleanup cannot delete a newer run");
assert.equal(
emitted.filter((event) => event.event === "input_rejected").length,
0,
"ambiguous transport is not rejection",
);
assert.deepEqual(
emitted.filter((event) => event.event === "input_consumed").map((event) => [event.runId, event.inputId]),
[[firstRun, "input_1"]],
);
await agent.shutdown();
});
test("an ambiguous failed send can still be reconciled by a later native parent", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server, {
onFetch: (url, init) => {
const response = server.handle(url, init);
if (url.includes("/prompt_async")) throw new Error("lost response");
return response;
},
});
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
await assert.rejects(
agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } }),
/lost response/,
);
assert.equal(agent.runs.size, 0);
assert.equal(
emitted.filter((event) => event.event === "input_rejected" || event.event === "input_consumed").length,
0,
);
const original = present(agent.inputRuns.get("ses_1/msg_first")).id;
server.push(assistantMessage("ses_1", "msg_first"));
await collect(emitted, (event) => event.event === "input_consumed");
assert.equal(emitted.at(-1)?.runId, original);
assert.equal(emitted.at(-1)?.inputId, "input_1");
assert.equal(agent.runs.size, 0, "reconciliation never revives a run");
await agent.shutdown();
});
for (const kind of ["success", "failure", "cancelled"] as const)
test(`a late command ${kind} cannot finish or consume a successor`, async () => {
const entered = Promise.withResolvers<void>();
const release = Promise.withResolvers<void>();
const server = new FakeOpenCode({
routes: (call) => {
if (call.path === "/command") return { body: [{ name: "review" }] };
if (call.path === "/session/ses_1/command")
return kind === "failure"
? { status: 500, body: "lost command response" }
: {
body: {
info: {
id: "msg_answer",
role: "assistant",
sessionID: "ses_1",
parentID: "msg_first",
time: { completed: 1 },
},
parts: [],
},
};
return undefined;
},
});
const { agent, emitted } = agentOn(server, {
onFetch: async (url, init) => {
const response = server.handle(url, init);
if (new URL(url).pathname === "/session/ses_1/command") {
entered.resolve();
await release.promise;
}
return response;
},
});
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const first = await agent.prompt({
sessionId: "ses_1",
input: { itemId: "first", delivery, blocks: [{ type: "text", text: "/review" }] },
});
await entered.promise;
if (kind === "cancelled") await agent.cancel({ sessionId: "ses_1" });
else await agent.finish("ses_1", { status: "completed" });
const next = await agent.prompt({ sessionId: "ses_1", input: { itemId: "next", blocks: [] } });
release.resolve();
if (kind === "success") await collect(emitted, (event) => event.event === "input_consumed");
else await settle(10);
assert.equal(agent.runs.get("ses_1")?.id, next.runId);
assert.deepEqual(
emitted.filter((event) => event.event === "run_finished").map((event) => event.runId),
[first.runId],
);
assert.deepEqual(
emitted.filter((event) => event.event === "input_consumed").map((event) => [event.runId, event.inputId]),
kind === "success" ? [[first.runId, "input_1"]] : [],
);
await agent.shutdown();
});
for (const [name, extra] of Object.entries({
unrelated: { parentID: "msg_other" },
unfinished: { time: { created: 1 } },
user: { role: "user" },
foreign: { sessionID: "ses_other" },
aborted: { error: { name: "MessageAbortedError" } },
}))
test(`a ${name} command response is not consumption evidence`, async () => {
const server = new FakeOpenCode({
routes: (call) => {
if (call.path === "/command") return { body: [{ name: "review" }] };
if (call.path.endsWith("/command"))
return {
body: {
info: {
id: "msg_answer",
role: "assistant",
sessionID: "ses_1",
parentID: "msg_first",
time: { completed: 1 },
...extra,
},
parts: [],
},
};
return undefined;
},
});
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
await agent.prompt({
sessionId: "ses_1",
input: { itemId: "first", delivery, blocks: [{ type: "text", text: "/review" }] },
});
await server.waitFor((call) => call.path === "/session/ses_1/command");
await settle(10);
assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
assert.ok(!emitted.some((event) => event.event === "run_finished" && event.outcome?.status === "completed"));
await agent.shutdown();
});
test("preparation reserves the run, rejects unsupported steering/queues and cleans up unsent input", async () => {
const entered = Promise.withResolvers<void>();
const release = Promise.withResolvers<void>();
const server = new FakeOpenCode({ routes: (call) => (call.path === "/command" ? { body: [] } : undefined) });
const { agent, emitted } = agentOn(server, {
onFetch: async (url, init) => {
const response = server.handle(url, init);
if (new URL(url).pathname === "/command") {
entered.resolve();
await release.promise;
}
return response;
},
});
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const preparing = agent.prompt({
sessionId: "ses_1",
input: {
delivery,
blocks: [
{ type: "text", text: "/unknown" },
{ type: "image", mimeType: "application/zip", data: "AA" },
],
},
});
await entered.promise;
const original = present(agent.runs.get("ses_1")).id;
await assert.rejects(
agent.prompt({
sessionId: "ses_1",
input: { delivery: { inputId: "steer", attemptId: "steer_attempt", intent: "steer" }, blocks: [] },
}),
/no proven native steering/,
);
await assert.rejects(
agent.prompt({
sessionId: "ses_1",
input: { delivery: { inputId: "queue", attemptId: "queue_attempt", intent: "queue" }, blocks: [] },
}),
/held by the host/,
);
assert.equal(agent.runs.get("ses_1")?.id, original);
release.resolve();
await assert.rejects(preparing, /cannot attach/);
assert.equal(agent.runs.size, 0);
assert.equal(agent.inputRuns.size, 0);
assert.equal(
server.calls.filter(
(call) =>
call.path.endsWith("/prompt_async") || (call.path.startsWith("/session/") && call.path.endsWith("/command")),
).length,
0,
);
assert.deepEqual(
emitted.filter((event) => event.event === "input_rejected").map((event) => [event.inputId, event.attemptId]),
[
["steer", "steer_attempt"],
["queue", "queue_attempt"],
["input_1", "attempt_1"],
],
);
assert.ok(!emitted.some((event) => event.event === "input_consumed" || event.event === "run_finished"));
await agent.shutdown();
});
test("a preparation defect also clears the reserved run without native dispatch", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
const block = { type: "text", text: "hello" };
Object.defineProperty(block, "text", {
get() {
throw new Error("preparation defect");
},
});
await assert.rejects(
agent.prompt({ sessionId: "ses_1", input: { delivery, blocks: [block] } }),
/preparation defect/,
);
assert.equal(agent.runs.size, 0);
assert.equal(agent.inputRuns.size, 0);
assert.equal(emitted.at(-1)?.event, "input_rejected");
assert.equal(emitted.at(-1)?.attemptId, "attempt_1");
assert.equal(server.calls.filter((call) => call.path.endsWith("/prompt_async")).length, 0);
await agent.shutdown();
});
test("an idle session ends the run as completed, or failed when it reported an error", async () => {
const server = new FakeOpenCode();
const { agent, emitted } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
const { runId } = await agent.prompt({ sessionId: "ses_1", input: { blocks: [{ type: "text", text: "x" }] } });
server.push({
type: "session.error",
properties: { sessionID: "ses_1", error: { name: "APIError", data: { message: "rate limited" } } },
});
server.push({ type: "session.idle", properties: { sessionID: "ses_1" } });
const seen = await collect(emitted, (event) => event.event === "run_finished");
assert.deepEqual(
seen.filter((event) => event.event === "notice").map((event) => [event.level, event.message]),
[["error", "rate limited"]],
);
assert.deepEqual(seen.at(-1), {
sessionId: "ses_1",
runId,
event: "run_finished",
outcome: { status: "failed", message: "rate limited" },
});
// A second idle is a no-op: one run_finished per run.
server.push({ type: "session.idle", properties: { sessionID: "ses_1" } });
await settle(10);
assert.equal(emitted.filter((event) => event.event === "run_finished").length, 1);
await agent.shutdown();
});
test("plugin tools reach OpenCode through the host's loopback MCP server, once per folder and tool list", async () => {
const hostCalls: { method: string; params: unknown }[] = [];
const server = new FakeOpenCode();
const { agent } = agentOn(server, {
onHost: (method, params) => {
hostCalls.push({ method, params });
return method === "host/mcp.serve"
? { url: (params as { url?: string }).url ?? "http://127.0.0.1:5555/mcp/token" }
: {};
},
});
const tools = [
{
name: "lucky_number",
description: "Returns today's lucky number.",
inputSchema: { type: "object", properties: {} },
},
];
const { sessionId } = await agent.createSession({ workspace: "/w", options: {}, tools });
assert.deepEqual(hostCalls, [{ method: "host/mcp.serve", params: { agentId: "opencode", workspace: "/w", tools } }]);
const added = server.calls.filter((call) => call.method === "POST" && call.path === "/mcp");
assert.equal(added.length, 1);
assert.deepEqual(added[0]?.query, { directory: "/w" });
assert.deepEqual(added[0]?.body, {
name: "convergence",
config: { type: "remote", url: "http://127.0.0.1:5555/mcp/token", enabled: true, oauth: false },
});
// A second chat in the folder with the same tools changes nothing; a
// prompt checks again, cheaply.
await agent.createSession({ workspace: "/w", options: {}, tools });
await agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "x" }] } });
assert.equal(hostCalls.length, 1);
assert.equal(server.calls.filter((call) => call.path === "/mcp").length, 1);
// New tools: the same address serves them, and OpenCode reconnects.
const more = [...tools, { name: "echo", inputSchema: { type: "object" } }];
await agent.resumeSession({ sessionId, workspace: "/w", options: {}, tools: more });
assert.deepEqual(hostCalls[1]?.params, {
agentId: "opencode",
workspace: "/w",
tools: more,
url: "http://127.0.0.1:5555/mcp/token",
});
assert.equal(server.calls.filter((call) => call.path === "/mcp").length, 2);
await agent.shutdown();
});
test("tools that cannot reach OpenCode are said so in the chat, and the session still starts", async () => {
const server = new FakeOpenCode({
routes: (call) =>
call.path === "/mcp" ? { body: { convergence: { status: "failed", error: "connection refused" } } } : undefined,
});
const { agent, emitted } = agentOn(server, {
onHost: (method) => (method === "host/mcp.serve" ? { url: "http://127.0.0.1:1/mcp/t" } : {}),
});
const { sessionId } = await agent.createSession({ workspace: "/w", options: {}, tools: [{ name: "t" }] });
const notice = emitted.find((event) => event.event === "notice");
assert.equal(present(notice).sessionId, sessionId);
assert.equal(present(notice).level, "warning");
assert.match(
String(present(notice).message),
/Plugin tools are not available to OpenCode: .*failed: connection refused/,
);
// A server on another computer cannot reach this one's loopback.
const remote = new FakeOpenCode();
const far = agentOn(remote, {
instance: { id: "team", serverUrl: "https://opencode.example:4096" },
onFetch: (url, init) => remote.handle(url.replace("https://opencode.example:4096", BASE), init),
});
await far.agent.createSession({ workspace: "/w", options: {}, tools: [{ name: "t" }] });
assert.match(
String(far.emitted.find((event) => event.event === "notice")?.message),
/only to an OpenCode server on this computer/,
);
assert.ok(!far.api.hostCalls.some((call) => call.method === "host/mcp.serve"));
await agent.shutdown();
await far.agent.shutdown();
});
test("initialize reports the version, the sign-in methods, and a rejected password as a sign-in problem", async () => {
const server = new FakeOpenCode({
routes: (call) => {
if (call.path === "/global/health") return { body: { healthy: true, version: "1.18.29" } };
if (call.path === "/provider/auth")
return {
body: {
openai: [
{ type: "oauth", label: "ChatGPT" },
{ type: "api", label: "API key" },
],
anthropic: [{ type: "api", label: "API key" }],
},
};
return undefined;
},
});
const { agent } = agentOn(server, { instance: { id: "team", password: "p" } });
const info = await agent.initialize();
assert.equal(info.id, "opencode:team");
assert.deepEqual(info.status, { state: "ready" });
assert.equal(info.version, "1.18.29");
assert.equal(info.maintenance, undefined, "a server somebody else runs is not ours to upgrade");
assert.equal(info.continuationKey, BASE);
assert.equal(info.capabilities?.steer, false);
assert.equal(info.capabilities?.rollback, true);
assert.deepEqual(info.authMethods, [
{ id: "anthropic", name: "anthropic", description: "API key" },
{ id: "openai", name: "openai", description: "ChatGPT, API key" },
]);
assert.ok(info.icon?.startsWith("<svg"));
assert.equal(
server.calls[0]?.headers.authorization,
`Basic ${Buffer.from("opencode:p").toString("base64")}`,
"the instance's own password",
);
const locked = new FakeOpenCode({ routes: () => ({ status: 401, body: "Unauthorized" }) });
const refused = await agentOn(locked).agent.initialize();
assert.equal(refused.status?.state, "auth_required");
assert.match(String(refused.status?.message), /CONVERGENCE_OPENCODE_SERVER_PASSWORD/);
const old = new FakeOpenCode({
routes: (call) => (call.path === "/global/health" ? { body: { version: "1.9.0" } } : undefined),
});
const tooOld = await agentOn(old).agent.initialize();
assert.equal(tooOld.status?.state, "unavailable");
assert.match(String(tooOld.status?.message), /1\.9\.0 is too old/);
});
test("rollback forgets the named message and everything after it, newest first, and touches no files", async () => {
const messages = [{ info: { id: "msg_1" } }, { info: { id: "msg_host-2" } }, { info: { id: "msg_3" } }];
const server = new FakeOpenCode({
routes: (call) =>
call.method === "GET" && call.path === "/session/ses_1/message" ? { body: messages } : undefined,
});
const { agent } = agentOn(server);
await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
await agent.rollback({ sessionId: "ses_1", itemId: "host-2" });
const deleted = server.calls.filter((call) => call.method === "DELETE").map((call) => call.path);
assert.deepEqual(deleted, ["/session/ses_1/message/msg_3", "/session/ses_1/message/msg_host-2"]);
assert.ok(!server.calls.some((call) => call.path.includes("revert")), "files belong to the host's checkpoints");
await assert.rejects(agent.rollback({ sessionId: "ses_1", itemId: "nope" }), /does not know message nope/);
await agent.shutdown();
});
test("sessions list only the chats, newest first, without placeholder titles; history nests subagents", async () => {
const server = new FakeOpenCode({
routes: (call) => {
if (call.method === "GET" && call.path === "/session") {
return {
body: [
{ id: "ses_old", title: "Old", time: { created: 1, updated: 1000 } },
{ id: "ses_new", title: "New session - 2026-09-16T09:01:02.003Z", time: { created: 2, updated: 2000 } },
{ id: "ses_kid", parentID: "ses_old", title: "Kid", time: { created: 3, updated: 3000 } },
],
};
}
if (call.path === "/session/ses_old/message") {
return {
body: [
{
info: { id: "msg_u", role: "user", time: { created: 1 } },
parts: [{ type: "text", id: "p1", messageID: "msg_u", text: "hi" }],
},
{
info: { id: "msg_a", role: "assistant", time: { created: 2 } },
parts: [{ type: "text", id: "p2", messageID: "msg_a", text: "hello" }],
},
],
};
}
return undefined;
},
});
const { agent } = agentOn(server);
const { sessions } = await agent.listSessions({ workspace: "/w" });
assert.deepEqual(
sessions.map((session) => [session.id, session.title]),
[
["ses_new", undefined],
["ses_old", "Old"],
],
);
assert.deepEqual(server.calls[0]?.query, { directory: "/w", roots: "true" });
const { items } = await agent.readSession({ workspace: "/w", sessionId: "ses_old" });
assert.deepEqual(
items.map((item) => {
if (item.role === "user") {
const block = item.blocks[0];
return [item.role, block && block.type === "text" ? block.text : undefined];
}
if (item.role === "assistant") return [item.role, item.text];
return [item.role, undefined];
}),
[
["user", "hi"],
["assistant", "hello"],
],
);
});
test("sign-in opens the provider's page in the browser; an API key method says how instead", async () => {
const server = new FakeOpenCode({
routes: (call) => {
if (call.path === "/provider/auth")
return {
body: { openai: [{ type: "oauth", label: "ChatGPT" }], anthropic: [{ type: "api", label: "API key" }] },
};
if (call.path === "/provider/openai/oauth/authorize")
return { body: { url: "https://auth.openai.com/authorize?x=1", method: "auto", instructions: "" } };
return undefined;
},
});
const { agent, api } = agentOn(server);
await agent.authenticate({ method: "openai" });
assert.deepEqual(api.opened, ["https://auth.openai.com/authorize?x=1"]);
const authorize = server.calls.find((call) => call.path === "/provider/openai/oauth/authorize");
assert.deepEqual(authorize?.body, { method: 0 });
await assert.rejects(agent.authenticate({ method: "anthropic" }), /has no browser login; run `opencode auth login`/);
await assert.rejects(agent.authenticate({ method: "nobody" }), /no sign-in methods for nobody/);
api.openUrl = async () => {
throw new Error("not allowed without a click");
};
await assert.rejects(
agent.authenticate({ method: "openai" }),
/open https:\/\/auth\.openai\.com\/authorize\?x=1 in your browser/,
);
api.openUrl = async () => ({ opened: false, reason: "a link opens only right after you click" });
await assert.rejects(
agent.authenticate({ method: "openai" }),
/sign in to openai \(a link opens only right after you click\)/,
);
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 94.8 KB | 5 permissions | Listed |
No comments yet.