Official
opencode-v2
OpenCode 2 agent provider: runs opencode serve (2.x) and talks to its /api 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-v2@0.1.0
Permissions in 0.1.0
Files
agent.test.ts27.3 KB
// The agent against a scripted OpenCode 2 server: one chat through its
// whole life (create, prompt, stream, approvals, questions, subagents,
// cancel), checked by the events it emits and the calls it makes.
import { test } from "node:test";
import assert from "node:assert/strict";
import type { AgentEvent } from "convergence/protocol";
import { permissionMode } from "../sdk/agent.ts";
import { ruleset } from "./permission.ts";
import { harness, session, until } from "./testing.test.ts";
const WORKSPACE = "/w";
const message = "msg_a1";
const of = <K extends AgentEvent["event"]>(emitted: AgentEvent[], kind: K) =>
emitted.filter((event): event is Extract<AgentEvent, { event: K }> => event.event === kind);
async function chat(options: Record<string, unknown> = {}, setup: Parameters<typeof harness>[0] = {}) {
const context = harness(setup);
const { sessionId } = await context.run(
context.agent.createSession({ workspace: WORKSPACE, options, tools: [], instructions: "" }),
);
return { ...context, sessionId };
}
test("a prompt streams text, a tool call and usage, and ends its run once", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
const created = server.find("POST", "/api/session");
// No model chosen yet: the server's default runs.
assert.deepEqual(created?.body, {
location: { directory: WORKSPACE },
permissions: ruleset(permissionMode.SUPERVISED),
});
const { runId } = await run(agent.prompt(sessionId, { itemId: "item-1", blocks: [{ type: "text", text: "hi" }] }));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/prompt`)?.body, { text: "hi", id: "msg_item-1" });
// The model and rules were set at create: nothing is switched again.
assert.equal(server.find("POST", `/api/session/${sessionId}/model`), undefined);
assert.equal(server.find("PATCH", `/api/session/${sessionId}`), undefined);
const inMessage = { sessionID: sessionId, assistantMessageID: message };
server.push(
{ type: "session.execution.started", data: { sessionID: sessionId } },
{
type: "session.step.started",
data: { ...inMessage, model: { id: "fledge-free", providerID: "opencode" } },
},
{ type: "session.text.delta", data: { ...inMessage, ordinal: 0, delta: "Hel" } },
{ type: "session.text.ended", data: { ...inMessage, ordinal: 0, text: "Hello" } },
{ type: "session.tool.input.started", data: { ...inMessage, id: "call_1", name: "shell" } },
{ type: "session.tool.called", data: { ...inMessage, id: "call_1", input: { command: "ls" } } },
{
type: "session.tool.success",
data: { ...inMessage, id: "call_1", content: [{ type: "text", text: "a.txt\n" }], metadata: { exit: 0 } },
},
{
type: "session.step.ended",
data: { ...inMessage, tokens: { input: 100, output: 10, reasoning: 0, cache: { read: 50, write: 0 } } },
},
{
type: "session.usage.updated",
data: {
sessionID: sessionId,
cost: 0.5,
tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } },
},
},
{ type: "session.execution.succeeded", data: { sessionID: sessionId } },
{ type: "session.execution.succeeded", data: { sessionID: sessionId } },
);
await until(() => of(emitted, "run_finished").length);
await new Promise((resolve) => setTimeout(resolve, 20));
assert.deepEqual(
of(emitted, "text_delta").map((event) => [event.itemId, event.text, event.mode, event.runId]),
[
[`${message}/text/0`, "Hel", "append", runId],
[`${message}/text/0`, "Hello", "replace", runId],
],
);
const tool = `${message}/call_1`;
assert.deepEqual(of(emitted, "tool_call_started")[0], {
sessionId,
runId,
event: "tool_call_started",
id: tool,
name: "shell",
kind: "execute",
title: "shell",
status: "pending",
});
const updates = of(emitted, "tool_call_updated");
assert.equal(updates[0]?.title, "ls");
assert.deepEqual(updates[1]?.content, [{ type: "terminal", command: "ls", output: "a.txt\n", exitCode: 0 }]);
assert.deepEqual(of(emitted, "usage")[0], {
sessionId,
runId,
event: "usage",
usedTokens: 160,
inputTokens: 100,
cachedInputTokens: 50,
outputTokens: 10,
reasoningTokens: 0,
costUsd: 0.5,
contextWindow: 200_000,
});
assert.deepEqual(of(emitted, "run_finished"), [
{ sessionId, runId, event: "run_finished", outcome: { status: "completed" } },
]);
});
test("an approval is asked with its tool call and answered on the server", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "edit" }] }));
const inMessage = { sessionID: sessionId, assistantMessageID: message };
const patch = "--- a.txt\n+++ a.txt\n@@ -1 +1 @@\n-hello\n+goodbye\n";
server.push(
{ type: "session.tool.input.started", data: { ...inMessage, id: "call_2", name: "edit" } },
{ type: "session.tool.called", data: { ...inMessage, id: "call_2", input: { path: "a.txt" } } },
{
type: "permission.asked",
data: {
id: "per_1",
sessionID: sessionId,
action: "edit",
resources: ["a.txt"],
save: ["*"],
metadata: { files: [{ file: "a.txt", patch }] },
source: { type: "tool", messageID: message, id: "call_2" },
},
},
);
await until(() => of(emitted, "approval").length);
const approval = of(emitted, "approval")[0];
assert.equal(approval?.title, "Edit a.txt");
assert.deepEqual(
approval?.options?.map((option) => option.id),
["once", "always", "reject"],
);
assert.equal(approval?.toolCall?.id, `${message}/call_2`);
assert.deepEqual(approval?.toolCall?.content, [{ type: "diff", path: "a.txt", diff: patch }]);
await run(agent.respondToApproval("per_1", "always"));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/permission/per_1/reply`)?.body, {
decision: "always",
});
// The server's own echo does not resolve the card twice.
server.push({ type: "permission.replied", data: { sessionID: sessionId, requestID: "per_1", reply: "always" } });
await new Promise((resolve) => setTimeout(resolve, 20));
assert.equal(of(emitted, "approval_resolved").length, 1);
});
test("full access answers approvals itself", async () => {
const { agent, server, emitted, run, sessionId } = await chat({ permission_mode: permissionMode.FULL });
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "go" }] }));
server.push({
type: "permission.asked",
data: { id: "per_2", sessionID: sessionId, action: "external_directory", resources: ["/etc"] },
});
await until(() => server.find("POST", `/api/session/${sessionId}/permission/per_2/reply`));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/permission/per_2/reply`)?.body, {
decision: "once",
});
assert.equal(of(emitted, "approval").length, 0);
});
test("a form is a question card, answered or dismissed", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "ask me" }] }));
const form = (id: string) => ({
id,
sessionID: sessionId,
title: "Questions",
fields: [
{
key: "q0",
type: "string",
title: "Color",
description: "Which color?",
options: [
{ value: "red", label: "red" },
{ value: "blue", label: "blue" },
],
custom: true,
},
],
});
server.push(
{ type: "form.created", data: { form: form("frm_1") } },
{ type: "form.created", data: { form: form("frm_2") } },
);
await until(() => of(emitted, "question").length === 2);
assert.deepEqual(of(emitted, "question")[0]?.fields, [
{
id: "q0",
label: "Color",
description: "Which color?",
kind: "select",
options: [
{ value: "red", label: "red" },
{ value: "blue", label: "blue" },
],
allowOther: true,
},
]);
await run(agent.respondToQuestion("frm_1", { values: { q0: "blue" }, cancelled: false }));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/form/frm_1/reply`)?.body, { answer: { q0: "blue" } });
await run(agent.respondToQuestion("frm_2", { values: {}, cancelled: true }));
assert.ok(server.find("DELETE", `/api/session/${sessionId}/form/frm_2`));
assert.equal(of(emitted, "question_resolved").length, 2);
});
test("a subagent's events go to the chat, under the call that started it", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
const { runId } = await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "delegate" }] }));
const inMessage = { sessionID: sessionId, assistantMessageID: message };
const input = { agent: "explore", description: "Count files", prompt: "Count the files" };
server.push(
{ type: "session.tool.input.started", data: { ...inMessage, id: "call_3", name: "subagent" } },
{ type: "session.tool.called", data: { ...inMessage, id: "call_3", input } },
{
type: "session.created",
data: { sessionID: "ses_child", parentID: sessionId, title: "Count files", agent: "explore" },
},
{
type: "session.tool.progress",
data: { ...inMessage, id: "call_3", metadata: { sessionID: "ses_child", status: "running" } },
},
{ type: "session.execution.started", data: { sessionID: "ses_child" } },
{
type: "session.text.ended",
data: { sessionID: "ses_child", assistantMessageID: "msg_c", ordinal: 0, text: "19 files" },
},
{ type: "session.execution.succeeded", data: { sessionID: "ses_child" } },
{
type: "session.tool.success",
data: {
...inMessage,
id: "call_3",
content: [{ type: "text", text: '<subagent sessionID="ses_child" state="completed">\n19 files\n</subagent>' }],
metadata: { sessionID: "ses_child", status: "completed" },
},
},
);
await until(() => of(emitted, "task").some((task) => task.summary === "19 files"));
const first = of(emitted, "task")[0];
assert.equal(first?.id, "ses_child");
assert.equal(first?.title, "Count files");
assert.equal(first?.toolCallId, `${message}/call_3`);
assert.equal(first?.prompt, "Count the files");
assert.equal(first?.runId, runId);
const text = of(emitted, "text_delta").find((event) => event.text === "19 files");
assert.equal(text?.sessionId, sessionId);
assert.equal(text?.taskId, "ses_child");
assert.ok(of(emitted, "task").some((task) => task.status === "completed"));
assert.equal(of(emitted, "run_finished").length, 0, "the child's end is not the chat's");
});
test("a prompt during a run steers it", async () => {
const { agent, server, run, sessionId } = await chat();
const first = await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "one" }] }));
const second = await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "two" }] }));
assert.equal(second.runId, first.runId);
const prompts = server.calls.filter((call) => call.path === `/api/session/${sessionId}/prompt`);
assert.deepEqual(prompts[1]?.body, { text: "two", delivery: "steer" });
});
test("preparation failure cannot leave a phantom native run or turn the retry into a steer", async () => {
let unavailable = true;
const { agent, server, run, sessionId } = await chat(
{},
{
routes: (call) =>
call.path === "/api/command" && unavailable ? { status: 503, body: "catalog unavailable" } : undefined,
},
);
await assert.rejects(run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "first" }] })));
assert.equal(agent.runs.has(sessionId), false);
assert.equal(server.calls.filter((call) => call.path.endsWith("/prompt")).length, 0);
unavailable = false;
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "retry" }] }));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/prompt`)?.body, { text: "retry" });
});
test("native admission and correlated promotion remain distinct and consume each input once", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
const first = await run(
agent.prompt(sessionId, {
blocks: [{ type: "text", text: "one" }],
delivery: { inputId: "logical-a", attemptId: "attempt-a", intent: "send" },
}),
);
const steer = await run(
agent.prompt(sessionId, {
blocks: [{ type: "text", text: "two" }],
delivery: { inputId: "logical-b", attemptId: "attempt-b", intent: "steer" },
}),
);
assert.equal(steer.runId, first.runId);
assert.equal(first.receipt?.evidence, "native_admission");
assert.equal(steer.receipt?.nativeInputId, "msg_logical-b");
assert.equal(of(emitted, "input_consumed").length, 0, "POST admission is not model pickup");
server.push(
{ type: "session.inbox.delivered", data: { sessionID: sessionId, inboxID: "unrelated" } },
{ type: "session.inbox.delivered", data: { sessionID: sessionId, inboxID: "msg_logical-a" } },
{ type: "session.input.promoted", data: { sessionID: sessionId, inputID: "msg_logical-b" } },
{ type: "session.inbox.delivered", data: { sessionID: sessionId, inboxID: "msg_logical-b" } },
);
await until(() => of(emitted, "input_consumed").length === 2);
assert.deepEqual(
of(emitted, "input_consumed").map((event) => [event.inputId, event.runId]),
[
["logical-a", first.runId],
["logical-b", first.runId],
],
);
assert.equal(of(emitted, "run_finished").length, 0, "pickup cannot end the active execution");
});
test("native steer refusal preserves the original execution and transport/identity uncertainty never claims rejection", async () => {
let failure = "";
const { agent, server, emitted, run, sessionId } = await chat(
{},
{
routes: (call) => {
if (!failure || !call.path.endsWith("/prompt")) return;
if (failure === "invalid")
return { status: 400, body: { _tag: "InvalidRequestError", message: "not admitted" } };
if (failure === "uncertain") return { status: 500, body: "connection lost after dispatch" };
return {
body: {
data: {
id: "wrong-id",
sessionID: "wrong-session",
type: "user",
time: { created: 1 },
payload: {},
delivery: "steer",
},
},
};
},
},
);
const first = await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "one" }] }));
for (const kind of ["invalid", "uncertain", "identity"]) {
failure = kind;
await assert.rejects(
run(
agent.prompt(sessionId, {
blocks: [{ type: "text", text: kind }],
delivery: { inputId: kind, attemptId: `attempt-${kind}`, intent: "steer" },
}),
),
);
assert.equal(agent.runs.get(sessionId), first.runId);
assert.equal(of(emitted, "run_finished").length, 0);
}
assert.deepEqual(
of(emitted, "input_rejected").map((event) => event.attemptId),
["attempt-invalid"],
);
const count = server.calls.filter((call) => call.path.endsWith("/prompt")).length;
await assert.rejects(
run(
agent.prompt(sessionId, {
blocks: [{ type: "text", text: "hold" }],
delivery: { inputId: "held", attemptId: "held-attempt", intent: "queue" },
}),
),
/held by the host/,
);
assert.equal(
server.calls.filter((call) => call.path.endsWith("/prompt")).length,
count,
"explicit Queue is not another native inbox",
);
});
test("a changed model, variant, mode and permission mode reach the session before the next prompt", async () => {
const { agent, server, run, sessionId } = await chat();
await run(agent.setOption(sessionId, "model", "opencode/fledge-free"));
await run(agent.setOption(sessionId, "variant", "high"));
await run(agent.setOption(sessionId, "agent", "build"));
await run(agent.setOption(sessionId, permissionMode.OPTION_ID, permissionMode.AUTO_EDITS));
assert.deepEqual(server.find("PATCH", `/api/session/${sessionId}`)?.body, {
permissions: ruleset(permissionMode.AUTO_EDITS),
});
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "go" }] }));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/model`)?.body, {
model: { providerID: "opencode", id: "fledge-free", variant: "high" },
});
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/agent`)?.body, { agent: "build" });
});
test("cancel interrupts the session and ends the run once", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "long" }] }));
await run(agent.cancel(sessionId));
assert.ok(server.find("POST", `/api/session/${sessionId}/interrupt`));
server.push({ type: "session.execution.interrupted", data: { sessionID: sessionId } });
await new Promise((resolve) => setTimeout(resolve, 20));
assert.deepEqual(
of(emitted, "run_finished").map((event) => event.outcome),
[{ status: "cancelled" }],
);
});
test("a failed execution ends the run with the server's reason", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "hi" }] }));
server.push({
type: "session.execution.failed",
data: { sessionID: sessionId, error: { type: "provider.no-route", message: "Model unavailable: x/y" } },
});
await until(() => of(emitted, "run_finished").length);
assert.deepEqual(of(emitted, "run_finished")[0]?.outcome, { status: "failed", message: "Model unavailable: x/y" });
assert.equal(of(emitted, "notice")[0]?.message, "Model unavailable: x/y");
});
test("a slash command the server knows goes to the command route", async () => {
const { agent, server, run, sessionId } = await chat();
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "/review the branch" }] }));
await until(() => server.find("POST", `/api/session/${sessionId}/command`));
assert.deepEqual(server.find("POST", `/api/session/${sessionId}/command`)?.body, {
name: "review",
text: "the branch",
});
assert.equal(server.find("POST", `/api/session/${sessionId}/prompt`), undefined);
});
test("a turn the server starts by itself is a run of its own", async () => {
const { server, emitted, sessionId } = await chat();
server.push({ type: "session.execution.started", data: { sessionID: sessionId } });
await until(() => of(emitted, "run_started").length);
server.push({ type: "session.execution.succeeded", data: { sessionID: sessionId } });
await until(() => of(emitted, "run_finished").length);
assert.equal(of(emitted, "run_finished")[0]?.runId, of(emitted, "run_started")[0]?.runId);
});
test("plugin tools are served by the host and added to the workspace", async () => {
const context = harness({
onHost: (method) => (method === "host/mcp.serve" ? { url: "http://127.0.0.1:7000/mcp/abc" } : {}),
});
const tools = [{ name: "lookup", description: "Looks things up", inputSchema: { type: "object" } }];
await context.run(
context.agent.createSession({ workspace: WORKSPACE, options: {}, tools, instructions: "Be brief." }),
);
const put = context.server.find("PUT", "/api/experimental/mcp/convergence");
assert.deepEqual(put?.query, { "location[directory]": WORKSPACE });
assert.deepEqual(put?.body, { config: { type: "remote", url: "http://127.0.0.1:7000/mcp/abc", codemode: false } });
const sessionId = "ses_new1";
await context.run(context.agent.prompt(sessionId, { blocks: [{ type: "text", text: "hi" }] }));
assert.deepEqual(
context.server.find("PUT", `/api/experimental/session/${sessionId}/instructions/entries/convergence`)?.body,
{ value: "Be brief." },
);
// The same tools are not offered twice.
assert.equal(context.server.calls.filter((call) => call.method === "PUT" && call.path.includes("/mcp/")).length, 1);
});
test("a stored session reads back with its subagent nested under the call", async () => {
const time = { created: 1_000 };
const routes = (call: { method: string; path: string }) => {
if (call.path === "/api/session/ses_root/message")
return {
body: {
data: [
{ type: "user", id: "msg_u", time, text: "delegate" },
{
type: "assistant",
id: "msg_a",
time,
agent: "build",
model: { id: "fledge-free", providerID: "opencode" },
content: [
{ type: "reasoning", text: "thinking" },
{
type: "tool",
id: "call_1",
name: "subagent",
time,
state: {
status: "completed",
input: { description: "Count files", prompt: "Count", agent: "explore" },
content: [
{ type: "text", text: '<subagent sessionID="ses_kid" state="completed">\n3\n</subagent>' },
],
metadata: { sessionID: "ses_kid", status: "completed" },
},
},
{ type: "text", text: "Done" },
],
},
{ type: "idle", id: "msg_i", time, outcome: "succeeded" },
],
cursor: { next: null },
},
};
if (call.path === "/api/session/ses_kid/message")
return {
body: {
data: [
{
type: "assistant",
id: "msg_k",
time,
agent: "explore",
model: { id: "fledge-free", providerID: "opencode" },
content: [{ type: "text", text: "3" }],
},
],
},
};
return undefined;
};
const { agent, run } = harness({ routes });
const { items } = await run(agent.readSession("ses_root"));
assert.deepEqual(
items.map((item) => [item.id, item.role]),
[
["msg_u", "user"],
["msg_a/reasoning/0", "reasoning"],
["msg_a/call_1", "tool"],
["ses_kid", "task"],
["msg_a/text/0", "assistant"],
],
);
const task = items[3];
assert.ok(task?.role === "task");
assert.equal(task.task.summary, "3");
assert.equal(task.task.toolCallId, "msg_a/call_1");
assert.deepEqual(
task.items?.map((item) => item.id),
["msg_k/text/0"],
);
});
test("sessions are listed newest first, without subagent sessions", async () => {
const routes = (call: { path: string }) =>
call.path === "/api/session"
? {
body: {
data: [
session("ses_old", { title: "Old", time: { created: 1, updated: 10 } }),
session("ses_new", { title: "New", time: { created: 2, updated: 20 } }),
session("ses_kid", { parentID: "ses_new", time: { created: 3, updated: 30 } }),
],
},
}
: undefined;
const { agent, run, server } = harness({ routes });
const { sessions } = await run(agent.listSessions(WORKSPACE));
assert.deepEqual(
sessions.map((entry) => entry.id),
["ses_new", "ses_old"],
);
assert.deepEqual(server.find("GET", "/api/session")?.query, { directory: WORKSPACE, limit: "200", parentID: "null" });
});
test("an unreachable server is reported as unavailable", async () => {
const { agent, run } = harness({
routes: () => ({ status: 401, body: { _tag: "UnauthorizedError", message: "no" } }),
});
const info = await run(agent.initialize());
assert.equal(info.status?.state, "auth_required");
});
test("options wait for a catalog the server is still building", async () => {
let ready = false;
const { agent, run, server } = harness({
routes: (call) => (call.path === "/api/provider" && !ready ? { body: { data: [] } } : undefined),
});
const listed = run(agent.listOptions(WORKSPACE));
await until(() => server.calls.some((call) => call.path === "/api/provider"));
ready = true;
server.push({ type: "provider.updated", location: { directory: WORKSPACE }, data: {} });
const { options } = await listed;
assert.equal(options[0]?.value, "opencode/fledge-free");
});
test("a catalog change sends the folder's chats their options again", async () => {
const { server, emitted, sessionId } = await chat();
server.push({ type: "model.updated", location: { directory: WORKSPACE }, data: {} });
await until(() => of(emitted, "config_options").length);
assert.equal(of(emitted, "config_options")[0]?.sessionId, sessionId);
});
test("message pages follow the cursor without an order", async () => {
const time = { created: 1 };
const page = (from: number, count: number) =>
Array.from({ length: count }, (_, at) => ({ type: "user", id: `msg_${from + at}`, time, text: "x" }));
const routes = (call: { path: string; query: Record<string, string> }) => {
if (call.path !== "/api/session/ses_long/message") return undefined;
if (!call.query["cursor"]) return { body: { data: page(0, 200), cursor: { next: "c1" } } };
return { body: { data: page(200, 5), cursor: { next: "c2" } } };
};
const { agent, run, server } = harness({ routes });
const { items } = await run(agent.readSession("ses_long"));
assert.equal(items.length, 205);
const pages = server.calls.filter((call) => call.path === "/api/session/ses_long/message");
assert.deepEqual(
pages.map((call) => call.query),
[
{ order: "asc", limit: "200" },
{ cursor: "c1", limit: "200" },
],
);
});
test("a background subagent ends when its result arrives as a synthetic message", async () => {
const { agent, server, emitted, run, sessionId } = await chat();
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "delegate" }] }));
const inMessage = { sessionID: sessionId, assistantMessageID: message };
const input = { agent: "explore", description: "Scan", prompt: "Scan", background: true };
server.push(
{ type: "session.tool.input.started", data: { ...inMessage, id: "call_4", name: "subagent" } },
{ type: "session.tool.called", data: { ...inMessage, id: "call_4", input } },
{ type: "session.created", data: { sessionID: "ses_bg", parentID: sessionId, title: "Scan" } },
{
type: "session.tool.success",
data: {
...inMessage,
id: "call_4",
content: [{ type: "text", text: "The subagent is working in the background" }],
metadata: { sessionID: "ses_bg", status: "running" },
},
},
{
type: "session.synthetic",
data: {
sessionID: sessionId,
text: '<subagent sessionID="ses_bg" state="completed" description="Scan">\nAll clear\n</subagent>',
metadata: { source: "subagent", childID: "ses_bg", state: "completed" },
},
},
);
await until(() => of(emitted, "task").some((task) => task.status === "completed"));
const tasks = of(emitted, "task");
assert.equal(tasks[0]?.background, true);
assert.ok(!tasks.some((task) => task.summary?.includes("working in the background")));
assert.equal(tasks.at(-1)?.summary, "All clear");
});
test("a session the host creates without tools or instructions takes prompts", async () => {
// The host leaves out an empty tool list and absent instructions; every
// chat in the app once failed with "cannot read property 'length' of
// undefined".
const { agent, server, run } = harness();
const { sessionId } = await run(agent.createSession({ workspace: WORKSPACE, options: {} }));
await run(agent.prompt(sessionId, { blocks: [{ type: "text", text: "hi" }] }));
assert.ok(server.find("POST", `/api/session/${sessionId}/prompt`));
assert.equal(server.find("PUT", "/api/experimental/mcp/convergence"), undefined, "no tools server without tools");
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.1.0latest | Oct 5, 2026 | >=2 <3 | 45.1 KB | 4 permissions | Listed |
No comments yet.