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

  • Read files fs.readMediumReads files in the listed places.Read, once, the servers the previous OpenCode provider keptPlaces: its own data folder
  • Provide agents agents.provideMediumAdds agents to the app.Provide the OpenCode agent, and serve plugin tools to it through the host's loopback MCP server
  • Run named programs processMediumStarts the listed programs.Run the OpenCode server (`opencode serve`, or the binary you choose in Settings), read its catalog from the command line when the server cannot answer, upgrade it (`opencode upgrade`), and ask or tell the npm installation that owns it about a newer versionPrograms: opencodenpm${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.Expand ~ in a configured binary, and read the OpenCode settings you set in the environment: the binary to run, an external server's address, and the server's user name and passwordVariables: HOMEOPENCODE_PATHOPENCODE_SERVER_URLOPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORDCONVERGENCE_OPENCODE_SERVER_PASSWORD

Files

map.test.ts30.8 KB
import { test } from "node:test";
import assert from "node:assert/strict";
import {
  Mapper,
  addUsage,
  approval,
  firstLine,
  parseTaskOutput,
  questionId,
  taskTitle,
  transcriptTree,
} from "./map.ts";
import type { Mapped, Part, TaskLink } from "./map.ts";
import { obj, arr } from "./map.ts";

const part = (sessionID: string, value: Part) => ({
  type: "message.part.updated",
  properties: { sessionID, part: value },
});

/// The single mapped record of a scenario.
function first(mapped: Mapped[]): Record<string, unknown> {
  return obj(mapped[0]);
}

/// The agent event of a single-event mapping.
function eventOf(mapped: Mapped[]): Record<string, unknown> {
  return obj(first(mapped)["event"]);
}

test("text deltas accumulate byte for byte, keyed by the part", () => {
  const mapper = new Mapper();
  const delta = (text: string) =>
    mapper.event({
      type: "message.part.delta",
      properties: { sessionID: "ses_1", messageID: "msg_a", partID: "prt_1", field: "text", delta: text },
    });
  assert.deepEqual(delta("Hello"), [
    { type: "event", session: "ses_1", event: { event: "text_delta", itemId: "prt_1", text: "Hello", mode: "append" } },
  ]);
  assert.equal(eventOf(delta(" "))["text"], " ", "whitespace is kept");
});

test("user message parts are not assistant text", () => {
  const mapper = new Mapper();
  mapper.event({ type: "message.updated", properties: { info: { id: "msg_user", role: "user" } } });
  assert.deepEqual(
    mapper.event(part("ses_1", { type: "text", id: "prt_u", messageID: "msg_user", sessionID: "ses_1", text: "hi" })),
    [],
  );
  assert.deepEqual(
    mapper.event({
      type: "message.part.delta",
      properties: { sessionID: "ses_1", messageID: "msg_user", partID: "prt_u", field: "text", delta: "hi" },
    }),
    [],
  );
});

test("assistant updates retain native parent identity even without model metadata", () => {
  const mapper = new Mapper();
  const info = { id: "msg_a", sessionID: "ses_1", role: "assistant", parentID: "msg_u" };
  assert.deepEqual(mapper.event({ type: "message.updated", data: { info } }), [
    { type: "assistant", session: "ses_1", parent: "msg_u", provider: null, model: null, variant: null },
  ]);
  const aborted = mapper.event({
    type: "message.updated",
    properties: { info: { ...info, error: { name: "MessageAbortedError" } } },
  });
  assert.equal(first(aborted)["parent"], null, "an aborted snapshot is not pickup evidence");
  assert.deepEqual(mapper.event({ type: "message.updated", properties: { info: { ...info, role: "system" } } }), []);
  assert.equal(
    first(mapper.event({ type: "message.updated", properties: { info: { ...info, parentID: 1 } } }))["parent"],
    null,
  );
});

test("a reasoning snapshot replaces", () => {
  const mapped = new Mapper().event(
    part("ses_1", { type: "reasoning", id: "prt_r", messageID: "msg_a", sessionID: "ses_1", text: "thinking" }),
  );
  assert.deepEqual(first(mapped)["event"], {
    event: "reasoning_delta",
    itemId: "prt_r",
    text: "thinking",
    mode: "replace",
  });
});

test("a command tool reports its terminal output and exit code", () => {
  const mapper = new Mapper();
  const running = mapper.event(
    part("ses_1", {
      type: "tool",
      id: "prt_b",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "call_1",
      tool: "bash",
      state: { status: "running", input: { command: "echo hi-there" }, time: { start: 1 } },
    }),
  );
  assert.equal(eventOf(running)["event"], "tool_call_started");
  assert.equal(eventOf(running)["kind"], "execute");
  assert.equal(eventOf(running)["status"], "running");
  assert.equal(eventOf(running)["title"], "echo hi-there");
  const completed = mapper.event(
    part("ses_1", {
      type: "tool",
      id: "prt_b",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "call_1",
      tool: "bash",
      state: {
        status: "completed",
        input: { command: "echo hi-there" },
        output: "hi-there\n",
        metadata: { output: "hi-there\n", exit: 0 },
        title: "echo hi-there",
      },
    }),
  );
  assert.equal(eventOf(completed)["event"], "tool_call_updated");
  assert.equal(eventOf(completed)["status"], "completed");
  assert.deepEqual(eventOf(completed)["content"], [
    { type: "terminal", command: "echo hi-there", output: "hi-there\n", exitCode: 0 },
  ]);
});

test("a file edit carries the unified diff and its location", () => {
  const diff = "@@ -1,2 +1,3 @@\n hello\n world\n+third\n";
  const mapped = new Mapper().event(
    part("ses_1", {
      type: "tool",
      id: "prt_e",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "call_2",
      tool: "edit",
      state: {
        status: "completed",
        input: { filePath: "/w/a.txt", oldString: "world", newString: "world\nthird" },
        output: "Edit applied successfully.",
        metadata: { diff },
        title: "a.txt",
      },
    }),
  );
  const call = eventOf(mapped);
  assert.equal(call["kind"], "edit");
  assert.deepEqual(call["locations"], [{ path: "/w/a.txt" }]);
  assert.deepEqual(call["content"], [{ type: "diff", path: "/w/a.txt", diff }]);
});

test("a plugin tool served by the host keeps its full name and is titled by its own", () => {
  const mapped = new Mapper().event(
    part("ses_1", {
      type: "tool",
      id: "prt_t",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "c",
      tool: "convergence_lucky_number",
      state: { status: "completed", input: {}, output: "The lucky number is 7319.", title: "" },
    }),
  );
  const call = eventOf(mapped);
  assert.equal(call["name"], "convergence_lucky_number");
  assert.equal(call["title"], "lucky_number");
  assert.equal(call["kind"], "other");
  assert.deepEqual(call["content"], [{ type: "text", text: "The lucky number is 7319." }]);
});

test("replies made in another client resolve the pending request", () => {
  const mapper = new Mapper();
  assert.deepEqual(
    mapper.event({ type: "permission.replied", properties: { sessionID: "ses_1", requestID: "per_1", reply: "once" } }),
    [{ type: "resolved", session: "ses_1", approval: "per_1" }],
  );
  assert.deepEqual(
    mapper.event({ type: "question.replied", properties: { sessionID: "ses_1", requestID: "que_1", answers: [] } }),
    [{ type: "resolved", session: "ses_1", question: "que_1" }],
  );
  assert.deepEqual(
    mapper.event({ type: "question.rejected", properties: { sessionID: "ses_1", requestID: "que_2" } }),
    [{ type: "resolved", session: "ses_1", question: "que_2" }],
  );
});

test("placeholder titles are not forwarded; real ones are", () => {
  const mapper = new Mapper();
  const update = (title: unknown) =>
    mapper.event({ type: "session.updated", properties: { sessionID: "ses_1", info: { id: "ses_1", title } } });
  assert.deepEqual(update("New session - 2026-09-16T09:01:02.003Z"), []);
  assert.deepEqual(update("Child session - 2026-09-16T09:01:02.003Z"), []);
  assert.deepEqual(update("New Chat"), []);
  assert.deepEqual(update("  "), []);
  assert.deepEqual(update(null), []);
  assert.deepEqual(update("Fix the parser"), [
    { type: "event", session: "ses_1", event: { event: "session_info", title: "Fix the parser" } },
  ]);
  // A real title that merely starts like the placeholder is kept.
  assert.equal(eventOf(update("New session - retry the import"))["title"], "New session - retry the import");
});

test("a permission becomes an approval with the native options", () => {
  const mapped = new Mapper().event({
    type: "permission.asked",
    properties: {
      id: "per_1",
      sessionID: "ses_1",
      permission: "bash",
      patterns: ["echo permtest"],
      metadata: { command: "echo permtest" },
      always: ["echo *"],
      tool: { messageID: "msg_a", callID: "call_3" },
    },
  });
  const firstApproval = first(mapped);
  assert.equal(firstApproval["session"], "ses_1");
  const request = obj(firstApproval["request"]);
  assert.equal(request["id"], "per_1");
  assert.equal(request["title"], "bash echo permtest");
  assert.deepEqual(
    arr(request["options"]).map((option: unknown) => [obj(option)["id"], obj(option)["kind"]]),
    [
      ["once", "allow_once"],
      ["always", "allow_always"],
      ["reject", "reject_once"],
    ],
  );
});

test("a permission without always patterns offers no always option", () => {
  const mapped = new Mapper().event({
    type: "permission.asked",
    properties: {
      id: "per_2",
      sessionID: "ses_1",
      permission: "external_directory",
      patterns: ["/etc"],
      metadata: {},
      always: [],
    },
  });
  const onlyRequest = obj(first(mapped)["request"]);
  assert.deepEqual(
    arr(onlyRequest["options"]).map((option: unknown) => obj(option)["id"]),
    ["once", "reject"],
  );
  assert.equal(onlyRequest["title"], "external_directory /etc");
});

test("a question becomes a structured field", () => {
  const mapped = new Mapper().event({
    type: "question.asked",
    properties: {
      id: "que_1",
      sessionID: "ses_1",
      questions: [
        {
          question: "Which database?",
          header: "Database",
          multiple: false,
          custom: true,
          options: [
            { label: "Postgres", description: "Relational" },
            { label: "SQLite", description: "Embedded" },
          ],
        },
      ],
    },
  });
  const question = obj(first(mapped)["request"]);
  assert.equal(question["id"], "que_1");
  assert.equal(question["message"], "Which database?");
  const field = obj(arr(question["fields"])[0]);
  assert.equal(field["kind"], "select");
  assert.equal(field["allowOther"], true);
  assert.equal(field["description"], "Database");
  assert.deepEqual(
    arr(field["options"]).map((option: unknown) => obj(option)["value"]),
    ["Postgres", "SQLite"],
  );
});

test("idle and error drive the run outcome", () => {
  const mapper = new Mapper();
  assert.deepEqual(
    mapper.event({
      type: "session.error",
      properties: { sessionID: "ses_1", error: { name: "APIError", data: { message: "Forbidden: not licensed" } } },
    }),
    [{ type: "failed", session: "ses_1", message: "Forbidden: not licensed" }],
  );
  assert.deepEqual(mapper.event({ type: "session.idle", properties: { sessionID: "ses_1" } }), [
    { type: "idle", session: "ses_1" },
  ]);
});

test("usage comes from the step totals", () => {
  const mapped = new Mapper().event(
    part("ses_1", {
      type: "step-finish",
      id: "prt_s",
      messageID: "msg_a",
      sessionID: "ses_1",
      tokens: { total: 22145, input: 20306, output: 47, reasoning: 0, cache: { read: 1792, write: 0 } },
      cost: 0.5,
    }),
  );
  // The breakdown is what the request cost; it need not add up to the
  // context.
  assert.deepEqual(mapped, [
    {
      type: "event",
      session: "ses_1",
      event: {
        event: "usage",
        usedTokens: 22145,
        inputTokens: 20306,
        cachedInputTokens: 1792,
        outputTokens: 47,
        reasoningTokens: 0,
        costUsd: 0.5,
      },
    },
  ]);
});

test("a session with a parent is announced as a subagent", () => {
  const mapper = new Mapper();
  const mapped = mapper.event({
    type: "session.created",
    properties: {
      sessionID: "ses_child",
      info: { id: "ses_child", parentID: "ses_1", title: "Search the tests", agent: "explore" },
    },
  });
  assert.deepEqual(mapped, [
    { type: "child", child: { id: "ses_child", parent: "ses_1", title: "Search the tests", agent: "explore" } },
    { type: "event", session: "ses_child", event: { event: "session_info", title: "Search the tests" } },
  ]);
  const topLevel = mapper.event({
    type: "session.created",
    properties: { sessionID: "ses_2", info: { id: "ses_2", title: "Fix the parser" } },
  });
  assert.ok(!topLevel.some((entry) => entry.type === "child"));
  // The placeholder a fresh child is minted with is still a placeholder.
  const placeholder = mapper.event({
    type: "session.created",
    properties: {
      sessionID: "ses_c2",
      info: { id: "ses_c2", parentID: "ses_1", title: "Child session - 2026-09-16T09:01:02.003Z", agent: "general" },
    },
  });
  assert.deepEqual(placeholder, [
    { type: "child", child: { id: "ses_c2", parent: "ses_1", title: null, agent: "general" } },
  ]);
});

test("compaction is reported so the transcript can show the boundary", () => {
  assert.deepEqual(new Mapper().event({ type: "session.compacted", properties: { sessionID: "ses_1" } }), [
    { type: "event", session: "ses_1", event: { event: "compacted" } },
  ]);
});

test("an approval for a call without arguments yet shows the requested command", () => {
  const mapper = new Mapper();
  mapper.event(
    part("ses_1", {
      type: "tool",
      id: "prt_b",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "call_1",
      tool: "bash",
      state: { status: "pending", input: {}, raw: "" },
    }),
  );
  const mapped = mapper.event({
    type: "permission.asked",
    properties: {
      id: "per_1",
      sessionID: "ses_1",
      permission: "bash",
      patterns: ["wc -l a.txt b.txt"],
      metadata: {},
      always: ["wc *"],
      tool: { messageID: "msg_a", callID: "call_1" },
    },
  });
  const announced = obj(first(mapped)["request"]);
  assert.equal(announced["title"], "bash wc -l a.txt b.txt");
  const announcedCall = obj(announced["toolCall"]);
  assert.equal(announcedCall["id"], "prt_b", "the announced call is still the one shown");
  assert.equal(announcedCall["title"], "wc -l a.txt b.txt");
  assert.deepEqual(announcedCall["content"], [{ type: "terminal", command: "wc -l a.txt b.txt", output: "" }]);
});

test("an approval carries the tool call it blocks, and a finished call is no longer held", () => {
  const mapper = new Mapper();
  mapper.event(
    part("ses_1", {
      type: "tool",
      id: "prt_b",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "call_1",
      tool: "bash",
      state: { status: "pending", input: { command: "rm -rf build" } },
    }),
  );
  const held = obj(
    first(
      mapper.event({
        type: "permission.asked",
        properties: {
          id: "per_1",
          sessionID: "ses_1",
          permission: "bash",
          patterns: ["rm *"],
          metadata: { command: "rm -rf build" },
          always: ["rm *"],
          tool: { messageID: "msg_a", callID: "call_1" },
        },
      }),
    )["request"],
  );
  const heldCall = obj(held["toolCall"]);
  assert.equal(heldCall["id"], "prt_b");
  assert.equal(heldCall["name"], "bash");
  assert.equal(obj(heldCall["input"])["command"], "rm -rf build");
  // An `always` grant is remembered against the folder.
  assert.match(String(held["warning"]), /workspace/);
  mapper.event(
    part("ses_1", {
      type: "tool",
      id: "prt_b",
      messageID: "msg_a",
      sessionID: "ses_1",
      callID: "call_1",
      tool: "bash",
      state: { status: "completed", input: { command: "rm -rf build" }, output: "", metadata: { exit: 0 } },
    }),
  );
  assert.equal(mapper.pendingTools.size, 0, "finished tool calls are still being held");
});

test("an unannounced request describes itself from its own fields", () => {
  const mapped = approval(
    {
      id: "per_2",
      sessionID: "ses_1",
      permission: "external_directory",
      patterns: ["/etc", "*"],
      metadata: {},
      always: [],
    },
    null,
  );
  assert.equal(mapped.toolCall.id, "per_2");
  // `*` is the catch-all rule matching, which tells a reader nothing.
  assert.equal(mapped.toolCall.title, "external directory /etc");
  assert.equal(mapped.warning, undefined, "nothing would be remembered, so nothing to warn about");
  // A plugin tool's request is named by the tool, not the rule name.
  const tool = approval(
    {
      id: "per_3",
      sessionID: "ses_1",
      permission: "convergence_lucky_number",
      patterns: ["*"],
      metadata: {},
      always: ["*"],
    },
    null,
  );
  assert.equal(tool.title, "convergence_lucky_number");
  assert.equal(tool.toolCall.title, "lucky_number");
});

test("question ids are stable names, not positions", () => {
  assert.equal(questionId(0, "Database"), "question-0-database");
  assert.equal(questionId(1, "  Migrate now?  "), "question-1-migrate-now");
  assert.equal(questionId(2, "build_target"), "question-2-build_target");
  assert.equal(questionId(3, "Café"), "question-3-caf");
  assert.equal(questionId(4, ""), "question-4");
  assert.equal(questionId(5, "???"), "question-5");
  assert.notEqual(questionId(0, "Which"), questionId(1, "Which"));
  const named = obj(
    first(
      new Mapper().event({
        type: "question.asked",
        properties: {
          id: "que_1",
          sessionID: "ses_1",
          questions: [
            { question: "Which database?", header: "Database", options: [] },
            { question: "Run it now?", header: "", options: [] },
          ],
        },
      }),
    )["request"],
  );
  assert.deepEqual(
    arr(named["fields"]).map((field: unknown) => obj(field)["id"]),
    ["question-0-database", "question-1"],
  );
});

test("todos become a plan", () => {
  const mapped = new Mapper().event({
    type: "todo.updated",
    properties: {
      sessionID: "ses_1",
      todos: [
        { content: "read the file", status: "completed", priority: "high" },
        { content: "write the fix", status: "in_progress", priority: "high" },
      ],
    },
  });
  assert.deepEqual(first(mapped)["event"], {
    event: "plan",
    entries: [
      { content: "read the file", status: "completed" },
      { content: "write the fix", status: "in_progress" },
    ],
  });
});

test("unknown events and parts are ignored", () => {
  const mapper = new Mapper();
  assert.deepEqual(mapper.event({ type: "plugin.added", properties: { id: "anthropic" } }), []);
  assert.deepEqual(
    mapper.event(
      part("ses_1", { type: "patch", id: "prt_p", messageID: "msg_a", sessionID: "ses_1", hash: "abc", files: [] }),
    ),
    [],
  );
  assert.deepEqual(mapper.event({ type: "server.heartbeat", properties: {} }), []);
});

test("history maps to transcript items", () => {
  const messages = [
    {
      info: { id: "msg_u", role: "user", sessionID: "ses_1", time: { created: 1700000000000 } },
      parts: [
        { type: "text", id: "prt_1", messageID: "msg_u", sessionID: "ses_1", text: "hello" },
        { type: "text", id: "prt_2", messageID: "msg_u", sessionID: "ses_1", text: "injected", synthetic: true },
      ],
    },
    {
      info: { id: "msg_a", role: "assistant", sessionID: "ses_1", time: { created: 1700000001000 } },
      parts: [
        { type: "reasoning", id: "prt_3", messageID: "msg_a", sessionID: "ses_1", text: "why" },
        {
          type: "tool",
          id: "prt_4",
          messageID: "msg_a",
          sessionID: "ses_1",
          callID: "c",
          tool: "read",
          state: {
            status: "completed",
            input: { filePath: "/w/a.txt" },
            output: "body",
            metadata: { preview: "body" },
            title: "a.txt",
          },
        },
        { type: "text", id: "prt_5", messageID: "msg_a", sessionID: "ses_1", text: "done" },
      ],
    },
  ];
  const items = transcriptTree("ses_1", new Map([["ses_1", { messages, children: [] }]]));
  assert.equal(items.length, 4);
  assert.deepEqual(items[0], {
    id: "msg_u",
    createdAt: "2023-11-14T22:13:20.000Z",
    role: "user",
    blocks: [{ type: "text", text: "hello" }],
  });
  assert.equal(obj(items[1])["role"], "reasoning");
  assert.equal(obj(items[1])["text"], "why");
  assert.equal(obj(items[2])["role"], "tool");
  assert.equal(obj(obj(items[2])["call"])["kind"], "read");
  assert.equal(obj(items[3])["role"], "assistant");
  assert.equal(obj(items[3])["text"], "done");
});

function taskPart(status: string, extra: Record<string, unknown> = {}): Part {
  const state = {
    status,
    input: {
      description: "Find the parser",
      prompt: "Look for the parser and report its path.",
      subagent_type: "explore",
    },
    title: "Find the parser",
    metadata: {
      parentSessionId: "ses_1",
      sessionId: "ses_child",
      model: { providerID: "opencode", modelID: "big-pickle" },
    },
    time: { start: 1700000000000 },
    ...extra,
  };
  return {
    type: "tool",
    id: "prt_task",
    messageID: "msg_a",
    sessionID: "ses_1",
    callID: "call_t",
    tool: "task",
    state,
  };
}

const links = (mapped: Mapped[]): TaskLink[] =>
  mapped.flatMap((entry) => (entry.type === "task_link" ? [entry.link] : []));

test("a running task call links its subagent session", () => {
  const mapper = new Mapper();
  const mapped = mapper.event(part("ses_1", taskPart("running")));
  assert.equal(eventOf(mapped)["event"], "tool_call_started");
  assert.equal(eventOf(mapped)["kind"], "task");
  const link = links(mapped)[0];
  assert.equal(link.child, "ses_child");
  assert.equal(link.parent, "ses_1");
  assert.equal(link.toolCallId, "prt_task", "the transcript knows the call by its part id");
  assert.equal(link.title, "Find the parser");
  assert.equal(link.prompt, "Look for the parser and report its path.");
  assert.equal(link.agent, "explore");
  assert.equal(link.model, "big-pickle");
  assert.equal(link.background, false);
  assert.equal(link.startedAt, new Date(1700000000000).toISOString());
  assert.equal(link.outcome, null);
  // Before the server names the child there is nothing to link.
  assert.deepEqual(links(mapper.event(part("ses_1", taskPart("pending", { metadata: {} })))), []);
});

test("a finished task call carries the answer without its wrapper", () => {
  const mapper = new Mapper();
  const output =
    '<task id="ses_child" state="completed">\n<task_result>\nThe parser is in src/parse.rs.\n</task_result>\n</task>';
  const link = links(
    mapper.event(part("ses_1", taskPart("completed", { output, time: { start: 1700000000000, end: 1700000005000 } }))),
  )[0];
  assert.deepEqual(link.outcome, {
    child: "ses_child",
    status: "completed",
    summary: "The parser is in src/parse.rs.",
  });
  assert.equal(link.endedAt, new Date(1700000005000).toISOString());
  assert.equal(
    links(mapper.event(part("ses_1", taskPart("error", { error: "Task cancelled" }))))[0]?.outcome?.status,
    "cancelled",
  );
  assert.equal(
    links(
      mapper.event(part("ses_1", taskPart("error", { error: "Subagent failed (task_id: ses_child): rate limited" }))),
    )[0]?.outcome?.status,
    "failed",
  );
});

test("a background task ends with the injected report", () => {
  const mapper = new Mapper();
  const started =
    '<task id="ses_child" state="running">\n<summary>Background task started</summary>\n<task_result>\nThe task is working in the background.\n</task_result>\n</task>';
  const link = links(
    mapper.event(
      part(
        "ses_1",
        taskPart("completed", {
          output: started,
          metadata: {
            parentSessionId: "ses_1",
            sessionId: "ses_child",
            background: true,
            jobId: "ses_child",
            model: { providerID: "opencode", modelID: "big-pickle" },
          },
        }),
      ),
    ),
  )[0];
  assert.equal(link.background, true);
  assert.equal(link.outcome, null, "the subagent is still running");
  mapper.event({ type: "message.updated", properties: { info: { id: "msg_u2", role: "user" } } });
  const report =
    '<task id="ses_child" state="completed">\n<summary>Background task completed: Find the parser</summary>\n<task_result>\nFound it.\n</task_result>\n</task>';
  assert.deepEqual(
    mapper.event(
      part("ses_1", {
        type: "text",
        id: "prt_r",
        messageID: "msg_u2",
        sessionID: "ses_1",
        text: report,
        synthetic: true,
      }),
    ),
    [{ type: "task_result", result: { child: "ses_child", status: "completed", summary: "Found it." } }],
  );
  assert.deepEqual(parseTaskOutput(report)?.summary, "Background task completed: Find the parser");
  assert.equal(parseTaskOutput("plain text"), null);
});

test("task titles drop the subagent suffix", () => {
  assert.equal(taskTitle("Find the parser (@explore subagent)"), "Find the parser");
  assert.equal(taskTitle("Find (the) parser"), "Find (the) parser");
  assert.equal(firstLine("\n  Look here\nthen there"), "Look here");
});

test("a step is reported once, and a subagent's usage keeps the latest context and sums the cost", () => {
  const mapper = new Mapper();
  const step = part("ses_1", {
    type: "step-finish",
    id: "prt_s",
    messageID: "msg_a",
    sessionID: "ses_1",
    tokens: { total: 100, input: 90, output: 10 },
    cost: 0.25,
  });
  assert.equal(mapper.event(step).length, 1);
  assert.deepEqual(mapper.event(step), []);
  let total = addUsage(null, { usedTokens: 100, costUsd: 0.25 });
  total = addUsage(total, { usedTokens: 180, costUsd: 0.5 });
  assert.equal(total.usedTokens, 180, "the latest context, not a sum of contexts");
  assert.equal(total.costUsd, 0.75);
  assert.equal("costUsd" in addUsage(null, { usedTokens: 1 }), false);
});

test("history nests each subagent under its task call, at any depth", () => {
  const answer =
    '<task id="ses_child" state="completed">\n<task_result>\nThe parser is in src/parse.rs.\n</task_result>\n</task>';
  const root = [
    {
      info: { id: "msg_u", role: "user", time: { created: 1700000000000 } },
      parts: [{ type: "text", id: "prt_1", messageID: "msg_u", sessionID: "ses_1", text: "find the parser" }],
    },
    {
      info: { id: "msg_a", role: "assistant", time: { created: 1700000001000 } },
      parts: [
        taskPart("completed", { output: answer, time: { start: 1700000001000, end: 1700000009000 } }),
        { type: "text", id: "prt_5", messageID: "msg_a", sessionID: "ses_1", text: "It is in src/parse.rs." },
      ],
    },
  ];
  const child = [
    {
      info: { id: "msg_cu", role: "user", time: { created: 1700000002000 } },
      parts: [
        {
          type: "text",
          id: "prt_c1",
          messageID: "msg_cu",
          sessionID: "ses_child",
          text: "Look for the parser and report its path.",
        },
      ],
    },
    {
      info: {
        id: "msg_ca",
        role: "assistant",
        modelID: "big-pickle",
        variant: "high",
        time: { created: 1700000003000, completed: 1700000008000 },
      },
      parts: [
        {
          type: "tool",
          id: "prt_c2",
          messageID: "msg_ca",
          sessionID: "ses_child",
          callID: "c2",
          tool: "grep",
          state: { status: "completed", input: { pattern: "parse" }, output: "src/parse.rs", title: "parse" },
        },
        {
          type: "tool",
          id: "prt_c3",
          messageID: "msg_ca",
          sessionID: "ses_child",
          callID: "c3",
          tool: "task",
          state: {
            status: "completed",
            input: { description: "Read it", prompt: "Read src/parse.rs", subagent_type: "general" },
            output: '<task id="ses_grand" state="completed">\n<task_result>\nIt parses.\n</task_result>\n</task>',
            metadata: { sessionId: "ses_grand" },
          },
        },
        {
          type: "step-finish",
          id: "prt_c4",
          messageID: "msg_ca",
          sessionID: "ses_child",
          tokens: { total: 5000, input: 4900, output: 100 },
          cost: 0.1,
        },
        {
          type: "text",
          id: "prt_c5",
          messageID: "msg_ca",
          sessionID: "ses_child",
          text: "The parser is in src/parse.rs.",
        },
      ],
    },
  ];
  const grand = [
    {
      info: { id: "msg_ga", role: "assistant", time: { created: 1700000004000, completed: 1700000005000 } },
      parts: [{ type: "text", id: "prt_g1", messageID: "msg_ga", sessionID: "ses_grand", text: "It parses." }],
    },
  ];
  const orphan = {
    id: "ses_orphan",
    parentID: "ses_1",
    title: "Check the lexer (@explore subagent)",
    agent: "explore",
  };
  const sessions = new Map([
    ["ses_1", { messages: root, children: [orphan] }],
    ["ses_child", { messages: child, children: [] }],
    ["ses_grand", { messages: grand, children: [] }],
  ]);
  const items = transcriptTree("ses_1", sessions);
  assert.deepEqual(
    items.map((item) => item.id),
    ["msg_u", "prt_task", "task-ses_child", "prt_5", "task-ses_orphan"],
  );
  const third = items[2];
  assert.equal(third.role, "task");
  if (third.role !== "task") throw new Error("expected a task item");
  const { task, items: nested } = third;
  assert.equal(task.id, "ses_child");
  assert.equal(task.title, "Find the parser");
  assert.equal(task.toolCallId, "prt_task");
  assert.equal(task.parentTaskId, undefined);
  assert.equal(task.status, "completed");
  assert.equal(task.name, "explore");
  assert.equal(task.prompt, "Look for the parser and report its path.");
  assert.equal(task.model, "big-pickle");
  assert.equal(task.effort, "high");
  assert.equal(task.summary, "The parser is in src/parse.rs.");
  assert.equal(task.toolUses, 2);
  assert.equal(task.usage?.usedTokens, 5000);
  assert.equal(task.startedAt, new Date(1700000001000).toISOString());
  assert.equal(task.endedAt, new Date(1700000009000).toISOString());
  assert.deepEqual(
    nested?.map((item) => item.id),
    ["msg_cu", "prt_c2", "prt_c3", "task-ses_grand", "prt_c5"],
  );
  const grandItem = nested?.[3];
  assert.ok(grandItem);
  if (grandItem.role !== "task") throw new Error("expected a task item");
  assert.equal(grandItem.task.parentTaskId, "ses_child");
  assert.equal(grandItem.task.toolCallId, "prt_c3");
  assert.equal(grandItem.task.title, "Read it");
  assert.equal(grandItem.task.summary, "It parses.");
  assert.equal(grandItem.items?.length, 1);
  const orphanItem = items[4];
  if (orphanItem.role !== "task") throw new Error("expected a task item");
  const orphanTask = orphanItem.task;
  assert.equal(orphanTask.title, "Check the lexer");
  assert.equal(orphanTask.toolCallId, undefined);
  assert.equal(orphanTask.name, "explore");
});

test("history takes a background answer from the report", () => {
  const started =
    '<task id="ses_child" state="running">\n<summary>Background task started</summary>\n<task_result>\nworking\n</task_result>\n</task>';
  const report =
    '<task id="ses_child" state="error">\n<summary>Background task failed: Find the parser</summary>\n<task_error>\nrate limited\n</task_error>\n</task>';
  const root = [
    {
      info: { id: "msg_a", role: "assistant", time: { created: 1700000001000 } },
      parts: [taskPart("completed", { output: started, metadata: { sessionId: "ses_child", background: true } })],
    },
    {
      info: { id: "msg_u2", role: "user", time: { created: 1700000009000 } },
      parts: [{ type: "text", id: "prt_r", messageID: "msg_u2", sessionID: "ses_1", text: report, synthetic: true }],
    },
  ];
  const items = transcriptTree("ses_1", new Map([["ses_1", { messages: root, children: [] }]]));
  assert.equal(items.length, 2, "the report is not a user message");
  const backgroundItem = items[1];
  if (backgroundItem.role !== "task") throw new Error("expected a task item");
  const { task, items: nested } = backgroundItem;
  assert.equal(task.background, true);
  assert.equal(task.status, "failed");
  assert.equal(task.summary, "rate limited");
  assert.deepEqual(nested, []);
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <394.8 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.