Official

subagents

Agents start subagents on any of your enabled agents and models, and you can @-tag an agent, model and effort in the composer to hand part of a message to one.

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/subagents@0.1.0

Permissions in 0.1.0

  • Use your agents' models models.useHighRuns prompts through the agents you enabled.Run each subagent through the agent and model it names, on your own accounts
  • Provide agent tools tools.provideMediumGives tools to agents.Give agents the spawn_subagent tool

Files

lib.ts6.1 KB
// The spawn_subagent schema, catalog description and composer mentions.
// Formatting keeps real model identifiers intact in the instructions.
import * as z from "zod";
import type { MentionDefinition, ModelAgent, ModelChoice, SubagentTypeInfo } from "convergence";

export type CatalogAgent = Pick<ModelAgent, "id" | "name" | "models"> & { readonly error?: string };
export type SubagentType = Pick<SubagentTypeInfo, "plugin" | "id" | "title" | "description">;
const agentDescription = "The agent id. Without it, the agent of this chat.";
export const spawnInput = z.looseObject({
  prompt: z.string().describe("The whole task. The subagent does not see this conversation."),
  agent: z.string().describe(agentDescription).optional(),
  model: z.string().describe("A model id the agent offers. Without it, the agent's default.").optional(),
  effort: z.string().describe("A reasoning effort the model offers. Without it, the model's default.").optional(),
  title: z.string().describe("A few words that name the subagent's task for the user.").optional(),
});
export type SpawnInput = z.infer<typeof spawnInput>;

export const TOOL = "spawn_subagent";

// The most tags the composer is offered; a catalog with hundreds of models
// (OpenCode) would otherwise flood every plugin with them.
export const MENTION_LIMIT = 1500;

// The most models of one agent the tool's description names.
const DESCRIBED_MODELS = 40;

// A value as one piece of a tag: no whitespace.
export function tokenPart(value: string) {
  return String(value).trim().replace(/\s+/g, "-");
}

// What the main agent reads when the user tags an agent/model/effort:
// `{token}` and `{part}` are filled in by the chat.
export function instruction(agentId: string, model: string, effort?: string) {
  const args = [`agent ${JSON.stringify(agentId)}`, `model ${JSON.stringify(model)}`];
  if (effort) args.push(`effort ${JSON.stringify(effort)}`);
  return (
    `The part of my message tagged {token} is for a subagent: call the ${TOOL} tool with ${args.join(", ")}, ` +
    "and a prompt that asks for that part with everything the subagent needs, since it does not see this conversation. " +
    "Use its answer in your reply. The part: {part}"
  );
}

function mention(agent: CatalogAgent, model: ModelChoice, effort?: ModelChoice["efforts"][number]): MentionDefinition {
  const token = [agent.id, model.value, effort?.value]
    .filter((part): part is string => Boolean(part))
    .map(tokenPart)
    .join("/");
  return {
    id: token,
    token,
    label: [agent.name ?? agent.id, model.name ?? model.value, effort?.name].filter(Boolean).join(" · "),
    detail: effort
      ? `Subagent on ${model.name ?? model.value}, ${effort.name ?? effort.value} effort`
      : `Subagent on ${model.name ?? model.value}`,
    icon: "bot",
    keywords: [agent.id, agent.name, model.value, model.name, model.group, effort?.value, effort?.name]
      .filter(Boolean)
      .join(" "),
    prompt: instruction(agent.id, model.value, effort?.value),
  };
}

// The composer tags: every agent/model, and every agent/model/effort the
// model takes.
export function mentionsFor(catalog: readonly CatalogAgent[]) {
  const out: MentionDefinition[] = [];
  for (const agent of catalog) {
    for (const model of agent.models) {
      out.push(mention(agent, model));
      for (const effort of model.efforts) out.push(mention(agent, model, effort));
      if (out.length >= MENTION_LIMIT) return out.slice(0, MENTION_LIMIT);
    }
  }
  return out;
}

function modelsLine(agent: CatalogAgent) {
  if (agent.error) return `unavailable (${agent.error})`;
  const models = agent.models;
  if (!models.length) return "its default model only";
  const named = models.slice(0, DESCRIBED_MODELS).map((model) => {
    const efforts = model.efforts.map((effort) => effort.value);
    return efforts.length ? `${model.value} [${efforts.join("|")}]` : model.value;
  });
  const more = models.length > DESCRIBED_MODELS ? `, and ${models.length - DESCRIBED_MODELS} more` : "";
  return `${named.join(", ")}${more}`;
}

// The tool as the agents see it, for the current catalog and subagent
// types.
export function toolSpec(catalog: readonly CatalogAgent[], types: readonly SubagentType[]) {
  const agents = catalog;
  const lines = [
    "Starts a subagent: one of the user's own agents, on a model it offers, does a task you give it in a separate session and returns its final answer. The user watches it work under this call.",
    "Use it to delegate a self-contained piece of work, to ask another model for a second opinion, or when the user's message asks for a subagent on an agent/model/effort.",
    "The subagent does not see this conversation: put everything it needs in `prompt`. It works in the same folder, with its own tools.",
  ];
  if (agents.length) {
    lines.push("Agents (`agent`), with their models (`model`) and [efforts] (`effort`):");
    for (const agent of agents) lines.push(`- ${agent.id} (${agent.name ?? agent.id}): ${modelsLine(agent)}`);
  }
  if (types.length) {
    lines.push("Subagent types (`type`, instead of agent and model):");
    for (const type of types) lines.push(`- ${type.plugin}/${type.id} (${type.title}): ${type.description}`);
  }
  const ids = agents.filter((agent) => !agent.error).map((agent) => agent.id);
  const agent = ids.length ? z.enum(ids).describe(agentDescription).optional() : spawnInput.shape.agent;
  const input = spawnInput
    .extend({
      agent,
      ...(types.length
        ? {
            type: z
              .enum(types.map((type) => `${type.plugin}/${type.id}`))
              .describe("A subagent type to run instead of an agent and model.")
              .optional(),
          }
        : {}),
      // The original schema leaves additional properties open and has no dialect marker.
    })
    .meta({ $schema: undefined, additionalProperties: undefined });
  return {
    name: TOOL,
    title: "Spawn subagent",
    description: lines.join("\n"),
    input,
  };
}

// A title from the prompt's first line.
export function titleOf(prompt: string) {
  const line = String(prompt).trim().split("\n")[0]?.trim() ?? "";
  return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line || "Subagent";
}

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.1.0latestOct 5, 2026>=2 <38.6 KB2 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.