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

controller.ts3.4 KB
// Offer the tool immediately, then follow live catalogs after agent bursts settle.
// The activation scope owns listeners, debounce fibers and replaceable tool scopes.
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Fiber from "effect/Fiber";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import * as z from "zod";
import { Events, Plugin, Tools, promise } from "convergence/effect";
import { mentionsFor, toolSpec } from "./lib.ts";
import type { CatalogAgent, SubagentType } from "./lib.ts";
import { spawn } from "./spawn.ts";

const SETTLE_MS = 1500;
export const program = Effect.gen(function* () {
  const { api } = yield* Plugin;
  const tools = yield* Tools;
  const events = yield* Events;
  const scope = yield* Effect.scope;
  let catalog: readonly CatalogAgent[] = [];
  let types: readonly SubagentType[] = [];
  let registered: string | undefined;
  let tool: Scope.Closeable | undefined;
  let timer: Fiber.Fiber<void> | undefined;
  let running = false;
  let again = false;

  const register = Effect.fn("Subagents.register")(function* () {
    const spec = toolSpec(catalog, types);
    const key = JSON.stringify({ ...spec, input: z.toJSONSchema(spec.input) });
    if (key === registered) return;
    registered = key;
    if (tool) yield* Scope.close(tool, Exit.void);
    tool = yield* Scope.fork(scope);
    yield* tools
      .register<typeof spec.input, z.ZodUnknown>({ ...spec, run: (input, ctx) => spawn(api, input, ctx) })
      .pipe(Scope.provide(tool));
  });
  const refresh: () => Effect.Effect<void> = Effect.fn("Subagents.refresh")(function* () {
    if (running) {
      again = true;
      return;
    }
    running = true;
    // Catalog errors retain the previous catalog and warn, as before.
    yield* promise("models.list", () => api.models.list()).pipe(
      Effect.tap((result) =>
        Effect.sync(() => {
          catalog = result.agents;
          api.mentions.set(mentionsFor(catalog));
        }),
      ),
      Effect.catch((error) =>
        Effect.sync(() => console.warn(`The subagents plugin could not list the agents' models: ${error.message}`)),
      ),
    );
    yield* promise("subagents.types", () => api.subagents.types()).pipe(
      Effect.tap((result) =>
        Effect.sync(() => {
          types = result;
        }),
      ),
      Effect.catch((error) =>
        Effect.sync(() => console.warn(`The subagents plugin could not list subagent types: ${error.message}`)),
      ),
    );
    yield* register();
    running = false;
    if (again) {
      again = false;
      yield* schedule();
    }
  });
  const schedule: () => Effect.Effect<void> = Effect.fn("Subagents.schedule")(function* () {
    if (timer) yield* Fiber.interrupt(timer);
    // Clear the timer before refreshing: a new burst must not interrupt an active refresh.
    timer = yield* Effect.sleep(SETTLE_MS).pipe(
      Effect.andThen(
        Effect.sync(() => {
          timer = undefined;
        }),
      ),
      Effect.andThen(refresh()),
      Effect.forkIn(scope, { startImmediately: true }),
    );
  });
  yield* register();
  yield* Effect.forkScoped(refresh(), { startImmediately: true });
  for (const event of ["agents_changed", "settings_changed", "workspaces_changed", "plugins_changed"] as const) {
    yield* events.stream(event).pipe(
      Stream.runForEach(() => schedule()),
      Effect.forkScoped({ startImmediately: true }),
    );
  }
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.