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

spawn.ts3 KB
// Run a tool call in its displayed subagent, or in the plugin owning a type.
// Model failures remain tool results, with the displayed run ended first.
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as z from "zod";
import { parse, promise } from "convergence/effect";
import type { Api, ToolContext } from "convergence";
import { TOOL, titleOf } from "./lib.ts";
import type { SpawnInput } from "./lib.ts";

class ModelFailed extends Data.TaggedError("ModelFailed")<{ readonly cause: unknown }> {}
const errorFields = z.object({ name: z.string().optional(), message: z.string().optional() });
const failed = (text: string) => ({ content: [{ type: "text", text }], isError: true });

export const spawn = Effect.fn("Subagents.spawn")(function* (
  api: { models: Pick<Api["models"], "prompt">; subagents: Pick<Api["subagents"], "run"> },
  input: SpawnInput,
  ctx: ToolContext,
) {
  const prompt = input.prompt.trim();
  if (!prompt) return failed(`${TOOL} needs a prompt.`);
  if (input.type) {
    const type = yield* parse("subagent type", z.string(), input.type);
    const result = yield* promise("subagents.run", () => api.subagents.run({ type, input: { ...input, prompt } }, ctx));
    return result ?? "The subagent finished without a result.";
  }
  const agentId = input.agent || ctx.agentId;
  if (!agentId) return failed("Name the agent to run the subagent on.");
  const subagent = yield* promise("subagent.start", () =>
    ctx.subagent.start({
      title: input.title || titleOf(prompt),
      agent: agentId,
      model: input.model,
      effort: input.effort,
      prompt,
    }),
  );
  return yield* Effect.tryPromise({
    try: () =>
      api.models.prompt({
        agentId,
        model: input.model,
        effort: input.effort,
        prompt,
        workspaceId: ctx.workspaceId,
        subagent,
        signal: ctx.signal,
      }),
    catch: (cause) => new ModelFailed({ cause }),
  }).pipe(
    Effect.map((answer) => {
      if (answer.status === "completed") {
        subagent.end({ status: "completed", summary: answer.text });
        return answer.text || "The subagent finished without a reply.";
      }
      const why =
        answer.status === "cancelled"
          ? "The subagent was stopped."
          : `The subagent failed: ${answer.message ?? "no reason given"}`;
      subagent.end({ status: answer.status === "cancelled" ? "cancelled" : "failed", summary: why });
      return failed(why);
    }),
    Effect.catchTag("ModelFailed", ({ cause }) =>
      Effect.sync(() => {
        const fields = errorFields.safeParse(cause);
        const stopped = fields.success && fields.data.name === "AbortError";
        const message = fields.success ? (fields.data.message ?? String(cause)) : String(cause);
        subagent.end({ status: stopped ? "cancelled" : "failed", summary: stopped ? "Stopped." : message });
        return failed(stopped ? "The subagent was stopped." : `The subagent could not run: ${message}`);
      }),
    ),
  );
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.