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

  • Provide agents agents.provideMediumAdds agents to the app.Provide the OpenCode 2 agent, and serve plugin tools to it through the host's loopback MCP server
  • Run named programs processMediumStarts the listed programs.Run the OpenCode 2 server (`opencode serve`, or the binary you choose in Settings) and read its versionPrograms: opencode${settings.binaryPath}
  • Network access netMediumConnects to the listed hosts.Talk to the OpenCode server it starts on this computer, at the port it picks for each launch, and to the external server you choose in SettingsHosts: localhost:*${settings.serverUrl}
  • Environment variables envMediumReads the listed environment variables.Read the user name and password of the OpenCode serverVariables: OPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORDCONVERGENCE_OPENCODE_SERVER_PASSWORD

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

VersionPublishedPlugin APISizePermissionsStatus
0.1.0latestOct 5, 2026>=2 <345.1 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.